How to Create a Simple Minesweeper Game in Batch Script
Minesweeper is one of the most iconic computer games ever made. While building a fully featured graphical version requires a rich UI framework, we can recreate its core logic entirely in the Windows Command Prompt using Batch Script.
This guide explains the process of designing a text-based Minesweeper, covering grid generation, mine placement, adjacent mine counting, and user interaction.
How Minesweeper Works
The rules are simple:
- A grid of cells is presented to the player, all initially hidden.
- Some cells contain mines.
- The player selects a cell to reveal. If it is a mine, the game ends. If it is not, the cell displays the number of adjacent mines (0 through 8).
- The player wins by revealing all non-mine cells.
Representing a 2D Grid in Batch
Batch Script does not support native arrays or multi-dimensional data structures. We simulate a 2D grid by encoding the row and column into the variable name.
For a cell at row r and column c, we use: cell_r_c.
:: Example: a 3x3 grid
set "cell_1_1=0" & set "cell_1_2=0" & set "cell_1_3=0"
set "cell_2_1=0" & set "cell_2_2=0" & set "cell_2_3=0"
set "cell_3_1=0" & set "cell_3_2=0" & set "cell_3_3=0"
We also need a second layer to track which cells are revealed: vis_r_c (0 = hidden, 1 = revealed).
Using a consistent naming convention like cell_ROW_COL and vis_ROW_COL makes debugging much easier, because you can quickly SET cell_ to list all grid values.
Placing Mines Randomly
We need to scatter a fixed number of mines across the grid without placing two in the same cell.
The Wrong Way: Sequential Placement
:: WRONG - mines may overlap
for /L %%i in (1,1,%mine_count%) do (
set /a "mr=(!RANDOM! %% %rows%) + 1"
set /a "mc=(!RANDOM! %% %cols%) + 1"
set "cell_!mr!_!mc!=M"
)
Output Concern: If the random generator picks the same row/column pair twice, you end up with fewer mines than intended, making the game unbalanced.
The Correct Way: Collision Check
We add a check before placing each mine. If the selected cell already has a mine, we try again. Because we need to resolve the random coordinates into a variable name lookup, we use for variable transfer to avoid nested delayed expansion issues.
set "placed=0"
:place_mine
if !placed! geq %mine_count% goto done_placing
set /a "mr=(!RANDOM! %% %rows%) + 1"
set /a "mc=(!RANDOM! %% %cols%) + 1"
for /f "tokens=1,2" %%a in ("!mr! !mc!") do (
if "!cell_%%a_%%b!"=="M" goto place_mine
set "cell_%%a_%%b=M"
)
set /a "placed+=1"
goto place_mine
:done_placing
Counting Adjacent Mines
After all mines are placed, we iterate through every non-mine cell and count how many of its 8 neighbors contain a mine. This is the numeric value displayed when the cell is revealed.
Because Batch cannot nest delayed expansion directly (writing !cell_!nr!_!nc!! causes the ! delimiters to pair incorrectly), we build the variable name into an intermediate string and resolve it through a for /f loop.
for /L %%r in (1,1,%rows%) do (
for /L %%c in (1,1,%cols%) do (
if "!cell_%%r_%%c!" neq "M" (
set "count=0"
REM Check all 8 directions using single-letter FOR variables
for %%d in (-1 0 1) do (
for %%e in (-1 0 1) do (
if not "%%d%%e"=="00" (
set /a "nr=%%r + %%d"
set /a "nc=%%c + %%e"
if !nr! geq 1 if !nr! leq %rows% if !nc! geq 1 if !nc! leq %cols% (
set "lookup=cell_!nr!_!nc!"
for /f %%v in ("!lookup!") do (
if "!%%v!"=="M" set /a "count+=1"
)
)
)
)
)
set "cell_%%r_%%c=!count!"
)
)
)
FOR loop variables must be single characters. Writing %%dr does not create a variable named dr, Batch interprets it as %%d followed by the literal letter r, causing a syntax error. Always use single letters like %%d and %%e for your loop variables.
Additionally, nested FOR loops with delayed expansion can be tricky. Variable references like !cell_!nr!_!nc!! do not work because Batch pairs ! delimiters left-to-right, producing !cell_! as one expansion and !_! as another. The solution is to build the variable name into an intermediate string (set "lookup=cell_!nr!_!nc!"), then resolve it through a for /f loop (for /f %%v in ("!lookup!") do if "!%%v!"=="M").
Rendering the Board
The display function draws the grid. Hidden cells show a dot (.), revealed cells show their number (or M for a mine if the game is over).
:draw_board
cls
echo.
echo Minesweeper [%rows%x%cols%] - Mines: %mine_count%
echo.
:: Column headers
set "header= "
for /L %%c in (1,1,%cols%) do set "header=!header! %%c"
echo !header!
:: Rows
for /L %%r in (1,1,%rows%) do (
set "row_line= %%r |"
for /L %%c in (1,1,%cols%) do (
if "!vis_%%r_%%c!"=="1" (
set "row_line=!row_line! !cell_%%r_%%c!"
) else (
set "row_line=!row_line! ."
)
)
echo !row_line!
)
echo.
goto :eof
The Complete Minesweeper Script
Below is a streamlined version for a 5x5 grid with 5 mines. You can adjust the rows, cols, and mine_count variables to change the difficulty.
@echo off
title Batch Minesweeper
setlocal enabledelayedexpansion
:: --- Game Configuration ---
set "rows=5"
set "cols=5"
set "mine_count=5"
set "revealed=0"
set /a "safe_cells=(%rows% * %cols%) - %mine_count%"
:: --- Initialize Grid ---
for /L %%r in (1,1,%rows%) do (
for /L %%c in (1,1,%cols%) do (
set "cell_%%r_%%c=0"
set "vis_%%r_%%c=0"
)
)
:: --- Place Mines ---
set "placed=0"
:place_mine
if !placed! geq %mine_count% goto count_neighbors
set /a "mr=(!RANDOM! %% %rows%) + 1"
set /a "mc=(!RANDOM! %% %cols%) + 1"
for /f "tokens=1,2" %%a in ("!mr! !mc!") do (
if "!cell_%%a_%%b!"=="M" goto place_mine
set "cell_%%a_%%b=M"
)
set /a "placed+=1"
goto place_mine
:: --- Count Neighbors ---
:count_neighbors
for /L %%r in (1,1,%rows%) do (
for /L %%c in (1,1,%cols%) do (
if "!cell_%%r_%%c!" neq "M" (
set "adj=0"
for %%d in (-1 0 1) do (
for %%e in (-1 0 1) do (
if not "%%d%%e"=="00" (
set /a "nr=%%r + %%d"
set /a "nc=%%c + %%e"
if !nr! geq 1 if !nr! leq %rows% if !nc! geq 1 if !nc! leq %cols% (
set "lookup=cell_!nr!_!nc!"
for /f %%v in ("!lookup!") do (
if "!%%v!"=="M" set /a "adj+=1"
)
)
)
)
)
set "cell_%%r_%%c=!adj!"
)
)
)
:: --- Game Loop ---
:game_loop
call :draw_board
echo Revealed: !revealed! / %safe_cells%
echo.
set "input="
set /p "input=Enter row,col (e.g., 2,3): "
if not defined input goto game_loop
:: Parse input
set "sel_r="
set "sel_c="
for /f "tokens=1,2 delims=," %%a in ("!input!") do (
set "sel_r=%%a"
set "sel_c=%%b"
)
:: Validate that both coordinates were provided
if not defined sel_r goto game_loop
if not defined sel_c goto game_loop
:: Bounds check (also rejects non-numeric input since comparison is arithmetic)
set /a "check_r=sel_r" 2>nul
set /a "check_c=sel_c" 2>nul
if !sel_r! lss 1 goto game_loop
if !sel_r! gtr %rows% goto game_loop
if !sel_c! lss 1 goto game_loop
if !sel_c! gtr %cols% goto game_loop
:: Already revealed check
for /f "tokens=1,2" %%a in ("!sel_r! !sel_c!") do (
if "!vis_%%a_%%b!"=="1" (
echo Already revealed. Try another cell.
pause
goto game_loop
)
REM Reveal the cell
set "vis_%%a_%%b=1"
REM Mine check
if "!cell_%%a_%%b!"=="M" goto game_over
)
set /a "revealed+=1"
:: Win check
if !revealed! geq %safe_cells% goto game_win
goto game_loop
:: --- Draw Board Subroutine ---
:draw_board
cls
echo ===========================
echo MINESWEEPER
echo ===========================
echo.
set "header= "
for /L %%c in (1,1,%cols%) do set "header=!header! %%c"
echo !header!
for /L %%r in (1,1,%rows%) do (
set "row_line= %%r |"
for /L %%c in (1,1,%cols%) do (
if "!vis_%%r_%%c!"=="1" (
set "row_line=!row_line! !cell_%%r_%%c!"
) else (
set "row_line=!row_line! ."
)
)
echo !row_line!
)
echo.
goto :eof
:: --- Game Over ---
:game_over
for /L %%r in (1,1,%rows%) do (
for /L %%c in (1,1,%cols%) do set "vis_%%r_%%c=1"
)
call :draw_board
color 0C
echo BOOM -- You hit a mine. Game Over.
pause
exit /b
:: --- Win Screen ---
:game_win
for /L %%r in (1,1,%rows%) do (
for /L %%c in (1,1,%cols%) do set "vis_%%r_%%c=1"
)
call :draw_board
color 0A
echo CONGRATULATIONS -- You cleared the field.
pause
exit /b
Key Techniques Explained
- Variable-name encoding:
cell_%%r_%%cturns a 2D coordinate into a flat variable name. This is the standard way to emulate arrays in Batch. - Single-character FOR variables: Batch FOR loop variables must be exactly one letter.
%%dis valid;%%dris not. The parser reads%%dras%%dplus the literalr, causing syntax errors. for /fvariable transfer: When you need to use a delayed-expansion value (!nr!) inside another delayed-expansion lookup (!cell_...!), transfer it into aforvariable first. This avoids the!cell_!nr!_!nc!!nesting problem where!delimiters pair incorrectly.- String-built row output: Each row is assembled into a single variable (
row_line) character by character, then printed with oneechostatement. This avoids issues with<nul set /pstripping leading spaces or behaving inconsistently across Windows versions. - Subroutine call:
call :draw_boarduses Batch's subroutine functionality. The:draw_boardlabel acts like a function, andgoto :eofreturns control to the caller. - Input parsing with
FOR /F: Thedelims=,option splits the user's "row,col" input into two separate tokens.
Best Practices
- Always validate coordinates before accessing grid variables. Out-of-bounds access won't crash Batch, but it will silently use empty variables, leading to logic errors.
- Use subroutines (
call :label) to keep your code organized. Drawing the board, placing mines, and counting neighbors are all great candidates for subroutines. - Test with small grids (3x3 or 4x4) before scaling up. Debugging a 10x10 grid with 20 mines is much harder.
- Use single-letter FOR variables to avoid parser confusion. If you need more than 26 loops, restructure your code or reuse variable names in non-overlapping scopes.
Conclusion
Building Minesweeper in Batch Script is a challenging but highly rewarding project. It demonstrates that even a language without native array support can handle complex, grid-based logic when you apply creative variable naming and careful loop management. This project strengthens your understanding of 2D data representation, nested loops, and subroutine design in the command-line environment.