likes
comments
collection
share

⚪落子无悔⚫从 0 开始的井字棋实现过程

作者站长头像
站长
· 阅读数 51

前言

三子棋又叫九宫棋、圈圈叉叉、一条龙、井字棋等。将正方形对角线连起来,相对两边依次摆上三个双方棋子,只要将自己的三个棋子走成一条线,对方就算输了。一说大家应该都不陌生,童年上学无聊肯定玩过,随时随地,都可以玩,只需要一张纸画一个棋盘,然后两个人就可以进行快乐的玩耍了!

好玩但是费纸,今天我带大家用 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>

由于我们接下来要用 OX 来表示棋子,它们是两个字母,因此我们要让 .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。

我们来看看效果:

⚪落子无悔⚫从 0 开始的井字棋实现过程

emmm...,好像不太对劲,我们不是设置了网格布局吗?

没错,我们是设置了网格布局,但是我们没告诉它该怎么布局啊,没收到我们的指令前它可不能擅作主张。

所以接下来我们要用到网格布局的两个属性:grid-template-rowsgrid-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;
}

再看看效果:

⚪落子无悔⚫从 0 开始的井字棋实现过程

可以,像模像样的了。

重新开始按钮

接下来我们绘制一个“重新开始”按钮,让小伙伴们玩急眼的时候可以(不)友(小)好(心)相(点)处(到)。

要注意将 cursor 设置为 pointer 哦。

#reset {
    padding: 8px;
    border-radius: 6px;
    font-size: 20px;
    cursor: pointer;
    background-color: #ff3860;
}

最终页面效果如下:

⚪落子无悔⚫从 0 开始的井字棋实现过程

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>

至此,页面绘制就圆满结束啦,接下来就是紧张刺激的“游戏逻辑”环节啦,小伙伴们准备好了吗?

游戏逻辑

首先我们分析一下游戏逻辑:

  1. 只有 9 格子是可以落子的地方
  2. 只能游戏开始时落子,格子不能重复落子
  3. 当一方棋子有三个相连,则获胜
  4. 当 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 先走。

在满足上述条件的情况下,我们要做以下几件事:

  1. 设置当前格子的 innerHTML 属性为 currentPlayer
  2. 玩家 X玩家 O 的棋子做一个颜色上的区分,当轮到 玩家 X 时,给当前格子添加类名 player${currentPlayer}
  3. 棋盘信息需要记录,因此我们新增一个变量 board ,玩家落子成功时,根据传入的 index 设置对应的棋子 OX
  4. 棋盘信息记录后,判断当前玩家是否获胜。
  5. 未获胜则切换执棋玩家。

我们先处理 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:当一方棋子有三个相连,则获胜

图片在某种意义上比语言更能表达想法,大家看图:

⚪落子无悔⚫从 0 开始的井字棋实现过程

获胜的情况只有这几种,对应的索引分别是:

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");
};

重新游戏

同样的,“重新开始” 的逻辑是让游戏回到最开始的地方:

  1. isGameActive 重置为 true
  2. 棋盘信息数组 board 清空
  3. 获胜提示隐藏
  4. 初始执棋玩家为 X
  5. 清空棋盘所有格子上的棋子
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);

看看最终效果:

⚪落子无悔⚫从 0 开始的井字棋实现过程

Github 源码地址

juejin-demo/game-demo at main · catwatermelon/juejin-demo (github.com)

结束语

本文就到此结束了,希望大家共同努力 💪💪。

如果文中有不对的地方,或是大家有不同的见解,欢迎指出 🙏🙏。

如果大家觉得所有收获,欢迎一键三连💕💕。

转载自:https://juejin.cn/post/7155325871635562503
评论
请登录