From 8cdc9c9b84c08b867e087d460264afc34e2dd870 Mon Sep 17 00:00:00 2001 From: hided62 Date: Mon, 27 Jul 2026 11:08:24 +0000 Subject: [PATCH] compare: trace nation betting user lifecycle --- .../monthly_nation_betting_lifecycle.json | 75 ++++++++ hwe/compare/monthly_event_trace.php | 162 ++++++++++++++++++ 2 files changed, 237 insertions(+) create mode 100644 hwe/compare/fixtures/monthly_nation_betting_lifecycle.json diff --git a/hwe/compare/fixtures/monthly_nation_betting_lifecycle.json b/hwe/compare/fixtures/monthly_nation_betting_lifecycle.json new file mode 100644 index 00000000..5d04f56e --- /dev/null +++ b/hwe/compare/fixtures/monthly_nation_betting_lifecycle.json @@ -0,0 +1,75 @@ +{ + "action": "NationBettingLifecycle", + "args": [1, 100], + "bettorGeneralIds": [1, 2], + "bettorOwnerIds": [1, 2], + "setup": { + "clearBetting": true, + "resetNations": true, + "deleteOtherNations": true, + "keepNationIds": [1, 2], + "nation": [ + { + "id": 1, + "values": { + "name": "위", + "color": "#111111", + "capital": 1, + "gennum": 1, + "gold": 1000, + "rice": 2000, + "tech": 100, + "power": 100, + "level": 2, + "type": "che_유가" + } + }, + { + "id": 2, + "values": { + "name": "촉", + "color": "#222222", + "capital": 2, + "gennum": 1, + "gold": 3000, + "rice": 4000, + "tech": 200, + "power": 300, + "level": 2, + "type": "che_병가" + } + } + ], + "resetCities": true, + "deleteOtherCities": true, + "keepCityIds": [1, 2], + "city": [ + { "id": 1, "values": { "name": "허창", "nation": 1, "level": 3 } }, + { "id": 2, "values": { "name": "성도", "nation": 2, "level": 3 } } + ], + "resetGenerals": true, + "deleteOtherGenerals": true, + "keepGeneralIds": [1, 2], + "general": [ + { "id": 1, "values": { "name": "장수1", "nation": 1, "city": 1, "owner": 1, "npc": 0 } }, + { "id": 2, "values": { "name": "장수2", "nation": 2, "city": 2, "owner": 2, "npc": 0 } } + ], + "inheritancePrevious": { + "1": 1000, + "2": 1000 + }, + "rankData": [ + { "generalId": 1, "type": "inherit_spent_dyn", "value": 0 }, + { "generalId": 1, "type": "inherit_earned_act", "value": 0 }, + { "generalId": 2, "type": "inherit_spent_dyn", "value": 0 }, + { "generalId": 2, "type": "inherit_earned_act", "value": 0 } + ], + "syncEnvironment": true + }, + "environment": { + "year": 200, + "month": 1, + "startyear": 190 + }, + "observe": {} +} diff --git a/hwe/compare/monthly_event_trace.php b/hwe/compare/monthly_event_trace.php index 12f2ade1..2ca68c24 100644 --- a/hwe/compare/monthly_event_trace.php +++ b/hwe/compare/monthly_event_trace.php @@ -591,6 +591,155 @@ function comparisonNpcNationLifecycleTrace( ]; } +/** + * Exercise nation betting from monthly open through real user bets and settlement. + * + * @param array $request + * @param array $environment + * @return array + */ +function comparisonNationBettingLifecycleTrace( + array $request, + array $environment, + int $eventIdBeforeAction, + int $worldHistoryIdBeforeAction, +): array { + $db = DB::db(); + $args = $request['args'] ?? []; + if (!is_array($args) || count($args) > 2) { + throw new \InvalidArgumentException('NationBettingLifecycle args must have at most two entries'); + } + $bettorGeneralIds = comparisonIntegerList( + $request['bettorGeneralIds'] ?? [], + 'bettorGeneralIds', + ); + $bettorOwnerIds = comparisonIntegerList( + $request['bettorOwnerIds'] ?? [], + 'bettorOwnerIds', + ); + if (count($bettorGeneralIds) !== 2 || count($bettorOwnerIds) !== 2) { + throw new \InvalidArgumentException( + 'NationBettingLifecycle requires exactly two bettorGeneralIds and bettorOwnerIds', + ); + } + + $year = $environment['year']; + $month = $environment['month']; + $startYear = $environment['startyear']; + (new \sammo\Event\Action\OpenNationBetting(...$args))->run([ + 'year' => $year, + 'month' => $month, + 'startyear' => $startYear, + ]); + + $gameStorage = KVStorage::getStorage($db, 'game_env'); + $gameStorage->resetCache(); + $bettingId = (int)$gameStorage->last_betting_id; + $betting = new Betting($bettingId); + $bettingInfo = $betting->getInfo(); + $candidateNationIds = array_map( + static fn(\sammo\DTO\SelectItem $candidate): int => (int)($candidate->aux['nation'] ?? 0), + $bettingInfo->candidates, + ); + if (count($candidateNationIds) !== 2) { + throw new \RuntimeException('NationBettingLifecycle requires exactly two candidates'); + } + + $finishEventCount = (int)$db->queryFirstField( + 'SELECT count(*) FROM event WHERE id > %i AND action LIKE %s', + $eventIdBeforeAction, + "%FinishNationBetting%{$bettingId}%", + ); + $afterOpen = [ + 'bettingId' => $bettingId, + 'candidateNationIds' => $candidateNationIds, + 'systemBonus' => (int)($db->queryFirstField( + 'SELECT amount FROM ng_betting WHERE betting_id = %i AND general_id = 0', + $bettingId, + ) ?? 0), + 'finishEventCount' => $finishEventCount, + 'historyLogs' => array_values($db->queryFirstColumn( + 'SELECT text FROM world_history WHERE id > %i AND nation_id = 0 ORDER BY id', + $worldHistoryIdBeforeAction, + )), + ]; + + $betting->bet($bettorGeneralIds[0], $bettorOwnerIds[0], [1], 100); + $betting->bet($bettorGeneralIds[1], $bettorOwnerIds[1], [0], 100); + $afterBets = [ + 'bets' => array_map( + static fn(array $row): array => [ + 'generalId' => (int)$row['general_id'], + 'ownerId' => $row['user_id'] === null ? null : (int)$row['user_id'], + 'selectionKey' => $row['betting_type'], + 'amount' => (int)$row['amount'], + ], + iterator_to_array($db->query( + 'SELECT general_id, user_id, betting_type, amount FROM ng_betting WHERE betting_id = %i ORDER BY general_id', + $bettingId, + )), + ), + 'inheritancePrevious' => array_map( + static function (int $ownerId): int { + $value = KVStorage::getStorage(DB::db(), "inheritance_{$ownerId}")->getValue('previous'); + return is_array($value) ? (int)($value[0] ?? 0) : 0; + }, + $bettorOwnerIds, + ), + 'spentRank' => array_map( + static fn(int $generalId): int => (int)(DB::db()->queryFirstField( + 'SELECT value FROM rank_data WHERE general_id = %i AND type = %s', + $generalId, + \sammo\Enums\RankColumn::inherit_point_spent_dynamic->value, + ) ?? 0), + $bettorGeneralIds, + ), + ]; + + $db->update('nation', ['level' => 0], 'nation = %i', $candidateNationIds[1]); + $finishResult = (new \sammo\Event\Action\FinishNationBetting($bettingId))->run([ + 'year' => $year, + 'month' => $month + 1, + 'startyear' => $startYear, + ]); + $settledInfo = (new Betting($bettingId))->getInfo(); + $afterFinish = [ + 'result' => $finishResult[1] ?? null, + 'finished' => $settledInfo->finished, + 'winner' => $settledInfo->winner, + 'inheritancePrevious' => array_map( + static function (int $ownerId): int { + $value = KVStorage::getStorage(DB::db(), "inheritance_{$ownerId}")->getValue('previous'); + return is_array($value) ? (int)($value[0] ?? 0) : 0; + }, + $bettorOwnerIds, + ), + 'earnedRank' => array_map( + static fn(int $generalId): int => (int)(DB::db()->queryFirstField( + 'SELECT value FROM rank_data WHERE general_id = %i AND type = %s', + $generalId, + \sammo\Enums\RankColumn::inherit_point_earned_by_action->value, + ) ?? 0), + $bettorGeneralIds, + ), + 'historyLogs' => array_values($db->queryFirstColumn( + 'SELECT text FROM world_history WHERE id > %i AND nation_id = 0 ORDER BY id', + $worldHistoryIdBeforeAction, + )), + ]; + + return [ + 'schemaVersion' => 1, + 'engine' => 'ref', + 'action' => 'NationBettingLifecycle', + 'phases' => [ + 'afterOpen' => $afterOpen, + 'afterBets' => $afterBets, + 'afterFinish' => $afterFinish, + ], + ]; +} + function comparisonMonthlyEventTraceMain(): void { try { @@ -618,6 +767,7 @@ function comparisonMonthlyEventTraceMain(): void 'InvaderEnding', 'ChangeCity', 'ProvideNPCTroopLeader', + 'NationBettingLifecycle', 'OpenNationBetting', 'FinishNationBetting', 'BlockScoutAction', @@ -993,6 +1143,18 @@ function comparisonMonthlyEventTraceMain(): void ), PHP_EOL; return; } + if ($actionName === 'NationBettingLifecycle') { + echo json_encode( + comparisonNationBettingLifecycleTrace( + $request, + $environment, + $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)) {