diff --git a/tests/s100-reselection-final-growth-check.php b/tests/s100-reselection-final-growth-check.php new file mode 100644 index 00000000..0e038494 --- /dev/null +++ b/tests/s100-reselection-final-growth-check.php @@ -0,0 +1,212 @@ + 180, 'year' => 195, 'month' => 1]; +const STAT_KEYS = ['leadership', 'strength', 'intel']; +const DEX_KEYS = ['dex1', 'dex2', 'dex3', 'dex4', 'dex5']; + +$_SERVER['REMOTE_ADDR'] = '127.0.0.1'; +$_SERVER['REQUEST_URI'] = '/s100-reselection-final-growth-check'; + +require APP_ROOT . '/hwe/lib.php'; +require APP_ROOT . '/hwe/func.php'; + +$mode = $argv[1] ?? ''; +$username = $argv[2] ?? 's100user01'; +$snapshotPath = $argv[3] ?? '/tmp/s100-reselection-final-growth.json'; +if (!in_array($mode, ['prepare', 'verify'], true)) { + throw new InvalidArgumentException('Mode must be prepare or verify'); +} +if (preg_match('/^s100user[0-9]{2}$/', $username) !== 1) { + throw new InvalidArgumentException('Username must match s100userNN'); +} + +$db = DB::db(); +$owner = RootDB::db()->queryFirstField( + 'SELECT no FROM member WHERE id=%s', + $username +); +if ($owner === null) { + throw new RuntimeException("No member for {$username}"); +} +$generalID = $db->queryFirstField('SELECT no FROM general WHERE owner=%i', $owner); +if ($generalID === null) { + throw new RuntimeException("No general for owner {$owner}"); +} + +if ($mode === 'prepare') { + $gameStor = KVStorage::getStorage($db, 'game_env'); + $gameStor->setValue('year', TEST_ENV['year']); + $gameStor->setValue('month', TEST_ENV['month']); + + $eventResult = (new AdvanceCentennialAllStar())->run(TEST_ENV); + $general = General::createObjFromDB((int) $generalID); + $beforeGrowth = readGeneralState($general); + + $statGrowthKey = null; + foreach (STAT_KEYS as $key) { + if ((int) $general->getVar($key) < GameConst::$maxLevel) { + $statGrowthKey = $key; + break; + } + } + if ($statGrowthKey === null) { + throw new RuntimeException('No stat can receive an organic level-up'); + } + $general->increaseVar( + "{$statGrowthKey}_exp", + GameConst::$upgradeLimit + ); + if (!$general->checkStatChange()) { + throw new RuntimeException('Organic stat level-up did not occur'); + } + + $general->addDex($general->getCrewTypeObj(), 12345, false); + $general->setAuxVar('next_change', '2000-01-01 00:00:00'); + $general->applyDB($db); + + $afterGrowth = readGeneralState($general); + $snapshot = [ + 'username' => $username, + 'generalID' => (int) $generalID, + 'eventResult' => $eventResult, + 'statGrowthKey' => $statGrowthKey, + 'beforeGrowth' => $beforeGrowth, + 'afterGrowth' => $afterGrowth, + ]; + if (file_put_contents( + $snapshotPath, + Json::encode($snapshot, Json::PRETTY) . PHP_EOL + ) === false) { + throw new RuntimeException("Could not write {$snapshotPath}"); + } + chmod($snapshotPath, 0600); + printf( + "Prepared final-growth reselection: user=%s target=%s stat=%s dexDelta=%d\n", + $username, + $afterGrowth['targetId'], + $statGrowthKey, + array_sum($afterGrowth['dex']) - array_sum($beforeGrowth['dex']) + ); + exit(0); +} + +$snapshot = Json::decode((string) file_get_contents($snapshotPath)); +$general = General::createObjFromDB((int) $generalID); +$actual = readGeneralState($general); +$targetInfoRaw = $db->queryFirstField( + 'SELECT info FROM select_pool WHERE general_id=%i', + $generalID +); +if ($targetInfoRaw === null) { + throw new RuntimeException('Reselected target is not assigned to the general'); +} +$targetInfo = Json::decode($targetInfoRaw); +$oldTargetId = $snapshot['afterGrowth']['targetId']; +$newTargetId = (string) ($targetInfo['uniqueName'] ?? ''); +if ($newTargetId === '' || $newTargetId === $oldTargetId) { + throw new RuntimeException('Chromium reselection did not change the target'); +} +if ($actual['targetId'] !== $newTargetId) { + throw new RuntimeException('General aux target does not match the assigned pool target'); +} + +$expectedStats = CentennialAllStarGrowthService::calculateUserCurrentTargetStats( + $targetInfo, + TEST_ENV +); +$expectedDex = []; +foreach (STAT_KEYS as $key) { + $organic = max( + 0, + (int) $snapshot['afterGrowth']['stats'][$key] + - (int) $snapshot['afterGrowth']['granted'][$key] + ); + $expectedStats[$key] = max($organic, $expectedStats[$key]); + assertSameValue($expectedStats[$key], $actual['stats'][$key], $key); + assertSameValue( + $actual['stats'][$key] - $organic, + $actual['granted'][$key], + "{$key} event grant" + ); +} +foreach (DEX_KEYS as $idx => $key) { + $organic = max( + 0, + (int) $snapshot['afterGrowth']['dex'][$key] + - (int) $snapshot['afterGrowth']['granted'][$key] + ); + $targetFloor = CentennialAllStarGrowthService::calculateDexTargetFloor( + (int) ($targetInfo['dex'][$idx] ?? 0), + TEST_ENV + ); + $expectedDex[$key] = max($organic, $targetFloor); + assertSameValue($expectedDex[$key], $actual['dex'][$key], $key); + assertSameValue($targetFloor, $actual['dexFloor'][$key], "{$key} floor"); + assertSameValue( + $actual['dex'][$key] - $organic, + $actual['granted'][$key], + "{$key} event grant" + ); +} + +printf( + "Verified final-growth reselection: user=%s old=%s new=%s stats=%s dex=%s\n", + $username, + $oldTargetId, + $newTargetId, + Json::encode($actual['stats']), + Json::encode($actual['dex']) +); + +/** + * @return array{ + * targetId:string, + * stats:array, + * dex:array, + * granted:array, + * dexFloor:array + * } + */ +function readGeneralState(General $general): array +{ + $aux = $general->getAuxVar(CentennialAllStarGrowthService::AUX_KEY); + if (!is_array($aux)) { + throw new RuntimeException('Centennial growth ledger is missing'); + } + $stats = []; + foreach (STAT_KEYS as $key) { + $stats[$key] = (int) $general->getVar($key); + } + $dex = []; + foreach (DEX_KEYS as $key) { + $dex[$key] = (int) $general->getVar($key); + } + return [ + 'targetId' => (string) ($aux['targetId'] ?? ''), + 'stats' => $stats, + 'dex' => $dex, + 'granted' => $aux['granted'] ?? [], + 'dexFloor' => $aux['dexFloor'] ?? [], + ]; +} + +function assertSameValue(int $expected, int $actual, string $label): void +{ + if ($actual !== $expected) { + throw new RuntimeException( + "{$label}: expected {$expected}, got {$actual}" + ); + } +}