Console Games
in Modern BASIC
Build classic games with simple text rendering and real-time keyboard input. Start with Snake, then level up to Tetris using 2D arrays (matrices).
How to use these examples
- Open the jdBasic Web REPL.
- Copy/paste the full game source from below into the editor.
- Run it. Use the controls shown on the screen.
These are console games (no SDL assets), so they work great in WASM/browser environments.
Game 1: Snake
What you’ll learn
- A classic game loop with timing (SLEEP)
- Real-time keyboard input using events (ON "KEYDOWN")
- Snake body stored as arrays of coordinates
- Collision detection (walls + self)
Controls
Move: W A S D or Arrows • Quit: Q
The "Recipe"
1) Draw playfield
Print border once, update only changes.
2) Move snake
Compute new head, append it. Erase tail unless food eaten.
3) Arrays as Body
Store SnakeX and SnakeY lists.
Snippet: Keyboard Event
' Callback for Keyboard Input
SUB HandleKeys(data)
K$ = CHR$(data[0]{"scancode"})
ENDSUB
ON "KEYDOWN" CALL HandleKeys
Snippet: Movement Logic
' Append new head
SnakeX = APPEND(SnakeX, NewHeadX)
SnakeY = APPEND(SnakeY, NewHeadY)
' If no food eaten -> drop tail
TailX = SnakeX[0]
TailY = SnakeY[0]
LOCATE TailY + 1, TailX + 1 : PRINT " ";
SnakeX = DROP(1, SnakeX)
SnakeY = DROP(1, SnakeY)
Full Snake Source Code
Copy and paste this entire block into the Web REPL.
' ==========================================================
' == SNAKE.JDB - Classic Snake Clone
' ==========================================================
' --- Configuration ---
WIDTH = 40
HEIGHT = 20
SPEED = 100 ' Delay in ms
' --- Globals ---
DIM SnakeX, SnakeY ' Arrays to hold body coordinates
FoodX = 0
FoodY = 0
DirX = 1
DirY = 0
Score = 0
GameOver = FALSE
Paused = FALSE
DIM K$ AS STRING
K$ = ""
' Callback for Keyboard Input
SUB HandleKeys(data)
K$ = CHR$(data[0]{"scancode"})
ENDSUB
ON "KEYDOWN" CALL HandleKeys
' --- Initialization ---
SUB InitGame()
' Reverse order so [Tail, Body, Head]
SnakeX = [INT(WIDTH/2)-2, INT(WIDTH/2)-1, INT(WIDTH/2)]
SnakeY = [INT(HEIGHT/2), INT(HEIGHT/2), INT(HEIGHT/2)]
DirX = 1
DirY = 0
Score = 0
GameOver = FALSE
CURSOR FALSE
DrawBorder()
PlaceFood()
ENDSUB
SUB DrawBorder()
CLS
COLOR 7, 0
PRINT "+" + ("-" * WIDTH) + "+"
FOR y = 1 TO HEIGHT
PRINT "|" + (" " * WIDTH) + "|"
NEXT y
PRINT "+" + ("-" * WIDTH) + "+"
LOCATE HEIGHT + 3, 1
PRINT "WASD / Arrows to Move. Q to Quit."
ENDSUB
SUB PlaceFood()
' Simple random placement
FoodX = INT(RND(1) * WIDTH) + 1
FoodY = INT(RND(1) * HEIGHT) + 1
LOCATE FoodY + 1, FoodX + 1
COLOR 12, 0 ' Red Food
PRINT "@";
ENDSUB
' --- Main Loop ---
InitGame()
DO
' 1. Input
IF K$ <> "" THEN
' Accept WASD and arrow equivalents
IF UCASE$(K$) = "Q" THEN GameOver = TRUE
IF UCASE$(K$) = "W" THEN DirX = 0 : DirY = -1
IF UCASE$(K$) = "S" THEN DirX = 0 : DirY = 1
IF UCASE$(K$) = "A" THEN DirX = -1 : DirY = 0
IF UCASE$(K$) = "D" THEN DirX = 1 : DirY = 0
K$ = ""
ENDIF
' 2. Logic
IF NOT GameOver THEN
Dims = LEN(SnakeX)
Count = Dims[0]
HeadIdx = Count - 1
CurrHeadX = SnakeX[HeadIdx]
CurrHeadY = SnakeY[HeadIdx]
NewHeadX = CurrHeadX + DirX
NewHeadY = CurrHeadY + DirY
' Collision: Walls
IF NewHeadX < 1 OR NewHeadX > WIDTH OR NewHeadY < 1 OR NewHeadY > HEIGHT THEN
GameOver = TRUE
ENDIF
' Collision: Self
FOR i = 0 TO HeadIdx - 1
IF SnakeX[i] = NewHeadX AND SnakeY[i] = NewHeadY THEN GameOver = TRUE
NEXT i
IF NOT GameOver THEN
' Append new head
SnakeX = APPEND(SnakeX, NewHeadX)
SnakeY = APPEND(SnakeY, NewHeadY)
' Draw head
LOCATE NewHeadY + 1, NewHeadX + 1
COLOR 10, 0
PRINT "O";
' Check Food
IF NewHeadX = FoodX AND NewHeadY = FoodY THEN
Score = Score + 10
PlaceFood()
' don't drop tail -> grows
ELSE
' Erase tail
TailX = SnakeX[0]
TailY = SnakeY[0]
LOCATE TailY + 1, TailX + 1
PRINT " ";
' Drop tail
SnakeX = DROP(1, SnakeX)
SnakeY = DROP(1, SnakeY)
ENDIF
ENDIF
ENDIF
' 3. UI Update
LOCATE HEIGHT + 2, 2
COLOR 14, 0
PRINT "Score: " + Score;
IF GameOver THEN
LOCATE HEIGHT / 2, (WIDTH / 2) - 4
COLOR 15, 4
PRINT " GAME OVER "
LOCATE((HEIGHT / 2) + 1, (WIDTH / 2) - 8)
COLOR 7, 0
PRINT "Press Q to Quit"
ENDIF
SLEEP SPEED
LOOP
CURSOR TRUE
COLOR 2,0
CLS
Game 2: Tetris
What you’ll learn
- Represent the board as a 2D array (a matrix)
- Rotation with TRANSPOSE + REVERSE
- Collision checks before moving/rotating
- Lock pieces + clear lines + scoring/levels
Controls
Left/Right: A / D
Rotate: W or Up Arrow
Drop: S or Down Arrow
Core Logic
1) Board Matrix
20x10 Grid. 0=Empty, >0=Color ID.
2) Rotation Trick
Transpose matrix, then reverse rows.
3) Gravity
Move piece down every X ticks. If hit -> Lock.
Snippet: Matrix Rotation
FUNC RotateMatrix(mat)
RETURN REVERSE(TRANSPOSE(mat))
ENDFUNC
Snippet: Collision
FUNC CheckCollision(pMatrix, px, py)
' Loop 4x4 matrix and check against board walls/blocks
' ... (see full source)
ENDFUNC
Full Tetris Source Code
A full matrix-based Tetris implementation.
' ============================================================
' == T E T R I S (jdBasic Matrix Edition)
' ============================================================
' --- CONSTANTS ---
ROWS = 20
COLS = 10
EMPTY = 0
WALL = 8
' --- GAME STATE ---
DIM Board[ROWS, COLS] ' The main grid
DIM CurrentPiece ' The 4x4 matrix of the active piece
DIM PieceX, PieceY ' Position of top-left of 4x4 box
DIM PieceColor ' Color of current piece
DIM GameOver = FALSE
DIM Score = 0
DIM Level = 1
DIM LinesCleared = 0
' --- TIMING ---
TickCounter = 0
Speed = 20 ' Lower is faster (frames per drop)
LastInputTime = 0
' --- EVENT HANDLING ---
' We map keys to actions using global flags that the main loop consumes
ActionRotate = 0
ActionMove = 0
ActionDrop = 0
SUB OnKeyDown(data)
code = data[0]{"scancode"}
' Arrow Keys / WASD
IF code = 276 OR code = 97 THEN ActionMove = -1 ' Left / a
IF code = 275 OR code = 100 THEN ActionMove = 1 ' Right / d
IF code = 273 OR code = 119 THEN ActionRotate = 1 ' w (Rotate)
IF code = 274 OR code = 115 THEN ActionDrop = 1 ' s (Soft Drop)
IF code = 27 THEN GameOver = TRUE ' ESC
ENDSUB
ON "KEYDOWN" CALL OnKeyDown
' ============================================================
' == LOGIC SUBROUTINES
' ============================================================
SUB SpawnPiece()
t = INT(RND(1) * 7)
PieceX = 3
PieceY = 0
' Define Shapes as 4x4 Matrices
SWITCH t
CASE 0: ' I
CurrentPiece = [[0,0,0,0], [1,1,1,1], [0,0,0,0], [0,0,0,0]]
PieceColor = 11 ' Cyan
CASE 1: ' J
CurrentPiece = [[1,0,0,0], [1,1,1,0], [0,0,0,0], [0,0,0,0]]
PieceColor = 9 ' Blue
CASE 2: ' L
CurrentPiece = [[0,0,1,0], [1,1,1,0], [0,0,0,0], [0,0,0,0]]
PieceColor = 6 ' Orange/Brown
CASE 3: ' O
CurrentPiece = [[0,1,1,0], [0,1,1,0], [0,0,0,0], [0,0,0,0]]
PieceColor = 14 ' Yellow
CASE 4: ' S
CurrentPiece = [[0,1,1,0], [1,1,0,0], [0,0,0,0], [0,0,0,0]]
PieceColor = 10 ' Green
CASE 5: ' T
CurrentPiece = [[0,1,0,0], [1,1,1,0], [0,0,0,0], [0,0,0,0]]
PieceColor = 13 ' Magenta
CASE 6: ' Z
CurrentPiece = [[1,1,0,0], [0,1,1,0], [0,0,0,0], [0,0,0,0]]
PieceColor = 12 ' Red
ENDSWITCH
ENDSUB
FUNC CheckCollision(pMatrix, px, py)
FOR r = 0 TO 3
FOR c = 0 TO 3
IF pMatrix[r, c] <> 0 THEN
boardR = py + r
boardC = px + c
' Check Boundaries
IF boardC < 0 OR boardC >= COLS OR boardR >= ROWS THEN RETURN TRUE
' Check Board (only if row is non-negative)
IF boardR >= 0 THEN
IF Board[boardR, boardC] <> EMPTY THEN RETURN TRUE
ENDIF
ENDIF
NEXT c
NEXT r
RETURN FALSE
ENDFUNC
SUB LockPiece()
FOR r = 0 TO 3
FOR c = 0 TO 3
IF CurrentPiece[r, c] <> 0 THEN
realR = PieceY + r
realC = PieceX + c
IF realR >= 0 AND realR < ROWS AND realC >= 0 AND realC < COLS THEN
Board[realR, realC] = PieceColor
ENDIF
ENDIF
NEXT c
NEXT r
ENDSUB
SUB CheckLines()
LinesInTurn = 0
FOR r = 0 TO ROWS - 1
RowFilled = TRUE
FOR c = 0 TO COLS - 1
IF Board[r, c] = EMPTY THEN
RowFilled = FALSE
EXITFOR
ENDIF
NEXT c
IF RowFilled THEN
LinesInTurn = LinesInTurn + 1
' Move everything down
FOR downR = r TO 1 STEP -1
FOR k = 0 TO COLS - 1
Board[downR, k] = Board[downR - 1, k]
NEXT k
NEXT downR
' Clear top row
FOR k = 0 TO COLS - 1
Board[0, k] = EMPTY
NEXT k
ENDIF
NEXT r
IF LinesInTurn > 0 THEN
LinesCleared = LinesCleared + LinesInTurn
Score = Score + (LinesInTurn * 100 * LinesInTurn)
Level = 1 + INT(LinesCleared / 10)
Speed = sMAX([1, 20 - Level])
ENDIF
ENDSUB
FUNC RotateMatrix(mat)
RETURN REVERSE(TRANSPOSE(mat))
ENDFUNC
' ============================================================
' == RENDERING
' ============================================================
SUB DrawBorder()
COLOR 7, 0
FOR y = 1 TO ROWS
LOCATE y + 1, 10 : PRINT "│"
LOCATE y + 1, 10 + (COLS * 2) + 1 : PRINT "│"
NEXT y
LOCATE ROWS + 2, 10 : PRINT "└" + COLS * 2 * "─" + "┘"
LOCATE 2, 35 : PRINT "SCORE: "
LOCATE 4, 35 : PRINT "LEVEL: "
LOCATE 6, 35 : PRINT "LINES: "
ENDSUB
SUB DrawBoard()
' Draw Static Board
FOR r = 0 TO ROWS - 1
LOCATE r + 2, 11
FOR c = 0 TO COLS - 1
val = Board[r, c]
IF val = EMPTY THEN
COLOR 0, 0 : PRINT " .";
ELSE
COLOR val, 0 : PRINT "[]";
ENDIF
NEXT c
NEXT r
' Draw Active Piece (Overlay)
IF GameOver = FALSE THEN
FOR r = 0 TO 3
FOR c = 0 TO 3
IF CurrentPiece[r, c] <> 0 THEN
drawY = PieceY + r
drawX = PieceX + c
IF drawY >= 0 AND drawY < ROWS AND drawX >= 0 AND drawX < COLS THEN
LOCATE drawY + 2, 11 + (drawX * 2)
COLOR PieceColor, 0 : PRINT "[]";
ENDIF
ENDIF
NEXT c
NEXT r
ENDIF
ENDSUB
' ============================================================
' == MAIN
' ============================================================
CLS
CURSOR FALSE
' Clear board
FOR r = 0 TO ROWS - 1
FOR c = 0 TO COLS - 1
Board[r, c] = EMPTY
NEXT c
NEXT r
DrawBorder()
SpawnPiece()
DO
' Render
DrawBoard()
LOCATE 2, 43 : COLOR 15,0 : PRINT Score
LOCATE 4, 43 : COLOR 15,0 : PRINT Level
LOCATE 6, 43 : COLOR 15,0 : PRINT LinesCleared
' Handle actions
IF ActionMove <> 0 THEN
IF NOT CheckCollision(CurrentPiece, PieceX + ActionMove, PieceY) THEN
PieceX = PieceX + ActionMove
ENDIF
ActionMove = 0
ENDIF
IF ActionRotate = 1 THEN
rotated = RotateMatrix(CurrentPiece)
IF NOT CheckCollision(rotated, PieceX, PieceY) THEN
CurrentPiece = rotated
ENDIF
ActionRotate = 0
ENDIF
' Gravity (tick)
TickCounter = TickCounter + 1
DropNow = (TickCounter MOD Speed) = 0 OR ActionDrop = 1
IF DropNow THEN
IF NOT CheckCollision(CurrentPiece, PieceX, PieceY + 1) THEN
PieceY = PieceY + 1
ELSE
' Lock + clear + new piece
LockPiece()
CheckLines()
SpawnPiece()
' If new piece collides immediately -> game over
IF CheckCollision(CurrentPiece, PieceX, PieceY) THEN GameOver = TRUE
ENDIF
ActionDrop = 0
ENDIF
IF GameOver THEN
LOCATE 12, 35 : COLOR 15,4 : PRINT " GAME OVER "
LOCATE 14, 33 : COLOR 7,0 : PRINT "Press ESC to exit"
ENDIF
SLEEP 16
LOOP UNTIL GameOver
CURSOR TRUE