网页井字棋实现教程
本文介绍如何使用 HTML、CSS 和 JavaScript 构建网页版井字棋游戏。内容涵盖 HTML 结构搭建、CSS 布局美化(Flexbox 与 Grid)、以及 JavaScript 交互逻辑(回合切换、胜负判断)。文章提供了详细步骤解析及完整源代码,适合前端初学者练习 DOM 操作与事件处理。
前置知识
在开始之前,请确保掌握以下基础知识:
- HTML:基础标签使用,语义化结构。
- CSS:通用样式重置 (
* { margin: 0; padding: 0; box-sizing: inherit; }),Flexbox 布局 (display: flex,justify-content,align-items),CSS Grid 布局 (grid-template-columns,grid-template-rows,grid-gap),背景和图像处理 (background-size,background-image),伪元素 (::before),伪类:hover,动画与过渡效果 (transition,transform),使用 CSS 类控制显示与隐藏。 - JavaScript:DOM 操作 (
document.getElementById(),document.querySelector()),类操作 (element.classList.add(),element.classList.remove()),事件监听与处理 (addEventListener(),removeEventListener()),回调函数,条件判断与数组方法 (Array.prototype.some()和Array.prototype.every())。
1. HTML 骨架
首先搭建 HTML 骨架。由于结构相对简单,代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>网页井字棋</title>
<!-- 引入外部样式表 -->
<link rel="stylesheet" href="./index.css">
<!-- 延迟加载外部 JavaScript 文件 -->
<script defer src="./index.js"></script>
</head>
<body>
<!-- 游戏主要容器 -->
<div class="wrapper">
<!-- 当前状态显示区域 -->
<div id="currentStatus" class="current-status">
<!-- 当前游戏角色的图片 -->
<img id="currentBeastImg" src="./1.gif" alt="unicorn">
<!-- 当前轮到哪方玩家 -->
<p> 's turn</p>
</div>
<!-- 游戏棋盘 -->
<div id="board" class="board">
<!-- 游戏棋盘的每个格子,标记为 data-cell -->
<div data-cell></div>
<div data-cell></div>
<div data-cell></div>
<div data-cell></div>
<div data-cell></div>
<div data-cell></div>
<div data-cell></div>
<div data-cell></div>
<div data-cell></div>
</div>
<!-- 游戏结束时显示的覆盖层 -->
<div id="gameEndOverlay" class="game-end-overlay">
<!-- 游戏胜利信息显示区域 -->
<div class="winning-message" data-winning-message>
<p></p>
</div>
<!-- 重启游戏的按钮容器 -->
<div class="btn-container">
<!-- 重启按钮 -->
<button id="resetButton">play again</button>
</div>
</div>
</div>
</body>
</html>
初始状态下,部分容器因无内容高度可能为 0,后续将通过 CSS 进行修饰。
2. CSS 装饰
编写 HTML 后,通过 CSS 进行样式美化。
1. 引入字体和全局样式
/* 引入自定义字体 Bungee Inline */
@import url("https://fonts.googleapis.com/css2?family=Bungee+Inline&display=swap");
/* 全局样式 */
* {
padding: 0;
margin: 0;
box-sizing: inherit;
}
引入自定义字体并清除默认边距,确保布局一致性。
2. 设置 body 样式
body {
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
text-align: center;
font-family: "Bungee Inline", cursive;
color: #f5f5f5;
overflow: hidden;
background-image: linear-gradient(to top, #a8edea 0%, #ffc7d9 100%);
}
使用 Flexbox 居中内容,设置视口高度和渐变背景。
3. 设置 .wrapper 样式
.wrapper {
background-color: #55acee53;
padding: 50px;
}
包裹游戏内容的容器,添加半透明背景和内边距。
4. 设置 .current-status 和其中的元素样式
.current-status {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 25px;
}
.current-status p {
margin: 0 5px 0 0;
font-size: 24px;
}
.current-status img {
width: auto;
height: 32px;
}
显示当前回合玩家信息,居中对齐。
5. 设置 board 和 .cell 样式
.board {
display: grid;
grid-template-columns: repeat(3, minmax(90px, 1fr));
grid-template-rows: repeat(3, minmax(90px, 1fr));
grid-gap: 12px;
width: 100%;
height: 100%;
max-width: 495px;
margin: 0 auto 15px;
}
.cell {
cursor: pointer;
position: relative;
background-color: #f5f5f5;
width: 90px;
height: 90px;
opacity: 0.5;
transition: opacity 0.2s ease-in-out;
}
.cell:hover {
opacity: 1;
}
使用 Grid 布局创建 3x3 棋盘,设置单元格大小和悬停透明度效果。
6. 鼠标悬浮时的图片效果
/* 鼠标悬浮时显示图片 */
.board.unicorn .cell:not(.dragon):not(.unicorn):hover::before,
.board.dragon .cell:not(.dragon):not(.unicorn):hover::before {
content: "";
width: 70%;
height: 70%;
display: block;
position: absolute;
background-repeat: no-repeat;
top: 50%;
left: 50%;
transform: translate3d(-50%, -50%, 0);
background-size: contain;
opacity: 50%;
}
.board.unicorn .cell:not(.dragon):hover::before {
background-image: url("./1.gif");
}
.board.dragon .cell:not(.unicorn):hover::before {
background-image: url("./2.gif");
}
利用伪元素在空白格子上显示对应玩家的图标预览。
7. 设置 game-end-overlay 样式
.game-end-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #0d1021;
}
.game-end-overlay.show {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
控制游戏结束弹窗的显示与隐藏。
8. 设置 .winning-message 样式
.winning-message {
margin: -50px 0 20px;
}
.winning-message img {
width: 100px;
}
.winning-message p {
font-size: 48px;
margin: 0;
}
设置获胜信息的排版。
9. 重启按钮样式
.reset-button {
color: #f5f5f5;
font-family: "Bungee Inline", cursive;
font-size: 30px;
white-space: nowrap;
border: none;
padding: 10px 20px;
background-color: #a186be;
box-shadow: 5px 5px 0 #55acee;
cursor: pointer;
transition: transform 0.1s ease-in-out;
position: relative;
}
.reset-button:hover {
transform: scale(1.2);
}
.reset-button:active {
top: 6px;
left: 6px;
box-shadow: none;
background-color: #9475b5;
}
定义'重新开始'按钮的交互样式。
3. JavaScript 交互
添加 JavaScript 实现游戏逻辑。
1. 获取页面元素
const board = document.getElementById('board');
const cells = document.querySelectorAll('[data-cell]');
const currentStatus = document.getElementById('currentStatus');
const resetButton = document.getElementById('resetButton');
const gameEndOverlay = document.getElementById('gameEndOverlay');
const currentBeastStatusImg = document.getElementById('currentBeastImg');
const winningMessage = document.querySelector('[data-winning-message]');
const winningMessageText = document.querySelector('[data-winning-message] p');
const winningMessageImg = document.createElement('img');
2. 初始化游戏状态
let gameIsLive = true;
let unicornTurn = true;
let winner = null;
3. 所有获胜组合
const winningCombinations = [
[0, 1, 2], [3, 4, 5], [6, 7, 8],
[0, 3, 6], [1, 4, 7], [2, 5, 8],
[0, 4, 8], [2, 4, 6]
];
4. 设置鼠标悬停时的样式
const setBoardHoverClass = () => {
board.classList.remove('unicorn');
board.classList.remove('dragon');
if (unicornTurn) {
board.classList.add('unicorn');
} else {
board.classList.add('dragon');
}
}
5. 在格子上放置图片
const placeBeastImg = (cell, currentBeast) => {
cell.classList.add(currentBeast);
}
6. 切换回合
const swapTurns = () => {
unicornTurn = !unicornTurn;
}
7. 更新当前状态
const updateCurrentStatus = () => {
if (unicornTurn) {
currentBeastStatusImg.src = './1.gif';
currentBeastStatusImg.alt = 'unicorn';
} else {
currentBeastStatusImg.src = './2.gif';
currentBeastStatusImg.alt = 'dragon';
}
}
8. 检查是否获胜
const checkWin = (currentBeast) => {
return winningCombinations.some(combination => {
return combination.every(i => {
return cells[i].classList.contains(currentBeast);
});
});
}
9. 判断是否平局
const isDraw = () => {
return [...cells].every(cell => {
return cell.classList.contains('unicorn') || cell.classList.contains('dragon');
});
}
10. 开始游戏
const startGame = () => {
cells.forEach(cell => {
winningMessageImg.remove();
cell.classList.remove('unicorn', 'dragon');
cell.removeEventListener('click', handleCellClick);
cell.addEventListener('click', handleCellClick, { once: true });
});
setBoardHoverClass();
gameEndOverlay.classList.remove('show');
}
11. 结束游戏
const endGame = (draw) => {
if (draw) {
winningMessageText.innerText = `draw!`;
} else {
winningMessageImg.src = unicornTurn ? './1.gif' : './2.gif';
winningMessageImg.alt = unicornTurn ? 'unicorn' : 'dragon';
winningMessage.insertBefore(winningMessageImg, winningMessageText);
winningMessageText.innerText = `wins!!!`;
}
gameEndOverlay.classList.add('show');
}
12. 处理格子点击事件
const handleCellClick = (e) => {
const cell = e.target;
const currentBeast = unicornTurn ? 'unicorn' : 'dragon';
placeBeastImg(cell, currentBeast);
if (checkWin(currentBeast)) {
endGame(false);
} else if (isDraw()) {
endGame(true);
} else {
swapTurns();
updateCurrentStatus();
setBoardHoverClass();
}
}
13. 重置游戏与启动
resetButton.addEventListener('click', startGame);
startGame();
完整源代码
将上述代码整合为一个完整的 HTML 文件即可运行。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>网页井字棋</title>
<style>
@import url("https://fonts.googleapis.com/css2?family=Bungee+Inline&display=swap");
* { padding: 0; margin: 0; box-sizing: inherit; }
body { margin: 0; padding: 0; display: flex; justify-content: center; align-items: center; height: 100vh; text-align: center; font-family: "Bungee Inline", cursive; color: #f5f5f5; overflow: hidden; background-image: linear-gradient(to top, #a8edea 0%, #ffc7d9 100%); }
.wrapper { background-color: #55acee53; padding: 50px; }
.current-status { display: flex; justify-content: center; align-items: center; margin-bottom: 25px; }
.current-status p { margin: 0 5px 0 0; font-size: 24px; }
.current-status img { width: auto; height: 32px; }
.board { display: grid; grid-template-columns: repeat(3, minmax(90px, 1fr)); grid-template-rows: repeat(3, minmax(90px, 1fr)); grid-gap: 12px; width: 100%; height: 100%; max-width: 495px; margin: 0 auto 15px; }
.board.unicorn .cell:not(.dragon):not(.unicorn):hover::before, .board.dragon .cell:not(.dragon):not(.unicorn):hover::before { content: ""; width: 70%; height: 70%; display: block; position: absolute; background-repeat: no-repeat; top: 50%; left: 50%; transform: translate3d(-50%, -50%, 0); background-size: contain; opacity: 50%; }
.board.unicorn .cell:not(.dragon):hover::before { background-image: url("./1.gif"); }
.board.dragon .cell:not(.unicorn):hover::before { background-image: url("./2.gif"); }
.cell { cursor: pointer; position: relative; background-color: #f5f5f5; width: 90px; height: 90px; opacity: 0.5; transition: opacity 0.2s ease-in-out; }
.cell:hover { opacity: 1; }
.cell.dragon, .cell.unicorn { opacity: 1; position: relative; cursor: not-allowed; }
.cell.dragon::before, .cell.unicorn::before { content: ""; width: 70%; height: 70%; display: block; position: absolute; background-repeat: no-repeat; top: 50%; left: 50%; transform: translate3d(-50%, -50%, 0); background-size: contain; }
.cell.dragon::before { background-image: url("./2.gif"); }
.cell.unicorn::before { background-image: url("./1.gif"); }
.game-end-overlay { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: #0d1021; }
.game-end-overlay.show { display: flex; flex-direction: column; justify-content: center; align-items: center; }
.winning-message { margin: -50px 0 20px; }
.winning-message img { width: 100px; }
.winning-message p { font-size: 48px; margin: 0; }
.btn-container { position: relative; }
.reset-button { color: #f5f5f5; font-family: "Bungee Inline", cursive; font-size: 30px; white-space: nowrap; border: none; padding: 10px 20px; background-color: #a186be; box-shadow: 5px 5px 0 #55acee; cursor: pointer; transition: transform 0.1s ease-in-out; position: relative; }
.reset-button:hover { transform: scale(1.2); }
.reset-button:active { top: 6px; left: 6px; box-shadow: none; background-color: #9475b5; }
</style>
</head>
<body>
<div class="wrapper">
<div id="currentStatus" class="current-status">
<img id="currentBeastImg" src="./1.gif" alt="unicorn">
<p> 's turn</p>
</div>
<div id="board" class="board">
<div data-cell></div><div data-cell></div><div data-cell></div>
<div data-cell></div><div data-cell></div><div data-cell></div>
<div data-cell></div><div data-cell></div><div data-cell></div>
</div>
<div id="gameEndOverlay" class="game-end-overlay">
<div class="winning-message" data-winning-message>
<p></p>
</div>
<div class="btn-container">
<button id="resetButton">play again</button>
</div>
</div>
</div>
<script>
const board = document.getElementById('board');
const cells = document.querySelectorAll('[data-cell]');
const currentStatus = document.getElementById('currentStatus');
const resetButton = document.getElementById('resetButton');
const gameEndOverlay = document.getElementById('gameEndOverlay');
const currentBeastStatusImg = document.getElementById('currentBeastImg');
const winningMessage = document.querySelector('[data-winning-message]');
const winningMessageText = document.querySelector('[data-winning-message] p');
const winningMessageImg = document.createElement('img');
let gameIsLive = true;
let unicornTurn = true;
let winner = null;
const winningCombinations = [
[0, 1, 2], [3, 4, 5], [6, 7, 8],
[0, 3, 6], [1, 4, 7], [2, 5, 8],
[0, 4, 8], [2, 4, 6]
];
const setBoardHoverClass = () => {
board.classList.remove('unicorn');
board.classList.remove('dragon');
if (unicornTurn) { board.classList.add('unicorn'); }
else { board.classList.add('dragon'); }
}
const placeBeastImg = (cell, currentBeast) => { cell.classList.add(currentBeast); }
const swapTurns = () => { unicornTurn = !unicornTurn; }
const updateCurrentStatus = () => {
if (unicornTurn) { currentBeastStatusImg.src = './1.gif'; currentBeastStatusImg.alt = 'unicorn'; }
else { currentBeastStatusImg.src = './2.gif'; currentBeastStatusImg.alt = 'dragon'; }
}
const checkWin = (currentBeast) => {
return winningCombinations.some(combination => {
return combination.every(i => { return cells[i].classList.contains(currentBeast); });
});
}
const isDraw = () => {
return [...cells].every(cell => { return cell.classList.contains('unicorn') || cell.classList.contains('dragon'); });
}
const startGame = () => {
cells.forEach(cell => {
winningMessageImg.remove();
cell.classList.remove('unicorn', 'dragon');
cell.removeEventListener('click', handleCellClick);
cell.addEventListener('click', handleCellClick, { once: true });
});
setBoardHoverClass();
gameEndOverlay.classList.remove('show');
}
const endGame = (draw) => {
if (draw) { winningMessageText.innerText = `draw!`; }
else {
winningMessageImg.src = unicornTurn ? './1.gif' : './2.gif';
winningMessageImg.alt = unicornTurn ? 'unicorn' : 'dragon';
winningMessage.insertBefore(winningMessageImg, winningMessageText);
winningMessageText.innerText = `wins!!!`;
}
gameEndOverlay.classList.add('show');
}
const handleCellClick = (e) => {
const cell = e.target;
const currentBeast = unicornTurn ? 'unicorn' : 'dragon';
placeBeastImg(cell, currentBeast);
if (checkWin(currentBeast)) { endGame(false); }
else if (isDraw()) { endGame(true); }
else { swapTurns(); updateCurrentStatus(); setBoardHoverClass(); }
}
resetButton.addEventListener('click', startGame);
startGame();
</script>
</body>
</html>

