jdBasic

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

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.

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

Want more?

Next up: Pong, Breakout, or a roguelike dungeon crawler.