最近感觉脑子有点不够用,手机真不能玩多了,玩多了降智hh。现在感觉不管是刷视频还是打游戏都影响状态,但是有时候又感觉很累想简单放松一下,同时又不想影响状态。
前不久也看了点最强大脑,说实话感觉和最早的几季有点不太像了,最早的几季主要考验记忆能力,最近这几季感觉很多都是玩类似智力测验这种更注重逻辑推理的游戏。有一季看到了何猷君21秒复原了数字华容道,之前我也只是听说过但没玩过,于是认真学习了一下,才了解到华容道原来还被称为智力游戏界“三大不可思议”(另外两个是独立钻石棋和魔方)。经过了解之后,终于学会了华容道这款游戏。现在又觉得自己玩还不够过瘾,还想直接把这款游戏用代码编写出来,于是前一篇文章写了一个网页版的数字华容道,这一篇则是编写了三国华容道,欢迎大家分享交流游戏经验和攻略。
以下是我所编写游戏的详细介绍:
基于HTML+CSS+JS实现离线版华容道游戏
华容道作为中国经典益智游戏,承载着几代人的童年回忆。今天给大家分享一款我开发的离线版华容道游戏,无需后端依赖,打开浏览器即可畅玩,支持多种经典布局、拖拽+按键双操作模式,还加入了主题切换、攻略提示等实用功能。下面详细介绍游戏特性和核心代码实现。
一、游戏核心功能亮点
这款华容道游戏在还原经典玩法的基础上,做了不少体验优化,核心功能包括:
5种经典游戏模式:横刀立马、齐头并进、兵分三路、雨声淅沥、指挥若定,满足不同难度需求双操作方式:支持鼠标拖拽棋子+方向键/虚拟方向按钮控制,操作流畅可视化体验:木质纹理棋子+明暗主题切换,适配不同使用场景游戏数据统计:实时显示游戏时间和移动步数,胜负判定自动化实用辅助功能:棋子ID显示/隐藏、分步攻略提示,新手友好完全离线运行:纯前端实现,无需网络,打开HTML文件即可玩
二、核心技术架构
游戏采用纯前端技术栈开发,无任何框架依赖,结构清晰:
HTML:搭建游戏面板、控制区、提示弹窗等基础结构CSS:实现响应式布局、棋子样式、主题切换、动画效果JavaScript:处理游戏逻辑(棋子移动、碰撞检测、胜利判定等)、交互事件、数据管理
三、关键代码解析
1. 游戏核心数据结构
首先定义游戏状态和配置,统一管理棋盘、棋子、游戏进度等数据:
// 游戏状态和配置
const gameState = {
board: [], // 棋盘网格数据
pieces: [], // 棋子集合
selectedPiece: null, // 当前选中棋子
moves: 0, // 移动步数
startTime: null, // 游戏开始时间
timer: null, // 计时定时器
isPlaying: false, // 游戏是否进行中
showIds: true, // 是否显示棋子ID
isDarkMode: false, // 是否暗色主题
currentMode: 'hengdao', // 当前游戏模式
// 拖动相关状态
dragData: {
isDragging: false,
startX: 0,
startY: 0,
piece: null
}
};
// 棋子配置(尺寸+名称映射)
const pieceConfig = [
{ id: 1, name: "曹操", width: 2, height: 2 }, // 2x2大棋子
{ id: 2, name: "关羽", width: 2, height: 1 }, // 2x1横向棋子
{ id: 3, name: "张飞", width: 1, height: 2 }, // 1x2纵向棋子
{ id: 4, name: "赵云", width: 1, height: 2 },
{ id: 5, name: "马超", width: 1, height: 2 },
{ id: 6, name: "黄忠", width: 1, height: 2 },
{ id: 7, name: "卒", width: 1, height: 1 }, // 1x1小棋子
{ id: 8, name: "卒", width: 1, height: 1 },
{ id: 9, name: "卒", width: 1, height: 1 },
{ id: 10, name: "卒", width: 1, height: 1 }
];
2. 棋盘与棋子渲染
通过网格布局创建棋盘,根据游戏模式初始化棋子位置:
// 创建棋盘(5行4列网格)
function createBoard() {
boardGrid.innerHTML = '';
for (let i = 0; i < 5; i++) {
for (let j = 0; j < 4; j++) {
const cell = document.createElement('div');
cell.className = 'cell';
cell.dataset.row = i;
cell.dataset.col = j;
boardGrid.appendChild(cell);
}
}
}
// 创建棋子并定位
function createPiece(config, row, col) {
const piece = document.createElement('div');
piece.className = `piece piece-${config.width}x${config.height}`;
piece.dataset.id = config.id;
piece.dataset.row = row;
piece.dataset.col = col;
// 添加棋子名称和ID显示
const nameElement = document.createElement('div');
nameElement.className = 'character-name';
nameElement.textContent = config.name;
piece.appendChild(nameElement);
if (gameState.showIds) {
const idElement = document.createElement('div');
idElement.className = 'character-id';
idElement.textContent = config.id;
piece.appendChild(idElement);
}
// 绑定拖动和点击事件
piece.addEventListener('click', selectPiece);
piece.addEventListener('mousedown', startDrag);
// 计算棋子位置(基于单元格大小和间隙)
updatePiecePosition(piece, row, col);
document.getElementById('gameBoard').appendChild(piece);
gameState.pieces.push({
element: piece,
id: config.id,
name: config.name,
row: row,
col: col,
width: config.width,
height: config.height
});
}
3. 核心交互逻辑
(1)拖拽移动实现
支持鼠标拖拽棋子移动,通过事件监听实现拖拽状态管理:
// 开始拖动
function startDrag(e) {
if (!gameState.isPlaying) return;
const pieceElement = e.currentTarget;
const piece = gameState.pieces.find(p => p.element === pieceElement);
if (piece) {
gameState.dragData.isDragging = true;
gameState.dragData.startX = e.clientX;
gameState.dragData.startY = e.clientY;
gameState.dragData.piece = piece;
// 绑定全局拖动和结束事件
document.addEventListener('mousemove', handleDrag);
document.addEventListener('mouseup', endDrag);
e.preventDefault();
}
}
// 处理拖动逻辑
function handleDrag(e) {
if (!gameState.dragData.isDragging) return;
const dx = e.clientX - gameState.dragData.startX;
const dy = e.clientY - gameState.dragData.startY;
const threshold = 20; // 拖动阈值,避免误操作
// 判断拖动方向
if (Math.abs(dx) > threshold || Math.abs(dy) > threshold) {
let direction = Math.abs(dx) > Math.abs(dy)
? (dx > 0 ? 'right' : 'left')
: (dy > 0 ? 'down' : 'up');
// 计算新位置并检查是否可移动
const piece = gameState.dragData.piece;
let newRow = piece.row, newCol = piece.col;
switch(direction) {
case 'up': newRow--; break;
case 'down': newRow++; break;
case 'left': newCol--; break;
case 'right': newCol++; break;
}
if (canMove(piece, newRow, newCol)) {
endDrag();
movePiece(piece, newRow, newCol);
gameState.moves++;
updateDisplay();
checkWinCondition();
}
}
}
(2)移动合法性校验
确保棋子不会超出棋盘边界,也不会与其他棋子重叠:
function canMove(piece, newRow, newCol) {
// 边界校验(棋盘5行4列)
if (newRow < 0 || newCol < 0) return false;
if (newRow + piece.height - 1 >= 5) return false;
if (newCol + piece.width - 1 >= 4) return false;
// 清除原位置数据(临时)
for (let r = piece.row; r < piece.row + piece.height; r++) {
for (let c = piece.col; c < piece.col + piece.width; c++) {
gameState.board[r][c] = 0;
}
}
// 检查新位置是否被占用
let canMove = true;
for (let r = newRow; r < newRow + piece.height; r++) {
for (let c = newCol; c < newCol + piece.width; c++) {
if (gameState.board[r][c] !== 0) {
canMove = false;
break;
}
}
if (!canMove) break;
}
// 恢复原位置数据
for (let r = piece.row; r < piece.row + piece.height; r++) {
for (let c = piece.col; c < piece.col + piece.width; c++) {
gameState.board[r][c] = piece.id;
}
}
return canMove;
}
(3)胜利条件判断
当曹操(2×2棋子)移动到棋盘底部中间位置时,判定游戏胜利:
function checkWinCondition() {
const caoCao = gameState.pieces.find(p => p.id === 1); // 曹操ID为1
if (!caoCao) return;
// 胜利条件:曹操底部到达第4行,且占据中间两列
if (caoCao.row + caoCao.height - 1 === 4 &&
caoCao.col === 1 && caoCao.col + caoCao.width - 1 === 2) {
gameState.isPlaying = false;
clearInterval(gameState.timer);
alert(`恭喜你成功逃脱!
用时: ${timeDisplay.textContent.split(': ')[1]}
移动步数: ${gameState.moves}`);
}
}
4. 主题切换与响应式设计
通过CSS变量实现明暗主题切换,适配移动端和桌面端:
/* 亮色主题变量 */
:root {
--huarong-border-color: #666;
--huarong-item-bg: #f5f5f5;
--huarong-item-text: #333;
/* 更多变量... */
}
/* 暗色主题变量 */
.dark {
--huarong-border-color: #bbb;
--huarong-item-bg: #f5f5f5;
--huarong-item-text: white;
/* 更多变量... */
}
/* 响应式布局 */
@media (max-width: 768px) {
.game-container {
flex-direction: column;
align-items: center;
}
.controls {
width: 100%;
max-width: 320px;
}
}
四、游戏使用说明
打开HTML文件即可启动游戏,无需安装任何插件选择游戏模式(默认横刀立马),点击”开始游戏”按钮开始操作方式:
鼠标拖拽:选中棋子后拖动到目标位置按键控制:选中棋子后,使用方向键或虚拟方向按钮移动 辅助功能:
查看攻略:点击”查看攻略”获取当前模式的最佳移动步骤显示/隐藏ID:点击对应按钮切换棋子ID显示状态主题切换:右上角按钮可切换明暗主题 胜利目标:移动曹操棋子到达棋盘底部中间的出口位置
五、扩展与优化建议
如果想进一步完善这款游戏,可以考虑以下方向:
增加难度分级:根据移动步数和时间设置星级评分自定义布局:支持用户手动摆放棋子,创建自定义关卡历史记录:保存玩家的最佳成绩(最短时间、最少步数)音效反馈:添加棋子移动、胜利等音效,提升沉浸感分享功能:支持将游戏成绩分享到社交平台
游戏界面
游戏界面

