Dark Dwarf Blog background

JavaScript 实现 Tic Tac Toe (1)

JavaScript 实现 Tic Tac Toe 游戏 (1)

第一部分主要讲解井字棋游戏初始化的一些东西,之后会讲一些技术性的东西。

下面我们通过实现一个简单的井字棋游戏来系统梳理一下前面的一些知识。

1. HTML & CSS 初始化

我们写一个最简单的 HTML 和 CSS:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <title>Tic Tac Toe</title>
    <link rel="stylesheet" href="style.css" />
  </head>
  <body>
    <main class="app">
      <h1>Tic Tac Toe</h1>

      <section class="controls">
        <label>
          Player X:
          <input id="playerX" type="text" placeholder="Name for X" />
        </label>
        <label>
          Player O:
          <input id="playerO" type="text" placeholder="Name for O" />
        </label>
        <button id="startBtn">Start / Restart</button>
      </section>

      <section class="board-container">
        <div id="board" class="board" aria-label="Tic tac toe board">
          <!-- 9 cells will be rendered here -->
        </div>
      </section>

      <section class="status">
        <div id="turnIndicator">Click Start to play</div>
        <div id="result" class="result" aria-live="polite"></div>
      </section>
    </main>

    <script type="module" src="script.js"></script>
  </body>
</html>
* {
  box-sizing: border-box;
}
html,
body {
  height: 100%;
}
body {
  margin: 0;
  font-family:
    system-ui,
    -apple-system,
    "Segoe UI",
    Roboto,
    "Helvetica Neue",
    Arial;
  display: flex;
  align-items: center;
  justify-content: center;
  background: linear-gradient(135deg, #f6f9ff, #e9f7f1);
  padding: 20px;
}
.app {
  max-width: 420px;
  width: 100%;
  text-align: center;
}
.controls {
  display: flex;
  gap: 8px;
  justify-content: center;
  margin-bottom: 12px;
  flex-wrap: wrap;
}
.controls input {
  padding: 6px;
}
.controls button {
  padding: 8px 12px;
  cursor: pointer;
}
.board-container {
  display: flex;
  justify-content: center;
}
.board {
  width: 320px;
  height: 320px;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-template-rows: repeat(3, 1fr);
  gap: 6px;
}
.cell {
  background: #fff;
  border-radius: 6px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 48px;
  font-weight: 700;
  cursor: pointer;
  user-select: none;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
}
.cell.taken {
  cursor: not-allowed;
  opacity: 0.9;
}
.status {
  margin-top: 12px;
}
.result {
  margin-top: 6px;
  font-weight: 600;
}
@media (max-width: 420px) {
  .board {
    width: 90vw;
    height: 90vw;
  }
  .cell {
    font-size: 6vw;
  }
}

我们之后的工作都会基于下面这个简陋的棋盘:

alt text

2. 模块划分与基本功能的实现

这些不是重点,因此粗略地带过

a.a. 模块划分

我们初步将这个游戏分成下面的模块:

  1. Gameboard 模块:负责棋盘相关的逻辑,比如棋盘的某个位置是否可以下棋、棋盘是否满了等。
  2. GameController 模块:负责具体的游戏逻辑,比如 Player 创建、下棋等。
  3. DisplayController 模块:负责具体的渲染操作。
  4. script 模块:主模块,负责组合前面实现的东西。

注意这里的 DisplayController 模块的设计,把渲染单独拎出来。

之后在讲解“发布——订阅”模式时会引入新的模块,不过这个之后再说。

b.b. 游戏相关模块的基本实现

我们把渲染模块先放在一遍,先带过一下游戏相关模块实现:

  1. Gameboard 模块:我们用一个长度为 9 的数组 [] 作为我们的棋盘,然后定义如下方法:
export default {
  reset,
  getBoard,
  getAt,
  isCellEmpty,
  setMarker,
  isFull,
  availableMoves,
};
  1. GameController 模块:我们创建两个Player 对象,然后记录对局的状态,比如当前的棋子是 X 还是 O、比赛是否结束这些状态量:
let players = { X: { name: "X" }, O: { name: "O" } };
let current = "X";
let isOver = false;
let winner = null;

然后定义如下方法:

export default {
  start,
  playAt,
  getState,
};

注意,为了之后渲染的方便,我们需要让一些函数的返回值更加格式化、并且返回更多信息,例如:

function playAt(index) {
  if (isOver) return { success: false, reason: "game-over" };
  if (!Number.isInteger(index) || index < 0 || index > 8)
    return { success: false, reason: "invalid-index" };
  if (!Gameboard.isCellEmpty(index)) return { success: false, reason: "taken" };

  const ok = Gameboard.setMarker(index, current);
  if (!ok) return { success: false, reason: "set-failed" };

  const board = Gameboard.getBoard();
  const w = checkWinner(board);
  if (w) {
    isOver = true;
    winner = w;
    return { success: true, winner: w, board };
  }

  if (Gameboard.isFull()) {
    isOver = true;
    winner = "tie";
    return { success: true, winner: "tie", board };
  }

  current = current === "X" ? "O" : "X";
  return { success: true, next: current, board };
}

接下来我们会讲解渲染部分的组织架构。