test(compare): trace deterministic monthly seed progression

This commit is contained in:
2026-08-03 18:57:01 +00:00
parent 3b2fd1082f
commit f16a6b6880
23 changed files with 1274 additions and 213 deletions
+65
View File
@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace sammo;
if (PHP_SAPI !== 'cli' || getenv('REF_DETERMINISTIC_INSTALL_ENABLED') !== '1') {
http_response_code(404);
exit(1);
}
chdir(dirname(__DIR__));
require_once 'lib.php';
require_once 'func.php';
$request = json_decode(stream_get_contents(STDIN), true, flags: JSON_THROW_ON_ERROR);
$generalIds = $request['generalIds'] ?? null;
if (!is_array($generalIds) || $generalIds === []) {
throw new \InvalidArgumentException('generalIds must be a non-empty array');
}
$db = DB::db();
$gameStorage = KVStorage::getStorage($db, 'game_env');
$env = $gameStorage->getAll(true);
$result = [];
$includeNation = ($request['includeNation'] ?? false) === true;
foreach ($generalIds as $generalId) {
if (!is_int($generalId) || $generalId < 1) {
throw new \InvalidArgumentException('generalIds must contain positive integers');
}
$general = General::createObjFromDB($generalId);
if ($general->getNPCType() < 2) {
throw new \InvalidArgumentException("general {$generalId} is not an NPC");
}
$ai = new GeneralAI($general);
$nationDecision = null;
if ($includeNation && $general->getNationID() !== 0 && $general->getVar('officer_level') >= 5) {
$nationStorage = KVStorage::getStorage($db, $general->getNationID(), 'nation_env');
$lastNationTurn = LastTurn::fromRaw($nationStorage->getValue("turn_last_{$general->getVar('officer_level')}"));
$reservedNation = buildNationCommandClass(null, $general, $env, $lastNationTurn);
$selectedNation = $ai->chooseNationTurn($reservedNation);
$nationDecision = [
'action' => $selectedNation->getRawClassName(),
'args' => $selectedNation->getArg(),
'reason' => $selectedNation->reason,
];
}
$reserved = $general->getReservedTurn(0, $env);
$selected = $ai->chooseGeneralTurn($reserved);
$result[] = [
'generalId' => $generalId,
'name' => $general->getName(),
'action' => $selected->getRawClassName(),
'brief' => $selected->getBrief(),
'args' => $selected->getArg(),
'reason' => $selected->reason,
'nationDecision' => $nationDecision,
];
}
echo json_encode([
'year' => (int) $env['year'],
'month' => (int) $env['month'],
'decisions' => $result,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
+129
View File
@@ -0,0 +1,129 @@
<?php
declare(strict_types=1);
namespace sammo;
if (PHP_SAPI !== 'cli' || getenv('REF_DETERMINISTIC_INSTALL_ENABLED') !== '1') {
http_response_code(404);
exit(1);
}
chdir(dirname(__DIR__));
require_once 'lib.php';
require_once 'func.php';
/** @return mixed */
function monthlySnapshotJson(mixed $value): mixed
{
if (!is_string($value) || $value === '') {
return $value;
}
try {
return json_decode($value, true, flags: JSON_THROW_ON_ERROR);
} catch (\JsonException) {
return $value;
}
}
/** @return array<string, mixed> */
function monthlySnapshotPick(array $row, array $mapping, array $jsonColumns = []): array
{
$result = [];
foreach ($mapping as $target => $source) {
$value = $row[$source] ?? null;
$result[$target] = in_array($source, $jsonColumns, true)
? monthlySnapshotJson($value)
: $value;
}
return $result;
}
$db = DB::db();
$game = KVStorage::getStorage($db, 'game_env');
$game->resetCache();
$world = $game->getValues([
'scenario', 'year', 'month', 'turnterm', 'turntime', 'starttime',
'init_year', 'init_month', 'develcost', 'killturn', 'isunited',
]);
$generals = [];
foreach ($db->query('SELECT * FROM general ORDER BY no') as $row) {
$general = monthlySnapshotPick($row, [
'id' => 'no', 'name' => 'name', 'nationId' => 'nation', 'cityId' => 'city',
'troopId' => 'troop', 'leadership' => 'leadership', 'strength' => 'strength',
'intelligence' => 'intel', 'experience' => 'experience', 'dedication' => 'dedication',
'officerLevel' => 'officer_level', 'injury' => 'injury', 'gold' => 'gold',
'rice' => 'rice', 'crew' => 'crew', 'crewTypeId' => 'crewtype', 'train' => 'train',
'atmos' => 'atmos', 'age' => 'age', 'npcState' => 'npc', 'turnTime' => 'turntime',
'recentWarTime' => 'recent_war', 'lastTurn' => 'last_turn', 'meta' => 'aux',
'affinity' => 'affinity', 'birthYear' => 'bornyear', 'deathYear' => 'deadyear',
'personality' => 'personal', 'specialDomestic' => 'special', 'specialWar' => 'special2',
'specAge' => 'specage', 'specAge2' => 'specage2',
'dex1' => 'dex1', 'dex2' => 'dex2', 'dex3' => 'dex3', 'dex4' => 'dex4', 'dex5' => 'dex5',
'killTurn' => 'killturn',
], ['last_turn', 'aux']);
foreach (['id', 'nationId', 'cityId', 'troopId', 'leadership', 'strength', 'intelligence',
'experience', 'dedication', 'officerLevel', 'injury', 'gold', 'rice', 'crew',
'crewTypeId', 'train', 'atmos', 'age', 'npcState', 'dex1', 'dex2', 'dex3',
'dex4', 'dex5', 'killTurn', 'affinity', 'birthYear', 'deathYear', 'specAge', 'specAge2'] as $key) {
$general[$key] = (int)$general[$key];
}
$generals[] = $general;
}
$cities = [];
foreach ($db->query('SELECT * FROM city ORDER BY city') as $row) {
$city = monthlySnapshotPick($row, [
'id' => 'city', 'name' => 'name', 'nationId' => 'nation', 'level' => 'level',
'population' => 'pop', 'populationMax' => 'pop_max', 'agriculture' => 'agri',
'agricultureMax' => 'agri_max', 'commerce' => 'comm', 'commerceMax' => 'comm_max',
'security' => 'secu', 'securityMax' => 'secu_max', 'supplyState' => 'supply',
'frontState' => 'front', 'defence' => 'def', 'defenceMax' => 'def_max',
'wall' => 'wall', 'wallMax' => 'wall_max', 'state' => 'state', 'term' => 'term',
'trust' => 'trust', 'trade' => 'trade', 'conflict' => 'conflict',
], ['conflict']);
foreach (array_keys($city) as $key) {
if ($key !== 'name' && $key !== 'conflict' && $key !== 'trust') {
$city[$key] = (int)$city[$key];
}
}
$cities[] = $city;
}
$nations = [];
foreach ($db->query('SELECT * FROM nation ORDER BY nation') as $row) {
$nation = monthlySnapshotPick($row, [
'id' => 'nation', 'name' => 'name', 'color' => 'color', 'capitalCityId' => 'capital',
'gold' => 'gold', 'rice' => 'rice', 'tech' => 'tech', 'level' => 'level',
'typeCode' => 'type', 'generalCount' => 'gennum', 'power' => 'power',
'war' => 'war', 'diplomacyLimit' => 'surlimit', 'capitalRevision' => 'capset',
'strategicCommandLimit' => 'strategic_cmd_limit', 'rate' => 'rate',
'rateTmp' => 'rate_tmp', 'bill' => 'bill', 'meta' => 'aux',
], ['aux']);
foreach (['id', 'capitalCityId', 'gold', 'rice', 'level', 'generalCount',
'power', 'war', 'diplomacyLimit', 'capitalRevision', 'strategicCommandLimit',
'rate', 'rateTmp', 'bill'] as $key) {
$nation[$key] = (int)$nation[$key];
}
$nation['tech'] = (float)$nation['tech'];
$nations[] = $nation;
}
$diplomacy = [];
foreach ($db->query('SELECT me, you, state, term, dead FROM diplomacy ORDER BY me, you') as $row) {
$diplomacy[] = [
'fromNationId' => (int)$row['me'], 'toNationId' => (int)$row['you'],
'state' => (int)$row['state'], 'term' => (int)$row['term'], 'dead' => (int)$row['dead'],
];
}
echo json_encode([
'schemaVersion' => 1,
'engine' => 'ref',
'world' => $world,
'generals' => $generals,
'cities' => $cities,
'nations' => $nations,
'diplomacy' => $diplomacy,
], JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION), PHP_EOL;
+10 -1
View File
@@ -181,6 +181,7 @@ function comparisonRowsById(string $table, string $idColumn, array $ids): array
function comparisonTurnStateSnapshot(array $request): array
{
$db = DB::db();
$observe = $request['observe'] ?? [];
if (!is_array($observe)) {
throw new \InvalidArgumentException('observe must be an object');
@@ -188,6 +189,15 @@ function comparisonTurnStateSnapshot(array $request): array
$generalIds = comparisonIntegerList($observe['generalIds'] ?? [], 'generalIds');
$cityIds = comparisonIntegerList($observe['cityIds'] ?? [], 'cityIds');
$nationIds = comparisonIntegerList($observe['nationIds'] ?? [], 'nationIds');
if (($observe['allGenerals'] ?? false) === true) {
$generalIds = array_map('intval', $db->queryFirstColumn('SELECT no FROM general ORDER BY no'));
}
if (($observe['allCities'] ?? false) === true) {
$cityIds = array_map('intval', $db->queryFirstColumn('SELECT city FROM city ORDER BY city'));
}
if (($observe['allNations'] ?? false) === true) {
$nationIds = array_map('intval', $db->queryFirstColumn('SELECT nation FROM nation ORDER BY nation'));
}
$logAfterId = $observe['logAfterId'] ?? 0;
$messageAfterId = $observe['messageAfterId'] ?? 0;
$includeNationHistoryLogs = $observe['includeNationHistoryLogs'] ?? false;
@@ -219,7 +229,6 @@ function comparisonTurnStateSnapshot(array $request): array
throw new \InvalidArgumentException('includeGlobalHistoryLogs must be a boolean');
}
$db = DB::db();
$game = KVStorage::getStorage($db, 'game_env');
$game->resetCache();
$worldValues = $game->getValues([