开发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的位置)。
  • 预留通道:在排列后几行时,需为后续调整保留移动通道,避免死锁
相关推荐
浪遏3 分钟前
面试官😏: 讲一下事件循环 ,顺便做道题🤪
前端·面试
Joeysoda24 分钟前
JavaEE进阶(2) Spring Web MVC: Session 和 Cookie
java·前端·网络·spring·java-ee
小周同学:1 小时前
npm : 无法加载文件 C:\Program Files\nodejs\npm.ps1,因为在此系统上禁止运行脚本。
前端·npm·node.js
浪遏1 小时前
面试官:字符串反转有多少种实现方式 ?| 一道题目检测你的基础
前端·面试
kjl5365661 小时前
前端题目类型
前端
IT、木易1 小时前
大白话CSS 优先级计算规则的详细推导与示例
前端·css·面试
B站计算机毕业设计超人2 小时前
计算机毕业设计SpringBoot+Vue.js民族婚纱预定系统(源码+文档+PPT+讲解)
java·vue.js·spring boot·后端·毕业设计·课程设计·毕设
TinTin Land2 小时前
后 Safe 时代:多签钱包安全新范式与防范前端攻击的新思路
前端·安全
IT、木易2 小时前
大白话html语义化标签优势与应用场景
前端·html
知识分享小能手2 小时前
Html5学习教程,从入门到精通, HTML5 新的 Input 类型:语法知识点与案例代码(16)
开发语言·前端·学习·html·html5·web·java开发