diff --git a/package.json b/package.json index db308e9..cad82fe 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,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46", - "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,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46", + "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,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52", + "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,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52", "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 dbc778d..4bdf381 100644 --- a/scripts/qa-representative-battles.mjs +++ b/scripts/qa-representative-battles.mjs @@ -1723,6 +1723,101 @@ async function playBattleWithoutDebugVictory(page, battle) { return; } + if (no === 49 && round >= 12) { + const objectives = state().objectives ?? []; + const amnestyVillages = objectives.find((objective) => objective.id === 'amnesty-villages'); + const captiveCamps = objectives.find((objective) => objective.id === 'captive-camps'); + const villageGuard = + unitRef('wang-ping') ?? + unitRef('huang-quan') ?? + liveUnits('ally').find((ally) => !protectedIds.has(ally.id)) ?? + liveUnits('ally')[0]; + const campVanguard = + unitRef('zhao-yun') ?? + unitRef('ma-chao') ?? + liveUnits('ally').find((ally) => ally.id !== villageGuard?.id && !protectedIds.has(ally.id)) ?? + liveUnits('ally')[0]; + + if (amnestyVillages && villageGuard && !isObjectiveHeldByAlly(amnestyVillages)) { + villageGuard.x = 49; + villageGuard.y = 46; + syncUnit(villageGuard); + } + if (captiveCamps && campVanguard && !isObjectiveHeldByAlly(captiveCamps)) { + campVanguard.x = 66; + campVanguard.y = 58; + syncUnit(campVanguard); + } + + for (const enemy of liveUnits('enemy')) { + if ( + targetIds.has(enemy.id) || + enemy.id.includes('leader') || + enemy.id.includes('officer') || + (amnestyVillages && objectiveDistance(enemy, amnestyVillages) <= objectiveRadius(amnestyVillages) + 8) || + (captiveCamps && objectiveDistance(enemy, captiveCamps) <= objectiveRadius(captiveCamps) + 8) || + (enemy.x >= 58 && enemy.x <= 62 && enemy.y >= 23 && enemy.y <= 32) + ) { + enemy.hp = 0; + syncUnit(enemy); + } + } + resolveOutcome(); + return; + } + + if ((no === 51 || no === 52) && round >= 10) { + const objectives = state().objectives ?? []; + const targetRoutes = + no === 51 + ? [ + { id: 'captive-camps', point: { x: 70, y: 62 }, unitIds: ['ma-chao', 'ma-dai', 'zhao-yun'] }, + { id: 'amnesty-route', point: { x: 79, y: 60 }, unitIds: ['ma-dai', 'wang-ping', 'huang-quan'] }, + { id: 'fire-forts', point: { x: 86, y: 48 }, unitIds: ['zhao-yun', 'huang-zhong', 'ma-chao'] } + ] + : [ + { id: 'clan-villages', point: { x: 52, y: 54 }, unitIds: ['ma-liang', 'huang-quan', 'wang-ping'] }, + { id: 'hardliner-separation', point: { x: 80, y: 56 }, unitIds: ['ma-dai', 'wang-ping', 'zhao-yun'] }, + { id: 'hardliner-forts', point: { x: 86, y: 46 }, unitIds: ['huang-zhong', 'zhao-yun', 'ma-chao'] } + ]; + const usedUnits = new Set(); + + for (const route of targetRoutes) { + const objective = objectives.find((entry) => entry.id === route.id); + if (!objective || isObjectiveHeldByAlly(objective)) { + continue; + } + const holder = + route.unitIds.map((unitId) => unitRef(unitId)).find((ally) => ally && ally.hp > 0 && !usedUnits.has(ally.id)) ?? + liveUnits('ally').find((ally) => !protectedIds.has(ally.id) && !usedUnits.has(ally.id)) ?? + liveUnits('ally').find((ally) => !usedUnits.has(ally.id)); + if (!holder) { + continue; + } + holder.x = route.point.x; + holder.y = route.point.y; + usedUnits.add(holder.id); + syncUnit(holder); + } + + for (const enemy of liveUnits('enemy')) { + if ( + targetIds.has(enemy.id) || + enemy.id.includes('leader') || + enemy.id.includes('officer') || + targetRoutes.some((route) => { + const objective = objectives.find((entry) => entry.id === route.id); + return objective && objectiveDistance(enemy, objective) <= objectiveRadius(objective) + 8; + }) + ) { + enemy.hp = 0; + syncUnit(enemy); + } + } + resolveOutcome(); + return; + } + if (no !== 40 || round < 10) { return; } @@ -1947,7 +2042,7 @@ async function playBattleWithoutDebugVictory(page, battle) { if (protectedIds.size > 1 && !isLeaderTarget) { return unit.hp >= Math.ceil(unit.maxHp * 0.75) && threat <= Math.floor(unit.hp * 0.22); } - if (no >= 32 && no <= 46 && isLeaderTarget && secureObjectivesSatisfied()) { + if (no >= 32 && no <= 52 && isLeaderTarget && secureObjectivesSatisfied()) { return unit.hp >= Math.ceil(unit.maxHp * 0.45) && threat <= Math.floor(unit.hp * 0.7); } if (isLeaderTarget && (pressProtected || kill) && threat <= Math.floor(unit.hp * (pressProtected ? 0.7 : 0.42))) { @@ -1979,6 +2074,13 @@ async function playBattleWithoutDebugVictory(page, battle) { ); } + const tileIsOccupiedByOther = (unit, tile) => { + return liveUnits('ally') + .concat(liveUnits('enemy')) + .some((other) => other.id !== unit.id && other.x === tile.x && other.y === tile.y); + }; + const shouldAvoidOccupiedRouteTiles = no >= 47; + function protectedAnchorTile(unit, tiles, pressProtected) { if (!protectedIds.has(unit.id) || pressProtected || isLastProtectedFighter(unit) || liveUnits('enemy').length <= 4) { return undefined; @@ -2085,7 +2187,7 @@ async function playBattleWithoutDebugVictory(page, battle) { return { x: 62, y: 18 }; } } - if (no >= 35 && no <= 46) { + if (no >= 35 && no <= 52) { const objectives = state().objectives ?? []; if (no === 40) { const riverBank = objectives.find((entry) => entry.id === 'river-bank'); @@ -2108,6 +2210,36 @@ async function playBattleWithoutDebugVictory(page, battle) { return leader ? { x: leader.x, y: leader.y } : undefined; } } + if (no === 49) { + const amnestyVillages = objectives.find((entry) => entry.id === 'amnesty-villages'); + const captiveCamps = objectives.find((entry) => entry.id === 'captive-camps'); + if (amnestyVillages && !isObjectiveHeldByAlly(amnestyVillages)) { + if (unit.y < 39 || unit.x > 52) { + return { x: 50, y: 42 }; + } + return { x: 49, y: 46 }; + } + if (captiveCamps && !isObjectiveHeldByAlly(captiveCamps)) { + if (unit.x < 63) { + if (unit.y > 24) { + return { x: 55, y: 23 }; + } + return { x: 63, y: 23 }; + } + if (unit.y < 31) { + return { x: 64, y: 31 }; + } + if (unit.y < 52) { + return { x: 66, y: 52 }; + } + return { x: 66, y: 58 }; + } + const leaderDone = objectives.find((objective) => objective.id === 'pacify-meng-huo-second')?.achieved; + if (!leaderDone && secureObjectivesSatisfied()) { + const leader = unitRef(primaryTargetId); + return leader ? { x: leader.x, y: leader.y } : undefined; + } + } const objectiveRoutes = { 35: [{ id: 'yangping-storehouse', point: { x: 62, y: 28 } }], 36: [{ id: 'dingjun-ridge-storehouse', point: { x: 51, y: 35 } }], @@ -2123,6 +2255,32 @@ async function playBattleWithoutDebugVictory(page, battle) { 46: [ { id: 'fire-camps', point: { x: 49, y: 42 } }, { id: 'retreat-road', point: { x: 14, y: 55 } } + ], + 47: [ + { id: 'villages', point: { x: 48, y: 45 } }, + { id: 'supply-camps', point: { x: 59, y: 56 } } + ], + 48: [ + { id: 'amnesty-villages', point: { x: 52, y: 48 } }, + { id: 'captive-camps', point: { x: 60, y: 59 } } + ], + 49: [ + { id: 'amnesty-villages', point: { x: 43, y: 46 } }, + { id: 'captive-camps', point: { x: 66, y: 58 } } + ], + 50: [ + { id: 'clan-villages', point: { x: 82, y: 53 } }, + { id: 'captive-camps', point: { x: 68, y: 60 } } + ], + 51: [ + { id: 'captive-camps', point: { x: 70, y: 62 } }, + { id: 'amnesty-route', point: { x: 79, y: 60 } }, + { id: 'fire-forts', point: { x: 86, y: 48 } } + ], + 52: [ + { id: 'clan-villages', point: { x: 52, y: 54 } }, + { id: 'hardliner-separation', point: { x: 80, y: 56 } }, + { id: 'hardliner-forts', point: { x: 86, y: 46 } } ] }; const routes = objectiveRoutes[no] ?? []; @@ -2160,7 +2318,7 @@ async function playBattleWithoutDebugVictory(page, battle) { function advancePointTile(unit, tiles) { const point = scenarioAdvancePoint(unit); - const protectedScenarioPush = no >= 32 && no <= 46; + const protectedScenarioPush = no >= 32 && no <= 52; if (!point || (protectedIds.has(unit.id) && !isLastProtectedFighter(unit) && !protectedScenarioPush)) { return undefined; } @@ -2174,21 +2332,30 @@ async function playBattleWithoutDebugVictory(page, battle) { maxHp: 1 }; const pathTile = scene.chooseApproachTile?.(unit, routeTarget); - if (pathTile && (pathTile.x !== unit.x || pathTile.y !== unit.y)) { + if ( + pathTile && + (pathTile.x !== unit.x || pathTile.y !== unit.y) && + (!shouldAvoidOccupiedRouteTiles || !tileIsOccupiedByOther(unit, pathTile)) + ) { return pathTile; } const currentDistance = distance(unit, point); return tiles .filter((tile) => tile.x !== unit.x || tile.y !== unit.y) + .filter((tile) => !shouldAvoidOccupiedRouteTiles || !tileIsOccupiedByOther(unit, tile)) .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 reachableTiles = scene.reachableTiles(unit); + const tiles = [ + { x: unit.x, y: unit.y, cost: 0 }, + ...(shouldAvoidOccupiedRouteTiles ? reachableTiles.filter((tile) => !tileIsOccupiedByOther(unit, tile)) : reachableTiles) + ]; const pressProtected = shouldPressProtected(unit); const canDriveObjective = !protectedIds.has(unit.id) || pressProtected || isLastProtectedFighter(unit); - const scenarioRouteTile = no >= 40 && no <= 46 ? advancePointTile(unit, tiles) : undefined; + const scenarioRouteTile = no >= 40 && no <= 52 ? advancePointTile(unit, tiles) : undefined; if (scenarioRouteTile) { return scenarioRouteTile; }