diff --git a/hwe/compare/turn_command_trace.php b/hwe/compare/turn_command_trace.php new file mode 100644 index 00000000..f71dd6c6 --- /dev/null +++ b/hwe/compare/turn_command_trace.php @@ -0,0 +1,159 @@ +> */ + public array $calls = []; + + public function __construct(private readonly RNG $inner) + { + } + + public static function getMaxInt(): int + { + return LiteHashDRBG::getMaxInt(); + } + + public function nextBytes(int $bytes): string + { + $value = $this->inner->nextBytes($bytes); + $this->record('nextBytes', ['bytes' => $bytes], bin2hex($value)); + return $value; + } + + public function nextBits(int $bits): string + { + $value = $this->inner->nextBits($bits); + $this->record('nextBits', ['bits' => $bits], bin2hex($value)); + return $value; + } + + public function nextInt(?int $max = null): int + { + $value = $this->inner->nextInt($max); + $this->record('nextInt', ['maxInclusive' => $max], $value); + return $value; + } + + public function nextFloat1(): float + { + $value = $this->inner->nextFloat1(); + $this->record('nextFloat1', [], $value); + return $value; + } + + private function record(string $operation, array $arguments, mixed $result): void + { + $this->calls[] = [ + 'seq' => $this->sequence++, + 'operation' => $operation, + 'arguments' => $arguments, + 'result' => $result, + ]; + } +} + +function comparisonRunTurnCommand(array $request): array +{ + if (getenv('TURN_DIFFERENTIAL_ENABLED') !== '1') { + throw new \RuntimeException('TURN_DIFFERENTIAL_ENABLED=1 is required'); + } + $kind = $request['kind'] ?? null; + if ($kind !== 'general' && $kind !== 'nation') { + throw new \InvalidArgumentException('kind must be general or nation'); + } + $actorGeneralId = $request['actorGeneralId'] ?? null; + $action = $request['action'] ?? null; + $args = $request['args'] ?? null; + if (!is_int($actorGeneralId) || $actorGeneralId < 1) { + throw new \InvalidArgumentException('actorGeneralId must be a positive integer'); + } + if (!is_string($action) || $action === '') { + throw new \InvalidArgumentException('action must be a non-empty string'); + } + + $snapshotRequest = ['observe' => $request['observe'] ?? []]; + $before = comparisonTurnStateSnapshot($snapshotRequest); + $db = DB::db(); + $gameStorage = KVStorage::getStorage($db, 'game_env'); + $gameStorage->resetCache(); + $environment = $gameStorage->getAll(); + $general = General::createObjFromDB($actorGeneralId); + $turn = new TurnExecutionHelper($general); + $seedDomain = $kind === 'general' ? 'generalCommand' : 'nationCommand'; + $seed = Util::simpleSerialize( + UniqueConst::$hiddenSeed, + $seedDomain, + (int)$environment['year'], + (int)$environment['month'], + $actorGeneralId, + $action, + ); + $tracingRng = new TurnComparisonTracingRNG(new LiteHashDRBG($seed)); + $rng = new RandUtil($tracingRng); + + if ($kind === 'general') { + $command = buildGeneralCommandClass($action, $general, $environment, $args); + $resultTurn = $turn->processCommand($rng, $command, false); + } else { + $command = buildNationCommandClass($action, $general, $environment, $general->getLastTurn(), $args); + $resultTurn = $turn->processNationCommand($rng, $command); + } + + $general->getLogger()->flush(); + $turn->applyDB(); + unset($turn); + $after = comparisonTurnStateSnapshot($snapshotRequest); + + return [ + 'schemaVersion' => 1, + 'engine' => 'ref', + 'execution' => [ + 'kind' => $kind, + 'actorGeneralId' => $actorGeneralId, + 'action' => $action, + 'args' => $args, + 'seedDomain' => $seedDomain, + 'outcome' => [ + 'lastTurn' => $resultTurn->toRaw(), + 'commandName' => $command->getName(), + ], + ], + 'before' => $before, + 'after' => $after, + 'rng' => $tracingRng->calls, + ]; +} + +try { + $input = stream_get_contents(STDIN); + $request = json_decode($input === '' ? '{}' : $input, true, flags: JSON_THROW_ON_ERROR); + if (!is_array($request)) { + throw new \InvalidArgumentException('request must be an object'); + } + echo json_encode( + comparisonRunTurnCommand($request), + JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION, + ), PHP_EOL; +} catch (\Throwable $throwable) { + fwrite(STDERR, $throwable::class . ': ' . $throwable->getMessage() . PHP_EOL); + exit(1); +} diff --git a/hwe/compare/turn_state_snapshot.php b/hwe/compare/turn_state_snapshot.php index 3eb6af4a..ca562c9b 100644 --- a/hwe/compare/turn_state_snapshot.php +++ b/hwe/compare/turn_state_snapshot.php @@ -314,17 +314,24 @@ function comparisonTurnStateSnapshot(array $request): array ]; } -try { - $input = stream_get_contents(STDIN); - $request = json_decode($input === '' ? '{}' : $input, true, flags: JSON_THROW_ON_ERROR); - if (!is_array($request)) { - throw new \InvalidArgumentException('request must be an object'); +function comparisonTurnStateSnapshotMain(): void +{ + try { + $input = stream_get_contents(STDIN); + $request = json_decode($input === '' ? '{}' : $input, true, flags: JSON_THROW_ON_ERROR); + if (!is_array($request)) { + throw new \InvalidArgumentException('request must be an object'); + } + echo json_encode( + comparisonTurnStateSnapshot($request), + JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION, + ), PHP_EOL; + } catch (\Throwable $throwable) { + fwrite(STDERR, $throwable::class . ': ' . $throwable->getMessage() . PHP_EOL); + exit(1); } - echo json_encode( - comparisonTurnStateSnapshot($request), - JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION, - ), PHP_EOL; -} catch (\Throwable $throwable) { - fwrite(STDERR, $throwable::class . ': ' . $throwable->getMessage() . PHP_EOL); - exit(1); +} + +if (realpath((string)($_SERVER['SCRIPT_FILENAME'] ?? '')) === __FILE__) { + comparisonTurnStateSnapshotMain(); }