From 47fa7885c5b647c66152f08db4c92ce216ecfb58 Mon Sep 17 00:00:00 2001 From: hided62 Date: Mon, 27 Jul 2026 10:48:40 +0000 Subject: [PATCH] compare: trace invader lifecycle across months --- .../fixtures/monthly_invader_lifecycle.json | 108 ++++++++ hwe/compare/monthly_event_trace.php | 247 ++++++++++++++++++ 2 files changed, 355 insertions(+) create mode 100644 hwe/compare/fixtures/monthly_invader_lifecycle.json diff --git a/hwe/compare/fixtures/monthly_invader_lifecycle.json b/hwe/compare/fixtures/monthly_invader_lifecycle.json new file mode 100644 index 00000000..1416a61c --- /dev/null +++ b/hwe/compare/fixtures/monthly_invader_lifecycle.json @@ -0,0 +1,108 @@ +{ + "action": "InvaderLifecycle", + "args": [10, 150, 100, 20], + "setup": { + "resetCities": true, + "resetGenerals": true, + "deleteOtherGenerals": true, + "keepGeneralIds": [1], + "resetNations": true, + "deleteOtherNations": true, + "keepNationIds": [1], + "resetDiplomacy": true, + "resetEvents": true, + "city": [ + { + "id": 1, + "values": { + "name": "기준도시", + "level": 3, + "nation": 1, + "pop": 1000, + "pop_max": 10000, + "agri": 2000, + "agri_max": 20000, + "comm": 3000, + "comm_max": 30000, + "secu": 4000, + "secu_max": 40000, + "def": 5000, + "def_max": 50000, + "wall": 6000, + "wall_max": 60000 + } + }, + { + "id": 48, + "values": { + "name": "남만", + "level": 4, + "nation": 0, + "pop": 1000, + "pop_max": 10000, + "agri": 2000, + "agri_max": 20000, + "comm": 3000, + "comm_max": 30000, + "secu": 4000, + "secu_max": 40000, + "def": 5000, + "def_max": 50000, + "wall": 6000, + "wall_max": 60000 + } + } + ], + "nation": [ + { + "id": 1, + "values": { + "name": "기준국", + "capital": 1, + "gennum": 1, + "tech": 100, + "level": 2, + "type": "che_유가" + } + } + ], + "general": [ + { + "id": 1, + "values": { + "name": "군주", + "nation": 1, + "city": 1, + "officer_level": 12, + "npc": 0, + "leadership": 50, + "strength": 50, + "intel": 50, + "experience": 1000, + "dedication": 1000, + "dex1": 10, + "dex2": 20, + "dex3": 30, + "dex4": 40, + "dex5": 50, + "turntime": "0200-01-01 00:05:00" + } + } + ], + "gameEnvironment": { + "isunited": 0, + "refreshLimit": 3 + }, + "syncEnvironment": true + }, + "environment": { + "year": 200, + "month": 1, + "startyear": 190, + "turnterm": 10, + "turntime": "0200-01-01 00:00:00", + "show_img_level": 3, + "fiction": [0] + }, + "observe": {} +} diff --git a/hwe/compare/monthly_event_trace.php b/hwe/compare/monthly_event_trace.php index dec892cb..d3dbbfb0 100644 --- a/hwe/compare/monthly_event_trace.php +++ b/hwe/compare/monthly_event_trace.php @@ -263,6 +263,230 @@ function comparisonMonthlyDetails(array $observe, int $worldHistoryAfterId): arr ]; } +/** + * Exercise the dynamically-created invader events against one evolving database. + * + * @param array $request + * @param array $environment + * @return array + */ +function comparisonInvaderLifecycleTrace( + array $request, + array $environment, + int $generalIdBeforeAction, + int $nationIdBeforeAction, + int $eventIdBeforeAction, + int $worldHistoryIdBeforeAction, +): array { + $db = DB::db(); + $args = $request['args'] ?? []; + if (!is_array($args) || count($args) > 4) { + throw new \InvalidArgumentException('InvaderLifecycle args must have at most four entries'); + } + $year = $environment['year']; + $month = $environment['month']; + $startYear = $environment['startyear']; + $raiseEnvironment = [ + 'year' => $year, + 'month' => $month, + 'startyear' => $startYear, + 'turnterm' => $environment['turnterm'], + 'turntime' => $environment['turntime'], + 'show_img_level' => $environment['show_img_level'] ?? 3, + 'stored_icons' => $environment['stored_icons'] ?? [], + 'icon_path' => $environment['icon_path'] ?? '.', + 'fiction' => $environment['fiction'] ?? [0], + ]; + + (new \sammo\Event\Action\RaiseInvader(...$args))->run($raiseEnvironment); + + $invaderNationIds = array_map( + 'intval', + $db->queryFirstColumn('SELECT nation FROM nation WHERE nation > %i ORDER BY nation', $nationIdBeforeAction), + ); + $existingNationId = (int)$db->queryFirstField( + 'SELECT nation FROM nation WHERE nation <= %i ORDER BY nation LIMIT 1', + $nationIdBeforeAction, + ); + $rulerIds = array_map( + 'intval', + $db->queryFirstColumn( + 'SELECT no FROM general WHERE nation IN %li AND officer_level = 12 ORDER BY nation', + $invaderNationIds, + ), + ); + if ($invaderNationIds === [] || $existingNationId <= 0 || count($rulerIds) !== count($invaderNationIds)) { + throw new \RuntimeException('InvaderLifecycle failed to create the expected entities'); + } + + $autoDeleteEvents = []; + $endingEventId = 0; + foreach ($db->query('SELECT id, action FROM event WHERE id > %i ORDER BY id', $eventIdBeforeAction) as $row) { + $action = comparisonJsonValue($row['action']); + $actionName = $action[0][0] ?? null; + if ($actionName === 'AutoDeleteInvader') { + $autoDeleteEvents[] = [ + 'eventId' => (int)$row['id'], + 'nationId' => (int)($action[0][1] ?? 0), + ]; + } elseif ($actionName === 'InvaderEnding') { + $endingEventId = (int)$row['id']; + } + } + if (count($autoDeleteEvents) !== count($invaderNationIds) || $endingEventId <= 0) { + throw new \RuntimeException('InvaderLifecycle failed to create follow-up events'); + } + + $diplomacyRows = iterator_to_array($db->query( + 'SELECT me, you, state, term FROM diplomacy WHERE (me IN %li AND you = %i) OR (me = %i AND you IN %li) ORDER BY me, you', + $invaderNationIds, + $existingNationId, + $existingNationId, + $invaderNationIds, + )); + $diplomacyStates = array_values(array_unique(array_map( + static fn(array $row): string => "{$row['state']}:{$row['term']}", + $diplomacyRows, + ))); + sort($diplomacyStates); + $generalCounts = array_map( + static fn(int $nationId): int => (int)DB::db()->queryFirstField( + 'SELECT count(*) FROM general WHERE nation = %i', + $nationId, + ), + $invaderNationIds, + ); + $diplomacyCounts = array_map( + static fn(int $nationId): int => (int)DB::db()->queryFirstField( + 'SELECT count(*) FROM diplomacy WHERE (me = %i AND you = %i) OR (me = %i AND you = %i)', + $nationId, + $existingNationId, + $existingNationId, + $nationId, + ), + $invaderNationIds, + ); + $gameStorage = KVStorage::getStorage($db, 'game_env'); + $afterRaise = [ + 'createdNationCount' => count($invaderNationIds), + 'createdGeneralCount' => (int)$db->queryFirstField( + 'SELECT count(*) FROM general WHERE no > %i', + $generalIdBeforeAction, + ), + 'followUpEventCount' => (int)$db->queryFirstField( + 'SELECT count(*) FROM event WHERE id > %i', + $eventIdBeforeAction, + ), + 'generalCountsPerNation' => array_values(array_unique($generalCounts)), + 'diplomacyCountsPerNation' => array_values(array_unique($diplomacyCounts)), + 'diplomacyStates' => $diplomacyStates, + 'isunited' => (int)$gameStorage->isunited, + 'blockChangeScout' => (bool)$gameStorage->block_change_scout, + ]; + + $warResults = []; + foreach ($autoDeleteEvents as $autoDeleteEvent) { + $warResult = (new \sammo\Event\Action\AutoDeleteInvader($autoDeleteEvent['nationId']))->run([ + 'year' => $year, + 'month' => $month + 1, + 'startyear' => $startYear, + 'currentEventID' => $autoDeleteEvent['eventId'], + ]); + $warResults[] = $warResult[1] ?? null; + } + $atWar = [ + 'results' => array_values(array_unique($warResults)), + 'autoDeleteEventCount' => (int)$db->queryFirstField( + 'SELECT count(*) FROM event WHERE id IN %li', + array_column($autoDeleteEvents, 'eventId'), + ), + ]; + + $db->update( + 'diplomacy', + ['state' => 2, 'term' => 0], + 'me IN %li OR you IN %li', + $invaderNationIds, + $invaderNationIds, + ); + $peaceResults = []; + foreach ($autoDeleteEvents as $autoDeleteEvent) { + $peaceResult = (new \sammo\Event\Action\AutoDeleteInvader($autoDeleteEvent['nationId']))->run([ + 'year' => $year, + 'month' => $month + 2, + 'startyear' => $startYear, + 'currentEventID' => $autoDeleteEvent['eventId'], + ]); + $peaceResults[] = $peaceResult[1] ?? null; + } + $atPeace = [ + 'results' => array_values(array_unique($peaceResults)), + 'autoDeleteEventCount' => (int)$db->queryFirstField( + 'SELECT count(*) FROM event WHERE id IN %li', + array_column($autoDeleteEvents, 'eventId'), + ), + 'rulerWanderTurnCount' => (int)$db->queryFirstField( + 'SELECT count(*) FROM general_turn WHERE general_id IN %li AND action = %s', + $rulerIds, + 'che_방랑', + ), + 'endingEventPresent' => (bool)$db->queryFirstField( + 'SELECT count(*) FROM event WHERE id = %i', + $endingEventId, + ), + ]; + + $invaderGeneralIds = array_map('intval', $db->queryFirstColumn( + 'SELECT no FROM general WHERE nation IN %li', + $invaderNationIds, + )); + if ($invaderGeneralIds !== []) { + $db->delete('general_turn', 'general_id IN %li', $invaderGeneralIds); + $db->delete('rank_data', 'general_id IN %li', $invaderGeneralIds); + $db->delete('troop', 'nation IN %li', $invaderNationIds); + $db->delete('general', 'no IN %li', $invaderGeneralIds); + } + $db->delete('nation_turn', 'nation_id IN %li', $invaderNationIds); + $db->delete('diplomacy', 'me IN %li OR you IN %li', $invaderNationIds, $invaderNationIds); + $db->delete('nation', 'nation IN %li', $invaderNationIds); + $db->update('city', ['nation' => $existingNationId], true); + + $endingResult = (new \sammo\Event\Action\InvaderEnding())->run([ + 'year' => $year, + 'month' => $month + 3, + 'startyear' => $startYear, + 'currentEventID' => $endingEventId, + ]); + $gameStorage->resetCache(); + $afterUserWin = [ + 'result' => $endingResult[1] ?? null, + 'endingEventPresent' => (bool)$db->queryFirstField( + 'SELECT count(*) FROM event WHERE id = %i', + $endingEventId, + ), + 'isunited' => (int)$gameStorage->isunited, + 'refreshLimit' => (int)$gameStorage->refreshLimit, + 'logs' => array_values($db->queryFirstColumn( + 'SELECT text FROM world_history WHERE id > %i AND nation_id = 0 AND year = %i AND month = %i ORDER BY id', + $worldHistoryIdBeforeAction, + $year, + $month + 3, + )), + ]; + + return [ + 'schemaVersion' => 1, + 'engine' => 'ref', + 'action' => 'InvaderLifecycle', + 'phases' => [ + 'afterRaise' => $afterRaise, + 'atWar' => $atWar, + 'atPeace' => $atPeace, + 'afterUserWin' => $afterUserWin, + ], + ]; +} + function comparisonMonthlyEventTraceMain(): void { try { @@ -284,6 +508,7 @@ function comparisonMonthlyEventTraceMain(): void 'RegNeutralNPC', 'RaiseNPCNation', 'RaiseInvader', + 'InvaderLifecycle', 'AutoDeleteInvader', 'InvaderEnding', 'ChangeCity', @@ -623,6 +848,28 @@ function comparisonMonthlyEventTraceMain(): void } } + if ($actionName === 'InvaderLifecycle') { + foreach (['turnterm', 'turntime'] as $requiredEnvironmentKey) { + if (!array_key_exists($requiredEnvironmentKey, $environment)) { + throw new \InvalidArgumentException( + "InvaderLifecycle requires environment.{$requiredEnvironmentKey}", + ); + } + } + echo json_encode( + comparisonInvaderLifecycleTrace( + $request, + $environment, + $generalIdBeforeAction, + $nationIdBeforeAction, + $eventIdBeforeAction, + $worldHistoryAfterId, + ), + JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION, + ), PHP_EOL; + return; + } + $actionClass = "\\sammo\\Event\\Action\\{$actionName}"; if (in_array($actionName, ['PreUpdateMonthly', 'PostUpdateMonthly', 'MonthlyBoundary'], true)) { $action = null;