Skip to main content

How to Create a Snake Game in Batch Script

Building a classic arcade game like Snake inside the Windows Command Prompt pushes Batch scripting well beyond its intended purpose as an administrative tool. Unlike a text menu or slot machine that waits indefinitely for user input, Snake requires real-time execution: the snake must keep moving even when no key is pressed, and the board must redraw itself fast enough to feel responsive.

Because Batch has no native non-blocking keyboard input and no frame buffer, achieving this requires careful use of the choice command's timeout feature alongside a simulated coordinate grid. This guide explains the core concepts and builds a working moving-head engine, then discusses honestly what would be required to extend it further.

The Core Strategy

Before writing any code, it helps to understand the four problems the script must solve.

ProblemSolution
Representing a 2D board in a text environmentAn X/Y coordinate system tracked with integer variables
Moving without waiting for inputchoice /t times out after one second and supplies a default
Drawing each frameNested for /l loops build each row as a string, then print it
Detecting collisionsInteger comparisons after every move

Non-Blocking Input With choice

The choice command is normally used to present a menu and wait indefinitely. Two flags change that behavior:

  • /t N sets a timeout of N seconds
  • /d X specifies which character to treat as the input if the timer expires

Combined, choice /c WASD /t 1 /d D /n waits up to one second for the player to press W, A, S, or D. If no key arrives within that second, it behaves exactly as if the player pressed D. This gives the game loop a fixed tick rate of approximately one move per second.

Reading the Result With errorlevel

choice communicates its result through the errorlevel variable. The value corresponds to the position of the chosen character within the /c string, counting from one.

For /c WASD:

Key pressederrorlevel
W1
A2
S3
D4

Critical: if errorlevel N in Batch means errorlevel is greater than or equal to N, not exactly equal to N. This means the checks must be written from highest to lowest so that each condition is caught before a broader one matches it.

:: Correct - checked from highest to lowest
if errorlevel 4 set "DIR=D"
if errorlevel 3 set "DIR=S"
if errorlevel 2 set "DIR=A"
if errorlevel 1 set "DIR=W"

If you write them lowest to highest, if errorlevel 1 matches every possible result because every errorlevel from 1 to 4 is greater than or equal to 1, and the direction will always be set to W regardless of which key was pressed.

The Coordinate System

The board is a rectangle defined by WIDTH and HEIGHT. Every position on the board is identified by an X value (column, counting left to right from 1) and a Y value (row, counting top to bottom from 1).

The snake's head is tracked with two variables, headX and headY. Each game tick, exactly one of these changes by one unit depending on the current direction.

Direction W (up) : headY -= 1
Direction S (down) : headY += 1
Direction A (left) : headX -= 1
Direction D (right) : headX += 1

After moving, the script checks whether the new position falls outside the board boundaries. If it does, the game ends.

Drawing the Board

Each frame is drawn by two nested for /l loops. The outer loop iterates over rows from 1 to HEIGHT. For each row, the inner loop iterates over columns from 1 to WIDTH. At each coordinate the script decides what character to place:

  • O if the position matches the apple
  • @ if the position matches the snake's head
  • . for empty space

Each character is appended to a string variable called row. When the inner loop finishes, echo !row! prints the entire row at once. Printing whole rows rather than individual characters is important because echo in Batch always appends a newline, so each call must output exactly one complete row.

The SpawnApple Subroutine

When the game starts, and again each time the snake eats an apple, a new apple position is chosen with %RANDOM%. The subroutine checks that the new position does not overlap the snake's head. If it does, the subroutine calls itself recursively until a clear position is found.

Because the board is small and the snake starts as a single-cell head, collisions during spawning are rare. In a version with a long tail the subroutine would need to check every tail segment, but for this engine checking only the head is sufficient.

A Note on Direction Reversal

A real Snake game prevents the player from reversing directly into themselves. For example, if the snake is moving right (D), pressing A should be ignored rather than immediately killing the player. In this engine, which has no tail to collide with, reversing direction simply sends the head back the way it came and is not immediately fatal. If you add tail tracking, you will need to add a guard such as:

:: Prevent reversing onto the tail when moving horizontally
if "!DIR!"=="A" if "!direction!"=="D" set "DIR=D"
if "!DIR!"=="D" if "!direction!"=="A" set "DIR=A"
:: Prevent reversing onto the tail when moving vertically
if "!DIR!"=="W" if "!direction!"=="S" set "DIR=S"
if "!DIR!"=="S" if "!direction!"=="W" set "DIR=W"

Full Working Script

@echo off
setlocal enabledelayedexpansion
title Batch Snake Engine
mode con cols=44 lines=28
color 0A

:: -----------------------------------------------
:: Game constants
:: -----------------------------------------------
set "WIDTH=20"
set "HEIGHT=10"

:: -----------------------------------------------
:: NewGame - initializes all state and starts loop
:: -----------------------------------------------
:NewGame
set "headX=10"
set "headY=5"
set "direction=D"
set "score=0"

call :SpawnApple
goto :GameLoop

:: -----------------------------------------------
:: GameLoop - one iteration equals one game tick
:: -----------------------------------------------
:GameLoop

:: Wait up to 1 second for directional input.
:: If nothing is pressed, default to the current direction.
choice /c WASD /n /t 1 /d !direction! >nul 2>&1

:: Read errorlevel from highest to lowest.
:: This must happen before any other command that could reset errorlevel.
if errorlevel 4 set "direction=D"
if errorlevel 3 set "direction=S"
if errorlevel 2 set "direction=A"
if errorlevel 1 set "direction=W"

:: Move the head one cell in the current direction
if "!direction!"=="W" set /a "headY-=1"
if "!direction!"=="S" set /a "headY+=1"
if "!direction!"=="A" set /a "headX-=1"
if "!direction!"=="D" set /a "headX+=1"

