test(compare): reproduce invader game resume
This commit is contained in:
@@ -0,0 +1,85 @@
|
|||||||
|
{
|
||||||
|
"action": "UnificationInvaderResume",
|
||||||
|
"winnerGeneralId": 1,
|
||||||
|
"setup": {
|
||||||
|
"resetCities": true,
|
||||||
|
"setAllCitiesNation": 1,
|
||||||
|
"resetGenerals": true,
|
||||||
|
"deleteOtherGenerals": true,
|
||||||
|
"keepGeneralIds": [1, 2, 3, 4, 5, 6, 7, 8],
|
||||||
|
"resetNations": true,
|
||||||
|
"deleteOtherNations": true,
|
||||||
|
"keepNationIds": [1],
|
||||||
|
"resetDiplomacy": true,
|
||||||
|
"resetEvents": true,
|
||||||
|
"clearInheritanceOwnerIds": [1],
|
||||||
|
"inheritancePrevious": {
|
||||||
|
"1": 0
|
||||||
|
},
|
||||||
|
"nation": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"values": {
|
||||||
|
"name": "재현국",
|
||||||
|
"color": "#336699",
|
||||||
|
"capital": 1,
|
||||||
|
"gennum": 8,
|
||||||
|
"gold": 100000,
|
||||||
|
"rice": 100000,
|
||||||
|
"tech": 1000,
|
||||||
|
"power": 1000,
|
||||||
|
"level": 2,
|
||||||
|
"type": "che_유가",
|
||||||
|
"aux": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"general": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"values": {
|
||||||
|
"name": "재현군주",
|
||||||
|
"nation": 1,
|
||||||
|
"city": 1,
|
||||||
|
"officer_level": 12,
|
||||||
|
"owner": 1,
|
||||||
|
"npc": 0,
|
||||||
|
"age": 30,
|
||||||
|
"bornyear": 170,
|
||||||
|
"deadyear": 260,
|
||||||
|
"belong": 240,
|
||||||
|
"aux": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ "id": 2, "values": { "name": "재현관직11", "nation": 1, "city": 1, "officer_level": 11, "owner": 0, "npc": 2, "aux": {} } },
|
||||||
|
{ "id": 3, "values": { "name": "재현관직10", "nation": 1, "city": 1, "officer_level": 10, "owner": 0, "npc": 2, "aux": {} } },
|
||||||
|
{ "id": 4, "values": { "name": "재현관직9", "nation": 1, "city": 1, "officer_level": 9, "owner": 0, "npc": 2, "aux": {} } },
|
||||||
|
{ "id": 5, "values": { "name": "재현관직8", "nation": 1, "city": 1, "officer_level": 8, "owner": 0, "npc": 2, "aux": {} } },
|
||||||
|
{ "id": 6, "values": { "name": "재현관직7", "nation": 1, "city": 1, "officer_level": 7, "owner": 0, "npc": 2, "aux": {} } },
|
||||||
|
{ "id": 7, "values": { "name": "재현관직6", "nation": 1, "city": 1, "officer_level": 6, "owner": 0, "npc": 2, "aux": {} } },
|
||||||
|
{ "id": 8, "values": { "name": "재현관직5", "nation": 1, "city": 1, "officer_level": 5, "owner": 0, "npc": 2, "aux": {} } }
|
||||||
|
],
|
||||||
|
"nationEnvironment": [
|
||||||
|
{ "nationId": 1, "key": "nationNotice", "value": { "msg": "재현" } },
|
||||||
|
{ "nationId": 1, "key": "scout_msg", "value": "재현" },
|
||||||
|
{ "nationId": 1, "key": "max_power", "value": {} }
|
||||||
|
],
|
||||||
|
"gameEnvironment": {
|
||||||
|
"isunited": 0,
|
||||||
|
"refreshLimit": 30,
|
||||||
|
"block_change_scout": true
|
||||||
|
},
|
||||||
|
"syncEnvironment": true
|
||||||
|
},
|
||||||
|
"environment": {
|
||||||
|
"year": 200,
|
||||||
|
"month": 1,
|
||||||
|
"startyear": 180,
|
||||||
|
"turnterm": 10,
|
||||||
|
"turntime": "0200-01-01 00:00:00",
|
||||||
|
"show_img_level": 3,
|
||||||
|
"stored_icons": [],
|
||||||
|
"icon_path": ".",
|
||||||
|
"fiction": [0]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
function comparisonMonthlyEventTraceMain(): void
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
@@ -775,6 +973,7 @@ function comparisonMonthlyEventTraceMain(): void
|
|||||||
'NpcNationLifecycle',
|
'NpcNationLifecycle',
|
||||||
'RaiseInvader',
|
'RaiseInvader',
|
||||||
'InvaderLifecycle',
|
'InvaderLifecycle',
|
||||||
|
'UnificationInvaderResume',
|
||||||
'AutoDeleteInvader',
|
'AutoDeleteInvader',
|
||||||
'InvaderEnding',
|
'InvaderEnding',
|
||||||
'ChangeCity',
|
'ChangeCity',
|
||||||
@@ -1141,6 +1340,13 @@ function comparisonMonthlyEventTraceMain(): void
|
|||||||
), PHP_EOL;
|
), PHP_EOL;
|
||||||
return;
|
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') {
|
if ($actionName === 'NpcNationLifecycle') {
|
||||||
foreach (['turnterm', 'turntime'] as $requiredEnvironmentKey) {
|
foreach (['turnterm', 'turntime'] as $requiredEnvironmentKey) {
|
||||||
if (!array_key_exists($requiredEnvironmentKey, $environment)) {
|
if (!array_key_exists($requiredEnvironmentKey, $environment)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user