test(compare): trace deterministic monthly seed progression
This commit is contained in:
+410
-129
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user