跳到主要内容前端实战:网页版井字棋游戏实现 | 极客日志HTML / CSS大前端算法
前端实战:网页版井字棋游戏实现
介绍使用 HTML、CSS 和 JavaScript 构建网页版井字棋游戏的完整流程。涵盖页面结构搭建、响应式样式设计(Flexbox 与 Grid 布局)及核心交互逻辑(DOM 操作、事件监听、胜负判定)。文中包含详细代码解析与最终整合示例,适合前端初学者巩固基础知识。
星星泡饭36 浏览 本文介绍如何使用 HTML、CSS 和 JavaScript 实现一个网页版井字棋游戏。内容包括页面骨架搭建、样式美化(Flexbox 与 Grid 布局)、以及交互逻辑(回合切换、胜负判断)。最终提供完整可运行的代码示例,适合前端初学者练习 DOM 操作与事件处理。
前置知识
在开始之前,请确保掌握以下基础知识:
- HTML:基础标签使用、语义化结构。
- CSS:通用样式重置、Flexbox 布局、Grid 布局、背景图像处理、伪元素/类、动画过渡效果、类控制显示隐藏。
- JavaScript:DOM 操作、类操作、事件监听与处理、回调函数、条件判断与数组方法(
some, 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">
<script defer src="./index.js"></script>
's turn
play again
相关免费在线工具
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- Gemini 图片去水印
基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
</head>
<body>
<div class="wrapper">
<div class="current-status">
<img id="currentBeastImg" src="./1.gif" alt="unicorn">
<p id="currentStatus">
</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 class="game-end-overlay">
<div class="winning-message" data-winning-message>
<p>
</p>
</div>
<div class="btn-container">
<button id="resetButton">
</button>
</div>
</div>
</div>
</body>
</html>
2. CSS 装饰
编写 CSS 代码对页面进行样式修饰,包括字体引入、全局重置、布局设计及交互反馈。
1. 引入字体和全局样式
@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;
}
使用 CSS 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.querySelector('.game-end-overlay');
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');
通过 ID 和选择器获取操作所需的 DOM 节点。
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);
14. 启动游戏
完整代码示例
以下是整合后的完整 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 class="current-status">
<img id="currentBeastImg" src="./1.gif" alt="unicorn">
<p id="currentStatus">'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 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.querySelector('.game-end-overlay');
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, , ],
[, , ], [, , ], [, , ],
[, , ], [, , ]
];
= () => {
board..();
board..();
(unicornTurn) {
board..();
} {
board..();
}
}
= () => {
cell..(currentBeast);
}
= () => {
unicornTurn = !unicornTurn;
}
= () => {
(unicornTurn) {
currentBeastStatusImg. = ;
currentBeastStatusImg. = ;
} {
currentBeastStatusImg. = ;
currentBeastStatusImg. = ;
}
}
= () => {
winningCombinations.( {
combination.( {
cells[i]..(currentBeast);
});
});
}
= () => {
[...cells].( {
cell..() || cell..();
});
}
= () => {
cells.( {
winningMessageImg.();
cell..();
cell..();
cell.(, handleCellClick);
cell.(, handleCellClick, { : });
});
();
gameEndOverlay..();
}
= () => {
(draw) {
winningMessageText. = ;
} {
winningMessageImg. = unicornTurn ? : ;
winningMessageImg. = unicornTurn ? : ;
winningMessage.(winningMessageImg, winningMessageText);
winningMessageText. =
}
gameEndOverlay..();
}
= () => {
cell = e.;
currentBeast = unicornTurn ? : ;
(cell, currentBeast);
((currentBeast)) {
();
} (()) {
();
} {
();
();
();
}
}
resetButton.(, startGame);
();
</script>
</body>
</html>
7
8
0
3
6
1
4
7
2
5
8
0
4
8
2
4
6
const
setBoardHoverClass
classList
remove
'unicorn'
classList
remove
'dragon'
if
classList
add
'unicorn'
else
classList
add
'dragon'
const
placeBeastImg
cell, currentBeast
classList
add
const
swapTurns
const
updateCurrentStatus
if
src
'./1.gif'
alt
'unicorn'
else
src
'./2.gif'
alt
'dragon'
const
checkWin
currentBeast
return
some
combination =>
return
every
i =>
return
classList
contains
const
isDraw
return
every
cell =>
return
classList
contains
'unicorn'
classList
contains
'dragon'
const
startGame
forEach
cell =>
remove
classList
remove
'unicorn'
classList
remove
'dragon'
removeEventListener
'click'
addEventListener
'click'
once
true
setBoardHoverClass
classList
remove
'show'
const
endGame
draw
if
innerText
`draw!`
else
src
'./1.gif'
'./2.gif'
alt
'unicorn'
'dragon'
insertBefore
innerText
`wins!!!`
classList
add
'show'
const
handleCellClick
e
const
target
const
'unicorn'
'dragon'
placeBeastImg
if
checkWin
endGame
false
else
if
isDraw
endGame
true
else
swapTurns
updateCurrentStatus
setBoardHoverClass
addEventListener
'click'
startGame