开发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的位置)。
  • 预留通道:在排列后几行时,需为后续调整保留移动通道,避免死锁
相关推荐
Pedantic15 小时前
SwiftUI 手势层级(Gesture Hierarchy)详解
前端
飘尘15 小时前
前端转型全栈(Java后端)的快速上手指引
前端·后端·全栈
一颗烂土豆15 小时前
Meshopt 压缩深度解析,为什么它比 Draco 更快
前端·javascript·webgl
浏览器工程师16 小时前
AI Agent 接浏览器任务,先别让它一路点到底
前端·后端
雨季mo浅忆16 小时前
VSCode自动格式化三要素
前端
爱勇宝17 小时前
深扒 Anthropic 1680 位工程师简历:应届生几乎没机会,AI 公司最缺的不是博士
前端·后端·程序员
kyriewen17 小时前
同事每天催我 Code Review,我写了个脚本让 AI 替我 review PR——现在他反过来催 AI 了
前端·javascript·ai编程
user205855615181320 小时前
Windows 项目安装时报 `node-sass` 错误,如何快速处理
前端
LiaCode20 小时前
Redis 在生产项目的使用
前端·后端
LiaCode20 小时前
一天学完 redis 的爽翻版核心知识总结
前端·后端