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.
182 lines
7.6 KiB
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] : [];
|
|
}
|
|
}
|
|
|