> */ 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); }