Files
MatchMaster/assets/Script/RocketRules.ts
T
2026-10-16 16:24:28 +08:00

115 lines
6.3 KiB
TypeScript

import { RainbowCell, RainbowCut, RAINBOW_SHAPES, rainbowPriority } from "./RainbowRules";
export interface RocketCut { cell: RainbowCell; fragments: RainbowCut[]; }
function key(cells: RainbowCell[]): string {
const x = Math.min(...cells.map(p => p.x)), y = Math.min(...cells.map(p => p.y));
return cells.map(p => (p.x - x) + "," + (p.y - y)).sort().join(";");
}
/** 每次仅删除一格;其余四邻接连通片各自匹配已有形状。 */
export function rocketCuts(cells: RainbowCell[], edgeOnly = false): RocketCut[] {
return cells.map(cell => {
const pending = cells.filter(p => p !== cell).slice();
const fragments: RainbowCut[] = [];
while (pending.length) {
const group = [pending.pop()];
for (let i = 0; i < group.length; i++) {
for (let j = pending.length - 1; j >= 0; j--) {
if (Math.abs(pending[j].x - group[i].x) + Math.abs(pending[j].y - group[i].y) === 1) {
group.push(pending.splice(j, 1)[0]);
}
}
}
const shape = RAINBOW_SHAPES.findIndex(points => points.length === group.length && key(points) === key(group));
if (shape < 0) throw new Error("火箭残块缺少对应形状: " + key(group));
fragments.push({ cell, shape, offset: {
x: Math.min(...group.map(p => p.x)) - Math.min(...RAINBOW_SHAPES[shape].map(p => p.x)),
y: Math.min(...group.map(p => p.y)) - Math.min(...RAINBOW_SHAPES[shape].map(p => p.y))
} });
}
return { cell, fragments };
}).filter(cut => !edgeOnly || cut.fragments.length === 1);
}
export const ROCKET_CUTS = RAINBOW_SHAPES.map(shape => rocketCuts(shape));
export function rocketEdgeOnly(block: any, protectedColors: Set<number>): boolean {
const info = block.block_Info;
return protectedColors.has(block.color) || (block.type !== 0 && block.type !== 16)
|| !!block.flowerType || info.lock != null || info.colorArray != null || info.colorChange != null
|| !!info.horizontal || !!info.vertical || info.floorMove === true
|| (block.teamBlocks || []).length > 1;
}
export function rocketCandidates(map: any): any[] {
const blocks = map.blocks.filter(node => cc.isValid(node, true) && node.active)
.map(node => node.getComponent("Block")).filter(Boolean);
const buttons = new Set<number>((map.buttonBlock || []).map(node => node.getComponent("MapBlock").color));
const protectedColors = new Set<number>();
(map.wallArray || []).forEach(node => {
const wall = node.getChildByName("wall").getComponent("Wall");
(wall.colorStartArray || []).forEach(color => protectedColors.add(color));
if (wall.special === 2) protectedColors.add(wall.color);
});
const glued = new Set<cc.Node>();
blocks.forEach(block => {
if (block.type === 9 || block.type === 19) {
glued.add(block.node);
if (block.block_Info.node) glued.add(block.block_Info.node);
}
if ((block.adhesiveNode || []).some(node => cc.isValid(node, true) && node.active
&& node.getComponent("Adhesive")?.target)) glued.add(block.node);
});
return blocks.filter(block => !block.spawnLocked && !block.over && !block.rainbowEliminating
&& !block.oldRainbow && block.color !== 100 && block.color !== 11
&& [10, 11, 22].indexOf(block.type) < 0 && !!ROCKET_CUTS[block.block_Info.block])
.map(block => {
const upper = block.type === 1 && block.block_Info.node?.getComponent("Block");
const layers = upper ? [block, upper] : [block];
if (layers.some(layer => glued.has(layer.node) || buttons.has(layer.color))) return null;
const edgeOnly = layers.some(layer => rocketEdgeOnly(layer, protectedColors));
const cuts = ROCKET_CUTS[block.block_Info.block].filter(cut => !edgeOnly || cut.fragments.length === 1);
return { block, cuts, edgeOnly, origin: block.rocketOrigin || block.node.uuid,
priority: block.allBlocks.length === 1 ? 2 : layers.some(layer => rainbowPriority(layer) > 0) ? 1 : 0 };
}).filter(candidate => candidate && candidate.cuts.length);
}
export function chooseRocketTarget(candidates: any[], hit: Set<string>, remaining: number,
random: () => number = Math.random): any {
if (!candidates.length) return null;
const priority = Math.min(...candidates.map(candidate => candidate.priority));
let pool = candidates.filter(candidate => candidate.priority === priority);
const fresh = pool.filter(candidate => !hit.has(candidate.origin));
if (fresh.length) pool = fresh;
const target = pool[Math.floor(random() * pool.length)];
// 1932 等候选不足的开局,重复从边缘裁切,保留主体供后续火箭命中。
const edges = target.cuts.filter(cut => cut.fragments.length === 1);
const cuts = candidates.length < remaining && edges.length ? edges : target.cuts;
return { target, cut: cuts[Math.floor(random() * cuts.length)] };
}
export interface RocketShot { x: number; y: number; origin: string; cut: RocketCut; }
/** 只在候选快照上模拟逐发裁切;同一原块的实际命中须保持规划顺序。 */
export function planRocketShots(map: any, count = 3, random: () => number = Math.random): RocketShot[] {
let pool = rocketCandidates(map).map(candidate => ({ ...candidate,
x: candidate.block.posX, y: candidate.block.posY }));
const shots: RocketShot[] = [], hit = new Set<string>();
for (let remaining = count; remaining > 0; remaining--) {
const choice = chooseRocketTarget(pool, hit, remaining, random);
if (!choice) break;
const { target, cut } = choice;
shots.push({ x: target.x + cut.cell.x, y: target.y + cut.cell.y, origin: target.origin, cut });
hit.add(target.origin);
pool = pool.filter(candidate => candidate !== target);
for (const fragment of cut.fragments) {
const cuts = ROCKET_CUTS[fragment.shape].filter(next => !target.edgeOnly || next.fragments.length === 1);
if (cuts.length) pool.push({ ...target, cuts,
x: target.x + fragment.offset.x, y: target.y + fragment.offset.y,
priority: RAINBOW_SHAPES[fragment.shape].length === 1 ? 2 : target.priority });
}
}
return shots;
}