(1)复刻《猪了个猪》挪了个船:位置生成实现cocos3.x
一、整体生成思路
生成一个可解的初始布局,再随机合法移动打乱。
流程:
- 清空网格与所有小船
- 按规则一艘一艘放满 16 艘小船(不会无解)
- 对这个合规布局做 80 步随机逆向移动
条件:
- 小船长款比为2:1
二、第一步:尝试放满 16 艘小船
函数:tryPlaceAllBoats()
1. 每艘小船生成
- 随机方向:水平 / 垂直
- 随机朝向:左/右 或 上/下
- 遍历整个棋盘,寻找所有能放的位置
2. 位置必须满足 4 条规则
函数:isPositionValid()
保证关卡有解的关键,任何位置不满足就直接跳过:
-
格子为空、不越界 小船占 2 格,两格都必须是空的、在棋盘内。
-
不能和其他船面对面 同一条直线上,反向船不能相对。
-
不能出现 3 艘连续同向 水平/垂直方向,不能出现 3 艘方向一样、连在一起的船。
-
不能形成循环依赖 比如 A 堵 B、B 堵 C、C 堵 A,形成死锁。
三、第二步:位置打分
候选位置打分,让布局更紧凑。
函数:getPlacementScore()
三个维度:
- 中心优先 越靠近棋盘中心,分数越高。
- 紧凑优先 周围船越多,分数越高。
- 方向多样性优先 水平、垂直穿插摆放,分数更高(避免全横或全竖)。
从前 5 高分位置里随机选一个。
四、第三步:逆向打乱
函数:shuffleByReverseMoves(80)
打乱规则:
- 找出当前所有能移动的船
- 随机选一艘,让它移动一步
- 移动前依然检查:
- 不能越界
- 不能撞船
- 不能违反面对面、连续三个方向一致、循环依赖
- 执行 80 步
五、小船生成完整流程
开始生成
↓
清空网格、删除所有小船
↓
循环 16 次:
随机方向+朝向
遍历棋盘找所有合规位置
位置打分(中心+紧凑+方向多样)
选高分位置放置
标记格子占用
↓
16 艘船全部放完(合规布局)
↓
执行 80 步随机合法移动(打乱)
↓
生成完成 → 进入游戏
六、核心代码
typescript
import { _decorator, Component, Node, Prefab, instantiate } from "cc";
const { ccclass, property } = _decorator;
// 小船数据结构
interface Boat {
id: number;
node: Node;
row: number;
col: number;
horizontal: boolean;
dir: number; // 1右/下 -1左/上
}
// 网格数据系统
export class GridSystem {
grid: (number | null)[][];
rows: number;
cols: number;
constructor(rows: number, cols: number) {
this.rows = rows;
this.cols = cols;
this.grid = Array.from({ length: rows }, () =>
Array.from({ length: cols }, () => null),
);
}
inBound(r: number, c: number) {
return r >= 0 && c >= 0 && r < this.rows && c < this.cols;
}
isEmpty(r: number, c: number) {
return this.inBound(r, c) && this.grid[r][c] === null;
}
place(id: number, cells: [number, number][]) {
for (const [r, c] of cells) this.grid[r][c] = id;
}
clear(cells: [number, number][]) {
for (const [r, c] of cells) this.grid[r][c] = null;
}
}
typescript
@ccclass("BoatController")
export class BoatController extends Component {
@property(Prefab) boat_h_r: Prefab | null = null;
@property(Prefab) boat_h_l: Prefab | null = null;
@property(Prefab) boat_v_t: Prefab | null = null;
@property(Prefab) boat_v_b: Prefab | null = null;
grid!: GridSystem;
boats: Boat[] = [];
readonly CELL = 75;
readonly rows = 10;
readonly cols = 10;
readonly maxBoats = 16; // 一共生成16艘船
startX = 0;
startY = 0;
onLoad() {
this.grid = new GridSystem(this.rows, this.cols);
// 网格居中计算
this.startX = -((this.cols - 1) * this.CELL) / 2;
this.startY = ((this.rows - 1) * this.CELL) / 2;
// 生成必解关卡
this.generateSolvableLevel();
}
// =========================================================
// 【主生成函数】
// =========================================================
generateSolvableLevel() {
let attempts = 0;
const maxAttempts = 20;
while (attempts < maxAttempts) {
attempts++;
this.node.destroyAllChildren();
this.boats = [];
this.grid = new GridSystem(this.rows, this.cols);
// 成功放满16艘船 → 打乱
if (this.tryPlaceAllBoats()) {
this.shuffleByReverseMoves(80);
return;
}
}
}
// =========================================================
// 【规则1】:放满16艘船
// =========================================================
tryPlaceAllBoats(): boolean {
let id = 1;
for (let i = 0; i < this.maxBoats; i++) {
const horizontal = Math.random() > 0.5;
const dir = Math.random() > 0.5 ? 1 : -1;
const candidates: { r: number; c: number; score: number }[] = [];
// 遍历所有位置找可放置点
for (let r = 0; r < this.rows; r++) {
for (let c = 0; c < this.cols; c++) {
if (!this.isPositionValid(r, c, horizontal, dir)) continue;
const score = this.getPlacementScore(r, c, horizontal);
candidates.push({ r, c, score });
}
}
if (candidates.length === 0) return false;
// 最优位置随机选择
candidates.sort((a, b) => b.score - a.score);
const topCount = Math.min(5, candidates.length);
const pick = candidates[Math.floor(Math.random() * topCount)];
// 放置船
const cells = this.getBoatCells(pick.r, pick.c, horizontal);
this.grid.place(id, cells);
const boat = this.createBoat(id, pick.r, pick.c, horizontal, dir);
this.boats.push(boat);
id++;
}
return true;
}
// =========================================================
// 【规则2】:位置合法性检查
// =========================================================
isPositionValid(
r: number,
c: number,
horizontal: boolean,
dir: number,
): boolean {
const cells = this.getBoatCells(r, c, horizontal);
// 1. 格子为空 + 不越界
for (const [cr, cc] of cells) {
if (!this.grid.inBound(cr, cc) || !this.grid.isEmpty(cr, cc))
return false;
}
// 2. 禁止面对面
if (this.hasOppositeOnSameLine(r, c, horizontal, dir)) return false;
// 临时放置,检查3连+循环依赖
this.grid.place(-1, cells);
const tempBoat: Boat = {
id: -1,
node: null!,
row: r,
col: c,
horizontal,
dir,
};
this.boats.push(tempBoat);
// 3. 禁止连续三搜船方向一致同向 4. 禁止循环依赖
const violates =
this.hasConsecutiveSameDirection() || this.hasCyclicDependency();
this.boats.pop();
this.grid.clear(cells);
return !violates;
}
// =========================================================
// 【规则3】:逆向打乱80步(保持可解)
// =========================================================
shuffleByReverseMoves(steps: number) {
for (let i = 0; i < steps; i++) {
const movable: { boat: Boat; dr: number; dc: number }[] = [];
for (const b of this.boats) {
const dr = b.horizontal ? 0 : b.dir;
const dc = b.horizontal ? b.dir : 0;
if (
this.canMoveBoat(b, dr, dc) &&
!this.wouldCreateOpposite(b, b.row + dr, b.col + dc)
) {
if (!this.wouldViolateOnMove(b, b.row + dr, b.col + dc)) {
movable.push({ boat: b, dr, dc });
}
}
}
if (movable.length === 0) continue;
const pick = movable[Math.floor(Math.random() * movable.length)];
this.applyMove(pick.boat, pick.dr, pick.dc);
}
}
// =========================================================
// 工具函数:船占2格
// =========================================================
getBoatCells(r: number, c: number, horizontal: boolean): [number, number][] {
return horizontal
? [
[r, c],
[r, c + 1],
]
: [
[r, c],
[r + 1, c],
];
}
// =========================================================
// 创建船节点
// =========================================================
createBoat(
id: number,
r: number,
c: number,
horizontal: boolean,
dir: number,
): Boat {
let prefab: Prefab | null = null;
if (horizontal) prefab = dir > 0 ? this.boat_h_r : this.boat_h_l;
else prefab = dir > 0 ? this.boat_v_b : this.boat_v_t;
const node = instantiate(prefab!);
this.node.addChild(node);
const x = horizontal
? this.startX + (c + 0.5) * this.CELL
: this.startX + c * this.CELL;
const y = horizontal
? this.startY - r * this.CELL
: this.startY - (r + 0.5) * this.CELL;
node.setPosition(x, y);
return { id, node, row: r, col: c, horizontal, dir };
}
// =========================================================
// 移动相关(生成时需要)
// =========================================================
canMoveBoat(boat: Boat, dr: number, dc: number): boolean {
const newCells = this.getBoatCells(
boat.row + dr,
boat.col + dc,
boat.horizontal,
);
for (const [r, c] of newCells) {
if (!this.grid.inBound(r, c)) continue;
const id = this.grid.grid[r][c];
if (id !== null && id !== boat.id) return false;
}
return true;
}
applyMove(boat: Boat, dr: number, dc: number) {
const oldCells = this.getBoatCells(boat.row, boat.col, boat.horizontal);
this.grid.clear(oldCells.filter(([r, c]) => this.grid.inBound(r, c)));
boat.row += dr;
boat.col += dc;
const newCells = this.getBoatCells(boat.row, boat.col, boat.horizontal);
const inBoundNew = newCells.filter(([r, c]) => this.grid.inBound(r, c));
if (inBoundNew.length > 0) this.grid.place(boat.id, inBoundNew);
const x = boat.horizontal
? this.startX + (boat.col + 0.5) * this.CELL
: this.startX + boat.col * this.CELL;
const y = boat.horizontal
? this.startY - boat.row * this.CELL
: this.startY - (boat.row + 0.5) * this.CELL;
boat.node.setPosition(x, y);
}
wouldViolateOnMove(boat: Boat, nr: number, nc: number): boolean {
// 省略...
return false;
}
// =========================================================
// 规则检查:面对面 / 连续三搜船方向一致 / 循环依赖
// =========================================================
hasOppositeOnSameLine(
r: number,
c: number,
h: boolean,
dir: number,
): boolean {
// 省略...
// 同线反向相对检查
return false;
}
// 省略...
wouldCreateOpposite(boat: Boat, r: number, c: number): boolean {
return false;
}
// 省略...
hasConsecutiveSameDirection(): boolean {
return false;
}
// 省略...
hasCyclicDependency(): boolean {
return false;
}
// =========================================================
// 位置评分:中心 + 紧凑 + 多样
// =========================================================
getPlacementScore(r: number, c: number, horizontal: boolean): number {
// 省略...
return 10; // 简化评分,不影响生成
}
}