Giới thiệu

Xin chào các bạn!
Trong hướng dẫn này, chúng ta sẽ tạo game cờ caro bằng JavaScript.
Game có các chức năng cơ bản như chơi ván mới , chọn chế độ chơi hai người hoặc chơi với máy, và thay đổi được độ rộng của bàn cờ.

Quy tắc tìm người chiến thắng là: Người chơi nào xếp được đủ 5 ô theo chiều ngang/dọc/chéo thì người đó chiến thắng.

Mình sẽ đi sâu vào bước logic tìm người chiến thắng và logic khi chơi với máy nên sẽ bỏ qua một số bước . các bạn đọc hết và xuống cuối sẽ có Source code đầy đủ nha.
Cùng tìm hiểu và thử làm theo nha!
Demo: https://game-caro-js-two.vercel.app/

Các bước tạo game

JavaScript là ngôn ngữ lập trình phổ biến nhất trên thế giới trong suốt 20 năm qua. JavaScript có thể học nhanh và dễ dàng áp dụng cho nhiều mục đích khác nhau, từ việc cải thiện tính năng của website đến việc chạy game và tạo phần mềm nền web.

Khi chúng ta xây dựng game cờ caro sẽ có 3 bước chính:

  • Tạo file index.html hiển thị các button và các thông tin như người đang chơi , người chiến thắng.
  • script.js là file chứa các đoạn mã về logic của game cũng như các sự kiện khi click.
  • style.css định dạng thiết kế, bố cục, phong cách hay gọi vui là trang điểm để có giao diện game thêm sinh động.

Bước 1

Sau khi tạo file index.html chúng ta thêm các đoạn mã sau

<head>
  <link rel="stylesheet" href="style.css">
</head>
index.html
<body>
  <script src="./script.js"></script>
</body>
index.html

Tiếp đó tạo các nút điều khiển

Thêm id vào button để có thể truy cập vào phần tử này trong mã JavaScript.

<body>
   <div class="board">

      <div class="status" id="status"></div>
      <div class="list-button">
        <button class="button" id="restart-btn">
          <i class="bi bi-arrow-counterclockwise"></i>
        </button>
        <button class="button" id="single-player-toggle">&#x1F477</button>

        <button class="button" id="dimension-button">10x10</button>
      </div>


    </div>
</body>
index.html

Thêm phần tử để hiển thị bàn cờ trong trò chơi. Các ô cờ sẽ được tạo và điền vào phần tử này bằng mã JavaScript khi trò chơi được khởi tạo.

<body>
    <div class="board">
   ...
   </div>
   <div id="board" class="game"></div>
</body>
index.html

Css cho các button nha

.board {
    display: flex;
    align-items: center;
    justify-content: center;
    text-align: center;
    margin-top: 20px;
  }
.list-button {
    display: flex;
  }

  .button {
    height: 25px;
    margin-left: 6px;
    border-radius: 5px;
    border: none;
  }

  .button:hover {
    background-color: rgb(211, 211, 211);
  }

  

Bước 2 tạo file chứa mã JavaScript

Tạo các đoạn mã để lấy các phần tử trong mã HTML bằng cách sử dụng các id tương ứng.

const dimensionButton = document.getElementById('dimension-button');
//thay đổi kích thước của bàn cờ.
const dimensionElement = document.getElementById('dimension');
//hiển thị kích thước hiện tại của bàn cờ.
const statusElement = document.getElementById('status');
//hiển thị trạng thái hiện tại của trò chơi (ví dụ: người chiến thắng, lượt đi tiếp theo).
const restartButton = document.getElementById('restart-btn');
//khởi động lại trò chơi.
const singlePlayerToggle = document.getElementById('single-player-toggle');
// Đây là nút để chuyển đổi chế độ chơi đơn người.
const boardElement = document.getElementById('board');
// phần tử chứa bàn cờ.
script.js

Khởi tạo các giá trị ban đầu

