test(compare): trace deterministic monthly seed progression

This commit is contained in:
2026-08-03 18:57:01 +00:00
parent 3b2fd1082f
commit f16a6b6880
23 changed files with 1274 additions and 213 deletions
+289 -38
View File
@@ -6,8 +6,26 @@ use sammo\Enums\EventTarget;
use sammo\Enums\InheritanceKey;
use \Symfony\Component\Lock;
class TurnExecutionHelper
{
class TurnExecutionHelper
{
/** @var array<string, int> */
private static array $comparisonActionCounts = [];
/** @var array<string, list<int>> */
private static array $comparisonActionGeneralIds = [];
/** @return array<string, int> */
public static function getComparisonActionCounts(): array
{
ksort(self::$comparisonActionCounts, SORT_STRING);
return self::$comparisonActionCounts;
}
/** @return array<string, list<int>> */
public static function getComparisonActionGeneralIds(): array
{
ksort(self::$comparisonActionGeneralIds, SORT_STRING);
return self::$comparisonActionGeneralIds;
}
/** @var General*/
protected $generalObj;
@@ -241,15 +259,54 @@ class TurnExecutionHelper
$currentTurn = null;
$gameStor = KVStorage::getStorage($db, 'game_env');
$autorun_user = $gameStor->autorun_user;
$autorun_user = $gameStor->autorun_user;
$traceCityChanges = PHP_SAPI === 'cli'
&& getenv('REF_DETERMINISTIC_INSTALL_ENABLED') === '1'
&& getenv('REF_AI_TRACE_CITY_CHANGES') === '1';
$observedCityStates = [];
if ($traceCityChanges) {
foreach ($db->query('SELECT city, nation, front, def, wall, pop, state, term, trust FROM city ORDER BY city') as $row) {
$observedCityStates[(int)$row['city']] = [
(int)$row['nation'],
(int)$row['front'],
(int)$row['def'],
(int)$row['wall'],
(int)$row['pop'],
(int)$row['state'],
(int)$row['term'],
(float)$row['trust'],
];
}
}
foreach ($generalsTodo as $rawGeneral) {
$currActionTime = new \DateTimeImmutable();
if ($currActionTime > $limitActionTime) {
return [true, $currentTurn];
}
foreach ($generalsTodo as $rawGeneral) {
// The comparison harness fixes the logical clock. A real wall-clock
// timeout here made the final post-month drain depend on host load,
// so identical seeds processed a variable number of new generals.
// Preserve the production timeout unless deterministic comparison
// mode was explicitly enabled.
if (getenv('REF_DETERMINISTIC_INSTALL_ENABLED') !== '1') {
$currActionTime = new \DateTimeImmutable();
if ($currActionTime > $limitActionTime) {
return [true, $currentTurn];
}
}
$general = General::createObjFromDB($rawGeneral['no']);
$general = General::createObjFromDB($rawGeneral['no']);
if (
PHP_SAPI === 'cli'
&& getenv('REF_DETERMINISTIC_INSTALL_ENABLED') === '1'
&& getenv('REF_AI_TRACE_SEQUENCE') === '1'
) {
fwrite(STDOUT, sprintf(
"TURN_START_REF id=%d nation=%d city=%d officer=%d turnTime=%s\n",
$general->getID(),
$general->getNationID(),
$general->getCityID(),
$general->getVar('officer_level'),
$general->getTurnTime()
));
}
$nationStor = KVStorage::getStorage($db, $general->getNationID(), 'nation_env');
$turnObj = new static($general);
@@ -302,9 +359,26 @@ class TurnExecutionHelper
if (!($nationCommandObj instanceof Command\Nation\휴식)) {
$hasReservedTurn = true;
}
if ($ai && ($general->getAuxVar('use_auto_nation_turn') ?? 1)) {
$nationCommandObj = $ai->chooseNationTurn($nationCommandObj);
$cityName = CityConst::byID($general->getCityID())->name;
if ($ai && ($general->getAuxVar('use_auto_nation_turn') ?? 1)) {
$nationCommandObj = $ai->chooseNationTurn($nationCommandObj);
$traceNationIds = getenv('REF_AI_TRACE_NATION_IDS');
if (
PHP_SAPI === 'cli'
&& getenv('REF_DETERMINISTIC_INSTALL_ENABLED') === '1'
&& is_string($traceNationIds)
&& in_array((string) $general->getID(), explode(',', $traceNationIds), true)
) {
fwrite(STDOUT, 'AI_NATION_TRACE ' . Json::encode([
'generalId' => $general->getID(),
'nationId' => $general->getNationID(),
'year' => $year,
'month' => $month,
'action' => $nationCommandObj->getRawClassName(),
'args' => $nationCommandObj->getArg(),
'reason' => $nationCommandObj->reason,
]) . "\n");
}
$cityName = CityConst::byID($general->getCityID())->name;
LogText("NationTurn", "General, {$general->getName()}, {$general->getID()}, {$cityName}, {$general->getStaticNation()['name']}, {$nationCommandObj->getBrief()}, {$nationCommandObj->reason}, ");
}
$rng = new RandUtil(new LiteHashDRBG(Util::simpleSerialize(
@@ -319,8 +393,20 @@ class TurnExecutionHelper
$rng,
$nationCommandObj
);
$nationStor->setValue($lastNationTurnKey, $resultNationTurn->toRaw());
$general->setRawCity(null);
$nationStor->setValue($lastNationTurnKey, $resultNationTurn->toRaw());
if (
PHP_SAPI === 'cli'
&& getenv('REF_DETERMINISTIC_INSTALL_ENABLED') === '1'
&& getenv('REF_AI_TRACE_SEQUENCE') === '1'
) {
fwrite(STDOUT, sprintf(
"ACTION_REF kind=nation actor=%d requested=%s resolved=%s\n",
$general->getID(),
$nationCommandObj->getRawClassName(),
$nationCommandObj->getRawClassName()
));
}
$general->setRawCity(null);
}
$generalCommandObj = $general->getReservedTurn(0, $env);
@@ -328,16 +414,57 @@ class TurnExecutionHelper
$hasReservedTurn = true;
}
if ($ai) {
$newGeneralCommandObj = $ai->chooseGeneralTurn($generalCommandObj); // npc AI 처리
if ($generalCommandObj !== $newGeneralCommandObj) {
$autorunMode = true;
$generalCommandObj = $newGeneralCommandObj;
}
$cityName = CityConst::byID($general->getCityID())->name;
if ($ai) {
$newGeneralCommandObj = $ai->chooseGeneralTurn($generalCommandObj); // npc AI 처리
if ($generalCommandObj !== $newGeneralCommandObj) {
$autorunMode = true;
$generalCommandObj = $newGeneralCommandObj;
}
$traceGeneralIds = getenv('REF_AI_TRACE_GENERAL_IDS');
if (
PHP_SAPI === 'cli'
&& getenv('REF_DETERMINISTIC_INSTALL_ENABLED') === '1'
&& is_string($traceGeneralIds)
&& in_array((string) $general->getID(), explode(',', $traceGeneralIds), true)
) {
fwrite(STDOUT, 'AI_TRACE ' . Json::encode([
'generalId' => $general->getID(),
'year' => $year,
'month' => $month,
'npc' => $general->getNPCType(),
'cityId' => $general->getCityID(),
'gold' => $general->getVar('gold'),
'rice' => $general->getVar('rice'),
'affinity' => $general->getVar('affinity'),
'stats' => [
'leadership' => $general->getVar('leadership'),
'strength' => $general->getVar('strength'),
'intel' => $general->getVar('intel'),
],
'fullStats' => [
'leadership' => $general->getLeadership(false),
'strength' => $general->getStrength(false),
'intel' => $general->getIntel(false),
],
'action' => $generalCommandObj->getRawClassName(),
'args' => $generalCommandObj->getArg(),
'reason' => $generalCommandObj->reason,
]) . "\n");
}
$cityName = CityConst::byID($general->getCityID())->name;
LogText("turn", "General, {$general->getName()}, {$general->getID()}, {$cityName}, {$general->getStaticNation()['name']}, {$generalCommandObj->getBrief()}, {$generalCommandObj->reason}, ");
}
$rng = new RandUtil(new LiteHashDRBG(Util::simpleSerialize(
if (
PHP_SAPI === 'cli'
&& getenv('REF_DETERMINISTIC_INSTALL_ENABLED') === '1'
&& getenv('REF_AI_TRACE_SUMMARY') === '1'
) {
$actionName = $generalCommandObj->getRawClassName();
self::$comparisonActionCounts[$actionName] =
(self::$comparisonActionCounts[$actionName] ?? 0) + 1;
self::$comparisonActionGeneralIds[$actionName][] = $general->getID();
}
$rng = new RandUtil(new LiteHashDRBG(Util::simpleSerialize(
UniqueConst::$hiddenSeed,
'generalCommand',
$year,
@@ -345,8 +472,82 @@ class TurnExecutionHelper
$general->getID(),
$generalCommandObj->getRawClassName()
)));
$turnObj->processCommand($rng, $generalCommandObj, $autorunMode);
}
$turnObj->processCommand($rng, $generalCommandObj, $autorunMode);
if ($traceCityChanges) {
$changes = [];
foreach ($db->query('SELECT city, nation, front, def, wall, pop, state, term, trust FROM city ORDER BY city') as $row) {
$cityId = (int)$row['city'];
$next = [
(int)$row['nation'],
(int)$row['front'],
(int)$row['def'],
(int)$row['wall'],
(int)$row['pop'],
(int)$row['state'],
(int)$row['term'],
(float)$row['trust'],
];
if (($observedCityStates[$cityId] ?? null) !== $next) {
$changes[] = [
'id' => $cityId,
'before' => $observedCityStates[$cityId] ?? null,
'after' => $next,
];
$observedCityStates[$cityId] = $next;
}
}
if ($changes) {
fwrite(STDOUT, 'CITY_CHANGE_REF ' . Json::encode([
'actor' => $general->getID(),
'changes' => $changes,
]) . "\n");
}
}
$traceCityIds = array_values(array_filter(array_map(
'intval',
explode(',', (string)getenv('REF_AI_TRACE_CITY_IDS'))
)));
if (
PHP_SAPI === 'cli'
&& getenv('REF_DETERMINISTIC_INSTALL_ENABLED') === '1'
&& $traceCityIds
&& in_array((string)$general->getID(), explode(',', (string)getenv('REF_AI_TRACE_GENERAL_IDS')), true)
) {
fwrite(STDOUT, 'AI_CITY_STATE_TRACE ' . Json::encode([
'generalId' => $general->getID(),
'cities' => $db->query(
'SELECT city, nation, front, supply, def, wall, state, term FROM city WHERE city IN %li ORDER BY city',
$traceCityIds
),
]) . "\n");
}
if (
PHP_SAPI === 'cli'
&& getenv('REF_DETERMINISTIC_INSTALL_ENABLED') === '1'
&& getenv('REF_AI_TRACE_SEQUENCE') === '1'
) {
fwrite(STDOUT, sprintf(
"ACTION_REF kind=general actor=%d requested=%s resolved=%s\n",
$general->getID(),
$generalCommandObj->getRawClassName(),
$generalCommandObj->getRawClassName()
));
}
$traceGeneralIds = getenv('REF_AI_TRACE_GENERAL_IDS');
if (
PHP_SAPI === 'cli'
&& getenv('REF_DETERMINISTIC_INSTALL_ENABLED') === '1'
&& is_string($traceGeneralIds)
&& in_array((string) $general->getID(), explode(',', $traceGeneralIds), true)
) {
fwrite(STDOUT, 'AI_GENERAL_POST_TRACE ' . Json::encode([
'generalId' => $general->getID(),
'action' => $generalCommandObj->getRawClassName(),
'gold' => $general->getVar('gold'),
'rice' => $general->getVar('rice'),
]) . "\n");
}
}
pullNationCommand($general->getVar('nation'), $general->getVar('officer_level'));
pullGeneralCommand($general->getID());
@@ -360,8 +561,31 @@ class TurnExecutionHelper
$general->setAuxVar('autorun_limit', $autorun_limit);
}
$turnObj->updateTurnTime();
$turnObj->applyDB();
$turnObj->updateTurnTime();
$traceGeneralIds = getenv('REF_AI_TRACE_GENERAL_IDS');
$traceCurrentGeneral =
PHP_SAPI === 'cli'
&& getenv('REF_DETERMINISTIC_INSTALL_ENABLED') === '1'
&& is_string($traceGeneralIds)
&& in_array((string) $general->getID(), explode(',', $traceGeneralIds), true);
if ($traceCurrentGeneral) {
fwrite(STDOUT, 'AI_GENERAL_PRE_APPLY_TRACE ' . Json::encode([
'generalId' => $general->getID(),
'gold' => $general->getVar('gold'),
'rice' => $general->getVar('rice'),
'updates' => $general->getUpdatedValues(),
]) . "\n");
}
$turnObj->applyDB();
if ($traceCurrentGeneral) {
fwrite(STDOUT, 'AI_GENERAL_DB_TRACE ' . Json::encode([
'generalId' => $general->getID(),
'stored' => $db->queryFirstRow(
'SELECT nation,city,gold,rice,crew,crewtype,train,atmos,leadership,strength,intel,leadership_exp,strength_exp,intel_exp,dex1,dex2,dex3,dex4,dex5 FROM general WHERE no=%i',
$general->getID()
),
]) . "\n");
}
}
return [false, $currentTurn];
@@ -458,27 +682,54 @@ class TurnExecutionHelper
return $gameStor->turntime;
}
$monthlyRng = new RandUtil(new LiteHashDRBG(Util::simpleSerialize(
$monthlyRng = new RandUtil(new LiteHashDRBG(Util::simpleSerialize(
UniqueConst::$hiddenSeed,
'monthly',
$gameStor->year,
$gameStor->month
)));
// 1달마다 처리하는 것들, 벌점 감소 및 건국,전턴,합병 -1, 군량 소모
static::runEventHandler($db, $gameStor, EventTarget::PreMonth);
if (!preUpdateMonthly()) {
$gameStor->month
)));
$traceMonthlyGeneral = static function (string $phase) use ($db): void {
$traceGeneralIds = getenv('REF_AI_TRACE_GENERAL_IDS');
if (
PHP_SAPI !== 'cli'
|| getenv('REF_DETERMINISTIC_INSTALL_ENABLED') !== '1'
|| !is_string($traceGeneralIds)
) {
return;
}
foreach (array_filter(explode(',', $traceGeneralIds), 'strlen') as $generalID) {
$stored = $db->queryFirstRow('SELECT gold,rice FROM general WHERE no=%i', (int)$generalID);
if ($stored) {
fwrite(STDOUT, 'AI_MONTH_PHASE_TRACE ' . Json::encode([
'phase' => $phase,
'generalId' => (int)$generalID,
'stored' => $stored,
]) . "\n");
}
}
};
$traceMonthlyGeneral('before-pre-month-event');
// 1달마다 처리하는 것들, 벌점 감소 및 건국,전턴,합병 -1, 군량 소모
static::runEventHandler($db, $gameStor, EventTarget::PreMonth);
$traceMonthlyGeneral('after-pre-month-event');
if (!preUpdateMonthly()) {
unlock();
throw new \RuntimeException('preUpdateMonthly() 처리 에러');
}
turnDate($nextTurn);
throw new \RuntimeException('preUpdateMonthly() 처리 에러');
}
$traceMonthlyGeneral('after-pre-update');
turnDate($nextTurn);
$traceMonthlyGeneral('after-turn-date');
// 분기계산. 장수들 턴보다 먼저 있다면 먼저처리
if ($gameStor->month == 1) {
checkStatistic();
}
static::runEventHandler($db, $gameStor, EventTarget::Month);
postUpdateMonthly($monthlyRng);
static::runEventHandler($db, $gameStor, EventTarget::Month);
$traceMonthlyGeneral('after-month-event');
postUpdateMonthly($monthlyRng);
$traceMonthlyGeneral('after-post-update');
// 다음달로 넘김
$prevTurn = $nextTurn;