You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
AdLike2048/assets/script/battle/ai/decision/AiShovelPlanner.ts

135 lines
5.2 KiB

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;
}
}