网页版井字棋的前端实现
本文介绍如何使用 HTML、CSS 和 JavaScript 实现一个网页版井字棋游戏。
前置知识
在开始之前,请确保掌握以下基础知识:
- HTML: 基础标签使用,结构搭建。
- CSS: 通用样式重置,Flexbox 布局,Grid 布局,背景图像处理,伪元素,动画与过渡效果。
- JavaScript: DOM 操作,类操作,事件监听,回调函数,条件判断与数组方法。
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>
</head>
<body>
<div id="board" class="wrapper">
<div id="currentStatus" class="current-status">
<img id="currentBeastImg" src="./1.gif" alt="unicorn">
<p> 's turn</p>
</div>
<div 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 data-winning-message>
<p></p>
</div>
<div class="btn-container">
<button id="resetButton">play again</button>
</div>
</div>
</div>
</body>
</html>
2. 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%);
}
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;
}
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 交互
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 文件代码,可直接保存为 .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 id="board" class="wrapper">
<div id="currentStatus" class="current-status">
<img id="currentBeastImg" src="./1.gif" alt="unicorn">
<p> 's turn</p>
</div>
<div 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 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>
注意:运行前请确保 1.gif 和 2.gif 图片文件存在于同一目录下。


