一、游戏介绍
1、历史背景
数字华容道脱胎于传统华容道,后者源自三国时期"曹操败走华容道"的故事。传统玩法是通过移动不同形状的木块,帮助"曹操"从出口逃脱。而数字华容道将棋子替换为数字,目标是通过滑动方块,将乱序的数字按1至N的顺序排列整齐(例如4×4棋盘需排列1-15,留一空格作为移动空间)。
2、基本规则
- 棋盘分为3×3、4×4、5×5等不同难度等级,数字范围随棋盘大小递增。
- 每次只能移动与空格相邻的数字方块,通过滑动调整顺序,最终达成从左到右、从上到下的数字连贯性
二、代码生成游戏盘面
1、实现思路:
1.按游戏难度生成一个目标数字盘面;
2.随机打乱整个盘面的数字;
3.判断盘面可解性,无解则重新生成直到有解为止。
2、实现代码:
TypeScript
/** 难度:3x3、4x4、5x5 */
const level = ref(4);
/** 数字盘面 */
const list = ref([]);
/**
* 随机打乱数组
* @param {number[]} array 要打乱的数组
*/
function shuffleArray(array: number[]) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
/**
* 判断数字华容道的可解性
* @param {number[]} array 数字盘面
* @param {number} row 盘面行数
*/
function isSolvable(array: number[], row: number = 4) {
// 去掉空格(0)
const puzzles = array.filter(item => item > 0);
// 逆序数:例如,在序列 [2, 3, 1] 中,2 和 1 构成一个逆序对,3 和 1 也构成一个逆序对。
let n = 0;
for (var i = 0; i < puzzles.length; i++) {
for (var j = i + 1; j < puzzles.length; j++) {
if (puzzles[i] > puzzles[j]) {
n++;
}
}
}
/**
* 若格子列数为奇数,则逆序数必须为偶数;
* 若格子列数为偶数,且逆序数为偶数,则当前空格所在行数与初始空格所在行数的差为偶数;
* 若格子列数为偶数,且逆序数为奇数,则当前空格所在行数与初始空格所在行数的差为奇数
*/
if (level.value % 2 !== 0) return n % 2 === 0;
// 空格所在的行
let emptyRow = Math.ceil((array.findIndex(item => item === 0) + 1) / row);
return emptyRow % 2 === 0 ? n % 2 === 0 : n % 2 !== 0;
}
/**
* 创建游戏
* @param {number} n 游戏难度
*/
function createGame(n?: number) {
level.value = n || level.value;
const ls = [];
for (var i = 0; i < level.value * level.value; i++) {
ls.push(i);
}
const array = shuffleArray(ls);
if (isSolvable(array, level.value)) { // 是否可解
list.value = array;
} else {
createGame();
}
}
createGame();
三、简单开发游戏界面
1.呈现游戏盘面;
2.增加计步、计时、难度选择、暂停/继续、重新开始。
TypeScript
<template>
<div class="page-box">
<div class="game-box" :style="{
gridTemplateColumns: `repeat(${level}, 1fr)`,
width: `calc(${level}00px + ${level - 1}0px)`,
}">
<template v-for="(n, i) in list" :key="n">
<div v-if="n === 0" :class="{ 'is-active': i === first }" @click="onItemClick(i)"></div>
<div v-else class="game-item" :class="{ 'is-active': i === first }" @click="onItemClick(i)">{{ n }}</div>
</template>
</div>
<div style="display: flex;justify-content: space-between;width: 50%;">
<div>步数:{{ step }}</div>
<div>耗时:{{ timeFormat }}</div>
</div>
<div style="display: flex;justify-content: space-between;width: 50%;">
<div>难度:</div>
<div @click="createGame(3)">3x3</div>
<div @click="createGame(4)">4x4</div>
<div @click="createGame(5)">5x5</div>
</div>
<div style="display: flex;justify-content: space-between;width: 50%;">
<button @click="createGame()">重新开始</button>
<button @click="changeStatus">{{isStop ? '继续' : '暂停' }}</button>
</div>
</div>
</template>
<style lang="scss">
.page-box {
display: flex;
flex-direction: column;
align-items: center;
zoom: 20%;
gap: 20px;
}
.game-box {
display: grid;
gap: 5px;
padding: 10px;
background: #d7d8db;
border-radius: 10px;
.game-item {
border: 1px solid #000;
border-radius: 10px;
height: 100px;
background-color: #fff;
font-size: 40px;
display: flex;
align-items: center;
justify-content: center;
}
.is-active {
box-shadow: 0 0 8px 8px rgba(255, 0, 0, 0.5) inset;
}
}
</style>
四、实现交互数字换位操作
1、点击空格和相邻数字交换位置;
2、统计步数;
3、验证是否完成游戏。
TypeScript
/** 步数 */
const step = ref(0);
/** 首次点击位置 */
const first = ref(-1);
/** 游戏是否暂停 */
const isStop = ref(false);
/** 是否完成游戏 */
function isSuccess(array: number[]) {
return array.every((n, m) => (n === 0 && m === level.value * level.value - 1) || n === m + 1)
}
/** 交换两个数组元素 */
function exchange(array: number[], i: number, j: number) {
[array[i], array[j]] = [array[j], array[i]];
return array;
}
function onItemClick(i: number) {
if (first.value === -1) {
first.value = i;
} else if (first.value !== i) {
if ([list.value[first.value], list.value[i]].includes(0) && (Math.abs(first.value - i) === 1 || (Math.abs(first.value - i) <= level.value && first.value % level.value === i % level.value))) {
exchange(list.value, i, first.value);
step.value++;
if (isSuccess(list.value)) {
uni.showToast({
title: '挑战成功',
icon: 'success'
});
isStop.value = true;
}
}
first.value = -1;
}
}
五、实现游戏计时和暂停/继续功能
TypeScript
/** 耗时(s) */
const time = ref(0);
let timer = null;
const timeFormat = computed(() => {
let res = '';
if (time.value >= 3600) res += `${Math.floor(time.value / 3600)}时`;
if (time.value >= 60) res += `${Math.floor(time.value % 3600 / 60)}分`;
return res + `${Math.floor(time.value % 60)}秒`
});
/** 计时和暂停 */
function changeStatus() {
if (isStop.value) {
isStop.value = false;
timeDown();
} else {
clearTimeout(timer);
timer = null;
isStop.value = true;
}
}
/** 倒计时 */
function timeDown() {
timer = setTimeout(() => {
if (isStop.value) return;
time.value++;
timeDown();
}, 1000);
}
还需在创建游戏时重置数据
TypeScript
/**
* 创建游戏
* @param {number} n 游戏难度
*/
function createGame(n?: number) {
// console.log('createGame', n, level.value)
level.value = n || level.value;
const ls = [];
for (var i = 0; i < level.value * level.value; i++) {
ls.push(i);
}
const array = shuffleArray(ls);
if (isSolvable(array, level.value)) { // 是否可解
//
first.value = -1;
step.value = 0;
if (timer) {
clearTimeout(timer);
timer = null;
}
time.value = 0;
isStop.value = false;
timeDown();
//
list.value = array;
} else {
createGame();
}
}
六、用鼠标玩太累了,支持下键盘操作
1、上下左右键控制空格先指定方位的数字进行位置交换;
2、空格键控制暂停/继续;
3、数字键控制对应数字进行位置交换。
TypeScript
window.addEventListener('keydown', (e) => {
switch(e.key) {
case 'ArrowUp':
first.value === -1 && onItemClick(list.value.findIndex(item => item === 0));
first.value >= level.value && onItemClick(first.value - level.value);
break;
case 'ArrowDown':
first.value === -1 && onItemClick(list.value.findIndex(item => item === 0));
first.value < level.value * (level.value - 1) && onItemClick(first.value + level.value);
break;
case 'ArrowLeft':
first.value === -1 && onItemClick(list.value.findIndex(item => item === 0));
first.value % level.value > 0 && onItemClick(first.value - 1);
break;
case 'ArrowRight':
first.value === -1 && onItemClick(list.value.findIndex(item => item === 0));
(first.value + 1) % level.value > 0 && onItemClick(first.value + 1);
break;
case 'Backspace':
break;
case ' ':
changeStatus();
break;
default :
const i = list.value.findIndex(item => item.toString() === e.key)
if(i !== -1) onItemClick(i);
break;
}
});
七、玩法与技巧
1、分阶段排列法
- 逐行推进:优先排列第一行(如1-3),再依次完成后续行。例如,在4×4棋盘中,先固定1-4,再处理5-8,以此类推210。
- 处理底层数字:若目标数字位于底层,需通过"绕行"策略,将上层数字暂时移开,腾出空间调整1012。
2、高阶技巧
- 循环移动法:利用空格形成循环路径,将目标数字逐步推至目标位置(如将4从底层移至第一行时,需反复调整1-3的位置)。
- 预留通道:在排列后几行时,需为后续调整保留移动通道,避免死锁