Expand early campaign release QA to battle 20

This commit is contained in:
2026-07-06 16:32:40 +09:00
parent 272ee005a0
commit 84b16c834b
2 changed files with 193 additions and 13 deletions

View File

@@ -33,8 +33,8 @@
"verify:public-deploy": "node scripts/verify-public-deploy.mjs",
"qa:representative": "node scripts/qa-representative-battles.mjs",
"qa:smoke": "node scripts/qa-representative-battles.mjs --set=smoke",
"qa:early": "node scripts/qa-representative-battles.mjs --set=campaign --battles=1,2,3,4,5,6,7,8,9,10,11,12,13",
"qa:early:repeat": "node scripts/qa-repeat.mjs --set=campaign --battles=1,2,3,4,5,6,7,8,9,10,11,12,13",
"qa:early": "node scripts/qa-representative-battles.mjs --set=campaign --battles=1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20",
"qa:early:repeat": "node scripts/qa-repeat.mjs --set=campaign --battles=1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20",
"deploy:nas": "powershell -NoProfile -ExecutionPolicy Bypass -File scripts/deploy-nas.ps1 -Build",
"deploy:nas:dist": "powershell -NoProfile -ExecutionPolicy Bypass -File scripts/deploy-nas.ps1",
"ship:release": "node scripts/ship-release.mjs"

View File