:: Check for wall collision
if !headX! lss 1 goto :GameOver
if !headX! gtr !WIDTH! goto :GameOver
if !headY! lss 1 goto :GameOver
if !headY! gtr !HEIGHT! goto :GameOver

:: Check whether the snake ate the apple
if !headX! equ !appleX! (
if !headY! equ !appleY! (
set /a "score+=1"
call :SpawnApple
)
)

:: -----------------------------------------------
:: Render the current frame
:: -----------------------------------------------
cls

:: Top border and score
echo +--------------------+
echo ^| BATCH SNAKE ^|
echo ^| Score: !score! ^|
echo +--------------------+

:: Draw each row of the board
for /l %%Y in (1,1,%HEIGHT%) do (
set "row= ^|"

for /l %%X in (1,1,%WIDTH%) do (
set "cell=."

if %%X equ !appleX! (
if %%Y equ !appleY! (
set "cell=O"
)
)

if %%X equ !headX! (
if %%Y equ !headY! (
set "cell=@"
)
)

set "row=!row!!cell!"
)

echo !row!^|
)

:: Bottom border and controls reminder
echo +--------------------+
echo W=Up A=Left S=Down D=Right
echo.

goto :GameLoop

:: -----------------------------------------------
:: SpawnApple - places apple at a random position
:: that does not overlap the snake head
:: -----------------------------------------------
:SpawnApple
set /a "appleX=(!RANDOM! %% WIDTH) + 1"
set /a "appleY=(!RANDOM! %% HEIGHT) + 1"

if !appleX! equ !headX! (
if !appleY! equ !headY! (
goto :SpawnApple
)
)
exit /b

:: -----------------------------------------------
:: GameOver - display result and offer replay
:: -----------------------------------------------
:GameOver
cls
echo +--------------------+
echo ^| GAME OVER^^! ^|
echo +--------------------+
echo ^| You hit the wall. ^|
echo +--------------------+
echo.
echo Final score: !score!
echo.

set "retry="
set /p "retry= Play again? (Y/N): "
if /i "!retry!"=="Y" goto :NewGame

echo.
echo Thanks for playing^^!
pause
exit /b

How Each Part Works Together

Initialization sets the head at the center of the board, direction to the right, score to zero, and places the first apple. These values persist across the loop because setlocal keeps them in the current environment.

The game tick begins with choice. If a key arrives within one second, errorlevel reflects that key. If the second elapses without input, errorlevel reflects the default direction. Either way the snake moves one cell and the board redraws.

The render loop uses for /l variables (%%X and %%Y) rather than environment variables for the loop counters. This is intentional: for /l counters are expanded at parse time with %% notation, which does not require delayed expansion and is slightly faster inside a tight loop. The cell content is still stored in !cell! with delayed expansion because it is set and read within the same block.

The apple check uses two separate if statements rather than if !headX! equ !appleX! if !headY! equ !appleY!. Both forms work, but separating them onto two lines with proper parentheses makes the nesting explicit and easier to extend when adding tail collision checks later.

Why a Full Tail Is Difficult

The moving-head engine above works correctly. Adding a growing tail requires solving three additional problems that are genuinely hard in Batch.

Storing the tail as an array

Batch has no array type. A tail of length N must be stored as N pairs of indexed variables:

tail_X_0, tail_Y_0 <- tip of tail
tail_X_1, tail_Y_1
...
tail_X_N, tail_Y_N <- position just behind head

Shifting the array every tick

Each frame, every segment must copy the coordinates of the segment ahead of it, from the tip toward the head. In a language with arrays this is a single loop. In Batch it requires a for /l loop counting down from the tail tip to position 1, reading tail_X_!i! and writing it to tail_X_!j! using delayed expansion inside the loop body. The head's previous position then becomes tail_X_!length!.

Checking self-collision

After moving the head, the script must compare headX and headY against every stored tail segment. This is another for /l loop through all segment indices.

None of these are impossible, but each adds dozens of lines and slows the render loop noticeably as the tail grows.

Reducing Screen Flicker

The script above uses cls to clear the screen before each frame. On most systems this causes a visible white flash. A cleaner alternative is to use an ANSI escape sequence to move the cursor back to the top-left corner without erasing the screen, then overwrite each line in place.

:: Move cursor to row 1, column 1 without clearing the screen
<nul set /p "=←[1;1H"

The character is the ESC character (ASCII 27). In Notepad you can insert it by holding Alt and typing 027 on the numeric keypad. ANSI escape sequences require Windows 10 version 1511 or later with virtual terminal processing enabled, which is the default in modern Command Prompt and Windows Terminal sessions.

Honest Limitations

LimitationCause
One-second minimum tick ratechoice /t cannot be set below one second
Screen flicker with clsNo native frame buffer in Batch
Slow render on large boardsEach cell requires a variable read inside a nested loop
No soundBatch has no audio output beyond echo ^G (system beep)

For a smoother experience, the same coordinate-based logic developed here translates directly into PowerShell, which supports sub-second sleeps, Console.SetCursorPosition for flicker-free drawing, and [Console]::KeyAvailable for true non-blocking input.

Summary

ConceptImplementation
Non-blocking inputchoice /c WASD /t 1 /d !direction! >nul
Reading choice resultif errorlevel N checked from highest to lowest
2D boardheadX and headY integer variables
Movementset /a arithmetic on headX or headY
RenderingNested for /l loops building a row string per line
Apple placement%RANDOM% with modulo, retried if overlapping head
Collision detectionInteger boundary comparisons after each move

This engine demonstrates that Batch Script can model real-time state with the right techniques. The choice timeout trick, coordinate arithmetic, and string-building render loop are patterns worth understanding even if you ultimately move to a more capable language for the finished product.