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/runtime/AiHeadlessBagHost.ts

311 lines
12 KiB

import type { Node } from 'cc';
import { BattleBagSession } from '../../bag/BattleBagSession';
import { aiLog, aiOp, aiWarn } from './AiLog';
import { DeploySync } from '../../runtime/DeploySync';
import type { ProjectBattleHandle } from '../../runtime/BattleRuntime';
import type { MapManager } from '../../map/legacy/MapManager';
import type { AiMatchProfile } from '../match/AiMatchProfile';
import { AI_PLACE_CAMP, AI_PLAYER_ID, AI_VIEW_LOCAL_CAMP } from './AiCampArea';
import { WeaponComposeConfigProvider } from '../../bag/WeaponComposeConfigProvider';
import { textDisplayName } from '../../config/BattleHeroTable';
import { TextListCellState, TextListLandHost } from '../../view/TextListLandHost';
import { TableNames } from '../../../game/ConfigTableData';
/** playerId=1 无头 BattleBagSession:与玩家同套大地块 + 召唤补空 + 五行数字合成 */
export class AiHeadlessBagHost {
/** 对手 playerId */
static readonly AI_PLAYER_ID = AI_PLAYER_ID;
private _bagSession: BattleBagSession | null = null;
private _deploySync: DeploySync | null = null;
private _battle: ProjectBattleHandle | null = null;
private readonly _land = new TextListLandHost();
/** 无头背包会话;未 bind 时为 null */
get bagSession(): BattleBagSession | null {
return this._bagSession;
}
/** AI 部署同步 */
get deploySync(): DeploySync | null {
return this._deploySync;
}
/** AI 背包地块(与玩家独立进度) */
get land(): TextListLandHost {
return this._land;
}
/**
* 绑定战斗、建池、开局待开启填字(对齐玩家 autoFill)。
* @param profile 本局匹配档案
* @param battle 战斗句柄
* @param map 地图
* @param bagLayout 玩家 ui_textList(槽位数与邻格几何;不改其底图)
*/
bind(profile: AiMatchProfile, battle: ProjectBattleHandle, map: MapManager, bagLayout?: Node | null): boolean {
this.dispose();
if (!map?.gridsInitialized) {
aiLog('[AiHeadlessBagHost] bind failed: map not ready');
return false;
}
this._logCampPutGrids(map);
const levelId = battle.levelConfigId;
this._land.bindHeadless(levelId, bagLayout ?? null);
const slotCount = this._resolveSlotCount(levelId, bagLayout ?? null);
if (slotCount <= 1) {
aiWarn(`AI 背包槽数非法 slotCount=${slotCount}`);
return false;
}
const bag = new BattleBagSession(map, levelId, battle.session, slotCount, {
rankId: profile.opponentRankId,
ownerPlayerId: AiHeadlessBagHost.AI_PLAYER_ID,
});
bag.landStateOf = (slotIndex) => this._land.stateOf(slotIndex);
bag.autoFillOpenSlots();
const deploy = new DeploySync();
deploy.bind(battle, map, bag, AiHeadlessBagHost.AI_PLAYER_ID, false);
this._bagSession = bag;
this._deploySync = deploy;
this._battle = battle;
const aiPlayer = battle.getPlayer(AiHeadlessBagHost.AI_PLAYER_ID);
if (aiPlayer) {
aiPlayer.attachBagSession(bag);
} else {
console.warn('[AiHeadlessBagHost] AI Player missing, compositions query unavailable');
}
// 开局立刻召唤一次:待开启格不能拖,必须先有开启格数字才能合成/解锁地块
const summon = bag.tryFillEmptyOpenSlots(battle);
if (summon === 'ok') {
bag.applyDigitDoubleAfterRefresh(bag.lastFilledSlotIndices());
}
console.warn(
'[AiHeadlessBagHost] bound',
`aiRank=${profile.opponentRankId}`,
`gold=${battle.session.players[AiHeadlessBagHost.AI_PLAYER_ID]?.gold}`,
`slots=${slotCount}`,
`summon=${summon}`,
`placeCamp=${AI_PLACE_CAMP}`,
);
this._logBagSlots('firstFill+summon');
return true;
}
/**
* 召唤:对齐玩家 tryFillEmptyOpenSlots(空开启格补字,最多 5)。
*/
tryRefresh(): 'ok' | 'not_enough' | 'failed' | 'pool_empty' | 'no_slot' {
if (!this._bagSession || !this._battle) return 'failed';
const gold = this._battle.session.players[AiHeadlessBagHost.AI_PLAYER_ID]?.gold ?? -1;
const cost = this._bagSession.normalRefreshCost;
if (gold < cost) {
return 'not_enough';
}
const result = this._bagSession.tryFillEmptyOpenSlots(this._battle);
if (result === 'ok') {
this._bagSession.applyDigitDoubleAfterRefresh(this._bagSession.lastFilledSlotIndices());
this._logBagSlots('summon');
} else if (result !== 'no_slot' && result !== 'not_enough') {
aiOp(`召唤结果 result=${result} aiGold=${gold} need=${cost}`);
}
return result;
}
/**
* 卡死时丢掉 1 个最低级开启格数字,腾出召唤空位(待开启格不丢)。
* @returns 是否丢弃成功
*/
discardStuckOpenSlot(): boolean {
const bag = this._bagSession;
if (!bag) return false;
const slots = bag.getBagSlots();
let best = -1;
let bestLv = 1e9;
for (let i = 0; i < slots.length; i++) {
const slot = slots[i];
if (!slot || !(slot.weapon2Id > 0)) continue;
if (this._land.stateOf(i) !== TextListCellState.Open) continue;
const lv = Math.floor(Number(slot.level) || 0);
if (!(lv > 0)) continue;
if (lv < bestLv || (lv === bestLv && i < best)) {
bestLv = lv;
best = i;
}
}
if (best < 0) return false;
if (!bag.discardBagSlot(best)) return false;
aiOp(`卡死丢弃 bag[${best}] Lv${bestLv} 腾召唤位`);
return true;
}
/**
* 背包数字合成落到待开启格后:推进地块并给新生待开启格补字。
* @param slotIndex 合成目标槽
*/
onBagMergedAt(slotIndex: number): void {
const bag = this._bagSession;
if (!bag) return;
if (this._land.stateOf(slotIndex) !== TextListCellState.OpenNoDrag) return;
const promoted = this._land.promoteAround(slotIndex);
bag.fillGateUnlockSlots(promoted.born, promoted.levelAdd);
if (promoted.born.length > 0) {
aiOp(`AI 待开启推进 slot=${slotIndex} born=${promoted.born.join(',')}`);
this._logBagSlots('gateUnlock');
}
}
/** 下次召唤费用(按当前空开启格相对满额 5 格折算) */
get nextRefreshCost(): number {
return this._bagSession?.normalRefreshCost ?? -1;
}
/** AI 当前桃子 */
get aiGold(): number {
return this._battle?.session.players[AiHeadlessBagHost.AI_PLAYER_ID]?.gold ?? -1;
}
/**
* 背包参与移动后打印源目标与当前槽位。
* @param tag 动作标签
* @param source 拖拽源
* @param target 放置目标
*/
logBagMove(tag: string, source: { kind: string; slotIndex?: number; gridIndex?: number }, target: { kind: string; slotIndex?: number; gridIndex?: number }): void {
const src = source.kind === 'bag'
? `bag[${source.slotIndex}]`
: `map[${source.gridIndex}]`;
const dst = target.kind === 'bag'
? `bag[${target.slotIndex}]`
: `map[${target.gridIndex}]`;
this._logBagSlots(`${tag} ${src}->${dst}`);
}
/** 释放背包与会话引用 */
dispose(): void {
const aiPlayer = this._battle?.getPlayer(AiHeadlessBagHost.AI_PLAYER_ID);
if (aiPlayer) {
aiPlayer.attachBagSession(null);
aiPlayer.skillController = null;
}
this._deploySync?.unbind();
this._deploySync = null;
this._bagSession = null;
this._battle = null;
this._land.clear();
}
/**
* 槽位数 = max(布局格子下标, 关卡地块下标) + 1。
* @param levelId 关卡
* @param bagLayout 布局
*/
private _resolveSlotCount(levelId: number, bagLayout: Node | null): number {
let maxIndex = 0;
if (bagLayout?.isValid) {
const children = bagLayout.children;
for (let i = 0; i < children.length; i++) {
const idx = TextListLandHost.indexOfNode(children[i]);
if (idx != null && idx > maxIndex) maxIndex = idx;
}
}
const row = gg.data.table.getTableData<ITableLevel>(TableNames.Level, levelId);
if (row) {
maxIndex = Math.max(maxIndex, this._maxLandIndex(row.normalland), this._maxLandIndex(row.tolockland));
}
if (maxIndex <= 0) {
aiWarn(`无法解析 AI 背包槽数 levelId=${levelId}`);
return 0;
}
return maxIndex + 1;
}
/**
* @param raw 地块下标表
*/
private _maxLandIndex(raw: readonly number[] | undefined): number {
if (!raw?.length) return 0;
let max = 0;
for (let i = 0; i < raw.length; i++) {
const n = Math.floor(Number(raw[i]));
if (Number.isFinite(n) && n > max) max = n;
}
return max;
}
/** 打印当前 AI 背包有字槽(中文名) */
private _logBagSlots(tag: string): void {
const bag = this._bagSession;
if (!bag) {
aiOp(`背包[${tag}] session missing`);
return;
}
WeaponComposeConfigProvider.instance.load();
const slots = bag.getBagSlots();
const parts: string[] = [];
let filled = 0;
for (let i = 0; i < slots.length; i++) {
const s = slots[i];
if (!s) continue;
filled++;
const land = this._land.stateOf(i);
parts.push(`[${i}|${land}]=${this._resolveTileName(s.weapon2Id)}Lv${s.level ?? 1}`);
}
aiOp(
`背包[${tag}] filled=${filled}/${slots.length} gold=${this._battle?.session.players[AiHeadlessBagHost.AI_PLAYER_ID]?.gold}`
+ ` | ${parts.join(' ')}`,
);
}
/** Text 表中文名;缺表回退 id */
private _resolveTileName(weapon2Id: number): string {
const row = WeaponComposeConfigProvider.instance.getWeapon2Config(weapon2Id);
if (!row) return `id=${weapon2Id}`;
const name = textDisplayName(row).trim();
return name || `id=${weapon2Id}`;
}
/** 开战打印各 CampArea 可放字格 / 可摆格数量 */
private _logCampPutGrids(map: MapManager): void {
const grids = map.mapGrids;
if (!Array.isArray(grids)) {
aiLog('[AiHeadlessBagHost] mapGrids missing');
return;
}
let localText = 0;
let enemyText = 0;
let localGrid = 0;
let enemyGrid = 0;
let localWalk = 0;
let enemyWalk = 0;
let dualPutDig = 0;
for (let i = 0; i < grids.length; i++) {
const mg = grids[i];
if (!mg?.node?.isValid || !mg.node.active) continue;
const camp = mg.CampAreaID | 0;
if (mg.isCanPutText && !mg.isCanWalk) {
if (camp === AI_VIEW_LOCAL_CAMP) localText++;
else if (camp === AI_PLACE_CAMP) enemyText++;
if (mg.isCanBeDig && camp === AI_PLACE_CAMP) dualPutDig++;
}
if (mg.isCanPutGrid && mg.isCanWalk) {
if (camp === AI_VIEW_LOCAL_CAMP) localGrid++;
else if (camp === AI_PLACE_CAMP) enemyGrid++;
}
if (mg.isCanWalk) {
if (camp === AI_VIEW_LOCAL_CAMP) localWalk++;
else if (camp === AI_PLACE_CAMP) enemyWalk++;
}
}
aiLog(
`[AiHeadlessBagHost] mapCamp textPut: local(1)=${localText} enemy(2)=${enemyText}`
+ ` | gridPut: local(1)=${localGrid} enemy(2)=${enemyGrid}`
+ ` | walk: local(1)=${localWalk} enemy(2)=${enemyWalk}`,
);
if (dualPutDig > 0) {
aiWarn(`地图 dig+putText 双标 enemy=${dualPutDig}(已按可放置优先当 Put)`);
}
if (enemyText <= 0) {
aiLog('[AiHeadlessBagHost] enemy CampAreaID=2 has no isCanPutText cell; AI text cannot place');
}
}
}