开发vue小游戏:数字华龙道

一、游戏介绍

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的位置)。
  • 预留通道:在排列后几行时,需为后续调整保留移动通道,避免死锁
相关推荐
崔庆才丨静觅10 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby606111 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了11 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅11 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅11 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅11 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment12 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅12 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊12 小时前
jwt介绍
前端
爱敲代码的小鱼12 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax