314 lines
10 KiB
PHP
314 lines
10 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace sammo;
|
|
|
|
if (PHP_SAPI !== 'cli') {
|
|
http_response_code(404);
|
|
exit;
|
|
}
|
|
|
|
chdir(dirname(__DIR__));
|
|
$_SERVER['REMOTE_ADDR'] ??= '127.0.0.1';
|
|
error_reporting(E_ALL & ~E_DEPRECATED);
|
|
require_once 'lib.php';
|
|
require_once 'func.php';
|
|
require_once __DIR__ . '/turn_state_snapshot.php';
|
|
|
|
final class TurnComparisonTracingRNG implements RNG
|
|
{
|
|
private int $sequence = 0;
|
|
|
|
/** @var list<array<string, mixed>> */
|
|
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,
|
|
];
|
|
}
|
|
}
|
|
|
|
/** @return array<string, mixed> */
|
|
function comparisonMappedPatch(array $row, array $mapping): array
|
|
{
|
|
$patch = [];
|
|
foreach ($mapping as $canonical => $legacy) {
|
|
if (array_key_exists($canonical, $row)) {
|
|
$patch[$legacy] = $row[$canonical];
|
|
}
|
|
}
|
|
return $patch;
|
|
}
|
|
|
|
function comparisonApplyTurnFixtureSetup(mixed $setup): void
|
|
{
|
|
if ($setup === null) {
|
|
return;
|
|
}
|
|
if (!is_array($setup)) {
|
|
throw new \InvalidArgumentException('setup must be an object');
|
|
}
|
|
$db = DB::db();
|
|
$world = $setup['world'] ?? null;
|
|
if ($world !== null) {
|
|
if (!is_array($world)) {
|
|
throw new \InvalidArgumentException('setup.world must be an object');
|
|
}
|
|
$game = KVStorage::getStorage($db, 'game_env');
|
|
foreach (['year', 'month', 'startyear'] as $key) {
|
|
$fixtureKey = $key === 'startyear' ? 'startYear' : $key;
|
|
if (!array_key_exists($fixtureKey, $world)) {
|
|
continue;
|
|
}
|
|
$value = $world[$fixtureKey];
|
|
if (!is_int($value) || $value < 1 || ($key === 'month' && $value > 12)) {
|
|
throw new \InvalidArgumentException("setup.world.{$fixtureKey} is invalid");
|
|
}
|
|
$game->setValue($key, $value);
|
|
}
|
|
$game->resetCache();
|
|
}
|
|
|
|
foreach (['nations', 'cities', 'generals', 'diplomacy'] as $collection) {
|
|
if (isset($setup[$collection]) && !is_array($setup[$collection])) {
|
|
throw new \InvalidArgumentException("setup.{$collection} must be an array");
|
|
}
|
|
}
|
|
|
|
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');
|
|
}
|
|
$data = comparisonMappedPatch($row, [
|
|
'id' => 'nation',
|
|
'name' => 'name',
|
|
'color' => 'color',
|
|
'capitalCityId' => 'capital',
|
|
'gold' => 'gold',
|
|
'rice' => 'rice',
|
|
'tech' => 'tech',
|
|
'level' => 'level',
|
|
'typeCode' => 'type',
|
|
'war' => 'war',
|
|
'generalCount' => 'gennum',
|
|
'power' => 'power',
|
|
]);
|
|
if (isset($row['meta'])) {
|
|
$data['aux'] = Json::encode($row['meta']);
|
|
}
|
|
$data += [
|
|
'name' => "fixture-nation-{$row['id']}",
|
|
'color' => '#777777',
|
|
'capital' => 0,
|
|
'gold' => 0,
|
|
'rice' => 0,
|
|
'level' => 1,
|
|
'type' => 'che_중립',
|
|
'aux' => '{}',
|
|
];
|
|
$db->insertUpdate('nation', $data, $data);
|
|
}
|
|
|
|
foreach ($setup['cities'] ?? [] as $row) {
|
|
if (!is_array($row) || !is_int($row['id'] ?? null) || $row['id'] < 1) {
|
|
throw new \InvalidArgumentException('setup.cities requires positive integer ids');
|
|
}
|
|
$patch = comparisonMappedPatch($row, [
|
|
'nationId' => 'nation',
|
|
'population' => 'pop',
|
|
'agriculture' => 'agri',
|
|
'commerce' => 'comm',
|
|
'security' => 'secu',
|
|
'supplyState' => 'supply',
|
|
'frontState' => 'front',
|
|
'defence' => 'def',
|
|
'wall' => 'wall',
|
|
'state' => 'state',
|
|
'term' => 'term',
|
|
'trust' => 'trust',
|
|
'trade' => 'trade',
|
|
]);
|
|
if ($patch !== []) {
|
|
$db->update('city', $patch, 'city = %i', $row['id']);
|
|
}
|
|
}
|
|
|
|
foreach ($setup['generals'] ?? [] as $row) {
|
|
if (!is_array($row) || !is_int($row['id'] ?? null) || $row['id'] < 1) {
|
|
throw new \InvalidArgumentException('setup.generals requires positive integer ids');
|
|
}
|
|
$patch = comparisonMappedPatch($row, [
|
|
'nationId' => 'nation',
|
|
'cityId' => 'city',
|
|
'troopId' => 'troop',
|
|
'leadership' => 'leadership',
|
|
'strength' => 'strength',
|
|
'intelligence' => 'intel',
|
|
'officerLevel' => 'officer_level',
|
|
'gold' => 'gold',
|
|
'rice' => 'rice',
|
|
'crew' => 'crew',
|
|
'crewTypeId' => 'crewtype',
|
|
'train' => 'train',
|
|
'atmos' => 'atmos',
|
|
'killTurn' => 'killturn',
|
|
'npcState' => 'npc',
|
|
]);
|
|
if ($patch !== []) {
|
|
$db->update('general', $patch, 'no = %i', $row['id']);
|
|
}
|
|
}
|
|
|
|
foreach ($setup['diplomacy'] ?? [] as $row) {
|
|
if (
|
|
!is_array($row) ||
|
|
!is_int($row['fromNationId'] ?? null) ||
|
|
!is_int($row['toNationId'] ?? null) ||
|
|
$row['fromNationId'] < 1 ||
|
|
$row['toNationId'] < 1
|
|
) {
|
|
throw new \InvalidArgumentException('setup.diplomacy requires positive integer nation ids');
|
|
}
|
|
$data = comparisonMappedPatch($row, [
|
|
'fromNationId' => 'me',
|
|
'toNationId' => 'you',
|
|
'state' => 'state',
|
|
'term' => 'term',
|
|
'dead' => 'dead',
|
|
]);
|
|
$data += ['state' => 3, 'term' => 0, 'dead' => 0];
|
|
$db->insertUpdate('diplomacy', $data, $data);
|
|
}
|
|
}
|
|
|
|
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');
|
|
}
|
|
|
|
comparisonApplyTurnFixtureSetup($request['setup'] ?? null);
|
|
$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);
|
|
}
|