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. (We can add an “advanced-gaming” / SDL section later.)
Game 1: Snake (best first game)
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)
- Food placement + scoring
Controls
Move: W A S D or arrow keys • Quit: Q
Core idea
Store the snake as two arrays: SnakeX and SnakeY. Each frame, you append a new head and remove the tail (unless you ate food).
Build it step-by-step (the “recipe”)
1) Draw the playfield
Print a border once, then only update the characters that changed.
2) Read keyboard input
Use an event callback to store the last pressed key into a global variable.
3) Move the snake
Compute a new head position from direction, append it, then erase/drop the tail.
4) Add food + score
When the head hits food, increase score and skip removing the tail (snake grows).
5) Collision
End the game if the head hits a wall or any body segment.
Snake snippet: keyboard event
' Callback for Keyboard Input
SUB HandleKeys(data)
K$ = chr$(data[0]{"scancode"})
ENDSUB
ON "KEYDOWN" CALL HandleKeysThe callback stores the last pressed key in K$. The main loop reads K$ and updates direction.
Snake snippet: grow / move logic
' Append new head
SnakeX = APPEND(SnakeX, NewHeadX)
SnakeY = APPEND(SnakeY, NewHeadY)
' If no food eaten -> erase and drop tail
TailX = SnakeX[0]
TailY = SnakeY[0]
LOCATE TailY + 1, TailX + 1 : PRINT " ";
SnakeX = DROP(1, SnakeX)
SnakeY = DROP(1, SnakeY)This is the entire “snake movement” trick in a nutshell.
Full Snake source (copy/paste into the REPL)
Next: TetrisShow snake_02.jdb
' ==========================================================
' == 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]
' Index 0 is Tail, Last Index is 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 your environment maps them
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
CLSEasy upgrades (great exercises)
- Pause on P (skip movement while paused)
- Speed up every 50 points (reduce SPEED)
- Better food spawn: ensure food isn’t placed on the snake body
- Walls wrap: exiting left enters right (and vice versa)
Game 2: Tetris (matrix edition)
What you’ll learn
- Represent the board as a 2D array (a matrix)
- Represent pieces as 4×4 matrices
- Rotation with TRANSPOSE + REVERSE
- Collision checks before moving/rotating
- Lock pieces + clear lines + scoring/levels
Controls
Left/Right: A / D or Arrow keys
Rotate: W or Up Arrow
Soft drop: S or Down Arrow
Quit: ESC
The 4 core Tetris systems
1) Board matrix
A 20×10 grid. Empty cells are 0. Filled cells store a “color id”.
2) Current piece matrix
A 4×4 matrix (tetromino) positioned by PieceX, PieceY.
3) Collision + locking
Before a move/rotate, check collision. If falling collides, lock into the board.
4) Line clearing + scoring
If a row has no empty cells, remove it and shift everything down.
Tetris snippet: rotate a 4×4 piece
FUNC RotateMatrix(mat)
RETURN REVERSE(TRANSPOSE(mat))
ENDFUNCThis is a classic trick: transpose rows/cols, then reverse to rotate clockwise.
Tetris snippet: collision check
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
IF boardC < 0 OR boardC >= COLS OR boardR >= ROWS THEN RETURN TRUE
IF boardR >= 0 THEN
IF Board[boardR, boardC] <> EMPTY THEN RETURN TRUE
ENDIF
ENDIF
NEXT c
NEXT r
RETURN FALSE
ENDFUNCAlways validate a move/rotation first, then apply it if safe.
Full Tetris source (copy/paste into the REPL)
Tip: this example uses input “actions” (flags) set by the key handler, then consumed by the main loop.
Show tetris.jdb
' ============================================================
' == 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 TRUETetris upgrades (next exercises)
- Hard drop (space): keep moving down until collision, then lock.
- Next piece preview: generate a “next piece” and show it on the side.
- Hold piece: allow swapping current piece once per drop.
- Better scoring: classic Tetris scoring table + combos.
Want more games?
Next we can add: Pong, Breakout, a roguelike dungeon crawler, and “advanced gaming” pages for SDL/sprites (non-WASM-friendly assets handled differently).