纯粹而简单 - 使用 Javascript 的井字游戏
您是否想构建一些有趣且简单的东西来练习您的前端技能,但是构建另一个 TODO 应用程序的想法让您想放弃已经开始的这条美妙的道路?
您可以在我的 Github 上找到已完成的项目。
不用再犹豫了,今天我们将构建一个简单的(鼓点)井字游戏。我们将介绍一些基础知识,例如使用 CSS 网格、查询选择器以及构建游戏流程和逻辑。
我们先来看看最终成品。
那么,最大的问题是……我们从哪里开始呢?
通常最好的方法是将应用程序分解成更小、更容易理解的部分。
首先,让我们分解一下用户界面:
- 标题
- 3x3网格
- 网格应该是可点击的
- 网格单元应该显示正确的玩家标志和信息显示
- 应该显示一条消息,通知当前玩家轮到他们了
- 应该告诉我们谁赢得了比赛
- 应该告诉我们比赛是否以平局结束
- 重启按钮
- 将重新开始整个游戏
接下来,让我们分解一下单元格点击的游戏流程:
- 需要追踪发生在我们细胞上的任何点击
- 需要检查是否进行了有效的移动
- 需要确保点击已播放的单元格时不会发生任何事情
- 我们应该更新我们的游戏状态
- 我们应该验证游戏状态
- 检查玩家是否获胜
- 检查比赛是否以平局结束
- 根据上述检查,停止游戏或更换活跃玩家
- 反映 UI 上所做的更新
- 冲洗并重复
就是这样,没有什么特别的或过于复杂的,但仍然是一个练习和提高的绝佳机会。
让我们进入有趣的部分并构建一些东西!
文件夹结构
我们将从构建用户界面开始,这样在构建游戏逻辑时我们就能有所了解。
正如我提到的,这是一个简单的游戏,所以不需要复杂的文件夹结构。
您总共应该有三个文件:
- index.html (将保存我们的 UI 结构并导入我们需要的其他文件)
- style.css (让我们的游戏看起来还算不错)
- script.js (将保存我们的游戏逻辑,并处理我们需要的所有其他内容)
HTML
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Tic Tac Toe</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<section>
<h1 class="game--title">Tic Tac Toe</h1>
<div class="game--container">
<div data-cell-index="0" class="cell"></div>
<div data-cell-index="1" class="cell"></div>
<div data-cell-index="2" class="cell"></div>
<div data-cell-index="3" class="cell"></div>
<div data-cell-index="4" class="cell"></div>
<div data-cell-index="5" class="cell"></div>
<div data-cell-index="6" class="cell"></div>
<div data-cell-index="7" class="cell"></div>
<div data-cell-index="8" class="cell"></div>
</div>
<h2 class="game--status"></h2>
<button class="game--restart">Restart Game</button>
</section>
<script src="script.js"></script>
</body>
</html>
除了常见的样板代码外,我们还在<head>
元素中包含了样式表,这样做是为了确保样式表始终在实际 HTML 代码之前加载。
我们还在结束标记的正上方添加了 script.js 文件,</body>
以确保 JavaScript 代码始终在 HTML 代码之后加载。
我们将实际的游戏单元格放在一个 div 元素中,以便使用 CSS 网格。此外,每个单元格都有一个“data-cell-index”属性,方便我们轻松追踪单元格的点击情况。
我们还有一个<h2>
元素,用于显示前面提到的游戏信息和重启按钮。
CSS
body {
font-family: "Arial", sans-serif;
}
section {
text-align: center;
}
.game--container {
display: grid;
grid-template-columns: repeat(3, auto);
width: 306px;
margin: 50px auto;
}
.cell {
font-family: "Permanent Marker", cursive;
width: 100px;
height: 100px;
box-shadow: 0 0 0 1px #333333;
border: 1px solid #333333;
cursor: pointer;
line-height: 100px;
font-size: 60px;
}
我希望将应用程序的 CSS 保持在最低限度,因此我唯一需要您注意的是“.game — container”的样式,因为这是我们实现 CSS 网格的地方。
因为我们想要一个 3x3 的网格,所以我们利用“grid-template-columns”属性将其设置为 repeat(3, auto);
简而言之,这会将包含的 div(单元格)分成三列,并让单元格自动决定其宽度。
JavaScript
现在我们到了最有趣的部分!
让我们先构造一些伪代码,使用之前编写的游戏逻辑模板将其分解成更小的部分,以此来启动我们的 JS。
/*
We store our game status element here to allow us to more easily
use it later on
*/
const statusDisplay = document.querySelector('.game--status');
/*
Here we declare some variables that we will use to track the
game state throught the game.
*/
/*
We will use gameActive to pause the game in case of an end scenario
*/
let gameActive = true;
/*
We will store our current player here, so we know whos turn
*/
let currentPlayer = "X";
/*
We will store our current game state here, the form of empty strings in an array
will allow us to easily track played cells and validate the game state later on
*/
let gameState = ["", "", "", "", "", "", "", "", ""];
/*
Here we have declared some messages we will display to the user during the game.
Since we have some dynamic factors in those messages, namely the current player,
we have declared them as functions, so that the actual message gets created with
current data every time we need it.
*/
const winningMessage = () => `Player ${currentPlayer} has won!`;
const drawMessage = () => `Game ended in a draw!`;
const currentPlayerTurn = () => `It's ${currentPlayer}'s turn`;
/*
We set the inital message to let the players know whose turn it is
*/
statusDisplay.innerHTML = currentPlayerTurn();
function handleCellPlayed() {
}
function handlePlayerChange() {
}
function handleResultValidation() {
}
function handleCellClick() {
}
function handleRestartGame() {
}
/*
And finally we add our event listeners to the actual game cells, as well as our
restart button
*/
document.querySelectorAll('.cell').forEach(cell => cell.addEventListener('click', handleCellClick));
document.querySelector('.game--restart').addEventListener('click', handleRestartGame);
我们还概述了处理游戏逻辑所需的所有功能,所以让我们开始编写逻辑吧!
处理单元格点击
在我们的单元格点击处理程序中,我们将处理两件事。
首先,我们需要检查被点击的单元格是否已经被点击过,如果没有,我们需要从那里继续我们的游戏流程。
让我们看看实际效果:
function handleCellClick(clickedCellEvent) {
/*
We will save the clicked html element in a variable for easier further use
*/
const clickedCell = clickedCellEvent.target;
/*
Here we will grab the 'data-cell-index' attribute from the clicked cell to identify where that cell is in our grid.
Please note that the getAttribute will return a string value. Since we need an actual number we will parse it to an
integer(number)
*/
const clickedCellIndex = parseInt(
clickedCell.getAttribute('data-cell-index')
);
/*
Next up we need to check whether the call has already been played,
or if the game is paused. If either of those is true we will simply ignore the click.
*/
if (gameState[clickedCellIndex] !== "" || !gameActive) {
return;
}
/*
If everything if in order we will proceed with the game flow
*/
handleCellPlayed(clickedCell, clickedCellIndex);
handleResultValidation();
}
我们将从单元格事件监听器中接收一个 ClickEvent 事件。这将使我们能够追踪哪个单元格被点击,并更轻松地获取其索引属性。
handleCellPlayed
在这个处理程序中,我们需要处理两件事:更新游戏内部状态,以及更新 UI。
function handleCellPlayed(clickedCell, clickedCellIndex) {
/*
We update our internal game state to reflect the played move,
as well as update the user interface to reflect the played move
*/
gameState[clickedCellIndex] = currentPlayer;
clickedCell.innerHTML = currentPlayer;
}
我们接受当前点击的单元格(我们的点击事件的 .target)以及已被点击的单元格的索引。
处理结果验证
接下来是井字游戏的核心——结果验证。在这里,我们将检查游戏是以胜利、平局还是还有剩余的棋步结束。
首先,我们来检查当前玩家是否赢得了游戏。
const winningConditions = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6]
];
function handleResultValidation() {
let roundWon = false;
for (let i = 0; i <= 7; i++) {
const winCondition = winningConditions[i];
let a = gameState[winCondition[0]];
let b = gameState[winCondition[1]];
let c = gameState[winCondition[2]];
if (a === '' || b === '' || c === '') {
continue;
}
if (a === b && b === c) {
roundWon = true;
break
}
}
if (roundWon) {
statusDisplay.innerHTML = winningMessage();
gameActive = false;
return;
}
}
在继续练习之前,请花一点时间将其分解一下。
我们的获胜条件数组中的值是单元格的索引,这些单元格需要由同一个玩家填充才能被视为胜利者。
在 for 循环中,我们遍历每一个元素,并检查游戏状态数组中对应索引的元素是否匹配。如果匹配,则宣布当前玩家获胜并结束游戏。
当然,我们还需要处理另外两种情况。首先,我们来检查一下游戏是否以平局结束。平局的唯一可能情况是所有字段都已填满。
const winningConditions = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6]
];
function handleResultValidation() {
let roundWon = false;
for (let i = 0; i <= 7; i++) {
const winCondition = winningConditions[i];
let a = gameState[winCondition[0]];
let b = gameState[winCondition[1]];
let c = gameState[winCondition[2]];
if (a === '' || b === '' || c === '') {
continue;
}
if (a === b && b === c) {
roundWon = true;
break
}
}
if (roundWon) {
statusDisplay.innerHTML = winningMessage();
gameActive = false;
return;
}
/*
We will check weather there are any values in our game state array
that are still not populated with a player sign
*/
let roundDraw = !gameState.includes("");
if (roundDraw) {
statusDisplay.innerHTML = drawMessage();
gameActive = false;
return;
}
/*
If we get to here we know that the no one won the game yet,
and that there are still moves to be played, so we continue by changing the current player.
*/
handlePlayerChange();
}
由于我们在 roundWon 的检查中有一个 return 语句,我们知道如果玩家赢得了该回合,脚本就会停止。这让我们避免使用 else 条件,并保持代码简洁。
处理玩家变化
在这里我们将简单地改变当前玩家并更新游戏状态消息以反映这一变化。
function handlePlayerChange() {
currentPlayer = currentPlayer === "X" ? "O" : "X";
statusDisplay.innerHTML = currentPlayerTurn();
}
我们在这里使用三元运算符来分配新玩家,您可以在这里了解更多信息。这真的太棒了!
剩下要做的就是连接我们的游戏重启功能。
处理RestartGame
在这里,我们将所有游戏跟踪变量恢复为默认值,通过移除所有标志来清除游戏板,以及将游戏状态更新回当前玩家消息。
function handleRestartGame() {
gameActive = true;
currentPlayer = "X";
gameState = ["", "", "", "", "", "", "", "", ""];
statusDisplay.innerHTML = currentPlayerTurn();
document.querySelectorAll('.cell')
.forEach(cell => cell.innerHTML = "");
}
结论
基本上,就是这样!
你有一个可以正常玩的井字棋游戏了 (* 击掌 *)
当然,我们还可以做很多事情,比如让游戏真正支持多人游戏,这样你就可以和地球另一端的朋友一起玩了。或者,为什么不编写一个可以和你一起玩游戏的算法呢?或者试试用你选择的框架编写应用,看看它与原生 JavaScript 相比如何?
这里有很多值得探索和发展的可能性,请告诉我您最喜欢哪一个,我会非常乐意制作另一个这样的指南!
与往常一样,您可以在我的 Github上找到已完成的项目。
文章来源:https://dev.to/bornasepic/pure-and-simple-tic-tac-toe-with-javascript-4pgn