使用 HTML、CSS、JavaScript 创建一个简单的井字游戏

2025-06-09

使用 HTML、CSS、JavaScript 创建一个简单的井字游戏

用 JavaScript 创建游戏是最有趣的学习方式。它能让你保持动力,这对于学习像 Web 开发这样的复杂技能至关重要。此外,你可以和朋友一起玩,或者直接向他们展示你的作品,他们一定会惊叹不已。在今天的博文中,我们将仅使用 HTML、CSS 和 JavaScript 创建一个井字游戏。

视频教程

如果您想观看详细的分步视频,您可以查看我在Youtube 频道上制作的有关该项目的视频:

实现 HTML

首先,在 head 部分,我会包含我们稍后要创建的 css 和 javascript 文件。我还会添加一个名为 Itim 的 Google 字体,我认为它非常适合这个游戏。

    <link rel="stylesheet" href="style.css">
    <link rel="preconnect" href="https://fonts.gstatic.com">
    <link href="https://fonts.googleapis.com/css2?family=Itim&display=swap" rel="stylesheet">
    <script src="./index.js"></script>
Enter fullscreen mode Exit fullscreen mode

HTML 主体部分相当简单。为了包裹所有内容,我将使用一个 main 标签,并background为其添加一个 class。main包装器内部包含五个部分。

第一部分将仅包含我们的标题h1

第二部分将显示当前轮到谁了。在显示部分中,我们有一个 span,用于包含XO取决于当前用户。我们将为这个 span 应用类来为文本着色。

第三部分用于放置游戏棋盘。它有一个containerclass,以便我们能够正确地放置棋子。在这个部分中,我们定义了九个 div,它们将充当棋盘内的棋子。

第四部分负责宣布游戏结束结果。默认情况下,它是空的,我们将使用 JavaScript 修改其内容。

最后一部分将保存我们的控件,其中包含一个重置按钮。

<main class="background">
        <section class="title">
            <h1>Tic Tac Toe</h1>
        </section>
        <section class="display">
            Player <span class="display-player playerX">X</span>'s turn
        </section>
        <section 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>
        </section>
        <section class="display announcer hide"></section>
        <section class="controls">
            <button id="reset">Reset</button>
        </section>
    </main>
Enter fullscreen mode Exit fullscreen mode

添加 CSS

我不会详细介绍 CSS 的每一行,但您可以观看视频,或查看项目 GitHub 存储库中的源代码。

首先,我将创建style.css文件并删除任何浏览器定义的边距和填充,并设置我在整个文档的 HTML 中包含的 Google 字体。

* {
    padding: 0;
    margin: 0;
    font-family: 'Itim', cursive;
}
Enter fullscreen mode Exit fullscreen mode

下一个需要添加的重要内容是面板的样式。我们将使用 CSS 网格来创建面板。我们可以将容器平均分成三份,即为列和行分别设置 3 倍的 33%。我们将设置最大宽度并应用 ,使容器居中margin: 0 auto;

.container {
    margin: 0 auto;
    display: grid;
    grid-template-columns: 33% 33% 33%;
    grid-template-rows: 33% 33% 33%;
    max-width: 300px;
}
Enter fullscreen mode Exit fullscreen mode

接下来,我们将为棋盘内的图块添加样式。我们将添加一条白色小边框,并将最小宽度和高度设置为 100 像素。我们将使用 flexbox 将内容居中,并将justify-content和设置align-itemscenter。我们将为其设置较大的字体大小,cursor: pointer以便用户知道此字段是可点击的。

.tile {
    border: 1px solid white;
    min-width: 100px;
    min-height: 100px;
    display: flex;
    justify-content: center;
    align-items: center;
    font-size: 50px;
    cursor: pointer;
}
Enter fullscreen mode Exit fullscreen mode

我将使用两种不同的颜色来更好地区分两个玩家。为此,我将创建两个实用程序类。玩家 X 使用绿色,玩家 O 使用蓝色。

.playerX {
    color: #09C372;
}

.playerO {
    color: #498AFB;
}
Enter fullscreen mode Exit fullscreen mode

这些是本项目 CSS 的关键方面。这并非项目使用的全部样式,因此请参阅项目的 GitHub 仓库。

