在AI工具辅助下完善了网页版数字华容道游戏,可直接下载,离线使用,祝使用愉快!
界面如下:

成功界面:

自制数字华容道游戏 | 支持多规格+自定义序列+无解判断
作为经典的益智游戏,数字华容道不仅能锻炼逻辑思维,还能打发碎片时间。这次我基于HTML/CSS/JavaScript自制了一款功能完备的数字华容道,支持多尺寸棋盘、自定义序列、无解判断等实用功能,完全适配PC和移动端,分享给大家一起交流学习~
核心功能
- 多规格棋盘自由切换:支持3×3到10×10共8种尺寸,从新手到大神难度全覆盖
- 实时数据统计:自动记录游戏步数和耗时,清晰展示通关效率
- 自定义序列功能:可手动输入数字序列,满足个性化游戏需求
- 逆序数无解判断:通过算法自动识别无解布局,避免无效尝试
- 响应式设计:PC端、手机端自适应显示,布局美观不凌乱
- 流畅交互体验:方块移动有过渡动画,按钮hover反馈明显,操作手感舒适
核心实现解析
1. 棋盘生成与自适应布局
通过动态创建表格实现不同尺寸棋盘,根据棋盘大小自动调整单元格尺寸,保证显示效果一致。
// 创建棋盘核心代码
function createBoard() {
let tableHTML = '<table>';
// 循环生成行和列
for (let i = 0; i < boardSize; i++) {
tableHTML += '<tr>';
for (let j = 0; j < boardSize; j++) {
tableHTML += '<td></td>';
}
tableHTML += '</tr>';
}
tableHTML += '</table>';
boardContainer.innerHTML = tableHTML;
// 自适应单元格大小
const cellSize = Math.min(400 / boardSize, 80);
document.querySelectorAll('td').forEach(td => {
td.style.width = `${cellSize}px`;
td.style.height = `${cellSize}px`;
if (boardSize > 6) td.style.fontSize = `${cellSize / 4}px`; // 大棋盘缩小字体
});
}
2. 逆序数无解判断算法
这是游戏的核心亮点之一,通过计算数字序列的逆序数判断布局是否有解,避免玩家陷入无解困境。
// 计算逆序数总和
const countInversions = (arr) => {
let filteredArr = arr.filter(item => item !== null); // 忽略空位
let inversions = 0;
// 双重循环统计逆序对
for (let i = 0; i < filteredArr.length - 1; i++) {
for (let j = i + 1; j < filteredArr.length; j++) {
if (filteredArr[i] > filteredArr[j]) inversions++;
}
}
return inversions;
};
// 布局有效性判断(逆序数为偶数则有解)
const total = countInversions(series);
if (total % 2 !== 0) {
desc.style.visibility = 'visible'; // 显示无解提示
}
3. 方块移动逻辑与通关检测
通过监听单元格点击事件,判断点击方块是否与空位相邻,仅允许上下左右相邻移动,同时实时检测是否通关。
// 移动逻辑核心代码
boardContainer.addEventListener('click', (event) => {
const target = event.target;
if (target.nodeName !== 'TD') return;
const current = document.querySelector('.current'); // 空位元素
const currentIndex = Array.from(document.querySelectorAll('td')).indexOf(current);
const targetIndex = Array.from(document.querySelectorAll('td')).indexOf(target);
// 计算行列位置,判断是否相邻
const currentRow = Math.floor(currentIndex / boardSize);
const currentCol = currentIndex % boardSize;
const targetRow = Math.floor(targetIndex / boardSize);
const targetCol = targetIndex % boardSize;
const isAdjacent =
(Math.abs(currentRow - targetRow) === 1 && currentCol === targetCol) ||
(Math.abs(currentCol - targetCol) === 1 && currentRow === targetRow);
if (isAdjacent) {
// 交换方块与空位内容
[target.innerText, current.innerText] = [current.innerText, target.innerText];
[target.className, current.className] = [current.className, target.className];
// 更新步数
step.innerText = parseInt(step.innerText) + 1;
startTimer(); // 启动计时器(首次移动触发)
check(); // 检测是否通关
}
});
4. 计时器与数据统计
实现精准计时功能,记录游戏开始到通关的耗时,配合步数统计,让玩家清晰了解自己的游戏表现。
// 计时器核心逻辑
function startTimer() {
if (!isGameActive) {
isGameActive = true;
timerInterval = setInterval(() => {
elapsedSeconds++;
updateTimerDisplay(); // 更新时间显示
}, 1000);
}
}
// 时间格式化显示(00:00格式)
function updateTimerDisplay() {
const minutes = Math.floor(elapsedSeconds / 60);
const seconds = elapsedSeconds % 60;
timerDisplay.textContent = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
}
游戏使用说明
- 选择棋盘尺寸:通过下拉框选择3×3到10×10的棋盘规格,默认3×3
- 开始游戏:点击"重开"按钮生成随机布局,点击相邻方块移动到空位
- 自定义序列:在输入框中按格式输入数字(如1,2,3,4,5,6,7,8),点击"确定"生成自定义布局
- 通关条件:将所有数字按1、2、3…的顺序排列,空位在最后一格即可通关
- 规则限制:只能移动与空位相邻的方块,不可对角线移动或跳格
完整代码
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>数字华容道</title>
<style>
h1 {
text-align: center;
color: #2c3e50;
}
.box {
border: 1px solid #cfcfcf;
margin: 0 auto;
max-width: 900px;
padding: 20px;
border-radius: 20px;
display: flex;
flex-wrap: wrap;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
background-color: #f9f9f9;
}
.game-container {
flex: 1;
min-width: 300px;
padding: 10px;
}
.info-container {
flex: 1;
min-width: 300px;
padding: 10px;
}
.fun {
display: flex;
justify-content: space-between;
margin: 10px 0;
flex-wrap: wrap;
}
.fun-item {
margin: 5px 10px;
}
table {
border-collapse: collapse;
margin: 20px auto;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
td {
width: 60px;
height: 60px;
text-align: center;
background-color: #f1c385;
user-select: none;
border: 1px solid #ccc;
font-size: 18px;
font-weight: bold;
transition: all 0.3s;
cursor: pointer;
border-radius: 5px;
}
.current {
background-color: #fff !important;
}
#error {
color: #e74c3c;
font-weight: bold;
}
#timer {
font-size: 18px;
font-weight: bold;
color: #2980b9;
}
#size-selector {
margin: 10px 0;
padding: 5px;
border-radius: 5px;
border: 1px solid #ccc;
}
.size-label {
font-weight: bold;
margin-right: 10px;
}
button {
padding: 5px 10px;
border-radius: 5px;
border: 1px solid #3498db;
background-color: #3498db;
color: white;
cursor: pointer;
transition: background-color 0.3s;
}
button:hover {
background-color: #2980b9;
}
input {
padding: 5px;
border-radius: 5px;
border: 1px solid #ccc;
}
.stats {
display: flex;
justify-content: space-around;
background-color: #ecf0f1;
padding: 10px;
border-radius: 10px;
margin: 10px 0;
}
.stat-item {
text-align: center;
}
.stat-value {
font-size: 20px;
font-weight: bold;
color: #2c3e50;
}
.stat-label {
font-size: 14px;
color: #7f8c8d;
}
@media (max-width: 768px) {
.box {
flex-direction: column;
}
.fun {
flex-direction: column;
align-items: center;
}
.fun-item {
margin: 5px 0;
}
}
</style>
</head>
<body>
<div class="box">
<div class="game-container">
<h1>数字华容道</h1>
<p><strong>规则:</strong>移动方块使数字按顺序排列即可通关!不能对角线移动,不能跳格子移动,只能相邻上下或左右移动。</p>
<hr />
<div class="stats">
<div class="stat-item">
<div class="stat-value" id="num">0</div>
<div class="stat-label">步数</div>
</div>
<div class="stat-item">
<div class="stat-value" id="timer">00:00</div>
<div class="stat-label">时间</div>
</div>
</div>
<div class="fun">
<div class="fun-item">
<span class="size-label">棋盘大小:</span>
<select id="size-selector">
<option value="3">3×3</option>
<option value="4">4×4</option>
<option value="5">5×5</option>
<option value="6">6×6</option>
<option value="7">7×7</option>
<option value="8">8×8</option>
<option value="9">9×9</option>
<option value="10">10×10</option>
</select>
</div>
<div class="fun-item">
<button id="reset">重开</button>
</div>
</div>
<hr />
<div class="fun">
<div class="fun-item">
<label>指定序列:</label>
<input type="text" id="custom-sequence" placeholder="例如:1,2,3,4,5,6,7,8,9,10,11,12,13,14,15" />
</div>
<div class="fun-item">
<button id="confirm">确定</button>
</div>
</div>
<hr />
<div id="board-container"></div>
<div id="error" style="text-align: center; margin-top: 10px;"></div>
</div>
<div class="info-container">
<p>
<strong>逆序数:</strong>
是指,在一个数列中,任取两个数字,如果前面的数字大于后面的数字,则这两个数字形成一个逆序。在数字华容道中,忽略空位,将盘面上的数字从上到下、从左到右排列成一个序列,然后计算这个序列的逆序数总和。如果逆序数的总和是偶数,那么这个布局是有解的;如果是奇数,则这个布局是无解的。
</p>
<p>
<strong>例如:</strong>如果一个布局的数字序列(空格忽略不计)是12345678,那么其逆序数为0(因为它已经是顺序排列),这是一个有解的布局。如果布局是12345687,其逆序数为1(因为只有数字8和7是逆序的),所以这个布局是无解的。
</p>
<div id="desc" style="color: red; visibility: hidden;">
逆序数为"奇数",此局无解,建议重开<br>
[1,2,3]<br>
[4,5,6]<br>
[8,7, ]<br>
</div>
</div>
</div>
<script>
// 获取DOM元素
const step = document.getElementById('num');
const error = document.getElementById('error');
const desc = document.getElementById('desc');
const input = document.getElementById('custom-sequence');
const boardContainer = document.getElementById('board-container');
const sizeSelector = document.getElementById('size-selector');
const timerDisplay = document.getElementById('timer');
// 游戏状态
let boardSize = 3; // 默认3x3
let seed = []; // 数字序列
let custom_seed = []; // 自定义序列
let timerInterval = null;
let elapsedSeconds = 0;
let isGameActive = false;
// 初始化游戏
function initGame() {
boardSize = parseInt(sizeSelector.value);
createBoard();
initSeed();
updateBoard();
resetTimer();
}
// 创建棋盘
function createBoard() {
let tableHTML = '<table>';
for (let i = 0; i < boardSize; i++) {
tableHTML += '<tr>';
for (let j = 0; j < boardSize; j++) {
tableHTML += '<td></td>';
}
tableHTML += '</tr>';
}
tableHTML += '</table>';
boardContainer.innerHTML = tableHTML;
// 设置单元格大小
const cellSize = Math.min(400 / boardSize, 80);
document.querySelectorAll('td').forEach(td => {
td.style.width = `${cellSize}px`;
td.style.height = `${cellSize}px`;
if (boardSize > 6) {
td.style.fontSize = `${cellSize / 4}px`;
}
});
}
// 初始化数字序列
function initSeed() {
seed = [];
const totalCells = boardSize * boardSize;
for (let i = 1; i < totalCells; i++) {
seed.push(i);
}
seed.push(null); // 最后一个为空
}
// 随机数组
const shuffle = (array) => {
// 先过滤掉null值
let filteredArray = array.filter(item => item !== null);
// 打乱非空部分
for (let i = filteredArray.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[filteredArray[i], filteredArray[j]] = [filteredArray[j], filteredArray[i]];
}
// 将打乱后的数组和null重新组合
let result = [...filteredArray, null];
return result;
}
// 计算逆序数总和
const countInversions = (arr) => {
// 过滤掉空值
let filteredArr = arr.filter(item => item !== null);
let inversions = 0;
for (let i = 0; i < filteredArr.length - 1; i++) {
for (let j = i + 1; j < filteredArr.length; j++) {
if (filteredArr[i] > filteredArr[j]) {
inversions++;
}
}
}
return inversions;
}
// 检查结果
const check = () => {
const tds = document.querySelectorAll('td');
let flag = true;
for (let i = 0; i < tds.length - 1; i++) {
const expectedValue = i + 1;
const actualValue = parseInt(tds[i].innerText);
if (actualValue !== expectedValue) {
flag = false;
break;
}
}
// 检查最后一个是否为空
if (tds[tds.length - 1].innerText !== '') {
flag = false;
}
if (flag) {
error.innerText = '恭喜你通关啦!👌';
stopTimer();
}
return flag;
}
// 计时器函数
function startTimer() {
if (!isGameActive) {
isGameActive = true;
timerInterval = setInterval(() => {
elapsedSeconds++;
updateTimerDisplay();
}, 1000);
}
}
function stopTimer() {
isGameActive = false;
if (timerInterval) {
clearInterval(timerInterval);
timerInterval = null;
}
}
function resetTimer() {
stopTimer();
elapsedSeconds = 0;
updateTimerDisplay();
}
function updateTimerDisplay() {
const minutes = Math.floor(elapsedSeconds / 60);
const seconds = elapsedSeconds % 60;
timerDisplay.textContent = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
}
// 更新棋盘数据
const updateBoard = () => {
desc.style.visibility = 'hidden';
const data = custom_seed.length ? custom_seed : shuffle([...seed]);
const tds = document.querySelectorAll('td');
// 将数据填充到表格
for (let i = 0; i < tds.length; i++) {
if (data[i] === null) {
tds[i].className = 'current';
tds[i].innerText = '';
} else {
tds[i].className = '';
tds[i].innerText = data[i];
}
}
error.innerText = '';
step.innerText = 0;
resetTimer();
// 计算逆序数,检查是否有解
const series = data.filter(item => item !== null);
const total = countInversions(series);
if (total % 2 !== 0) {
desc.style.visibility = 'visible';
}
custom_seed = []; // 清空自定义序列
}
// 初始化游戏
initGame();
// 重开游戏
document.getElementById('reset').addEventListener('click', () => {
input.value = '';
updateBoard();
});
// 自定义序列
document.getElementById('confirm').addEventListener('click', () => {
const value = input.value;
if (value) {
const inputArray = value.split(',').map(item => {
const num = parseInt(item.trim());
return isNaN(num) ? null : num;
});
// 验证输入
const totalCells = boardSize * boardSize;
const expectedNumbers = Array.from({length: totalCells - 1}, (_, i) => i + 1);
const inputNumbers = inputArray.filter(item => item !== null);
const sortedInput = [...inputNumbers].sort((a, b) => a - b);
// 检查是否包含所有必要的数字
if (JSON.stringify(sortedInput) !== JSON.stringify(expectedNumbers) || inputArray.length !== totalCells) {
alert(`指定数列错误,请输入${totalCells-1}个不重复的数字(1-${totalCells-1}),用英文逗号分隔`);
return;
}
custom_seed = inputArray;
updateBoard();
}
});
// 监听棋盘大小变化
sizeSelector.addEventListener('change', () => {
input.value = '';
initGame();
});
// 监听点击事件,移动方块处理
boardContainer.addEventListener('click', (event) => {
const target = event.target;
if (target.nodeName !== 'TD') return;
const current = document.querySelector('.current');
if (target === current) return;
// 获取当前空白格和点击格的位置
const currentIndex = Array.from(document.querySelectorAll('td')).indexOf(current);
const targetIndex = Array.from(document.querySelectorAll('td')).indexOf(target);
const currentRow = Math.floor(currentIndex / boardSize);
const currentCol = currentIndex % boardSize;
const targetRow = Math.floor(targetIndex / boardSize);
const targetCol = targetIndex % boardSize;
// 检查是否相邻
const isAdjacent =
(Math.abs(currentRow - targetRow) === 1 && currentCol === targetCol) ||
(Math.abs(currentCol - targetCol) === 1 && currentRow === targetRow);
if (isAdjacent) {
// 启动计时器(如果尚未启动)
startTimer();
// 交换内容
const targetText = target.innerText;
target.innerText = '';
target.className = 'current';
current.innerText = targetText;
current.className = '';
// 更新步骤
let num = parseInt(step.innerText) || 0;
num++;
step.innerText = num;
error.innerText = '';
check();
} else {
error.innerText = '只能移动相邻的方块!';
}
});
</script>
</body>
</html>
扩展
这款数字华容道已经实现了核心玩法和实用功能,代码结构清晰,易于理解和修改。后续可以考虑这些扩展方向:
- 增加排行榜功能:记录不同尺寸棋盘的最快通关时间和最少步数
- 自定义主题:支持切换棋盘颜色、方块样式等个性化设置
- 难度分级:根据棋盘尺寸和打乱程度划分难度等级
- 移动动画优化:添加更流畅的方块移动过渡效果
8万+

被折叠的 条评论
为什么被折叠?



