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/match/AiMatchService.ts

242 lines
11 KiB

3 weeks ago
import type { AiMatchBuildInput } from '../types/AiTypes';
import { aiLog, aiOp } from '../runtime/AiLog';
import type { AiMatchProfile } from './AiMatchProfile';
import { AiBattlebuffPicker } from './AiBattlebuffPicker';
import { RankDataManager } from '../../../manager/RankDataManager';
import { DailyShopDataManager } from '../../../manager/DailyShopDataManager';
import { MatchStreakStats } from '../../../manager/MatchStreakStats';
/** Const 11: 匹配星偏移权重 */
const CONST_STAR_WEIGHT_ID = 11;
/** Const 12: 主动 buff 数量偏移权重 (-1,0,+1) */
const CONST_ACTIVE_OFFSET_ID = 12;
/** Const 14: 被动 buff 数量偏移权重 (-2,-1,0,+1) */
const CONST_PASSIVE_OFFSET_ID = 14;
/** Const 15: 连胜 → 主动 +1 档加成 */
const CONST_WIN_ACTIVE_BONUS_ID = 15;
/** Const 16: 连胜 → 被动 +1 档加成 */
const CONST_WIN_PASSIVE_BONUS_ID = 16;
/** Const 17: 连败 → 主动 -1 档加成 */
const CONST_LOSE_ACTIVE_BONUS_ID = 17;
/** Const 18: 连败 → 被动 -2 档加成 */
const CONST_LOSE_PASSIVE_BONUS_ID = 18;
/** Const 19: 最大主动技能数量 */
const CONST_MAX_ACTIVE_ID = 19;
/** Const 20: 最大被动技能数量 */
const CONST_MAX_PASSIVE_ID = 20;
/** Const 12 主动数量偏移 */
const ACTIVE_OFFSETS = [-1, 0, 1];
/** Const 14 被动数量偏移 */
const PASSIVE_OFFSETS = [-2, -1, 0, 1];
/** 开战前匹配:Const 11/12/14/15~20 -> AiMatchProfile */
export class AiMatchService {
private readonly _buffPicker = new AiBattlebuffPicker();
/**
* 构建本局 AI 对手档案。
* @param input 玩家 buff 数量等入参
* @returns 成功返回档案;配表/权重非法时返回 null
*/
buildProfile(input: AiMatchBuildInput): AiMatchProfile | null {
const playerStars = RankDataManager.getPlayerRankStars();
const playerRankId = RankDataManager.getPlayerRankId();
2 weeks ago
let starMatch = RankDataManager.resolveOpponentStarMatch(playerStars);
3 weeks ago
if (!starMatch) {
2 weeks ago
console.warn(
'[AiMatchService] star match failed,回退玩家段位',
`playerStars=${playerStars} playerRankId=${playerRankId}`,
);
starMatch = {
starOffset: 0,
opponentStars: Math.max(1, playerStars),
opponentRankId: playerRankId > 0 ? playerRankId : 1,
};
3 weeks ago
}
const caps = this._readSlotCaps();
const playerActive = Math.max(0, Math.floor(Number(input.playerActiveBuffCount) || 0));
const playerPassive = Math.max(0, Math.floor(Number(input.playerPassiveBuffCount) || 0));
2 weeks ago
let activeWeights = this._copyWeights(gg.data.table.getConstArray(CONST_ACTIVE_OFFSET_ID), ACTIVE_OFFSETS.length, 'Const12');
let passiveWeights = this._copyWeights(gg.data.table.getConstArray(CONST_PASSIVE_OFFSET_ID), PASSIVE_OFFSETS.length, 'Const14');
if (!activeWeights) {
console.warn('[AiMatchService] Const12 权重无效,用均匀权重');
activeWeights = [1, 1, 1];
}
if (!passiveWeights) {
console.warn('[AiMatchService] Const14 权重无效,用均匀权重');
passiveWeights = [1, 1, 1, 1];
}
3 weeks ago
this._applyStreakWeightBonus(activeWeights, passiveWeights, playerActive, playerPassive, caps);
2 weeks ago
let activeOff = this._rollOffset(activeWeights, ACTIVE_OFFSETS, 'Const12');
let passiveOff = this._rollOffset(passiveWeights, PASSIVE_OFFSETS, 'Const14');
if (activeOff == null) activeOff = 0;
if (passiveOff == null) passiveOff = 0;
3 weeks ago
const activeRaw = playerActive + activeOff;
const passiveRaw = playerPassive + passiveOff;
const activeBuffCount = Math.max(0, Math.min(caps.active, activeRaw));
const passiveBuffCount = Math.max(0, Math.min(caps.passive, passiveRaw));
aiOp(
`AI buff 数量推导`
+ ` 玩家A=${playerActive} 偏移=${activeOff}(权重[${activeWeights.join(',')}]) raw=${activeRaw} → clamp[0,${caps.active}]=${activeBuffCount}`
+ ` | 玩家P=${playerPassive} 偏移=${passiveOff}(权重[${passiveWeights.join(',')}]) raw=${passiveRaw} → clamp[0,${caps.passive}]=${passiveBuffCount}`
+ ` | streakW=${MatchStreakStats.getWinStreak()} streakL=${MatchStreakStats.getLoseStreak()}`,
);
const seed = input.seed != null
? Math.floor(Number(input.seed) || 0)
: ((Date.now() % 100000) ^ (starMatch.opponentStars * 9973));
let activeBuffIds = this._buffPicker.pick(1, activeBuffCount, seed);
let passiveBuffIds = this._buffPicker.pick(2, passiveBuffCount, seed ^ 0x9e3779b9);
2 weeks ago
let resolvedActiveCount = activeBuffIds.length;
let resolvedPassiveCount = passiveBuffIds.length;
// 候选不足时按实际数量建档,禁止整局 AI 因 buff 失败而不启动
if (resolvedActiveCount !== activeBuffCount || resolvedPassiveCount !== passiveBuffCount) {
console.warn(
'[AiMatchService] buff 抽取不足,按实际数量建档',
`active=${activeBuffCount}/${resolvedActiveCount}`,
`passive=${passiveBuffCount}/${resolvedPassiveCount}`,
);
3 weeks ago
}
aiOp(
`AI buff 抽取 seed=${seed}`
2 weeks ago
+ ` active[${resolvedActiveCount}]=[${activeBuffIds.join(',')}]`
+ ` passive[${resolvedPassiveCount}]=[${passiveBuffIds.join(',')}]`,
3 weeks ago
);
const gmActive = gg.game.gmAiActiveBuffIds;
const gmPassive = gg.game.gmAiPassiveBuffIds;
// 正常:上列每局重抽;GM 有值则常驻覆盖(直至 GM 清除 / 游戏 reset)
if (gmActive?.length) {
activeBuffIds = gmActive.slice();
resolvedActiveCount = activeBuffIds.length;
aiOp(`GM 常驻覆盖 AI 主动 buff=[${activeBuffIds.join(',')}]`);
}
if (gmPassive?.length) {
passiveBuffIds = gmPassive.slice();
resolvedPassiveCount = passiveBuffIds.length;
aiOp(`GM 常驻覆盖 AI 被动 buff=[${passiveBuffIds.join(',')}]`);
}
const profile: AiMatchProfile = {
playerRankId,
playerStars,
opponentRankId: starMatch.opponentRankId,
opponentStars: starMatch.opponentStars,
starOffset: starMatch.starOffset,
activeBuffCount: resolvedActiveCount,
passiveBuffCount: resolvedPassiveCount,
activeBuffIds,
passiveBuffIds,
seed,
};
aiOp(
`匹配档案 playerRank=${playerRankId} stars=${playerStars}`
+ ` -> aiRank=${profile.opponentRankId} stars=${profile.opponentStars}`
+ ` buffA=${resolvedActiveCount}[${activeBuffIds.join(',')}]`
+ ` buffP=${resolvedPassiveCount}[${passiveBuffIds.join(',')}]`
+ ` streakW=${MatchStreakStats.getWinStreak()} streakL=${MatchStreakStats.getLoseStreak()}`
+ ` caps=${caps.active}/${caps.passive}`,
);
return profile;
}
/** buff 抽取器 */
get buffPicker(): AiBattlebuffPicker {
return this._buffPicker;
}
/** Const 19/20 槽位上限 */
private _readSlotCaps(): { active: number; passive: number } {
const fromShop = DailyShopDataManager.getSlotCaps();
const active = Math.max(0, Math.floor(Number(gg.data.table.getConst(CONST_MAX_ACTIVE_ID)) || 0));
const passive = Math.max(0, Math.floor(Number(gg.data.table.getConst(CONST_MAX_PASSIVE_ID)) || 0));
if (active <= 0 || passive <= 0) {
aiLog(`[AiMatchService] Const19/20 invalid, use DailyShop caps active=${active} passive=${passive} shopA=${fromShop.active} shopP=${fromShop.passive}`);
return {
active: Math.max(1, fromShop.active),
passive: Math.max(1, fromShop.passive),
};
}
return { active, passive };
}
/**
* 复制权重数组;长度不符仍截断/补 0 并 warn。
* @param raw 常量表数组
* @param expectLen 期望档位数
* @param label 日志标签
*/
private _copyWeights(raw: number[], expectLen: number, label: string): number[] | null {
const weights: number[] = [];
for (let i = 0; i < expectLen; i++) {
weights.push(Math.max(0, Math.floor(Number(raw[i]) || 0)));
}
if (raw.length !== expectLen) {
aiLog(`[AiMatchService] ${label} weight length invalid len=${raw.length} expect=${expectLen}`);
}
let total = 0;
for (let i = 0; i < weights.length; i++) total += weights[i];
if (total <= 0) {
aiLog(`[AiMatchService] ${label} total weight <= 0`);
return null;
}
return weights;
}
/**
* Const 15~18:连胜/连败修正 12/14 权重(不写回表)。
* @param activeWeights Const12 三档
* @param passiveWeights Const14 四档
* @param playerActive 玩家主动数
* @param playerPassive 玩家被动数
* @param caps 槽位上限
*/
private _applyStreakWeightBonus(activeWeights: number[], passiveWeights: number[], playerActive: number, playerPassive: number, caps: { active: number; passive: number }): void {
const winShares = MatchStreakStats.effectiveShares(MatchStreakStats.getWinStreak());
const loseShares = MatchStreakStats.effectiveShares(MatchStreakStats.getLoseStreak());
if (winShares > 0) {
const addA = Math.max(0, Math.floor(Number(gg.data.table.getConst(CONST_WIN_ACTIVE_BONUS_ID)) || 0)) * winShares;
const addP = Math.max(0, Math.floor(Number(gg.data.table.getConst(CONST_WIN_PASSIVE_BONUS_ID)) || 0)) * winShares;
// 主动:+1 档 index=2;已满上限则加到 0 档 index=1
const aIdx = playerActive >= caps.active ? 1 : 2;
activeWeights[aIdx] = (activeWeights[aIdx] || 0) + addA;
// 被动:+1 档 index=3;已满上限则加到 0 档 index=2
const pIdx = playerPassive >= caps.passive ? 2 : 3;
passiveWeights[pIdx] = (passiveWeights[pIdx] || 0) + addP;
aiOp(`连胜修正 shares=${winShares} active[+${addA}@${aIdx}] passive[+${addP}@${pIdx}]`);
}
if (loseShares > 0) {
const addA = Math.max(0, Math.floor(Number(gg.data.table.getConst(CONST_LOSE_ACTIVE_BONUS_ID)) || 0)) * loseShares;
const addP = Math.max(0, Math.floor(Number(gg.data.table.getConst(CONST_LOSE_PASSIVE_BONUS_ID)) || 0)) * loseShares;
// 主动:-1 档 index=0;被动:-2 档 index=0
activeWeights[0] = (activeWeights[0] || 0) + addA;
passiveWeights[0] = (passiveWeights[0] || 0) + addP;
aiOp(`连败修正 shares=${loseShares} active[-1 +${addA}] passive[-2 +${addP}]`);
}
}
/** 加权随机偏移;权重全 0 则返回 null */
private _rollOffset(weights: number[], offsets: number[], label: string): number | null {
let total = 0;
for (let i = 0; i < weights.length; i++) total += Math.max(0, weights[i] || 0);
if (total <= 0) {
aiLog(`[AiMatchService] ${label} total weight <= 0`);
return null;
}
let roll = Math.random() * total;
for (let i = 0; i < offsets.length; i++) {
roll -= Math.max(0, weights[i] || 0);
if (roll < 0) return offsets[i];
}
return offsets[offsets.length - 1];
}
}