QA and tune early campaign battles

This commit is contained in:
2026-06-27 10:03:18 +09:00
parent b74332973b
commit 2ebd5fc6c3
7 changed files with 255 additions and 56 deletions

View File

@@ -6,6 +6,76 @@ const headless = process.env.QA_HEADLESS !== '0';
const maxRounds = Number(process.env.QA_MAX_ROUNDS ?? 80);
const pnpmCommand = process.env.PNPM_CMD ?? 'pnpm';
const coreBrothers = ['liu-bei', 'guan-yu', 'zhang-fei'];
const xuzhouFullSortie = ['liu-bei', 'guan-yu', 'zhang-fei', 'jian-yong', 'mi-zhu'];
const earlyCampaignBattles = [
{ no: 1, id: 'first-battle-zhuo-commandery', selected: [], targets: ['rebel-leader'], protected: ['liu-bei'] },
{
no: 2,
id: 'second-battle-yellow-turban-pursuit',
selected: coreBrothers,
targets: ['pursuit-leader-han-seok'],
protected: ['liu-bei']
},
{
no: 3,
id: 'third-battle-guangzong-road',
selected: coreBrothers,
targets: ['guangzong-leader-ma-yuan'],
protected: ['liu-bei']
},
{
no: 4,
id: 'fourth-battle-guangzong-camp',
selected: coreBrothers,
targets: ['guangzong-main-leader-zhang-jue'],
protected: ['liu-bei']
},
{
no: 5,
id: 'fifth-battle-sishui-vanguard',
selected: coreBrothers,
targets: ['sishui-leader-hu-zhen'],
protected: ['liu-bei']
},
{
no: 6,
id: 'sixth-battle-jieqiao-relief',
selected: coreBrothers,
targets: ['jieqiao-leader-qu-yi'],
protected: ['liu-bei']
},
{
no: 7,
id: 'seventh-battle-xuzhou-rescue',
selected: coreBrothers,
targets: ['xuzhou-leader-xiahou-dun'],
protected: ['liu-bei']
},
{
no: 8,
id: 'eighth-battle-xiaopei-supply-road',
selected: xuzhouFullSortie,
targets: ['xiaopei-leader-gao-shun'],
protected: ['liu-bei']
},
{
no: 9,
id: 'ninth-battle-xuzhou-gate-night-raid',
selected: xuzhouFullSortie,
targets: ['xuzhou-leader-cao-bao'],
protected: ['liu-bei']
},
{
no: 10,
id: 'tenth-battle-xuzhou-breakout',
selected: xuzhouFullSortie,
targets: ['xuzhou-escape-leader-song-xian'],
protected: ['liu-bei']
}
];
const allRepresentativeBattles = [
{ no: 1, id: 'first-battle-zhuo-commandery', selected: [], targets: ['rebel-leader'], protected: ['liu-bei'] },
{
@@ -66,6 +136,16 @@ const allRepresentativeBattles = [
}
];
const qaBattleSets = {
representative: allRepresentativeBattles,
early: earlyCampaignBattles
};
const qaSetName = process.env.QA_SET ?? 'representative';
const qaBattleSet = qaBattleSets[qaSetName];
if (!qaBattleSet) {
throw new Error(`Unknown QA_SET "${qaSetName}". Use one of: ${Object.keys(qaBattleSets).join(', ')}`);
}
const requestedBattles = new Set(
(process.env.QA_BATTLES ?? '')
.split(',')
@@ -74,8 +154,8 @@ const requestedBattles = new Set(
);
const representativeBattles =
requestedBattles.size > 0
? allRepresentativeBattles.filter((battle) => requestedBattles.has(String(battle.no)) || requestedBattles.has(battle.id))
: allRepresentativeBattles;
? qaBattleSet.filter((battle) => requestedBattles.has(String(battle.no)) || requestedBattles.has(battle.id))
: qaBattleSet;
let serverProcess;
@@ -242,11 +322,28 @@ async function playBattleWithoutDebugVictory(page, battle) {
}
function focusTarget(unit) {
const enemies = liveUnits('enemy');
const canProgressTo = (enemy) => {
if (scene.canAttack(unit, enemy)) {
return true;
}
const approachTile = scene.chooseApproachTile?.(unit, enemy);
return Boolean(approachTile && (approachTile.x !== unit.x || approachTile.y !== unit.y));
};
const progressEnemies = enemies.filter(canProgressTo);
const nearestEnemy = progressEnemies.sort((a, b) => distance(unit, a) - distance(unit, b) || priorityForTarget(b) - priorityForTarget(a))[0];
const aliveTargets = targets.map((targetId) => unitRef(targetId)).filter((target) => target && target.hp > 0);
if (aliveTargets.length > 0) {
return aliveTargets.sort((a, b) => distance(unit, a) - distance(unit, b))[0];
const reachableTargets = aliveTargets.filter(canProgressTo);
const nearestTarget = (reachableTargets.length > 0 ? reachableTargets : aliveTargets).sort((a, b) => distance(unit, a) - distance(unit, b))[0];
if (nearestEnemy && nearestEnemy.id !== nearestTarget.id && distance(unit, nearestEnemy) + 5 < distance(unit, nearestTarget)) {
return nearestEnemy;
}
return nearestTarget;
}
return liveUnits('enemy').sort((a, b) => priorityForTarget(b) - priorityForTarget(a) || distance(unit, a) - distance(unit, b))[0];
return (progressEnemies.length > 0 ? progressEnemies : enemies).sort(
(a, b) => priorityForTarget(b) - priorityForTarget(a) || distance(unit, a) - distance(unit, b)
)[0];
}
function attackChoice(unit) {
@@ -300,13 +397,12 @@ async function playBattleWithoutDebugVictory(page, battle) {
}
const focus = focusTarget(unit);
const enemies = liveUnits('enemy');
const vanguardInPosition = liveUnits('ally').some(
(ally) => ally.id !== unit.id && !protectedIds.has(ally.id) && (!focus || distance(ally, focus) <= 10)
);
return (
!vanguardInPosition ||
(focus && targetIds.has(focus.id) && focus.hp <= Math.ceil(focus.maxHp * 0.3))
);
const vanguards = liveUnits('ally').filter((ally) => ally.id !== unit.id && !protectedIds.has(ally.id));
const vanguardInPosition = vanguards.some((ally) => !focus || distance(ally, focus) <= 10);
if (vanguards.length === 0) {
return unit.hp >= Math.ceil(unit.maxHp * 0.8);
}
return Boolean(vanguardInPosition && focus && targetIds.has(focus.id) && focus.hp <= Math.ceil(focus.maxHp * 0.3));
}
function canTakeAttack(unit, attack, threat = protectedIds.has(unit.id) ? incomingThreat(unit, unit) : 0) {
@@ -367,6 +463,14 @@ async function playBattleWithoutDebugVictory(page, battle) {
const focus = focusTarget(unit);
if (focus) {
const currentDistance = distance(unit, focus);
const approachTile = scene.chooseApproachTile?.(unit, focus);
if (
approachTile &&
(approachTile.x !== unit.x || approachTile.y !== unit.y) &&
(!pressProtected || canPressProtectedTile(unit, approachTile, focus, currentDistance))
) {
return approachTile;
}
const pressureTile = tiles
.filter((tile) => distance(tile, focus) < currentDistance)
.filter((tile) => !pressProtected || canPressProtectedTile(unit, tile, focus, currentDistance))