import { aiOp } from '../runtime/AiLog'; import { AiActionKind } from '../types/AiTypes'; import { AiShovelPolicyType } from '../runtime/AiBattlePersonality'; import { AiBoardProbe } from './AiBoardProbe'; import type { MapGrid } from '../../map/legacy/MapGrid'; import { MapCellKind } from 'db://assets/mx/module/merge'; import type { AiPlannerContext, AiPlannedAction } from '../types/AiTypes'; /** 需求 6.2 铲子使用策略 */ export class AiShovelPlanner { private readonly _probe = new AiBoardProbe(); /** * 尝试规划一步铲子动作。 * @param ctx 决策上下文 */ plan(ctx: AiPlannerContext): AiPlannedAction | null { const slotIndex = this._probe.findShovelSlotIndex(ctx.bagSession); if (slotIndex < 0) return null; const digs = this._probe.listEmptyDigGrids(ctx.bagSession, ctx.map); if (digs.length <= 0) return null; const pick = this._pickDig(ctx, digs); if (!pick) return null; aiOp( `铲子规划 policy=${ctx.personality.shovelPolicy}` + ` digCandidates=${digs.length} pick=map[${pick.Index}] rc=(${pick.Row},${pick.Col})` + ` walk=${pick.isCanWalk} dig=${pick.isCanBeDig}` + ` score=${this._type3Score(ctx, pick)}`, ); return { kind: AiActionKind.Shovel, merge: { source: { kind: 'bag', slotIndex }, target: { kind: 'map', gridIndex: pick.Index }, }, }; } /** * 按本局铲子类型选挖格。 * @param ctx 上下文 * @param digs 候选挖格 */ private _pickDig(ctx: AiPlannerContext, digs: MapGrid[]): MapGrid | null { const policy = ctx.personality.shovelPolicy; if (policy === AiShovelPolicyType.Type1) { let x = (Math.floor(Number(ctx.profile.seed) || 0) >>> 0) || 1; x ^= x << 13; x ^= x >>> 17; x ^= x << 5; const idx = (x >>> 0) % digs.length; return digs[idx] ?? null; } if (policy === AiShovelPolicyType.Type2) { // 先扩已开岛 / 纯 Dig,再贴邻 let best: MapGrid | null = null; let bestScore = -1e99; for (let i = 0; i < digs.length; i++) { const dig = digs[i]; let score = 0; if (this._hasOpenedNeighbor(ctx, dig)) score += 100000; score += this._countWalkNeighbors(ctx, dig) * 1000; if (score > bestScore) { bestScore = score; best = dig; } } return best; } // Type3 贴路:可走邻接中优先「靠已开 Put / 靠中部」,禁止总挖地图顶边 Dig 条 return this._pickType3AlongPath(ctx, digs); } /** * 贴路选挖:优先贴已开 Put;其次可走邻接候选;避免固定扫到顶边 Dig。 * @param ctx 上下文 * @param digs 全部空挖格 */ private _pickType3AlongPath(ctx: AiPlannerContext, digs: MapGrid[]): MapGrid | null { let best: MapGrid | null = null; let bestScore = -1e99; for (let i = 0; i < digs.length; i++) { const dig = digs[i]; const score = this._type3Score(ctx, dig); if (score > bestScore) { bestScore = score; best = dig; } } return best; } /** * Type3 挖格分:贴已开 Put ≫ 可走邻接 ≫ 靠河;边条 Dig 降权。 * dig+putText 双标已按可放置优先剔除,不会进入候选。 * @param ctx 上下文 * @param dig 候选 */ private _type3Score(ctx: AiPlannerContext, dig: MapGrid): number { let score = 0; if (this._hasOpenedNeighbor(ctx, dig)) score += 100000; score += this._countWalkNeighbors(ctx, dig) * 1000; // 顶/底边 Dig 降权,避免连续铲开整条边 Dig 后把塔摆到地图外缘 const maxRow = Math.max(0, (ctx.map.mapRows | 0) - 1); if (dig.Row <= 0 || dig.Row >= maxRow) score -= 50000; score += dig.Row * 10; return score; } private _hasOpenedNeighbor(ctx: AiPlannerContext, dig: MapGrid): boolean { const grids = ctx.map.mapGrids; if (!Array.isArray(grids)) return false; for (let i = 0; i < grids.length; i++) { const mg = grids[i]; if (!mg?.node?.isValid) continue; if (Math.abs(mg.Row - dig.Row) + Math.abs(mg.Col - dig.Col) !== 1) continue; if (ctx.bagSession.merge.map.getCellKind(mg.Index) === MapCellKind.Put) return true; if (ctx.bagSession.merge.map.hasTile(mg.Index)) return true; } return false; } private _countWalkNeighbors(ctx: AiPlannerContext, dig: MapGrid): number { const grids = ctx.map.mapGrids; if (!Array.isArray(grids)) return 0; let n = 0; for (let i = 0; i < grids.length; i++) { const mg = grids[i]; if (!mg?.node?.isValid) continue; if (Math.abs(mg.Row - dig.Row) + Math.abs(mg.Col - dig.Col) !== 1) continue; if (mg.isCanWalk) n++; } return n; } }