Skip to content

拼图游戏实现思路

1. 游戏容器初始化

  • 创建盒子区域:设定游戏主盒子区域的大小及基本样式,为小方块提供相对定位的容器。

2. 方块生成与管理

  • 创建小方块数据模型
    • 将每个小方块看作一个独立对象(建议通过构造函数实现)。
    • 使用数组集中存放所有小方块的信息。
    • 精确计算并记录每个小方块的核心属性:当前坐标(left / top)、背景图切割位置(background-position)、以及它的正确目标坐标
  • 渲染小方块 DOM
    • 根据计算好的数据生成对应的 DOM 节点,并统一添加到 game 盒子中。
    • 特殊处理:将最后一个方块(通常是右下角)设置为不可见(例如 display: none),作为拼图滑动所需的空白区域。

3. 游戏核心交互

  • 洗牌(打乱顺序)
    • 游戏开始时,通过生成随机数来随机选取方块进行打乱。
    • 打乱的核心逻辑是交换两个方块对象的 lefttop 属性,并通过修改 DOM 样式实现视觉移动。
  • 方块移动(交换位置)
    • 边界与合法性判断:点击方块时,必须判断该方块是否与空白方块相邻。只有相邻的方块才能进行位置交换。

4. 胜负判定

  • 判断游戏是否结束
    • 每次成功交换位置后,都需要触发胜负判断逻辑。
    • 遍历所有方块,比对当前状态:只有当所有方块当前的坐标都与其正确的坐标完全一致时,才判定为游戏胜利。

💡 开发注意事项

坐标精度问题:在判断当前坐标与正确坐标是否相等时,由于浏览器渲染或计算可能会产生小数点的精度问题。强烈建议将坐标转换为整数后再进行判定,以避免因微小误差导致胜负判定失败。

代码实现

html
<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title></title>
	</head>
	<body>
		<div id="game" class="game"></div>
		<script type="module" src="./script/index.js"></script>
	</body>
</html>
js
import { getRandom, isEqual } from "./utils.js";

/* 游戏配置 */
const gameConfig = {
	width: 500, //宽度
	height: 500, //高度
	rows: 3,
	cols: 3,
	bgUrl: "images/lol.png",
	dom: document.querySelector("#game"),
	blockWidth: 0, //小方块的宽
	blockHeight: 0, //小方块的高
	blockNum: 0, //小方块的数量
	isGameOver: false,//游戏是否结束
}

const bWidth = gameConfig.width / gameConfig.cols;
const bHeight = gameConfig.height / gameConfig.rows;
const bNum = (gameConfig.width / bWidth) * (gameConfig.height / bHeight);
gameConfig.blockWidth = bWidth;
gameConfig.blockHeight = bHeight;
gameConfig.blockNum = bNum;
console.log("gameConfig==>", gameConfig);


/* 初始化 */
const bocksArr = []; //存放方块信息集合

// 初始game容器
const initGameDom = () => {
	const {
		width,
		height,
		bgUrl
	} = gameConfig;

	game.style.position = 'relative';
	game.style.width = width + 'px';
	game.style.height = height + 'px';
	game.style.border = "1px solid #ccc";
	// game.style.background = `url(${bgUrl}) no-repeat`;
}

// 小方块对象构造函数
class Block {
	// 构造函数
	constructor(left, top, isVisible) {
		this.left = left; //方块的x坐标
		this.top = top; //方块的y坐标
		this.bgX = -left; //小方块背景图x
		this.bgY = -top; //小方块背景图y
		this.correctX = left; //小方块正确坐标x
		this.correctY = top; //小方块正确坐标y
		this.node = null; //小方块dom节点
		this.isVisible = isVisible; //是否展示

		this.createBlock(); //创建小方块dom
	}

