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
+4 -2
View File
@@ -64,7 +64,9 @@ class che_견문 extends Command\GeneralCommand{
$sightseeing = new SightseeingMessage();
[$type, $text] = $sightseeing->pickAction();
// The command RNG is derived from the global seed, year/month,
// general id, and action. Avoid PHP's process-global mt_rand here.
[$type, $text] = $sightseeing->pickAction($rng);
$exp = 0;
@@ -122,4 +124,4 @@ class che_견문 extends Command\GeneralCommand{
}
}
}
+31 -8
View File
@@ -2,7 +2,7 @@
namespace sammo\Command\General;
use \sammo\{
DB, Util, JosaUtil,
DB, Util, JosaUtil, Json,
General,
ActionLogger,
LastTurn,
@@ -118,12 +118,35 @@ class che_기술연구 extends che_상업투자{
$score /= 4;
}
$genCount = Util::valueFit(
$db->queryFirstField('SELECT gennum FROM nation WHERE nation=%i', $general->getVar('nation')),
GameConst::$initialNationGenLimit
);
$nationUpdated = [
$genCount = Util::valueFit(
$db->queryFirstField('SELECT gennum FROM nation WHERE nation=%i', $general->getVar('nation')),
GameConst::$initialNationGenLimit
);
$traceGeneralIds = getenv('REF_AI_TRACE_GENERAL_IDS');
$traceNationIds = getenv('REF_AI_TRACE_NATION_IDS');
if (
PHP_SAPI === 'cli'
&& getenv('REF_DETERMINISTIC_INSTALL_ENABLED') === '1'
&& (
(is_string($traceGeneralIds)
&& in_array((string) $general->getID(), explode(',', $traceGeneralIds), true))
|| (is_string($traceNationIds)
&& in_array((string) $general->getVar('nation'), explode(',', $traceNationIds), true))
)
) {
fwrite(STDOUT, 'AI_ACTION_PATCH_TRACE ' . Json::encode([
'engine' => 'ref-tech',
'generalId' => $general->getID(),
'nationId' => $general->getVar('nation'),
'currentTech' => $this->nation['tech'],
'techScore' => $score,
'generalCount' => $genCount,
'delta' => $score / $genCount,
]) . "\n");
}
$nationUpdated = [
'tech' => $this->nation['tech'] + $score/$genCount
];
$db->update('nation', $nationUpdated, 'nation=%i', $general->getVar('nation'));
@@ -143,4 +166,4 @@ class che_기술연구 extends che_상업투자{
}
}
}
@@ -187,7 +187,8 @@ class che_랜덤임관 extends Command\GeneralCommand
LEFT JOIN `rank_data` AS rb ON g.`no` = rb.general_id AND rb.`type` = 'deathcrew_person'
LEFT JOIN `nation` AS n ON g.`nation` = n.`nation`
WHERE g.`npc` IN (0, 1, 2, 3, 6) AND g.nation != 0 AND n.scout=0 AND n.gennum < %i AND n.nation NOT IN %li
GROUP BY g.`nation`",
GROUP BY g.`nation`
ORDER BY g.`nation`",
$genLimit,
$notIn
);
@@ -199,7 +200,8 @@ class che_랜덤임관 extends Command\GeneralCommand
LEFT JOIN `rank_data` AS rb ON g.`no` = rb.general_id AND rb.`type` = 'deathcrew_person'
LEFT JOIN `nation` AS n ON g.`nation` = n.`nation`
WHERE g.`npc` IN (0, 1, 2, 3, 6) AND g.nation != 0 AND n.scout=0 AND n.gennum < %i
GROUP BY g.`nation`",
GROUP BY g.`nation`
ORDER BY g.`nation`",
$genLimit
);
}
+15
View File
@@ -94,6 +94,21 @@ class ProcessIncome extends \sammo\Event\Action
foreach ($generalRawList as $rawGeneral) {
$generalObj = new General($rawGeneral, null, null, null, null, $year, $month, false);
$gold = Util::round(getBill($generalObj->getVar('dedication')) * $ratio);
if (
getenv('REF_DETERMINISTIC_INSTALL_ENABLED') === '1'
&& getenv('SEED_PARITY_MONTHLY_RESOURCE_TRACE') === '1'
&& in_array((string)$generalObj->getID(), explode(',', (string)getenv('REF_AI_TRACE_GENERAL_IDS')), true)
) {
fwrite(STDOUT, 'MONTHLY_RESOURCE_REF ' . json_encode([
'action' => 'ProcessIncome',
'type' => 'gold',
'generalId' => $generalObj->getID(),
'current' => $generalObj->getVar('gold'),
'pay' => $gold,
'ratio' => $ratio,
'originOutcome' => $originoutcome,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n");
}
$generalObj->increaseVar('gold', $gold);
$logger = $generalObj->getLogger();
@@ -71,6 +71,21 @@ class ProcessSemiAnnual extends \sammo\Event\Action
$resource = $this->resource;
$traceGeneralIDs = array_filter(explode(',', (string)getenv('REF_AI_TRACE_GENERAL_IDS')), 'strlen');
if (getenv('REF_DETERMINISTIC_INSTALL_ENABLED') === '1' && getenv('SEED_PARITY_MONTHLY_RESOURCE_TRACE') === '1') {
foreach ($traceGeneralIDs as $generalID) {
$current = $db->queryFirstField('SELECT %b FROM general WHERE no = %i', $resource, (int)$generalID);
if ($current !== null) {
fwrite(STDOUT, 'MONTHLY_RESOURCE_REF ' . json_encode([
'action' => 'ProcessSemiAnnual',
'resource' => $resource,
'generalId' => (int)$generalID,
'current' => (int)$current,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n");
}
}
}
// 내정 1% 감소
$db->update('city', [
'dead' => 0,
@@ -90,6 +105,20 @@ class ProcessSemiAnnual extends \sammo\Event\Action
$resource => $db->sqleval('IF(%b > 10000, %b * 0.97, %b * 0.99)', $resource, $resource, $resource)
], '%b > 1000', $resource);
if (getenv('REF_DETERMINISTIC_INSTALL_ENABLED') === '1' && getenv('SEED_PARITY_MONTHLY_RESOURCE_TRACE') === '1') {
foreach ($traceGeneralIDs as $generalID) {
$next = $db->queryFirstField('SELECT %b FROM general WHERE no = %i', $resource, (int)$generalID);
if ($next !== null) {
fwrite(STDOUT, 'MONTHLY_RESOURCE_REF ' . json_encode([
'action' => 'ProcessSemiAnnualAfter',
'resource' => $resource,
'generalId' => (int)$generalID,
'next' => (int)$next,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n");
}
}
}
// > 100000 유지비 5%, > 100000 유지비 3%, > 1000 유지비 1%
$db->update('nation', [
$resource => $db->sqleval('IF(%b > 100000, %b * 0.95, IF(%b > 10000, %b * 0.97, %b * 0.99))', $resource, $resource, $resource, $resource, $resource)
+27 -1
View File
@@ -114,7 +114,7 @@ class RaiseDisaster extends \sammo\Event\Action
if (!$isGood) {
//FIXME: factory 형태로 바꿔야함
$generalListByCity = Util::arrayGroupBy($db->query(
'SELECT %l FROM general WHERE city IN %li',
'SELECT %l FROM general WHERE city IN %li ORDER BY city, no',
Util::formatListOfBackticks(General::mergeQueryColumn()[0]),
Util::squeezeFromArray($targetCityList, 'city')),
'city');
@@ -133,6 +133,19 @@ class RaiseDisaster extends \sammo\Event\Action
'def' => $db->sqleval('def * %d', $affectRatio),
'wall' => $db->sqleval('wall * %d', $affectRatio),
], 'city = %i', $city['city']);
if (in_array((string)$city['city'], array_filter(explode(',', (string)getenv('REF_AI_TRACE_CITY_IDS')), 'strlen'), true)) {
$storedTrust = $db->queryFirstField('SELECT trust FROM city WHERE city = %i', $city['city']);
fwrite(STDOUT, 'MONTHLY_FLOAT_TRACE ' . json_encode([
'engine' => 'ref',
'cityId' => $city['city'],
'year' => $year,
'month' => $month,
'isGood' => false,
'inputTrust' => $city['trust'],
'affectRatio' => $affectRatio,
'storedTrust' => $storedTrust,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n");
}
$generalList = array_map(
function ($rawGeneral) use ($city, $year, $month) {
@@ -158,6 +171,19 @@ class RaiseDisaster extends \sammo\Event\Action
'def' => $db->sqleval('least(def * %d, def_max)', $affectRatio),
'wall' => $db->sqleval('least(wall * %d, wall_max)', $affectRatio),
], 'city = %i', $city['city']);
if (in_array((string)$city['city'], array_filter(explode(',', (string)getenv('REF_AI_TRACE_CITY_IDS')), 'strlen'), true)) {
$storedTrust = $db->queryFirstField('SELECT trust FROM city WHERE city = %i', $city['city']);
fwrite(STDOUT, 'MONTHLY_FLOAT_TRACE ' . json_encode([
'engine' => 'ref',
'cityId' => $city['city'],
'year' => $year,
'month' => $month,
'isGood' => true,
'inputTrust' => $city['trust'],
'affectRatio' => $affectRatio,
'storedTrust' => $storedTrust,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n");
}
}
}
}
+410 -129
View File
@@ -9,8 +9,30 @@ use sammo\Enums\PenaltyKey;
use sammo\Enums\RankColumn;
use sammo\Scenario\NPC;
class GeneralAI
{
class GeneralAI
{
private function traceNationRng(string $phase): void
{
$traceNationIds = getenv('REF_AI_TRACE_NATION_IDS');
if (
PHP_SAPI !== 'cli'
|| getenv('REF_DETERMINISTIC_INSTALL_ENABLED') !== '1'
|| !is_string($traceNationIds)
|| !in_array((string) $this->general->getID(), explode(',', $traceNationIds), true)
) {
return;
}
$rng = $this->rng->rng;
$reflection = new \ReflectionObject($rng);
$bufferIdx = $reflection->getProperty('bufferIdx')->getValue($rng);
fwrite(STDOUT, 'AI_NATION_RNG_TRACE ' . Json::encode([
'generalId' => $this->general->getID(),
'phase' => $phase,
'bufferIdx' => $bufferIdx,
]) . "\n");
}
protected RandUtil $rng;
protected array $city;
@@ -150,13 +172,25 @@ class GeneralAI
$gameStor = KVStorage::getStorage($db, 'game_env');
$this->env = $gameStor->getAll(true);
$this->rng = new RandUtil(new LiteHashDRBG(Util::simpleSerialize(
UniqueConst::$hiddenSeed,
'GeneralAI',
$this->env['year'],
$this->env['month'],
$general->getID(),
)));
$serializedSeed = Util::simpleSerialize(
UniqueConst::$hiddenSeed,
'GeneralAI',
$this->env['year'],
$this->env['month'],
$general->getID(),
);
if (in_array((string)$general->getID(), array_filter(explode(',', (string)getenv('REF_AI_TRACE_GENERAL_IDS')), 'strlen'), true)) {
fwrite(STDOUT, 'AI_GENERAL_SEED_TRACE ' . Json::encode([
'generalId' => $general->getID(),
'year' => $this->env['year'],
'month' => $this->env['month'],
'seedHex' => bin2hex($serializedSeed),
]) . "\n");
}
$this->rng = new RandUtil(new LiteHashDRBG($serializedSeed));
if (in_array((string)$general->getID(), array_filter(explode(',', (string)getenv('REF_AI_TRACE_GENERAL_IDS')), 'strlen'), true)) {
$this->rng->setTraceGeneralId($general->getID());
}
$this->leadership = $general->getLeadership();
$this->strength = $general->getStrength();
@@ -172,17 +206,21 @@ class GeneralAI
return $this->general;
}
protected function calcGenType(General $general)
{
$leadership = $general->getLeadership(false);
$strength = Util::valueFit($general->getStrength(false), 1);
$intel = Util::valueFit($general->getIntel(false), 1);
protected function calcGenType(General $general)
{
$leadership = $general->getLeadership(false);
$strength = Util::valueFit($general->getStrength(false), 1);
$intel = Util::valueFit($general->getIntel(false), 1);
$mixedDraw = null;
$mixedProbability = null;
//무장
if ($strength >= $intel) {
$genType = self::t무장;
if ($intel >= $strength * 0.8) { //무지장
if ($this->rng->nextBool($intel / $strength / 2)) {
$mixedProbability = $intel / $strength / 2;
$mixedDraw = $this->rng->nextBool($mixedProbability);
if ($mixedDraw) {
$genType |= self::t지장;
}
}
@@ -190,17 +228,32 @@ class GeneralAI
} else {
$genType = self::t지장;
if ($strength >= $intel * 0.8) { //지무장
if ($this->rng->nextBool($strength / $intel / 2)) {
$mixedProbability = $strength / $intel / 2;
$mixedDraw = $this->rng->nextBool($mixedProbability);
if ($mixedDraw) {
$genType |= self::t무장;
}
}
}
//통솔
if ($leadership >= $this->nationPolicy->minNPCWarLeadership) {
$genType |= self::t통솔장;
}
return $genType;
if ($leadership >= $this->nationPolicy->minNPCWarLeadership) {
$genType |= self::t통솔장;
}
if (in_array((string)$general->getID(), array_filter(explode(',', (string)getenv('REF_AI_TRACE_GENERAL_IDS')), 'strlen'), true)) {
fwrite(STDOUT, 'AI_TRACE ' . Json::encode([
'generalId' => $general->getID(),
'stage' => 'calc-gen-type',
'leadership' => $leadership,
'strength' => $strength,
'intel' => $intel,
'mixedProbability' => $mixedProbability,
'mixedDraw' => $mixedDraw,
'minLeadership' => $this->nationPolicy->minNPCWarLeadership,
'genType' => $genType,
]) . "\n");
}
return $genType;
}
protected function calcDiplomacyState()
@@ -857,12 +910,20 @@ class GeneralAI
return null;
}
$cityCandidates = [];
foreach ($this->frontCities as $frontCity) {
$cityCandidates[$frontCity['city']] = $frontCity['important'];
}
$cmd = buildNationCommandClass('che_발령', $this->general, $this->env, $lastTurn, [
$cityCandidates = [];
foreach ($this->frontCities as $frontCity) {
$cityCandidates[$frontCity['city']] = $frontCity['important'];
}
if (in_array((string)$me->getID(), array_filter(explode(',', (string)getenv('REF_AI_TRACE_GENERAL_IDS')), 'strlen'), true)) {
fwrite(STDOUT, 'AI_NPC_FRONT_ASSIGN_TRACE ' . Json::encode([
'generalId' => $me->getID(),
'frontCityIds' => array_keys($cityCandidates),
'candidateGeneralIds' => array_keys($generalCandidates),
]) . "\n");
}
$cmd = buildNationCommandClass('che_발령', $this->general, $this->env, $lastTurn, [
'destGeneralID' => $this->rng->choice($generalCandidates)->getID(),
'destCityID' => $this->rng->choiceUsingWeight($cityCandidates)
]);
@@ -1135,12 +1196,20 @@ class GeneralAI
return null;
}
$cityCandidates = [];
foreach ($this->frontCities as $frontCity) {
$cityCandidates[$frontCity['city']] = $frontCity['important'];
}
$cmd = buildNationCommandClass('che_발령', $this->general, $this->env, $lastTurn, [
$cityCandidates = [];
foreach ($this->frontCities as $frontCity) {
$cityCandidates[$frontCity['city']] = $frontCity['important'];
}
if (in_array((string)$me->getID(), array_filter(explode(',', (string)getenv('REF_AI_TRACE_GENERAL_IDS')), 'strlen'), true)) {
fwrite(STDOUT, 'AI_NPC_FRONT_ASSIGN_TRACE ' . Json::encode([
'generalId' => $me->getID(),
'frontCityIds' => array_keys($cityCandidates),
'candidateGeneralIds' => array_keys($generalCandidates),
]) . "\n");
}
$cmd = buildNationCommandClass('che_발령', $this->general, $this->env, $lastTurn, [
'destGeneralID' => $this->rng->choice($generalCandidates)->getID(),
'destCityID' => $this->rng->choiceUsingWeight($cityCandidates)
]);
@@ -1564,18 +1633,33 @@ class GeneralAI
continue;
}
//국고와 '충분한 금액'의 기하평균
$payAmount = sqrt(($enoughMoney - $targetNPCGeneral->getVar($resName)) * $resVal);
$payAmount = Util::valueFit($payAmount, $resVal - $reqNationRes, $enoughMoney - $targetNPCGeneral->getVar($resName));
$payAmount = sqrt(($enoughMoney - $targetNPCGeneral->getVar($resName)) * $resVal);
$payAmount = Util::valueFit($payAmount, $resVal - $reqNationRes, $enoughMoney - $targetNPCGeneral->getVar($resName));
if ($resVal < $payAmount / 2) {
continue;
}
$candidateArgs[] = [
$payAmount = Util::valueFit($payAmount, 100, $this->maxResourceActionAmount);
if (in_array((string)$this->general->getID(), explode(',', (string)getenv('REF_AI_TRACE_GENERAL_IDS')), true)) {
fwrite(STDOUT, 'AI_REWARD_TRACE ' . Json::encode([
'engine' => 'ref',
'actor' => $this->general->getID(),
'target' => $targetNPCGeneral->getID(),
'resource' => $resName,
'nationResource' => $resVal,
'targetResource' => $targetNPCGeneral->getVar($resName),
'required' => $reqMoney,
'enough' => $enoughMoney,
'maxResourceActionAmount' => $this->maxResourceActionAmount,
'amount' => $payAmount,
]) . "\n");
}
$candidateArgs[] = [
[
'destGeneralID' => $targetNPCGeneral->getID(),
'isGold' => $resName == 'gold',
'amount' => Util::valueFit($payAmount, 100, $this->maxResourceActionAmount)
'amount' => $payAmount
],
max(count($npcWarGenerals), count($npcCivilGenerals)) - $idx
];
@@ -2214,7 +2298,28 @@ class GeneralAI
return null;
}
return $this->rng->choiceUsingWeightPair($cmdList);
$picked = $this->rng->choiceUsingWeightPair($cmdList);
$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_DEVEL_TRACE ' . Json::encode([
'generalId' => $general->getID(),
'year' => $this->env['year'],
'month' => $this->env['month'],
'genType' => $genType,
'city' => $city,
'candidates' => array_map(static fn (array $entry): array => [
'action' => $entry[0]->getRawClassName(),
'weight' => $entry[1],
], $cmdList),
'picked' => $picked->getRawClassName(),
]) . "\n");
}
return $picked;
}
protected function do긴급내정(): ?GeneralCommand
@@ -2359,18 +2464,54 @@ class GeneralAI
return null;
}
$cmd = $this->rng->choiceUsingWeightPair($cmdList);
return $cmd;
$cmd = $this->rng->choiceUsingWeightPair($cmdList);
$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_DEVEL_TRACE ' . Json::encode([
'generalId' => $general->getID(),
'year' => $this->env['year'],
'month' => $this->env['month'],
'mode' => '전쟁내정',
'genType' => $genType,
'dipState' => $this->dipState,
'city' => $city,
'nationTech' => $nation['tech'],
'candidates' => array_map(static fn (array $entry): array => [
'action' => $entry[0]->getRawClassName(),
'weight' => $entry[1],
], $cmdList),
'picked' => $cmd->getRawClassName(),
]) . "\n");
}
return $cmd;
}
protected function do금쌀구매(): ?GeneralCommand
{
$general = $this->general;
if ($this->city['trade'] === null && !$this->generalPolicy->can상인무시) {
return null;
}
protected function do금쌀구매(): ?GeneralCommand
{
$general = $this->general;
$traceGeneralIDs = array_filter(explode(',', (string)getenv('REF_AI_TRACE_GENERAL_IDS')), 'strlen');
$traceEnabled = in_array((string)$general->getID(), $traceGeneralIDs, true);
$trace = static function (string $stage, array $values = []) use ($traceEnabled, $general): void {
if (!$traceEnabled) {
return;
}
fwrite(STDOUT, 'AI_ECONOMY_TRACE ' . json_encode([
'generalId' => $general->getID(),
'stage' => $stage,
...$values,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n");
};
if ($this->city['trade'] === null && !$this->generalPolicy->can상인무시) {
$trace('no-trader');
return null;
}
$kill = $general->getRankVar(RankColumn::killcrew) + 50000;
$death = $general->getRankVar(RankColumn::deathcrew) + 50000;
@@ -2379,12 +2520,24 @@ class GeneralAI
$absGold = $general->getVar('gold');
$absRice = $general->getVar('rice');
$relGold = $absGold;
$relRice = $absRice * $deathRate;
if ($absGold + $absRice < $this->baseDevelCost * 2) {
return null;
}
$relGold = $absGold;
$relRice = $absRice * $deathRate;
$trace('resources', [
'absGold' => $absGold,
'absRice' => $absRice,
'relGold' => $relGold,
'relRice' => $relRice,
'deathRate' => $deathRate,
'baseDevelCost' => $this->baseDevelCost,
'canIgnoreTrader' => $this->generalPolicy->can상인무시,
'trade' => $this->city['trade'],
]);
if ($absGold + $absRice < $this->baseDevelCost * 2) {
$trace('insufficient-base-resource');
return null;
}
$crewType = $general->getCrewTypeObj();
if ($this->generalPolicy->can모병) {
@@ -2400,10 +2553,19 @@ class GeneralAI
}
$goldCost = $costCmd->getCost()[0];
$riceCost = $crewType->riceWithTech(
$this->nation['tech'],
Util::toInt($this->fullLeadership * 100)
);
$riceCost = $crewType->riceWithTech(
$this->nation['tech'],
Util::toInt($this->fullLeadership * 100)
);
$trace('recruit-cost', [
'crewTypeId' => $crewType->id,
'crewCost' => $crewType->cost,
'crewRice' => $crewType->rice,
'tech' => $this->nation['tech'],
'crewAmount' => Util::toInt($this->fullLeadership * 100),
'goldCost' => $goldCost,
'riceCost' => $riceCost,
]);
if (($relGold + $relRice) * 1.5 <= $goldCost + $riceCost) {
return null;
@@ -2437,10 +2599,16 @@ class GeneralAI
'buyRice' => true,
'amount' => $amount
]
);
if ($cmd->hasFullConditionMet()) {
return $cmd;
}
);
$conditionMet = $cmd->hasFullConditionMet();
$trace('buy', [
'amount' => $amount,
'minimumResourceActionAmount' => $this->nationPolicy->minimumResourceActionAmount,
'conditionMet' => $conditionMet,
]);
if ($conditionMet) {
return $cmd;
}
}
}
@@ -2480,14 +2648,28 @@ class GeneralAI
return null;
}
protected function do징병(): ?GeneralCommand
{
if (in_array($this->dipState, [self::d평화, self::d선포])) {
return null;
}
if (!($this->genType & self::t통솔장)) {
return null;
protected function do징병(): ?GeneralCommand
{
$traceGeneralIds = array_filter(explode(',', (string)getenv('REF_AI_TRACE_GENERAL_IDS')), 'strlen');
$traceEnabled = in_array((string)$this->general->getID(), $traceGeneralIds, true);
$trace = function (string $stage, array $values = []) use ($traceEnabled): void {
if (!$traceEnabled) {
return;
}
fwrite(STDOUT, 'AI_RECRUIT_TRACE ' . Json::encode([
'generalId' => $this->general->getID(),
'stage' => $stage,
...$values,
]) . "\n");
};
if (in_array($this->dipState, [self::d평화, self::d선포])) {
$trace('diplomacy', ['dipState' => $this->dipState]);
return null;
}
if (!($this->genType & self::t통솔장)) {
$trace('general-type', ['genType' => $this->genType]);
return null;
}
@@ -2497,21 +2679,32 @@ class GeneralAI
$nation = $this->nation;
$env = $this->env;
if ($general->getVar('crew') >= $this->nationPolicy->minWarCrew) {
return null;
}
if (!$this->generalPolicy->can한계징병) {
if ($general->getVar('crew') >= $this->nationPolicy->minWarCrew) {
$trace('existing-crew', ['crew' => $general->getVar('crew'), 'minWarCrew' => $this->nationPolicy->minWarCrew]);
return null;
}
$trace('population-policy', [
'population' => $city['pop'],
'populationMax' => $city['pop_max'],
'safeRatio' => $this->nationPolicy->safeRecruitCityPopulationRatio,
'minPopulation' => $this->nationPolicy->minNPCRecruitCityPopulation,
'canLimitRecruit' => $this->generalPolicy->can한계징병,
]);
if (!$this->generalPolicy->can한계징병) {
$remainPop = $city['pop'] - $this->nationPolicy->minNPCRecruitCityPopulation - $this->fullLeadership * 100;
if ($remainPop <= 0) {
return null;
if ($remainPop <= 0) {
$trace('population-floor', ['remainPop' => $remainPop, 'fullLeadership' => $this->fullLeadership]);
return null;
}
$maxPop = $city['pop_max'] - $this->nationPolicy->minNPCRecruitCityPopulation;
if (($city['pop'] / $city['pop_max'] < $this->nationPolicy->safeRecruitCityPopulationRatio) &&
($this->rng->nextBool($remainPop / $maxPop))
) {
return null;
($this->rng->nextBool($remainPop / $maxPop))
) {
$trace('population-random', ['remainPop' => $remainPop, 'maxPop' => $maxPop, 'fullLeadership' => $this->fullLeadership]);
return null;
}
}
@@ -2532,7 +2725,7 @@ class GeneralAI
}
}
if (!$armType) {
if (!$armType) {
$dex = [
GameUnitConst::T_FOOTMAN => sqrt($general->getVar('dex1') + 500),
GameUnitConst::T_ARCHER => sqrt($general->getVar('dex2') + 500),
@@ -2551,8 +2744,26 @@ class GeneralAI
$availableArmType[GameUnitConst::T_WIZARD] = $dex[GameUnitConst::T_WIZARD] * $this->fullIntel * 3;
}
$armType = $this->rng->choiceUsingWeight($availableArmType);
}
if ($traceEnabled) {
$armTypeDraw = $this->rng->nextFloat1();
$cursor = $armTypeDraw * array_sum($availableArmType);
foreach ($availableArmType as $candidateArmType => $weight) {
if ($cursor <= $weight) {
$armType = $candidateArmType;
break;
}
$cursor -= max(0, $weight);
}
} else {
$armType = $this->rng->choiceUsingWeight($availableArmType);
}
}
$trace('arm-type', [
'forcedArmType' => $general->getAuxVar('armType'),
'armType' => $armType,
'armTypeDraw' => $armTypeDraw ?? null,
'armTypeWeights' => $availableArmType ?? [],
]);
$cities = [];
@@ -2576,9 +2787,10 @@ class GeneralAI
}
}
if ($types) {
$type = $this->rng->choiceUsingWeight($types);
} else {
if ($types) {
$type = $this->rng->choiceUsingWeight($types);
$trace('crew-type', ['armType' => $armType, 'candidates' => $types, 'picked' => $type]);
} else {
throw new MustNotBeReachedException('에러:' . print_r([$general->getName(), $general->getAuxVar('armType'), $armType, $cities, $regions, $relYear, $tech], true));
}
@@ -2604,8 +2816,9 @@ class GeneralAI
$rice = $general->getVar('rice');
$rice -= $this->fullLeadership * 4;
if ($gold <= 0 || $rice <= 0) {
return null;
if ($gold <= 0 || $rice <= 0) {
$trace('reserve-floor', ['remainingGold' => $gold, 'remainingRice' => $rice, 'fullLeadership' => $this->fullLeadership]);
return null;
}
$crew = $this->fullLeadership * 100;
@@ -2639,15 +2852,18 @@ class GeneralAI
]);
}
if (!$this->generalPolicy->can한계징병 && $rice * 1.1 <= $riceCost) {
//이 쌀도 없어?
return null;
}
if (!$cmd->hasFullConditionMet()) {
return null;
}
return $cmd;
if (!$this->generalPolicy->can한계징병 && $rice * 1.1 <= $riceCost) {
//이 쌀도 없어?
$trace('rice-cost', ['remainingGold' => $gold, 'remainingRice' => $rice, 'goldCost' => $cost, 'riceCost' => $riceCost, 'crewAmount' => $crew, 'crewTypeId' => $type]);
return null;
}
if (!$cmd->hasFullConditionMet()) {
$trace('constraint', ['remainingGold' => $gold, 'remainingRice' => $rice, 'goldCost' => $cost, 'riceCost' => $riceCost, 'crewAmount' => $crew, 'crewTypeId' => $type]);
return null;
}
$trace('selected', ['remainingGold' => $gold, 'remainingRice' => $rice, 'goldCost' => $cost, 'riceCost' => $riceCost, 'crewAmount' => $crew, 'crewTypeId' => $type]);
return $cmd;
}
protected function do전투준비(): ?GeneralCommand
@@ -2766,10 +2982,18 @@ class GeneralAI
throw new \RuntimeException('출병 불가' . $cityID . var_export($attackableNations, true) . var_export($nearCities, true));
}
$cmd = buildGeneralCommandClass('che_출병', $general, $this->env, ['destCityID' => $this->rng->choice($attackableCities)]);
if (!$cmd->hasFullConditionMet()) {
return null;
}
$cmd = buildGeneralCommandClass('che_출병', $general, $this->env, ['destCityID' => $this->rng->choice($attackableCities)]);
if (!$cmd->hasFullConditionMet()) {
if (in_array((string)$general->getID(), array_filter(explode(',', (string)getenv('REF_AI_TRACE_GENERAL_IDS')), 'strlen'), true)) {
error_log('AI_GENERAL_CONSTRAINT_TRACE ' . json_encode([
'generalId' => $general->getID(),
'action' => 'che_출병',
'args' => $cmd->getArg(),
'reason' => $cmd->testFullConditionMet(),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
}
return null;
}
return $cmd;
}
@@ -2949,13 +3173,23 @@ class GeneralAI
}
}
if (!$recruitableCityList) {
LogText("{$this->general->getName()}, {$this->general->getID()} 후방워프 불가: 배후 도시", [count($this->backupCities), count($this->supplyCities)]);
return null;
}
$cmd = buildGeneralCommandClass('che_NPC능동', $this->general, $this->env, [
if (!$recruitableCityList) {
LogText("{$this->general->getName()}, {$this->general->getID()} 후방워프 불가: 배후 도시", [count($this->backupCities), count($this->supplyCities)]);
return null;
}
if (in_array((string) $this->general->getID(), array_filter(explode(',', (string) getenv('REF_AI_TRACE_GENERAL_IDS')), 'strlen'), true)) {
fwrite(STDOUT, 'AI_WARP_TRACE ' . Json::encode([
'generalId' => $this->general->getID(),
'kind' => 'rear',
'fullLeadership' => $this->fullLeadership,
'minRecruitPop' => $minRecruitPop,
'recruitable' => $recruitableCityList,
]) . "\n");
}
$cmd = buildGeneralCommandClass('che_NPC능동', $this->general, $this->env, [
'optionText' => '순간이동',
'destCityID' => $this->rng->choiceUsingWeight($recruitableCityList),
]);
@@ -3483,7 +3717,13 @@ class GeneralAI
$supplyCities = [];
$backupCities = [];
foreach ($db->query('SELECT * FROM city WHERE nation = %i', $nationID) as $nationCity) {
$cityQuery = 'SELECT * FROM city WHERE nation = %i';
if (getenv('REF_DETERMINISTIC_INSTALL_ENABLED') === '1') {
// ng_compare must not let MariaDB's physical row order redefine
// which candidate a deterministic RNG index points at.
$cityQuery .= ' ORDER BY city';
}
foreach ($db->query($cityQuery, $nationID) as $nationCity) {
$nationCity['generals'] = new \ArrayObject();
$cityID = $nationCity['city'];
$dev =
@@ -3534,7 +3774,11 @@ class GeneralAI
$nationCities = &$this->nationCities;
$db = DB::db();
$generalIDList = $db->queryFirstColumn('SELECT no FROM general WHERE nation = %i AND no != %i', $nationID, $this->general->getID());
$generalQuery = 'SELECT no FROM general WHERE nation = %i AND no != %i';
if (getenv('REF_DETERMINISTIC_INSTALL_ENABLED') === '1') {
$generalQuery .= ' ORDER BY no';
}
$generalIDList = $db->queryFirstColumn($generalQuery, $nationID, $this->general->getID());
$nationGenerals = General::createObjListFromDB($generalIDList);
@@ -3613,9 +3857,10 @@ class GeneralAI
}
public function chooseNationTurn(NationCommand $reservedCommand): NationCommand
{
$this->updateInstance();
public function chooseNationTurn(NationCommand $reservedCommand): NationCommand
{
$this->updateInstance();
$this->traceNationRng('after-update');
//TODO: NationTurn과 InstantNationTurn 구분 필요
$lastTurn = $reservedCommand->getLastTurn();
@@ -3658,7 +3903,7 @@ class GeneralAI
$general->getLogger()->pushGeneralActionLog($text);
}
foreach ($this->nationPolicy->priority as $actionName) {
foreach ($this->nationPolicy->priority as $actionName) {
if (!property_exists($this->nationPolicy, 'can' . $actionName)) {
trigger_error("can{$actionName}이 없음", E_USER_NOTICE);
@@ -3671,7 +3916,9 @@ class GeneralAI
continue;
}
/** @var ?NationCommand */
$result = $this->{'do' . $actionName}($lastTurn);
$this->traceNationRng("before-{$actionName}");
$result = $this->{'do' . $actionName}($lastTurn);
$this->traceNationRng("after-{$actionName}");
if ($result !== null) {
$result->reason = 'do' . $actionName;
return $result;
@@ -3826,17 +4073,33 @@ class GeneralAI
}
}
foreach ($this->generalPolicy->priority as $actionName) {
if (!property_exists($this->generalPolicy, 'can' . $actionName)) {
trigger_error("can{$actionName}이 없음", E_USER_NOTICE);
continue;
}
if (!($this->generalPolicy->{'can' . $actionName})) {
continue;
}
/** @var ?GeneralCommand */
$result = $this->{'do' . $actionName}();
if ($result !== null) {
foreach ($this->generalPolicy->priority as $actionName) {
if (!property_exists($this->generalPolicy, 'can' . $actionName)) {
trigger_error("can{$actionName}이 없음", E_USER_NOTICE);
continue;
}
if (!($this->generalPolicy->{'can' . $actionName})) {
if (in_array((string)$general->getID(), array_filter(explode(',', (string)getenv('REF_AI_TRACE_GENERAL_IDS')), 'strlen'), true)) {
fwrite(STDOUT, 'AI_GENERAL_PRIORITY_TRACE ' . Json::encode([
'generalId' => $general->getID(),
'actionName' => $actionName,
'allowed' => false,
'result' => null,
]) . "\n");
}
continue;
}
/** @var ?GeneralCommand */
$result = $this->{'do' . $actionName}();
if (in_array((string)$general->getID(), array_filter(explode(',', (string)getenv('REF_AI_TRACE_GENERAL_IDS')), 'strlen'), true)) {
fwrite(STDOUT, 'AI_GENERAL_PRIORITY_TRACE ' . Json::encode([
'generalId' => $general->getID(),
'actionName' => $actionName,
'allowed' => true,
'result' => $result === null ? null : get_class($result),
]) . "\n");
}
if ($result !== null) {
$result->reason = 'do' . $actionName;
return $result;
}
@@ -3947,9 +4210,18 @@ class GeneralAI
continue;
}
$randGeneral->setVar('officer_level', $chiefLevel);
$randGeneral->setVar('officer_city', 0);
$randGeneral->applyDB($db);
$randGeneral->setVar('officer_level', $chiefLevel);
$randGeneral->setVar('officer_city', 0);
if (getenv('REF_AI_TRACE_SEQUENCE') === '1') {
fwrite(STDOUT, 'AI_PROMOTION_TRACE ' . Json::encode([
'engine' => 'ref',
'mode' => 'non-lord',
'actor' => $this->general->getID(),
'chiefLevel' => $chiefLevel,
'picked' => $randGeneral->getID(),
]) . "\n");
}
$randGeneral->applyDB($db);
$this->nation['chief_set'] |= doOfficerSet(0, $chiefLevel);
$setChiefLevel |= doOfficerSet(0, $chiefLevel);
$this->chiefGenerals[$chiefLevel] = $randGeneral;
@@ -4145,8 +4417,17 @@ class GeneralAI
}
}
$nextChiefs[$chiefLevel] = $newChief;
$newChief->setVar('officer_level', $chiefLevel);
$nextChiefs[$chiefLevel] = $newChief;
if (getenv('REF_AI_TRACE_SEQUENCE') === '1') {
fwrite(STDOUT, 'AI_PROMOTION_TRACE ' . Json::encode([
'engine' => 'ref',
'mode' => 'lord',
'actor' => $this->general->getID(),
'chiefLevel' => $chiefLevel,
'picked' => $newChief->getID(),
]) . "\n");
}
$newChief->setVar('officer_level', $chiefLevel);
$newChief->setVar('officer_city', 0);
$nation['chief_set'] |= doOfficerSet(0, $chiefLevel);
$updatedChiefSet |= doOfficerSet(0, $chiefLevel);
@@ -31,7 +31,7 @@ class che_도시치료 extends BaseGeneralTrigger
if ($general->getNationID() == 0) {
/** @var array{int,string,string}[] $patients */
$patients = $db->queryAllLists(
'SELECT no,name,nation FROM general WHERE city=%i AND nation=%i AND injury > 10 AND no != %i',
'SELECT no,name,nation FROM general WHERE city=%i AND nation=%i AND injury > 10 AND no != %i ORDER BY no',
$general->getCityID(),
0,
$general->getID()
@@ -40,7 +40,7 @@ class che_도시치료 extends BaseGeneralTrigger
else {
/** @var array{int,string,string}[] $patients */
$patients = $db->queryAllLists(
'SELECT no,name,nation FROM general WHERE city=%i AND injury > 10 AND no != %i',
'SELECT no,name,nation FROM general WHERE city=%i AND injury > 10 AND no != %i ORDER BY no',
$general->getCityID(),
$general->getID()
);
+14 -3
View File
@@ -93,7 +93,14 @@ class ResetHelper{
$gameStor->resetCache();
}
$hiddenSeed = bin2hex(random_bytes(16));//32byte, 128bit random seed
$hiddenSeed = bin2hex(random_bytes(16));//32byte, 128bit random seed
if (getenv('REF_DETERMINISTIC_INSTALL_ENABLED') === '1') {
$comparisonSeed = getenv('REF_HIDDEN_SEED');
if (!is_string($comparisonSeed) || !preg_match('/^[A-Za-z0-9._-]{1,128}$/D', $comparisonSeed)) {
throw new \RuntimeException('REF_HIDDEN_SEED is invalid for deterministic comparison install');
}
$hiddenSeed = $comparisonSeed;
}
$result = Util::generateFileUsingSimpleTemplate(
$servRoot.'/d_setting/UniqueConst.orig.php',
@@ -302,7 +309,11 @@ class ResetHelper{
'server_cnt'=>$serverCnt,
];
foreach(RootDB::db()->query('SELECT `no`, `name`, `picture`, `imgsvr` FROM member WHERE grade >= 6') as $admin){
$comparisonInstall = getenv('REF_DETERMINISTIC_INSTALL_ENABLED') === '1';
$adminMembers = $comparisonInstall
? []
: RootDB::db()->query('SELECT `no`, `name`, `picture`, `imgsvr` FROM member WHERE grade >= 6');
foreach($adminMembers as $admin){
$db->insert('general', [
'owner'=>$admin['no'],
'name'=>$admin['name'],
@@ -368,4 +379,4 @@ class ResetHelper{
'result'=>true
];
}
}
}
@@ -112,10 +112,11 @@ class SightseeingMessage{
}
}
public function pickAction():array{
if (static::$comparisonRng !== null) {
[$type, $texts] = static::$comparisonRng->choiceUsingWeightPair(static::$messages ?? []);
$text = static::$comparisonRng->choice($texts);
public function pickAction(?\sammo\RandUtil $rng = null):array{
$actionRng = $rng ?? static::$comparisonRng;
if ($actionRng !== null) {
[$type, $texts] = $actionRng->choiceUsingWeightPair(static::$messages ?? []);
$text = $actionRng->choice($texts);
} else {
[$type, $texts] = Util::choiceRandomUsingWeightPair(static::$messages??[]);
$text = Util::choiceRandom($texts);
+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;
+28
View File
@@ -371,6 +371,34 @@ class WarUnitGeneral extends WarUnit
function applyDB(\MeekroDB $db): bool
{
$traceGeneralIds = getenv('REF_AI_TRACE_GENERAL_IDS');
if (
PHP_SAPI === 'cli'
&& getenv('REF_DETERMINISTIC_INSTALL_ENABLED') === '1'
&& is_string($traceGeneralIds)
&& in_array((string) $this->getGeneral()->getID(), explode(',', $traceGeneralIds), true)
) {
$general = $this->getGeneral();
fwrite(STDOUT, 'AI_GENERAL_PRE_APPLY_TRACE ' . Json::encode([
'engine' => 'ref-war',
'generalId' => $general->getID(),
'stats' => [
'leadership' => $general->getVar('leadership'),
'strength' => $general->getVar('strength'),
'intelligence' => $general->getVar('intel'),
],
'meta' => [
'leadership_exp' => $general->getVar('leadership_exp'),
'strength_exp' => $general->getVar('strength_exp'),
'intel_exp' => $general->getVar('intel_exp'),
'dex1' => $general->getVar('dex1'),
'dex2' => $general->getVar('dex2'),
'dex3' => $general->getVar('dex3'),
'dex4' => $general->getVar('dex4'),
'dex5' => $general->getVar('dex5'),
],
]) . "\n");
}
$affected = $this->getGeneral()->applyDB($db);
$this->getLogger()->flush();
return $affected;