From bf9684902703c40e9a9c4d5525e7bbc66ef5ae48 Mon Sep 17 00:00:00 2001 From: hided62 Date: Sat, 25 Jul 2026 14:17:25 +0000 Subject: [PATCH] test: execute legacy turn command fixtures --- hwe/compare/turn_command_trace.php | 93 ++++++++++++++++++- hwe/compare/turn_state_snapshot.php | 58 +++++++++++- .../TextDecoration/SightseeingMessage.php | 20 +++- 3 files changed, 162 insertions(+), 9 deletions(-) diff --git a/hwe/compare/turn_command_trace.php b/hwe/compare/turn_command_trace.php index 0f994a73..c1405e9c 100644 --- a/hwe/compare/turn_command_trace.php +++ b/hwe/compare/turn_command_trace.php @@ -65,7 +65,7 @@ final class TurnComparisonTracingRNG implements RNG $this->calls[] = [ 'seq' => $this->sequence++, 'operation' => $operation, - 'arguments' => $arguments, + 'arguments' => $arguments === [] ? (object)[] : $arguments, 'result' => $result, ]; } @@ -110,6 +110,13 @@ function comparisonApplyTurnFixtureSetup(mixed $setup): void $game->setValue($key, $value); } $game->resetCache(); + if (array_key_exists('hiddenSeed', $world)) { + $hiddenSeed = $world['hiddenSeed']; + if (!is_string($hiddenSeed) || $hiddenSeed === '' || strlen($hiddenSeed) > 256) { + throw new \InvalidArgumentException('setup.world.hiddenSeed is invalid'); + } + UniqueConst::$hiddenSeed = $hiddenSeed; + } } foreach (['nations', 'cities', 'generals', 'diplomacy'] as $collection) { @@ -118,6 +125,33 @@ function comparisonApplyTurnFixtureSetup(mixed $setup): void } } + if (($setup['isolateWorld'] ?? false) === true) { + $generalIds = array_values(array_map( + static fn(array $row): int => (int)$row['id'], + $setup['generals'] ?? [], + )); + $nationIds = array_values(array_map( + static fn(array $row): int => (int)$row['id'], + $setup['nations'] ?? [], + )); + if ($generalIds === [] || $nationIds === []) { + throw new \InvalidArgumentException('isolateWorld requires generals and nations'); + } + $db->delete('general_turn', 'general_id NOT IN %li', $generalIds); + $db->delete('nation_turn', 'nation_id NOT IN %li', $nationIds); + $db->delete('troop', '1 = 1'); + $db->delete('diplomacy', '1 = 1'); + $db->delete('general', 'no NOT IN %li', $generalIds); + $db->delete('nation', 'nation NOT IN %li', $nationIds); + $db->update('city', [ + 'nation' => 0, + 'supply' => 0, + 'front' => 0, + 'state' => 0, + 'term' => 0, + ], '1 = 1'); + } + foreach ($setup['nations'] ?? [] as $row) { if (!is_array($row) || !is_int($row['id'] ?? null) || $row['id'] < 1) { throw new \InvalidArgumentException('setup.nations requires positive integer ids'); @@ -133,6 +167,7 @@ function comparisonApplyTurnFixtureSetup(mixed $setup): void 'level' => 'level', 'typeCode' => 'type', 'war' => 'war', + 'diplomacyLimit' => 'surlimit', 'generalCount' => 'gennum', 'power' => 'power', ]); @@ -187,16 +222,44 @@ function comparisonApplyTurnFixtureSetup(mixed $setup): void 'leadership' => 'leadership', 'strength' => 'strength', 'intelligence' => 'intel', + 'leadershipExp' => 'leadership_exp', + 'strengthExp' => 'strength_exp', + 'intelExp' => 'intel_exp', + 'experience' => 'experience', + 'dedication' => 'dedication', + 'expLevel' => 'explevel', 'officerLevel' => 'officer_level', + 'officerCityId' => 'officer_city', + 'injury' => 'injury', + 'age' => 'age', 'gold' => 'gold', 'rice' => 'rice', 'crew' => 'crew', 'crewTypeId' => 'crewtype', 'train' => 'train', 'atmos' => 'atmos', + 'dex1' => 'dex1', + 'dex2' => 'dex2', + 'dex3' => 'dex3', + 'dex4' => 'dex4', + 'dex5' => 'dex5', 'killTurn' => 'killturn', 'npcState' => 'npc', + 'blockState' => 'block', + 'specialDomestic' => 'special', + 'specialWar' => 'special2', + 'personality' => 'personal', + 'itemHorse' => 'horse', + 'itemWeapon' => 'weapon', + 'itemBook' => 'book', + 'itemExtra' => 'item', ]); + if (isset($row['meta'])) { + $patch['aux'] = Json::encode($row['meta']); + } + if (isset($row['lastTurn']) && is_array($row['lastTurn'])) { + $patch['last_turn'] = Json::encode($row['lastTurn']); + } if ($patch !== []) { $db->update('general', $patch, 'no = %i', $row['id']); } @@ -252,6 +315,15 @@ function comparisonRunTurnCommand(array $request): array $environment = $gameStorage->getAll(); $general = General::createObjFromDB($actorGeneralId); $turn = new TurnExecutionHelper($general); + $general->increaseInheritancePoint(Enums\InheritanceKey::lived_month, 1); + $preprocessRng = new RandUtil(new LiteHashDRBG(Util::simpleSerialize( + UniqueConst::$hiddenSeed, + 'preprocess', + (int)$environment['year'], + (int)$environment['month'], + $actorGeneralId, + ))); + $turn->preprocessCommand($preprocessRng, $environment); $seedDomain = $kind === 'general' ? 'generalCommand' : 'nationCommand'; $seed = Util::simpleSerialize( UniqueConst::$hiddenSeed, @@ -263,7 +335,11 @@ function comparisonRunTurnCommand(array $request): array ); $tracingRng = new TurnComparisonTracingRNG(new LiteHashDRBG($seed)); $rng = new RandUtil($tracingRng); + if ($kind === 'general' && $action === 'che_견문') { + \sammo\TextDecoration\SightseeingMessage::setComparisonRng($rng); + } + $previousLastTurnRaw = $general->getLastTurn()->toRaw(); if ($kind === 'general') { $command = buildGeneralCommandClass($action, $general, $environment, $args); $resultTurn = $turn->processCommand($rng, $command, false); @@ -276,6 +352,18 @@ function comparisonRunTurnCommand(array $request): array $turn->applyDB(); unset($turn); $after = comparisonTurnStateSnapshot($snapshotRequest); + $resultTurnRaw = $resultTurn->toRaw(); + $resultTerm = (int)($resultTurnRaw['term'] ?? 0); + $preReqTurn = $command->getPreReqTurn(); + $completed = ($resultTurnRaw['command'] ?? null) === $command->getName() + && ( + ($preReqTurn === 0 && $resultTerm === 0) + || ( + $preReqTurn > 0 + && ($previousLastTurnRaw['command'] ?? null) === $command->getName() + && (int)($previousLastTurnRaw['term'] ?? 0) === $preReqTurn + ) + ); return [ 'schemaVersion' => 1, @@ -287,8 +375,9 @@ function comparisonRunTurnCommand(array $request): array 'args' => $args, 'seedDomain' => $seedDomain, 'outcome' => [ - 'lastTurn' => $resultTurn->toRaw(), + 'lastTurn' => $resultTurnRaw, 'commandName' => $command->getName(), + 'completed' => $completed, ], ], 'before' => $before, diff --git a/hwe/compare/turn_state_snapshot.php b/hwe/compare/turn_state_snapshot.php index ca562c9b..292be75a 100644 --- a/hwe/compare/turn_state_snapshot.php +++ b/hwe/compare/turn_state_snapshot.php @@ -45,6 +45,14 @@ function comparisonJsonValue(mixed $value): mixed } } +function comparisonOptionalCode(mixed $value): ?string +{ + if (!is_string($value) || $value === '' || $value === 'None') { + return null; + } + return $value; +} + /** @param array $row */ function comparisonPickRow(array $row, array $mapping, array $jsonKeys = []): array { @@ -97,11 +105,22 @@ function comparisonTurnStateSnapshot(array $request): array $db = DB::db(); $game = KVStorage::getStorage($db, 'game_env'); $game->resetCache(); - $worldValues = $game->getValues(['year', 'month', 'turnterm', 'turntime', 'isunited']); + $worldValues = $game->getValues([ + 'year', + 'month', + 'turnterm', + 'turntime', + 'isunited', + 'scenario', + 'init_year', + 'init_month', + 'develcost', + ]); $generals = array_map( - static fn(array $row): array => comparisonPickRow( - $row, + static function (array $row): array { + $result = comparisonPickRow( + $row, [ 'id' => 'no', 'name' => 'name', @@ -113,6 +132,7 @@ function comparisonTurnStateSnapshot(array $request): array 'intelligence' => 'intel', 'experience' => 'experience', 'dedication' => 'dedication', + 'expLevel' => 'explevel', 'officerLevel' => 'officer_level', 'injury' => 'injury', 'gold' => 'gold', @@ -130,11 +150,36 @@ function comparisonTurnStateSnapshot(array $request): array 'leadershipExp' => 'leadership_exp', 'strengthExp' => 'strength_exp', 'intelExp' => 'intel_exp', + 'dex1' => 'dex1', + 'dex2' => 'dex2', + 'dex3' => 'dex3', + 'dex4' => 'dex4', + 'dex5' => 'dex5', 'killTurn' => 'killturn', 'mySet' => 'myset', + 'specialDomestic' => 'special', + 'specialWar' => 'special2', + 'personality' => 'personal', + 'itemHorse' => 'horse', + 'itemWeapon' => 'weapon', + 'itemBook' => 'book', + 'itemExtra' => 'item', ], ['last_turn', 'aux'], - ), + ); + foreach ([ + 'specialDomestic', + 'specialWar', + 'personality', + 'itemHorse', + 'itemWeapon', + 'itemBook', + 'itemExtra', + ] as $key) { + $result[$key] = comparisonOptionalCode($result[$key] ?? null); + } + return $result; + }, comparisonRowsById('general', 'no', $generalIds), ); @@ -185,6 +230,7 @@ function comparisonTurnStateSnapshot(array $request): array 'generalCount' => 'gennum', 'power' => 'power', 'war' => 'war', + 'diplomacyLimit' => 'surlimit', 'meta' => 'aux', ], ['aux'], @@ -298,6 +344,10 @@ function comparisonTurnStateSnapshot(array $request): array 'tickMinutes' => (int)$worldValues['turnterm'], 'turnTime' => (string)$worldValues['turntime'], 'isUnited' => (int)$worldValues['isunited'], + 'scenarioId' => (int)$worldValues['scenario'], + 'initYear' => (int)$worldValues['init_year'], + 'initMonth' => (int)$worldValues['init_month'], + 'develCost' => (int)$worldValues['develcost'], ], 'generals' => $generals, 'cities' => $cities, diff --git a/hwe/sammo/TextDecoration/SightseeingMessage.php b/hwe/sammo/TextDecoration/SightseeingMessage.php index 86218182..f3bab255 100644 --- a/hwe/sammo/TextDecoration/SightseeingMessage.php +++ b/hwe/sammo/TextDecoration/SightseeingMessage.php @@ -17,6 +17,15 @@ class SightseeingMessage{ const HeavyWounded = 0x2000; static protected $messages = null; + static protected ?\sammo\RandUtil $comparisonRng = null; + + public static function setComparisonRng(?\sammo\RandUtil $rng): void + { + if ($rng !== null && getenv('TURN_DIFFERENTIAL_ENABLED') !== '1') { + throw new \RuntimeException('comparison RNG is test-only'); + } + static::$comparisonRng = $rng; + } protected static function getMessageList():array{ /* [[속성, [ @@ -104,9 +113,14 @@ class SightseeingMessage{ } public function pickAction():array{ - [$type, $texts] = Util::choiceRandomUsingWeightPair(static::$messages??[]); - $text = Util::choiceRandom($texts); + if (static::$comparisonRng !== null) { + [$type, $texts] = static::$comparisonRng->choiceUsingWeightPair(static::$messages ?? []); + $text = static::$comparisonRng->choice($texts); + } else { + [$type, $texts] = Util::choiceRandomUsingWeightPair(static::$messages??[]); + $text = Util::choiceRandom($texts); + } return [$type, $text]; } -} \ No newline at end of file +}