⚪落子无悔⚫从 0 开始的井字棋实现过程
前言
三子棋又叫九宫棋、圈圈叉叉、一条龙、井字棋等。将正方形对角线连起来,相对两边依次摆上三个双方棋子,只要将自己的三个棋子走成一条线,对方就算输了。一说大家应该都不陌生,童年上学无聊肯定玩过,随时随地,都可以玩,只需要一张纸画一个棋盘,然后两个人就可以进行快乐的玩耍了!
好玩但是费纸,今天我带大家用 JS + CSS 实现一个简单的三子棋。
页面绘制
这部分是页面的绘制,如果不感兴趣的朋友可以跳过这节,直接看“游戏逻辑”部分。
重置样式
首先我们重置样式并设置字体。
* {
padding: 0;
margin: 0;
font-family: "Itim", cursive;
}
ps:通配符会匹配页面上的 所有元素 ,相当于将整颗 DOM 树都进行了遍历,效率很低 ,大家日常开中不要这么写,咱们写 demo 自己知道就好了。
背景板
背景部分我们使用 flex
布局做一个 纵向 的水平居中处理,这是因为我们游戏布局整体来说是竖着排列的,因此将 flex-direction
属性值设置为 column
,最后设置背景色和整体字体颜色。
body {
background-color: #12181b;
color: white;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
游戏名及当前执棋玩家
<div class="display">
玩家 <span class="display-player playerX">X</span> 回合
</div>
游戏名由于我们整体已经设置了白色字体颜色,因此这里只要给个 font-size
调整字体大小以及 font-weight
调整为粗体就行。
而当前执棋玩家这部分我们做一个“玩家 X 回合”的效果,这里的 X 表示执棋玩家,我们要对这个 X 做一个颜色上的区分,同时也为了后续游戏轮到另一个玩家时,能够 单独 的对这个 X 进行修改,因此 X 要额外用一个 span
标签包裹。
棋盘绘制
总算到了棋盘了。三子棋是一个 3 * 3 的棋盘,因此我们要绘制个 3 * 3 的网格。
既然都提到网格了,就不得不说说 CSS 中的网格布局 Grid
了。
网格布局在处理类似键盘、格子这种布局是一柄好刀,它可以很方便的进行布局。那么我们的棋盘用 Grid
布局应该怎么做呢?
首先我们肯定要给格子们一个 包裹容器 ,然后通过 display: grid
将这个包裹容器设置为网格布局啦。
.container {
display: grid;
max-width: 300px;
}
接下来用 9 个 div
充当棋子。
<div class="container">
<div class="tile"></div>
<div class="tile"></div>
<div class="tile"></div>
<div class="tile"></div>
<div class="tile"></div>
<div class="tile"></div>
<div class="tile"></div>
<div class="tile"></div>
<div class="tile"></div>
</div>
由于我们接下来要用 O
和 X
来表示棋子,它们是两个字母,因此我们要让 .tile
增大字体并且水平垂直居中,然后通过 cursor
属性设置鼠标 hover 时的图标,增强用户体验。
.tile {
border: 1px solid white;
min-width: 100px;
min-height: 100px;
display: flex;
justify-content: center;
align-items: center;
font-size: 50px;
cursor: pointer;
}
注:棋盘大小设置为 300px * 300px,因此一个格子就是 100px * 100px。
我们来看看效果:
emmm...,好像不太对劲,我们不是设置了网格布局吗?
没错,我们是设置了网格布局,但是我们没告诉它该怎么布局啊,没收到我们的指令前它可不能擅作主张。
所以接下来我们要用到网格布局的两个属性:grid-template-rows
和 grid-template-columns
来告诉 grid
我们要设置的网格行的尺寸大小和列的尺寸大小。
更多关于
grid-template-rows
的用法,请看:grid-template-rows - CSS(层叠样式表) | MDN (mozilla.org)
由于我们是 三行三列 的布局,我们可以这么设置:
.container {
...
grid-template-columns: 1fr 1fr 1fr;
grid-template-rows: 1fr 1fr 1fr;
}
再看看效果:
可以,像模像样的了。
重新开始按钮
接下来我们绘制一个“重新开始”按钮,让小伙伴们玩急眼的时候可以(不)友(小)好(心)相(点)处(到)。
要注意将 cursor
设置为 pointer
哦。
#reset {
padding: 8px;
border-radius: 6px;
font-size: 20px;
cursor: pointer;
background-color: #ff3860;
}
最终页面效果如下:
HTML 代码如下:
<body>
<div class="title">三子棋</div>
<div class="display">
玩家 <span class="display-player playerX">X</span> 回合
</div>
<div class="container">
<div class="tile"></div>
<div class="tile"></div>
<div class="tile"></div>
<div class="tile"></div>
<div class="tile"></div>
<div class="tile"></div>
<div class="tile"></div>
<div class="tile"></div>
<div class="tile"></div>
</div>
<div class="display announcer hide"></div>
<div id="reset">重新游戏</div>
</body>
至此,页面绘制就圆满结束啦,接下来就是紧张刺激的“游戏逻辑”环节啦,小伙伴们准备好了吗?
游戏逻辑
首先我们分析一下游戏逻辑:
- 只有 9 格子是可以落子的地方
- 只能游戏开始时落子,格子不能重复落子
- 当一方棋子有三个相连,则获胜
- 当 9 个格子都落满了棋,则平局
限制落子范围
首先我们看 逻辑 1:只有 9 格子是可以落子的地方 。
由于只有 9 个格子可以落子,因此我们直接通过 以类名获取元素 的方式得到格子集合。
const tiles = Array.from(document.querySelectorAll(".tile"));
这里用到了 Array.from
,它将类数组转为数组(document.querySelectorAll
返回的是 HTMLCollection
集合,这是一个类数组)。
对于每个格子,我们在对其进行点击 click
的时候,执行落子逻辑。
tiles.forEach((tile, index) => {
tile.addEventListener("click", () => userAction(tile, index));
});
这里我们传入被点击的 tile
元素以及对应的索引,方便后续操作。
游戏开始时落子,且不能重复落子
接下来我们要处理 逻辑 2:只能游戏开始时落子,格子不能重复落子 了。
我们新增一个变量 isGameActive
,表示游戏是否开始,初始值为 true
。
如果点击的格子已经落子了,那么它只可能有两种情况:要么落了 O
,要么落了 X
。我们写一个函数判断是否允许落子:
const isValidAction = (tile) => {
if (tile.innerText === "X" || tile.innerText === "O") {
return false;
}
return true;
};
看看 userAction
:
const userAction = (tile, index) => {
if (isValidAction(tile) && isGameActive) {
...dosomething
}
};
其中,如果当前格子允许落子且游戏进行中,那么我们就可以进行落子啦。
落子
具体的,我们设置一个变量 currentPlayer
表示当前执棋玩家,初始值为 X
,表示 X
先走。
在满足上述条件的情况下,我们要做以下几件事:
- 设置当前格子的
innerHTML
属性为currentPlayer
。 - 对 玩家 X 和 玩家 O 的棋子做一个颜色上的区分,当轮到 玩家 X 时,给当前格子添加类名
player${currentPlayer}
。 - 棋盘信息需要记录,因此我们新增一个变量
board
,玩家落子成功时,根据传入的index
设置对应的棋子O
或X
。 - 棋盘信息记录后,判断当前玩家是否获胜。
- 未获胜则切换执棋玩家。
我们先处理 1、2、3 步。
let board = ["", "", "", "", "", "", "", "", ""]; // 棋子信息记录
let currentPlayer = "X"; // 当前玩家
let isGameActive = true; // 游戏是否开始
// 棋子信息更新
const updateBoard = (index) => {
board[index] = currentPlayer;
};
const userAction = (tile, index) => {
if (isValidAction(tile) && isGameActive) {
tile.innerText = currentPlayer;
tile.classList.add(`player${currentPlayer}`);
updateBoard(index);
}
};
获胜的情况
接下来我们要处理 逻辑 3:当一方棋子有三个相连,则获胜 。
图片在某种意义上比语言更能表达想法,大家看图:
获胜的情况只有这几种,对应的索引分别是:
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],
];
那么有了这个 winningConditions
,我们判断是否获胜就可以通过枚举这 8 种情况实现了。如果判断获胜,我们要结束游戏并给出提示。
function handleResultValidation() {
let roundWon = false;
// 遍历查看是否满足8 个获胜条件之一
for (let i = 0; i <= 7; i++) {
const winCondition = winningConditions[i];
const a = board[winCondition[0]];
const b = board[winCondition[1]];
const c = board[winCondition[2]];
// 查看是否三格都有棋子,都有才可能赢
if (a === "" || b === "" || c === "") {
continue;
}
// 在三格都有棋子的基础上,满足三格格子的棋子都相同则获胜,退出循环
if (a === b && b === c) {
roundWon = true; // 获胜
break;
}
}
if (roundWon) {
// 给出提示
announce(currentPlayer === "X" ? PLAYERX_WON : PLAYERO_WON);
isGameActive = false; // 将游戏状态设置为结束
return;
}
// 这里是平局的情况下,判断棋盘信息 board 是否落满棋子,是则给出平局提示。
if (!board.includes("")) announce(TIE);
}
获胜提示
接下来是提示部分。触发这段逻辑的话,将页面上提示框的 display
属性从 none
改为 block
,同时设置其 innerHTML
属性为获胜的玩家。这部分的代码比较简单,就不多作解释了。
const announce = (type) => {
switch (type) {
case PLAYERO_WON:
announcer.innerHTML =
'玩家 <span class="playerO">O</span> 获胜!';
break;
case PLAYERX_WON:
announcer.innerHTML =
'玩家 <span class="playerX">X</span> 获胜!';
break;
case TIE:
announcer.innerText = "平局!";
}
announcer.classList.remove("hide");
};
重新游戏
同样的,“重新开始” 的逻辑是让游戏回到最开始的地方:
isGameActive
重置为true
- 棋盘信息数组
board
清空 - 获胜提示隐藏
- 初始执棋玩家为
X
- 清空棋盘所有格子上的棋子
const resetBoard = () => {
board = ["", "", "", "", "", "", "", "", ""];
isGameActive = true;
announcer.classList.add("hide");
if (currentPlayer === "O") {
changePlayer();
}
tiles.forEach((tile) => {
tile.innerText = "";
tile.classList.remove("playerX");
tile.classList.remove("playerO");
});
};
resetButton.addEventListener("click", resetBoard);
看看最终效果:
Github 源码地址
juejin-demo/game-demo at main · catwatermelon/juejin-demo (github.com)
结束语
本文就到此结束了,希望大家共同努力 💪💪。
如果文中有不对的地方,或是大家有不同的见解,欢迎指出 🙏🙏。
如果大家觉得所有收获,欢迎一键三连💕💕。
转载自:https://juejin.cn/post/7155325871635562503