当前位置: 首页 > article >正文

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

一、游戏介绍

1、历史背景
        数字华容道脱胎于传统华容道,后者源自三国时期“曹操败走华容道”的故事。传统玩法是通过移动不同形状的木块,帮助“曹操”从出口逃脱。而数字华容道将棋子替换为数字,目标是通过滑动方块,将乱序的数字按1至N的顺序排列整齐(例如4×4棋盘需排列1-15,留一空格作为移动空间)。

2、基本规则

  • 棋盘分为3×3、4×4、5×5等不同难度等级,数字范围随棋盘大小递增。
  • 每次只能移动与空格相邻的数字方块,通过滑动调整顺序,最终达成从左到右、从上到下的数字连贯性

二、代码生成游戏盘面

1、实现思路:

1.按游戏难度生成一个目标数字盘面;

2.随机打乱整个盘面的数字;

3.判断盘面可解性,无解则重新生成直到有解为止。

2、实现代码:

/** 难度: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.增加计步、计时、难度选择、暂停/继续、重新开始。

<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、验证是否完成游戏。


/** 步数 */
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;
	}
}

五、实现游戏计时和暂停/继续功能

/** 耗时(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);
}

还需在创建游戏时重置数据

/**
 * 创建游戏
 * @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、数字键控制对应数字进行位置交换。


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的位置)。
  • 预留通道:在排列后几行时,需为后续调整保留移动通道,避免死锁

http://www.kler.cn/a/575843.html

相关文章:

  • java中有了ArrayList为什么还有LinkedList
  • io学习------>进程
  • LeetCode 栈章节
  • centos中使用svn整理
  • 3.1ACM定级赛复盘
  • 文本处理Bert面试内容整理-BERT的变种有哪些?
  • k8s面试题总结(十)
  • 分布式锁—6.Redisson的同步器组件
  • CS144 Lab Checkpoint 3: the TCP sender
  • Virtual Machine Platform windows功能启用报错
  • 【Leetcode 每日一题】2597. 美丽子集的数目
  • Spring (六)容器-生命周期
  • DeepSeek V3 源码:从入门到放弃!
  • Python Pandas实现导出两个Excel数据集的对应值的差异值分析
  • 网络安全中蓝牙攻击有哪些?
  • 期权帮|中证1000股指期权交割结算价怎么算?
  • SVG 参考手册
  • AI预测体彩排3新模型百十个定位预测+胆码预测+杀和尾+杀和值2025年3月7日第12弹
  • ①Modbus TCP转Modbus RTU/ASCII网关同步采集无需编程高速轻松组网
  • 如何看torch 和torchvision版本,如何看CUDA的版本,我是ubuntu的系统,建立的环境是py38。