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

116 lines
5.5 KiB

import { Weapon2TileType, canDragMapTile, MapCellKind } from 'db://assets/mx/module/merge';
import type { AiPlannerContext, AiPlannedAction } from '../types/AiTypes';
import { AiActionKind } from '../types/AiTypes';
import { AiBoardProbe } from './AiBoardProbe';
import { WeaponComposeConfigProvider } from '../../bag/WeaponComposeConfigProvider';
import { aiOp } from '../runtime/AiLog';
/** 需求 6.4 占格互换(空位不足时) */
export class AiSwapPlanner {
private readonly _probe = new AiBoardProbe();
/**
* 空位不足时尝试互换;优先挤更低档位(1级普通 → 紫)。
* 背包有铲且仍有 Dig 时不互换(本拍应由铲子优先占用);无铲时允许互换,避免 Dig 未开完且刷不起包时长期空转。
* @param ctx 决策上下文
* @param allowWithEmptyPut 摆放已失败时允许有空位仍取舍(解背包积压)
*/
plan(ctx: AiPlannerContext, allowWithEmptyPut = false): AiPlannedAction | null {
const emptyPut = this._probe.countEmptyTextPut(ctx.bagSession, ctx.map);
if (emptyPut > 0 && !allowWithEmptyPut) {
aiOp(`取舍跳过 emptyPut=${emptyPut}(有空位不互换)`);
return null;
}
if (emptyPut > 0 && allowWithEmptyPut) {
aiOp(`取舍放宽 emptyPut=${emptyPut}(摆放失败强制取舍)`);
}
const digs = this._probe.listEmptyDigGrids(ctx.bagSession, ctx.map);
const shovelSlot = this._probe.findShovelSlotIndex(ctx.bagSession);
if (digs.length > 0 && shovelSlot >= 0) {
aiOp(`取舍跳过 dig=${digs.length} 有铲,留给铲子`);
return null;
}
WeaponComposeConfigProvider.instance.load();
const provider = WeaponComposeConfigProvider.instance;
const slots = ctx.bagSession.getBagSlots();
const grids = ctx.map.mapGrids;
if (!Array.isArray(grids)) return null;
let best: { slotIndex: number; gridIndex: number; rank: number; inQ: number; occW2: number; occLv: number } | null = null;
let skippedCompose = 0;
for (let s = 0; s < slots.length; s++) {
const slot = slots[s];
if (!slot) continue;
const srcType = Number(provider.getWeapon2Type(slot.weapon2Id)) as Weapon2TileType;
if (srcType !== Weapon2TileType.Single && srcType !== Weapon2TileType.Combo) continue;
for (let i = 0; i < grids.length; i++) {
const mg = grids[i];
if (!mg?.node?.isValid || !mg.node.active) continue;
if (!ctx.bagSession.validator.isAllowedCampForPlace(mg.Index)) continue;
if (ctx.bagSession.merge.map.getCellKind(mg.Index) !== MapCellKind.Put) continue;
const tile = ctx.bagSession.merge.map.getTile(mg.Index);
if (!tile) continue;
// 多格成将占格禁止被橙/紫互换拆掉(否则 DeploySync sell 神将,表现为「合成了但不打」)
const compose = this._probe.findComposeAtGrid(ctx.bagSession, mg.Index);
if (compose && compose.gridIndices.length > 1) {
skippedCompose++;
continue;
}
const dstType = Number(provider.getWeapon2Type(tile.weapon2Id)) as Weapon2TileType;
if (dstType !== Weapon2TileType.Single && dstType !== Weapon2TileType.Combo) continue;
if (!canDragMapTile(tile, dstType, false)) continue;
if (this._probe.canHeroReplaceHero(slot, tile)) {
const inQ = Number(provider.getWeapon2Config(slot.weapon2Id)?.quality) || 0;
if (!best || -1 < best.rank) {
best = {
slotIndex: s,
gridIndex: mg.Index,
rank: -1,
inQ,
occW2: tile.weapon2Id,
occLv: tile.level ?? 1,
};
}
continue;
}
const rank = this._probe.qualitySwapTargetRank(slot, tile, ctx.bagSession, mg.Index);
if (rank < 0) continue;
const inQ = Number(provider.getWeapon2Config(slot.weapon2Id)?.quality) || 0;
if (!best
|| rank < best.rank
|| (rank === best.rank && s < best.slotIndex)
|| (rank === best.rank && s === best.slotIndex && mg.Index < best.gridIndex)) {
best = {
slotIndex: s,
gridIndex: mg.Index,
rank,
inQ,
occW2: tile.weapon2Id,
occLv: tile.level ?? 1,
};
}
}
}
if (!best) {
aiOp(`取舍无候选 emptyPut=0 dig=${digs.length} skipCompose=${skippedCompose}`);
return null;
}
aiOp(
`取舍执行 bag[${best.slotIndex}] q=${best.inQ}`
+ ` -> map[${best.gridIndex}] w2=${best.occW2} lv=${best.occLv} rank=${best.rank}`
+ ` dig=${digs.length} skipCompose=${skippedCompose}`,
);
return {
kind: AiActionKind.Swap,
merge: {
source: { kind: 'bag', slotIndex: best.slotIndex },
target: { kind: 'map', gridIndex: best.gridIndex },
},
};
}
}