@@ -197,49 +197,171 @@ const refugeCampaignBattles = [
no: 14,
id: 'fourteenth-battle-cao-break',
selected: sunQianSortie,
targets: ['cao-break-guard-a', 'cao-break-guard-b', 'cao-break-leader-che-zhou'],
targets: [
'cao-break-scout-a',
'cao-break-scout-b',
'cao-break-infantry-a',
'cao-break-infantry-b',
'cao-break-infantry-c',
'cao-break-infantry-d',
'cao-break-archer-a',
'cao-break-archer-b',
'cao-break-archer-c',
'cao-break-cavalry-a',
'cao-break-cavalry-b',
'cao-break-cavalry-c',
'cao-break-guard-a',
'cao-break-guard-b',
'cao-break-leader-che-zhou'
],
protected: ['liu-bei']
},
{
no: 15,
id: 'fifteenth-battle-yuan-refuge-road',
selected: sunQianSortie,
targets: ['yuan-refuge-guard-a', 'yuan-refuge-archer-c', 'yuan-refuge-leader-cai-yang'],
targets: [
'yuan-refuge-scout-a',
'yuan-refuge-scout-b',
'yuan-refuge-infantry-a',
'yuan-refuge-infantry-b',
'yuan-refuge-infantry-c',
'yuan-refuge-infantry-d',
'yuan-refuge-archer-a',
'yuan-refuge-archer-b',
'yuan-refuge-archer-c',
'yuan-refuge-cavalry-a',
'yuan-refuge-cavalry-b',
'yuan-refuge-cavalry-c',
'yuan-refuge-guard-a',
'yuan-refuge-guard-b',
'yuan-refuge-guard-c',
'yuan-refuge-strategist-a',
'yuan-refuge-leader-cai-yang'
],
protected: ['liu-bei']
},
{
no: 16,
id: 'sixteenth-battle-liu-biao-refuge',
selected: liuBiaoSortie,
targets: ['liu-biao-road-leader-cao-ren'],
targets: [
'liu-biao-road-scout-a',
'liu-biao-road-scout-b',
'liu-biao-road-infantry-a',
'liu-biao-road-infantry-b',
'liu-biao-road-infantry-c',
'liu-biao-road-infantry-d',
'liu-biao-road-archer-a',
'liu-biao-road-archer-b',
'liu-biao-road-archer-c',
'liu-biao-road-archer-d',
'liu-biao-road-cavalry-a',
'liu-biao-road-cavalry-b',
'liu-biao-road-cavalry-c',
'liu-biao-road-cavalry-d',
'liu-biao-road-guard-a',
'liu-biao-road-guard-b',
'liu-biao-road-strategist-a',
'liu-biao-road-leader-cao-ren'
],
protected: ['liu-bei']
},
{
no: 17,
id: 'seventeenth-battle-wolong-visit-road',
selected: liuBiaoSortie,
targets: ['wolong-road-leader-cai-xun'],
targets: [
'wolong-road-scout-a',
'wolong-road-scout-b',
'wolong-road-infantry-a',
'wolong-road-infantry-b',
'wolong-road-infantry-c',
'wolong-road-infantry-d',
'wolong-road-archer-a',
'wolong-road-archer-b',
'wolong-road-archer-c',
'wolong-road-cavalry-a',
'wolong-road-cavalry-b',
'wolong-road-guard-a',
'wolong-road-guard-b',
'wolong-road-strategist-a',
'wolong-road-leader-cai-xun'
],
protected: ['liu-bei']
},
{
no: 18,
id: 'eighteenth-battle-bowang-ambush',
selected: zhugeFirstSortie,
targets: ['bowang-leader-xiahou-dun'],
targets: [
'bowang-scout-a',
'bowang-scout-b',
'bowang-infantry-a',
'bowang-infantry-b',
'bowang-infantry-c',
'bowang-infantry-d',
'bowang-archer-a',
'bowang-archer-b',
'bowang-archer-c',
'bowang-cavalry-a',
'bowang-cavalry-b',
'bowang-cavalry-c',
'bowang-guard-a',
'bowang-guard-b',
'bowang-strategist-a',
'bowang-leader-xiahou-dun'
],
protected: ['liu-bei']
},
{
no: 19,
id: 'nineteenth-battle-changban-refuge',
selected: zhugeFirstSortie,
targets: ['changban-infantry-b', 'changban-cavalry-b', 'changban-guard-b', 'changban-leader-cao-chun'],
targets: [
'changban-scout-a',
'changban-scout-b',
'changban-cavalry-a',
'changban-cavalry-b',
'changban-cavalry-c',
'changban-cavalry-d',
'changban-infantry-a',
'changban-infantry-b',
'changban-infantry-c',
'changban-infantry-d',
'changban-archer-a',
'changban-archer-b',
'changban-archer-c',
'changban-guard-a',
'changban-guard-b',
'changban-strategist-a',
'changban-leader-cao-chun'
],
protected: ['liu-bei', 'zhao-yun']
},
{
no: 20,
id: 'twentieth-battle-jiangdong-envoy',
selected: envoySortie,
targets: ['envoy-road-leader-wen-pin'],
targets: [
'envoy-road-scout-a',
'envoy-road-scout-b',
'envoy-road-cavalry-a',
'envoy-road-cavalry-b',
'envoy-road-cavalry-c',
'envoy-road-cavalry-d',
'envoy-road-infantry-a',
'envoy-road-infantry-b',
'envoy-road-infantry-c',
'envoy-road-infantry-d',
'envoy-road-archer-a',
'envoy-road-archer-b',
'envoy-road-archer-c',
'envoy-road-guard-a',
'envoy-road-guard-b',
'envoy-road-strategist-a',
'envoy-road-leader-wen-pin'
],
protected: ['liu-bei', 'zhuge-liang', 'sun-qian']
}
];
@@ -1005,9 +1127,9 @@ async function normalizeRepresentativeBattleUnits(page, battle) {
}
const selectedIds = new Set([...(selected ?? []), ...(protectedUnitIds ?? [])]);
const hpFloor = no >= 28 ? 86 : no >= 21 ? 82 : no >= 18 ? 76 : no >= 11 ? 68 : no >= 2 ? 56 : 0;
const attackFloor = no >= 28 ? 78 : no >= 21 ? 70 : no >= 18 ? 54 : no >= 11 ? 42 : no >= 2 ? 24 : 0;
const statFloor = no >= 28 ? 132 : no >= 21 ? 126 : no >= 18 ? 112 : no >= 11 ? 98 : no >= 2 ? 88 : 0;
const hpFloor = no >= 28 ? 86 : no >= 21 ? 84 : no >= 18 ? 84 : no >= 14 ? 80 : no >= 11 ? 68 : no >= 2 ? 56 : 0;
const attackFloor = no >= 28 ? 78 : no >= 21 ? 70 : no >= 18 ? 64 : no >= 14 ? 60 : no >= 11 ? 42 : no >= 2 ? 24 : 0;
const statFloor = no >= 28 ? 132 : no >= 21 ? 126 : no >= 18 ? 122 : no >= 14 ? 118 : no >= 11 ? 98 : no >= 2 ? 88 : 0;
if (hpFloor <= 0) {
return;
@@ -1440,6 +1562,36 @@ async function playBattleWithoutDebugVictory(page, battle) {
return safeTiles.sort((a, b) => b.score - a.score || a.focusDistance - b.focusDistance || (a.tile.cost ?? 0) - (b.tile.cost ?? 0))[0]?.tile;
}
function scenarioAdvancePoint(unit) {
if (no === 14) {
const objectives = state().objectives ?? [];
const relayDone = objectives.find((objective) => objective.id === 'relay')?.achieved;
const gateDone = objectives.find((objective) => objective.id === 'gate')?.achieved;
if (!relayDone) {
return { x: 18, y: 16 };
}
if (!gateDone) {
if (unit.x < 22 || unit.y > 18) {
return { x: 23, y: 18 };
}
return { x: 25, y: 13 };
}
}
return undefined;
}
function advancePointTile(unit, tiles) {
const point = scenarioAdvancePoint(unit);
if (!point || protectedIds.has(unit.id)) {
return undefined;
}
const currentDistance = distance(unit, point);
return tiles
.filter((tile) => tile.x !== unit.x || tile.y !== unit.y)
.filter((tile) => distance(tile, point) < currentDistance)
.sort((a, b) => distance(a, point) - distance(b, point) || (a.cost ?? 0) - (b.cost ?? 0))[0];
}
function chooseMove(unit) {
const tiles = [{ x: unit.x, y: unit.y, cost: 0 }, ...scene.reachableTiles(unit)];
const pressProtected = shouldPressProtected(unit);
@@ -1481,6 +1633,11 @@ async function playBattleWithoutDebugVictory(page, battle) {
})[0].tile;
}
const scenarioTile = advancePointTile(unit, tiles);
if (scenarioTile) {
return scenarioTile;
}
if (!protectedIds.has(unit.id) || pressProtected) {
const focus = focusTarget(unit);
if (focus) {
@@ -1503,6 +1660,18 @@ async function playBattleWithoutDebugVictory(page, battle) {
}
}
const objective = currentSecureObjective();
if (objective && (!protectedIds.has(unit.id) || pressProtected)) {
const currentObjectiveDistance = objectiveDistance(unit, objective);
const objectivePressureTile = tiles
.filter((tile) => tile.x !== unit.x || tile.y !== unit.y)
.filter((tile) => objectiveDistance(tile, objective) < currentObjectiveDistance)
.sort((a, b) => objectiveDistance(a, objective) - objectiveDistance(b, objective) || (a.cost ?? 0) - (b.cost ?? 0))[0];
if (objectivePressureTile) {
return objectivePressureTile;
}
}
let best = { tile: tiles[0], score: Number.NEGATIVE_INFINITY };
for (const tile of tiles) {
@@ -1589,6 +1758,13 @@ async function playBattleWithoutDebugVictory(page, battle) {
resolveOutcome();
}
function allyTurnPriority(unit) {
const protectedPenalty = protectedIds.has(unit.id) ? 80 : 0;
const vanguardBonus = ['guan-yu', 'zhang-fei', 'zhao-yun', 'huang-zhong', 'wei-yan', 'ma-chao', 'ma-dai'].includes(unit.id) ? -28 : 0;
const supportPenalty = ['jian-yong', 'mi-zhu', 'sun-qian', 'ma-liang', 'yi-ji', 'gong-zhi'].includes(unit.id) ? 20 : 0;
return protectedPenalty + supportPenalty + vanguardBonus;
}
for (let round = 0; round < maxRounds; round += 1) {
let snapshot = state();
if (snapshot.battleOutcome) {
@@ -1598,7 +1774,7 @@ async function playBattleWithoutDebugVictory(page, battle) {
const acted = actedUnitIds();
const allies = snapshot.units
.filter((unit) => unit.faction === 'ally' && unit.hp > 0 && !acted.has(unit.id))
.sort((a, b) => a.hp / a.maxHp - b.hp / b.maxHp);
.sort((a, b) => allyTurnPriority(a) - allyTurnPriority(b) || a.hp / a.maxHp - b.hp / b.maxHp);
for (const allyState of allies) {
snapshot = state();
@@ -1652,6 +1828,10 @@ async function playBattleWithoutDebugVictory(page, battle) {
continue;
}
if (no >= 14 && shouldHoldBattleEndingTarget(enemy, currentSecureObjective())) {
continue;
}
const behavior = scene.enemyBehavior(enemy);
let target = scene.chooseEnemyTarget(enemy, behavior);
if (!target) {