Hướng dẫn xây dựng game Minesweeper CLI bằng Node.js (Phần 1/3)
Trong hướng dẫn này, bạn sẽ thực hiện game Minesweeper kinh điển dưới dạng giao diện dòng lệnh bằng cách sử dụng Node.js, đầu vào/đầu ra tiêu chuẩn và một số thuật toán đơn giản.
Mục tiêu của phần này
Trong phần này, bạn sẽ học cách:
- Phân tích kích thước lưới từ CLI.
- Tạo một lưới 2D và đặt mìn ngẫu nhiên.
- Đếm số mìn lân cận bằng cách sử dụng các delta lân cận.
Chúng ta sẽ bắt đầu từ một tệp trống và hoàn thành trong Phần 3 với một trò chơi hoàn chỉnh mà bạn có thể mở rộng.
Quy tắc của trò chơi
Khi bắt đầu trò chơi, một số lượng mìn cố định sẽ được đặt ngẫu nhiên trên một lưới kích thước N x N, nơi mỗi mìn chiếm một ô ẩn. Mục tiêu là mở tất cả các ô an toàn trên lưới mà không để lộ mìn.
Khi bạn mở một ô:
- Nếu nó chứa mìn → bạn thua (game over).
- Nếu không chứa mìn, nó sẽ hiển thị một số (0–8).
Số đó là số lượng mìn trong 8 ô lân cận (trên, dưới, trái, phải và đường chéo). Ví dụ, nếu một ô hiển thị số 2, có chính xác hai ô lân cận chứa mìn.
Bạn cũng có thể “đánh dấu” một ô nếu bạn nghi ngờ nó chứa mìn.
Bạn thắng nếu:
- Mở tất cả các ô an toàn.
- Đánh dấu chính xác tất cả các mìn.
Bước 0: Tạo script
Hãy bắt đầu bằng cách tạo một tệp mới có tên minesweeper.js và mở nó trong một trình soạn thảo văn bản.
$ code minesweeper.js
Bước 1: Tạo điểm vào IIFE
Trong tệp này, hãy tạo điểm vào của script bằng cách sử dụng Immediately Invoked Function Expression (IIFE).
javascript
(() => {
//
})();
Bước 2: Tạo lưới trống
Trước IIFE, hãy định nghĩa một hàm mới có tên createGrid() để tạo một lưới vuông có kích thước tùy ý.
javascript
function createGrid() {
//
}
Trong thân hàm, hãy khai báo một hằng số có tên size để định nghĩa kích thước của lưới và khởi tạo với giá trị 6.
javascript
function createGrid() {
const size = 6;
}
Tiếp theo, hãy khai báo một hằng số có tên grid để đại diện cho lưới hai chiều, và khởi tạo nó với một mảng của các mảng, trong đó mỗi phần tử của các mảng con chứa một đối tượng đại diện cho một ô, với:
minecho biết nếu nó chứa mìnadjacentcho biết có bao nhiêu mìn xung quanhrevealedcho biết nếu ô đã được mởflaggedcho biết nếu ô đã được đánh dấu
Và hãy trả về mảng grid.
javascript
function createGrid() {
const size = 6;
const grid = Array.from({ length: size }, () => Array.from({ length: size }, () => ({
mine: false,
adjacent: 0,
revealed: false,
flagged: false
})));
return grid;
}
Cuối cùng, trong điểm vào, hãy khai báo một hằng số mới có tên grid và khởi tạo nó với giá trị trả về từ hàm createGrid().
javascript
(() => {
const grid = createGrid();
})();
Bước 3: Phân tích kích thước lưới từ CLI
Hãy định nghĩa một hàm mới có tên parseGridSize() để xác định và thiết lập kích thước của lưới từ giao diện dòng lệnh.
javascript
function parseGridSize() {
//
}
Trong thân hàm, hãy khai báo một hằng số có tên size và khởi tạo nó với giá trị của tham số dòng lệnh thứ 3 của script được chuyển đổi thành số nguyên cơ số 10 bằng hàm parseInt().
javascript
function parseGridSize() {
const size = parseInt(process.argv[2], 10);
}
Tiếp theo, hãy trả về giá trị đã chuyển đổi nếu nó là một số hợp lệ nằm giữa 3 và 10 (bao gồm), hoặc 6 nếu không.
javascript
function parseGridSize() {
const size = parseInt(process.argv[2], 10);
return isNaN(size) || size < 3 || size > 10 ? 6 : size;
}
Sau đó, hãy cập nhật hàm createGrid() bằng cách:
- Thay đổi chữ ký của nó để bao gồm một tham số mới có tên
size. - Loại bỏ hằng số
sizekhỏi thân hàm. - Gọi hàm
createGrid()bằng giá trị trả về từ hàmparseGridSize()làm tham số.
javascript
function createGrid(size) {
const grid = Array.from(/* ... */);
return grid;
}
(() => {
const size = parseGridSize();
const grid = createGrid(size);
})();
Bây giờ bạn có thể thực thi script minesweeper.js với một tham số bổ sung (và tùy chọn).
$ node minesweeper.js 9
Bước 4: Đặt mìn ngẫu nhiên trong lưới
Tạo số ngẫu nhiên trong phạm vi
Để có thể tạo một vị trí ngẫu nhiên trong lưới, hãy định nghĩa một hàm trợ giúp mới có tên generateRandomNumber() trả về một số nguyên ngẫu nhiên trong một phạm vi giá trị.
javascript
function generateRandomNumber(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
Đặt mìn trong lưới
Hãy định nghĩa một hàm khác có tên placeMines() để ngẫu nhiên đặt N mìn trong lưới, trong đó N là một số nguyên bằng kích thước của lưới.
javascript
function placeMines(grid) {
//
}
Trong thân hàm, hãy định nghĩa hai hằng số có tên min và max và khởi tạo chúng với chỉ số thấp nhất và cao nhất được xác định trong mảng grid.
javascript
function placeMines(grid) {
const min = 0;
const max = grid.length - 1;
}
Hãy khai báo một biến có tên mines để đếm số lượng mìn đã đặt trong lưới và khởi tạo nó với 0.
javascript
function placeMines(grid) {
const min = 0;
const max = grid.length - 1;
let mines = 0;
}
Sử dụng một vòng lặp while để:
- Tạo tại mỗi vòng lặp một số hàng và cột ngẫu nhiên.
- Kiểm tra xem ô ở vị trí này không chứa mìn.
- Đặt một mìn tại vị trí này.
- Cập nhật bộ đếm mìn để theo dõi số lượng mìn đã được đặt.
javascript
function placeMines(grid) {
const min = 0;
const max = grid.length - 1;
let mines = 0;
while (mines < grid.length) {
let row = generateRandomNumber(min, max);
let col = generateRandomNumber(min, max);
let square = grid[row][col];
if (!square.mine) {
square.mine = true;
mines++;
}
}
}
Cập nhật lưới
Hãy cập nhật hàm createGrid() và gọi hàm placeMines() bên trong nó để cập nhật lưới mới tạo.
javascript
function createGrid(size) {
const grid = Array.from(/* ... */);
placeMines(grid);
return grid;
}
Bước 5: Tính toán và đặt số trong lưới
Tính toán mìn lân cận
Hãy định nghĩa một hàm mới có tên placeNumbers() để cho mỗi ô của lưới đếm bao nhiêu ô lân cận chứa mìn và cập nhật giá trị thuộc tính lân cận của nó tương ứng.
javascript
function placeNumbers(grid) {
//
}
Trong thân hàm, hãy thiết lập hai vòng lặp lồng nhau để lặp qua mỗi ô của từng hàng, theo từng cột, và bỏ qua những ô chứa mìn.
javascript
function placeNumbers(grid) {
for (let row = 0 ; row < grid.length ; row++) {
for (let col = 0 ; col < grid.length ; col++) {
let square = grid[row][col];
if (square.mine) {
continue;
}
}
}
}
Định nghĩa một hằng số có tên deltas chứa danh sách tất cả các vị trí tương đối của các ô xung quanh.
javascript
function placeNumbers(grid) {
const deltas = [
[-1, -1], // trên-trái
[-1, 0], // trên
[-1, 1], // trên-phải
[0, 1], // phải
[1, 1], // dưới-phải
[1, 0], // dưới
[1, -1], // dưới-trái
[0, -1], // trái
];
for (let row = 0 ; row < grid.length ; row++) {
for (let col = 0 ; col < grid.length ; col++) {
let square = grid[row][col];
if (square.mine) {
continue;
}
}
}
}
Định nghĩa một vòng lặp for...of để lặp qua từng phần tử của mảng deltas, và kiểm tra xem vị trí của ô trừ hoặc cộng với giá trị của delta hiện tại có nằm ngoài phạm vi của lưới hay không.
javascript
function placeNumbers(grid) {
const deltas = [[-1, -1], [-1, 0], [-1, 1], [0, 1], [1, 1], [1, 0], [1, -1], [0, -1]];
for (let row = 0 ; row < grid.length ; row++) {
for (let col = 0 ; col < grid.length ; col++) {
let square = grid[row][col];
if (square.mine) {
continue;
}
for (const [deltaX, deltaY] of deltas) {
if (row + deltaX < 0 || row + deltaX > grid.length - 1 || col + deltaY < 0 || col + deltaY > grid.length - 1) {
continue;
}
}
}
}
}
Khai báo một biến có tên count để theo dõi số lượng mìn trong các ô lân cận nằm trong phạm vi của lưới, và cập nhật thuộc tính lân cận của ô hiện tại khi vòng lặp for...of kết thúc.
javascript
function placeNumbers(grid) {
const deltas = [[-1, -1], [-1, 0], [-1, 1], [0, 1], [1, 1], [1, 0], [1, -1], [0, -1]];
for (let row = 0 ; row < grid.length ; row++) {
for (let col = 0 ; col < grid.length ; col++) {
let square = grid[row][col];
if (square.mine) {
continue;
}
let count = 0;
for (const [deltaX, deltaY] of deltas) {
if (row + deltaX < 0 || row + deltaX > grid.length - 1 || col + deltaY < 0 || col + deltaY > grid.length - 1) {
continue;
}
if (grid[row + deltaX][col + deltaY].mine) {
count++;
}
}
square.adjacent = count;
}
}
}
Cập nhật lưới
Hãy cập nhật hàm createGrid() và gọi hàm placeNumbers() bên trong nó để cập nhật lưới.
javascript
function createGrid(size) {
const grid = Array.from(/* ... */);
placeMines(grid);
placeNumbers(grid);
return grid;
}
Kết luận
Chúc mừng bạn — bạn đã xây dựng mô hình đầy đủ của game Minesweeper: một lưới vuông, mìn được đặt ngẫu nhiên và các số lân cận được tính toán chính xác cho mỗi ô an toàn.
Trong Phần 2, bạn sẽ biến mô hình này thành một trò chơi có thể chơi trong terminal: hiển thị lưới có thể đọc được, phân tích các lệnh, cập nhật trạng thái và xử lý các điều kiện thắng/thua.
Cảm ơn bạn đã đọc và hẹn gặp lại trong Phần 2.
Những gì tiếp theo?
👉 Mới bắt đầu với lập trình? Khám phá Thử thách Kỹ năng Backend — một thử thách dành cho người mới bắt đầu để dạy bạn cách sử dụng CLI, chạy mã JavaScript/Node.js và tư duy như một nhà phát triển trong chỉ 21 ngày.
👉 Sẵn sàng trở thành chuyên gia với phát triển backend? Tham gia Chương trình Thành thạo Backend — lộ trình hoàn chỉnh từ người mới đến chuyên gia để trở thành một nhà phát triển backend Node.js chuyên nghiệp chỉ trong 12 tháng.