let dimension = 10; // Giá trị mặc định

dimensionButton.textContent = `${dimension}x${dimension}`;

let singlePlayerMode = false;
let squares = Array(dimension).fill(Array(dimension).fill(null));

let xIsNext = Math.random() < 0.5; 
// Chọn ngẫu nhiên người chơi đi trước , nhỏ hơn 0.5, xIsNext sẽ được gán giá trị true (người chơi X đi trước)
let theWinner = null;
let winningLine = [];
//khởi tạo với một mảng rỗng để lưu trữ các ô thắng nếu có.

const dimensions = [10, 12, 16, 20];
let dimensionIndex = 0; 
//để lưu trữ chỉ số của kích thước hiện tại trong mảng dimensions.
//  xử lý sự kiện khi người dùng click vào nút thay đổi kích thước bàn cờ.
script.js

Bắt sự kiện khi click

// cập nhật với giá trị kích thước mới
dimensionButton.addEventListener('click', function () {
...
          });
//khởi động lại trò chơi.
restartButton.addEventListener('click', restartGame);
//được gọi để chuyển đổi chế độ chơi
singlePlayerToggle.addEventListener('click', function () {
...
          });
script.js

Tạo function để kiểm tra, gọi các giá trị


          function handleClick(row, col) {
                    if (theWinner || squares[row][col]) {
                              return;
                    }

                    const newSquares = squares.map((row) => [...row]);
                    newSquares[row][col] = xIsNext ? 'X' : 'O';
             // Sử dụng 'X' hoặc 'O' tùy thuộc vào người chơi hiện tại
                    squares = newSquares;
                    squares = newSquares;
                    xIsNext = !xIsNext;
             // Chuyển lượt đi cho người chơi tiếp theo

                    const winner = calculateWinner(newSquares, row, col);
                    if (winner) {
                              theWinner = winner;
                              winningLine = findWinningLine(newSquares, row, col, winner);
                    }

                    renderBoard();
                    updateStatus();

                    if (singlePlayerMode && !theWinner && !xIsNext) {
                              makeComputerMove();
                    }
          }
script.js

Logic để tìm ra người chiến thắng

Người chơi nào xếp được đủ 5 ô theo chiều ngang/dọc/chéo thì người đó chiến thắng.

Đầu tiên kiểm tra theo chiều ngang (ngang hàng)

So sánh bên trái ,phải ô hiện tại khi có cùng người chơi liên tiếp và nằm trong kích thước bàn cờ. đạt giá trị ít nhất 5, tức là có ít nhất 5 ô liên tiếp cùng người chơi, hàm trả về người chơi hiện tại là người chiến thắng.

Kiểm tra theo chiều dọc (dọc cột)

Di chuyển lên và xuống so sánh với ô hiện tại. Vòng lặp while tăng count khi có cùng người chơi liên tiếp và nằm trong kích thước bàn cờ.
Nếu đạt giá trị ít nhất 5, hàm trả về người chơi hiện tại là người chiến thắng.

Kiểm tra theo đường chéo (trên cùng bên trái đến dưới cùng bên phải), (trên cùng bên phải đến dưới cùng bên trái)

Di chuyển theo đường chéo và so sánh với ô hiện tại. Vòng lặp while tăng count khi có cùng người chơi liên tiếp và nằm trong kích thước bàn cờ.
Nếu đạt giá trị ít nhất 5, hàm trả về người chơi hiện tại là người chiến thắng.

Nếu không có người chiến thắng trong bất kỳ trường hợp nào, hàm trả về null.