模式选择界面:



游戏成功界面:
总的代码(可保存为html文件,直接使用浏览器打开运行)
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>华容道 - 离线版</title>
<style>
:root {
--huarong-border-color: #666;
--huarong-arrow-color: #666;
--huarong-overlay-bg: rgba(255,255,255,0.9);
--huarong-overlay-text: #333;
--huarong-text-shadow: 1px 1px 2px rgba(0,0,0,0.2);
--huarong-button-shadow: 0 4px 8px rgba(0,0,0,0.1);
--huarong-item-bg: #f5f5f5;
--huarong-item-text: #333;
--huarong-item-text-shadow: 1px 1px 2px rgba(255,255,255,0.5);
--huarong-id-bg: rgba(255,255,255,0.9);
--huarong-id-text: #333;
--huarong-time-text: #333;
--huarong-time-text-shadow: 1px 1px 2px rgba(255,255,255,0.5);
--huarong-info-panel-bg: rgba(255,255,255,0.9);
--huarong-info-panel-text: #333;
--huarong-success-overlay-bg: rgba(255,255,255,0.9);
--huarong-success-desc-text: #333;
--huarong-disclaimer-text: #666;
--huarong-fill-border: #999;
--huarong-item-hover-shadow: rgba(0,0,0,0.3);
--huarong-item-disabled-shadow: rgba(100,100,100,0.3);
--huarong-slider-hover-shadow: rgba(0,0,0,0.4);
--huarong-slider-disabled-shadow: rgba(100,100,100,0.3);
--huarong-hint-color: #4CAF50;
}
.dark {
--huarong-border-color: #bbb;
--huarong-arrow-color: #bbb;
--huarong-overlay-bg: rgba(0,0,0,0.8);
--huarong-overlay-text: white;
--huarong-text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
--huarong-button-shadow: 0 4px 8px rgba(0,0,0,0.3);
--huarong-item-bg: #f5f5f5;
--huarong-item-text: white;
--huarong-item-text-shadow: 1px 1px 2px black;
--huarong-id-bg: rgba(0,0,0,0.7);
--huarong-id-text: white;
--huarong-time-text: #ffffff;
--huarong-time-text-shadow: 1px 1px 2px rgba(0,0,0,0.5);
--huarong-info-panel-bg: rgba(0,0,0,0.7);
--huarong-info-panel-text: white;
--huarong-success-overlay-bg: rgba(0,0,0,0.7);
--huarong-success-desc-text: #ffffff;
--huarong-disclaimer-text: #888;
--huarong-fill-border: #ccc;
--huarong-item-hover-shadow: rgba(82,82,82,0.5);
--huarong-item-disabled-shadow: rgba(168,168,168,0.5);
--huarong-slider-hover-shadow: rgba(37,37,37,0.5);
--huarong-slider-disabled-shadow: rgba(168,168,168,0.5);
--huarong-hint-color: #81C784;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: #f5f5f5;
color: #333;
line-height: 1.6;
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
}
.container {
max-width: 1200px;
width: 100%;
display: flex;
flex-direction: column;
gap: 20px;
}
header {
text-align: center;
margin-bottom: 20px;
}
h1 {
font-size: 2.5rem;
margin-bottom: 10px;
color: #333;
}
.description {
font-size: 1.1rem;
max-width: 800px;
margin: 0 auto 20px;
}
.game-container {
display: flex;
flex-wrap: wrap;
gap: 20px;
justify-content: center;
}
.game-board {
position: relative;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
}
.demo1-huarongRoadWrap {
position: relative;
padding: 12px;
width: fit-content;
border: none;
border-radius: 4px;
background-color: #e0e0e0;
margin-bottom: 20px;
}
.demo1-huarongRoadWrap:before,
.demo1-huarongRoadWrap:after {
content: "";
position: absolute;
top: 0;
width: 2px;
height: 100%;
background-color: var(--huarong-border-color,#bbb);
}
.demo1-huarongRoadWrap:before {
left: 0;
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
}
.demo1-huarongRoadWrap:after {
right: 0;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
}
.demo1-huarongRoadWrap .exit-arrow {
position: absolute;
bottom: -15px;
left: 50%;
transform: translateX(-50%);
color: var(--huarong-arrow-color,#bbb);
font-size: 16px;
}
.board-grid {
display: grid;
grid-template-columns: repeat(4, 80px);
grid-template-rows: repeat(5, 80px);
gap: 2px;
width: 326px;
height: 406px;
background-color: #b0b0b0;
padding: 2px;
border-radius: 4px;
}
.cell {
background-color: #d0d0d0;
border-radius: 2px;
}
.piece {
position: absolute;
color: var(--huarong-item-text,white);
text-shadow: var(--huarong-item-text-shadow,1px 1px 2px black);
border-radius: 4px;
box-sizing: border-box;
user-select: none;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-weight: 700;
cursor: grab;
transition: all 0.2s;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
text-align: center;
padding: 5px;
z-index: 2;
}
.piece:active {
cursor: grabbing;
transform: scale(1.02);
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
}
.piece:hover {
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
}
.piece.selected {
box-shadow: 0 0 0 3px #4CAF50;
}
.piece.hint {
box-shadow: 0 0 0 3px var(--huarong-hint-color), 0 0 15px 2px var(--huarong-hint-color);
}
.piece .character-id {
position: absolute;
top: 5px;
left: 5px;
background: var(--huarong-id-bg,rgba(0,0,0,.7));
color: var(--huarong-id-text,white);
border-radius: 50%;
width: 24px;
height: 24px;
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
}
/* 木质棋子样式(替换原有的棋子颜色定义) */
.piece-2x2 {
width: 162px;
height: 162px;
background-color: #deb887;
background-image: url('data:image/svg+xml;utf8,<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg"><filter><feTurbulence type="fractalNoise" baseFrequency="0.65" numOctaves="3" stitchTiles="stitch"/><feColorMatrix type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0.1 0"/></filter><rect width="100" height="100" filter="url(%23noise)" fill="rgba(139,69,19,0.1)"/></svg>');
border: 2px solid #8b4513;
box-shadow: 0 4px 8px rgba(0,0,0,0.2), inset 0 0 30px rgba(139,69,19,0.3);
} /* 曹操(木质) */
.piece-2x1 {
width: 162px;
height: 80px;
background-color: #deb887;
background-image: url('data:image/svg+xml;utf8,<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg"><filter><feTurbulence type="fractalNoise" baseFrequency="0.65" numOctaves="3" stitchTiles="stitch"/><feColorMatrix type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0.1 0"/></filter><rect width="100" height="100" filter="url(%23noise)" fill="rgba(139,69,19,0.1)"/></svg>');
border: 2px solid #8b4513;
box-shadow: 0 4px 8px rgba(0,0,0,0.2), inset 0 0 30px rgba(139,69,19,0.3);
} /* 关羽(木质) */
.piece-1x2 {
width: 80px;
height: 162px;
background-color: #deb887;
background-image: url('data:image/svg+xml;utf8,<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg"><filter><feTurbulence type="fractalNoise" baseFrequency="0.65" numOctaves="3" stitchTiles="stitch"/><feColorMatrix type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0.1 0"/></filter><rect width="100" height="100" filter="url(%23noise)" fill="rgba(139,69,19,0.1)"/></svg>');
border: 2px solid #8b4513;
box-shadow: 0 4px 8px rgba(0,0,0,0.2), inset 0 0 30px rgba(139,69,19,0.3);
} /* 张飞、赵云等(木质) */
.piece-1x1 {
width: 80px;
height: 80px;
background-color: #deb887;
background-image: url('data:image/svg+xml;utf8,<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg"><filter><feTurbulence type="fractalNoise" baseFrequency="0.65" numOctaves="3" stitchTiles="stitch"/><feColorMatrix type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0.1 0"/></filter><rect width="100" height="100" filter="url(%23noise)" fill="rgba(139,69,19,0.1)"/></svg>');
border: 2px solid #8b4513;
box-shadow: 0 4px 8px rgba(0,0,0,0.2), inset 0 0 30px rgba(139,69,19,0.3);
} /* 卒(木质) */
/* 棋子上的文字样式优化 */
.piece .character-name {
font-size: 16px;
font-weight: bold;
color: #5d2e00;
text-shadow: 1px 1px 2px rgba(255,255,255,0.7);
margin-top: 5px;
}
.piece-1x1 .character-name {
font-size: 14px;
}
.piece-2x2 .character-name {
font-size: 20px;
}
.controls {
display: flex;
flex-direction: column;
gap: 15px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
padding: 20px;
width: 300px;
}
.control-group {
margin-bottom: 15px;
}
.control-group h3 {
margin-bottom: 10px;
font-size: 1.2rem;
color: #333;
}
.buttons {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.game-control-button {
padding: 10px 15px;
font-size: 16px;
cursor: pointer;
background: #4CAF50;
color: white;
border: none;
border-radius: 4px;
transition: all 0.3s;
flex: 1;
min-width: 120px;
}
.game-control-button:hover {
background: #45a049;
transform: translateY(-2px);
}
.game-control-button.reset {
background: #f44336;
}
.game-control-button.reset:hover {
background: #d32f2f;
}
.game-control-button.hint {
background: #ff9800;
}
.game-control-button.hint:hover {
background: #f57c00;
}
.direction-controls {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(3, 1fr);
gap: 5px;
width: 150px;
margin: 0 auto;
}
.direction-button {
padding: 10px;
font-size: 18px;
cursor: pointer;
background: #3498db;
color: white;
border: none;
border-radius: 4px;
transition: all 0.3s;
}
.direction-button:hover {
background: #2980b9;
}
.direction-button.up {
grid-column: 2;
grid-row: 1;
}
.direction-button.left {
grid-column: 1;
grid-row: 2;
}
.direction-button.right {
grid-column: 3;
grid-row: 2;
}
.direction-button.down {
grid-column: 2;
grid-row: 3;
}
.time-display {
font-size: 18px;
font-weight: 700;
text-shadow: var(--huarong-time-text-shadow,1px 1px 2px rgba(0,0,0,.5));
margin-bottom: 10px;
text-align: center;
padding: 10px;
border-radius: 4px;
background-color: #f0f0f0;
}
.time-display.warning {
color: #ff5252;
}
.time-display.normal {
color: var(--huarong-time-text,#333);
}
.time-display.paused {
color: #9e9e9e;
font-style: italic;
}
.moves-display {
font-size: 16px;
margin-bottom: 10px;
text-align: center;
}
.theme-toggle {
position: fixed;
top: 20px;
right: 20px;
padding: 10px 15px;
background-color: #333;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
z-index: 100;
}
.dark-mode {
background-color: #1a1a1a;
color: #f0f0f0;
}
.dark-mode .game-board,
.dark-mode .controls,
.dark-mode .game-info {
background-color: #2d2d2d;
color: #f0f0f0;
}
.dark-mode h1,
.dark-mode h2,
.dark-mode h3 {
color: #f0f0f0;
}
.dark-mode .description {
color: #ccc;
}
.dark-mode .time-display {
background-color: #3d3d3d;
}
.hint-panel {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: var(--huarong-overlay-bg);
color: var(--huarong-overlay-text);
padding: 20px;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0,0,0,0.2);
max-width: 500px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
z-index: 1000;
display: none;
}
.hint-panel.active {
display: block;
}
.hint-panel h2 {
margin-bottom: 15px;
color: #4CAF50;
}
.hint-panel .close-btn {
position: absolute;
top: 15px;
right: 15px;
background: none;
border: none;
font-size: 20px;
cursor: pointer;
color: var(--huarong-overlay-text);
}
.hint-steps {
list-style-type: decimal;
padding-left: 20px;
}
.hint-steps li {
margin-bottom: 10px;
padding: 5px;
border-bottom: 1px solid rgba(0,0,0,0.1);
}
.overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5);
z-index: 999;
display: none;
}
.overlay.active {
display: block;
}
.mode-select {
width: 100%;
padding: 10px;
border-radius: 4px;
border: 1px solid #ddd;
background-color: white;
font-size: 16px;
}
.dark-mode .mode-select {
background-color: #333;
color: white;
border-color: #555;
}
@media (max-width: 768px) {
.game-container {
flex-direction: column;
align-items: center;
}
.controls {
width: 100%;
max-width: 320px;
}
}
</style>
</head>
<body>
<button class="theme-toggle" id="themeToggle">切换主题</button>
<div class="container">
<header>
<h1>华容道 - 离线版</h1>
<p class="description">华容道是一种中国古老的益智游戏,你需要通过移动棋子,帮助曹操(最大的棋子)逃出华容道。</p>
</header>
<div class="game-container">
<div class="game-board">
<div class="demo1-huarongRoadWrap" id="gameBoard">
<div class="board-grid" id="boardGrid"></div>
<div class="exit-arrow">▼</div>
</div>
<div class="time-display paused" id="timeDisplay">点击开始游戏</div>
<div class="moves-display" id="movesDisplay">移动步数: 0</div>
</div>
<div class="controls">
<div class="control-group">
<h3>游戏模式</h3>
<select class="mode-select" id="modeSelect">
<option value="hengdao">横刀立马</option>
<option value="qitou">齐头并进</option>
<option value="bingfen">兵分三路</option>
<option value="yusheng">雨声淅沥</option>
<option value="zhihui">指挥若定</option>
</select>
</div>
<div class="control-group">
<h3>游戏控制</h3>
<div class="buttons">
<button class="game-control-button" id="startButton">开始游戏</button>
<button class="game-control-button reset" id="resetButton">重置游戏</button>
</div>
</div>
<div class="control-group">
<h3>方向控制</h3>
<div class="direction-controls">
<button class="direction-button up" data-direction="up">↑</button>
<button class="direction-button left" data-direction="left">←</button>
<button class="direction-button right" data-direction="right">→</button>
<button class="direction-button down" data-direction="down">↓</button>
</div>
</div>
<div class="control-group">
<h3>游戏选项</h3>
<div class="buttons">
<button class="game-control-button hint" id="hintButton">查看攻略</button>
<button class="game-control-button" id="showIdsButton">显示/隐藏ID</button>
</div>
</div>
<div class="control-group">
<h3>棋子ID说明</h3>
<div id="idInfoPanel" class="id-info-panel">
<p>1 - 曹操 (2x2)</p>
<p>2 - 关羽 (2x1)</p>
<p>3-6 - 张飞、赵云、马超、黄忠 (1x2)</p>
<p>7-10 - 卒 (1x1)</p>
</div>
</div>
</div>
</div>
</div>
<!-- 提示面板 -->
<div class="overlay" id="hintOverlay"></div>
<div class="hint-panel" id="hintPanel">
<button class="close-btn" id="closeHint">×</button>
<h2 id="hintTitle">最佳移动步骤</h2>
<ol class="hint-steps" id="hintSteps"></ol>
</div>
<script>
// 游戏状态和配置
const gameState = {
board: [],
pieces: [],
selectedPiece: null,
moves: 0,
startTime: null,
timer: null,
isPlaying: false,
showIds: true,
isDarkMode: false,
currentMode: 'hengdao',
// 拖动相关状态
dragData: {
isDragging: false,
startX: 0,
startY: 0,
piece: null
}
};
// 棋子配置(保持不变)
const pieceConfig = [
{ id: 1, name: "曹操", width: 2, height: 2 }, // 曹操:2x2(占2行2列)
{ id: 2, name: "关羽", width: 2, height: 1 }, // 关羽:2x1(占1行2列)
{ id: 3, name: "张飞", width: 1, height: 2 }, // 张飞:1x2(占2行1列)
{ id: 4, name: "赵云", width: 1, height: 2 }, // 赵云:1x2(占2行1列)
{ id: 5, name: "马超", width: 1, height: 2 }, // 马超:1x2(占2行1列)
{ id: 6, name: "黄忠", width: 1, height: 2 }, // 黄忠:1x2(占2行1列)
{ id: 7, name: "卒", width: 1, height: 1 }, // 卒:1x1(占1行1列)
{ id: 8, name: "卒", width: 1, height: 1 },
{ id: 9, name: "卒", width: 1, height: 1 },
{ id: 10, name: "卒", width: 1, height: 1 }
];
// 各种游戏模式的布局(最终完整修正版)
const gameModes = {
// 横刀立马
hengdao: {
board: [
[3, 1, 1, 5], // 第0行:张飞(3,1x2)、曹操(1,2x2)、曹操、马超(5,1x2)
[3, 1, 1, 5], // 第1行:张飞(3,1x2)、曹操(1,2x2)、曹操、马超(5,1x2)
[4, 2, 2, 6], // 第2行:赵云(4,1x2)、关羽(2,2x1)、关羽、黄忠(6,1x2)
[4, 7, 8, 6], // 第3行:赵云(4,1x2)、卒7、卒8、黄忠(6,1x2)
[9, 0, 0, 10] // 第4行:卒9、空位、空位、卒10
],
steps: [
"1. 卒8右移",
"2. 黄忠下移",
"3. 曹操右移",
"4. 张飞右移",
"5. 张飞下移",
"6. 赵云上移",
"7. 黄忠左移",
"8. 关羽上移",
"9. 卒8左移",
"10. 卒8上移",
"11. 曹操下移",
"12. 曹操左移",
"13. 马超上移",
"14. 卒7左移",
"15. 关羽下移",
"16. 赵云右移",
"17. 赵云下移",
"18. 曹操右移",
"19. 马超左移",
"20. 马超上移",
"21. 卒9右移",
"22. 卒9上移",
"23. 张飞上移",
"24. 张飞左移",
"25. 黄忠上移",
"26. 曹操下移",
"27. 卒8右移",
"28. 卒8下移",
"29. 马超右移",
"30. 马超下移",
"31. 曹操左移",
"32. 关羽上移",
"33. 卒10左移",
"34. 赵云上移",
"35. 曹操右移",
"36. 曹操下移,成功逃脱"
]
},
// 齐头并进
qitou: {
board: [
[6, 1, 1, 5], // 第0行:黄忠(6,1x2)、曹操(1,2x2)、曹操、马超(5,1x2)
[6, 1, 1, 5], // 第1行:黄忠(6,1x2)、曹操(1,2x2)、曹操、马超(5,1x2)
[7, 8, 9, 10], // 第2行:卒7、卒8、卒9、卒10(全为卒)
[3, 2, 2, 4], // 第3行:张飞(3,1x2)、关羽(2,2x1)、关羽、赵云(4,1x2)
[3, 0, 0, 4] // 第4行:张飞(3,1x2)(下半部分)、空位、空位、赵云(4,1x2)(下半部分)
],
steps: [
"1. 卒8右移",
"2. 卒9右移",
"3. 曹操下移(占据第2行中间两列)",
"4. 黄忠下移(占据第3行第0列)",
"5. 马超下移(占据第3行第3列)",
"6. 关羽上移(占据第2行中间两列)",
"7. 卒7、8左移(腾出中间空间)",
"8. 曹操左移(占据第3行第0-1列)",
"9. 张飞上移(占据第1-2行第0列)",
"10. 曹操右移(占据第3行第1-2列)",
"11. 曹操下移(到达第4行中间两列),成功逃脱"
]
},
// 兵分三路
bingfen: {
board: [
[7, 1, 1, 8], // 第0行:卒7、曹操(1,2x2)、曹操、卒8
[6, 1, 1, 5], // 第1行:黄忠(6,1x2)、曹操(1,2x2)、曹操、马超(5,1x2)
[6, 2, 2, 5], // 第2行:黄忠(6,1x2)、关羽(2,2x1)、关羽、马超(5,1x2)
[3, 9, 10, 4], // 第3行:张飞(3,1x2)、卒9、卒10、赵云(4,1x2)
[3, 0, 0, 4] // 第4行:张飞(3,1x2)、空位、空位、赵云(4,1x2)
],
steps: [
"1. 关羽上移",
"2. 卒7右移",
"3. 卒8下移",
"4. 曹操下移",
"5. 张飞右移",
"6. 张飞下移",
"7. 赵云上移",
"8. 黄忠左移",
"9. 关羽上移",
"10. 曹操左移",
"11. 马超上移",
"12. 卒9右移",
"13. 卒9上移",
"14. 曹操下移",
"15. 曹操右移",
"16. 赵云下移",
"17. 黄忠上移",
"18. 曹操下移,成功逃脱"
]
},
// 雨声淅沥
yusheng: {
board: [
[6, 1, 1, 7], // 第0行:黄忠(6,1x2)、曹操(1,2x2)、曹操、卒7
[6, 1, 1, 8], // 第1行:黄忠(6,1x2)、曹操(1,2x2)、曹操、卒8
[5, 2, 2, 4], // 第2行:马超(5,1x2)、关羽(2,2x1)、关羽、赵云(4,1x2)
[5, 3, 0, 4], // 第3行:马超(5,1x2)、张飞(3,1x2)、空位、赵云(4,1x2)
[9, 3, 0, 10] // 第4行:卒9、张飞(3,1x2)、空位、卒10
],
steps: [
"1. 关羽右移",
"2. 卒8右移",
"3. 马超下移",
"4. 曹操下移",
"5. 张飞上移",
"6. 黄忠下移",
"7. 曹操左移",
"8. 关羽左移",
"9. 曹操右移",
"10. 曹操下移,成功逃脱"
]
},
// 指挥若定(匹配最新图示)
zhihui: {
board: [
[6, 1, 1, 5], // 第0行:黄忠(6,1x2)、曹操(1,2x2)、曹操、马超(5,1x2)
[6, 1, 1, 5], // 第1行:黄忠(6,1x2)、曹操(1,2x2)、曹操、马超(5,1x2)
[7, 2, 2, 8], // 第2行:卒7、关羽(2,2x1)、关羽、卒8
[3, 9, 10, 4], // 第3行:张飞(3,1x2)、卒9、卒10、赵云(4,1x2)
[3, 0, 0, 4], // 第4行:张飞(3,1x2)(下半部分)、空位、空位、赵云(4,1x2)(下半部分)
],
steps: [
"1. 卒9右移",
"2. 卒10右移",
"3. 关羽下移(占据第2行中间两列)",
"4. 曹操下移(占据第2行中间两列)",
"5. 黄忠下移(占据第3行第0列)",
"6. 马超下移(占据第3行第3列)",
"7. 卒7右移(占据第2行第0列)",
"8. 卒8左移(占据第2行第3列)",
"9. 曹操左移(占据第3行第0-1列)",
"10. 张飞上移(占据第1-2行第0列)",
"11. 曹操右移(占据第3行第1-2列)",
"12. 关羽上移(占据第2行中间两列)",
"13. 曹操下移(到达第4行中间两列),成功逃脱"
]
}
};
// DOM元素
const boardGrid = document.getElementById('boardGrid');
const timeDisplay = document.getElementById('timeDisplay');
const movesDisplay = document.getElementById('movesDisplay');
const startButton = document.getElementById('startButton');
const resetButton = document.getElementById('resetButton');
const hintButton = document.getElementById('hintButton');
const showIdsButton = document.getElementById('showIdsButton');
const themeToggle = document.getElementById('themeToggle');
const idInfoPanel = document.getElementById('idInfoPanel');
const directionButtons = document.querySelectorAll('.direction-button');
const gameBoard = document.getElementById('gameBoard');
const modeSelect = document.getElementById('modeSelect');
const hintPanel = document.getElementById('hintPanel');
const hintOverlay = document.getElementById('hintOverlay');
const closeHint = document.getElementById('closeHint');
const hintTitle = document.getElementById('hintTitle');
const hintSteps = document.getElementById('hintSteps');
// 初始化游戏
function initGame() {
createBoard();
setGameMode(gameState.currentMode);
updateDisplay();
// 添加事件监听器
document.addEventListener('keydown', handleKeyPress);
startButton.addEventListener('click', startGame);
resetButton.addEventListener('click', resetGame);
hintButton.addEventListener('click', showHintPanel);
closeHint.addEventListener('click', hideHintPanel);
hintOverlay.addEventListener('click', hideHintPanel);
showIdsButton.addEventListener('click', toggleIds);
themeToggle.addEventListener('click', toggleTheme);
modeSelect.addEventListener('change', (e) => {
gameState.currentMode = e.target.value;
resetGame();
});
// 添加方向按钮事件监听
directionButtons.forEach(button => {
button.addEventListener('click', () => {
if (!gameState.isPlaying) return;
const direction = button.dataset.direction;
handleDirectionMove(direction);
});
});
}
// 设置游戏模式
function setGameMode(mode) {
const selectedMode = gameModes[mode];
if (!selectedMode) return;
gameState.board = JSON.parse(JSON.stringify(selectedMode.board));
createPieces();
updateHintPanel(mode);
}
// 更新提示面板内容
function updateHintPanel(mode) {
const selectedMode = gameModes[mode];
if (!selectedMode) return;
// 更新标题
const modeNames = {
'hengdao': '横刀立马',
'qitou': '齐头并进',
'bingfen': '兵分三路',
'yusheng': '雨声淅沥',
'zhihui': '指挥若定'
};
hintTitle.textContent = `${modeNames[mode]} - 最佳移动步骤`;
// 更新步骤
hintSteps.innerHTML = '';
selectedMode.steps.forEach(step => {
const li = document.createElement('li');
li.textContent = step;
hintSteps.appendChild(li);
});
}
// 显示提示面板
function showHintPanel() {
if (!gameState.isPlaying) {
alert('请先点击"开始游戏"');
return;
}
hintPanel.classList.add('active');
hintOverlay.classList.add('active');
}
// 隐藏提示面板
function hideHintPanel() {
hintPanel.classList.remove('active');
hintOverlay.classList.remove('active');
}
// 创建棋盘
function createBoard() {
boardGrid.innerHTML = '';
for (let i = 0; i < 5; i++) {
for (let j = 0; j < 4; j++) {
const cell = document.createElement('div');
cell.className = 'cell';
cell.dataset.row = i;
cell.dataset.col = j;
boardGrid.appendChild(cell);
}
}
}
// 创建棋子
function createPieces() {
gameState.pieces = [];
// 清空棋盘上的所有棋子
document.querySelectorAll('.piece').forEach(piece => piece.remove());
// 创建棋子并放置在初始位置
for (let row = 0; row < 5; row++) {
for (let col = 0; col < 4; col++) {
const pieceId = gameState.board[row][col];
if (pieceId !== 0) {
const pieceConfigItem = pieceConfig.find(p => p.id === pieceId);
// 确保每个棋子只创建一次
if (pieceConfigItem && !gameState.pieces.find(p => p.id === pieceId)) {
createPiece(pieceConfigItem, row, col);
}
}
}
}
}
// 创建单个棋子(添加拖动事件)
function createPiece(config, row, col) {
const piece = document.createElement('div');
piece.className = `piece piece-${config.width}x${config.height}`;
piece.dataset.id = config.id;
piece.dataset.row = row;
piece.dataset.col = col;
piece.dataset.width = config.width;
piece.dataset.height = config.height;
// 添加棋子名称
const nameElement = document.createElement('div');
nameElement.className = 'character-name';
nameElement.textContent = config.name;
piece.appendChild(nameElement);
if (gameState.showIds) {
const idElement = document.createElement('div');
idElement.className = 'character-id';
idElement.textContent = config.id;
piece.appendChild(idElement);
}
// 点击选中棋子
piece.addEventListener('click', (e) => {
if (!gameState.isPlaying) return;
// 拖动时不触发点击选中
if (!gameState.dragData.isDragging) {
selectPiece(piece);
}
e.stopPropagation();
});
// 拖动相关事件
piece.addEventListener('mousedown', startDrag);
// 计算位置
updatePiecePosition(piece, row, col);
document.getElementById('gameBoard').appendChild(piece);
gameState.pieces.push({
element: piece,
id: config.id,
name: config.name,
row: row,
col: col,
width: config.width,
height: config.height
});
}
// 开始拖动
function startDrag(e) {
if (!gameState.isPlaying) return;
// 只处理左键拖动
if (e.button !== 0) return;
const pieceElement = e.currentTarget;
const piece = gameState.pieces.find(p => p.element === pieceElement);
if (piece) {
gameState.dragData.isDragging = true;
gameState.dragData.startX = e.clientX;
gameState.dragData.startY = e.clientY;
gameState.dragData.piece = piece;
// 选中当前棋子
selectPiece(pieceElement);
// 添加全局事件监听
document.addEventListener('mousemove', handleDrag);
document.addEventListener('mouseup', endDrag);
// 防止拖动时选中文本
e.preventDefault();
}
}
// 处理拖动
function handleDrag(e) {
if (!gameState.dragData.isDragging) return;
const dx = e.clientX - gameState.dragData.startX;
const dy = e.clientY - gameState.dragData.startY;
const threshold = 20; // 拖动阈值,避免误操作
// 根据拖动方向判断移动方向
if (Math.abs(dx) > threshold || Math.abs(dy) > threshold) {
let direction;
if (Math.abs(dx) > Math.abs(dy)) {
direction = dx > 0 ? 'right' : 'left';
} else {
direction = dy > 0 ? 'down' : 'up';
}
// 尝试移动
const piece = gameState.dragData.piece;
let newRow = piece.row;
let newCol = piece.col;
switch(direction) {
case 'up':
newRow--;
break;
case 'down':
newRow++;
break;
case 'left':
newCol--;
break;
case 'right':
newCol++;
break;
}
if (canMove(piece, newRow, newCol)) {
// 结束拖动并移动棋子
endDrag();
movePiece(piece, newRow, newCol);
gameState.moves++;
updateDisplay();
checkWinCondition();
}
}
}
// 结束拖动
function endDrag() {
gameState.dragData.isDragging = false;
gameState.dragData.piece = null;
// 移除全局事件监听
document.removeEventListener('mousemove', handleDrag);
document.removeEventListener('mouseup', endDrag);
}
// 更新棋子位置
function updatePiecePosition(piece, row, col) {
const cellSize = 80; // 单元格大小
const gap = 2; // 间隙大小
const boardPadding = 12;// 棋盘内边距
piece.style.left = `${col * (cellSize + gap) + boardPadding}px`;
piece.style.top = `${row * (cellSize + gap) + boardPadding}px`;
}
// 选择棋子
function selectPiece(pieceElement) {
// 取消之前选中的棋子
if (gameState.selectedPiece) {
gameState.selectedPiece.element.classList.remove('selected');
}
// 选中新棋子
gameState.selectedPiece = gameState.pieces.find(p => p.element === pieceElement);
if (gameState.selectedPiece) {
pieceElement.classList.add('selected');
}
}
// 开始游戏
function startGame() {
if (gameState.isPlaying) return;
gameState.isPlaying = true;
startButton.textContent = "游戏中...";
startButton.disabled = true;
timeDisplay.className = 'time-display normal';
// 开始计时
gameState.startTime = new Date();
gameState.timer = setInterval(updateTimer, 1000);
}
// 处理键盘按键
function handleKeyPress(event) {
if (!gameState.isPlaying || !gameState.selectedPiece) return;
let direction;
switch(event.key) {
case 'ArrowUp':
direction = 'up';
break;
case 'ArrowDown':
direction = 'down';
break;
case 'ArrowLeft':
direction = 'left';
break;
case 'ArrowRight':
direction = 'right';
break;
default:
return;
}
handleDirectionMove(direction);
}
// 处理方向移动
function handleDirectionMove(direction) {
if (!gameState.selectedPiece) return;
const piece = gameState.selectedPiece;
let newRow = piece.row;
let newCol = piece.col;
switch(direction) {
case 'up':
newRow--;
break;
case 'down':
newRow++;
break;
case 'left':
newCol--;
break;
case 'right':
newCol++;
break;
}
if (canMove(piece, newRow, newCol)) {
movePiece(piece, newRow, newCol);
gameState.moves++;
updateDisplay();
checkWinCondition();
}
}
// 检查是否可以移动
function canMove(piece, newRow, newCol) {
// 检查边界
if (newRow < 0 || newCol < 0) return false;
if (newRow + piece.height - 1 >= 5) return false; // 棋盘共5行
if (newCol + piece.width - 1 >= 4) return false; // 棋盘共4列
// 清除原位置
for (let r = piece.row; r < piece.row + piece.height; r++) {
for (let c = piece.col; c < piece.col + piece.width; c++) {
gameState.board[r][c] = 0;
}
}
// 检查新位置是否有其他棋子
let canMove = true;
for (let r = newRow; r < newRow + piece.height; r++) {
for (let c = newCol; c < newCol + piece.width; c++) {
if (gameState.board[r][c] !== 0) {
canMove = false;
break;
}
}
if (!canMove) break;
}
// 恢复原位置
for (let r = piece.row; r < piece.row + piece.height; r++) {
for (let c = piece.col; c < piece.col + piece.width; c++) {
gameState.board[r][c] = piece.id;
}
}
return canMove;
}
// 移动棋子
function movePiece(piece, newRow, newCol) {
// 清除原位置
for (let r = piece.row; r < piece.row + piece.height; r++) {
for (let c = piece.col; c < piece.col + piece.width; c++) {
gameState.board[r][c] = 0;
}
}
// 更新棋子位置
piece.row = newRow;
piece.col = newCol;
piece.element.dataset.row = newRow;
piece.element.dataset.col = newCol;
// 更新新位置
for (let r = newRow; r < newRow + piece.height; r++) {
for (let c = newCol; c < newCol + piece.width; c++) {
gameState.board[r][c] = piece.id;
}
}
// 更新DOM位置
updatePiecePosition(piece.element, newRow, newCol);
}
// 更新计时器
function updateTimer() {
if (!gameState.isPlaying) return;
const now = new Date();
const elapsedSeconds = Math.floor((now - gameState.startTime) / 1000);
const minutes = Math.floor(elapsedSeconds / 60).toString().padStart(2, '0');
const seconds = (elapsedSeconds % 60).toString().padStart(2, '0');
timeDisplay.textContent = `时间: ${minutes}:${seconds}`;
// 超过5分钟显示警告色
if (elapsedSeconds > 300) {
timeDisplay.className = 'time-display warning';
}
}
// 重置游戏
function resetGame() {
// 清除计时器
clearInterval(gameState.timer);
// 重置游戏状态
gameState.isPlaying = false;
gameState.moves = 0;
gameState.selectedPiece = null;
gameState.startTime = null;
gameState.timer = null;
// 重新初始化棋盘
setGameMode(gameState.currentMode);
// 更新显示
startButton.textContent = "开始游戏";
startButton.disabled = false;
timeDisplay.textContent = "点击开始游戏";
timeDisplay.className = 'time-display paused';
updateDisplay();
}
// 检查胜利条件(曹操逃出)
function checkWinCondition() {
const caoCao = gameState.pieces.find(p => p.id === 1); // 曹操的ID是1
if (!caoCao) return;
// 曹操(2x2)的底部边缘到达第4行(最后一行),且位置在中间两列
if (caoCao.row + caoCao.height - 1 === 4 &&
caoCao.col === 1 && caoCao.col + caoCao.width - 1 === 2) {
gameState.isPlaying = false;
clearInterval(gameState.timer);
alert(`恭喜你成功逃脱!
用时: ${timeDisplay.textContent.split(': ')[1]}
移动步数: ${gameState.moves}`);
}
}
// 切换ID显示
function toggleIds() {
gameState.showIds = !gameState.showIds;
document.querySelectorAll('.piece .character-id').forEach(el => {
el.style.display = gameState.showIds ? 'flex' : 'none';
});
}
// 切换主题
function toggleTheme() {
gameState.isDarkMode = !gameState.isDarkMode;
document.body.classList.toggle('dark-mode', gameState.isDarkMode);
document.body.classList.toggle('dark', gameState.isDarkMode);
themeToggle.textContent = gameState.isDarkMode ? "切换为亮色模式" : "切换为暗色模式";
}
// 更新显示
function updateDisplay() {
movesDisplay.textContent = `移动步数: ${gameState.moves}`;
}
// 初始化游戏
window.addEventListener('DOMContentLoaded', initGame);
</script>
</body>
</html>


