QA and tune early campaign battles
This commit is contained in:
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user