test(compare): reproduce invader game resume

This commit is contained in:
2026-08-12 15:35:52 +00:00
parent 6c8aa3ed9f
commit 1721e5f6d4
2 changed files with 291 additions and 0 deletions
+206
View File
@@ -752,6 +752,204 @@ function comparisonNationBettingLifecycleTrace(
];
}
/**
* Reproduce the player-visible handoff from a completed unification to the
* invader event and prove that the real turn engine can cross the next monthly
* boundary afterwards. The surrounding runner executes this only against its
* disposable MariaDB clone.
*
* @param array<string, mixed> $request
* @param array<string, mixed> $environment
* @return array<string, mixed>
*/
function comparisonUnificationInvaderResumeTrace(array $request, array $environment): array
{
$db = DB::db();
$gameStorage = KVStorage::getStorage($db, 'game_env');
$winnerGeneralId = $request['winnerGeneralId'] ?? null;
if (!is_int($winnerGeneralId) || $winnerGeneralId <= 0) {
throw new \InvalidArgumentException('UnificationInvaderResume requires a positive winnerGeneralId');
}
$winnerNationId = (int)($db->queryFirstField(
'SELECT nation FROM general WHERE no = %i',
$winnerGeneralId,
) ?? 0);
if ($winnerNationId <= 0) {
throw new \RuntimeException('winner general must belong to a nation');
}
$allCityCount = count(CityConst::all());
$winnerCityCount = (int)$db->queryFirstField(
'SELECT COUNT(*) FROM city WHERE nation = %i',
$winnerNationId,
);
if ($winnerCityCount !== $allCityCount) {
throw new \RuntimeException(
"winner must occupy every scenario city: winner={$winnerCityCount}, scenario={$allCityCount}",
);
}
$year = $environment['year'] ?? null;
$month = $environment['month'] ?? null;
$startYear = $environment['startyear'] ?? null;
if (!is_int($year) || !is_int($month) || !is_int($startYear) || $year - $startYear < 20) {
throw new \InvalidArgumentException('fixture must start at least 20 game years after startyear');
}
$unificationTick = Util::toInt($gameStorage->turntime);
$elapsedMonths = ($year - $startYear) * 12 + $month - 1;
$gameStorage->starttime = GameClock::addTicks(
cutTurn($unificationTick, Util::toInt($gameStorage->turnterm)),
-$elapsedMonths * GameClock::TICKS_PER_TURN,
);
$clock = GameClock::fromStorage($gameStorage);
$clock->persistTick($gameStorage, $unificationTick, GameClock::MODE_MANUAL);
$db->update('general', ['turntime' => $unificationTick], true);
$db->update('plock', ['plock' => 0], true);
$gameStorage->resetCache();
refreshNationStaticInfo();
$before = [
'year' => Util::toInt($gameStorage->year),
'month' => Util::toInt($gameStorage->month),
'isunited' => Util::toInt($gameStorage->isunited),
'clockTick' => GameClock::fromStorage($gameStorage)->nowTick(),
'turntime' => Util::toInt($gameStorage->turntime),
'nationCount' => (int)$db->queryFirstField('SELECT COUNT(*) FROM nation WHERE level > 0'),
'winnerCityCount' => $winnerCityCount,
];
checkEmperior();
$gameStorage->resetCache();
$invaderMessages = [];
foreach ($db->query(
'SELECT id, mailbox, message FROM message WHERE mailbox = %i ORDER BY id',
$winnerGeneralId,
) as $row) {
$message = Json::decode($row['message']);
if (($message['option']['action'] ?? null) !== 'raiseInvader') {
continue;
}
$invaderMessages[] = [
'id' => (int)$row['id'],
'mailbox' => (int)$row['mailbox'],
'text' => (string)($message['text'] ?? ''),
'args' => $message['option']['args'] ?? null,
'used' => (bool)($message['option']['used'] ?? false),
];
}
if (count($invaderMessages) !== 3) {
throw new \RuntimeException('unification must create exactly three invader choices for the ruler');
}
$buttonSnapshot = [
'year' => Util::toInt($gameStorage->year),
'month' => Util::toInt($gameStorage->month),
'isunited' => Util::toInt($gameStorage->isunited),
'clockMode' => GameClock::fromStorage($gameStorage)->getMode(),
'clockTick' => GameClock::fromStorage($gameStorage)->nowTick(),
'turntime' => Util::toInt($gameStorage->turntime),
'plock' => (int)$db->queryFirstField("SELECT plock FROM plock WHERE type = 'GAME' LIMIT 1"),
'messages' => $invaderMessages,
];
// Simulate an autoreset poll that recorded the original unification pause.
// A successful event start must replace this stale operational wall anchor.
$pausedAnchor = '2000-01-01 00:00:00.000000';
$gameStorage->autoreset_united_wall_anchor = $pausedAnchor;
$selectedMessageId = $invaderMessages[array_key_last($invaderMessages)]['id'];
$message = Message::getMessageByID($selectedMessageId);
if (!$message instanceof RaiseInvaderMessage) {
throw new \RuntimeException('selected invader message did not hydrate as RaiseInvaderMessage');
}
$reason = '';
$decisionResult = $message->agreeMessage($winnerGeneralId, $reason);
$gameStorage->resetCache();
$afterSummon = [
'result' => $decisionResult,
'reason' => $reason,
'isunited' => Util::toInt($gameStorage->isunited),
'clockMode' => GameClock::fromStorage($gameStorage)->getMode(),
'clockTick' => GameClock::fromStorage($gameStorage)->nowTick(),
'turntime' => Util::toInt($gameStorage->turntime),
'turnterm' => Util::toInt($gameStorage->turnterm),
'plock' => (int)$db->queryFirstField("SELECT plock FROM plock WHERE type = 'GAME' LIMIT 1"),
'nationCount' => (int)$db->queryFirstField('SELECT COUNT(*) FROM nation WHERE level > 0'),
'invaderNationCount' => (int)$db->queryFirstField("SELECT COUNT(*) FROM nation WHERE name LIKE 'ⓞ%족'"),
'eventCount' => (int)$db->queryFirstField(
"SELECT COUNT(*) FROM event WHERE action LIKE '%Invader%'",
),
'autoresetAnchor' => $gameStorage->autoreset_united_wall_anchor,
'autoresetAnchorRefreshed' => $gameStorage->autoreset_united_wall_anchor !== $pausedAnchor,
];
$resumeBoundary = addTurn(
cutTurn(Util::toInt($gameStorage->turntime), Util::toInt($gameStorage->turnterm)),
Util::toInt($gameStorage->turnterm),
);
GameClock::fromStorage($gameStorage)->persistTick(
$gameStorage,
GameClock::addTicks($resumeBoundary, 1),
GameClock::MODE_MANUAL,
);
$gameStorage->resetCache();
// Likewise, active event progress must move the inactivity anchor. Leaving
// this stale makes j_autoreset close a healthy invader game on the original
// unification timeline.
$stalledAnchor = '2000-01-02 00:00:00.000000';
$gameStorage->autoreset_united_wall_anchor = $stalledAnchor;
$engineCalls = 0;
$executedCalls = 0;
$lockedCalls = 0;
while ($engineCalls < 20 && Util::toInt($gameStorage->turntime) < $resumeBoundary) {
$executed = false;
$locked = false;
TurnExecutionHelper::executeAllCommand($executed, $locked);
$engineCalls++;
if ($executed) {
$executedCalls++;
}
if ($locked) {
$lockedCalls++;
}
$gameStorage->resetCache();
}
$afterResume = [
'year' => Util::toInt($gameStorage->year),
'month' => Util::toInt($gameStorage->month),
'isunited' => Util::toInt($gameStorage->isunited),
'clockMode' => GameClock::fromStorage($gameStorage)->getMode(),
'clockTick' => GameClock::fromStorage($gameStorage)->nowTick(),
'turntime' => Util::toInt($gameStorage->turntime),
'resumeBoundary' => $resumeBoundary,
'engineCalls' => $engineCalls,
'executedCalls' => $executedCalls,
'lockedCalls' => $lockedCalls,
'plock' => (int)$db->queryFirstField("SELECT plock FROM plock WHERE type = 'GAME' LIMIT 1"),
'worldHistoryCount' => (int)$db->queryFirstField('SELECT COUNT(*) FROM world_history'),
'generalRecordCount' => (int)$db->queryFirstField('SELECT COUNT(*) FROM general_record'),
'autoresetAnchor' => $gameStorage->autoreset_united_wall_anchor,
'autoresetAnchorRefreshed' => $gameStorage->autoreset_united_wall_anchor !== $stalledAnchor,
];
return [
'schemaVersion' => 1,
'engine' => 'ref',
'action' => 'UnificationInvaderResume',
'before' => $before,
'buttonSnapshot' => $buttonSnapshot,
'afterSummon' => $afterSummon,
'afterResume' => $afterResume,
];
}
function comparisonMonthlyEventTraceMain(): void
{
try {
@@ -775,6 +973,7 @@ function comparisonMonthlyEventTraceMain(): void
'NpcNationLifecycle',
'RaiseInvader',
'InvaderLifecycle',
'UnificationInvaderResume',
'AutoDeleteInvader',
'InvaderEnding',
'ChangeCity',
@@ -1141,6 +1340,13 @@ function comparisonMonthlyEventTraceMain(): void
), PHP_EOL;
return;
}
if ($actionName === 'UnificationInvaderResume') {
echo json_encode(
comparisonUnificationInvaderResumeTrace($request, $environment),
JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION,
);
return;
}
if ($actionName === 'NpcNationLifecycle') {
foreach (['turnterm', 'turntime'] as $requiredEnvironmentKey) {
if (!array_key_exists($requiredEnvironmentKey, $environment)) {