实现 Javascript 部分

由于我们将 JavaScript 文件包含在 中<head>,因此我们必须将代码中的所有内容包裹在这个事件处理程序之间。这样做是必要的,因为我们的脚本会在浏览器解析 HTML 正文之前加载。如果您不想将所有内容包裹在这个函数中,请随意将其添加defer到脚本标签中,或将脚本标签移到 的底部body

window.addEventListener('DOMContentLoaded', () => {
  // everything goes here
});
Enter fullscreen mode Exit fullscreen mode

首先,我们将保存对 DOM 节点的引用。我们将使用 抓取所有图块document.querySelectorAll()。我们需要一个数组,但此函数返回一个 NodeList,因此我们必须使用 将其转换为合适的数组Array.from()。我们还将获取对播放器显示、重置按钮和播音员的引用。

const tiles = Array.from(document.querySelectorAll('.tile'));
const playerDisplay = document.querySelector('.display-player');
const resetButton = document.querySelector('#reset');
const announcer = document.querySelector('.announcer');
Enter fullscreen mode Exit fullscreen mode

接下来,我们将添加控制游戏所需的全局变量。我们将用一个包含九个空字符串的数组初始化一个棋盘。它将保存棋盘上每个棋子的 X 和 O 值。我们将用一个currentPlayer变量保存当前回合中活跃玩家的符号。该isGameActive变量将一直为真,直到有人获胜或游戏以平局结束。在这些情况下,我们将它设置为假,这样剩余的棋子将处于非活跃状态,直到游戏重置。我们有三个常量表示游戏结束状态。使用这些常量是为了避免拼写错误。

let board = ['', '', '', '', '', '', '', '', ''];
let currentPlayer = 'X';
let isGameActive = true;

const PLAYERX_WON = 'PLAYERX_WON';
const PLAYERO_WON = 'PLAYERO_WON';
const TIE = 'TIE';
Enter fullscreen mode Exit fullscreen mode

下一步,我们将存储棋盘上所有获胜位置。在每个子数组中,我们将存储三个可能获胜的位置的索引。因此,这[0, 1, 2]将表示第一条水平线被玩家占据的情况。我们将使用这个数组来判断是否有获胜者。

/*
   Indexes within the board
   [0] [1] [2]
   [3] [4] [5]
   [6] [7] [8]
*/

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]
];
Enter fullscreen mode Exit fullscreen mode

现在我们将编写一些实用函数。在isValidAction函数中,我们将判断用户是否要执行有效操作。如果图块内部文本为X“或”,O则返回 false,因为操作无效;否则,图块为空,因此操作有效。

const isValidAction = (tile) => {
    if (tile.innerText === 'X' || tile.innerText === 'O'){
        return false;
    }

    return true;
};
Enter fullscreen mode Exit fullscreen mode

下一个实用函数非常简单。在这个函数中,我们将接收一个索引作为参数,并将棋盘数组中相应的元素设置为当前玩家的标志。

const updateBoard =  (index) => {
   board[index] = currentPlayer;
}
Enter fullscreen mode Exit fullscreen mode

我们将编写一个小函数来处理玩家的变化。在这个函数中,我们首先从 中删除当前玩家的类playerDisplay。字符串模板字面量player${currentPlayer}将根据当前玩家的不同而变为playerXplayerO。接下来,我们将使用三元表达式来更改当前玩家的值。如果是 ,X则为 ,O否则为X。既然我们改变了用户的值,我们需要更新 的innerTextplayerDisplay并将新的玩家类应用于它。

const changePlayer = () => {
    playerDisplay.classList.remove(`player${currentPlayer}`);
    currentPlayer = currentPlayer === 'X' ? 'O' : 'X';
    playerDisplay.innerText = currentPlayer;
    playerDisplay.classList.add(`player${currentPlayer}`);
}
Enter fullscreen mode Exit fullscreen mode

现在我们来编写 announer 函数,用于播报游戏结束结果。它将接收一个游戏结束类型,并innerText根据结果更新播音员 DOM 节点。最后一行需要删除 hide 类,因为播音员默认处于隐藏状态,直到游戏结束。

