test(compare): trace deterministic monthly seed progression
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace sammo;
|
||||
|
||||
if (PHP_SAPI !== 'cli' || getenv('REF_DETERMINISTIC_INSTALL_ENABLED') !== '1') {
|
||||
http_response_code(404);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
chdir(dirname(__DIR__));
|
||||
require_once 'lib.php';
|
||||
require_once 'func.php';
|
||||
|
||||
$request = json_decode(stream_get_contents(STDIN), true, flags: JSON_THROW_ON_ERROR);
|
||||
$generalIds = $request['generalIds'] ?? null;
|
||||
if (!is_array($generalIds) || $generalIds === []) {
|
||||
throw new \InvalidArgumentException('generalIds must be a non-empty array');
|
||||
}
|
||||
|
||||
$db = DB::db();
|
||||
$gameStorage = KVStorage::getStorage($db, 'game_env');
|
||||
$env = $gameStorage->getAll(true);
|
||||
$result = [];
|
||||
$includeNation = ($request['includeNation'] ?? false) === true;
|
||||
foreach ($generalIds as $generalId) {
|
||||
if (!is_int($generalId) || $generalId < 1) {
|
||||
throw new \InvalidArgumentException('generalIds must contain positive integers');
|
||||
}
|
||||
$general = General::createObjFromDB($generalId);
|
||||
if ($general->getNPCType() < 2) {
|
||||
throw new \InvalidArgumentException("general {$generalId} is not an NPC");
|
||||
}
|
||||
$ai = new GeneralAI($general);
|
||||
$nationDecision = null;
|
||||
if ($includeNation && $general->getNationID() !== 0 && $general->getVar('officer_level') >= 5) {
|
||||
$nationStorage = KVStorage::getStorage($db, $general->getNationID(), 'nation_env');
|
||||
$lastNationTurn = LastTurn::fromRaw($nationStorage->getValue("turn_last_{$general->getVar('officer_level')}"));
|
||||
$reservedNation = buildNationCommandClass(null, $general, $env, $lastNationTurn);
|
||||
$selectedNation = $ai->chooseNationTurn($reservedNation);
|
||||
$nationDecision = [
|
||||
'action' => $selectedNation->getRawClassName(),
|
||||
'args' => $selectedNation->getArg(),
|
||||
'reason' => $selectedNation->reason,
|
||||
];
|
||||
}
|
||||
$reserved = $general->getReservedTurn(0, $env);
|
||||
$selected = $ai->chooseGeneralTurn($reserved);
|
||||
$result[] = [
|
||||
'generalId' => $generalId,
|
||||
'name' => $general->getName(),
|
||||
'action' => $selected->getRawClassName(),
|
||||
'brief' => $selected->getBrief(),
|
||||
'args' => $selected->getArg(),
|
||||
'reason' => $selected->reason,
|
||||
'nationDecision' => $nationDecision,
|
||||
];
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'year' => (int) $env['year'],
|
||||
'month' => (int) $env['month'],
|
||||
'decisions' => $result,
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
|
||||
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace sammo;
|
||||
|
||||
if (PHP_SAPI !== 'cli' || getenv('REF_DETERMINISTIC_INSTALL_ENABLED') !== '1') {
|
||||
http_response_code(404);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
chdir(dirname(__DIR__));
|
||||
require_once 'lib.php';
|
||||
require_once 'func.php';
|
||||
|
||||
/** @return mixed */
|
||||
function monthlySnapshotJson(mixed $value): mixed
|
||||
{
|
||||
if (!is_string($value) || $value === '') {
|
||||
return $value;
|
||||
}
|
||||
try {
|
||||
return json_decode($value, true, flags: JSON_THROW_ON_ERROR);
|
||||
} catch (\JsonException) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
function monthlySnapshotPick(array $row, array $mapping, array $jsonColumns = []): array
|
||||
{
|
||||
$result = [];
|
||||
foreach ($mapping as $target => $source) {
|
||||
$value = $row[$source] ?? null;
|
||||
$result[$target] = in_array($source, $jsonColumns, true)
|
||||
? monthlySnapshotJson($value)
|
||||
: $value;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
$db = DB::db();
|
||||
$game = KVStorage::getStorage($db, 'game_env');
|
||||
$game->resetCache();
|
||||
$world = $game->getValues([
|
||||
'scenario', 'year', 'month', 'turnterm', 'turntime', 'starttime',
|
||||
'init_year', 'init_month', 'develcost', 'killturn', 'isunited',
|
||||
]);
|
||||
|
||||
$generals = [];
|
||||
foreach ($db->query('SELECT * FROM general ORDER BY no') as $row) {
|
||||
$general = monthlySnapshotPick($row, [
|
||||
'id' => 'no', 'name' => 'name', 'nationId' => 'nation', 'cityId' => 'city',
|
||||
'troopId' => 'troop', 'leadership' => 'leadership', 'strength' => 'strength',
|
||||
'intelligence' => 'intel', 'experience' => 'experience', 'dedication' => 'dedication',
|
||||
'officerLevel' => 'officer_level', 'injury' => 'injury', 'gold' => 'gold',
|
||||
'rice' => 'rice', 'crew' => 'crew', 'crewTypeId' => 'crewtype', 'train' => 'train',
|
||||
'atmos' => 'atmos', 'age' => 'age', 'npcState' => 'npc', 'turnTime' => 'turntime',
|
||||
'recentWarTime' => 'recent_war', 'lastTurn' => 'last_turn', 'meta' => 'aux',
|
||||
'affinity' => 'affinity', 'birthYear' => 'bornyear', 'deathYear' => 'deadyear',
|
||||
'personality' => 'personal', 'specialDomestic' => 'special', 'specialWar' => 'special2',
|
||||
'specAge' => 'specage', 'specAge2' => 'specage2',
|
||||
'dex1' => 'dex1', 'dex2' => 'dex2', 'dex3' => 'dex3', 'dex4' => 'dex4', 'dex5' => 'dex5',
|
||||
'killTurn' => 'killturn',
|
||||
], ['last_turn', 'aux']);
|
||||
foreach (['id', 'nationId', 'cityId', 'troopId', 'leadership', 'strength', 'intelligence',
|
||||
'experience', 'dedication', 'officerLevel', 'injury', 'gold', 'rice', 'crew',
|
||||
'crewTypeId', 'train', 'atmos', 'age', 'npcState', 'dex1', 'dex2', 'dex3',
|
||||
'dex4', 'dex5', 'killTurn', 'affinity', 'birthYear', 'deathYear', 'specAge', 'specAge2'] as $key) {
|
||||
$general[$key] = (int)$general[$key];
|
||||
}
|
||||
$generals[] = $general;
|
||||
}
|
||||
|
||||
$cities = [];
|
||||
foreach ($db->query('SELECT * FROM city ORDER BY city') as $row) {
|
||||
$city = monthlySnapshotPick($row, [
|
||||
'id' => 'city', 'name' => 'name', 'nationId' => 'nation', 'level' => 'level',
|
||||
'population' => 'pop', 'populationMax' => 'pop_max', 'agriculture' => 'agri',
|
||||
'agricultureMax' => 'agri_max', 'commerce' => 'comm', 'commerceMax' => 'comm_max',
|
||||
'security' => 'secu', 'securityMax' => 'secu_max', 'supplyState' => 'supply',
|
||||
'frontState' => 'front', 'defence' => 'def', 'defenceMax' => 'def_max',
|
||||
'wall' => 'wall', 'wallMax' => 'wall_max', 'state' => 'state', 'term' => 'term',
|
||||
'trust' => 'trust', 'trade' => 'trade', 'conflict' => 'conflict',
|
||||
], ['conflict']);
|
||||
foreach (array_keys($city) as $key) {
|
||||
if ($key !== 'name' && $key !== 'conflict' && $key !== 'trust') {
|
||||
$city[$key] = (int)$city[$key];
|
||||
}
|
||||
}
|
||||
$cities[] = $city;
|
||||
}
|
||||
|
||||
$nations = [];
|
||||
foreach ($db->query('SELECT * FROM nation ORDER BY nation') as $row) {
|
||||
$nation = monthlySnapshotPick($row, [
|
||||
'id' => 'nation', 'name' => 'name', 'color' => 'color', 'capitalCityId' => 'capital',
|
||||
'gold' => 'gold', 'rice' => 'rice', 'tech' => 'tech', 'level' => 'level',
|
||||
'typeCode' => 'type', 'generalCount' => 'gennum', 'power' => 'power',
|
||||
'war' => 'war', 'diplomacyLimit' => 'surlimit', 'capitalRevision' => 'capset',
|
||||
'strategicCommandLimit' => 'strategic_cmd_limit', 'rate' => 'rate',
|
||||
'rateTmp' => 'rate_tmp', 'bill' => 'bill', 'meta' => 'aux',
|
||||
], ['aux']);
|
||||
foreach (['id', 'capitalCityId', 'gold', 'rice', 'level', 'generalCount',
|
||||
'power', 'war', 'diplomacyLimit', 'capitalRevision', 'strategicCommandLimit',
|
||||
'rate', 'rateTmp', 'bill'] as $key) {
|
||||
$nation[$key] = (int)$nation[$key];
|
||||
}
|
||||
$nation['tech'] = (float)$nation['tech'];
|
||||
$nations[] = $nation;
|
||||
}
|
||||
|
||||
$diplomacy = [];
|
||||
foreach ($db->query('SELECT me, you, state, term, dead FROM diplomacy ORDER BY me, you') as $row) {
|
||||
$diplomacy[] = [
|
||||
'fromNationId' => (int)$row['me'], 'toNationId' => (int)$row['you'],
|
||||
'state' => (int)$row['state'], 'term' => (int)$row['term'], 'dead' => (int)$row['dead'],
|
||||
];
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'schemaVersion' => 1,
|
||||
'engine' => 'ref',
|
||||
'world' => $world,
|
||||
'generals' => $generals,
|
||||
'cities' => $cities,
|
||||
'nations' => $nations,
|
||||
'diplomacy' => $diplomacy,
|
||||
], JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION), PHP_EOL;
|
||||
@@ -181,6 +181,7 @@ function comparisonRowsById(string $table, string $idColumn, array $ids): array
|
||||
|
||||
function comparisonTurnStateSnapshot(array $request): array
|
||||
{
|
||||
$db = DB::db();
|
||||
$observe = $request['observe'] ?? [];
|
||||
if (!is_array($observe)) {
|
||||
throw new \InvalidArgumentException('observe must be an object');
|
||||
@@ -188,6 +189,15 @@ function comparisonTurnStateSnapshot(array $request): array
|
||||
$generalIds = comparisonIntegerList($observe['generalIds'] ?? [], 'generalIds');
|
||||
$cityIds = comparisonIntegerList($observe['cityIds'] ?? [], 'cityIds');
|
||||
$nationIds = comparisonIntegerList($observe['nationIds'] ?? [], 'nationIds');
|
||||
if (($observe['allGenerals'] ?? false) === true) {
|
||||
$generalIds = array_map('intval', $db->queryFirstColumn('SELECT no FROM general ORDER BY no'));
|
||||
}
|
||||
if (($observe['allCities'] ?? false) === true) {
|
||||
$cityIds = array_map('intval', $db->queryFirstColumn('SELECT city FROM city ORDER BY city'));
|
||||
}
|
||||
if (($observe['allNations'] ?? false) === true) {
|
||||
$nationIds = array_map('intval', $db->queryFirstColumn('SELECT nation FROM nation ORDER BY nation'));
|
||||
}
|
||||
$logAfterId = $observe['logAfterId'] ?? 0;
|
||||
$messageAfterId = $observe['messageAfterId'] ?? 0;
|
||||
$includeNationHistoryLogs = $observe['includeNationHistoryLogs'] ?? false;
|
||||
@@ -219,7 +229,6 @@ function comparisonTurnStateSnapshot(array $request): array
|
||||
throw new \InvalidArgumentException('includeGlobalHistoryLogs must be a boolean');
|
||||
}
|
||||
|
||||
$db = DB::db();
|
||||
$game = KVStorage::getStorage($db, 'game_env');
|
||||
$game->resetCache();
|
||||
$worldValues = $game->getValues([
|
||||
|
||||
+16
-7
@@ -1169,7 +1169,9 @@ function checkDelay()
|
||||
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||
|
||||
//서버정보
|
||||
$now = new \DateTimeImmutable();
|
||||
// ng_compare can inject a deterministic semantic clock. Runtime limits
|
||||
// still use the wall clock; only delay normalization follows game time.
|
||||
$now = TimeUtil::nowDateTimeImmutable();
|
||||
$turntime = new \DateTimeImmutable($gameStor->turntime);
|
||||
$timeMinDiff = intdiv($now->getTimestamp() - $turntime->getTimestamp(), 60);
|
||||
|
||||
@@ -1293,12 +1295,19 @@ function triggerTournament(RandUtil $rng)
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$tnmt_pattern) {
|
||||
// 0 : 전력전, 1 : 통솔전, 2 : 일기토, 3 : 설전
|
||||
//전력전 40%, 통, 일, 설 각 20%
|
||||
$tnmt_pattern = [0, 0, 1, 2, 3];
|
||||
shuffle($tnmt_pattern);
|
||||
}
|
||||
if (!$tnmt_pattern) {
|
||||
// 0 : 전력전, 1 : 통솔전, 2 : 일기토, 3 : 설전
|
||||
//전력전 40%, 통, 일, 설 각 20%
|
||||
$tnmt_pattern = [0, 0, 1, 2, 3];
|
||||
if (getenv('REF_DETERMINISTIC_INSTALL_ENABLED') === '1') {
|
||||
// The legacy global shuffle escapes the configured hidden seed.
|
||||
// The comparison branch keeps production behaviour untouched and
|
||||
// consumes the current month's deterministic RNG instead.
|
||||
$tnmt_pattern = $rng->shuffle($tnmt_pattern);
|
||||
} else {
|
||||
shuffle($tnmt_pattern);
|
||||
}
|
||||
}
|
||||
|
||||
$tnmt_type = array_pop($tnmt_pattern);
|
||||
$gameStor->setValue('tnmt_pattern', $tnmt_pattern);
|
||||
|
||||
+32
-1
@@ -319,7 +319,38 @@ function postUpdateMonthly(RandUtil $rng)
|
||||
|
||||
//약간의 랜덤치 부여 (95% ~ 105%)
|
||||
|
||||
$nation['power'] = Util::round($nation['power'] * $rng->nextRange(0.95, 1.05));
|
||||
$powerMultiplier = $rng->nextRange(0.95, 1.05);
|
||||
$basePower = $nation['power'];
|
||||
$nation['power'] = Util::round($basePower * $powerMultiplier);
|
||||
$traceNationIDs = getenv('REF_TRACE_NATION_POWER_IDS');
|
||||
if (
|
||||
PHP_SAPI === 'cli'
|
||||
&& getenv('REF_DETERMINISTIC_INSTALL_ENABLED') === '1'
|
||||
&& is_string($traceNationIDs)
|
||||
&& in_array((string) $nationID, explode(',', $traceNationIDs), true)
|
||||
) {
|
||||
$traceComponents = $db->queryFirstRow('SELECT
|
||||
((A.gold + A.rice) + (SELECT SUM(gold + rice) FROM general WHERE nation = A.nation)) / 100 AS resourcePowerRaw,
|
||||
ROUND(((A.gold + A.rice) + (SELECT SUM(gold + rice) FROM general WHERE nation = A.nation)) / 100) AS resourcePower,
|
||||
A.tech AS techPower,
|
||||
(SELECT SUM(pop) * SUM(pop + agri + comm + secu + wall + def) / SUM(pop_max + agri_max + comm_max + secu_max + wall_max + def_max) / 100 FROM city WHERE nation = A.nation AND supply = 1) AS cityPowerRaw,
|
||||
(SELECT ROUND(SUM(pop) * SUM(pop + agri + comm + secu + wall + def) / SUM(pop_max + agri_max + comm_max + secu_max + wall_max + def_max) / 100) FROM city WHERE nation = A.nation AND supply = 1) AS cityPower,
|
||||
(SELECT SUM((ra.value + 1000) / (rb.value + 1000) * (CASE WHEN g.npc < 2 THEN 1.2 ELSE 1 END) * (CASE WHEN g.leadership >= 40 THEN g.leadership ELSE 0 END) * 2 + (SQRT(g.intel * g.strength) * 2 + g.leadership / 2) / 2)
|
||||
FROM general g
|
||||
LEFT JOIN rank_data ra ON g.no = ra.general_id AND ra.type = \'killcrew_person\'
|
||||
LEFT JOIN rank_data rb ON g.no = rb.general_id AND rb.type = \'deathcrew_person\'
|
||||
WHERE g.nation = A.nation) AS generalPower,
|
||||
(SELECT SUM(experience + dedication) / 100 FROM general WHERE nation = A.nation) AS experiencePowerRaw,
|
||||
(SELECT ROUND(SUM(experience + dedication) / 100) FROM general WHERE nation = A.nation) AS experiencePower
|
||||
FROM nation A WHERE A.nation = %i', $nationID) ?? [];
|
||||
fwrite(STDOUT, 'NATION_POWER_TRACE ' . Json::encode([
|
||||
'nationId' => $nationID,
|
||||
...$traceComponents,
|
||||
'basePower' => $basePower,
|
||||
'multiplier' => $powerMultiplier,
|
||||
'power' => $nation['power'],
|
||||
]) . PHP_EOL);
|
||||
}
|
||||
$powerValues['maxPower'] = max($powerValues['maxPower'] ?? 0, $nation['power']);
|
||||
$powerValues['maxCrew'] = max($powerValues['maxCrew'] ?? 0, Util::toInt($nation['totalCrew']));
|
||||
|
||||
|
||||
@@ -367,7 +367,7 @@ function disaster(RandUtil $rng) {
|
||||
[$queryColumns,,] = General::mergeQueryColumn();
|
||||
$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($queryColumns),
|
||||
Util::squeezeFromArray($targetCityList, 'city')
|
||||
),
|
||||
|
||||
+17
-2
@@ -15,11 +15,24 @@ function calcTournamentTerm(int $turnTerm): int{
|
||||
|
||||
function processTournament()
|
||||
{
|
||||
if (
|
||||
getenv('REF_DETERMINISTIC_INSTALL_ENABLED') === '1'
|
||||
&& getenv('REF_DETERMINISTIC_TOURNAMENT_MODE') === 'hold'
|
||||
) {
|
||||
// Legacy tournament matches mix PHP rand(), shuffle(), and MariaDB
|
||||
// ORDER BY RAND(), so they are outside the hidden/monthly seed stream.
|
||||
// Hold the real-time subsystem during monthly world-state comparison.
|
||||
return;
|
||||
}
|
||||
|
||||
$db = DB::db();
|
||||
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||
|
||||
$admin = $gameStor->getValues(['tournament', 'phase', 'tnmt_type', 'tnmt_auto', 'tnmt_time', 'turnterm', 'last_tournament_betting_id']);
|
||||
$now = new \DateTime();
|
||||
// The comparison harness supplies a fixed clock through TimeUtil. Keep the
|
||||
// production clock unchanged while preventing tournament catch-up from
|
||||
// escaping that deterministic month boundary.
|
||||
$now = TimeUtil::nowDateTime();
|
||||
$offset = $now->getTimestamp() - (new \DateTime($admin['tnmt_time']))->getTimestamp();
|
||||
|
||||
//수동일땐 무시
|
||||
@@ -286,7 +299,9 @@ function startTournament($type)
|
||||
$unit = calcTournamentTerm($turnTerm);
|
||||
|
||||
$gameStor->tnmt_auto = true;
|
||||
$gameStor->tnmt_time = (new \DateTimeImmutable())->add(new \DateInterval("PT{$unit}M"))->format('Y-m-d H:i:s');
|
||||
$gameStor->tnmt_time = TimeUtil::nowDateTimeImmutable()
|
||||
->add(new \DateInterval("PT{$unit}M"))
|
||||
->format('Y-m-d H:i:s');
|
||||
$gameStor->tournament = 1;
|
||||
$gameStor->tnmt_type = $type;
|
||||
$gameStor->last_tournament_betting_id = 0;
|
||||
|
||||
+12
-4
@@ -199,8 +199,16 @@ if($reserve_open){
|
||||
]);
|
||||
}
|
||||
|
||||
try{
|
||||
Json::die(ResetHelper::buildScenario(
|
||||
try{
|
||||
$installTime = TimeUtil::now();
|
||||
if (getenv('REF_DETERMINISTIC_INSTALL_ENABLED') === '1') {
|
||||
$comparisonInstallAt = getenv('REF_INSTALL_AT');
|
||||
if (!is_string($comparisonInstallAt) || $comparisonInstallAt === '') {
|
||||
throw new \RuntimeException('REF_INSTALL_AT is required for deterministic comparison install');
|
||||
}
|
||||
$installTime = (new \DateTimeImmutable($comparisonInstallAt))->format('Y-m-d H:i:s');
|
||||
}
|
||||
Json::die(ResetHelper::buildScenario(
|
||||
$turnterm,
|
||||
$sync,
|
||||
$scenario,
|
||||
@@ -211,7 +219,7 @@ try{
|
||||
$show_img_level,
|
||||
!!$tournament_trig,
|
||||
$join_mode,
|
||||
TimeUtil::now(),
|
||||
$installTime,
|
||||
$autorun_user
|
||||
));
|
||||
}
|
||||
@@ -220,4 +228,4 @@ catch(\Exception $e){
|
||||
'result'=>false,
|
||||
'reason'=>$e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
+78
-2
@@ -42,10 +42,20 @@ function processWar(string $warSeed, General $attackerGeneral, array $rawAttacke
|
||||
|
||||
/** @var WarUnit[] */
|
||||
$defenderList = [];
|
||||
$defenderCandidateTrace = [];
|
||||
foreach($defenderCityGeneralList as $defenderGeneral){
|
||||
$defenderGeneral->setRawCity($rawDefenderCity);
|
||||
$defenderCandidate = new WarUnitGeneral($rng, $defenderGeneral, $rawDefenderNation, false);
|
||||
if(extractBattleOrder($defenderCandidate, $attacker) <= 0){
|
||||
$defenderOrder = extractBattleOrder($defenderCandidate, $attacker);
|
||||
$defenderCandidateTrace[] = [
|
||||
'id' => $defenderGeneral->getID(),
|
||||
'crew' => $defenderGeneral->getVar('crew'),
|
||||
'crewtype' => $defenderGeneral->getVar('crewtype'),
|
||||
'train' => $defenderGeneral->getVar('train'),
|
||||
'atmos' => $defenderGeneral->getVar('atmos'),
|
||||
'order' => $defenderOrder,
|
||||
];
|
||||
if($defenderOrder <= 0){
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -56,6 +66,14 @@ function processWar(string $warSeed, General $attackerGeneral, array $rawAttacke
|
||||
$defenderList[] = $city;
|
||||
}
|
||||
|
||||
$summarizeDefenderOrder = static function (WarUnit $unit) use ($attacker): array {
|
||||
return [
|
||||
'id' => $unit instanceof WarUnitGeneral ? $unit->getGeneral()->getID() : 0,
|
||||
'order' => extractBattleOrder($unit, $attacker),
|
||||
];
|
||||
};
|
||||
$defenderOrderBeforeSort = array_map($summarizeDefenderOrder, $defenderList);
|
||||
|
||||
usort($defenderList, function (WarUnit $lhs, WarUnit $rhs) use ($attacker) {
|
||||
return - (extractBattleOrder($lhs, $attacker) <=> extractBattleOrder($rhs, $attacker));
|
||||
});
|
||||
@@ -85,7 +103,39 @@ function processWar(string $warSeed, General $attackerGeneral, array $rawAttacke
|
||||
return $nextDefender;
|
||||
};
|
||||
|
||||
$conquerCity = processWar_NG($warSeed, $attacker, $getNextDefender, $city);
|
||||
$traceGeneralIds = getenv('REF_AI_TRACE_GENERAL_IDS');
|
||||
$traceGeneralIdList = is_string($traceGeneralIds) ? explode(',', $traceGeneralIds) : [];
|
||||
$traceDefender = false;
|
||||
foreach ($defenderCityGeneralList as $defenderGeneral) {
|
||||
if (in_array((string)$defenderGeneral->getID(), $traceGeneralIdList, true)) {
|
||||
$traceDefender = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$warTrace =
|
||||
PHP_SAPI === 'cli'
|
||||
&& getenv('REF_DETERMINISTIC_INSTALL_ENABLED') === '1'
|
||||
&& is_string($traceGeneralIds)
|
||||
&& (in_array((string)$attackerGeneral->getID(), $traceGeneralIdList, true) || $traceDefender)
|
||||
? static function (array $event) use ($attackerGeneral): void {
|
||||
fwrite(STDOUT, 'AI_WAR_TRACE ' . Json::encode([
|
||||
'generalId' => $attackerGeneral->getID(),
|
||||
'event' => $event,
|
||||
]) . "\n");
|
||||
}
|
||||
: null;
|
||||
if ($warTrace !== null) {
|
||||
fwrite(STDOUT, 'AI_WAR_DEFENDER_CANDIDATE_TRACE ' . Json::encode([
|
||||
'generalId' => $attackerGeneral->getID(),
|
||||
'candidates' => $defenderCandidateTrace,
|
||||
]) . "\n");
|
||||
fwrite(STDOUT, 'AI_WAR_DEFENDER_ORDER_TRACE ' . Json::encode([
|
||||
'generalId' => $attackerGeneral->getID(),
|
||||
'before' => $defenderOrderBeforeSort,
|
||||
'after' => array_map($summarizeDefenderOrder, $defenderList),
|
||||
]) . "\n");
|
||||
}
|
||||
$conquerCity = processWar_NG($warSeed, $attacker, $getNextDefender, $city, $warTrace);
|
||||
|
||||
$attacker->applyDB($db);
|
||||
|
||||
@@ -157,6 +207,32 @@ function processWar(string $warSeed, General $attackerGeneral, array $rawAttacke
|
||||
$defenderIncTech /= 4;
|
||||
}
|
||||
|
||||
$traceNationIds = array_filter(explode(',', (string) getenv('REF_WAR_TECH_TRACE_NATION_IDS')), 'strlen');
|
||||
if (getenv('REF_DETERMINISTIC_INSTALL_ENABLED') === '1') {
|
||||
foreach ([
|
||||
['side' => 'attacker', 'nation' => $rawAttackerNation, 'baseGain' => $attacker->getDead() * 0.012, 'gain' => $attackerIncTech, 'effective' => $attackerGenCnt_eff],
|
||||
['side' => 'defender', 'nation' => $rawDefenderNation, 'baseGain' => $attacker->getKilled() * 0.009, 'gain' => $defenderIncTech, 'effective' => $defenderGenCnt_eff],
|
||||
] as $traceEntry) {
|
||||
if (!in_array((string) $traceEntry['nation']['nation'], $traceNationIds, true)) {
|
||||
continue;
|
||||
}
|
||||
$divisor = max(GameConst::$initialNationGenLimit, $traceEntry['nation']['gennum']);
|
||||
fwrite(STDOUT, 'WAR_TECH_TRACE ' . Json::encode([
|
||||
'engine' => 'ref',
|
||||
'nationId' => $traceEntry['nation']['nation'],
|
||||
'side' => $traceEntry['side'],
|
||||
'currentTech' => $traceEntry['nation']['tech'],
|
||||
'baseGain' => $traceEntry['baseGain'],
|
||||
'gain' => $traceEntry['gain'],
|
||||
'total' => $traceEntry['nation']['gennum'],
|
||||
'effective' => $traceEntry['effective'],
|
||||
'divisor' => $divisor,
|
||||
'delta' => $traceEntry['gain'] / $divisor,
|
||||
'attackerGeneralId' => $attacker->getGeneral()->getID(),
|
||||
]) . "\n");
|
||||
}
|
||||
}
|
||||
|
||||
$updateAttackerNation['tech'] = $db->sqleval(
|
||||
'tech + %d',
|
||||
$attackerIncTech / max(GameConst::$initialNationGenLimit, $rawAttackerNation['gennum'])
|
||||
|
||||
@@ -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{
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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()
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
+34
-5
@@ -4,13 +4,36 @@ namespace sammo;
|
||||
|
||||
class RandUtil
|
||||
{
|
||||
private ?int $traceGeneralId = null;
|
||||
private int $traceSequence = 0;
|
||||
|
||||
public function __construct(public readonly RNG $rng)
|
||||
{
|
||||
}
|
||||
|
||||
public function setTraceGeneralId(?int $generalId): void
|
||||
{
|
||||
$this->traceGeneralId = $generalId;
|
||||
}
|
||||
|
||||
private function trace(string $method, mixed $result): void
|
||||
{
|
||||
if ($this->traceGeneralId === null) {
|
||||
return;
|
||||
}
|
||||
fwrite(STDOUT, 'AI_RNG_TRACE ' . json_encode([
|
||||
'generalId' => $this->traceGeneralId,
|
||||
'sequence' => $this->traceSequence++,
|
||||
'method' => $method,
|
||||
'result' => $result,
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n");
|
||||
}
|
||||
|
||||
public function nextFloat1(): float
|
||||
{
|
||||
return $this->rng->nextFloat1();
|
||||
$result = $this->rng->nextFloat1();
|
||||
$this->trace('nextFloat1', $result);
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function nextRange(int|float $min, int|float $max): float
|
||||
@@ -25,16 +48,22 @@ class RandUtil
|
||||
if ($range > $this->rng->getMaxInt()) {
|
||||
throw new \InvalidArgumentException("Invalid random int range");
|
||||
}
|
||||
return $this->rng->nextInt($range) + $min;
|
||||
$result = $this->rng->nextInt($range) + $min;
|
||||
$this->trace('nextRangeInt', $result);
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function nextInt(?int $max = null): int{
|
||||
return $this->rng->nextInt($max);
|
||||
$result = $this->rng->nextInt($max);
|
||||
$this->trace('nextInt', $result);
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function nextBit(): bool
|
||||
{
|
||||
return $this->rng->nextBits(1) !== "\0";
|
||||
$result = $this->rng->nextBits(1) !== "\0";
|
||||
$this->trace('nextBit', $result);
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function nextBool(int|float $prob = 0.5): bool
|
||||
@@ -97,7 +126,7 @@ class RandUtil
|
||||
if(!$keys){
|
||||
throw new \InvalidArgumentException();
|
||||
}
|
||||
$keyIdx = $this->rng->nextInt(count($keys) - 1);
|
||||
$keyIdx = $this->nextInt(count($keys) - 1);
|
||||
return $items[$keys[$keyIdx]];
|
||||
}
|
||||
|
||||
|
||||
+22
-1
@@ -7,6 +7,19 @@ use phpDocumentor\Reflection\Types\Boolean;
|
||||
class TimeUtil
|
||||
{
|
||||
|
||||
private static function comparisonNow(): ?\DateTimeImmutable
|
||||
{
|
||||
$comparisonNow = getenv('REF_COMPARISON_NOW');
|
||||
if (
|
||||
getenv('REF_DETERMINISTIC_INSTALL_ENABLED') !== '1'
|
||||
|| !is_string($comparisonNow)
|
||||
|| $comparisonNow === ''
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return new \DateTimeImmutable($comparisonNow);
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
public static function DateToday()
|
||||
{
|
||||
@@ -87,7 +100,7 @@ class TimeUtil
|
||||
|
||||
public static function now(bool $withFraction = false): string
|
||||
{
|
||||
$obj = new \DateTime();
|
||||
$obj = static::comparisonNow() ?? new \DateTimeImmutable();
|
||||
return static::format($obj, $withFraction);
|
||||
}
|
||||
|
||||
@@ -182,11 +195,19 @@ class TimeUtil
|
||||
}
|
||||
|
||||
public static function nowDateTime(): \DateTime{
|
||||
$comparisonNow = static::comparisonNow();
|
||||
if ($comparisonNow !== null) {
|
||||
return \DateTime::createFromImmutable($comparisonNow);
|
||||
}
|
||||
$now = time();
|
||||
return static::secondsToDateTime($now, false, true);
|
||||
}
|
||||
|
||||
public static function nowDateTimeImmutable(): \DateTimeImmutable{
|
||||
$comparisonNow = static::comparisonNow();
|
||||
if ($comparisonNow !== null) {
|
||||
return $comparisonNow;
|
||||
}
|
||||
$now = time();
|
||||
return static::secondsToDateTime($now, true, true);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user