Tic-Tac-Toe with JavaScript: Creating the Board Class | Ali Alaa - Front-end Web Developerhttps://alialaa.com/blog/tic-tac-toe-js
本文由 3 个部分组成。在第一部分中,我们将开始构建井字棋游戏棋盘背后的逻辑。我们将学习如何创建一个代表棋盘的 Javascript 类。除了一些有助于我们获取有关棋盘信息的方法外,该类将保存棋盘的当前状态。
让我们从创建项目的文件夹开始。文件夹的结构会很简单;一个index.html文件和一个script.js文件。除此之外,我们将有一个名为classes的文件夹,我们将把我们的 JS 类放入其中。所以让我们从Board类开始,在classes文件夹中创建一个board.js文件。这将是我们此时的文件夹结构:
project
│ index.html
│ script.js
│
|───classes
│ classes.js
在index.html中,我们将有一个基本的 html 文档。在文档的底部,我们将使用脚本标签导入我们的script.js文件。由于我们要使用modules,我们必须添加type="module"
到 script 标签:
Tic Tac Toe
现在让我们尝试在classes/board.js文件中添加一些代码,然后将该文件导入到我们的script.js文件中,并确保我们的代码将在浏览器中运行。所以在classes/board.js中添加一个简单的类:
export default class Board {
constructor() {
console.log("Hello Board");
}
}
现在在我们的script.js中,让我们尝试导入并初始化我们的 Board 类:
import Board from "./classes/board.js";
const board = new Board();
如果您现在在浏览器中打开index.html,您应该会在控制台中看到“Hello Board”,因为我们在类构造函数中记录了这个字符串。但是,在某些浏览器中,您可能会收到如下所示的CORS 错误:
发生这种情况是因为某些浏览器阻止了从file://协议获取的资源。因此,为了使用http,我们需要将文件夹放在服务器上。在本地服务器上运行我们的项目的一种快速方法是使用名为serve的 NPM 包。使用它所要做的就是打开你的 CMD/Terminal 并切换到你的文件夹目录:
cd path/to/your/folder
确保您的机器上安装了npm,然后运行以下命令:
npm serve
您将获得一个类似http://localhost:5000的 localhost url ,您可以在浏览器中打开它,现在 CORS 错误应该消失了,您应该在控制台中看到“Hello Board”。
现在让我们开始构建我们的棋盘。首先,我们将为我们的棋盘类提供一个参数。该参数将是一个长度为 9 的数组。该数组将保存棋盘的状态。状态是指棋盘的当前配置或X 和O 的位置。数组中的每个索引都将引用棋盘的某个单元格。如果我们将状态定义为["x","","o","o","","","x","",""]它将映射到:
左侧的棋盘显示每个单元格的指定数组索引。右边是一个具有数组配置的棋盘:["x","","o","o","","","x","",""]。
现在让我们转到board.js并为类的构造函数添加我们的参数,即棋盘的状态。并且默认将是一个空棋盘;因此,一个由 9 个空单元组成的数组:
export default class Board {
constructor(state = ["", "", "", "", "", "", "", "", ""]) {
this.state = state;
}
}
我们要创建的第一个方法对于游戏逻辑来说不是必需的;但是,它将帮助我们在开发过程中在浏览器控制台中可视化棋盘。这个方法将被称为printFormattedBoard:
printFormattedBoard() {
let formattedString = '';
this.state.forEach((cell, index) => {
formattedString += cell ? ` ${cell} |` : ' |';
if((index + 1) % 3 === 0) {
formattedString = formattedString.slice(0,-1);
if(index < 8) formattedString += '\n\u2015\u2015\u2015 \u2015\u2015\u2015 \u2015\u2015\u2015\n';
}
});
console.log('%c' + formattedString, 'color: #c11dd4;font-size:16px');
}
此方法使用 forEach 迭代状态数组,并打印每个单元格内容 + 旁边的垂直线。每 3 个单元格,我们在新行中使用 \u2015 unicode 字符打印 3 条水平线。我们还确保在最后 3 个单元格之后不打印 3 条水平线。为了测试这一点,我们在script.js中输入:
import Board from "./classes/board.js";
const board = new Board(["x", "o", "", "", "o", "", "", "", "o"]);
board.printFormattedBoard();
现在在控制台中,我们应该看到我们的棋盘格式如下:
接下来的 3 种方法将用于检查棋盘的当前状态。我们需要检查三件事;棋盘是空的吗?棋盘满了吗?棋盘是否处于最终状态?最终状态是其中一个玩家获胜或游戏为平局。
要检查棋盘是否为空,我们将使用数组助手every。
isEmpty() {
return this.state.every(function(cell) {
return cell === "";
});
}
如果每次迭代都返回 true,则 each 助手将返回 true;即如果所有单元格cell === ""
都为真。cell === ""
可以重构为!cell
,因为空字符串是错误的陈述。此外,我们可以使用箭头函数代替普通函数。因此,isEmpty 和 isFull 可以这样写:
isEmpty() {
return this.state.every(cell => !cell);
}
isFull() {
return this.state.every(cell => cell);
}
我们需要检查的最后一件事是最终状态棋盘。这种方法会很长但非常重复。首先,我们将使用 isEmpty 并在棋盘为空时返回 false。然后使用 if 条件,我们将检查水平、垂直和对角线获胜。如果不满足任何条件,我们将检查棋盘是否已满。如果棋盘已满且不满足任何获胜条件,则必须是平局。
如果发生获胜或平局,将返回一个对象,其中包含获胜者、获胜方向(垂直、水平或对角线)以及获胜者获胜的行/列数或对角线获胜的情况;将返回对角线的名称(从左上角到右下角的对角线为main ,从右上角到左下角的对角线为counter)。当我们为游戏构建 UI 时,这个对象将非常有用。
isTerminal() {
//Return False if board in empty
if(this.isEmpty()) return false;
//Checking Horizontal Wins
if(this.state[0] === this.state[1] && this.state[0] === this.state[2] && this.state[0]) {
return {'winner': this.state[0], 'direction': 'H', 'row': 1};
}
if(this.state[3] === this.state[4] && this.state[3] === this.state[5] && this.state[3]) {
return {'winner': this.state[3], 'direction': 'H', 'row': 2};
}
if(this.state[6] === this.state[7] && this.state[6] === this.state[8] && this.state[6]) {
return {'winner': this.state[6], 'direction': 'H', 'row': 3};
}
//Checking Vertical Wins
if(this.state[0] === this.state[3] && this.state[0] === this.state[6] && this.state[0]) {
return {'winner': this.state[0], 'direction': 'V', 'column': 1};
}
if(this.state[1] === this.state[4] && this.state[1] === this.state[7] && this.state[1]) {
return {'winner': this.state[1], 'direction': 'V', 'column': 2};
}
if(this.state[2] === this.state[5] && this.state[2] === this.state[8] && this.state[2]) {
return {'winner': this.state[2], 'direction': 'V', 'column': 3};
}
//Checking Diagonal Wins
if(this.state[0] === this.state[4] && this.state[0] === this.state[8] && this.state[0]) {
return {'winner': this.state[0], 'direction': 'D', 'diagonal': 'main'};
}
if(this.state[2] === this.state[4] && this.state[2] === this.state[6] && this.state[2]) {
return {'winner': this.state[2], 'direction': 'D', 'diagonal': 'counter'};
}
//If no winner but the board is full, then it's a draw
if(this.isFull()) {
return {'winner': 'draw'};
}
//return false otherwise
return false;
}
现在让我们通过尝试一些棋盘配置并记录我们方法的值来测试此代码。例如,通过在 script.js 中包含此代码:
import Board from "./classes/board.js";
const board = new Board(["x", "o", "x", "x", "o", "o", "o", "o", "x"]);
board.printFormattedBoard();
console.log(board.isEmpty());
console.log(board.isFull());
console.log(board.isTerminal());
您的控制台应如下所示:
尝试其他一些电路板状态并确保一切都按预期工作!
insert方法将简单地在某个单元格处插入一个符号。该方法将接收符号(x 或 o)和位置(单元格索引)。首先,如果单元格不存在或符号无效,我们将返回错误,以确保我们不会意外滥用此方法。然后,如果单元格已被占用,我们将返回 false。否则,我们将简单地更新状态数组并返回 true:
insert(symbol, position) {
if(![0,1,2,3,4,5,6,7,8].includes(position)) {
throw new Error('Cell index does not exist!')
}
if(!['x','o'].includes(symbol)) {
throw new Error('The symbol can only be x or o!')
}
if(this.state[position]) {
return false;
}
this.state[position] = symbol;
return true;
}
最后,我们将创建一个方法,该方法返回一个包含所有可用移动的数组。这将简单地迭代状态数组并仅在单元格为空时将单元格的索引推送到返回的数组:
getAvailableMoves() {
const moves = [];
this.state.forEach((cell, index) => {
if(!cell) moves.push(index);
});
return moves;
}
现在让我们做一些测试。假设我们有这个棋盘配置,让我们测试一下我们的一些方法:
import Board from "./classes/board.js";
const board = new Board(["x", "o", "", "x", "o", "", "o", "", "x"]);
board.printFormattedBoard();
console.log(board.isTerminal());
board.insert("o", 7);
board.printFormattedBoard();
console.log(board.getAvailableMoves());
console.log(board.isTerminal());
这应该是我们的结果:
这就是我们完成的 Board 类的样子:
/**
* @desc This class represents the board, contains methods that checks board state, insert a symbol, etc..
* @param {Array} state - an array representing the state of the board
*/
class Board {
//Initializing the board
constructor(state = ["", "", "", "", "", "", "", "", ""]) {
this.state = state;
}
//Logs a visualized board with the current state to the console
printFormattedBoard() {
let formattedString = "";
this.state.forEach((cell, index) => {
formattedString += cell ? ` ${cell} |` : " |";
if ((index + 1) % 3 === 0) {
formattedString = formattedString.slice(0, -1);
if (index < 8)
formattedString +=
"\n\u2015\u2015\u2015 \u2015\u2015\u2015 \u2015\u2015\u2015\n";
}
});
console.log("%c" + formattedString, "color: #c11dd4;font-size:16px");
}
//Checks if board has no symbols yet
isEmpty() {
return this.state.every(cell => !cell);
}
//Check if board has no spaces available
isFull() {
return this.state.every(cell => cell);
}
/**
* Inserts a new symbol(x,o) into a cell
* @param {String} symbol
* @param {Number} position
* @return {Boolean} boolean represent success of the operation
*/
insert(symbol, position) {
if (![0, 1, 2, 3, 4, 5, 6, 7, 8].includes(position)) {
throw new Error("Cell index does not exist!");
}
if (!["x", "o"].includes(symbol)) {
throw new Error("The symbol can only be x or o!");
}
if (this.state[position]) {
return false;
}
this.state[position] = symbol;
return true;
}
//Returns an array containing available moves for the current state
getAvailableMoves() {
const moves = [];
this.state.forEach((cell, index) => {
if (!cell) moves.push(index);
});
return moves;
}
/**
* Checks if the board has a terminal state ie. a player wins or the board is full with no winner
* @return {Object} an object containing the winner, direction of winning and row/column/diagonal number/name
*/
isTerminal() {
//Return False if board in empty
if (this.isEmpty()) return false;
//Checking Horizontal Wins
if (this.state[0] === this.state[1] && this.state[0] === this.state[2] && this.state[0]) {
return { winner: this.state[0], direction: "H", row: 1 };
}
if (this.state[3] === this.state[4] && this.state[3] === this.state[5] && this.state[3]) {
return { winner: this.state[3], direction: "H", row: 2 };
}
if (this.state[6] === this.state[7] && this.state[6] === this.state[8] && this.state[6]) {
return { winner: this.state[6], direction: "H", row: 3 };
}
//Checking Vertical Wins
if (this.state[0] === this.state[3] && this.state[0] === this.state[6] && this.state[0]) {
return { winner: this.state[0], direction: "V", column: 1 };
}
if (this.state[1] === this.state[4] && this.state[1] === this.state[7] && this.state[1]) {
return { winner: this.state[1], direction: "V", column: 2 };
}
if (this.state[2] === this.state[5] && this.state[2] === this.state[8] && this.state[2]) {
return { winner: this.state[2], direction: "V", column: 3 };
}
//Checking Diagonal Wins
if (this.state[0] === this.state[4] && this.state[0] === this.state[8] && this.state[0]) {
return { winner: this.state[0], direction: "D", diagonal: "main" };
}
if (this.state[2] === this.state[4] && this.state[2] === this.state[6] && this.state[2]) {
return { winner: this.state[2], direction: "D", diagonal: "counter" };
}
//If no winner but the board is full, then it's a draw
if (this.isFull()) {
return { winner: "draw" };
}
//return false otherwise
return false;
}
}
export default Board;
在下一部分中,我们将开始创建一个Player类。这个类将使用一种算法来获得最好的移动。我们还将为此玩家添加不同的难度级别。