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

197 lines
7.9 KiB

import { Weapon2TileType } from 'db://assets/mx/module/merge';
import type { AiPlannerContext, AiPlannedAction } from '../types/AiTypes';
import { AiActionKind } from '../types/AiTypes';
import { AiTextPolicyType } from '../runtime/AiBattlePersonality';
import { AiBoardProbe } from './AiBoardProbe';
import { WeaponComposeConfigProvider } from '../../bag/WeaponComposeConfigProvider';
import { aiOp } from '../runtime/AiLog';
/** 需求 6.3 / 6.3.1 普通字策略(含单字冲级) */
export class AiTextPolicyPlanner {
private readonly _probe = new AiBoardProbe();
/**
* 每个 tick 开始时更新单字冲级目标。
* @param ctx 决策上下文
*/
refreshFocus(ctx: AiPlannerContext): void {
if (ctx.personality.textPolicy !== AiTextPolicyType.Type2) {
ctx.textFocusWeapon2Id = null;
return;
}
ctx.textFocusWeapon2Id = this._pickFocusWeapon2Id(ctx);
}
/**
* 尝试规划一步普通字整理动作。
* @param ctx 决策上下文
*/
plan(ctx: AiPlannerContext): AiPlannedAction | null {
if (ctx.personality.textPolicy === AiTextPolicyType.Type2) {
return this._planFocusPlace(ctx);
}
return this._planKeepHighLevel(ctx);
}
/** 类型2:优先把目标字从背包落到空位 */
private _planFocusPlace(ctx: AiPlannerContext): AiPlannedAction | null {
const focus = ctx.textFocusWeapon2Id;
if (focus == null || focus <= 0) return null;
const slots = ctx.bagSession.getBagSlots();
let bestSlot = -1;
let bestLv = -1;
for (let i = 0; i < slots.length; i++) {
const slot = slots[i];
if (!slot || slot.weapon2Id !== focus) continue;
const lv = slot.level ?? 1;
if (lv > bestLv) {
bestLv = lv;
bestSlot = i;
}
}
if (bestSlot < 0) return null;
const slot = slots[bestSlot]!;
const w2Type = Number(WeaponComposeConfigProvider.instance.getWeapon2Type(slot.weapon2Id)) as Weapon2TileType;
const grids = ctx.map.mapGrids;
if (!Array.isArray(grids)) return null;
let bestGrid = -1;
let bestScore = -1e99;
const w1 = this._probe.resolveWeapon1Id(slot) ?? 0;
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 (!this._probe.canDropTextOnEmptyGrid(ctx.bagSession, mg, w2Type)) continue;
const score = this._probe.formationScore(ctx.map, mg, w1);
if (score > bestScore) {
bestScore = score;
bestGrid = mg.Index;
}
}
if (bestGrid < 0) return null;
const mg = ctx.map.getMapGridByIndex(bestGrid);
if (!mg) return null;
aiOp(
`字策略(冲级) bag[${bestSlot}] w2=${focus} lv=${bestLv}`
+ ` -> map[${bestGrid}] rc=(${mg.Row},${mg.Col}) form=${Math.round(bestScore)}`,
);
return {
kind: AiActionKind.TextPolicy,
merge: {
source: { kind: 'bag', slotIndex: bestSlot },
target: { kind: 'map', gridIndex: bestGrid },
},
place: { weapon1Id: w1, level: slot.level ?? 1, row: mg.Row, col: mg.Col },
};
}
/** 类型1:优先落局内等级最高的普通字 */
private _planKeepHighLevel(ctx: AiPlannerContext): AiPlannedAction | null {
WeaponComposeConfigProvider.instance.load();
const slots = ctx.bagSession.getBagSlots();
let bestSlot = -1;
let bestLv = -1;
for (let i = 0; i < slots.length; i++) {
const slot = slots[i];
if (!slot) continue;
if (Number(WeaponComposeConfigProvider.instance.getWeapon2Type(slot.weapon2Id)) !== Weapon2TileType.Single) {
continue;
}
const lv = slot.level ?? 1;
if (lv > bestLv) {
bestLv = lv;
bestSlot = i;
}
}
if (bestSlot < 0) return null;
const slot = slots[bestSlot]!;
const w2Type = Weapon2TileType.Single;
const grids = ctx.map.mapGrids;
if (!Array.isArray(grids)) return null;
let bestGrid = -1;
let bestScore = -1e99;
const w1 = this._probe.resolveWeapon1Id(slot) ?? 0;
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 (!this._probe.canDropTextOnEmptyGrid(ctx.bagSession, mg, w2Type)) continue;
const score = this._probe.formationScore(ctx.map, mg, w1);
if (score > bestScore) {
bestScore = score;
bestGrid = mg.Index;
}
}
if (bestGrid < 0) return null;
const mg = ctx.map.getMapGridByIndex(bestGrid);
if (!mg) return null;
aiOp(
`字策略(保高等级) bag[${bestSlot}] w2=${slot.weapon2Id} lv=${bestLv}`
+ ` -> map[${bestGrid}] rc=(${mg.Row},${mg.Col}) form=${Math.round(bestScore)}`,
);
return {
kind: AiActionKind.TextPolicy,
merge: {
source: { kind: 'bag', slotIndex: bestSlot },
target: { kind: 'map', gridIndex: bestGrid },
},
place: { weapon1Id: w1, level: slot.level ?? 1, row: mg.Row, col: mg.Col },
};
}
private _pickFocusWeapon2Id(ctx: AiPlannerContext): number | null {
WeaponComposeConfigProvider.instance.load();
const bestById: { id: number; level: number }[] = [];
const slots = ctx.bagSession.getBagSlots();
for (let i = 0; i < slots.length; i++) {
const slot = slots[i];
if (slot) this._accumulateFocusCandidate(bestById, slot.weapon2Id, slot.level ?? 1);
}
const mapIndices = ctx.bagSession.merge.map.gridIndices;
for (let i = 0; i < mapIndices.length; i++) {
const tile = ctx.bagSession.merge.map.getTile(mapIndices[i]);
if (tile) this._accumulateFocusCandidate(bestById, tile.weapon2Id, tile.level ?? 1);
}
if (bestById.length <= 0) return null;
this._sortFocusCandidates(bestById);
return bestById[0].id;
}
/**
* 按等级降序、id 升序排列冲级候选。
* @param bestById 候选表
*/
private _sortFocusCandidates(bestById: { id: number; level: number }[]): void {
for (let i = 0; i < bestById.length; i++) {
for (let j = i + 1; j < bestById.length; j++) {
const a = bestById[i];
const b = bestById[j];
const swap = b.level > a.level || (b.level === a.level && b.id < a.id);
if (!swap) continue;
bestById[i] = b;
bestById[j] = a;
}
}
}
/**
* 累计未满级普通字的最高等级。
* @param bestById 结果表
* @param weapon2Id Text.id
* @param level 局内等级
*/
private _accumulateFocusCandidate(bestById: { id: number; level: number }[], weapon2Id: number, level: number): void {
const provider = WeaponComposeConfigProvider.instance;
if (Number(provider.getWeapon2Type(weapon2Id)) !== Weapon2TileType.Single) return;
const w1 = this._probe.resolveWeapon1Id({ weapon2Id });
const max = w1 != null ? provider.getMaxWeaponLevel(w1) : 1;
if (level >= max) return;
for (let i = 0; i < bestById.length; i++) {
if (bestById[i].id === weapon2Id) {
if (level > bestById[i].level) bestById[i].level = level;
return;
}
}
bestById.push({ id: weapon2Id, level });
}
}