	// 创建小方块dom
	createBlock() {
		this.node = document.createElement('div');
		this.node.style.boxSizing = 'border-box';
		this.node.style.position = "absolute";
		this.node.style.width = gameConfig.blockWidth + 'px';
		this.node.style.height = gameConfig.blockHeight + 'px';
		this.node.style.border = '1px solid #fff';
		this.node.style.left = this.left + 'px';
		this.node.style.top = this.top + 'px';
		this.node.style.cursor = "pointer";
		this.node.style.background = `url(${gameConfig.bgUrl}) no-repeat ${this.bgX}px ${this.bgY}px`;
		this.node.style.display = this.isVisible ? 'block' : 'none';
		gameConfig.dom.appendChild(this.node);
	}
	// 显示方块
	show() {
		this.node.style.left = this.left + 'px';
		this.node.style.top = this.top + 'px';
	}

	//判断当前方块是否在正确的位置上
	isCorrect() {
		return isEqual(this.left, this.correctX) && isEqual(this.top, this.correctY);
	}
}

// 初始化小方块
const initBlock = () => {
	// 构造小方块信息
	for (let i = 0; i < gameConfig.rows; i++) {
		for (let j = 0; j < gameConfig.cols; j++) {
			// i行  j列
			const left = j * gameConfig.blockWidth;
			const top = i * gameConfig.blockHeight;
			const visible = !(i === gameConfig.rows - 1 && j === gameConfig.cols - 1);
			const block = new Block(left, top, visible);
			bocksArr.push(block);
		}
	}
	console.log("bocksArr", bocksArr);
}

// 交换位置
const exchange = (b1, b2) => {
	let temp = b1.left;
	b1.left = b2.left;
	b2.left = temp;
	temp = b1.top;
	b1.top = b2.top;
	b2.top = temp;
	b1.show();
	b2.show();
}

// 洗牌 随机生成一个位置,交换对应的left和top
const shuffle = () => {
	bocksArr.forEach((blockItem, bIndex) => {
		// 最后白色方块不参与洗牌
		if (bIndex === bocksArr.length - 1) {
			return
		}
		// 当前元素和随机位置进行交换left和top
		const index = getRandom(0, bocksArr.length - 2);
		exchange(blockItem, bocksArr[index]);
	})
	console.log("bocksArr洗牌", bocksArr);
}

// 是否胜利
const isWin = () => {
	const hasWrong = bocksArr.filter(blockItem => {
		return !blockItem.isCorrect();
	})

	if (!hasWrong.length) {
		gameConfig.isGameOver = true;
		bocksArr.forEach((blockItem) => {
			blockItem.isVisible = true;
			blockItem.node.style.border = "0";
			blockItem.node.style.display = "block";
		})
	}
}

// 绑定事件
const bindEvent = () => {
	bocksArr.forEach((blockItem, index) => {
		blockItem.node.onclick = () => {
			// 游戏结束,不能点击
			if (gameConfig.isGameOver) {
				return
			}

			const emptyBlock = bocksArr.find(item => !item.isVisible);//找到空白块

			// 只有相邻的才可以交换位置
			const isNeighbor =
				blockItem.left === emptyBlock.left && isEqual(Math.abs(blockItem.top - emptyBlock.top), gameConfig.blockHeight)
				||
				blockItem.top === emptyBlock.top && isEqual(Math.abs(blockItem.left - emptyBlock.left), gameConfig.blockWidth)

			// if (!isNeighbor) {
			// 	return
			// }
			exchange(blockItem, emptyBlock);
			isWin();//判断是否胜利
		}
	})
}



// 初始化操作
const init = () => {
	initGameDom(); //初始化容器
	initBlock(); //初始化小方块
	shuffle();//洗牌
	bindEvent();//绑定事件
}

init();
js
/**
 * 获取随机数
 */
const getRandom = (min, max) => {
	return Math.floor(Math.random() * (max - min + 1) + min)
}

/* 
判断两个数是否相等
*/
const isEqual = (a, b) => {
	return parseInt(a) === parseInt(b)
}

export {
	getRandom,
	isEqual
}

MIT License