jdBasic Try Live

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).

Text mode Keyboard input Arrays Game loop Collision Score

How to use these examples

  1. Open the jdBasic Web REPL.
  2. Copy/paste the full game source from below into the editor.
  3. 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 HandleKeys

The 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: Tetris
Show 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
CLS

Easy 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))
ENDFUNC

This 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
ENDFUNC

Always 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 TRUE

Tetris 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).