From 5e279eb666e654ef7e5e7032fbeeb65efdab7767 Mon Sep 17 00:00:00 2001 From: hided62 Date: Wed, 5 Aug 2026 03:08:04 +0000 Subject: [PATCH] test: capture legacy battle parity fixtures --- hwe/compare/battle_fixture_capture.php | 102 +++++++++++++++++++++++++ hwe/compare/battle_trace.php | 78 ++++++++++++++++++- hwe/process_war.php | 19 +++++ 3 files changed, 196 insertions(+), 3 deletions(-) create mode 100644 hwe/compare/battle_fixture_capture.php diff --git a/hwe/compare/battle_fixture_capture.php b/hwe/compare/battle_fixture_capture.php new file mode 100644 index 00000000..46ff0178 --- /dev/null +++ b/hwe/compare/battle_fixture_capture.php @@ -0,0 +1,102 @@ +getRaw(true); + $inheritBuff = $general->getAuxVar('inheritBuff'); + + $fixture = [ + 'no' => $general->getID(), + 'name' => $general->getName(), + 'nation' => $general->getNationID(), + 'city' => $general->getCityID(), + 'turntime' => $general->getTurnTime(General::TURNTIME_FULL_MS), + 'turntick' => $general->getTurnTick(), + 'personal' => $raw['personal'] ?? null, + 'special' => $raw['special'] ?? null, + 'special2' => $raw['special2'] ?? null, + 'crew' => $raw['crew'], + 'crewtype' => $raw['crewtype'], + 'atmos' => $raw['atmos'], + 'train' => $raw['train'], + 'intel' => $raw['intel'], + 'intel_exp' => $raw['intel_exp'], + 'book' => $raw['book'] ?? 'None', + 'strength' => $raw['strength'], + 'strength_exp' => $raw['strength_exp'], + 'weapon' => $raw['weapon'] ?? 'None', + 'injury' => $raw['injury'], + 'leadership' => $raw['leadership'], + 'leadership_exp' => $raw['leadership_exp'], + 'horse' => $raw['horse'] ?? 'None', + 'item' => $raw['item'] ?? 'None', + 'explevel' => $raw['explevel'], + 'experience' => $raw['experience'], + 'dedication' => $raw['dedication'], + 'officer_level' => $raw['officer_level'], + 'officer_city' => $raw['officer_city'], + 'gold' => $raw['gold'], + 'rice' => $raw['rice'], + 'dex1' => $raw['dex1'], + 'dex2' => $raw['dex2'], + 'dex3' => $raw['dex3'], + 'dex4' => $raw['dex4'], + 'dex5' => $raw['dex5'], + 'defence_train' => $raw['defence_train'], + 'recent_war' => $raw['recent_war'] ?? null, + 'warnum' => $general->getRankVar(RankColumn::warnum, 0), + 'killnum' => $general->getRankVar(RankColumn::killnum, 0), + 'killcrew' => $general->getRankVar(RankColumn::killcrew, 0), + ]; + if ($inheritBuff !== null) { + $fixture['inheritBuff'] = $inheritBuff; + } + return $fixture; +} + +function comparisonBuildBattleFixture( + string $seed, + int $year, + int $month, + int $startYear, + General $attacker, + array $attackerNation, + array $defenders, + array $defenderCity, + array $defenderNation, +): array { + return [ + 'action' => 'battle', + 'seed' => $seed, + 'repeatCnt' => 1, + 'year' => $year, + 'month' => $month, + 'startYear' => $startYear, + 'scenarioEffect' => GameConst::$scenarioEffect, + 'attackerGeneral' => comparisonBuildBattleGeneralFixture($attacker), + 'attackerCity' => $attacker->getRawCity(), + 'attackerNation' => $attackerNation, + // General::getGeneralList() can retain IDs as array keys. JSON would + // then encode the collection as an object, while both simulators use + // an ordered list. Keep the legacy iteration order but discard keys. + 'defenderGenerals' => array_values(array_map( + static fn(General $general): array => comparisonBuildBattleGeneralFixture($general), + $defenders, + )), + 'defenderCity' => $defenderCity, + 'defenderNation' => $defenderNation, + ]; +} diff --git a/hwe/compare/battle_trace.php b/hwe/compare/battle_trace.php index 1af87df4..adf322c6 100644 --- a/hwe/compare/battle_trace.php +++ b/hwe/compare/battle_trace.php @@ -71,6 +71,48 @@ final class ComparisonTracingRNG implements RNG } } +final class ComparisonTracingRandUtil extends RandUtil +{ + /** @var array */ + public array $boolCalls = []; + + public function __construct(private readonly ComparisonTracingRNG $tracingRng) + { + parent::__construct($tracingRng); + } + + public function nextBool(int|float $prob = 0.5): bool + { + $rngSeq = count($this->tracingRng->calls); + $result = parent::nextBool($prob); + $this->boolCalls[] = [ + 'rngSeq' => $rngSeq, + 'probability' => $prob, + 'result' => $result, + ]; + return $result; + } +} + +final class ComparisonGeneral extends General +{ + public function getTurnTime(int $short = self::TURNTIME_FULL_MS): ?string + { + $formatted = (string)($this->getRaw()['turntime'] ?? '1970-01-01 00:00:00'); + return match ($short) { + self::TURNTIME_FULL_MS => $formatted, + self::TURNTIME_FULL => substr($formatted, 0, 19), + self::TURNTIME_HMS => substr($formatted, 11, 8), + self::TURNTIME_HM => substr($formatted, 11, 5), + }; + } + + public function getTurnTick(): ?int + { + return (int)($this->getRaw()['turntick'] ?? 0); + } +} + function comparisonExtractRankVar(array $raw): Map { $rankVars = new Map(); @@ -97,7 +139,7 @@ function comparisonBuildGeneral( } $raw['aux'] = Json::encode($aux); $raw['owner'] = 0; - return new General( + return new ComparisonGeneral( $raw, comparisonExtractRankVar($raw), null, @@ -135,7 +177,7 @@ function comparisonRunBattle(array $fixture): array GameConst::$scenarioEffect = $scenarioEffect; $tracingRng = new ComparisonTracingRNG(new LiteHashDRBG($seed)); - $warRng = new RandUtil($tracingRng); + $warRng = new ComparisonTracingRandUtil($tracingRng); $attacker = new WarUnitGeneral( $warRng, comparisonBuildGeneral($rawAttacker, $rawAttackerCity, $rawAttackerNation, $year, $month), @@ -156,11 +198,17 @@ function comparisonRunBattle(array $fixture): array if (count($defenderList) && extractBattleOrder($city, $attacker) > 0) { $defenderList[] = $city; } + $summarizeDefenderOrder = static fn(WarUnit $unit): array => [ + 'id' => $unit instanceof WarUnitGeneral ? $unit->getGeneral()->getID() : 0, + 'order' => extractBattleOrder($unit, $attacker), + ]; + $defenderOrderBeforeSort = array_map($summarizeDefenderOrder, $defenderList); usort( $defenderList, fn(WarUnit $lhs, WarUnit $rhs): int => -(extractBattleOrder($lhs, $attacker) <=> extractBattleOrder($rhs, $attacker)), ); + $defenderOrderAfterSort = array_map($summarizeDefenderOrder, $defenderList); $iterDefender = new \ArrayIterator($defenderList); $iterDefender->rewind(); @@ -213,8 +261,13 @@ function comparisonRunBattle(array $fixture): array 'attacker' => buildWarTraceUnitSnapshot($attacker), 'city' => buildWarTraceUnitSnapshot($city), 'finishedDefenders' => $finishedDefenders, + 'defenderOrder' => [ + 'before' => $defenderOrderBeforeSort, + 'after' => $defenderOrderAfterSort, + ], 'events' => $events, 'rng' => $tracingRng->calls, + 'boolRng' => $warRng->boolCalls, 'logs' => [ 'attacker' => $attackerLogs, 'defenders' => $defenderLogs, @@ -224,8 +277,27 @@ function comparisonRunBattle(array $fixture): array } $fixturePath = $argv[1] ?? null; +if ($fixturePath === '--jsonl') { + try { + while (($line = fgets(STDIN)) !== false) { + $line = trim($line); + if ($line === '') { + continue; + } + $fixture = json_decode($line, true, flags: JSON_THROW_ON_ERROR); + echo json_encode( + comparisonRunBattle($fixture), + JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION, + ), PHP_EOL; + } + exit(0); + } catch (\Throwable $throwable) { + fwrite(STDERR, $throwable::class . ': ' . $throwable->getMessage() . PHP_EOL); + exit(1); + } +} if ($fixturePath === null || ($fixturePath !== '-' && !is_file($fixturePath))) { - fwrite(STDERR, "usage: php compare/battle_trace.php \n"); + fwrite(STDERR, "usage: php compare/battle_trace.php \n"); exit(2); } diff --git a/hwe/process_war.php b/hwe/process_war.php index 502314c4..68490380 100644 --- a/hwe/process_war.php +++ b/hwe/process_war.php @@ -40,6 +40,25 @@ function processWar(string $warSeed, General $attackerGeneral, array $rawAttacke $defenderCityGeneralIDList = $db->queryFirstColumn('SELECT no FROM general WHERE nation=%i AND city=%i AND nation!=0', $city->getVar('nation'), $city->getVar('city')); $defenderCityGeneralList = General::createObjListFromDB($defenderCityGeneralIDList, null); + $captureBattleFixture = + PHP_SAPI === 'cli' + && getenv('REF_DETERMINISTIC_INSTALL_ENABLED') === '1' + && getenv('REF_BATTLE_FIXTURE_TRACE') === '1'; + if ($captureBattleFixture) { + require_once __DIR__ . '/compare/battle_fixture_capture.php'; + fwrite(STDOUT, 'AI_WAR_FIXTURE_REF ' . Json::encode(comparisonBuildBattleFixture( + $warSeed, + (int)$year, + (int)$month, + (int)$startYear, + $attackerGeneral, + $rawAttackerNation, + $defenderCityGeneralList, + $rawDefenderCity, + $rawDefenderNation, + )) . "\n"); + } + /** @var WarUnit[] */ $defenderList = []; $defenderCandidateTrace = [];