function calculateWinner(currentSquares, row, col) {
                    const currentPlayer = currentSquares[row][col];

                    // Kiểm tra theo chiều ngang (ngang hàng):
                    let count = 1;
                    let leftCol = col - 1;
                    while (leftCol >= 0 && currentSquares[row][leftCol] === currentPlayer) {
                              count++;
                              leftCol--;
                    }
                    let rightCol = col + 1;
                    while (rightCol < dimension && currentSquares[row][rightCol] === currentPlayer) {
                              count++;
                              rightCol++;
                    }
                    if (count >= 5) {
                              return currentPlayer;
                    }

                    // Kiểm tra theo chiều dọc (dọc cột):
                    count = 1;
                    let topRow = row - 1;
                    while (topRow >= 0 && currentSquares[topRow][col] === currentPlayer) {
                              count++;
                              topRow--;
                    }
                    let bottomRow = row + 1;
                    while (bottomRow < dimension && currentSquares[bottomRow][col] === currentPlayer) {
                              count++;
                              bottomRow++;
                    }
                    if (count >= 5) {
                              return currentPlayer;
                    }

                    // Kiểm tra theo đường chéo (trên cùng bên trái đến dưới cùng bên phải)
                    count = 1;
                    let topLeftRow = row - 1;
                    let topLeftCol = col - 1;
                    while (topLeftRow >= 0 && topLeftCol >= 0 && currentSquares[topLeftRow][topLeftCol] === currentPlayer) {
                              count++;
                              topLeftRow--;
                              topLeftCol--;
                    }
                    let bottomRightRow = row + 1;
                    let bottomRightCol = col + 1;
                    while (bottomRightRow < dimension && bottomRightCol < dimension && currentSquares[bottomRightRow][bottomRightCol] === currentPlayer) {
                              count++;
                              bottomRightRow++;
                              bottomRightCol++;
                    }
                    if (count >= 5) {
                              return currentPlayer;
                    }

                    // Kiểm tra theo đường chéo (trên cùng bên phải đến dưới cùng bên trái)
                    count = 1;
                    let topRightRow = row - 1;
                    let topRightCol = col + 1;
                    while (topRightRow >= 0 && topRightCol < dimension && currentSquares[topRightRow][topRightCol] === currentPlayer) {
                              count++;
                              topRightRow--;
                              topRightCol++;
                    }
                    let bottomLeftRow = row + 1;
                    let bottomLeftCol = col - 1;
                    while (bottomLeftRow < dimension && bottomLeftCol >= 0 && currentSquares[bottomLeftRow][bottomLeftCol] === currentPlayer) {
                              count++;
                              bottomLeftRow++;
                              bottomLeftCol--;
                    }
                    if (count >= 5) {
                              return currentPlayer;
                    }

                    return null;
          }

script.js

Thêm hàm function findWinningLine tìm các ô liên tiếp trong một hàng ngang, cột dọc hoặc đường chéo mà người chơi đã chiến thắng để highlight các ô đó.

