|
|
|
|
import type { AiPlannerContext, AiPlannedAction } from '../types/AiTypes';
|
|
|
|
|
import { AiActionKind } from '../types/AiTypes';
|
|
|
|
|
import { AiPlacementScorer } from './AiPlacementScorer';
|
|
|
|
|
import { AiBoardProbe } from './AiBoardProbe';
|
|
|
|
|
import { aiOp } from '../runtime/AiLog';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* AI 决策:解锁 → 产卡使用 → 五行合成 → 上阵。
|
|
|
|
|
*/
|
|
|
|
|
export class AiActionPlanner {
|
|
|
|
|
private readonly _probe = new AiBoardProbe();
|
|
|
|
|
private readonly _place = new AiPlacementScorer();
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* AI 空 Put 格数量(供刷新门禁)。
|
|
|
|
|
* @param ctx 决策上下文
|
|
|
|
|
*/
|
|
|
|
|
countEmptyTextPut(ctx: AiPlannerContext): number {
|
|
|
|
|
return this._probe.countEmptyTextPut(ctx.bagSession, ctx.map);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 规划下一步原子动作;无可执行时返回 null。
|
|
|
|
|
* @param ctx 决策上下文
|
|
|
|
|
*/
|
|
|
|
|
planNext(ctx: AiPlannerContext): AiPlannedAction | null {
|
|
|
|
|
const emptyPut = this._probe.countEmptyTextPut(ctx.bagSession, ctx.map);
|
|
|
|
|
const dig = this._probe.listEmptyDigGrids(ctx.bagSession, ctx.map).length;
|
|
|
|
|
|
|
|
|
|
const unlock = this._probe.findUnlockAreaAction(ctx.bagSession, ctx.map);
|
|
|
|
|
if (unlock) {
|
|
|
|
|
return this._logAction('解锁地块', { kind: AiActionKind.Place, merge: unlock }, emptyPut, dig);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const card = this._probe.findBattleCardAction(ctx.bagSession, ctx.map);
|
|
|
|
|
if (card) {
|
|
|
|
|
// Place:勿用 Upgrade,避免 onBagMergedAt 误推进待开启地块
|
|
|
|
|
return this._logAction('产卡使用', { kind: AiActionKind.Place, merge: card }, emptyPut, dig);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const tool = this._probe.findBagToolAction(ctx.bagSession, ctx.map);
|
|
|
|
|
if (tool) {
|
|
|
|
|
return this._logAction('功能符使用', { kind: AiActionKind.Place, merge: tool }, emptyPut, dig);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const upgrade = this._probe.findUpgradeAction(ctx.bagSession, ctx.map);
|
|
|
|
|
if (upgrade) {
|
|
|
|
|
return this._logAction('数字合成', { kind: AiActionKind.Upgrade, merge: upgrade }, emptyPut, dig);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const place = this._place.plan(ctx);
|
|
|
|
|
if (place) return this._logAction('上阵', place, emptyPut, dig);
|
|
|
|
|
|
|
|
|
|
aiOp(`本拍无动作 emptyPut=${emptyPut} dig=${dig}`);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 仅规划落子(同拍回填空位)。
|
|
|
|
|
* @param ctx 决策上下文
|
|
|
|
|
*/
|
|
|
|
|
planPlaceOnly(ctx: AiPlannerContext): AiPlannedAction | null {
|
|
|
|
|
const place = this._place.plan(ctx);
|
|
|
|
|
if (!place) return null;
|
|
|
|
|
const emptyPut = this._probe.countEmptyTextPut(ctx.bagSession, ctx.map);
|
|
|
|
|
return this._logAction('摆放(同拍回填)', place, emptyPut, 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 打印并返回规划动作。
|
|
|
|
|
* @param label 动作中文名
|
|
|
|
|
* @param action 动作
|
|
|
|
|
* @param emptyPut 空 Put
|
|
|
|
|
* @param dig 可铲数
|
|
|
|
|
*/
|
|
|
|
|
private _logAction(label: string, action: AiPlannedAction, emptyPut: number, dig: number): AiPlannedAction {
|
|
|
|
|
const m = action.merge;
|
|
|
|
|
let detail = `kind=${action.kind}`;
|
|
|
|
|
if (m) {
|
|
|
|
|
const src = m.source.kind === 'bag' ? `bag[${m.source.slotIndex}]` : `map[${m.source.gridIndex}]`;
|
|
|
|
|
const dst = m.target.kind === 'bag' ? `bag[${m.target.slotIndex}]` : `map[${m.target.gridIndex}]`;
|
|
|
|
|
detail += ` ${src}->${dst}`;
|
|
|
|
|
}
|
|
|
|
|
aiOp(`决策 ${label} | ${detail} | emptyPut=${emptyPut} dig=${dig}`);
|
|
|
|
|
return action;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @deprecated 使用 planNext
|
|
|
|
|
* @param ctx 决策上下文
|
|
|
|
|
*/
|
|
|
|
|
planRound(ctx: AiPlannerContext): AiPlannedAction[] {
|
|
|
|
|
const action = this.planNext(ctx);
|
|
|
|
|
return action ? [action] : [];
|
|
|
|
|
}
|
|
|
|
|
}
|