Welcome back to Minesweeper Part 2.
Last time you built the core: grid, mine placement, and adjacency counts. Now you’ll wire it up into a fully playable CLI game.
Here's what you’ll add in this part:
- A clean terminal UI: headers, separators, and aligned columns
- A game loop with a visible prompt
- Command parser for
rROW,COL
(reveal) andfROW,COL
(flag) - Input validation + helpful error messages
- Win/lose checks (boom vs. all mines flagged)
By the end, a single node minesweeper.js 9
gives you a working game you can extend in minutes.
Let’s build!
Step 6: Set up the game loop
Now that our grid generation process is complete, let’s initialize our game by:
- Clearing the terminal window.
- Printing a “Welcome” message that tells the user how to play the game.
- Printing the formatted grid.
- Prompting the user to play by typing a command.
Clear the terminal window
Let’s define a new function named clearScreen()
that outputs the \x1Bc
escape code to the standard output, which clears the console from any previous output.
function clearScreen() { process.stdout.write('\x1Bc'); }
Print a “Welcome” message
Let’s define a new function named printWelcome()
that greets the user and tells them how to play.
Whenever prompted the user can:
- Type
rROW,COL
to reveal a square (e.g.,r0,3
). - Type
fROW,COL
to flag/unflag a square (e.g.,f2,4
).
function printWelcome() { process.stdout.write('Welcome to MinesweeperJS!\n\n'); process.stdout.write('> Type "rROW,COL" to reveal a square (e.g., r0,3).\n'); process.stdout.write('> Type "fROW,COL" to flag/unflag a square (e.g., f2,4).\n\n'); process.stdout.write('Got it? Let\'s play!\n\n'); }
For example:
-
r0,0
reveals the square at row 0, column 0. -
f1,1
flags the square at row 1, column 1.
0 1 2 3 ──────────── 0 │ 2 ■ ■ ■ │ 1 │ ■ ⚑ ■ ■ │ 2 │ ■ ■ ■ ■ │ 3 │ ■ ■ ■ ■ │ ────────────
Prompt the user to play
Let’s define a new function named printPrompt()
that outputs a >
symbol, prompting the user to enter a command (e.g., r1,3
for revealing the square at row 1, column 3).
function printPrompt() { process.stdout.write('> '); }
Print the formatted grid
Let’s define a new function named printGrid()
that outputs the updated grid in the following format:
0 1 2 3 4 5 6 7 8 9 ────────────────────────────── 0 │ 0 1 1 ■ * ■ ■ ■ ■ ■ │ 1 │ 0 2 ⚑ ■ ■ ■ ■ ■ ■ ■ │ 2 │ 0 0 ⚑ ■ ■ ■ ■ ■ ■ ■ │ 3 │ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ │ 4 │ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ │ 5 │ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ │ 6 │ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ │ 7 │ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ │ 8 │ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ │ 9 │ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ │ ──────────────────────────────
Where:
- The numbers at the top of the grid represent the column numbers of the grid.
- The numbers on the side of the grid represent the row numbers of the grid.
- The ■ symbols inside the grid represent the squares that haven’t yet been revealed.
- The numbers inside the grid represent the squares that have already been revealed.
- The ⚑ symbols inside the grid represent the squares that have been flagged.
- The * symbols inside the grid represent a mine that exploded.
Within the function’s body, let’s first output the column numbers and the top grid separation line under each number.
function printGrid(grid) { process.stdout.write(' '); for (let i = 0 ; i < grid.length ; i++) { process.stdout.write(` ${i} `); } process.stdout.write('\n '); for (let i = 0 ; i < grid.length ; i++) { process.stdout.write('───'); } process.stdout.write('\n'); }
Let’s then start a for
loop that, for each row, outputs:
- The row number
- The state of the square (unrevealed, adjacent mines, mine, or flagged)
- The right grid separation line
function printGrid(grid) { process.stdout.write(' '); for (let i = 0 ; i < grid.length ; i++) { process.stdout.write(` ${i} `); } process.stdout.write('\n '); for (let i = 0 ; i < grid.length ; i++) { process.stdout.write('───'); } process.stdout.write('\n'); for (let row = 0 ; row < grid.length ; row++) { process.stdout.write(`${row} │`); for (let col = 0 ; col < grid.length ; col++) { let square = grid[row][col]; let char = '■'; if (square.revealed) { char = square.mine ? '*' : String(square.adjacent); } else if (square.flagged) { char = '⚑'; } process.stdout.write(` ${char} `); } process.stdout.write('│\n'); } }
Finally, let’s output the bottom grid separation line.
function printGrid(grid) { process.stdout.write(' '); for (let i = 0 ; i < grid.length ; i++) { process.stdout.write(` ${i} `); } process.stdout.write('\n '); for (let i = 0 ; i < grid.length ; i++) { process.stdout.write('───'); } process.stdout.write('\n'); for (let row = 0 ; row < grid.length ; row++) { process.stdout.write(`${row} │`); for (let col = 0 ; col < grid.length ; col++) { let square = grid[row][col]; let char = '■'; if (square.revealed) { char = square.mine ? '*' : String(square.adjacent); } else if (square.flagged) { char = '⚑'; } process.stdout.write(` ${char} `); } process.stdout.write('│\n'); } process.stdout.write(' '); for (let i = 0 ; i < grid.length ; i++) { process.stdout.write('───'); } process.stdout.write('\n\n'); }
Put it together
Let’s now invoke each of these functions within the IIFE in the following order.
(() => { const size = parseGridSize(); const grid = createGrid(size); clearScreen(); printWelcome(); printGrid(grid); printPrompt(); })();
When launching the script, you should now see something similar in your terminal window.
$ node minesweeper.js 10 Welcome to MinesweeperJS! > Type "rROW,COL" to reveal a square (e.g., r0,3). > Type "fROW,COL" to flag/unflag a square (e.g., f2,4). Got it? Let's play! 0 1 2 3 4 5 6 7 8 9 ────────────────────────────── 0 │ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ │ 1 │ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ │ 2 │ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ │ 3 │ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ │ 4 │ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ │ 5 │ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ │ 6 │ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ │ 7 │ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ │ 8 │ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ │ 9 │ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ │ ────────────────────────────── >
Step 7: Read and parse user input
Let’s set up an event listener that will execute a callback function every time the standard input stream receives a 'data'
event, which will be in turn triggered when the user presses the ENTER
key.
(() => { // ... process.stdin.on('data', input => { // }); })();
Let’s convert the input into a string using the toString()
method and clean it by removing any whitespaces and newlines before and after using the trim()
method.
(() => { // ... process.stdin.on('data', input => { const line = input.toString().trim(); }); })();
Step 8: Check the command format
Let’s use a regular expression that will match and extract the following pattern r|fROW,COL
from the user input line, where:
-
rROW,COL
is a command used to reveal a square at row ROW and column COL in the grid (e.g.,r0,2
). -
fROW,COL
is a command used to flag/unflag a square at row ROW and column COL in the grid (e.g.,f3,9
).
(() => { // ... process.stdin.on('data', input => { const line = input.toString().trim(); const match = line.match(/^(r|f)(\d+),(\d+)$/i); }); })();
If the command is invalid because it doesn’t match the regular expression format:
- Clear the terminal screen
- Output an error message
- Print the grid
- Print the prompt
(() => { // ... process.stdin.on('data', input => { const line = input.toString().trim(); const match = line.match(/^(r|f)(\d+),(\d+)$/i); if (!match) { clearScreen(); process.stdout.write('Error: Invalid command\n\n'); printGrid(grid); printPrompt(); } }); })();
Step 9: Check the square position
If the command format is valid, extract and convert the command, the row number, and the column number.
(() => { // ... process.stdin.on('data', input => { const line = input.toString().trim(); const match = line.match(/^(r|f)(\d+),(\d+)$/i); if (!match) { clearScreen(); process.stdout.write('Error: Invalid command\n\n'); printGrid(grid); printPrompt(); } else { const cmd = match[1]; const row = parseInt(match[2]); const col = parseInt(match[3]); } }); })();
If the square position is out of bounds (negative or greater than the grid’s last index):
- Clear the terminal screen
- Output an error message
- Print the grid
- Print the prompt
(() => { // ... process.stdin.on('data', input => { const line = input.toString().trim(); const match = line.match(/^(r|f)(\d+),(\d+)$/i); if (!match) { clearScreen(); process.stdout.write('Error: Invalid command\n\n'); printGrid(grid); printPrompt(); } else { const cmd = match[1]; const row = parseInt(match[2]); const col = parseInt(match[3]); if (row < 0 || col < 0 || row > grid.length - 1 || col > grid.length - 1) { clearScreen(); process.stdout.write('Error: Invalid square\n\n'); printGrid(grid); printPrompt(); } } }); })();
Step 10: Reveal a square
If the square position is valid and the command is rROW,COL
:
- Clear the terminal screen
- Mark the square as revealed
- Print the updated grid
- Check if the square was a mine, output a “Game over” message, and terminate the process
- Print the prompt
(() => { // ... process.stdin.on('data', input => { const line = input.toString().trim(); const match = line.match(/^(r|f)(\d+),(\d+)$/i); if (!match) { clearScreen(); process.stdout.write('Error: Invalid command\n\n'); printGrid(grid); printPrompt(); } else { const cmd = match[1]; const row = parseInt(match[2]); const col = parseInt(match[3]); if (row < 0 || col < 0 || row > grid.length - 1 || col > grid.length - 1) { clearScreen(); process.stdout.write('Error: Invalid square\n\n'); printGrid(grid); printPrompt(); } else { let square = grid[row][col]; clearScreen(); if (cmd === 'r') { square.revealed = true; } printGrid(grid); if (square.revealed && square.mine) { process.stdout.write('💥 Boooooom! Game over...\n\n'); process.exit(0); } printPrompt(); } } }); })();
Check if all squares are revealed
Let’s define a new function named checkRevealedSquares()
that returns true if all the safe squares (the ones without mines) have been revealed, and false otherwise.
function checkRevealedSquares(grid) { const maxRevealed = (grid.length * grid.length) - grid.length; let count = 0; for (let row = 0 ; row < grid.length ; row++) { for (let col = 0 ; col < grid.length ; col++) { let square = grid[row][col]; if (!square.mine && square.revealed) { count++; } } } return count === maxRevealed; }
Let’s update the IIFE to invoke this function, output a “win” message if it returns true
, and immediately terminate the process.
(() => { // ... let flags = size; process.stdin.on('data', input => { // ... if (!match) { // ... } else { const cmd = match[1]; const row = parseInt(match[2]); const col = parseInt(match[3]); if (/* ... */) { // ... } else { // ... if (square.revealed && square.mine) { process.stdout.write('💥 Boooooom! Game over...\n\n'); process.exit(0); } else if (checkRevealedSquares(grid)) { process.stdout.write('🏆 You win!\n\n'); process.exit(0); } printPrompt(); } } }); })();
Step 11: Flag a square
At the top of the IIFE, let’s declare a new variable named flags
that will keep track of the number of flags available and initialize it to the size of the grid. Note that the number of flags available should equal the number of mines in the grid.
(() => { const size = parseGridSize(); const grid = createGrid(size); let flags = size; // ... })();
Within the if
block that handles the “reveal” command, let’s add a condition that checks if the revealed square was previously flagged, unflag it and increase the number of flags available.
(() => { // ... let flags = size; process.stdin.on('data', input => { // ... if (!match) { // ... } else { const cmd = match[1]; const row = parseInt(match[2]); const col = parseInt(match[3]); if (/* ... */) { // ... } else { let square = grid[row][col]; clearScreen(); if (cmd === 'r') { square.revealed = true; if (square.flagged) { square.flagged = false; flags++; } } // ... } } }); })();
Let’s create a new else...if
block to handle the “flag” command, and within it let’s add 3 more checks:
- If the square is unflagged and there are no more flags available, output an error message.
- If the square is already flagged, unflag it and increase the number of available flags.
- If the square is not yet revealed, flag it and decrease the number of available flags.
(() => { // ... let flags = size; process.stdin.on('data', input => { // ... if (!match) { // ... } else { const cmd = match[1]; const row = parseInt(match[2]); const col = parseInt(match[3]); if (/* ... */) { // ... } else { let square = grid[row][col]; clearScreen(); if (cmd === 'r') { square.revealed = true; if (square.flagged) { square.flagged = false; flags++; } } else if (cmd === 'f') { if (!square.flagged && !flags) { process.stdout.write('Error: No more flags\n\n'); } else if (square.flagged) { square.flagged = false; flags++; } else if (!square.revealed) { square.flagged = true; flags--; } } // ... } } }); })();
Check if all mines are flagged
Let’s define a new function named checkFlaggedMines()
that returns true
if all the mines have been correctly flagged, and false
otherwise.
function checkFlaggedMines(grid) { let count = 0; for (let row = 0 ; row < grid.length ; row++) { for (let col = 0 ; col < grid.length ; col++) { let square = grid[row][col]; if (square.mine && square.flagged) { count++; } } } return count === grid.length; }
Let’s update the IIFE to invoke this function, output a “win” message if it returns true
, and immediately terminate the process.
(() => { // ... let flags = size; process.stdin.on('data', input => { // ... if (!match) { // ... } else { const cmd = match[1]; const row = parseInt(match[2]); const col = parseInt(match[3]); if (/* ... */) { // ... } else { // ... if (square.revealed && square.mine) { process.stdout.write('💥 Boooooom! Game over...\n\n'); process.exit(0); } else if (checkFlaggedMines(grid) || checkRevealedSquares(grid)) { process.stdout.write('🏆 You win!\n\n'); process.exit(0); } printPrompt(); } } }); })();
Final thoughts
Congratulations!
You’ve built a playable Minesweeper CLI in Node.js — from grid generation and random mine placement to adjacency counts, flagging, and a simple command parser.
In Part 3, we’ll add the final touch to our game by storing the player’s username, score, and game duration into a file on the disk to create a “high score” screen.
Thank you for reading and see you in Part 3.
What’s next?
💾 Want to run this project on your machine?
Download the source code for free at https://learnbackend.dev/downloads/code-in-action/minesweeper
💻 Not sure if programming is your thing?
Discover the Learn Backend Skill Surge Challenge — a beginner challenge designed to teach you how to use the CLI, write and run JavaScript/Node.js code, and think like a professional developer in just 21 days!
🚀 Ready to go pro with backend development?
Join the Learn Backend Mastery Program today — a zero-to-hero roadmap to become a professional Node.js backend developer and land your first job in 12 months.
Top comments (0)