跳到主要内容
原生 HTML/CSS/JS 实现网页井字棋游戏 | 极客日志
HTML / CSS 大前端 算法
原生 HTML/CSS/JS 实现网页井字棋游戏 基于 HTML、CSS 和 JavaScript 构建的双人井字棋游戏。实现了棋盘网格布局、鼠标悬停预览效果、回合切换逻辑及胜负判定。包含样式重置、Flexbox/Grid 布局应用以及 DOM 事件监听与状态管理,适合作为前端基础交互练习案例。
前言
在学习完 HTML、CSS 和 JavaScript 后,动手做一个小案例是巩固知识的最佳方式。本文将带你从零开始构建一个双人网页井字棋游戏,涵盖布局、样式交互以及核心逻辑实现。
前置知识准备
在开始之前,确保你熟悉以下基础概念:
HTML : 语义化标签与 DOM 结构搭建
CSS : Flexbox 布局、Grid 网格系统、伪类选择器 (:hover)、过渡动画 (transition)
JavaScript : DOM 操作、事件监听、数组方法 (some, every)、状态管理
1. HTML 骨架搭建
首先构建页面的基本结构。我们需要一个容器包裹整个游戏区域,包括状态显示区、棋盘格子和结果反馈层。
<!DOCTYPE html >
<html lang ="zh-CN" >
<head >
<meta charset ="UTF-8" >
<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 class ="wrapper" >
's turn
play again
<div class ="current-status" >
<img id ="currentBeastImg" src ="./1.gif" alt ="player" >
<p id ="statusText" >
</p >
</div >
<div class ="board" id ="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" id ="gameEndOverlay" >
<div class ="winning-message" data-winning-message >
<p id ="winningMessageText" >
</p >
</div >
<button class ="reset-button" id ="resetButton" >
</button >
</div >
</div >
</body >
</html >
这里我们使用了 data-cell 属性来标记棋盘格子,方便后续通过 JS 批量获取。初始状态下,由于没有内容,部分容器高度可能为 0,接下来我们通过 CSS 进行美化。
2. CSS 样式设计
全局重置与字体 引入自定义字体并清除默认边距,确保不同浏览器下的一致性。
@import url("https://fonts.googleapis.com/css2?family=Bungee+Inline&display=swap" );
* {
padding : 0 ;
margin : 0 ;
box-sizing : inherit;
}
页面布局 使用 Flexbox 将内容垂直水平居中,并设置全屏高度。
body {
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% );
}
棋盘网格 利用 CSS Grid 创建 3x3 的棋盘,并设置单元格间距。
.board {
display : grid;
grid-template-columns : repeat (3 , minmax (90px , 1 fr));
grid-template-rows : repeat (3 , minmax (90px , 1 fr));
grid-gap : 12px ;
width : 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 ;
}
悬停预览效果 这是提升体验的关键点。当鼠标悬停在空白格子上时,根据当前玩家显示对应的图标预览。
.board .unicorn .cell :not (.dragon ):not (.unicorn ):hover ::before {
content : "" ;
width : 70% ;
height : 70% ;
position : absolute;
top : 50% ;
left : 50% ;
transform : translate3d (-50% , -50% , 0 );
background-image : url ("./1.gif" );
background-size : contain;
opacity : 50% ;
}
.board .dragon .cell :not (.dragon ):not (.unicorn ):hover ::before {
content : "" ;
width : 70% ;
height : 70% ;
position : absolute;
top : 50% ;
left : 50% ;
transform : translate3d (-50% , -50% , 0 );
background-image : url ("./2.gif" );
background-size : contain;
opacity : 50% ;
}
按钮与遮罩层 设计重启按钮的交互态(Hover/Active),以及游戏结束时的覆盖层样式。
.reset-button {
color : #f5f5f5 ;
font-family : "Bungee Inline" , cursive;
font-size : 30px ;
border : none;
padding : 10px 20px ;
background-color : #a186be ;
box-shadow : 5px 5px 0 #55acee ;
cursor : pointer;
transition : transform 0.1s ease-in-out;
}
.reset-button :hover {
transform : scale (1.2 );
}
.reset-button :active {
top : 6px ;
left : 6px ;
box-shadow : none;
background-color : #9475b5 ;
}
.game-end-overlay {
display : none;
position : fixed;
top : 0 ; left : 0 ; right : 0 ; bottom : 0 ;
background-color : rgba (13 , 16 , 33 , 0.8 );
}
.game-end-overlay .show {
display : flex;
flex-direction : column;
justify-content : center;
align-items : center;
}
3. JavaScript 交互逻辑
初始化与元素获取 在脚本加载时获取关键 DOM 节点,并定义游戏状态变量。
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 ]
];
核心函数实现 切换回合与状态更新
每次点击后,需要判断是否有效,然后切换当前玩家并更新 UI。
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 handleCellClick = (e ) => {
const cell = e.target ;
const currentBeast = unicornTurn ? 'unicorn' : 'dragon' ;
if (cell.classList .contains ('unicorn' ) || cell.classList .contains ('dragon' )) return ;
cell.classList .add (currentBeast);
if (checkWin (currentBeast)) {
endGame (false );
} else if (isDraw ()) {
endGame (true );
} else {
swapTurns ();
updateCurrentStatus ();
setBoardHoverClass ();
}
};
const endGame = (draw ) => {
if (draw) {
winningMessageText.innerText = 'draw!' ;
} else {
winningMessageImg.src = unicornTurn ? './1.gif' : './2.gif' ;
winningMessage.insertBefore (winningMessageImg, winningMessageText);
winningMessageText.innerText = 'wins!!!' ;
}
gameEndOverlay.classList .add ('show' );
};
const startGame = ( ) => {
cells.forEach (cell => {
cell.classList .remove ('unicorn' , 'dragon' );
cell.removeEventListener ('click' , handleCellClick);
cell.addEventListener ('click' , handleCellClick, { once : false });
});
setBoardHoverClass ();
gameEndOverlay.classList .remove ('show' );
winningMessageText.innerText = '' ;
winningMessageImg.remove ();
};
resetButton.addEventListener ('click' , startGame);
setBoardHoverClass ();
startGame ();
完整代码参考 为了方便大家直接运行,以下是整合后的完整 HTML 文件代码。你可以将其保存为 .html 文件并在浏览器中打开。
<!DOCTYPE html >
<html lang ="zh-CN" >
<head >
<meta charset ="UTF-8" >
<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 , 1 fr)); grid-template-rows : repeat (3 , minmax (90px , 1 fr)); 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" >
<p id ="statusText" > 's turn</p >
</div >
<div class ="board" id ="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" id ="gameEndOverlay" >
<div class ="winning-message" data-winning-message >
<p id ="winningMessageText" > </p >
</div >
<button class ="reset-button" id ="resetButton" > play again</button >
</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 , , ],
[ , , ], [ , , ], [ , , ],
[ , , ], [ , , ]
];
= ( ) => {
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. ( , handleCellClick);
cell. ( , handleCellClick, { : });
});
();
gameEndOverlay. . ( );
winningMessageText. = ;
}
= ( ) => {
(draw) {
winningMessageText. = ;
} {
winningMessageImg. = unicornTurn ? : ;
winningMessageImg. = unicornTurn ? : ;
winningMessage. (winningMessageImg, winningMessageText);
winningMessageText. = ;
}
gameEndOverlay. . ( );
}
= ( ) => {
cell = e. ;
currentBeast = unicornTurn ? : ;
(cell. . ( ) || cell. . ( )) ;
(cell, currentBeast);
( (currentBeast)) {
( );
} ( ()) {
( );
} {
();
();
();
}
}
resetButton. ( , startGame);
();
</script >
</body >
</html >
至此,一个简单的交互式井字棋游戏就完成了。通过这个案例,你可以复习到 DOM 操作、事件委托、条件判断以及基本的游戏循环逻辑。试着修改一下配色或者增加 AI 功能,让它变得更有趣吧!
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'
'dragon'
removeEventListener
'click'
addEventListener
'click'
once
false
setBoardHoverClass
classList
remove
'show'
innerText
''
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'
if
classList
contains
'unicorn'
classList
contains
'dragon'
return
placeBeastImg
if
checkWin
endGame
false
else
if
isDraw
endGame
true
else
swapTurns
updateCurrentStatus
setBoardHoverClass
addEventListener
'click'
startGame
相关免费在线工具 加密/解密文本 使用加密算法(如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