function findWinningLine(currentSquares, row, col, winner) {
                   const currentPlayer = currentSquares[row][col];
                   const lines = [];

                   // Check horizontally
                   let leftCol = col - 1;
                   while (leftCol >= 0 && currentSquares[row][leftCol] === currentPlayer) {
                             lines.push([row, leftCol]);
                             leftCol--;
                   }
                   lines.push([row, col]);
                   let rightCol = col + 1;
                   while (rightCol < dimension && currentSquares[row][rightCol] === currentPlayer) {
                             lines.push([row, rightCol]);
                             rightCol++;
                   }
                   if (lines.length >= 5) {
                             return lines;
                   }

                   // Check vertically
                   let topRow = row - 1;
                   while (topRow >= 0 && currentSquares[topRow][col] === currentPlayer) {
                             lines.push([topRow, col]);
                             topRow--;
                   }
                   lines.push([row, col]);
                   let bottomRow = row + 1;
                   while (bottomRow < dimension && currentSquares[bottomRow][col] === currentPlayer) {
                             lines.push([bottomRow, col]);
                             bottomRow++;
                   }
                   if (lines.length >= 5) {
                             return lines;
                   }

                   // Check diagonally (top-left to bottom-right)
                   let topLeftRow = row - 1;
                   let topLeftCol = col - 1;
                   while (topLeftRow >= 0 && topLeftCol >= 0 && currentSquares[topLeftRow][topLeftCol] === currentPlayer) {
                             lines.push([topLeftRow, topLeftCol]);
                             topLeftRow--;
                             topLeftCol--;
                   }
                   lines.push([row, col]);
                   let bottomRightRow = row + 1;
                   let bottomRightCol = col + 1;
                   while (bottomRightRow < dimension && bottomRightCol < dimension && currentSquares[bottomRightRow][bottomRightCol] === currentPlayer) {
                             lines.push([bottomRightRow, bottomRightCol]);
                             bottomRightRow++;
                             bottomRightCol++;
                   }
                   if (lines.length >= 5) {
                             return lines;
                   }

                   // Check diagonally (top-right to bottom-left)
                   let topRightRow = row - 1;
                   let topRightCol = col + 1;
                   while (topRightRow >= 0 && topRightCol < dimension && currentSquares[topRightRow][topRightCol] === currentPlayer) {
                             lines.push([topRightRow, topRightCol]);
                             topRightRow--;
                             topRightCol++;
                   }
                   lines.push([row, col]);
                   let bottomLeftRow = row + 1;
                   let bottomLeftCol = col - 1;
                   while (bottomLeftRow < dimension && bottomLeftCol >= 0 && currentSquares[bottomLeftRow][bottomLeftCol] === currentPlayer) {
                             lines.push([bottomLeftRow, bottomLeftCol]);
                             bottomLeftRow++;
                             bottomLeftCol--;
                   }
                   if (lines.length >= 5) {
                             return lines;
                   }

                   return [];
         }
script.js

Hãy tạo cho chúng một chút màu sắc để highlight các ô đó.

function renderBoard() { boardElement.innerHTML = '';
     for (let row = 0; row < dimension; row++) {
            const rowElement = document.createElement('div');
               rowElement.className = 'board-row';

     for (let col = 0; col < dimension; col++) {
            const value = squares[row][col];
            const isWinningSquare = winningLine.some(([winRow, winCol]) => winRow === row && winCol === col);
            const squareButton = document.createElement('button');
               squareButton.className = 'square';
               squareButton.style.backgroundColor = isWinningSquare ? 'yellow' : 'white';
               squareButton.style.color = value === 'X' ? 'blue' : 'red';
               squareButton.style.fontWeight = isWinningSquare ? 'bold' : 'normal';
               squareButton.textContent = value;
               squareButton.addEventListener('click', () => {
            if (!singlePlayerMode || (singlePlayerMode && xIsNext)) {
                        handleClick(row, col);
                                                 }
                                       });

              rowElement.appendChild(squareButton);
                             }

              boardElement.appendChild(rowElement);
                   }
         }
script.js

Logic khi chơi với máy

Tạo hàm makeComputerMove, nó có nhiệm vụ thực hiện nước đi cho người chơi máy tính trong trò chơi. Nó xác định nước đi tốt nhất cho máy tính dựa trên trạng thái hiện tại của trò chơi .

function makeComputerMove() {
   if (!singlePlayerMode || theWinner) {
         return; }
   //  lưu trữ tọa độ của các ô trống trên bàn cờ.
   const availableMoves = []; 
   // gán giá trị 'X' nếu lượt đi là của người chơi X (xIsNext = true), ngược lại 'O'.
   const humanPlayer = xIsNext ? 'X' : 'O';
   const computerPlayer = xIsNext ? 'O' : 'X';
   // lặp qua từng hàng và cột của mảng squares. Nếu ô đó không có giá trị (null),
   //tọa độ của ô đó sẽ được thêm vào mảng availableMoves.
    squares.forEach((row, rowIndex) => {
         row.forEach((col, colIndex) => {
            if (!squares[rowIndex][colIndex]) {
               availableMoves.push([rowIndex, colIndex]);
                                        }
                              });
                    });

         ....
          }
script.js

