From 84b16c834b966c966c393020167c431abb0abed2 Mon Sep 17 00:00:00 2001 From: Wickedness Date: Mon, 6 Jul 2026 16:32:40 +0900 Subject: [PATCH] Expand early campaign release QA to battle 20 --- package.json | 4 +- scripts/qa-representative-battles.mjs | 202 ++++++++++++++++++++++++-- 2 files changed, 193 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index f734b09..a453d9e 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/scripts/qa-representative-battles.mjs b/scripts/qa-representative-battles.mjs index c782ad4..34cccc2 100644 --- a/scripts/qa-representative-battles.mjs +++ b/scripts/qa-representative-battles.mjs @@ -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) {