const announce = (type) => {
    switch(type){
       case PLAYERO_WON:
            announcer.innerHTML = 'Player <span class="playerO">O</span> Won';
            break;
       case PLAYERX_WON:
            announcer.innerHTML = 'Player <span class="playerX">X</span> Won';
            break;
       case TIE:
            announcer.innerText = 'Tie';
        }
    announcer.classList.remove('hide');
};
Enter fullscreen mode Exit fullscreen mode

接下来,我们将编写本项目最有趣的部分之一——结果评估。首先,我们将创建一个 roundWon 变量并将其初始化为 false。然后,我们将循环遍历winConditions数组,检查棋盘上每个获胜条件。例如,在第二次迭代中,我们将检查以下值:board[3](a)、board[4](b)、board[5](c)。

我们还会进行一些优化,如果任何字段为空,我们将调用continue并跳至下一次迭代,因为如果获胜条件中存在空块,则无法获胜。如果所有字段都相等,则表示有获胜者,因此我们将 roundWon 设置为 true 并中断 for 循环,因为任何进一步的迭代都将是浪费计算。

循环结束后,我们会检查变量的值roundWon,如果为真,我们会宣布胜者并将游戏设置为非活动状态。如果没有胜者,我们会检查棋盘上是否有空牌。如果没有胜者,并且没有空牌,我们会宣布平局。

function handleResultValidation() {
  let roundWon = false;
  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;
  }

  if (!board.includes("")) announce(TIE);
}
Enter fullscreen mode Exit fullscreen mode

接下来我们将处理用户的操作。此函数将接收一个图块和一个索引作为参数。当用户点击图块时,此函数将被调用。首先,我们需要检查这是否是一个有效的操作,并且还要检查游戏当前是否处于活动状态。如果两者都为真,我们将使用innerText当前玩家的符号更新图块的 ,添加相应的类并更新棋盘数组。现在所有内容都已更新,我们需要检查游戏是否结束,因此我们调用handleResultValidation()。最后,我们必须调用changePlayer方法将回合交给另一个玩家。

const userAction = (tile, index) => {
  if (isValidAction(tile) && isGameActive) {
    tile.innerText = currentPlayer;
    tile.classList.add(`player${currentPlayer}`);
    updateBoard(index);
    handleResultValidation();
    changePlayer();
  }
};
Enter fullscreen mode Exit fullscreen mode

为了让游戏正常运行,我们必须为图块添加事件监听器。我们可以循环遍历图块数组,为每个图块添加一个事件监听器。(为了获得更佳的性能,我们可以只为容器添加一个事件监听器,并使用事件冒泡来捕获父级图块的点击事件,但我认为对于初学者来说,这样更容易理解。)

tiles.forEach( (tile, index) => {
    tile.addEventListener('click', () => userAction(tile, index));
});
Enter fullscreen mode Exit fullscreen mode

我们只缺少一个功能:重置游戏。为此,我们将编写一个resetBoard函数。在这个函数中,我们将棋盘设置为由九个空字符串组成,将游戏设置为活动状态,移除播音员,并将玩家改回X(根据定义,X始终处于起始状态)。

我们要做的最后一件事是循环遍历图块并将 innerText 设置回空字符串,并从图块中删除任何玩家特定的类。

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');
    });
}
Enter fullscreen mode Exit fullscreen mode

现在我们只需要将此函数注册为重置按钮的点击事件处理程序。

resetButton.addEventListener('click', resetBoard);
Enter fullscreen mode Exit fullscreen mode

就是这样,我们有一个功能齐全的井字游戏,您可以和朋友一起玩,享受快乐时光。

如果您在任何时候遇到困难,请观看视频、在 Twitter 上向我发送 DM 或查看项目的 GitHub 存储库。

祝您黑客愉快!

您可以从哪里向我了解更多信息?

我创建了涵盖多个平台上的 Web 开发的教育内容,请随意查看。

我还创建了一个简报,分享我创作的一周或两周的教育内容。不废话💩,只讲教育内容。

🔗 链接:

鏂囩珷鏉ユ簮锛�https://dev.to/javascriptacademy/create-a-simple-tic-tac-toe-game-using-html-css-javascript-i4k
PREV
10 门适合初学者的最佳数据科学和机器学习课程
NEXT
你应该知道的 5 个 JavaScript 核心概念(对象)