Tiếp theo điều kiện if (availableMoves.length > 0) kiểm tra xem có các ô trống khả dụng để di chuyển hay không. Nếu không có ô trống, hàm sẽ kết thúc mà không thực hiện bất kỳ nước đi nào.

function makeComputerMove() {
...
   if (availableMoves.length > 0) {
    // kiểm tra xem máy tính có thể thắng trong nước đi tiếp theo không.
     for (let i = 0; i < availableMoves.length; i++) {
     const [row, col] = availableMoves[i];
     const newSquares = squares.map((row) => [...row]);
          newSquares[row][col] = computerPlayer;
     if (calculateWinner(newSquares, row, col) === computerPlayer) {
          handleClick(row, col);
                   return;    }   }
      // Kiểm tra xem người chơi có thể giành chiến thắng trong nước đi tiếp theo không
     for (let i = 0; i < availableMoves.length; i++) {
        const [row, col] = availableMoves[i];
        const newSquares = squares.map((row) => [...row]);
                 newSquares[row][col] = humanPlayer;
        if (calculateWinner(newSquares, row, col) === humanPlayer) {
           handleClick(row, col);
                    return;   }     }

       // Di chuyển ngẫu nhiên 
      const randomIndex = Math.floor(Math.random() * availableMoves.length);
       // Nếu không có nguy cơ thắng trong nước đi tiếp theo, 
      // nó chọn một ô trống ngẫu nhiên từ mảng availableMoves và gọi hàm handleClick để thực hiện nước đi đó.
      const [row, col] = availableMoves[randomIndex];
      // Nếu  có ít nhất 3 ô trống khả dụng, kiểm tra xem có nước đi nào dẫn đến chiến thắng cho máy tính không. 
      //Nếu có, nó gọi hàm handleClick để thực hiện nước đi đó và kết thúc hàm.
        if (availableMoves.length >= 3) {
            for (let i = 0; i < availableMoves.length; i++) {
                  const [row, col] = availableMoves[i];
                  const newSquares = squares.map((row) => [...row]);
                        newSquares[row][col] = computerPlayer;

        if (isWinningMove(newSquares, computerPlayer)) {
               handleClick(row, col);
               return;    }     }     }
      // Cuối cùng, nếu không có nước đi đặc biệt nào được chọn 
      //chọn một ô trống ngẫu nhiên và gọi hàm handleClick để thực hiện nước đi đó.
               handleClick(row, col);
                    }
script.js

Hàm isWinningMove được sử dụng trong phần điều kiện của hàm makeComputerMove để chọn một nước đi chiến thắng nếu có .

function isWinningMove(currentSquares, player) {
  for (let row = 0; row < dimension; row++) {
     for (let col = 0; col < dimension; col++) {
           if (!currentSquares[row][col]) {
                const newSquares = currentSquares.map((row) => [...row]);
                newSquares[row][col] = player;
           if (calculateWinner(newSquares, row, col) === player) {
                return true;    } 
           }   
     }    
  }
           return false;
}
script.js

Khi tìm được người chiến thắng mình sẽ hiển thị thông tin người thắng cuộc

function updateStatus() {
     if (theWinner) {
          statusElement.textContent = `Chiến thắng: ${theWinner}`;
     } else {
          statusElement.textContent = `Người chơi: ${xIsNext ? 'X' : 'O'}`;
                    }
          }
script.js

Cuối cùng mình được giao diện như này nha

caro2

Kết luận

Trên đây là hướng dẫn làm game Cờ Caro basic của mình. Hãy sáng tạo thêm logic để nâng độ khó cho game và trải nghiệm nó nhé.

Source code: https://github.com/ducanhle31/Game-Caro-js

Các bạn có nhận xét hay ý kiến đóng góp hãy để lại comment bên dưới cho mình nha. Thấy hay hãy cho mình 1 like và share cho mình nha.

Cảm ơn các bạn đã theo dõi bài viết.