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/AiActionPlanner.ts

182 lines
7.6 KiB

import type { AiPlannerContext, AiPlannedAction } from '../types/AiTypes';
import { AiActionKind } from '../types/AiTypes';
import { AiShovelPlanner } from './AiShovelPlanner';
import { AiSwapPlanner } from './AiSwapPlanner';
import { AiTextPolicyPlanner } from './AiTextPolicyPlanner';
import { AiPlacementScorer } from './AiPlacementScorer';
import { AiBoardProbe } from './AiBoardProbe';
import { aiOp } from '../runtime/AiLog';
import { Weapon2TileType } from 'db://assets/mx/module/merge';
import { WeaponComposeConfigProvider } from '../../bag/WeaponComposeConfigProvider';
import { isNonCombatWeapon1 } from '../../heros/HeroActorRes';
/** 需求 6.1:成将 → 铲 → 升级(含包内) → 摆放 → 取舍/字策略 → 重摆(避免重摆占满拍导致背包积压) */
export class AiActionPlanner {
private readonly _probe = new AiBoardProbe();
private readonly _shovel = new AiShovelPlanner();
private readonly _swap = new AiSwapPlanner();
private readonly _text = new AiTextPolicyPlanner();
private readonly _place = new AiPlacementScorer();
/** 普通字策略(供 Controller 在 tick 初刷新 focus) */
get textPlanner(): AiTextPolicyPlanner {
return this._text;
}
/**
* AI 空 Put 格数量(供刷新门禁)。
* @param ctx 决策上下文
*/
countEmptyTextPut(ctx: AiPlannerContext): number {
return this._probe.countEmptyTextPut(ctx.bagSession, ctx.map);
}
/**
* 规划下一步原子动作;无可执行时返回 null。
* 有铲先铲,再升级,再摆放;多格神将可贴路重摆优先于落 1–3 级单字。
* @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 merge = this._probe.findMergeFormAction(ctx.bagSession, ctx.map);
if (merge) {
return this._logAction('合成', { kind: AiActionKind.Merge, merge }, emptyPut, dig);
}
const assemble = this._probe.findAssembleHeroAction(ctx.bagSession, ctx.map);
if (assemble && assemble.moves.length > 0) {
return this._logAction(
`拼装 steps=${assemble.moves.length}`,
{
kind: AiActionKind.Merge,
merge: assemble.moves[0],
moves: assemble.moves,
},
emptyPut,
dig,
);
}
const shovel = this._shovel.plan(ctx);
if (shovel) return this._logAction('铲子', shovel, 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);
const multiRepo = this._probe.findMultiCellHeroRepositionAction(ctx.bagSession, ctx.map);
// 合成神将贴路优先于 1–3 级单字落子:有可贴路重摆时先挪神将
if (multiRepo && multiRepo.moves.length > 0 && this._shouldDeferPlaceForHeroRepo(ctx, place)) {
return this._logAction(
`阵型重摆(神将优先) steps=${multiRepo.moves.length}`,
{
kind: AiActionKind.Reposition,
merge: multiRepo.moves[0],
moves: multiRepo.moves,
},
emptyPut,
dig,
);
}
if (place) return this._logAction('摆放', place, emptyPut, dig);
// 有空位却摆放规划失败:仍尝试取舍,避免背包积压空转
const swap = this._swap.plan(ctx, emptyPut > 0);
if (swap) return this._logAction(emptyPut > 0 ? '取舍(空位摆失败)' : '取舍', swap, emptyPut, dig);
const text = this._text.plan(ctx);
if (text) return this._logAction('字策略', text, emptyPut, dig);
const dedupe = this._probe.findDuplicateComboRetractAction(ctx.bagSession, ctx.map);
if (dedupe) {
return this._logAction('散字收回', { kind: AiActionKind.Merge, merge: dedupe }, emptyPut, dig);
}
const dupReplace = this._probe.findDuplicateComboReplaceAction(ctx.bagSession, ctx.map);
if (dupReplace) {
return this._logAction('散字替换', { kind: AiActionKind.Swap, merge: dupReplace }, emptyPut, dig);
}
const reposition = this._probe.findHeroRepositionAction(ctx.bagSession, ctx.map);
if (reposition && reposition.moves.length > 0) {
return this._logAction(
`阵型重摆 steps=${reposition.moves.length}`,
{
kind: AiActionKind.Reposition,
merge: reposition.moves[0],
moves: reposition.moves,
},
emptyPut,
dig,
);
}
aiOp(`本拍无动作 emptyPut=${emptyPut} dig=${dig}`);
return null;
}
/**
* 当前落子是否应让位于多格神将贴路重摆。
* 无落子 / 1–3 级普通单字 / 无攻击功能字 → 让位;高等级单字与组合散字仍可先摆。
* @param ctx 上下文
* @param place 落子规划
*/
private _shouldDeferPlaceForHeroRepo(ctx: AiPlannerContext, place: AiPlannedAction | null): boolean {
if (!place?.merge || place.merge.source.kind !== 'bag') return true;
const slot = ctx.bagSession.getBagSlots()[place.merge.source.slotIndex];
if (!slot) return true;
WeaponComposeConfigProvider.instance.load();
const w2Type = Number(WeaponComposeConfigProvider.instance.getWeapon2Type(slot.weapon2Id)) as Weapon2TileType;
const w1 = this._probe.resolveWeapon1Id(slot) ?? 0;
if (isNonCombatWeapon1(w1)) return true;
if (w2Type !== Weapon2TileType.Single) return false;
const lv = Math.max(1, Math.floor(Number(slot.level) || 1));
return lv <= 3;
}
/**
* 仅规划落子(互换/升级后同拍回填空位)。
* @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}`;
}
if (action.moves && action.moves.length > 1) {
detail += ` moves=${action.moves.length}`;
}
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] : [];
}
}