Compare commits
118
Commits
v0.39.0
...
ng_compare
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c8aa3ed9f | ||
|
|
191bb6428a | ||
|
|
ba7676e0f6 | ||
|
|
5e279eb666 | ||
|
|
605fd3c9d1 | ||
|
|
5d897105ed | ||
|
|
5432590591 | ||
|
|
bee18f20d0 | ||
|
|
f16a6b6880 | ||
|
|
3b2fd1082f | ||
|
|
faab4561f6 | ||
|
|
dcddea15b4 | ||
|
|
90d078c100 | ||
|
|
765dee831c | ||
|
|
10c4814f34 | ||
|
|
fe221a7bd9 | ||
|
|
8cdc9c9b84 | ||
|
|
df189f4408 | ||
|
|
47fa7885c5 | ||
|
|
004dbb39b4 | ||
|
|
cd94c15137 | ||
|
|
eeb965353e | ||
|
|
6dd09bd4b4 | ||
|
|
a73189a604 | ||
|
|
4341341a07 | ||
|
|
4b82f0b35c | ||
|
|
b417e117ad | ||
|
|
85e435e99d | ||
|
|
e48c13a92f | ||
|
|
4495f0c06e | ||
|
|
312711b7ed | ||
|
|
3e8c18b8d3 | ||
|
|
fb0f768f2c | ||
|
|
5fc230d89b | ||
|
|
5fd1a6c752 | ||
|
|
76a5ea273a | ||
|
|
3d0a5a4be8 | ||
|
|
23309113ec | ||
|
|
1a1bc541f5 | ||
|
|
20060a89fd | ||
|
|
24f788d1a0 | ||
|
|
813957479d | ||
|
|
cdf58c54e0 | ||
|
|
6399134380 | ||
|
|
eb49e6f8ff | ||
|
|
ef855eb2cc | ||
|
|
c970e6586d | ||
|
|
6eaa529048 | ||
|
|
fe39241a90 | ||
|
|
d936d93be8 | ||
|
|
89127c0258 | ||
|
|
57aa07453e | ||
|
|
0722f6ab5e | ||
|
|
b9c51da782 | ||
|
|
c290404e42 | ||
|
|
cc0f965cb4 | ||
|
|
64736d194e | ||
|
|
963b04e8b0 | ||
|
|
e58ffab2a2 | ||
|
|
de29e2e157 | ||
|
|
7aa1b1889c | ||
|
|
618cfe5720 | ||
|
|
a8cecf8ec2 | ||
|
|
d858c64a3c | ||
|
|
f49b1d26a5 | ||
|
|
92e5f2472d | ||
|
|
11144f1f88 | ||
|
|
34c9a9b53e | ||
|
|
84d9cf9eb4 | ||
|
|
8a4cc5df21 | ||
|
|
df0b8ccebe | ||
|
|
ac47bc58bd | ||
|
|
570ef6f005 | ||
|
|
7eecde62fa | ||
|
|
4e0e5c9cd7 | ||
|
|
18abcde0d5 | ||
|
|
6176b64c28 | ||
|
|
3bae1157b6 | ||
|
|
5a51ad5699 | ||
|
|
312d20269c | ||
|
|
178d6f042b | ||
|
|
3352b64d0f | ||
|
|
e7d86a16d5 | ||
|
|
820c25d8dc | ||
|
|
f2dcb4d4d1 | ||
|
|
de357cfe95 | ||
|
|
0f333f8c96 | ||
|
|
93e8adadac | ||
|
|
8f98626075 | ||
|
|
a321eb1362 | ||
|
|
1a17fad0a5 | ||
|
|
c74efbb372 | ||
|
|
66f79ca5d2 | ||
|
|
f42f844f17 | ||
|
|
aee98e15d7 | ||
|
|
0936dabc9e | ||
|
|
39b209caf0 | ||
|
|
07867b3aaf | ||
|
|
de1f74cb44 | ||
|
|
65c97107c6 | ||
|
|
d796eda13c | ||
|
|
2a9029470f | ||
|
|
254364bab8 | ||
|
|
0319410ade | ||
|
|
6b64f6f8ed | ||
|
|
62fcb2e987 | ||
|
|
ff3ef4e4f3 | ||
|
|
008b0e416c | ||
|
|
bf96849027 | ||
|
|
1b035e4a42 | ||
|
|
47dec60c2b | ||
|
|
c03a28be93 | ||
|
|
d1ab2b0450 | ||
|
|
fe9ae97821 | ||
|
|
98e0a676cb | ||
|
|
2bb1387bc4 | ||
|
|
af382c541e | ||
|
|
8616a094e5 |
@@ -0,0 +1,102 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace sammo;
|
||||||
|
|
||||||
|
use sammo\Enums\RankColumn;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the existing battle simulator request from live, pre-battle objects.
|
||||||
|
*
|
||||||
|
* This file is loaded only by the deterministic CLI comparison path. It must
|
||||||
|
* remain read-only: callers emit the returned value without changing the live
|
||||||
|
* objects or consuming RNG.
|
||||||
|
*/
|
||||||
|
function comparisonBuildBattleGeneralFixture(General $general): array
|
||||||
|
{
|
||||||
|
$raw = $general->getRaw(true);
|
||||||
|
$inheritBuff = $general->getAuxVar('inheritBuff');
|
||||||
|
|
||||||
|
$fixture = [
|
||||||
|
'no' => $general->getID(),
|
||||||
|
'name' => $general->getName(),
|
||||||
|
'nation' => $general->getNationID(),
|
||||||
|
'city' => $general->getCityID(),
|
||||||
|
'turntime' => $general->getTurnTime(General::TURNTIME_FULL_MS),
|
||||||
|
'turntick' => $general->getTurnTick(),
|
||||||
|
'personal' => $raw['personal'] ?? null,
|
||||||
|
'special' => $raw['special'] ?? null,
|
||||||
|
'special2' => $raw['special2'] ?? null,
|
||||||
|
'crew' => $raw['crew'],
|
||||||
|
'crewtype' => $raw['crewtype'],
|
||||||
|
'atmos' => $raw['atmos'],
|
||||||
|
'train' => $raw['train'],
|
||||||
|
'intel' => $raw['intel'],
|
||||||
|
'intel_exp' => $raw['intel_exp'],
|
||||||
|
'book' => $raw['book'] ?? 'None',
|
||||||
|
'strength' => $raw['strength'],
|
||||||
|
'strength_exp' => $raw['strength_exp'],
|
||||||
|
'weapon' => $raw['weapon'] ?? 'None',
|
||||||
|
'injury' => $raw['injury'],
|
||||||
|
'leadership' => $raw['leadership'],
|
||||||
|
'leadership_exp' => $raw['leadership_exp'],
|
||||||
|
'horse' => $raw['horse'] ?? 'None',
|
||||||
|
'item' => $raw['item'] ?? 'None',
|
||||||
|
'explevel' => $raw['explevel'],
|
||||||
|
'experience' => $raw['experience'],
|
||||||
|
'dedication' => $raw['dedication'],
|
||||||
|
'officer_level' => $raw['officer_level'],
|
||||||
|
'officer_city' => $raw['officer_city'],
|
||||||
|
'gold' => $raw['gold'],
|
||||||
|
'rice' => $raw['rice'],
|
||||||
|
'dex1' => $raw['dex1'],
|
||||||
|
'dex2' => $raw['dex2'],
|
||||||
|
'dex3' => $raw['dex3'],
|
||||||
|
'dex4' => $raw['dex4'],
|
||||||
|
'dex5' => $raw['dex5'],
|
||||||
|
'defence_train' => $raw['defence_train'],
|
||||||
|
'recent_war' => $raw['recent_war'] ?? null,
|
||||||
|
'warnum' => $general->getRankVar(RankColumn::warnum, 0),
|
||||||
|
'killnum' => $general->getRankVar(RankColumn::killnum, 0),
|
||||||
|
'killcrew' => $general->getRankVar(RankColumn::killcrew, 0),
|
||||||
|
];
|
||||||
|
if ($inheritBuff !== null) {
|
||||||
|
$fixture['inheritBuff'] = $inheritBuff;
|
||||||
|
}
|
||||||
|
return $fixture;
|
||||||
|
}
|
||||||
|
|
||||||
|
function comparisonBuildBattleFixture(
|
||||||
|
string $seed,
|
||||||
|
int $year,
|
||||||
|
int $month,
|
||||||
|
int $startYear,
|
||||||
|
General $attacker,
|
||||||
|
array $attackerNation,
|
||||||
|
array $defenders,
|
||||||
|
array $defenderCity,
|
||||||
|
array $defenderNation,
|
||||||
|
): array {
|
||||||
|
return [
|
||||||
|
'action' => 'battle',
|
||||||
|
'seed' => $seed,
|
||||||
|
'repeatCnt' => 1,
|
||||||
|
'year' => $year,
|
||||||
|
'month' => $month,
|
||||||
|
'startYear' => $startYear,
|
||||||
|
'scenarioEffect' => GameConst::$scenarioEffect,
|
||||||
|
'attackerGeneral' => comparisonBuildBattleGeneralFixture($attacker),
|
||||||
|
'attackerCity' => $attacker->getRawCity(),
|
||||||
|
'attackerNation' => $attackerNation,
|
||||||
|
// General::getGeneralList() can retain IDs as array keys. JSON would
|
||||||
|
// then encode the collection as an object, while both simulators use
|
||||||
|
// an ordered list. Keep the legacy iteration order but discard keys.
|
||||||
|
'defenderGenerals' => array_values(array_map(
|
||||||
|
static fn(General $general): array => comparisonBuildBattleGeneralFixture($general),
|
||||||
|
$defenders,
|
||||||
|
)),
|
||||||
|
'defenderCity' => $defenderCity,
|
||||||
|
'defenderNation' => $defenderNation,
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<?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';
|
||||||
|
|
||||||
|
$rows = DB::db()->query(
|
||||||
|
'SELECT general_id, log_type, year, month, text
|
||||||
|
FROM general_record
|
||||||
|
WHERE log_type IN ("battle_brief", "battle")
|
||||||
|
ORDER BY id'
|
||||||
|
);
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'schemaVersion' => 1,
|
||||||
|
'engine' => 'ref',
|
||||||
|
'logs' => array_map(static fn(array $row): array => [
|
||||||
|
'generalId' => (int)$row['general_id'],
|
||||||
|
'category' => $row['log_type'] === 'battle_brief' ? 'BATTLE_BRIEF' : 'BATTLE_DETAIL',
|
||||||
|
'year' => (int)$row['year'],
|
||||||
|
'month' => (int)$row['month'],
|
||||||
|
'text' => (string)$row['text'],
|
||||||
|
], $rows),
|
||||||
|
], JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), PHP_EOL;
|
||||||
@@ -0,0 +1,314 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace sammo;
|
||||||
|
|
||||||
|
use Ds\Map;
|
||||||
|
use sammo\Enums\RankColumn;
|
||||||
|
|
||||||
|
if (PHP_SAPI !== 'cli') {
|
||||||
|
http_response_code(404);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
chdir(dirname(__DIR__));
|
||||||
|
require_once 'lib.php';
|
||||||
|
require_once 'func.php';
|
||||||
|
|
||||||
|
final class ComparisonTracingRNG implements RNG
|
||||||
|
{
|
||||||
|
private int $sequence = 0;
|
||||||
|
|
||||||
|
/** @var array<int, array<string, mixed>> */
|
||||||
|
public array $calls = [];
|
||||||
|
|
||||||
|
public function __construct(private readonly RNG $inner)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getMaxInt(): int
|
||||||
|
{
|
||||||
|
return LiteHashDRBG::getMaxInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function nextBytes(int $bytes): string
|
||||||
|
{
|
||||||
|
$value = $this->inner->nextBytes($bytes);
|
||||||
|
$this->record('nextBytes', ['bytes' => $bytes], bin2hex($value));
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function nextBits(int $bits): string
|
||||||
|
{
|
||||||
|
$value = $this->inner->nextBits($bits);
|
||||||
|
$this->record('nextBits', ['bits' => $bits], bin2hex($value));
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function nextInt(?int $max = null): int
|
||||||
|
{
|
||||||
|
$value = $this->inner->nextInt($max);
|
||||||
|
$this->record('nextInt', ['maxInclusive' => $max], $value);
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function nextFloat1(): float
|
||||||
|
{
|
||||||
|
$value = $this->inner->nextFloat1();
|
||||||
|
$this->record('nextFloat1', [], $value);
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function record(string $operation, array $arguments, mixed $result): void
|
||||||
|
{
|
||||||
|
$this->calls[] = [
|
||||||
|
'seq' => $this->sequence++,
|
||||||
|
'operation' => $operation,
|
||||||
|
'arguments' => $arguments,
|
||||||
|
'result' => $result,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class ComparisonTracingRandUtil extends RandUtil
|
||||||
|
{
|
||||||
|
/** @var array<int, array{rngSeq: int, probability: int|float, result: bool}> */
|
||||||
|
public array $boolCalls = [];
|
||||||
|
|
||||||
|
public function __construct(private readonly ComparisonTracingRNG $tracingRng)
|
||||||
|
{
|
||||||
|
parent::__construct($tracingRng);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function nextBool(int|float $prob = 0.5): bool
|
||||||
|
{
|
||||||
|
$rngSeq = count($this->tracingRng->calls);
|
||||||
|
$result = parent::nextBool($prob);
|
||||||
|
$this->boolCalls[] = [
|
||||||
|
'rngSeq' => $rngSeq,
|
||||||
|
'probability' => $prob,
|
||||||
|
'result' => $result,
|
||||||
|
];
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class ComparisonGeneral extends General
|
||||||
|
{
|
||||||
|
public function getTurnTime(int $short = self::TURNTIME_FULL_MS): ?string
|
||||||
|
{
|
||||||
|
$formatted = (string)($this->getRaw()['turntime'] ?? '1970-01-01 00:00:00');
|
||||||
|
return match ($short) {
|
||||||
|
self::TURNTIME_FULL_MS => $formatted,
|
||||||
|
self::TURNTIME_FULL => substr($formatted, 0, 19),
|
||||||
|
self::TURNTIME_HMS => substr($formatted, 11, 8),
|
||||||
|
self::TURNTIME_HM => substr($formatted, 11, 5),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTurnTick(): ?int
|
||||||
|
{
|
||||||
|
return (int)($this->getRaw()['turntick'] ?? 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function comparisonExtractRankVar(array $raw): Map
|
||||||
|
{
|
||||||
|
$rankVars = new Map();
|
||||||
|
foreach ($raw as $rawKey => $rawValue) {
|
||||||
|
$key = RankColumn::tryFrom($rawKey);
|
||||||
|
if ($key !== null) {
|
||||||
|
$rankVars[$key] = $rawValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $rankVars;
|
||||||
|
}
|
||||||
|
|
||||||
|
function comparisonBuildGeneral(
|
||||||
|
array $raw,
|
||||||
|
array $city,
|
||||||
|
array $nation,
|
||||||
|
int $year,
|
||||||
|
int $month,
|
||||||
|
bool $defender = false,
|
||||||
|
): General {
|
||||||
|
$aux = [];
|
||||||
|
if (array_key_exists('inheritBuff', $raw)) {
|
||||||
|
$aux['inheritBuff'] = $raw['inheritBuff'];
|
||||||
|
}
|
||||||
|
$raw['aux'] = Json::encode($aux);
|
||||||
|
$raw['owner'] = 0;
|
||||||
|
return new ComparisonGeneral(
|
||||||
|
$raw,
|
||||||
|
comparisonExtractRankVar($raw),
|
||||||
|
null,
|
||||||
|
$city,
|
||||||
|
$nation,
|
||||||
|
$year,
|
||||||
|
$month,
|
||||||
|
$defender,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function comparisonRunBattle(array $fixture): array
|
||||||
|
{
|
||||||
|
$seed = (string)($fixture['seed'] ?? 'battle-differential');
|
||||||
|
$year = (int)$fixture['year'];
|
||||||
|
$month = (int)$fixture['month'];
|
||||||
|
$startYear = (int)($fixture['startYear'] ?? 180);
|
||||||
|
$rawAttacker = $fixture['attackerGeneral'];
|
||||||
|
$rawAttackerCity = $fixture['attackerCity'];
|
||||||
|
$rawAttackerNation = $fixture['attackerNation'];
|
||||||
|
$rawDefenderCity = $fixture['defenderCity'];
|
||||||
|
$rawDefenderNation = $fixture['defenderNation'];
|
||||||
|
// 비교 fixture에 명시된 경우에만 ref runtime의 scenario action chain을
|
||||||
|
// 활성화합니다. 일반 비교 fixture의 기존 동작은 그대로 유지합니다.
|
||||||
|
$scenarioEffect = $fixture['scenarioEffect'] ?? null;
|
||||||
|
if ($scenarioEffect === '' || $scenarioEffect === 'None') {
|
||||||
|
$scenarioEffect = null;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
$scenarioEffect !== null
|
||||||
|
&& (!is_string($scenarioEffect) || getScenarioEffectClass($scenarioEffect) === null)
|
||||||
|
) {
|
||||||
|
throw new \InvalidArgumentException('scenarioEffect is invalid');
|
||||||
|
}
|
||||||
|
GameConst::$scenarioEffect = $scenarioEffect;
|
||||||
|
|
||||||
|
$tracingRng = new ComparisonTracingRNG(new LiteHashDRBG($seed));
|
||||||
|
$warRng = new ComparisonTracingRandUtil($tracingRng);
|
||||||
|
$attacker = new WarUnitGeneral(
|
||||||
|
$warRng,
|
||||||
|
comparisonBuildGeneral($rawAttacker, $rawAttackerCity, $rawAttackerNation, $year, $month),
|
||||||
|
$rawAttackerNation,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
$city = new WarUnitCity($warRng, $rawDefenderCity, $rawDefenderNation, $year, $month, $startYear);
|
||||||
|
|
||||||
|
$defenderList = [];
|
||||||
|
foreach ($fixture['defenderGenerals'] as $rawDefender) {
|
||||||
|
$defenderList[] = new WarUnitGeneral(
|
||||||
|
$warRng,
|
||||||
|
comparisonBuildGeneral($rawDefender, $rawDefenderCity, $rawDefenderNation, $year, $month, true),
|
||||||
|
$rawDefenderNation,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (count($defenderList) && extractBattleOrder($city, $attacker) > 0) {
|
||||||
|
$defenderList[] = $city;
|
||||||
|
}
|
||||||
|
$summarizeDefenderOrder = static fn(WarUnit $unit): array => [
|
||||||
|
'id' => $unit instanceof WarUnitGeneral ? $unit->getGeneral()->getID() : 0,
|
||||||
|
'order' => extractBattleOrder($unit, $attacker),
|
||||||
|
];
|
||||||
|
$defenderOrderBeforeSort = array_map($summarizeDefenderOrder, $defenderList);
|
||||||
|
usort(
|
||||||
|
$defenderList,
|
||||||
|
fn(WarUnit $lhs, WarUnit $rhs): int =>
|
||||||
|
-(extractBattleOrder($lhs, $attacker) <=> extractBattleOrder($rhs, $attacker)),
|
||||||
|
);
|
||||||
|
$defenderOrderAfterSort = array_map($summarizeDefenderOrder, $defenderList);
|
||||||
|
|
||||||
|
$iterDefender = new \ArrayIterator($defenderList);
|
||||||
|
$iterDefender->rewind();
|
||||||
|
$finishedDefenders = [];
|
||||||
|
$getNextDefender = function (?WarUnit $previous, bool $requestNext) use (
|
||||||
|
$iterDefender,
|
||||||
|
$attacker,
|
||||||
|
&$finishedDefenders,
|
||||||
|
): ?WarUnit {
|
||||||
|
if ($previous !== null) {
|
||||||
|
$finishedDefenders[] = buildWarTraceUnitSnapshot($previous);
|
||||||
|
}
|
||||||
|
if (!$requestNext || !$iterDefender->valid()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$next = $iterDefender->current();
|
||||||
|
if (extractBattleOrder($next, $attacker) <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$iterDefender->next();
|
||||||
|
return $next;
|
||||||
|
};
|
||||||
|
|
||||||
|
$events = [];
|
||||||
|
$conquered = processWar_NG(
|
||||||
|
$seed,
|
||||||
|
$attacker,
|
||||||
|
$getNextDefender,
|
||||||
|
$city,
|
||||||
|
static function (array $event) use (&$events): void {
|
||||||
|
$events[] = $event;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
$attackerLogs = $attacker->getLogger()->rollback();
|
||||||
|
$defenderLogs = [];
|
||||||
|
foreach ($defenderList as $defenderUnit) {
|
||||||
|
if (!($defenderUnit instanceof WarUnitGeneral)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$snapshot = buildWarTraceUnitSnapshot($defenderUnit);
|
||||||
|
$defenderLogs[(string)$snapshot['id']] = $defenderUnit->getLogger()->rollback();
|
||||||
|
}
|
||||||
|
$cityLogs = $city->getLogger()->rollback();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'engine' => 'ref',
|
||||||
|
'seed' => $seed,
|
||||||
|
'conquered' => $conquered,
|
||||||
|
'attacker' => buildWarTraceUnitSnapshot($attacker),
|
||||||
|
'city' => buildWarTraceUnitSnapshot($city),
|
||||||
|
'finishedDefenders' => $finishedDefenders,
|
||||||
|
'defenderOrder' => [
|
||||||
|
'before' => $defenderOrderBeforeSort,
|
||||||
|
'after' => $defenderOrderAfterSort,
|
||||||
|
],
|
||||||
|
'events' => $events,
|
||||||
|
'rng' => $tracingRng->calls,
|
||||||
|
'boolRng' => $warRng->boolCalls,
|
||||||
|
'logs' => [
|
||||||
|
'attacker' => $attackerLogs,
|
||||||
|
'defenders' => $defenderLogs,
|
||||||
|
'city' => $cityLogs,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$fixturePath = $argv[1] ?? null;
|
||||||
|
if ($fixturePath === '--jsonl') {
|
||||||
|
try {
|
||||||
|
while (($line = fgets(STDIN)) !== false) {
|
||||||
|
$line = trim($line);
|
||||||
|
if ($line === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$fixture = json_decode($line, true, flags: JSON_THROW_ON_ERROR);
|
||||||
|
echo json_encode(
|
||||||
|
comparisonRunBattle($fixture),
|
||||||
|
JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION,
|
||||||
|
), PHP_EOL;
|
||||||
|
}
|
||||||
|
exit(0);
|
||||||
|
} catch (\Throwable $throwable) {
|
||||||
|
fwrite(STDERR, $throwable::class . ': ' . $throwable->getMessage() . PHP_EOL);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($fixturePath === null || ($fixturePath !== '-' && !is_file($fixturePath))) {
|
||||||
|
fwrite(STDERR, "usage: php compare/battle_trace.php <fixture.json|-|--jsonl>\n");
|
||||||
|
exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$fixtureJson = $fixturePath === '-' ? stream_get_contents(STDIN) : file_get_contents($fixturePath);
|
||||||
|
$fixture = json_decode((string)$fixtureJson, true, flags: JSON_THROW_ON_ERROR);
|
||||||
|
echo json_encode(
|
||||||
|
comparisonRunBattle($fixture),
|
||||||
|
JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION,
|
||||||
|
), PHP_EOL;
|
||||||
|
} catch (\Throwable $throwable) {
|
||||||
|
fwrite(STDERR, $throwable::class . ': ' . $throwable->getMessage() . PHP_EOL);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"action": "AddGlobalBetray",
|
||||||
|
"args": [2, 1],
|
||||||
|
"setup": {
|
||||||
|
"resetGenerals": true,
|
||||||
|
"deleteOtherGenerals": true,
|
||||||
|
"keepGeneralIds": [1, 2, 3],
|
||||||
|
"general": [
|
||||||
|
{ "id": 1, "values": { "name": "배신0", "betray": 0 } },
|
||||||
|
{ "id": 2, "values": { "name": "배신1", "betray": 1 } },
|
||||||
|
{ "id": 3, "values": { "name": "배신2", "betray": 2 } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"environment": {
|
||||||
|
"year": 194,
|
||||||
|
"month": 1,
|
||||||
|
"startyear": 190
|
||||||
|
},
|
||||||
|
"observe": {
|
||||||
|
"generalIds": [1, 2, 3]
|
||||||
|
},
|
||||||
|
"compactOutput": true
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
{
|
||||||
|
"action": "AssignGeneralSpeciality",
|
||||||
|
"setup": {
|
||||||
|
"resetGenerals": true,
|
||||||
|
"deleteOtherGenerals": true,
|
||||||
|
"keepGeneralIds": [1, 2, 3],
|
||||||
|
"general": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"values": {
|
||||||
|
"name": "내정대상",
|
||||||
|
"nation": 1,
|
||||||
|
"age": 30,
|
||||||
|
"leadership": 40,
|
||||||
|
"strength": 45,
|
||||||
|
"intel": 80,
|
||||||
|
"special": "None",
|
||||||
|
"specage": 30,
|
||||||
|
"special2": "che_신산",
|
||||||
|
"specage2": 99,
|
||||||
|
"aux": {
|
||||||
|
"prev_types_special": ["che_경작"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"values": {
|
||||||
|
"name": "전투대상",
|
||||||
|
"nation": 1,
|
||||||
|
"age": 30,
|
||||||
|
"leadership": 80,
|
||||||
|
"strength": 75,
|
||||||
|
"intel": 40,
|
||||||
|
"special": "che_인덕",
|
||||||
|
"specage": 99,
|
||||||
|
"special2": "None",
|
||||||
|
"specage2": 30,
|
||||||
|
"dex1": 200,
|
||||||
|
"dex2": 10,
|
||||||
|
"dex3": 10,
|
||||||
|
"dex4": 10,
|
||||||
|
"dex5": 10,
|
||||||
|
"aux": {
|
||||||
|
"prev_types_special2": ["che_돌격"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"values": {
|
||||||
|
"name": "계승대상",
|
||||||
|
"nation": 2,
|
||||||
|
"age": 30,
|
||||||
|
"leadership": 50,
|
||||||
|
"strength": 50,
|
||||||
|
"intel": 50,
|
||||||
|
"special": "che_경작",
|
||||||
|
"specage": 99,
|
||||||
|
"special2": "None",
|
||||||
|
"specage2": 30,
|
||||||
|
"aux": {
|
||||||
|
"inheritSpecificSpecialWar": "che_의술",
|
||||||
|
"marker": 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"environment": {
|
||||||
|
"year": 200,
|
||||||
|
"month": 1,
|
||||||
|
"startyear": 190
|
||||||
|
},
|
||||||
|
"observe": {
|
||||||
|
"generalIds": [1, 2, 3]
|
||||||
|
},
|
||||||
|
"compactOutput": true
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
{
|
||||||
|
"action": "AutoDeleteInvader",
|
||||||
|
"compactOutput": true,
|
||||||
|
"args": [1],
|
||||||
|
"setup": {
|
||||||
|
"resetGenerals": true,
|
||||||
|
"deleteOtherGenerals": true,
|
||||||
|
"keepGeneralIds": [1],
|
||||||
|
"resetNations": true,
|
||||||
|
"deleteOtherNations": true,
|
||||||
|
"keepNationIds": [1],
|
||||||
|
"resetDiplomacy": true,
|
||||||
|
"nation": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"values": {
|
||||||
|
"name": "ⓞ남만족",
|
||||||
|
"capital": 69,
|
||||||
|
"gennum": 1,
|
||||||
|
"tech": 100,
|
||||||
|
"level": 2,
|
||||||
|
"type": "che_병가"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"general": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"values": {
|
||||||
|
"name": "ⓞ남만대왕",
|
||||||
|
"nation": 1,
|
||||||
|
"city": 69,
|
||||||
|
"officer_level": 12,
|
||||||
|
"npc": 9
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"id": 990001,
|
||||||
|
"action": [["AutoDeleteInvader", 1]]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"environment": {
|
||||||
|
"year": 200,
|
||||||
|
"month": 1,
|
||||||
|
"startyear": 190,
|
||||||
|
"currentEventID": 990001
|
||||||
|
},
|
||||||
|
"observe": {
|
||||||
|
"generalIds": [1]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"action": "BlockScoutAction",
|
||||||
|
"args": [true],
|
||||||
|
"setup": {
|
||||||
|
"resetNations": true,
|
||||||
|
"deleteOtherNations": true,
|
||||||
|
"keepNationIds": [1, 2],
|
||||||
|
"nation": [
|
||||||
|
{ "id": 1, "values": { "name": "위", "level": 2, "scout": 0 } },
|
||||||
|
{ "id": 2, "values": { "name": "촉", "level": 2, "scout": 0 } }
|
||||||
|
],
|
||||||
|
"gameEnvironment": {
|
||||||
|
"block_change_scout": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"environment": {
|
||||||
|
"year": 200,
|
||||||
|
"month": 1,
|
||||||
|
"startyear": 190
|
||||||
|
},
|
||||||
|
"observe": {
|
||||||
|
"nationIds": [1, 2]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
{
|
||||||
|
"action": "MonthlyBoundary",
|
||||||
|
"compactOutput": true,
|
||||||
|
"environment": {
|
||||||
|
"year": 190,
|
||||||
|
"month": 12,
|
||||||
|
"startyear": 190,
|
||||||
|
"turnterm": 10,
|
||||||
|
"starttime": "2026-07-25 00:00:00",
|
||||||
|
"turntime": "2026-07-25 02:00:00"
|
||||||
|
},
|
||||||
|
"setup": {
|
||||||
|
"syncEnvironment": true,
|
||||||
|
"world": {
|
||||||
|
"hiddenSeed": "monthly-boundary-core-catalog"
|
||||||
|
},
|
||||||
|
"gameEnvironment": {
|
||||||
|
"isunited": 1,
|
||||||
|
"refreshLimit": 3,
|
||||||
|
"block_change_scout": false
|
||||||
|
},
|
||||||
|
"resetCities": true,
|
||||||
|
"deleteOtherCities": true,
|
||||||
|
"keepCityIds": [1, 2],
|
||||||
|
"city": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"values": {
|
||||||
|
"name": "경계도시",
|
||||||
|
"level": 1,
|
||||||
|
"nation": 1,
|
||||||
|
"pop": 10000,
|
||||||
|
"pop_max": 20000,
|
||||||
|
"agri": 1000,
|
||||||
|
"agri_max": 2000,
|
||||||
|
"comm": 1000,
|
||||||
|
"comm_max": 2000,
|
||||||
|
"secu": 1000,
|
||||||
|
"secu_max": 2000,
|
||||||
|
"trust": 50,
|
||||||
|
"trade": 100,
|
||||||
|
"def": 1000,
|
||||||
|
"def_max": 2000,
|
||||||
|
"wall": 1000,
|
||||||
|
"wall_max": 2000,
|
||||||
|
"officer_set": 1,
|
||||||
|
"state": 31,
|
||||||
|
"term": 1,
|
||||||
|
"supply": 1,
|
||||||
|
"front": 2,
|
||||||
|
"conflict": {},
|
||||||
|
"dead": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"values": {
|
||||||
|
"name": "중립도시",
|
||||||
|
"level": 8,
|
||||||
|
"nation": 0,
|
||||||
|
"pop": 12000,
|
||||||
|
"pop_max": 22000,
|
||||||
|
"agri": 1200,
|
||||||
|
"agri_max": 2200,
|
||||||
|
"comm": 1200,
|
||||||
|
"comm_max": 2200,
|
||||||
|
"secu": 1200,
|
||||||
|
"secu_max": 2200,
|
||||||
|
"trust": 55,
|
||||||
|
"trade": 100,
|
||||||
|
"def": 1200,
|
||||||
|
"def_max": 2200,
|
||||||
|
"wall": 1200,
|
||||||
|
"wall_max": 2200,
|
||||||
|
"officer_set": 1,
|
||||||
|
"state": 0,
|
||||||
|
"term": 0,
|
||||||
|
"supply": 0,
|
||||||
|
"front": 0,
|
||||||
|
"conflict": {},
|
||||||
|
"dead": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"resetGenerals": true,
|
||||||
|
"deleteOtherGenerals": true,
|
||||||
|
"keepGeneralIds": [1, 2],
|
||||||
|
"general": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"values": {
|
||||||
|
"name": "경계군주",
|
||||||
|
"nation": 1,
|
||||||
|
"city": 1,
|
||||||
|
"officer_level": 12,
|
||||||
|
"npc": 0,
|
||||||
|
"age": 30,
|
||||||
|
"belong": 3,
|
||||||
|
"makelimit": 2,
|
||||||
|
"gold": 1000,
|
||||||
|
"rice": 1000,
|
||||||
|
"crew": 100,
|
||||||
|
"train": 80,
|
||||||
|
"atmos": 70,
|
||||||
|
"leadership": 50,
|
||||||
|
"strength": 50,
|
||||||
|
"intel": 50,
|
||||||
|
"experience": 0,
|
||||||
|
"dedication": 0,
|
||||||
|
"dex1": 0,
|
||||||
|
"dex2": 0,
|
||||||
|
"dex3": 0,
|
||||||
|
"dex4": 0,
|
||||||
|
"dex5": 0,
|
||||||
|
"personal": "None",
|
||||||
|
"special": "None",
|
||||||
|
"specage": 0,
|
||||||
|
"special2": "None",
|
||||||
|
"specage2": 0,
|
||||||
|
"betray": 0,
|
||||||
|
"bornyear": 160,
|
||||||
|
"deadyear": 250
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"values": {
|
||||||
|
"name": "경계재야",
|
||||||
|
"nation": 0,
|
||||||
|
"city": 2,
|
||||||
|
"officer_level": 0,
|
||||||
|
"npc": 0,
|
||||||
|
"age": 25,
|
||||||
|
"belong": 4,
|
||||||
|
"makelimit": 0,
|
||||||
|
"gold": 500,
|
||||||
|
"rice": 500,
|
||||||
|
"crew": 50,
|
||||||
|
"train": 60,
|
||||||
|
"atmos": 50,
|
||||||
|
"leadership": 40,
|
||||||
|
"strength": 40,
|
||||||
|
"intel": 40,
|
||||||
|
"experience": 0,
|
||||||
|
"dedication": 0,
|
||||||
|
"dex1": 0,
|
||||||
|
"dex2": 0,
|
||||||
|
"dex3": 0,
|
||||||
|
"dex4": 0,
|
||||||
|
"dex5": 0,
|
||||||
|
"personal": "None",
|
||||||
|
"special": "None",
|
||||||
|
"specage": 0,
|
||||||
|
"special2": "None",
|
||||||
|
"specage2": 0,
|
||||||
|
"betray": 0,
|
||||||
|
"bornyear": 165,
|
||||||
|
"deadyear": 250
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"resetNations": true,
|
||||||
|
"deleteOtherNations": true,
|
||||||
|
"keepNationIds": [1],
|
||||||
|
"nation": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"values": {
|
||||||
|
"name": "경계국",
|
||||||
|
"color": "#123456",
|
||||||
|
"capital": 1,
|
||||||
|
"gennum": 1,
|
||||||
|
"gold": 10000,
|
||||||
|
"rice": 20000,
|
||||||
|
"tech": 100,
|
||||||
|
"power": 10,
|
||||||
|
"level": 2,
|
||||||
|
"type": "che_유가",
|
||||||
|
"bill": 100,
|
||||||
|
"rate": 20,
|
||||||
|
"rate_tmp": 10,
|
||||||
|
"scout": 1,
|
||||||
|
"chief_set": 1,
|
||||||
|
"strategic_cmd_limit": 2,
|
||||||
|
"surlimit": 1,
|
||||||
|
"spy": "{}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"resetDiplomacy": true,
|
||||||
|
"resetEvents": true,
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"id": 991001,
|
||||||
|
"target": "PRE_MONTH",
|
||||||
|
"priority": 1000,
|
||||||
|
"condition": true,
|
||||||
|
"action": [
|
||||||
|
["NoticeToHistoryLog", "aggregate-pre", 6]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 991002,
|
||||||
|
"target": "MONTH",
|
||||||
|
"priority": 1000,
|
||||||
|
"condition": true,
|
||||||
|
"action": [
|
||||||
|
["ProcessIncome", "gold"],
|
||||||
|
["NewYear"],
|
||||||
|
["ResetOfficerLock"],
|
||||||
|
["RandomizeCityTradeRate"],
|
||||||
|
["ChangeCity", "all", {"pop": "+10", "trust": 60}],
|
||||||
|
["NoticeToHistoryLog", "aggregate-month", 6],
|
||||||
|
["DeleteEvent"]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"observe": {
|
||||||
|
"generalIds": [1, 2],
|
||||||
|
"nationIds": [1],
|
||||||
|
"cityIds": [1, 2],
|
||||||
|
"eventIds": [991001, 991002],
|
||||||
|
"accessGeneralIds": [1, 2]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"action": "NewYear",
|
||||||
|
"environment": {
|
||||||
|
"year": 191,
|
||||||
|
"month": 1,
|
||||||
|
"startyear": 190
|
||||||
|
},
|
||||||
|
"setup": {
|
||||||
|
"resetGenerals": true,
|
||||||
|
"deleteOtherGenerals": true,
|
||||||
|
"keepGeneralIds": [1, 2],
|
||||||
|
"general": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"values": {
|
||||||
|
"nation": 1,
|
||||||
|
"age": 30,
|
||||||
|
"belong": 3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"values": {
|
||||||
|
"nation": 0,
|
||||||
|
"age": 30,
|
||||||
|
"belong": 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"observe": {
|
||||||
|
"generalIds": [1, 2]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"action": "NoticeToHistoryLog",
|
||||||
|
"args": ["<S>새해 알림</>", 6],
|
||||||
|
"environment": {
|
||||||
|
"year": 191,
|
||||||
|
"month": 1,
|
||||||
|
"startyear": 190
|
||||||
|
},
|
||||||
|
"setup": {},
|
||||||
|
"observe": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
{
|
||||||
|
"action": "ProcessIncome",
|
||||||
|
"args": ["gold"],
|
||||||
|
"environment": {
|
||||||
|
"year": 190,
|
||||||
|
"month": 7,
|
||||||
|
"startyear": 190
|
||||||
|
},
|
||||||
|
"setup": {
|
||||||
|
"resetCities": true,
|
||||||
|
"resetGenerals": true,
|
||||||
|
"deleteOtherGenerals": true,
|
||||||
|
"keepGeneralIds": [1],
|
||||||
|
"resetNations": true,
|
||||||
|
"deleteOtherNations": true,
|
||||||
|
"keepNationIds": [1],
|
||||||
|
"general": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"values": {
|
||||||
|
"nation": 1,
|
||||||
|
"city": 1,
|
||||||
|
"officer_level": 5,
|
||||||
|
"npc": 0,
|
||||||
|
"gold": 1000,
|
||||||
|
"rice": 1000,
|
||||||
|
"dedication": 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nation": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"values": {
|
||||||
|
"name": "갑국",
|
||||||
|
"capital": 1,
|
||||||
|
"gold": 10000,
|
||||||
|
"rice": 20000,
|
||||||
|
"level": 1,
|
||||||
|
"rate_tmp": 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"city": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"values": {
|
||||||
|
"name": "갑성",
|
||||||
|
"nation": 1,
|
||||||
|
"level": 4,
|
||||||
|
"supply": 1,
|
||||||
|
"pop": 10000,
|
||||||
|
"pop_max": 20000,
|
||||||
|
"agri": 1000,
|
||||||
|
"agri_max": 2000,
|
||||||
|
"comm": 1000,
|
||||||
|
"comm_max": 2000,
|
||||||
|
"secu": 1000,
|
||||||
|
"secu_max": 2000,
|
||||||
|
"trust": 80,
|
||||||
|
"def": 500,
|
||||||
|
"def_max": 1000,
|
||||||
|
"wall": 500,
|
||||||
|
"wall_max": 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"observe": {
|
||||||
|
"generalIds": [1],
|
||||||
|
"nationIds": [1],
|
||||||
|
"cityIds": [1],
|
||||||
|
"nationEnvironmentKeys": ["prev_income_gold", "prev_income_rice"]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"action": "ResetOfficerLock",
|
||||||
|
"environment": {
|
||||||
|
"year": 191,
|
||||||
|
"month": 1,
|
||||||
|
"startyear": 190
|
||||||
|
},
|
||||||
|
"setup": {
|
||||||
|
"resetCities": true,
|
||||||
|
"resetNations": true,
|
||||||
|
"city": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"values": {
|
||||||
|
"nation": 1,
|
||||||
|
"officer_set": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nation": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"values": {
|
||||||
|
"chief_set": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"observe": {
|
||||||
|
"cityIds": [1],
|
||||||
|
"nationIds": [1]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"action": "CreateAdminNPC",
|
||||||
|
"environment": {
|
||||||
|
"year": 193,
|
||||||
|
"month": 12,
|
||||||
|
"startyear": 190
|
||||||
|
},
|
||||||
|
"observe": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"action": "CreateManyNPC",
|
||||||
|
"args": [2, 0],
|
||||||
|
"environment": {
|
||||||
|
"year": 193,
|
||||||
|
"month": 5,
|
||||||
|
"startyear": 190,
|
||||||
|
"turnterm": 10,
|
||||||
|
"turntime": "0193-05-01 00:00:00"
|
||||||
|
},
|
||||||
|
"setup": {
|
||||||
|
"resetGenerals": true
|
||||||
|
},
|
||||||
|
"observe": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
{
|
||||||
|
"action": "FinishNationBetting",
|
||||||
|
"args": [1],
|
||||||
|
"setup": {
|
||||||
|
"clearBetting": true,
|
||||||
|
"resetNations": true,
|
||||||
|
"deleteOtherNations": true,
|
||||||
|
"keepNationIds": [1, 2],
|
||||||
|
"nation": [
|
||||||
|
{ "id": 1, "values": { "name": "위", "gennum": 1, "power": 100, "level": 2 } },
|
||||||
|
{ "id": 2, "values": { "name": "촉", "gennum": 1, "power": 300, "level": 0 } }
|
||||||
|
],
|
||||||
|
"resetGenerals": true,
|
||||||
|
"deleteOtherGenerals": true,
|
||||||
|
"keepGeneralIds": [1, 2],
|
||||||
|
"general": [
|
||||||
|
{ "id": 1, "values": { "name": "장수1", "nation": 1, "owner": 1, "npc": 0 } },
|
||||||
|
{ "id": 2, "values": { "name": "장수2", "nation": 2, "owner": 2, "npc": 0 } }
|
||||||
|
],
|
||||||
|
"bettingInfo": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"type": "bettingNation",
|
||||||
|
"name": "천통국 예상",
|
||||||
|
"finished": false,
|
||||||
|
"selectCnt": 1,
|
||||||
|
"isExclusive": null,
|
||||||
|
"reqInheritancePoint": true,
|
||||||
|
"openYearMonth": 2400,
|
||||||
|
"closeYearMonth": 2424,
|
||||||
|
"candidates": [
|
||||||
|
{
|
||||||
|
"title": "위",
|
||||||
|
"info": "국력: 100<br>장수 수: 1<br>도시 수: 1",
|
||||||
|
"isHtml": true,
|
||||||
|
"aux": { "nation": 1 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "촉",
|
||||||
|
"info": "국력: 300<br>장수 수: 1<br>도시 수: 1",
|
||||||
|
"isHtml": true,
|
||||||
|
"aux": { "nation": 2 }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"winner": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"bet": [
|
||||||
|
{ "betting_id": 1, "general_id": 0, "user_id": null, "betting_type": "[-1]", "amount": 100 },
|
||||||
|
{ "betting_id": 1, "general_id": 1, "user_id": 1, "betting_type": "[0]", "amount": 100 },
|
||||||
|
{ "betting_id": 1, "general_id": 2, "user_id": 2, "betting_type": "[1]", "amount": 100 }
|
||||||
|
],
|
||||||
|
"inheritancePrevious": {
|
||||||
|
"1": 900,
|
||||||
|
"2": 900
|
||||||
|
},
|
||||||
|
"syncEnvironment": true
|
||||||
|
},
|
||||||
|
"environment": {
|
||||||
|
"year": 202,
|
||||||
|
"month": 1,
|
||||||
|
"startyear": 190
|
||||||
|
},
|
||||||
|
"observe": {
|
||||||
|
"generalIds": [1, 2],
|
||||||
|
"nationIds": [1, 2],
|
||||||
|
"ownerIds": [1, 2]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"action": "InvaderEnding",
|
||||||
|
"compactOutput": true,
|
||||||
|
"setup": {
|
||||||
|
"resetNations": true,
|
||||||
|
"deleteOtherNations": true,
|
||||||
|
"keepNationIds": [1],
|
||||||
|
"setAllCitiesNation": 1,
|
||||||
|
"nation": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"values": {
|
||||||
|
"name": "기준국",
|
||||||
|
"capital": 1,
|
||||||
|
"gennum": 1,
|
||||||
|
"tech": 100,
|
||||||
|
"level": 2,
|
||||||
|
"type": "che_유가"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"gameEnvironment": {
|
||||||
|
"isunited": 1,
|
||||||
|
"refreshLimit": 3
|
||||||
|
},
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"id": 990002,
|
||||||
|
"action": [["InvaderEnding"]]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"environment": {
|
||||||
|
"year": 200,
|
||||||
|
"month": 1,
|
||||||
|
"startyear": 190,
|
||||||
|
"currentEventID": 990002
|
||||||
|
},
|
||||||
|
"observe": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
{
|
||||||
|
"action": "InvaderLifecycle",
|
||||||
|
"args": [10, 150, 100, 20],
|
||||||
|
"setup": {
|
||||||
|
"resetCities": true,
|
||||||
|
"resetGenerals": true,
|
||||||
|
"deleteOtherGenerals": true,
|
||||||
|
"keepGeneralIds": [1],
|
||||||
|
"resetNations": true,
|
||||||
|
"deleteOtherNations": true,
|
||||||
|
"keepNationIds": [1],
|
||||||
|
"resetDiplomacy": true,
|
||||||
|
"resetEvents": true,
|
||||||
|
"city": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"values": {
|
||||||
|
"name": "기준도시",
|
||||||
|
"level": 3,
|
||||||
|
"nation": 1,
|
||||||
|
"pop": 1000,
|
||||||
|
"pop_max": 10000,
|
||||||
|
"agri": 2000,
|
||||||
|
"agri_max": 20000,
|
||||||
|
"comm": 3000,
|
||||||
|
"comm_max": 30000,
|
||||||
|
"secu": 4000,
|
||||||
|
"secu_max": 40000,
|
||||||
|
"def": 5000,
|
||||||
|
"def_max": 50000,
|
||||||
|
"wall": 6000,
|
||||||
|
"wall_max": 60000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 48,
|
||||||
|
"values": {
|
||||||
|
"name": "남만",
|
||||||
|
"level": 4,
|
||||||
|
"nation": 0,
|
||||||
|
"pop": 1000,
|
||||||
|
"pop_max": 10000,
|
||||||
|
"agri": 2000,
|
||||||
|
"agri_max": 20000,
|
||||||
|
"comm": 3000,
|
||||||
|
"comm_max": 30000,
|
||||||
|
"secu": 4000,
|
||||||
|
"secu_max": 40000,
|
||||||
|
"def": 5000,
|
||||||
|
"def_max": 50000,
|
||||||
|
"wall": 6000,
|
||||||
|
"wall_max": 60000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nation": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"values": {
|
||||||
|
"name": "기준국",
|
||||||
|
"capital": 1,
|
||||||
|
"gennum": 1,
|
||||||
|
"tech": 100,
|
||||||
|
"level": 2,
|
||||||
|
"type": "che_유가"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"general": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"values": {
|
||||||
|
"name": "군주",
|
||||||
|
"nation": 1,
|
||||||
|
"city": 1,
|
||||||
|
"officer_level": 12,
|
||||||
|
"npc": 0,
|
||||||
|
"leadership": 50,
|
||||||
|
"strength": 50,
|
||||||
|
"intel": 50,
|
||||||
|
"experience": 1000,
|
||||||
|
"dedication": 1000,
|
||||||
|
"dex1": 10,
|
||||||
|
"dex2": 20,
|
||||||
|
"dex3": 30,
|
||||||
|
"dex4": 40,
|
||||||
|
"dex5": 50,
|
||||||
|
"turntime": "0200-01-01 00:05:00"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"gameEnvironment": {
|
||||||
|
"isunited": 0,
|
||||||
|
"refreshLimit": 3
|
||||||
|
},
|
||||||
|
"syncEnvironment": true
|
||||||
|
},
|
||||||
|
"environment": {
|
||||||
|
"year": 200,
|
||||||
|
"month": 1,
|
||||||
|
"startyear": 190,
|
||||||
|
"turnterm": 10,
|
||||||
|
"turntime": "0200-01-01 00:00:00",
|
||||||
|
"show_img_level": 3,
|
||||||
|
"fiction": [0]
|
||||||
|
},
|
||||||
|
"observe": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"action": "LostUniqueItem",
|
||||||
|
"args": [0.2],
|
||||||
|
"environment": {
|
||||||
|
"year": 210,
|
||||||
|
"month": 1,
|
||||||
|
"startyear": 180
|
||||||
|
},
|
||||||
|
"setup": {
|
||||||
|
"syncEnvironment": true,
|
||||||
|
"resetGenerals": true,
|
||||||
|
"deleteOtherGenerals": true,
|
||||||
|
"keepGeneralIds": [1, 2],
|
||||||
|
"general": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"values": {
|
||||||
|
"name": "갑",
|
||||||
|
"npc": 0,
|
||||||
|
"horse": "che_명마_12_옥란백용구",
|
||||||
|
"weapon": "che_무기_15_의천검",
|
||||||
|
"book": "che_서적_01_효경전",
|
||||||
|
"item": "che_저지_삼황내문"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"values": {
|
||||||
|
"name": "을",
|
||||||
|
"npc": 1,
|
||||||
|
"horse": "che_명마_12_사륜거",
|
||||||
|
"weapon": "che_무기_01_단도",
|
||||||
|
"book": "None",
|
||||||
|
"item": "che_부적_태현청생부"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"observe": {
|
||||||
|
"generalIds": [1, 2]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
{
|
||||||
|
"action": "MergeInheritPointRank",
|
||||||
|
"environment": {
|
||||||
|
"year": 210,
|
||||||
|
"month": 1,
|
||||||
|
"startyear": 180
|
||||||
|
},
|
||||||
|
"setup": {
|
||||||
|
"resetGenerals": true,
|
||||||
|
"deleteOtherGenerals": true,
|
||||||
|
"keepGeneralIds": [1],
|
||||||
|
"clearInheritanceOwnerIds": [100],
|
||||||
|
"gameEnvironment": {
|
||||||
|
"isunited": 0
|
||||||
|
},
|
||||||
|
"general": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"values": {
|
||||||
|
"name": "갑",
|
||||||
|
"nation": 1,
|
||||||
|
"owner": 100,
|
||||||
|
"npc": 0,
|
||||||
|
"belong": 3,
|
||||||
|
"dex1": 1000,
|
||||||
|
"dex2": 0,
|
||||||
|
"dex3": 0,
|
||||||
|
"dex4": 0,
|
||||||
|
"dex5": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"inheritancePoints": [
|
||||||
|
{"ownerId": 100, "key": "previous", "value": 999},
|
||||||
|
{"ownerId": 100, "key": "lived_month", "value": 2},
|
||||||
|
{"ownerId": 100, "key": "max_domestic_critical", "value": 4},
|
||||||
|
{"ownerId": 100, "key": "active_action", "value": 15},
|
||||||
|
{"ownerId": 100, "key": "unifier", "value": 6},
|
||||||
|
{"ownerId": 100, "key": "tournament", "value": 7}
|
||||||
|
],
|
||||||
|
"rankData": [
|
||||||
|
{"generalId": 1, "type": "warnum", "value": 2},
|
||||||
|
{"generalId": 1, "type": "firenum", "value": 1},
|
||||||
|
{"generalId": 1, "type": "betwin", "value": 2},
|
||||||
|
{"generalId": 1, "type": "betgold", "value": 4000},
|
||||||
|
{"generalId": 1, "type": "betwingold", "value": 2000},
|
||||||
|
{"generalId": 1, "type": "inherit_earned_dyn", "value": 999},
|
||||||
|
{"generalId": 1, "type": "inherit_earned_act", "value": 11},
|
||||||
|
{"generalId": 1, "type": "inherit_earned", "value": 999},
|
||||||
|
{"generalId": 1, "type": "inherit_spent_dyn", "value": 9},
|
||||||
|
{"generalId": 1, "type": "inherit_spent", "value": 999}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"observe": {
|
||||||
|
"generalIds": [1],
|
||||||
|
"ownerIds": [100],
|
||||||
|
"inheritanceKeys": [
|
||||||
|
"previous",
|
||||||
|
"lived_month",
|
||||||
|
"max_domestic_critical",
|
||||||
|
"active_action",
|
||||||
|
"unifier",
|
||||||
|
"tournament"
|
||||||
|
],
|
||||||
|
"rankGeneralIds": [1],
|
||||||
|
"rankTypes": [
|
||||||
|
"inherit_earned",
|
||||||
|
"inherit_earned_act",
|
||||||
|
"inherit_earned_dyn",
|
||||||
|
"inherit_spent",
|
||||||
|
"inherit_spent_dyn"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
{
|
||||||
|
"action": "NationBettingLifecycle",
|
||||||
|
"args": [1, 100],
|
||||||
|
"bettorGeneralIds": [1, 2],
|
||||||
|
"bettorOwnerIds": [1, 2],
|
||||||
|
"setup": {
|
||||||
|
"clearBetting": true,
|
||||||
|
"resetNations": true,
|
||||||
|
"deleteOtherNations": true,
|
||||||
|
"keepNationIds": [1, 2],
|
||||||
|
"nation": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"values": {
|
||||||
|
"name": "위",
|
||||||
|
"color": "#111111",
|
||||||
|
"capital": 1,
|
||||||
|
"gennum": 1,
|
||||||
|
"gold": 1000,
|
||||||
|
"rice": 2000,
|
||||||
|
"tech": 100,
|
||||||
|
"power": 100,
|
||||||
|
"level": 2,
|
||||||
|
"type": "che_유가"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"values": {
|
||||||
|
"name": "촉",
|
||||||
|
"color": "#222222",
|
||||||
|
"capital": 2,
|
||||||
|
"gennum": 1,
|
||||||
|
"gold": 3000,
|
||||||
|
"rice": 4000,
|
||||||
|
"tech": 200,
|
||||||
|
"power": 300,
|
||||||
|
"level": 2,
|
||||||
|
"type": "che_병가"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"resetCities": true,
|
||||||
|
"deleteOtherCities": true,
|
||||||
|
"keepCityIds": [1, 2],
|
||||||
|
"city": [
|
||||||
|
{ "id": 1, "values": { "name": "허창", "nation": 1, "level": 3 } },
|
||||||
|
{ "id": 2, "values": { "name": "성도", "nation": 2, "level": 3 } }
|
||||||
|
],
|
||||||
|
"resetGenerals": true,
|
||||||
|
"deleteOtherGenerals": true,
|
||||||
|
"keepGeneralIds": [1, 2],
|
||||||
|
"general": [
|
||||||
|
{ "id": 1, "values": { "name": "장수1", "nation": 1, "city": 1, "owner": 1, "npc": 0 } },
|
||||||
|
{ "id": 2, "values": { "name": "장수2", "nation": 2, "city": 2, "owner": 2, "npc": 0 } }
|
||||||
|
],
|
||||||
|
"inheritancePrevious": {
|
||||||
|
"1": 1000,
|
||||||
|
"2": 1000
|
||||||
|
},
|
||||||
|
"rankData": [
|
||||||
|
{ "generalId": 1, "type": "inherit_spent_dyn", "value": 0 },
|
||||||
|
{ "generalId": 1, "type": "inherit_earned_act", "value": 0 },
|
||||||
|
{ "generalId": 2, "type": "inherit_spent_dyn", "value": 0 },
|
||||||
|
{ "generalId": 2, "type": "inherit_earned_act", "value": 0 }
|
||||||
|
],
|
||||||
|
"syncEnvironment": true
|
||||||
|
},
|
||||||
|
"environment": {
|
||||||
|
"year": 200,
|
||||||
|
"month": 1,
|
||||||
|
"startyear": 190
|
||||||
|
},
|
||||||
|
"observe": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
{
|
||||||
|
"action": "NpcNationLifecycle",
|
||||||
|
"setup": {
|
||||||
|
"resetCities": true,
|
||||||
|
"resetGenerals": true,
|
||||||
|
"resetNations": true,
|
||||||
|
"city": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"values": {
|
||||||
|
"name": "점유도시",
|
||||||
|
"level": 5,
|
||||||
|
"nation": 1,
|
||||||
|
"pop": 10001,
|
||||||
|
"pop_max": 50001,
|
||||||
|
"agri": 1001,
|
||||||
|
"agri_max": 5001,
|
||||||
|
"comm": 2001,
|
||||||
|
"comm_max": 6001,
|
||||||
|
"secu": 3001,
|
||||||
|
"secu_max": 7001,
|
||||||
|
"def": 4001,
|
||||||
|
"def_max": 8001,
|
||||||
|
"wall": 5001,
|
||||||
|
"wall_max": 9001
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 48,
|
||||||
|
"values": {
|
||||||
|
"name": "건국도시",
|
||||||
|
"level": 5,
|
||||||
|
"nation": 0,
|
||||||
|
"pop": 10048,
|
||||||
|
"pop_max": 50048,
|
||||||
|
"agri": 1048,
|
||||||
|
"agri_max": 5048,
|
||||||
|
"comm": 2048,
|
||||||
|
"comm_max": 6048,
|
||||||
|
"secu": 3048,
|
||||||
|
"secu_max": 7048,
|
||||||
|
"def": 4048,
|
||||||
|
"def_max": 8048,
|
||||||
|
"wall": 5048,
|
||||||
|
"wall_max": 9048
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nation": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"values": {
|
||||||
|
"name": "기준국",
|
||||||
|
"capital": 1,
|
||||||
|
"gennum": 0,
|
||||||
|
"tech": 120,
|
||||||
|
"level": 2,
|
||||||
|
"type": "che_유가"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"gameEnvironment": {
|
||||||
|
"lastNPCTroopLeaderID": 40
|
||||||
|
},
|
||||||
|
"syncEnvironment": true
|
||||||
|
},
|
||||||
|
"environment": {
|
||||||
|
"year": 200,
|
||||||
|
"month": 1,
|
||||||
|
"startyear": 190,
|
||||||
|
"turnterm": 10,
|
||||||
|
"turntime": "0200-01-01 00:00:00",
|
||||||
|
"show_img_level": 3,
|
||||||
|
"fiction": [0]
|
||||||
|
},
|
||||||
|
"observe": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
{
|
||||||
|
"action": "OpenNationBetting",
|
||||||
|
"args": [1, 500],
|
||||||
|
"setup": {
|
||||||
|
"clearBetting": true,
|
||||||
|
"resetNations": true,
|
||||||
|
"deleteOtherNations": true,
|
||||||
|
"keepNationIds": [1, 2],
|
||||||
|
"nation": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"values": {
|
||||||
|
"name": "위",
|
||||||
|
"color": "#111111",
|
||||||
|
"capital": 1,
|
||||||
|
"gennum": 1,
|
||||||
|
"gold": 1000,
|
||||||
|
"rice": 2000,
|
||||||
|
"tech": 100,
|
||||||
|
"power": 100,
|
||||||
|
"level": 2,
|
||||||
|
"type": "che_유가"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"values": {
|
||||||
|
"name": "촉",
|
||||||
|
"color": "#222222",
|
||||||
|
"capital": 2,
|
||||||
|
"gennum": 1,
|
||||||
|
"gold": 3000,
|
||||||
|
"rice": 4000,
|
||||||
|
"tech": 200,
|
||||||
|
"power": 300,
|
||||||
|
"level": 2,
|
||||||
|
"type": "che_병가"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"resetCities": true,
|
||||||
|
"city": [
|
||||||
|
{ "id": 1, "values": { "nation": 1, "level": 3 } },
|
||||||
|
{ "id": 2, "values": { "nation": 2, "level": 3 } }
|
||||||
|
],
|
||||||
|
"resetGenerals": true,
|
||||||
|
"deleteOtherGenerals": true,
|
||||||
|
"keepGeneralIds": [1, 2],
|
||||||
|
"general": [
|
||||||
|
{ "id": 1, "values": { "name": "장수1", "nation": 1, "city": 1, "owner": 1, "npc": 0 } },
|
||||||
|
{ "id": 2, "values": { "name": "장수2", "nation": 2, "city": 2, "owner": 2, "npc": 0 } }
|
||||||
|
],
|
||||||
|
"syncEnvironment": true
|
||||||
|
},
|
||||||
|
"environment": {
|
||||||
|
"year": 200,
|
||||||
|
"month": 1,
|
||||||
|
"startyear": 190
|
||||||
|
},
|
||||||
|
"observe": {
|
||||||
|
"generalIds": [1, 2],
|
||||||
|
"cityIds": [1, 2],
|
||||||
|
"nationIds": [1, 2]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
{
|
||||||
|
"action": "PostUpdateMonthly",
|
||||||
|
"environment": {
|
||||||
|
"year": 193,
|
||||||
|
"month": 2,
|
||||||
|
"startyear": 193,
|
||||||
|
"seedYear": 193,
|
||||||
|
"seedMonth": 1
|
||||||
|
},
|
||||||
|
"setup": {
|
||||||
|
"world": {
|
||||||
|
"hiddenSeed": "monthly-post-diplomacy-fixture"
|
||||||
|
},
|
||||||
|
"syncEnvironment": true,
|
||||||
|
"resetGenerals": true,
|
||||||
|
"deleteOtherGenerals": true,
|
||||||
|
"keepGeneralIds": [1, 2, 3, 4],
|
||||||
|
"resetNations": true,
|
||||||
|
"deleteOtherNations": true,
|
||||||
|
"keepNationIds": [1, 2, 3, 4],
|
||||||
|
"general": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"values": {
|
||||||
|
"name": "갑장",
|
||||||
|
"nation": 1,
|
||||||
|
"npc": 2,
|
||||||
|
"crew": 0,
|
||||||
|
"gold": 0,
|
||||||
|
"rice": 0,
|
||||||
|
"leadership": 10,
|
||||||
|
"strength": 10,
|
||||||
|
"intel": 10,
|
||||||
|
"experience": 0,
|
||||||
|
"dedication": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"values": {
|
||||||
|
"name": "을장",
|
||||||
|
"nation": 2,
|
||||||
|
"npc": 2,
|
||||||
|
"crew": 0,
|
||||||
|
"gold": 0,
|
||||||
|
"rice": 0,
|
||||||
|
"leadership": 10,
|
||||||
|
"strength": 10,
|
||||||
|
"intel": 10,
|
||||||
|
"experience": 0,
|
||||||
|
"dedication": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"values": {
|
||||||
|
"name": "병장",
|
||||||
|
"nation": 3,
|
||||||
|
"npc": 2,
|
||||||
|
"crew": 0,
|
||||||
|
"gold": 0,
|
||||||
|
"rice": 0,
|
||||||
|
"leadership": 10,
|
||||||
|
"strength": 10,
|
||||||
|
"intel": 10,
|
||||||
|
"experience": 0,
|
||||||
|
"dedication": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"values": {
|
||||||
|
"name": "정장",
|
||||||
|
"nation": 4,
|
||||||
|
"npc": 2,
|
||||||
|
"crew": 0,
|
||||||
|
"gold": 0,
|
||||||
|
"rice": 0,
|
||||||
|
"leadership": 10,
|
||||||
|
"strength": 10,
|
||||||
|
"intel": 10,
|
||||||
|
"experience": 0,
|
||||||
|
"dedication": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nation": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"values": {
|
||||||
|
"name": "갑국",
|
||||||
|
"gennum": 2,
|
||||||
|
"level": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"values": {
|
||||||
|
"name": "을국",
|
||||||
|
"gennum": 1,
|
||||||
|
"level": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"values": {
|
||||||
|
"name": "병국",
|
||||||
|
"gennum": 1,
|
||||||
|
"level": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"values": {
|
||||||
|
"name": "정국",
|
||||||
|
"gennum": 1,
|
||||||
|
"level": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"resetDiplomacy": true,
|
||||||
|
"diplomacy": [
|
||||||
|
{
|
||||||
|
"fromNationId": 1,
|
||||||
|
"toNationId": 2,
|
||||||
|
"state": 1,
|
||||||
|
"term": 1,
|
||||||
|
"dead": 777
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fromNationId": 2,
|
||||||
|
"toNationId": 1,
|
||||||
|
"state": 1,
|
||||||
|
"term": 1,
|
||||||
|
"dead": 888
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fromNationId": 1,
|
||||||
|
"toNationId": 3,
|
||||||
|
"state": 0,
|
||||||
|
"term": 5,
|
||||||
|
"dead": 250
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fromNationId": 3,
|
||||||
|
"toNationId": 1,
|
||||||
|
"state": 0,
|
||||||
|
"term": 5,
|
||||||
|
"dead": 50
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fromNationId": 3,
|
||||||
|
"toNationId": 4,
|
||||||
|
"state": 0,
|
||||||
|
"term": 1,
|
||||||
|
"dead": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fromNationId": 4,
|
||||||
|
"toNationId": 3,
|
||||||
|
"state": 0,
|
||||||
|
"term": 1,
|
||||||
|
"dead": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fromNationId": 2,
|
||||||
|
"toNationId": 4,
|
||||||
|
"state": 7,
|
||||||
|
"term": 1,
|
||||||
|
"dead": 999
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"observe": {
|
||||||
|
"generalIds": [1, 2, 3, 4],
|
||||||
|
"nationIds": [1, 2, 3, 4]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
{
|
||||||
|
"action": "PostUpdateMonthly",
|
||||||
|
"environment": {
|
||||||
|
"year": 193,
|
||||||
|
"month": 2,
|
||||||
|
"startyear": 193,
|
||||||
|
"seedYear": 193,
|
||||||
|
"seedMonth": 1
|
||||||
|
},
|
||||||
|
"setup": {
|
||||||
|
"world": {
|
||||||
|
"hiddenSeed": "monthly-post-nation-stats-fixture"
|
||||||
|
},
|
||||||
|
"syncEnvironment": true,
|
||||||
|
"resetCities": true,
|
||||||
|
"resetGenerals": true,
|
||||||
|
"deleteOtherGenerals": true,
|
||||||
|
"keepGeneralIds": [1, 2],
|
||||||
|
"resetNations": true,
|
||||||
|
"deleteOtherNations": true,
|
||||||
|
"keepNationIds": [1, 2],
|
||||||
|
"resetDiplomacy": true,
|
||||||
|
"general": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"values": {
|
||||||
|
"nation": 1,
|
||||||
|
"npc": 0,
|
||||||
|
"gold": 1000,
|
||||||
|
"rice": 2000,
|
||||||
|
"crew": 100,
|
||||||
|
"leadership": 50,
|
||||||
|
"strength": 40,
|
||||||
|
"intel": 30,
|
||||||
|
"experience": 100,
|
||||||
|
"dedication": 200,
|
||||||
|
"dex1": 1000,
|
||||||
|
"dex2": 0,
|
||||||
|
"dex3": 0,
|
||||||
|
"dex4": 0,
|
||||||
|
"dex5": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"values": {
|
||||||
|
"nation": 2,
|
||||||
|
"npc": 2,
|
||||||
|
"gold": 3000,
|
||||||
|
"rice": 4000,
|
||||||
|
"crew": 200,
|
||||||
|
"leadership": 60,
|
||||||
|
"strength": 50,
|
||||||
|
"intel": 40,
|
||||||
|
"experience": 300,
|
||||||
|
"dedication": 400,
|
||||||
|
"dex1": 2000,
|
||||||
|
"dex2": 1000,
|
||||||
|
"dex3": 0,
|
||||||
|
"dex4": 0,
|
||||||
|
"dex5": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"rankData": [
|
||||||
|
{"generalId": 1, "type": "killcrew_person", "value": 100},
|
||||||
|
{"generalId": 1, "type": "deathcrew_person", "value": 0},
|
||||||
|
{"generalId": 2, "type": "killcrew_person", "value": 0},
|
||||||
|
{"generalId": 2, "type": "deathcrew_person", "value": 100}
|
||||||
|
],
|
||||||
|
"nation": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"values": {
|
||||||
|
"name": "갑국",
|
||||||
|
"gold": 10000,
|
||||||
|
"rice": 20000,
|
||||||
|
"tech": 100,
|
||||||
|
"level": 2,
|
||||||
|
"gennum": 9,
|
||||||
|
"power": 7
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"values": {
|
||||||
|
"name": "을국",
|
||||||
|
"gold": 2000,
|
||||||
|
"rice": 3000,
|
||||||
|
"tech": 50,
|
||||||
|
"level": 1,
|
||||||
|
"gennum": 8,
|
||||||
|
"power": 8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"city": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"values": {
|
||||||
|
"name": "갑성",
|
||||||
|
"nation": 1,
|
||||||
|
"level": 4,
|
||||||
|
"supply": 1,
|
||||||
|
"pop": 1000,
|
||||||
|
"pop_max": 2000,
|
||||||
|
"agri": 100,
|
||||||
|
"agri_max": 200,
|
||||||
|
"comm": 100,
|
||||||
|
"comm_max": 200,
|
||||||
|
"secu": 100,
|
||||||
|
"secu_max": 200,
|
||||||
|
"def": 100,
|
||||||
|
"def_max": 200,
|
||||||
|
"wall": 100,
|
||||||
|
"wall_max": 200
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"values": {
|
||||||
|
"name": "을성",
|
||||||
|
"nation": 2,
|
||||||
|
"level": 4,
|
||||||
|
"supply": 1,
|
||||||
|
"pop": 2000,
|
||||||
|
"pop_max": 4000,
|
||||||
|
"agri": 200,
|
||||||
|
"agri_max": 400,
|
||||||
|
"comm": 200,
|
||||||
|
"comm_max": 400,
|
||||||
|
"secu": 200,
|
||||||
|
"secu_max": 400,
|
||||||
|
"def": 200,
|
||||||
|
"def_max": 400,
|
||||||
|
"wall": 200,
|
||||||
|
"wall_max": 400
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nationEnvironment": [
|
||||||
|
{
|
||||||
|
"nationId": 1,
|
||||||
|
"key": "max_power",
|
||||||
|
"value": {
|
||||||
|
"maxPower": 999,
|
||||||
|
"maxCrew": 50,
|
||||||
|
"maxCities": ["옛도시", "옛도시2"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{"nationId": 1, "key": "available_war_setting_cnt", "value": 1}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"observe": {
|
||||||
|
"generalIds": [1, 2],
|
||||||
|
"nationIds": [1, 2],
|
||||||
|
"cityIds": [1, 2],
|
||||||
|
"rankGeneralIds": [1, 2],
|
||||||
|
"rankTypes": ["killcrew_person", "deathcrew_person"],
|
||||||
|
"nationEnvironmentKeys": ["max_power", "available_war_setting_cnt"]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
{
|
||||||
|
"action": "PostUpdateMonthly",
|
||||||
|
"environment": {
|
||||||
|
"year": 193,
|
||||||
|
"month": 2,
|
||||||
|
"startyear": 193,
|
||||||
|
"seedYear": 193,
|
||||||
|
"seedMonth": 1
|
||||||
|
},
|
||||||
|
"setup": {
|
||||||
|
"world": {
|
||||||
|
"hiddenSeed": "monthly-post-tail-2"
|
||||||
|
},
|
||||||
|
"syncEnvironment": true,
|
||||||
|
"clearNeutralAuctions": true,
|
||||||
|
"resetCities": true,
|
||||||
|
"resetGenerals": true,
|
||||||
|
"deleteOtherGenerals": true,
|
||||||
|
"keepGeneralIds": [1, 2],
|
||||||
|
"resetNations": true,
|
||||||
|
"deleteOtherNations": true,
|
||||||
|
"keepNationIds": [1, 2],
|
||||||
|
"resetDiplomacy": true,
|
||||||
|
"gameEnvironment": {
|
||||||
|
"tournament": 0,
|
||||||
|
"tnmt_trig": true,
|
||||||
|
"tnmt_pattern": [0, 1, 2, 3],
|
||||||
|
"prev_winner": null
|
||||||
|
},
|
||||||
|
"general": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"values": {
|
||||||
|
"name": "갑장",
|
||||||
|
"nation": 1,
|
||||||
|
"city": 1,
|
||||||
|
"officer_level": 12,
|
||||||
|
"npc": 0,
|
||||||
|
"gold": 5000,
|
||||||
|
"rice": 7000,
|
||||||
|
"crew": 100,
|
||||||
|
"leadership": 50,
|
||||||
|
"strength": 40,
|
||||||
|
"intel": 30,
|
||||||
|
"experience": 100,
|
||||||
|
"dedication": 200
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"values": {
|
||||||
|
"name": "을장",
|
||||||
|
"nation": 2,
|
||||||
|
"city": 2,
|
||||||
|
"officer_level": 12,
|
||||||
|
"npc": 0,
|
||||||
|
"gold": 6000,
|
||||||
|
"rice": 8000,
|
||||||
|
"crew": 200,
|
||||||
|
"leadership": 60,
|
||||||
|
"strength": 50,
|
||||||
|
"intel": 40,
|
||||||
|
"experience": 300,
|
||||||
|
"dedication": 400
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nation": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"values": {
|
||||||
|
"name": "갑국",
|
||||||
|
"capital": 1,
|
||||||
|
"gold": 10000,
|
||||||
|
"rice": 20000,
|
||||||
|
"tech": 100,
|
||||||
|
"level": 2,
|
||||||
|
"gennum": 1,
|
||||||
|
"power": 7
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"values": {
|
||||||
|
"name": "을국",
|
||||||
|
"capital": 2,
|
||||||
|
"gold": 2000,
|
||||||
|
"rice": 3000,
|
||||||
|
"tech": 50,
|
||||||
|
"level": 1,
|
||||||
|
"gennum": 1,
|
||||||
|
"power": 8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"city": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"values": {
|
||||||
|
"name": "갑성",
|
||||||
|
"nation": 1,
|
||||||
|
"level": 4,
|
||||||
|
"supply": 1,
|
||||||
|
"front": 3,
|
||||||
|
"pop": 1000,
|
||||||
|
"pop_max": 2000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"values": {
|
||||||
|
"name": "을성",
|
||||||
|
"nation": 2,
|
||||||
|
"level": 4,
|
||||||
|
"supply": 1,
|
||||||
|
"front": 3,
|
||||||
|
"pop": 2000,
|
||||||
|
"pop_max": 4000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"observe": {
|
||||||
|
"generalIds": [1, 2],
|
||||||
|
"nationIds": [1, 2],
|
||||||
|
"cityIds": [1, 2]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
{
|
||||||
|
"action": "PostUpdateMonthly",
|
||||||
|
"environment": {
|
||||||
|
"year": 195,
|
||||||
|
"month": 2,
|
||||||
|
"startyear": 193,
|
||||||
|
"seedYear": 195,
|
||||||
|
"seedMonth": 1
|
||||||
|
},
|
||||||
|
"setup": {
|
||||||
|
"world": {
|
||||||
|
"hiddenSeed": "monthly-post-wander-fixture"
|
||||||
|
},
|
||||||
|
"syncEnvironment": true,
|
||||||
|
"resetCities": true,
|
||||||
|
"city": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"values": {
|
||||||
|
"name": "방랑성",
|
||||||
|
"nation": 4,
|
||||||
|
"level": 4,
|
||||||
|
"front": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"values": {
|
||||||
|
"name": "존속성",
|
||||||
|
"nation": 1,
|
||||||
|
"level": 4,
|
||||||
|
"front": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"resetGenerals": true,
|
||||||
|
"deleteOtherGenerals": true,
|
||||||
|
"keepGeneralIds": [1, 2, 3],
|
||||||
|
"general": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"values": {
|
||||||
|
"name": "방랑주",
|
||||||
|
"nation": 4,
|
||||||
|
"city": 1,
|
||||||
|
"officer_level": 12,
|
||||||
|
"npc": 2,
|
||||||
|
"gold": 2000,
|
||||||
|
"rice": 3000,
|
||||||
|
"crew": 100,
|
||||||
|
"makelimit": 0,
|
||||||
|
"belong": 7,
|
||||||
|
"leadership": 50,
|
||||||
|
"strength": 40,
|
||||||
|
"intel": 30,
|
||||||
|
"experience": 100,
|
||||||
|
"dedication": 200
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"values": {
|
||||||
|
"name": "방랑객",
|
||||||
|
"nation": 4,
|
||||||
|
"city": 1,
|
||||||
|
"officer_level": 1,
|
||||||
|
"npc": 0,
|
||||||
|
"gold": 1500,
|
||||||
|
"rice": 4000,
|
||||||
|
"crew": 200,
|
||||||
|
"makelimit": 0,
|
||||||
|
"belong": 5,
|
||||||
|
"leadership": 60,
|
||||||
|
"strength": 50,
|
||||||
|
"intel": 40,
|
||||||
|
"experience": 300,
|
||||||
|
"dedication": 400
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"values": {
|
||||||
|
"name": "존속장",
|
||||||
|
"nation": 1,
|
||||||
|
"city": 2,
|
||||||
|
"officer_level": 12,
|
||||||
|
"npc": 2,
|
||||||
|
"gold": 500,
|
||||||
|
"rice": 500,
|
||||||
|
"crew": 50,
|
||||||
|
"leadership": 50,
|
||||||
|
"strength": 50,
|
||||||
|
"intel": 50,
|
||||||
|
"experience": 0,
|
||||||
|
"dedication": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"resetNations": true,
|
||||||
|
"deleteOtherNations": true,
|
||||||
|
"keepNationIds": [1, 4],
|
||||||
|
"nation": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"values": {
|
||||||
|
"name": "존속국",
|
||||||
|
"gennum": 1,
|
||||||
|
"level": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"values": {
|
||||||
|
"name": "방랑국",
|
||||||
|
"gennum": 2,
|
||||||
|
"level": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"resetDiplomacy": true,
|
||||||
|
"diplomacy": [
|
||||||
|
{
|
||||||
|
"fromNationId": 1,
|
||||||
|
"toNationId": 4,
|
||||||
|
"state": 2,
|
||||||
|
"term": 0,
|
||||||
|
"dead": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fromNationId": 4,
|
||||||
|
"toNationId": 1,
|
||||||
|
"state": 2,
|
||||||
|
"term": 0,
|
||||||
|
"dead": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"observe": {
|
||||||
|
"generalIds": [1, 2, 3],
|
||||||
|
"nationIds": [1, 4],
|
||||||
|
"cityIds": [1, 2]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"action": "PreUpdateMonthly",
|
||||||
|
"environment": {
|
||||||
|
"year": 200,
|
||||||
|
"month": 12,
|
||||||
|
"startyear": 190
|
||||||
|
},
|
||||||
|
"setup": {
|
||||||
|
"syncEnvironment": true,
|
||||||
|
"resetGenerals": true,
|
||||||
|
"deleteOtherGenerals": true,
|
||||||
|
"keepGeneralIds": [1, 2],
|
||||||
|
"general": [
|
||||||
|
{"id": 1, "values": {"makelimit": 2}},
|
||||||
|
{"id": 2, "values": {"makelimit": 0}}
|
||||||
|
],
|
||||||
|
"generalAccessLog": [
|
||||||
|
{"generalId": 1, "refreshScoreTotal": 101},
|
||||||
|
{"generalId": 2, "refreshScoreTotal": 1}
|
||||||
|
],
|
||||||
|
"nation": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"values": {
|
||||||
|
"rate": 35,
|
||||||
|
"rate_tmp": 10,
|
||||||
|
"strategic_cmd_limit": 2,
|
||||||
|
"surlimit": 1,
|
||||||
|
"spy": "{\"1\":1,\"2\":2}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"city": [
|
||||||
|
{"id": 1, "values": {"state": 31, "term": 1, "conflict": {"1": 10}}},
|
||||||
|
{"id": 2, "values": {"state": 32, "term": 2, "conflict": {"1": 20}}},
|
||||||
|
{"id": 3, "values": {"state": 33, "term": 0, "conflict": {"1": 30}}},
|
||||||
|
{"id": 4, "values": {"state": 34, "term": 3, "conflict": {"1": 40}}},
|
||||||
|
{"id": 5, "values": {"state": 41, "term": 1, "conflict": {"1": 50}}},
|
||||||
|
{"id": 6, "values": {"state": 42, "term": 2, "conflict": {"1": 60}}},
|
||||||
|
{"id": 7, "values": {"state": 43, "term": 3, "conflict": {"1": 70}}}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"observe": {
|
||||||
|
"generalIds": [1, 2],
|
||||||
|
"nationIds": [1],
|
||||||
|
"cityIds": [1, 2, 3, 4, 5, 6, 7],
|
||||||
|
"accessGeneralIds": [1, 2]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
{
|
||||||
|
"action": "ProcessIncome",
|
||||||
|
"args": ["gold"],
|
||||||
|
"environment": {
|
||||||
|
"year": 190,
|
||||||
|
"month": 7,
|
||||||
|
"startyear": 190
|
||||||
|
},
|
||||||
|
"setup": {
|
||||||
|
"resetCities": true,
|
||||||
|
"resetGenerals": true,
|
||||||
|
"deleteOtherGenerals": true,
|
||||||
|
"keepGeneralIds": [1],
|
||||||
|
"resetNations": true,
|
||||||
|
"deleteOtherNations": true,
|
||||||
|
"keepNationIds": [1],
|
||||||
|
"general": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"values": {
|
||||||
|
"nation": 1,
|
||||||
|
"city": 1,
|
||||||
|
"officer_level": 5,
|
||||||
|
"npc": 0,
|
||||||
|
"gold": 1000,
|
||||||
|
"rice": 1000,
|
||||||
|
"dedication": 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nation": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"values": {
|
||||||
|
"name": "갑국",
|
||||||
|
"capital": 1,
|
||||||
|
"gold": 10000,
|
||||||
|
"rice": 20000,
|
||||||
|
"level": 1,
|
||||||
|
"rate": 35,
|
||||||
|
"rate_tmp": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"city": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"values": {
|
||||||
|
"name": "갑성",
|
||||||
|
"nation": 1,
|
||||||
|
"level": 4,
|
||||||
|
"supply": 1,
|
||||||
|
"pop": 10000,
|
||||||
|
"pop_max": 20000,
|
||||||
|
"agri": 1000,
|
||||||
|
"agri_max": 2000,
|
||||||
|
"comm": 1000,
|
||||||
|
"comm_max": 2000,
|
||||||
|
"secu": 1000,
|
||||||
|
"secu_max": 2000,
|
||||||
|
"trust": 80,
|
||||||
|
"def": 500,
|
||||||
|
"def_max": 1000,
|
||||||
|
"wall": 500,
|
||||||
|
"wall_max": 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"observe": {
|
||||||
|
"generalIds": [1],
|
||||||
|
"nationIds": [1],
|
||||||
|
"cityIds": [1],
|
||||||
|
"nationEnvironmentKeys": ["prev_income_gold", "prev_income_rice"]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
{
|
||||||
|
"action": "ProcessSemiAnnual",
|
||||||
|
"resource": "gold",
|
||||||
|
"environment": {
|
||||||
|
"year": 193,
|
||||||
|
"month": 1,
|
||||||
|
"startyear": 190
|
||||||
|
},
|
||||||
|
"setup": {
|
||||||
|
"resetCities": true,
|
||||||
|
"resetGenerals": true,
|
||||||
|
"nation": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"values": {
|
||||||
|
"name": "반기검증국",
|
||||||
|
"gold": 100001,
|
||||||
|
"rice": 7777,
|
||||||
|
"rate_tmp": 20,
|
||||||
|
"type": "che_도가"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"city": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"values": {
|
||||||
|
"nation": 1,
|
||||||
|
"supply": 1,
|
||||||
|
"pop": 10000,
|
||||||
|
"pop_max": 50000,
|
||||||
|
"agri": 1001,
|
||||||
|
"agri_max": 5000,
|
||||||
|
"comm": 1001,
|
||||||
|
"comm_max": 5000,
|
||||||
|
"secu": 1001,
|
||||||
|
"secu_max": 2000,
|
||||||
|
"trust": 55,
|
||||||
|
"def": 1001,
|
||||||
|
"def_max": 5000,
|
||||||
|
"wall": 1001,
|
||||||
|
"wall_max": 5000,
|
||||||
|
"dead": 123
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"values": {
|
||||||
|
"nation": 1,
|
||||||
|
"supply": 0,
|
||||||
|
"pop": 10000,
|
||||||
|
"pop_max": 50000,
|
||||||
|
"agri": 1001,
|
||||||
|
"agri_max": 5000,
|
||||||
|
"comm": 1001,
|
||||||
|
"comm_max": 5000,
|
||||||
|
"secu": 1001,
|
||||||
|
"secu_max": 2000,
|
||||||
|
"trust": 55,
|
||||||
|
"def": 1001,
|
||||||
|
"def_max": 5000,
|
||||||
|
"wall": 1001,
|
||||||
|
"wall_max": 5000,
|
||||||
|
"dead": 123
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"values": {
|
||||||
|
"nation": 0,
|
||||||
|
"supply": 1,
|
||||||
|
"pop": 10000,
|
||||||
|
"pop_max": 50000,
|
||||||
|
"agri": 1001,
|
||||||
|
"agri_max": 5000,
|
||||||
|
"comm": 1001,
|
||||||
|
"comm_max": 5000,
|
||||||
|
"secu": 1001,
|
||||||
|
"secu_max": 2000,
|
||||||
|
"trust": 55,
|
||||||
|
"def": 1001,
|
||||||
|
"def_max": 5000,
|
||||||
|
"wall": 1001,
|
||||||
|
"wall_max": 5000,
|
||||||
|
"dead": 123
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"general": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"values": {
|
||||||
|
"nation": 1,
|
||||||
|
"city": 1,
|
||||||
|
"gold": 10001,
|
||||||
|
"rice": 8888
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"observe": {
|
||||||
|
"generalIds": [1],
|
||||||
|
"cityIds": [1, 2, 3],
|
||||||
|
"nationIds": [1]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
{
|
||||||
|
"action": "ProcessSemiAnnual",
|
||||||
|
"resource": "gold",
|
||||||
|
"environment": {
|
||||||
|
"year": 193,
|
||||||
|
"month": 1,
|
||||||
|
"startyear": 190
|
||||||
|
},
|
||||||
|
"setup": {
|
||||||
|
"resetCities": true,
|
||||||
|
"resetGenerals": true,
|
||||||
|
"nation": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"values": {
|
||||||
|
"name": "반기검증국",
|
||||||
|
"gold": 100001,
|
||||||
|
"rice": 7777,
|
||||||
|
"rate": 50,
|
||||||
|
"rate_tmp": 20,
|
||||||
|
"type": "che_중립"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"city": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"values": {
|
||||||
|
"nation": 1,
|
||||||
|
"supply": 1,
|
||||||
|
"pop": 10000,
|
||||||
|
"pop_max": 50000,
|
||||||
|
"agri": 1001,
|
||||||
|
"agri_max": 5000,
|
||||||
|
"comm": 1001,
|
||||||
|
"comm_max": 5000,
|
||||||
|
"secu": 1001,
|
||||||
|
"secu_max": 2000,
|
||||||
|
"trust": 55,
|
||||||
|
"def": 1001,
|
||||||
|
"def_max": 5000,
|
||||||
|
"wall": 1001,
|
||||||
|
"wall_max": 5000,
|
||||||
|
"dead": 123
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"general": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"values": {
|
||||||
|
"nation": 1,
|
||||||
|
"city": 1,
|
||||||
|
"gold": 10001,
|
||||||
|
"rice": 8888
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"observe": {
|
||||||
|
"generalIds": [1],
|
||||||
|
"cityIds": [1],
|
||||||
|
"nationIds": [1]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
{
|
||||||
|
"action": "ProcessWarIncome",
|
||||||
|
"environment": {
|
||||||
|
"year": 193,
|
||||||
|
"month": 1,
|
||||||
|
"startyear": 190
|
||||||
|
},
|
||||||
|
"setup": {
|
||||||
|
"resetCities": true,
|
||||||
|
"nation": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"values": {
|
||||||
|
"name": "전쟁수입국",
|
||||||
|
"gold": 1000,
|
||||||
|
"level": 1,
|
||||||
|
"type": "che_중립"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"values": {
|
||||||
|
"name": "비활성국",
|
||||||
|
"gold": 2000,
|
||||||
|
"level": 0,
|
||||||
|
"type": "che_중립"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"values": {
|
||||||
|
"name": "법가국",
|
||||||
|
"gold": 3000,
|
||||||
|
"level": 1,
|
||||||
|
"type": "che_법가"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"city": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"values": {
|
||||||
|
"nation": 1,
|
||||||
|
"supply": 1,
|
||||||
|
"pop": 1000,
|
||||||
|
"pop_max": 2000,
|
||||||
|
"dead": 101
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"values": {
|
||||||
|
"nation": 1,
|
||||||
|
"supply": 0,
|
||||||
|
"pop": 1000,
|
||||||
|
"pop_max": 2000,
|
||||||
|
"dead": 999
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"values": {
|
||||||
|
"nation": 2,
|
||||||
|
"supply": 1,
|
||||||
|
"pop": 1000,
|
||||||
|
"pop_max": 2000,
|
||||||
|
"dead": 1000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"values": {
|
||||||
|
"nation": 3,
|
||||||
|
"supply": 1,
|
||||||
|
"pop": 1000,
|
||||||
|
"pop_max": 2000,
|
||||||
|
"dead": 105
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"values": {
|
||||||
|
"nation": 0,
|
||||||
|
"supply": 1,
|
||||||
|
"pop": 999,
|
||||||
|
"pop_max": 1000,
|
||||||
|
"dead": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"observe": {
|
||||||
|
"cityIds": [1, 2, 3, 4, 5],
|
||||||
|
"nationIds": [1, 2, 3]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
{
|
||||||
|
"action": "RaiseInvader",
|
||||||
|
"compactOutput": true,
|
||||||
|
"args": [10, 150, 100, 20],
|
||||||
|
"setup": {
|
||||||
|
"resetCities": true,
|
||||||
|
"resetGenerals": true,
|
||||||
|
"deleteOtherGenerals": true,
|
||||||
|
"keepGeneralIds": [1],
|
||||||
|
"resetNations": true,
|
||||||
|
"deleteOtherNations": true,
|
||||||
|
"keepNationIds": [1],
|
||||||
|
"city": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"values": {
|
||||||
|
"level": 3,
|
||||||
|
"nation": 1,
|
||||||
|
"pop": 1000,
|
||||||
|
"pop_max": 10000,
|
||||||
|
"agri": 2000,
|
||||||
|
"agri_max": 20000,
|
||||||
|
"comm": 3000,
|
||||||
|
"comm_max": 30000,
|
||||||
|
"secu": 4000,
|
||||||
|
"secu_max": 40000,
|
||||||
|
"def": 5000,
|
||||||
|
"def_max": 50000,
|
||||||
|
"wall": 6000,
|
||||||
|
"wall_max": 60000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 48,
|
||||||
|
"values": {
|
||||||
|
"level": 4,
|
||||||
|
"nation": 0,
|
||||||
|
"pop": 1000,
|
||||||
|
"pop_max": 10000,
|
||||||
|
"agri": 2000,
|
||||||
|
"agri_max": 20000,
|
||||||
|
"comm": 3000,
|
||||||
|
"comm_max": 30000,
|
||||||
|
"secu": 4000,
|
||||||
|
"secu_max": 40000,
|
||||||
|
"def": 5000,
|
||||||
|
"def_max": 50000,
|
||||||
|
"wall": 6000,
|
||||||
|
"wall_max": 60000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nation": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"values": {
|
||||||
|
"name": "기준국",
|
||||||
|
"capital": 1,
|
||||||
|
"gennum": 1,
|
||||||
|
"tech": 100,
|
||||||
|
"level": 2,
|
||||||
|
"type": "che_유가"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"general": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"values": {
|
||||||
|
"name": "군주",
|
||||||
|
"nation": 1,
|
||||||
|
"city": 1,
|
||||||
|
"officer_level": 12,
|
||||||
|
"npc": 0,
|
||||||
|
"leadership": 50,
|
||||||
|
"strength": 50,
|
||||||
|
"intel": 50,
|
||||||
|
"experience": 1000,
|
||||||
|
"dedication": 1000,
|
||||||
|
"dex1": 10,
|
||||||
|
"dex2": 20,
|
||||||
|
"dex3": 30,
|
||||||
|
"dex4": 40,
|
||||||
|
"dex5": 50,
|
||||||
|
"turntime": "0200-01-01 00:05:00"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"syncEnvironment": true
|
||||||
|
},
|
||||||
|
"environment": {
|
||||||
|
"year": 200,
|
||||||
|
"month": 1,
|
||||||
|
"startyear": 190,
|
||||||
|
"turnterm": 10,
|
||||||
|
"turntime": "0200-01-01 00:00:00",
|
||||||
|
"show_img_level": 3,
|
||||||
|
"fiction": [0]
|
||||||
|
},
|
||||||
|
"observe": {
|
||||||
|
"generalIds": [1],
|
||||||
|
"cityIds": [1, 48],
|
||||||
|
"nationIds": [1]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
{
|
||||||
|
"action": "RaiseNPCNation",
|
||||||
|
"setup": {
|
||||||
|
"resetCities": true,
|
||||||
|
"resetGenerals": true,
|
||||||
|
"resetNations": true,
|
||||||
|
"city": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"values": {
|
||||||
|
"name": "점유도시",
|
||||||
|
"level": 5,
|
||||||
|
"nation": 1,
|
||||||
|
"pop": 10001,
|
||||||
|
"pop_max": 50001,
|
||||||
|
"agri": 1001,
|
||||||
|
"agri_max": 5001,
|
||||||
|
"comm": 2001,
|
||||||
|
"comm_max": 6001,
|
||||||
|
"secu": 3001,
|
||||||
|
"secu_max": 7001,
|
||||||
|
"def": 4001,
|
||||||
|
"def_max": 8001,
|
||||||
|
"wall": 5001,
|
||||||
|
"wall_max": 9001
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 48,
|
||||||
|
"values": {
|
||||||
|
"name": "건국도시",
|
||||||
|
"level": 5,
|
||||||
|
"nation": 0,
|
||||||
|
"pop": 10048,
|
||||||
|
"pop_max": 50048,
|
||||||
|
"agri": 1048,
|
||||||
|
"agri_max": 5048,
|
||||||
|
"comm": 2048,
|
||||||
|
"comm_max": 6048,
|
||||||
|
"secu": 3048,
|
||||||
|
"secu_max": 7048,
|
||||||
|
"def": 4048,
|
||||||
|
"def_max": 8048,
|
||||||
|
"wall": 5048,
|
||||||
|
"wall_max": 9048
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nation": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"values": {
|
||||||
|
"name": "기준국",
|
||||||
|
"capital": 1,
|
||||||
|
"gennum": 1,
|
||||||
|
"tech": 120,
|
||||||
|
"level": 2,
|
||||||
|
"type": "che_유가"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"syncEnvironment": true
|
||||||
|
},
|
||||||
|
"environment": {
|
||||||
|
"year": 200,
|
||||||
|
"month": 1,
|
||||||
|
"startyear": 190,
|
||||||
|
"turnterm": 10,
|
||||||
|
"turntime": "0200-01-01 00:00:00",
|
||||||
|
"show_img_level": 3,
|
||||||
|
"fiction": [0]
|
||||||
|
},
|
||||||
|
"observe": {
|
||||||
|
"cityIds": [1, 48],
|
||||||
|
"nationIds": [1]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"action": "RegNeutralNPC",
|
||||||
|
"args": [
|
||||||
|
0,
|
||||||
|
"등록재야",
|
||||||
|
null,
|
||||||
|
0,
|
||||||
|
33,
|
||||||
|
45,
|
||||||
|
55,
|
||||||
|
65,
|
||||||
|
180,
|
||||||
|
245,
|
||||||
|
null,
|
||||||
|
"무쌍",
|
||||||
|
""
|
||||||
|
],
|
||||||
|
"setup": {
|
||||||
|
"syncEnvironment": true
|
||||||
|
},
|
||||||
|
"environment": {
|
||||||
|
"year": 200,
|
||||||
|
"month": 1,
|
||||||
|
"startyear": 190,
|
||||||
|
"turnterm": 10,
|
||||||
|
"turntime": "0200-01-01 00:00:00",
|
||||||
|
"show_img_level": 3,
|
||||||
|
"fiction": [0]
|
||||||
|
},
|
||||||
|
"observe": {
|
||||||
|
"generalIds": [],
|
||||||
|
"cityIds": [33],
|
||||||
|
"nationIds": [1]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"action": "RegNPC",
|
||||||
|
"args": [
|
||||||
|
0,
|
||||||
|
"등록장수",
|
||||||
|
null,
|
||||||
|
0,
|
||||||
|
33,
|
||||||
|
60,
|
||||||
|
50,
|
||||||
|
40,
|
||||||
|
7,
|
||||||
|
186,
|
||||||
|
240,
|
||||||
|
null,
|
||||||
|
"인덕",
|
||||||
|
"등록 대사"
|
||||||
|
],
|
||||||
|
"setup": {
|
||||||
|
"syncEnvironment": true
|
||||||
|
},
|
||||||
|
"environment": {
|
||||||
|
"year": 200,
|
||||||
|
"month": 1,
|
||||||
|
"startyear": 190,
|
||||||
|
"turnterm": 10,
|
||||||
|
"turntime": "0200-01-01 00:00:00",
|
||||||
|
"show_img_level": 3,
|
||||||
|
"fiction": [0]
|
||||||
|
},
|
||||||
|
"observe": {
|
||||||
|
"generalIds": [],
|
||||||
|
"cityIds": [33],
|
||||||
|
"nationIds": [1]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"action": "UnblockScoutAction",
|
||||||
|
"args": [false],
|
||||||
|
"setup": {
|
||||||
|
"resetNations": true,
|
||||||
|
"deleteOtherNations": true,
|
||||||
|
"keepNationIds": [1, 2],
|
||||||
|
"nation": [
|
||||||
|
{ "id": 1, "values": { "name": "위", "level": 2, "scout": 1 } },
|
||||||
|
{ "id": 2, "values": { "name": "촉", "level": 2, "scout": 1 } }
|
||||||
|
],
|
||||||
|
"gameEnvironment": {
|
||||||
|
"block_change_scout": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"environment": {
|
||||||
|
"year": 200,
|
||||||
|
"month": 1,
|
||||||
|
"startyear": 190
|
||||||
|
},
|
||||||
|
"observe": {
|
||||||
|
"nationIds": [1, 2]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
{
|
||||||
|
"action": "UpdateCitySupply",
|
||||||
|
"environment": {
|
||||||
|
"year": 193,
|
||||||
|
"month": 1,
|
||||||
|
"startyear": 190
|
||||||
|
},
|
||||||
|
"setup": {
|
||||||
|
"nation": [
|
||||||
|
{
|
||||||
|
"id": 99,
|
||||||
|
"values": {
|
||||||
|
"name": "보급검증국",
|
||||||
|
"color": "#777777",
|
||||||
|
"capital": 1,
|
||||||
|
"level": 1,
|
||||||
|
"type": "che_중립"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 98,
|
||||||
|
"values": {
|
||||||
|
"name": "불일치수도국",
|
||||||
|
"color": "#888888",
|
||||||
|
"capital": 2,
|
||||||
|
"level": 1,
|
||||||
|
"type": "che_중립"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"city": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"values": {
|
||||||
|
"nation": 99,
|
||||||
|
"supply": 0,
|
||||||
|
"front": 2,
|
||||||
|
"pop": 1001,
|
||||||
|
"agri": 501,
|
||||||
|
"comm": 499,
|
||||||
|
"secu": 99,
|
||||||
|
"trust": 50,
|
||||||
|
"def": 101,
|
||||||
|
"wall": 50,
|
||||||
|
"officer_set": 7,
|
||||||
|
"term": 2,
|
||||||
|
"conflict": {"98": 3}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 9,
|
||||||
|
"values": {
|
||||||
|
"nation": 99,
|
||||||
|
"supply": 0,
|
||||||
|
"front": 2,
|
||||||
|
"pop": 1001,
|
||||||
|
"agri": 501,
|
||||||
|
"comm": 499,
|
||||||
|
"secu": 99,
|
||||||
|
"trust": 50,
|
||||||
|
"def": 101,
|
||||||
|
"wall": 50,
|
||||||
|
"officer_set": 7,
|
||||||
|
"term": 2,
|
||||||
|
"conflict": {"98": 3}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"values": {
|
||||||
|
"nation": 99,
|
||||||
|
"supply": 1,
|
||||||
|
"front": 2,
|
||||||
|
"pop": 1001,
|
||||||
|
"agri": 501,
|
||||||
|
"comm": 499,
|
||||||
|
"secu": 99,
|
||||||
|
"trust": 33,
|
||||||
|
"def": 101,
|
||||||
|
"wall": 50,
|
||||||
|
"officer_set": 7,
|
||||||
|
"term": 2,
|
||||||
|
"conflict": {"98": 3}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"values": {
|
||||||
|
"nation": 0,
|
||||||
|
"supply": 0,
|
||||||
|
"front": 0,
|
||||||
|
"trust": 50
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"general": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"values": {
|
||||||
|
"nation": 99,
|
||||||
|
"city": 2,
|
||||||
|
"officer_level": 4,
|
||||||
|
"officer_city": 2,
|
||||||
|
"crew": 101,
|
||||||
|
"train": 51,
|
||||||
|
"atmos": 99
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"values": {
|
||||||
|
"nation": 99,
|
||||||
|
"city": 1,
|
||||||
|
"officer_level": 4,
|
||||||
|
"officer_city": 2,
|
||||||
|
"crew": 101,
|
||||||
|
"train": 51,
|
||||||
|
"atmos": 99
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"values": {
|
||||||
|
"nation": 98,
|
||||||
|
"city": 2,
|
||||||
|
"officer_level": 3,
|
||||||
|
"officer_city": 1,
|
||||||
|
"crew": 101,
|
||||||
|
"train": 51,
|
||||||
|
"atmos": 99
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"observe": {
|
||||||
|
"generalIds": [1, 2, 3],
|
||||||
|
"cityIds": [1, 2, 3, 9],
|
||||||
|
"nationIds": [98, 99]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
{
|
||||||
|
"action": "UpdateNationLevel",
|
||||||
|
"environment": {
|
||||||
|
"year": 193,
|
||||||
|
"month": 2,
|
||||||
|
"startyear": 190,
|
||||||
|
"killturn": 1000,
|
||||||
|
"turnterm": 10
|
||||||
|
},
|
||||||
|
"setup": {
|
||||||
|
"resetCities": true,
|
||||||
|
"resetGenerals": true,
|
||||||
|
"resetUniqueOccupancy": true,
|
||||||
|
"syncEnvironment": true,
|
||||||
|
"clearNationTurnIds": [1],
|
||||||
|
"clearInheritanceOwnerIds": [990061],
|
||||||
|
"nation": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"values": {
|
||||||
|
"name": "작위검증국",
|
||||||
|
"color": "#777777",
|
||||||
|
"capital": 1,
|
||||||
|
"gold": 10000,
|
||||||
|
"rice": 20000,
|
||||||
|
"level": 0,
|
||||||
|
"type": "che_중립",
|
||||||
|
"aux": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"city": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"values": {
|
||||||
|
"nation": 1,
|
||||||
|
"level": 4
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"values": {
|
||||||
|
"nation": 1,
|
||||||
|
"level": 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"general": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"values": {
|
||||||
|
"name": "조조",
|
||||||
|
"nation": 1,
|
||||||
|
"city": 1,
|
||||||
|
"officer_level": 12,
|
||||||
|
"owner": 990061,
|
||||||
|
"npc": 0,
|
||||||
|
"killturn": 1000,
|
||||||
|
"belong": 10,
|
||||||
|
"horse": "None",
|
||||||
|
"weapon": "None",
|
||||||
|
"book": "None",
|
||||||
|
"item": "None",
|
||||||
|
"aux": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"values": {
|
||||||
|
"nation": 0,
|
||||||
|
"city": 2,
|
||||||
|
"officer_level": 1,
|
||||||
|
"npc": 0,
|
||||||
|
"killturn": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"observe": {
|
||||||
|
"generalIds": [1],
|
||||||
|
"cityIds": [1, 2],
|
||||||
|
"nationIds": [1],
|
||||||
|
"ownerIds": [990061]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace sammo;
|
||||||
|
|
||||||
|
if (PHP_SAPI !== 'cli') {
|
||||||
|
http_response_code(404);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
chdir(dirname(__DIR__));
|
||||||
|
require_once 'lib.php';
|
||||||
|
require_once 'func.php';
|
||||||
|
|
||||||
|
$input = stream_get_contents(STDIN);
|
||||||
|
$keys = Json::decode($input);
|
||||||
|
if (!is_array($keys)) {
|
||||||
|
fwrite(STDERR, "Expected a JSON array of item keys.\n");
|
||||||
|
exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = [];
|
||||||
|
foreach ($keys as $key) {
|
||||||
|
if (!is_string($key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$item = buildItemClass($key);
|
||||||
|
$result[$key] = [
|
||||||
|
'rawName' => $item->getRawName(),
|
||||||
|
'name' => $item->getName(),
|
||||||
|
'info' => $item->getInfo(),
|
||||||
|
'cost' => $item->getCost(),
|
||||||
|
'buyable' => $item->isBuyable(),
|
||||||
|
'consumable' => $item->isConsumable(),
|
||||||
|
'reqSecu' => $item->getReqSecu(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
echo Json::encode($result);
|
||||||
@@ -0,0 +1,1596 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace sammo;
|
||||||
|
|
||||||
|
if (PHP_SAPI !== 'cli' || getenv('TURN_DIFFERENTIAL_ENABLED') !== '1') {
|
||||||
|
http_response_code(404);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
chdir(dirname(__DIR__));
|
||||||
|
$_SERVER['REMOTE_ADDR'] ??= '127.0.0.1';
|
||||||
|
error_reporting(E_ALL & ~E_DEPRECATED);
|
||||||
|
require_once 'lib.php';
|
||||||
|
require_once 'func.php';
|
||||||
|
require_once __DIR__ . '/turn_state_snapshot.php';
|
||||||
|
|
||||||
|
function comparisonGameTick(mixed $value, string $label): int
|
||||||
|
{
|
||||||
|
if (is_int($value)) {
|
||||||
|
return GameClock::requireSafeTick($value);
|
||||||
|
}
|
||||||
|
if (!is_string($value) || $value === '') {
|
||||||
|
throw new \InvalidArgumentException("{$label} must be a game tick or projected date string");
|
||||||
|
}
|
||||||
|
$gameStorage = KVStorage::getStorage(DB::db(), 'game_env');
|
||||||
|
return GameClock::fromStorage($gameStorage)->dateTimeToTick(new \DateTimeImmutable($value));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<string, mixed> $values */
|
||||||
|
function comparisonMonthlyPatch(string $table, int $id, array $values): void
|
||||||
|
{
|
||||||
|
$definitions = [
|
||||||
|
'city' => [
|
||||||
|
'idColumn' => 'city',
|
||||||
|
'allowed' => [
|
||||||
|
'name', 'level', 'nation', 'supply', 'front', 'pop', 'pop_max',
|
||||||
|
'agri', 'agri_max', 'comm', 'comm_max', 'secu', 'secu_max',
|
||||||
|
'trust', 'trade', 'def', 'def_max', 'wall', 'wall_max',
|
||||||
|
'officer_set', 'state', 'region', 'term', 'conflict', 'dead',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'nation' => [
|
||||||
|
'idColumn' => 'nation',
|
||||||
|
'allowed' => [
|
||||||
|
'name', 'color', 'capital', 'gennum', 'gold', 'rice', 'tech',
|
||||||
|
'power', 'level', 'type', 'bill', 'rate_tmp', 'scout', 'chief_set', 'aux',
|
||||||
|
'rate', 'strategic_cmd_limit', 'surlimit', 'spy',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'general' => [
|
||||||
|
'idColumn' => 'no',
|
||||||
|
'allowed' => [
|
||||||
|
'name', 'nation', 'city', 'officer_level', 'officer_city',
|
||||||
|
'crew', 'train', 'atmos', 'owner', 'npc', 'killturn', 'belong',
|
||||||
|
'gold', 'rice', 'horse', 'weapon', 'book', 'item', 'aux',
|
||||||
|
'leadership', 'strength', 'intel', 'experience', 'dedication',
|
||||||
|
'injury',
|
||||||
|
'dex1', 'dex2', 'dex3', 'dex4', 'dex5', 'personal', 'special',
|
||||||
|
'specage', 'special2', 'specage2', 'betray', 'age',
|
||||||
|
'bornyear', 'deadyear', 'turntime', 'makelimit',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
$definition = $definitions[$table] ?? null;
|
||||||
|
if ($definition === null) {
|
||||||
|
throw new \InvalidArgumentException("unsupported setup table: {$table}");
|
||||||
|
}
|
||||||
|
$patch = [];
|
||||||
|
foreach ($values as $key => $value) {
|
||||||
|
if (!is_string($key) || !in_array($key, $definition['allowed'], true)) {
|
||||||
|
throw new \InvalidArgumentException("unsupported {$table} setup field");
|
||||||
|
}
|
||||||
|
if (in_array($key, ['conflict', 'aux'], true) && is_array($value)) {
|
||||||
|
$value = json_encode($value, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);
|
||||||
|
}
|
||||||
|
$patch[$key] = $value;
|
||||||
|
}
|
||||||
|
if ($table === 'nation') {
|
||||||
|
$patch += [
|
||||||
|
'nation' => $id,
|
||||||
|
'name' => "fixture-nation-{$id}",
|
||||||
|
'color' => '#777777',
|
||||||
|
'capital' => 0,
|
||||||
|
'gold' => 0,
|
||||||
|
'rice' => 0,
|
||||||
|
'level' => 1,
|
||||||
|
'type' => 'che_중립',
|
||||||
|
'aux' => '{}',
|
||||||
|
];
|
||||||
|
DB::db()->insertUpdate('nation', $patch, $patch);
|
||||||
|
} elseif ($patch !== []) {
|
||||||
|
DB::db()->update($table, $patch, "`{$definition['idColumn']}` = %i", $id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<string, mixed> $observe */
|
||||||
|
function comparisonMonthlyDetails(array $observe, int $worldHistoryAfterId): array
|
||||||
|
{
|
||||||
|
$db = DB::db();
|
||||||
|
$generals = array_map(
|
||||||
|
static fn(array $row): array => comparisonPickRow(
|
||||||
|
$row,
|
||||||
|
[
|
||||||
|
'id' => 'no',
|
||||||
|
'nationId' => 'nation',
|
||||||
|
'cityId' => 'city',
|
||||||
|
'officerLevel' => 'officer_level',
|
||||||
|
'officerCityId' => 'officer_city',
|
||||||
|
'crew' => 'crew',
|
||||||
|
'train' => 'train',
|
||||||
|
'atmos' => 'atmos',
|
||||||
|
'gold' => 'gold',
|
||||||
|
'rice' => 'rice',
|
||||||
|
'injury' => 'injury',
|
||||||
|
'leadership' => 'leadership',
|
||||||
|
'strength' => 'strength',
|
||||||
|
'intelligence' => 'intel',
|
||||||
|
'age' => 'age',
|
||||||
|
'specialDomestic' => 'special',
|
||||||
|
'specAge' => 'specage',
|
||||||
|
'specialWar' => 'special2',
|
||||||
|
'specAge2' => 'specage2',
|
||||||
|
'betray' => 'betray',
|
||||||
|
'makeLimit' => 'makelimit',
|
||||||
|
'horse' => 'horse',
|
||||||
|
'weapon' => 'weapon',
|
||||||
|
'book' => 'book',
|
||||||
|
'item' => 'item',
|
||||||
|
'dex1' => 'dex1',
|
||||||
|
'dex2' => 'dex2',
|
||||||
|
'dex3' => 'dex3',
|
||||||
|
'dex4' => 'dex4',
|
||||||
|
'dex5' => 'dex5',
|
||||||
|
'aux' => 'aux',
|
||||||
|
],
|
||||||
|
['aux'],
|
||||||
|
),
|
||||||
|
comparisonRowsById('general', 'no', comparisonIntegerList($observe['generalIds'] ?? [], 'generalIds')),
|
||||||
|
);
|
||||||
|
$cities = array_map(
|
||||||
|
static fn(array $row): array => comparisonPickRow(
|
||||||
|
$row,
|
||||||
|
[
|
||||||
|
'id' => 'city',
|
||||||
|
'nationId' => 'nation',
|
||||||
|
'state' => 'state',
|
||||||
|
'supplyState' => 'supply',
|
||||||
|
'frontState' => 'front',
|
||||||
|
'officerSet' => 'officer_set',
|
||||||
|
'term' => 'term',
|
||||||
|
'dead' => 'dead',
|
||||||
|
'population' => 'pop',
|
||||||
|
'populationMax' => 'pop_max',
|
||||||
|
'agriculture' => 'agri',
|
||||||
|
'agricultureMax' => 'agri_max',
|
||||||
|
'commerce' => 'comm',
|
||||||
|
'commerceMax' => 'comm_max',
|
||||||
|
'security' => 'secu',
|
||||||
|
'securityMax' => 'secu_max',
|
||||||
|
'trust' => 'trust',
|
||||||
|
'trade' => 'trade',
|
||||||
|
'defence' => 'def',
|
||||||
|
'defenceMax' => 'def_max',
|
||||||
|
'wall' => 'wall',
|
||||||
|
'wallMax' => 'wall_max',
|
||||||
|
'conflict' => 'conflict',
|
||||||
|
],
|
||||||
|
['conflict'],
|
||||||
|
),
|
||||||
|
comparisonRowsById('city', 'city', comparisonIntegerList($observe['cityIds'] ?? [], 'cityIds')),
|
||||||
|
);
|
||||||
|
$nations = array_map(
|
||||||
|
static fn(array $row): array => comparisonPickRow(
|
||||||
|
$row,
|
||||||
|
[
|
||||||
|
'id' => 'nation',
|
||||||
|
'gold' => 'gold',
|
||||||
|
'rice' => 'rice',
|
||||||
|
'rate' => 'rate_tmp',
|
||||||
|
'baseRate' => 'rate',
|
||||||
|
'power' => 'power',
|
||||||
|
'generalCount' => 'gennum',
|
||||||
|
'tech' => 'tech',
|
||||||
|
'typeCode' => 'type',
|
||||||
|
'scout' => 'scout',
|
||||||
|
'strategicCommandLimit' => 'strategic_cmd_limit',
|
||||||
|
'surrenderLimit' => 'surlimit',
|
||||||
|
'spy' => 'spy',
|
||||||
|
'chiefSet' => 'chief_set',
|
||||||
|
],
|
||||||
|
['spy'],
|
||||||
|
),
|
||||||
|
comparisonRowsById('nation', 'nation', comparisonIntegerList($observe['nationIds'] ?? [], 'nationIds')),
|
||||||
|
);
|
||||||
|
$worldHistory = array_map(
|
||||||
|
static fn(array $row): array => comparisonPickRow(
|
||||||
|
$row,
|
||||||
|
[
|
||||||
|
'id' => 'id',
|
||||||
|
'nationId' => 'nation_id',
|
||||||
|
'year' => 'year',
|
||||||
|
'month' => 'month',
|
||||||
|
'text' => 'text',
|
||||||
|
],
|
||||||
|
),
|
||||||
|
$db->query(
|
||||||
|
'SELECT id, nation_id, year, month, text FROM world_history WHERE id > %i ORDER BY id',
|
||||||
|
$worldHistoryAfterId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
$inheritanceKeys = $observe['inheritanceKeys'] ?? ['unifier'];
|
||||||
|
if (!is_array($inheritanceKeys) || array_filter($inheritanceKeys, static fn($key): bool => !is_string($key))) {
|
||||||
|
throw new \InvalidArgumentException('inheritanceKeys must be an array of strings');
|
||||||
|
}
|
||||||
|
$inheritancePoints = [];
|
||||||
|
foreach (comparisonIntegerList($observe['ownerIds'] ?? [], 'ownerIds') as $ownerId) {
|
||||||
|
$entry = ['ownerId' => $ownerId];
|
||||||
|
foreach ($inheritanceKeys as $key) {
|
||||||
|
$value = $db->queryFirstField(
|
||||||
|
'SELECT value FROM storage WHERE namespace = %s AND `key` = %s',
|
||||||
|
"inheritance_{$ownerId}",
|
||||||
|
$key,
|
||||||
|
);
|
||||||
|
$entry[$key] = $value === null ? 0 : (comparisonJsonValue($value)[0] ?? 0);
|
||||||
|
}
|
||||||
|
$inheritancePoints[] = $entry;
|
||||||
|
}
|
||||||
|
$rankGeneralIds = comparisonIntegerList($observe['rankGeneralIds'] ?? [], 'rankGeneralIds');
|
||||||
|
$rankTypes = $observe['rankTypes'] ?? [];
|
||||||
|
if (!is_array($rankTypes) || array_filter($rankTypes, static fn($type): bool => !is_string($type))) {
|
||||||
|
throw new \InvalidArgumentException('rankTypes must be an array of strings');
|
||||||
|
}
|
||||||
|
$rankData = (!$rankGeneralIds || !$rankTypes)
|
||||||
|
? []
|
||||||
|
: iterator_to_array($db->query(
|
||||||
|
'SELECT general_id AS generalId, nation_id AS nationId, `type`, `value` FROM rank_data WHERE general_id IN %li AND `type` IN %ls ORDER BY general_id, `type`',
|
||||||
|
$rankGeneralIds,
|
||||||
|
$rankTypes,
|
||||||
|
));
|
||||||
|
$generalAccessLogs = [];
|
||||||
|
$accessGeneralIds = comparisonIntegerList($observe['accessGeneralIds'] ?? [], 'accessGeneralIds');
|
||||||
|
if ($accessGeneralIds) {
|
||||||
|
$generalAccessLogs = iterator_to_array($db->query(
|
||||||
|
'SELECT general_id AS generalId, refresh_score_total AS refreshScoreTotal FROM general_access_log WHERE general_id IN %li ORDER BY general_id',
|
||||||
|
$accessGeneralIds,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
$nationEnvironment = [];
|
||||||
|
$nationEnvironmentKeys = $observe['nationEnvironmentKeys'] ?? [];
|
||||||
|
if (
|
||||||
|
!is_array($nationEnvironmentKeys)
|
||||||
|
|| array_filter($nationEnvironmentKeys, static fn($key): bool => !is_string($key))
|
||||||
|
) {
|
||||||
|
throw new \InvalidArgumentException('nationEnvironmentKeys must be an array of strings');
|
||||||
|
}
|
||||||
|
foreach (comparisonIntegerList($observe['nationIds'] ?? [], 'nationIds') as $nationId) {
|
||||||
|
$entry = ['nationId' => $nationId];
|
||||||
|
$storage = KVStorage::getStorage($db, $nationId, 'nation_env');
|
||||||
|
foreach ($nationEnvironmentKeys as $key) {
|
||||||
|
$entry[$key] = $storage->getValue($key);
|
||||||
|
}
|
||||||
|
$nationEnvironment[] = $entry;
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
'generals' => $generals,
|
||||||
|
'cities' => $cities,
|
||||||
|
'nations' => $nations,
|
||||||
|
'worldHistory' => $worldHistory,
|
||||||
|
'inheritancePoints' => $inheritancePoints,
|
||||||
|
'rankData' => $rankData,
|
||||||
|
'generalAccessLogs' => $generalAccessLogs,
|
||||||
|
'nationEnvironment' => $nationEnvironment,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exercise the dynamically-created invader events against one evolving database.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $request
|
||||||
|
* @param array<string, mixed> $environment
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
function comparisonInvaderLifecycleTrace(
|
||||||
|
array $request,
|
||||||
|
array $environment,
|
||||||
|
int $generalIdBeforeAction,
|
||||||
|
int $nationIdBeforeAction,
|
||||||
|
int $eventIdBeforeAction,
|
||||||
|
int $worldHistoryIdBeforeAction,
|
||||||
|
): array {
|
||||||
|
$db = DB::db();
|
||||||
|
$args = $request['args'] ?? [];
|
||||||
|
if (!is_array($args) || count($args) > 4) {
|
||||||
|
throw new \InvalidArgumentException('InvaderLifecycle args must have at most four entries');
|
||||||
|
}
|
||||||
|
$year = $environment['year'];
|
||||||
|
$month = $environment['month'];
|
||||||
|
$startYear = $environment['startyear'];
|
||||||
|
$raiseEnvironment = [
|
||||||
|
'year' => $year,
|
||||||
|
'month' => $month,
|
||||||
|
'startyear' => $startYear,
|
||||||
|
'turnterm' => $environment['turnterm'],
|
||||||
|
'turntime' => $environment['turntime'],
|
||||||
|
'show_img_level' => $environment['show_img_level'] ?? 3,
|
||||||
|
'stored_icons' => $environment['stored_icons'] ?? [],
|
||||||
|
'icon_path' => $environment['icon_path'] ?? '.',
|
||||||
|
'fiction' => $environment['fiction'] ?? [0],
|
||||||
|
];
|
||||||
|
|
||||||
|
(new \sammo\Event\Action\RaiseInvader(...$args))->run($raiseEnvironment);
|
||||||
|
|
||||||
|
$invaderNationIds = array_map(
|
||||||
|
'intval',
|
||||||
|
$db->queryFirstColumn('SELECT nation FROM nation WHERE nation > %i ORDER BY nation', $nationIdBeforeAction),
|
||||||
|
);
|
||||||
|
$existingNationId = (int)$db->queryFirstField(
|
||||||
|
'SELECT nation FROM nation WHERE nation <= %i ORDER BY nation LIMIT 1',
|
||||||
|
$nationIdBeforeAction,
|
||||||
|
);
|
||||||
|
$rulerIds = array_map(
|
||||||
|
'intval',
|
||||||
|
$db->queryFirstColumn(
|
||||||
|
'SELECT no FROM general WHERE nation IN %li AND officer_level = 12 ORDER BY nation',
|
||||||
|
$invaderNationIds,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if ($invaderNationIds === [] || $existingNationId <= 0 || count($rulerIds) !== count($invaderNationIds)) {
|
||||||
|
throw new \RuntimeException('InvaderLifecycle failed to create the expected entities');
|
||||||
|
}
|
||||||
|
|
||||||
|
$autoDeleteEvents = [];
|
||||||
|
$endingEventId = 0;
|
||||||
|
foreach ($db->query('SELECT id, action FROM event WHERE id > %i ORDER BY id', $eventIdBeforeAction) as $row) {
|
||||||
|
$action = comparisonJsonValue($row['action']);
|
||||||
|
$actionName = $action[0][0] ?? null;
|
||||||
|
if ($actionName === 'AutoDeleteInvader') {
|
||||||
|
$autoDeleteEvents[] = [
|
||||||
|
'eventId' => (int)$row['id'],
|
||||||
|
'nationId' => (int)($action[0][1] ?? 0),
|
||||||
|
];
|
||||||
|
} elseif ($actionName === 'InvaderEnding') {
|
||||||
|
$endingEventId = (int)$row['id'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (count($autoDeleteEvents) !== count($invaderNationIds) || $endingEventId <= 0) {
|
||||||
|
throw new \RuntimeException('InvaderLifecycle failed to create follow-up events');
|
||||||
|
}
|
||||||
|
|
||||||
|
$diplomacyRows = iterator_to_array($db->query(
|
||||||
|
'SELECT me, you, state, term FROM diplomacy WHERE (me IN %li AND you = %i) OR (me = %i AND you IN %li) ORDER BY me, you',
|
||||||
|
$invaderNationIds,
|
||||||
|
$existingNationId,
|
||||||
|
$existingNationId,
|
||||||
|
$invaderNationIds,
|
||||||
|
));
|
||||||
|
$diplomacyStates = array_values(array_unique(array_map(
|
||||||
|
static fn(array $row): string => "{$row['state']}:{$row['term']}",
|
||||||
|
$diplomacyRows,
|
||||||
|
)));
|
||||||
|
sort($diplomacyStates);
|
||||||
|
$generalCounts = array_map(
|
||||||
|
static fn(int $nationId): int => (int)DB::db()->queryFirstField(
|
||||||
|
'SELECT count(*) FROM general WHERE nation = %i',
|
||||||
|
$nationId,
|
||||||
|
),
|
||||||
|
$invaderNationIds,
|
||||||
|
);
|
||||||
|
$diplomacyCounts = array_map(
|
||||||
|
static fn(int $nationId): int => (int)DB::db()->queryFirstField(
|
||||||
|
'SELECT count(*) FROM diplomacy WHERE (me = %i AND you = %i) OR (me = %i AND you = %i)',
|
||||||
|
$nationId,
|
||||||
|
$existingNationId,
|
||||||
|
$existingNationId,
|
||||||
|
$nationId,
|
||||||
|
),
|
||||||
|
$invaderNationIds,
|
||||||
|
);
|
||||||
|
$gameStorage = KVStorage::getStorage($db, 'game_env');
|
||||||
|
$afterRaise = [
|
||||||
|
'createdNationCount' => count($invaderNationIds),
|
||||||
|
'createdGeneralCount' => (int)$db->queryFirstField(
|
||||||
|
'SELECT count(*) FROM general WHERE no > %i',
|
||||||
|
$generalIdBeforeAction,
|
||||||
|
),
|
||||||
|
'followUpEventCount' => (int)$db->queryFirstField(
|
||||||
|
'SELECT count(*) FROM event WHERE id > %i',
|
||||||
|
$eventIdBeforeAction,
|
||||||
|
),
|
||||||
|
'generalCountsPerNation' => array_values(array_unique($generalCounts)),
|
||||||
|
'diplomacyCountsPerNation' => array_values(array_unique($diplomacyCounts)),
|
||||||
|
'diplomacyStates' => $diplomacyStates,
|
||||||
|
'isunited' => (int)$gameStorage->isunited,
|
||||||
|
'blockChangeScout' => (bool)$gameStorage->block_change_scout,
|
||||||
|
];
|
||||||
|
|
||||||
|
$warResults = [];
|
||||||
|
foreach ($autoDeleteEvents as $autoDeleteEvent) {
|
||||||
|
$warResult = (new \sammo\Event\Action\AutoDeleteInvader($autoDeleteEvent['nationId']))->run([
|
||||||
|
'year' => $year,
|
||||||
|
'month' => $month + 1,
|
||||||
|
'startyear' => $startYear,
|
||||||
|
'currentEventID' => $autoDeleteEvent['eventId'],
|
||||||
|
]);
|
||||||
|
$warResults[] = $warResult[1] ?? null;
|
||||||
|
}
|
||||||
|
$atWar = [
|
||||||
|
'results' => array_values(array_unique($warResults)),
|
||||||
|
'autoDeleteEventCount' => (int)$db->queryFirstField(
|
||||||
|
'SELECT count(*) FROM event WHERE id IN %li',
|
||||||
|
array_column($autoDeleteEvents, 'eventId'),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
$db->update(
|
||||||
|
'diplomacy',
|
||||||
|
['state' => 2, 'term' => 0],
|
||||||
|
'me IN %li OR you IN %li',
|
||||||
|
$invaderNationIds,
|
||||||
|
$invaderNationIds,
|
||||||
|
);
|
||||||
|
$peaceResults = [];
|
||||||
|
foreach ($autoDeleteEvents as $autoDeleteEvent) {
|
||||||
|
$peaceResult = (new \sammo\Event\Action\AutoDeleteInvader($autoDeleteEvent['nationId']))->run([
|
||||||
|
'year' => $year,
|
||||||
|
'month' => $month + 2,
|
||||||
|
'startyear' => $startYear,
|
||||||
|
'currentEventID' => $autoDeleteEvent['eventId'],
|
||||||
|
]);
|
||||||
|
$peaceResults[] = $peaceResult[1] ?? null;
|
||||||
|
}
|
||||||
|
$atPeace = [
|
||||||
|
'results' => array_values(array_unique($peaceResults)),
|
||||||
|
'autoDeleteEventCount' => (int)$db->queryFirstField(
|
||||||
|
'SELECT count(*) FROM event WHERE id IN %li',
|
||||||
|
array_column($autoDeleteEvents, 'eventId'),
|
||||||
|
),
|
||||||
|
'rulerWanderTurnCount' => (int)$db->queryFirstField(
|
||||||
|
'SELECT count(*) FROM general_turn WHERE general_id IN %li AND action = %s',
|
||||||
|
$rulerIds,
|
||||||
|
'che_방랑',
|
||||||
|
),
|
||||||
|
'endingEventPresent' => (bool)$db->queryFirstField(
|
||||||
|
'SELECT count(*) FROM event WHERE id = %i',
|
||||||
|
$endingEventId,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
$invaderGeneralIds = array_map('intval', $db->queryFirstColumn(
|
||||||
|
'SELECT no FROM general WHERE nation IN %li',
|
||||||
|
$invaderNationIds,
|
||||||
|
));
|
||||||
|
if ($invaderGeneralIds !== []) {
|
||||||
|
$db->delete('general_turn', 'general_id IN %li', $invaderGeneralIds);
|
||||||
|
$db->delete('rank_data', 'general_id IN %li', $invaderGeneralIds);
|
||||||
|
$db->delete('troop', 'nation IN %li', $invaderNationIds);
|
||||||
|
$db->delete('general', 'no IN %li', $invaderGeneralIds);
|
||||||
|
}
|
||||||
|
$db->delete('nation_turn', 'nation_id IN %li', $invaderNationIds);
|
||||||
|
$db->delete('diplomacy', 'me IN %li OR you IN %li', $invaderNationIds, $invaderNationIds);
|
||||||
|
$db->delete('nation', 'nation IN %li', $invaderNationIds);
|
||||||
|
$db->update('city', ['nation' => $existingNationId], true);
|
||||||
|
|
||||||
|
$endingResult = (new \sammo\Event\Action\InvaderEnding())->run([
|
||||||
|
'year' => $year,
|
||||||
|
'month' => $month + 3,
|
||||||
|
'startyear' => $startYear,
|
||||||
|
'currentEventID' => $endingEventId,
|
||||||
|
]);
|
||||||
|
$gameStorage->resetCache();
|
||||||
|
$afterUserWin = [
|
||||||
|
'result' => $endingResult[1] ?? null,
|
||||||
|
'endingEventPresent' => (bool)$db->queryFirstField(
|
||||||
|
'SELECT count(*) FROM event WHERE id = %i',
|
||||||
|
$endingEventId,
|
||||||
|
),
|
||||||
|
'isunited' => (int)$gameStorage->isunited,
|
||||||
|
'refreshLimit' => (int)$gameStorage->refreshLimit,
|
||||||
|
'logs' => array_values($db->queryFirstColumn(
|
||||||
|
'SELECT text FROM world_history WHERE id > %i AND nation_id = 0 AND year = %i AND month = %i ORDER BY id',
|
||||||
|
$worldHistoryIdBeforeAction,
|
||||||
|
$year,
|
||||||
|
$month + 3,
|
||||||
|
)),
|
||||||
|
];
|
||||||
|
|
||||||
|
return [
|
||||||
|
'schemaVersion' => 1,
|
||||||
|
'engine' => 'ref',
|
||||||
|
'action' => 'InvaderLifecycle',
|
||||||
|
'phases' => [
|
||||||
|
'afterRaise' => $afterRaise,
|
||||||
|
'atWar' => $atWar,
|
||||||
|
'atPeace' => $atPeace,
|
||||||
|
'afterUserWin' => $afterUserWin,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exercise NPC nation creation and the same-month troop leader follow-up.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $environment
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
function comparisonNpcNationLifecycleTrace(
|
||||||
|
array $environment,
|
||||||
|
int $generalIdBeforeAction,
|
||||||
|
int $nationIdBeforeAction,
|
||||||
|
int $worldHistoryIdBeforeAction,
|
||||||
|
): array {
|
||||||
|
$db = DB::db();
|
||||||
|
$actionEnvironment = [
|
||||||
|
'year' => $environment['year'],
|
||||||
|
'month' => $environment['month'],
|
||||||
|
'startyear' => $environment['startyear'],
|
||||||
|
'turnterm' => $environment['turnterm'],
|
||||||
|
'turntime' => $environment['turntime'],
|
||||||
|
'show_img_level' => $environment['show_img_level'] ?? 3,
|
||||||
|
'stored_icons' => $environment['stored_icons'] ?? [],
|
||||||
|
'icon_path' => $environment['icon_path'] ?? '.',
|
||||||
|
'fiction' => $environment['fiction'] ?? [0],
|
||||||
|
];
|
||||||
|
$gameStorage = KVStorage::getStorage($db, 'game_env');
|
||||||
|
$leaderCounterBefore = (int)($gameStorage->lastNPCTroopLeaderID ?? 0);
|
||||||
|
|
||||||
|
(new \sammo\Event\Action\RaiseNPCNation())->run($actionEnvironment);
|
||||||
|
$createdNationIds = array_map(
|
||||||
|
'intval',
|
||||||
|
$db->queryFirstColumn('SELECT nation FROM nation WHERE nation > %i ORDER BY nation', $nationIdBeforeAction),
|
||||||
|
);
|
||||||
|
if ($createdNationIds === []) {
|
||||||
|
throw new \RuntimeException('NpcNationLifecycle failed to create a nation');
|
||||||
|
}
|
||||||
|
$createdNationGeneralCounts = array_map(
|
||||||
|
static fn(int $nationId): int => (int)DB::db()->queryFirstField(
|
||||||
|
'SELECT count(*) FROM general WHERE nation = %i',
|
||||||
|
$nationId,
|
||||||
|
),
|
||||||
|
$createdNationIds,
|
||||||
|
);
|
||||||
|
$afterRaise = [
|
||||||
|
'createdNationCount' => count($createdNationIds),
|
||||||
|
'createdGeneralCount' => (int)$db->queryFirstField(
|
||||||
|
'SELECT count(*) FROM general WHERE no > %i',
|
||||||
|
$generalIdBeforeAction,
|
||||||
|
),
|
||||||
|
'generalCountsPerCreatedNation' => array_values(array_unique($createdNationGeneralCounts)),
|
||||||
|
'historyLogs' => array_values($db->queryFirstColumn(
|
||||||
|
'SELECT text FROM world_history WHERE id > %i AND nation_id = 0 ORDER BY id',
|
||||||
|
$worldHistoryIdBeforeAction,
|
||||||
|
)),
|
||||||
|
];
|
||||||
|
|
||||||
|
(new \sammo\Event\Action\ProvideNPCTroopLeader())->run($actionEnvironment);
|
||||||
|
$leaderIds = array_map(
|
||||||
|
'intval',
|
||||||
|
$db->queryFirstColumn('SELECT no FROM general WHERE no > %i AND npc = 5 ORDER BY no', $generalIdBeforeAction),
|
||||||
|
);
|
||||||
|
if ($leaderIds === []) {
|
||||||
|
throw new \RuntimeException('NpcNationLifecycle failed to create troop leaders');
|
||||||
|
}
|
||||||
|
$leaderNationCounts = array_map(
|
||||||
|
static fn(int $nationId): int => (int)DB::db()->queryFirstField(
|
||||||
|
'SELECT count(*) FROM general WHERE nation = %i AND npc = 5',
|
||||||
|
$nationId,
|
||||||
|
),
|
||||||
|
array_map(
|
||||||
|
'intval',
|
||||||
|
$db->queryFirstColumn('SELECT nation FROM nation WHERE level > 0 ORDER BY nation'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
$gatherTurnCounts = array_map(
|
||||||
|
static fn(int $generalId): int => (int)DB::db()->queryFirstField(
|
||||||
|
'SELECT count(*) FROM general_turn WHERE general_id = %i AND action = %s',
|
||||||
|
$generalId,
|
||||||
|
'che_집합',
|
||||||
|
),
|
||||||
|
$leaderIds,
|
||||||
|
);
|
||||||
|
$gameStorage->resetCache();
|
||||||
|
$afterProvide = [
|
||||||
|
'createdLeaderCount' => count($leaderIds),
|
||||||
|
'leaderCountsPerActiveNation' => array_values(array_unique($leaderNationCounts)),
|
||||||
|
'troopCount' => (int)$db->queryFirstField(
|
||||||
|
'SELECT count(*) FROM troop WHERE troop_leader IN %li',
|
||||||
|
$leaderIds,
|
||||||
|
),
|
||||||
|
'gatherTurnCounts' => array_values(array_unique($gatherTurnCounts)),
|
||||||
|
'leaderCounterDelta' => (int)$gameStorage->lastNPCTroopLeaderID - $leaderCounterBefore,
|
||||||
|
];
|
||||||
|
|
||||||
|
return [
|
||||||
|
'schemaVersion' => 1,
|
||||||
|
'engine' => 'ref',
|
||||||
|
'action' => 'NpcNationLifecycle',
|
||||||
|
'phases' => [
|
||||||
|
'afterRaise' => $afterRaise,
|
||||||
|
'afterProvide' => $afterProvide,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exercise nation betting from monthly open through real user bets and settlement.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $request
|
||||||
|
* @param array<string, mixed> $environment
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
function comparisonNationBettingLifecycleTrace(
|
||||||
|
array $request,
|
||||||
|
array $environment,
|
||||||
|
int $eventIdBeforeAction,
|
||||||
|
int $worldHistoryIdBeforeAction,
|
||||||
|
): array {
|
||||||
|
$db = DB::db();
|
||||||
|
$args = $request['args'] ?? [];
|
||||||
|
if (!is_array($args) || count($args) > 2) {
|
||||||
|
throw new \InvalidArgumentException('NationBettingLifecycle args must have at most two entries');
|
||||||
|
}
|
||||||
|
$bettorGeneralIds = comparisonIntegerList(
|
||||||
|
$request['bettorGeneralIds'] ?? [],
|
||||||
|
'bettorGeneralIds',
|
||||||
|
);
|
||||||
|
$bettorOwnerIds = comparisonIntegerList(
|
||||||
|
$request['bettorOwnerIds'] ?? [],
|
||||||
|
'bettorOwnerIds',
|
||||||
|
);
|
||||||
|
if (count($bettorGeneralIds) !== 2 || count($bettorOwnerIds) !== 2) {
|
||||||
|
throw new \InvalidArgumentException(
|
||||||
|
'NationBettingLifecycle requires exactly two bettorGeneralIds and bettorOwnerIds',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$year = $environment['year'];
|
||||||
|
$month = $environment['month'];
|
||||||
|
$startYear = $environment['startyear'];
|
||||||
|
(new \sammo\Event\Action\OpenNationBetting(...$args))->run([
|
||||||
|
'year' => $year,
|
||||||
|
'month' => $month,
|
||||||
|
'startyear' => $startYear,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$gameStorage = KVStorage::getStorage($db, 'game_env');
|
||||||
|
$gameStorage->resetCache();
|
||||||
|
$bettingId = (int)$gameStorage->last_betting_id;
|
||||||
|
$betting = new Betting($bettingId);
|
||||||
|
$bettingInfo = $betting->getInfo();
|
||||||
|
$candidateNationIds = array_map(
|
||||||
|
static fn(\sammo\DTO\SelectItem $candidate): int => (int)($candidate->aux['nation'] ?? 0),
|
||||||
|
$bettingInfo->candidates,
|
||||||
|
);
|
||||||
|
if (count($candidateNationIds) !== 2) {
|
||||||
|
throw new \RuntimeException('NationBettingLifecycle requires exactly two candidates');
|
||||||
|
}
|
||||||
|
|
||||||
|
$finishEventCount = (int)$db->queryFirstField(
|
||||||
|
'SELECT count(*) FROM event WHERE id > %i AND action LIKE %s',
|
||||||
|
$eventIdBeforeAction,
|
||||||
|
"%FinishNationBetting%{$bettingId}%",
|
||||||
|
);
|
||||||
|
$afterOpen = [
|
||||||
|
'bettingId' => $bettingId,
|
||||||
|
'candidateNationIds' => $candidateNationIds,
|
||||||
|
'systemBonus' => (int)($db->queryFirstField(
|
||||||
|
'SELECT amount FROM ng_betting WHERE betting_id = %i AND general_id = 0',
|
||||||
|
$bettingId,
|
||||||
|
) ?? 0),
|
||||||
|
'finishEventCount' => $finishEventCount,
|
||||||
|
'historyLogs' => array_values($db->queryFirstColumn(
|
||||||
|
'SELECT text FROM world_history WHERE id > %i AND nation_id = 0 ORDER BY id',
|
||||||
|
$worldHistoryIdBeforeAction,
|
||||||
|
)),
|
||||||
|
];
|
||||||
|
|
||||||
|
$betting->bet($bettorGeneralIds[0], $bettorOwnerIds[0], [1], 100);
|
||||||
|
$betting->bet($bettorGeneralIds[1], $bettorOwnerIds[1], [0], 100);
|
||||||
|
$afterBets = [
|
||||||
|
'bets' => array_map(
|
||||||
|
static fn(array $row): array => [
|
||||||
|
'generalId' => (int)$row['general_id'],
|
||||||
|
'ownerId' => $row['user_id'] === null ? null : (int)$row['user_id'],
|
||||||
|
'selectionKey' => $row['betting_type'],
|
||||||
|
'amount' => (int)$row['amount'],
|
||||||
|
],
|
||||||
|
iterator_to_array($db->query(
|
||||||
|
'SELECT general_id, user_id, betting_type, amount FROM ng_betting WHERE betting_id = %i ORDER BY general_id',
|
||||||
|
$bettingId,
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
'inheritancePrevious' => array_map(
|
||||||
|
static function (int $ownerId): int {
|
||||||
|
$value = KVStorage::getStorage(DB::db(), "inheritance_{$ownerId}")->getValue('previous');
|
||||||
|
return is_array($value) ? (int)($value[0] ?? 0) : 0;
|
||||||
|
},
|
||||||
|
$bettorOwnerIds,
|
||||||
|
),
|
||||||
|
'spentRank' => array_map(
|
||||||
|
static fn(int $generalId): int => (int)(DB::db()->queryFirstField(
|
||||||
|
'SELECT value FROM rank_data WHERE general_id = %i AND type = %s',
|
||||||
|
$generalId,
|
||||||
|
\sammo\Enums\RankColumn::inherit_point_spent_dynamic->value,
|
||||||
|
) ?? 0),
|
||||||
|
$bettorGeneralIds,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
$db->update('nation', ['level' => 0], 'nation = %i', $candidateNationIds[1]);
|
||||||
|
$finishResult = (new \sammo\Event\Action\FinishNationBetting($bettingId))->run([
|
||||||
|
'year' => $year,
|
||||||
|
'month' => $month + 1,
|
||||||
|
'startyear' => $startYear,
|
||||||
|
]);
|
||||||
|
$settledInfo = (new Betting($bettingId))->getInfo();
|
||||||
|
$afterFinish = [
|
||||||
|
'result' => $finishResult[1] ?? null,
|
||||||
|
'finished' => $settledInfo->finished,
|
||||||
|
'winner' => $settledInfo->winner,
|
||||||
|
'inheritancePrevious' => array_map(
|
||||||
|
static function (int $ownerId): int {
|
||||||
|
$value = KVStorage::getStorage(DB::db(), "inheritance_{$ownerId}")->getValue('previous');
|
||||||
|
return is_array($value) ? (int)($value[0] ?? 0) : 0;
|
||||||
|
},
|
||||||
|
$bettorOwnerIds,
|
||||||
|
),
|
||||||
|
'earnedRank' => array_map(
|
||||||
|
static fn(int $generalId): int => (int)(DB::db()->queryFirstField(
|
||||||
|
'SELECT value FROM rank_data WHERE general_id = %i AND type = %s',
|
||||||
|
$generalId,
|
||||||
|
\sammo\Enums\RankColumn::inherit_point_earned_by_action->value,
|
||||||
|
) ?? 0),
|
||||||
|
$bettorGeneralIds,
|
||||||
|
),
|
||||||
|
'historyLogs' => array_values($db->queryFirstColumn(
|
||||||
|
'SELECT text FROM world_history WHERE id > %i AND nation_id = 0 ORDER BY id',
|
||||||
|
$worldHistoryIdBeforeAction,
|
||||||
|
)),
|
||||||
|
];
|
||||||
|
|
||||||
|
return [
|
||||||
|
'schemaVersion' => 1,
|
||||||
|
'engine' => 'ref',
|
||||||
|
'action' => 'NationBettingLifecycle',
|
||||||
|
'phases' => [
|
||||||
|
'afterOpen' => $afterOpen,
|
||||||
|
'afterBets' => $afterBets,
|
||||||
|
'afterFinish' => $afterFinish,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function comparisonMonthlyEventTraceMain(): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$input = stream_get_contents(STDIN);
|
||||||
|
$request = json_decode($input === '' ? '{}' : $input, true, flags: JSON_THROW_ON_ERROR);
|
||||||
|
if (!is_array($request)) {
|
||||||
|
throw new \InvalidArgumentException('request must be an object');
|
||||||
|
}
|
||||||
|
$actionName = $request['action'] ?? null;
|
||||||
|
$supportedActions = [
|
||||||
|
'UpdateCitySupply',
|
||||||
|
'UpdateNationLevel',
|
||||||
|
'RaiseDisaster',
|
||||||
|
'ProcessSemiAnnual',
|
||||||
|
'ProcessWarIncome',
|
||||||
|
'CreateAdminNPC',
|
||||||
|
'CreateManyNPC',
|
||||||
|
'RegNPC',
|
||||||
|
'RegNeutralNPC',
|
||||||
|
'RaiseNPCNation',
|
||||||
|
'NpcNationLifecycle',
|
||||||
|
'RaiseInvader',
|
||||||
|
'InvaderLifecycle',
|
||||||
|
'AutoDeleteInvader',
|
||||||
|
'InvaderEnding',
|
||||||
|
'ChangeCity',
|
||||||
|
'ProvideNPCTroopLeader',
|
||||||
|
'NationBettingLifecycle',
|
||||||
|
'OpenNationBetting',
|
||||||
|
'FinishNationBetting',
|
||||||
|
'BlockScoutAction',
|
||||||
|
'UnblockScoutAction',
|
||||||
|
'AssignGeneralSpeciality',
|
||||||
|
'AddGlobalBetray',
|
||||||
|
'LostUniqueItem',
|
||||||
|
'MergeInheritPointRank',
|
||||||
|
'ProcessIncome',
|
||||||
|
'NoticeToHistoryLog',
|
||||||
|
'NewYear',
|
||||||
|
'ResetOfficerLock',
|
||||||
|
'PreUpdateMonthly',
|
||||||
|
'PostUpdateMonthly',
|
||||||
|
'MonthlyBoundary',
|
||||||
|
];
|
||||||
|
if (!in_array($actionName, $supportedActions, true)) {
|
||||||
|
throw new \InvalidArgumentException('unsupported monthly action');
|
||||||
|
}
|
||||||
|
$setup = $request['setup'] ?? [];
|
||||||
|
if (!is_array($setup)) {
|
||||||
|
throw new \InvalidArgumentException('setup must be an object');
|
||||||
|
}
|
||||||
|
foreach (['city', 'nation', 'general'] as $table) {
|
||||||
|
$rows = $setup[$table] ?? [];
|
||||||
|
if (!is_array($rows)) {
|
||||||
|
throw new \InvalidArgumentException("setup.{$table} must be an array");
|
||||||
|
}
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
if (!is_array($row) || !is_int($row['id'] ?? null) || !is_array($row['values'] ?? null)) {
|
||||||
|
throw new \InvalidArgumentException("invalid setup.{$table} row");
|
||||||
|
}
|
||||||
|
comparisonMonthlyPatch($table, $row['id'], $row['values']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (($setup['resetCities'] ?? false) === true) {
|
||||||
|
DB::db()->query('UPDATE city SET nation = 0, level = 1');
|
||||||
|
foreach ($setup['city'] ?? [] as $row) {
|
||||||
|
comparisonMonthlyPatch('city', $row['id'], $row['values']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (($setup['deleteOtherCities'] ?? false) === true) {
|
||||||
|
$keepCityIds = comparisonIntegerList($setup['keepCityIds'] ?? [], 'keepCityIds');
|
||||||
|
if ($keepCityIds === []) {
|
||||||
|
throw new \InvalidArgumentException('deleteOtherCities requires keepCityIds');
|
||||||
|
}
|
||||||
|
DB::db()->query('DELETE FROM city WHERE city NOT IN %li', $keepCityIds);
|
||||||
|
}
|
||||||
|
if (($setup['resetGenerals'] ?? false) === true) {
|
||||||
|
DB::db()->query(
|
||||||
|
'UPDATE general SET nation = 0, officer_level = 1, npc = 2, killturn = 0, horse = %s, weapon = %s, book = %s, item = %s',
|
||||||
|
'None',
|
||||||
|
'None',
|
||||||
|
'None',
|
||||||
|
'None',
|
||||||
|
);
|
||||||
|
foreach ($setup['general'] ?? [] as $row) {
|
||||||
|
comparisonMonthlyPatch('general', $row['id'], $row['values']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (($setup['deleteOtherGenerals'] ?? false) === true) {
|
||||||
|
$keepGeneralIds = comparisonIntegerList($setup['keepGeneralIds'] ?? [], 'keepGeneralIds');
|
||||||
|
if (!$keepGeneralIds) {
|
||||||
|
throw new \InvalidArgumentException('deleteOtherGenerals requires keepGeneralIds');
|
||||||
|
}
|
||||||
|
DB::db()->query('DELETE FROM general_turn WHERE general_id NOT IN %li', $keepGeneralIds);
|
||||||
|
DB::db()->query('DELETE FROM rank_data WHERE general_id NOT IN %li', $keepGeneralIds);
|
||||||
|
DB::db()->query('DELETE FROM general WHERE no NOT IN %li', $keepGeneralIds);
|
||||||
|
}
|
||||||
|
if (($setup['resetNations'] ?? false) === true) {
|
||||||
|
DB::db()->query('UPDATE nation SET level = 0, gennum = 0, tech = 0');
|
||||||
|
foreach ($setup['nation'] ?? [] as $row) {
|
||||||
|
comparisonMonthlyPatch('nation', $row['id'], $row['values']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (($setup['deleteOtherNations'] ?? false) === true) {
|
||||||
|
$keepNationIds = comparisonIntegerList($setup['keepNationIds'] ?? [], 'keepNationIds');
|
||||||
|
if (!$keepNationIds) {
|
||||||
|
throw new \InvalidArgumentException('deleteOtherNations requires keepNationIds');
|
||||||
|
}
|
||||||
|
DB::db()->query(
|
||||||
|
'DELETE FROM diplomacy WHERE me NOT IN %li OR you NOT IN %li',
|
||||||
|
$keepNationIds,
|
||||||
|
$keepNationIds,
|
||||||
|
);
|
||||||
|
DB::db()->query('DELETE FROM nation_turn WHERE nation_id NOT IN %li', $keepNationIds);
|
||||||
|
DB::db()->query('DELETE FROM nation WHERE nation NOT IN %li', $keepNationIds);
|
||||||
|
}
|
||||||
|
if (($setup['resetUniqueOccupancy'] ?? false) === true) {
|
||||||
|
DB::db()->delete('ng_auction', '`type` = %s AND finished = 0', 'uniqueItem');
|
||||||
|
DB::db()->query('DELETE FROM storage WHERE namespace LIKE %s', 'ut\\_%');
|
||||||
|
}
|
||||||
|
if (($setup['clearNeutralAuctions'] ?? false) === true) {
|
||||||
|
DB::db()->delete(
|
||||||
|
'ng_auction',
|
||||||
|
'`type` IN %ls AND host_general_id = 0',
|
||||||
|
['buyRice', 'sellRice'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
foreach (comparisonIntegerList($setup['clearNationTurnIds'] ?? [], 'clearNationTurnIds') as $nationId) {
|
||||||
|
DB::db()->delete('nation_turn', 'nation_id = %i', $nationId);
|
||||||
|
}
|
||||||
|
foreach (comparisonIntegerList($setup['clearInheritanceOwnerIds'] ?? [], 'clearInheritanceOwnerIds') as $ownerId) {
|
||||||
|
DB::db()->delete('storage', 'namespace = %s', "inheritance_{$ownerId}");
|
||||||
|
}
|
||||||
|
if (($setup['clearBetting'] ?? false) === true) {
|
||||||
|
DB::db()->query('DELETE FROM ng_betting');
|
||||||
|
DB::db()->query('DELETE FROM storage WHERE namespace = %s', 'betting');
|
||||||
|
$gameStorage = KVStorage::getStorage(DB::db(), 'game_env');
|
||||||
|
$gameStorage->setValue('last_betting_id', 0);
|
||||||
|
}
|
||||||
|
foreach ($setup['bettingInfo'] ?? [] as $rawBettingInfo) {
|
||||||
|
if (!is_array($rawBettingInfo) || !is_int($rawBettingInfo['id'] ?? null)) {
|
||||||
|
throw new \InvalidArgumentException('invalid setup.bettingInfo row');
|
||||||
|
}
|
||||||
|
KVStorage::getStorage(DB::db(), 'betting')->setValue(
|
||||||
|
"id_{$rawBettingInfo['id']}",
|
||||||
|
$rawBettingInfo,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
foreach ($setup['bet'] ?? [] as $row) {
|
||||||
|
if (
|
||||||
|
!is_array($row)
|
||||||
|
|| !is_int($row['betting_id'] ?? null)
|
||||||
|
|| !is_int($row['general_id'] ?? null)
|
||||||
|
|| !is_string($row['betting_type'] ?? null)
|
||||||
|
|| !is_int($row['amount'] ?? null)
|
||||||
|
) {
|
||||||
|
throw new \InvalidArgumentException('invalid setup.bet row');
|
||||||
|
}
|
||||||
|
DB::db()->insert('ng_betting', [
|
||||||
|
'betting_id' => $row['betting_id'],
|
||||||
|
'general_id' => $row['general_id'],
|
||||||
|
'user_id' => $row['user_id'] ?? null,
|
||||||
|
'betting_type' => $row['betting_type'],
|
||||||
|
'amount' => $row['amount'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
foreach ($setup['inheritancePrevious'] ?? [] as $ownerId => $value) {
|
||||||
|
if (!is_numeric($ownerId) || !is_int($value)) {
|
||||||
|
throw new \InvalidArgumentException('invalid setup.inheritancePrevious entry');
|
||||||
|
}
|
||||||
|
KVStorage::getStorage(DB::db(), "inheritance_{$ownerId}")->setValue('previous', [$value, null]);
|
||||||
|
}
|
||||||
|
foreach ($setup['inheritancePoints'] ?? [] as $row) {
|
||||||
|
if (
|
||||||
|
!is_array($row)
|
||||||
|
|| !is_int($row['ownerId'] ?? null)
|
||||||
|
|| !is_string($row['key'] ?? null)
|
||||||
|
|| !is_numeric($row['value'] ?? null)
|
||||||
|
) {
|
||||||
|
throw new \InvalidArgumentException('invalid setup.inheritancePoints row');
|
||||||
|
}
|
||||||
|
KVStorage::getStorage(DB::db(), "inheritance_{$row['ownerId']}")->setValue(
|
||||||
|
$row['key'],
|
||||||
|
[$row['value'], null],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
foreach ($setup['rankData'] ?? [] as $row) {
|
||||||
|
if (
|
||||||
|
!is_array($row)
|
||||||
|
|| !is_int($row['generalId'] ?? null)
|
||||||
|
|| !is_string($row['type'] ?? null)
|
||||||
|
|| !is_int($row['value'] ?? null)
|
||||||
|
) {
|
||||||
|
throw new \InvalidArgumentException('invalid setup.rankData row');
|
||||||
|
}
|
||||||
|
$nationId = (int)(DB::db()->queryFirstField(
|
||||||
|
'SELECT nation FROM general WHERE no = %i',
|
||||||
|
$row['generalId'],
|
||||||
|
) ?? 0);
|
||||||
|
DB::db()->insertUpdate('rank_data', [
|
||||||
|
'general_id' => $row['generalId'],
|
||||||
|
'nation_id' => $nationId,
|
||||||
|
'type' => $row['type'],
|
||||||
|
'value' => $row['value'],
|
||||||
|
], [
|
||||||
|
'nation_id' => $nationId,
|
||||||
|
'value' => $row['value'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
foreach ($setup['generalAccessLog'] ?? [] as $row) {
|
||||||
|
if (
|
||||||
|
!is_array($row)
|
||||||
|
|| !is_int($row['generalId'] ?? null)
|
||||||
|
|| !is_int($row['refreshScoreTotal'] ?? null)
|
||||||
|
) {
|
||||||
|
throw new \InvalidArgumentException('invalid setup.generalAccessLog row');
|
||||||
|
}
|
||||||
|
DB::db()->insertUpdate('general_access_log', [
|
||||||
|
'general_id' => $row['generalId'],
|
||||||
|
'refresh_score_total' => $row['refreshScoreTotal'],
|
||||||
|
], [
|
||||||
|
'refresh_score_total' => $row['refreshScoreTotal'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
foreach ($setup['nationEnvironment'] ?? [] as $row) {
|
||||||
|
if (
|
||||||
|
!is_array($row)
|
||||||
|
|| !is_int($row['nationId'] ?? null)
|
||||||
|
|| !is_string($row['key'] ?? null)
|
||||||
|
|| !array_key_exists('value', $row)
|
||||||
|
) {
|
||||||
|
throw new \InvalidArgumentException('invalid setup.nationEnvironment row');
|
||||||
|
}
|
||||||
|
KVStorage::getStorage(DB::db(), $row['nationId'], 'nation_env')->setValue(
|
||||||
|
$row['key'],
|
||||||
|
$row['value'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (($setup['resetDiplomacy'] ?? false) === true) {
|
||||||
|
DB::db()->query('DELETE FROM diplomacy');
|
||||||
|
}
|
||||||
|
foreach ($setup['diplomacy'] ?? [] as $row) {
|
||||||
|
if (
|
||||||
|
!is_array($row)
|
||||||
|
|| !is_int($row['fromNationId'] ?? null)
|
||||||
|
|| !is_int($row['toNationId'] ?? null)
|
||||||
|
|| !is_int($row['state'] ?? null)
|
||||||
|
|| !is_int($row['term'] ?? null)
|
||||||
|
) {
|
||||||
|
throw new \InvalidArgumentException('invalid setup.diplomacy row');
|
||||||
|
}
|
||||||
|
$diplomacyPatch = [
|
||||||
|
'me' => $row['fromNationId'],
|
||||||
|
'you' => $row['toNationId'],
|
||||||
|
'state' => $row['state'],
|
||||||
|
'term' => $row['term'],
|
||||||
|
'dead' => $row['dead'] ?? 0,
|
||||||
|
];
|
||||||
|
DB::db()->insertUpdate('diplomacy', $diplomacyPatch, $diplomacyPatch);
|
||||||
|
}
|
||||||
|
if (($setup['resetEvents'] ?? false) === true) {
|
||||||
|
DB::db()->query('DELETE FROM event');
|
||||||
|
}
|
||||||
|
foreach ($setup['event'] ?? [] as $row) {
|
||||||
|
if (!is_array($row) || !is_int($row['id'] ?? null) || !is_array($row['action'] ?? null)) {
|
||||||
|
throw new \InvalidArgumentException('invalid setup.event row');
|
||||||
|
}
|
||||||
|
$eventPatch = [
|
||||||
|
'id' => $row['id'],
|
||||||
|
'target' => $row['target'] ?? 'month',
|
||||||
|
'priority' => $row['priority'] ?? 1000,
|
||||||
|
'condition' => Json::encode($row['condition'] ?? true),
|
||||||
|
'action' => Json::encode($row['action']),
|
||||||
|
];
|
||||||
|
DB::db()->insertUpdate('event', $eventPatch, $eventPatch);
|
||||||
|
}
|
||||||
|
$gameEnvironmentPatch = $setup['gameEnvironment'] ?? [];
|
||||||
|
if (!is_array($gameEnvironmentPatch)) {
|
||||||
|
throw new \InvalidArgumentException('setup.gameEnvironment must be an object');
|
||||||
|
}
|
||||||
|
$gameStorage = KVStorage::getStorage(DB::db(), 'game_env');
|
||||||
|
$worldPatch = $setup['world'] ?? [];
|
||||||
|
if (!is_array($worldPatch)) {
|
||||||
|
throw new \InvalidArgumentException('setup.world must be an object');
|
||||||
|
}
|
||||||
|
if (array_key_exists('hiddenSeed', $worldPatch)) {
|
||||||
|
if (!is_string($worldPatch['hiddenSeed']) || $worldPatch['hiddenSeed'] === '') {
|
||||||
|
throw new \InvalidArgumentException('setup.world.hiddenSeed must be a non-empty string');
|
||||||
|
}
|
||||||
|
UniqueConst::$hiddenSeed = $worldPatch['hiddenSeed'];
|
||||||
|
}
|
||||||
|
foreach ([
|
||||||
|
'isunited',
|
||||||
|
'refreshLimit',
|
||||||
|
'block_change_scout',
|
||||||
|
'lastNPCTroopLeaderID',
|
||||||
|
'tournament',
|
||||||
|
'tnmt_trig',
|
||||||
|
'tnmt_pattern',
|
||||||
|
'prev_winner',
|
||||||
|
] as $key) {
|
||||||
|
if (array_key_exists($key, $gameEnvironmentPatch)) {
|
||||||
|
$gameStorage->setValue($key, $gameEnvironmentPatch[$key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (($setup['setAllCitiesNation'] ?? null) !== null) {
|
||||||
|
if (!is_int($setup['setAllCitiesNation'])) {
|
||||||
|
throw new \InvalidArgumentException('setup.setAllCitiesNation must be an integer');
|
||||||
|
}
|
||||||
|
DB::db()->update('city', ['nation' => $setup['setAllCitiesNation']], true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$db = DB::db();
|
||||||
|
$generalIdBeforeAction =
|
||||||
|
(int)($db->queryFirstField('SELECT COALESCE(MAX(no), 0) FROM general') ?? 0);
|
||||||
|
$nationIdBeforeAction =
|
||||||
|
(int)($db->queryFirstField('SELECT COALESCE(MAX(nation), 0) FROM nation') ?? 0);
|
||||||
|
$eventIdBeforeAction =
|
||||||
|
(int)($db->queryFirstField('SELECT COALESCE(MAX(id), 0) FROM event') ?? 0);
|
||||||
|
$auctionIdBeforeAction =
|
||||||
|
(int)($db->queryFirstField('SELECT COALESCE(MAX(id), 0) FROM ng_auction') ?? 0);
|
||||||
|
$snapshotRequest = ['observe' => $request['observe'] ?? []];
|
||||||
|
$snapshotRequest['observe']['logAfterId'] =
|
||||||
|
(int)($db->queryFirstField('SELECT COALESCE(MAX(id), 0) FROM general_record') ?? 0);
|
||||||
|
$worldHistoryAfterId =
|
||||||
|
(int)($db->queryFirstField('SELECT COALESCE(MAX(id), 0) FROM world_history') ?? 0);
|
||||||
|
$before = comparisonTurnStateSnapshot($snapshotRequest);
|
||||||
|
$beforeDetails = comparisonMonthlyDetails($snapshotRequest['observe'], $worldHistoryAfterId);
|
||||||
|
|
||||||
|
$environment = $request['environment'] ?? [];
|
||||||
|
if (!is_array($environment)) {
|
||||||
|
throw new \InvalidArgumentException('environment must be an object');
|
||||||
|
}
|
||||||
|
foreach (['starttime', 'turntime'] as $timeKey) {
|
||||||
|
if (array_key_exists($timeKey, $environment)) {
|
||||||
|
$environment[$timeKey] = comparisonGameTick(
|
||||||
|
$environment[$timeKey],
|
||||||
|
"environment.{$timeKey}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$year = $environment['year'] ?? null;
|
||||||
|
$month = $environment['month'] ?? null;
|
||||||
|
$startYear = $environment['startyear'] ?? null;
|
||||||
|
if (!is_int($year) || !is_int($month) || !is_int($startYear)) {
|
||||||
|
throw new \InvalidArgumentException('environment year, month, and startyear must be integers');
|
||||||
|
}
|
||||||
|
if (($setup['syncEnvironment'] ?? false) === true) {
|
||||||
|
$gameStorage = KVStorage::getStorage(DB::db(), 'game_env');
|
||||||
|
$environmentValues = [
|
||||||
|
'year' => $year,
|
||||||
|
'month' => $month,
|
||||||
|
'startyear' => $startYear,
|
||||||
|
'killturn' => $environment['killturn'] ?? $gameStorage->killturn,
|
||||||
|
'turnterm' => $environment['turnterm'] ?? $gameStorage->turnterm,
|
||||||
|
];
|
||||||
|
if (isset($environment['starttime'])) {
|
||||||
|
$environmentValues['starttime'] = $environment['starttime'];
|
||||||
|
}
|
||||||
|
foreach ($environmentValues as $key => $value) {
|
||||||
|
$gameStorage->setValue($key, $value);
|
||||||
|
}
|
||||||
|
if ($actionName === 'MonthlyBoundary') {
|
||||||
|
$before = comparisonTurnStateSnapshot($snapshotRequest);
|
||||||
|
$beforeDetails = comparisonMonthlyDetails($snapshotRequest['observe'], $worldHistoryAfterId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($actionName === 'InvaderLifecycle') {
|
||||||
|
foreach (['turnterm', 'turntime'] as $requiredEnvironmentKey) {
|
||||||
|
if (!array_key_exists($requiredEnvironmentKey, $environment)) {
|
||||||
|
throw new \InvalidArgumentException(
|
||||||
|
"InvaderLifecycle requires environment.{$requiredEnvironmentKey}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
echo json_encode(
|
||||||
|
comparisonInvaderLifecycleTrace(
|
||||||
|
$request,
|
||||||
|
$environment,
|
||||||
|
$generalIdBeforeAction,
|
||||||
|
$nationIdBeforeAction,
|
||||||
|
$eventIdBeforeAction,
|
||||||
|
$worldHistoryAfterId,
|
||||||
|
),
|
||||||
|
JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION,
|
||||||
|
), PHP_EOL;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ($actionName === 'NpcNationLifecycle') {
|
||||||
|
foreach (['turnterm', 'turntime'] as $requiredEnvironmentKey) {
|
||||||
|
if (!array_key_exists($requiredEnvironmentKey, $environment)) {
|
||||||
|
throw new \InvalidArgumentException(
|
||||||
|
"NpcNationLifecycle requires environment.{$requiredEnvironmentKey}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
echo json_encode(
|
||||||
|
comparisonNpcNationLifecycleTrace(
|
||||||
|
$environment,
|
||||||
|
$generalIdBeforeAction,
|
||||||
|
$nationIdBeforeAction,
|
||||||
|
$worldHistoryAfterId,
|
||||||
|
),
|
||||||
|
JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION,
|
||||||
|
), PHP_EOL;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ($actionName === 'NationBettingLifecycle') {
|
||||||
|
echo json_encode(
|
||||||
|
comparisonNationBettingLifecycleTrace(
|
||||||
|
$request,
|
||||||
|
$environment,
|
||||||
|
$eventIdBeforeAction,
|
||||||
|
$worldHistoryAfterId,
|
||||||
|
),
|
||||||
|
JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION,
|
||||||
|
), PHP_EOL;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$actionClass = "\\sammo\\Event\\Action\\{$actionName}";
|
||||||
|
if (in_array($actionName, ['PreUpdateMonthly', 'PostUpdateMonthly', 'MonthlyBoundary'], true)) {
|
||||||
|
$action = null;
|
||||||
|
} elseif ($actionName === 'ProcessSemiAnnual') {
|
||||||
|
$resource = $request['resource'] ?? null;
|
||||||
|
if (!is_string($resource)) {
|
||||||
|
throw new \InvalidArgumentException('ProcessSemiAnnual requires resource');
|
||||||
|
}
|
||||||
|
$action = new $actionClass($resource);
|
||||||
|
} elseif (in_array($actionName, ['CreateManyNPC', 'RegNPC', 'RegNeutralNPC', 'RaiseInvader', 'AutoDeleteInvader', 'ChangeCity', 'OpenNationBetting', 'FinishNationBetting', 'BlockScoutAction', 'UnblockScoutAction', 'AddGlobalBetray', 'LostUniqueItem', 'ProcessIncome', 'NoticeToHistoryLog'], true)) {
|
||||||
|
$args = $request['args'] ?? [];
|
||||||
|
if (!is_array($args)) {
|
||||||
|
throw new \InvalidArgumentException("{$actionName} args must be an array");
|
||||||
|
}
|
||||||
|
if ($actionName === 'CreateManyNPC' && count($args) > 2) {
|
||||||
|
throw new \InvalidArgumentException('CreateManyNPC args must have at most two entries');
|
||||||
|
}
|
||||||
|
if ($actionName === 'RaiseInvader' && count($args) > 4) {
|
||||||
|
throw new \InvalidArgumentException('RaiseInvader args must have at most four entries');
|
||||||
|
}
|
||||||
|
if ($actionName === 'ChangeCity' && count($args) !== 2) {
|
||||||
|
throw new \InvalidArgumentException('ChangeCity args must have exactly two entries');
|
||||||
|
}
|
||||||
|
if ($actionName === 'OpenNationBetting' && count($args) > 2) {
|
||||||
|
throw new \InvalidArgumentException('OpenNationBetting args must have at most two entries');
|
||||||
|
}
|
||||||
|
if ($actionName === 'FinishNationBetting' && count($args) !== 1) {
|
||||||
|
throw new \InvalidArgumentException('FinishNationBetting args must have exactly one entry');
|
||||||
|
}
|
||||||
|
if (in_array($actionName, ['BlockScoutAction', 'UnblockScoutAction'], true) && count($args) > 1) {
|
||||||
|
throw new \InvalidArgumentException("{$actionName} args must have at most one entry");
|
||||||
|
}
|
||||||
|
if ($actionName === 'AddGlobalBetray' && count($args) > 2) {
|
||||||
|
throw new \InvalidArgumentException('AddGlobalBetray args must have at most two entries');
|
||||||
|
}
|
||||||
|
if ($actionName === 'LostUniqueItem' && count($args) > 1) {
|
||||||
|
throw new \InvalidArgumentException('LostUniqueItem args must have at most one entry');
|
||||||
|
}
|
||||||
|
if ($actionName === 'ProcessIncome' && count($args) !== 1) {
|
||||||
|
throw new \InvalidArgumentException('ProcessIncome args must have exactly one entry');
|
||||||
|
}
|
||||||
|
if ($actionName === 'NoticeToHistoryLog' && (count($args) < 1 || count($args) > 2)) {
|
||||||
|
throw new \InvalidArgumentException('NoticeToHistoryLog args must have one or two entries');
|
||||||
|
}
|
||||||
|
$action = new $actionClass(...$args);
|
||||||
|
} else {
|
||||||
|
$action = new $actionClass();
|
||||||
|
}
|
||||||
|
$actionEnvironment = [
|
||||||
|
'year' => $year,
|
||||||
|
'month' => $month,
|
||||||
|
'startyear' => $startYear,
|
||||||
|
];
|
||||||
|
if (isset($environment['currentEventID'])) {
|
||||||
|
if (!is_int($environment['currentEventID'])) {
|
||||||
|
throw new \InvalidArgumentException('currentEventID must be an integer');
|
||||||
|
}
|
||||||
|
$actionEnvironment['currentEventID'] = $environment['currentEventID'];
|
||||||
|
}
|
||||||
|
if ($actionName === 'UpdateNationLevel') {
|
||||||
|
$killturn = $environment['killturn'] ?? null;
|
||||||
|
$turnterm = $environment['turnterm'] ?? null;
|
||||||
|
if (!is_int($killturn) || !is_int($turnterm) || $turnterm <= 0) {
|
||||||
|
throw new \InvalidArgumentException('UpdateNationLevel requires integer killturn and positive turnterm');
|
||||||
|
}
|
||||||
|
$actionEnvironment['killturn'] = $killturn;
|
||||||
|
$actionEnvironment['turnterm'] = $turnterm;
|
||||||
|
}
|
||||||
|
if (in_array($actionName, ['CreateManyNPC', 'RegNPC', 'RegNeutralNPC', 'RaiseNPCNation', 'RaiseInvader', 'ProvideNPCTroopLeader'], true)) {
|
||||||
|
$turnterm = $environment['turnterm'] ?? null;
|
||||||
|
$turntime = $environment['turntime'] ?? null;
|
||||||
|
$showImgLevel = $environment['show_img_level'] ?? 3;
|
||||||
|
$fiction = $environment['fiction'] ?? [0];
|
||||||
|
if (
|
||||||
|
!is_int($turnterm)
|
||||||
|
|| $turnterm <= 0
|
||||||
|
|| !is_int($turntime)
|
||||||
|
|| !is_int($showImgLevel)
|
||||||
|
|| !is_array($fiction)
|
||||||
|
) {
|
||||||
|
throw new \InvalidArgumentException(
|
||||||
|
"{$actionName} requires positive turnterm, game-tick turntime, integer show_img_level, and array fiction",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$actionEnvironment += [
|
||||||
|
'turnterm' => $turnterm,
|
||||||
|
'turntime' => $turntime,
|
||||||
|
'show_img_level' => $showImgLevel,
|
||||||
|
'stored_icons' => $environment['stored_icons'] ?? [],
|
||||||
|
'icon_path' => $environment['icon_path'] ?? '.',
|
||||||
|
'fiction' => $fiction,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if ($actionName === 'PreUpdateMonthly') {
|
||||||
|
if (!preUpdateMonthly()) {
|
||||||
|
throw new \RuntimeException('preUpdateMonthly failed');
|
||||||
|
}
|
||||||
|
} elseif ($actionName === 'PostUpdateMonthly') {
|
||||||
|
$seedYear = $environment['seedYear'] ?? null;
|
||||||
|
$seedMonth = $environment['seedMonth'] ?? null;
|
||||||
|
if (!is_int($seedYear) || !is_int($seedMonth)) {
|
||||||
|
throw new \InvalidArgumentException('PostUpdateMonthly requires seedYear and seedMonth');
|
||||||
|
}
|
||||||
|
postUpdateMonthly(new RandUtil(new LiteHashDRBG(Util::simpleSerialize(
|
||||||
|
UniqueConst::$hiddenSeed,
|
||||||
|
'monthly',
|
||||||
|
$seedYear,
|
||||||
|
$seedMonth,
|
||||||
|
))));
|
||||||
|
} elseif ($actionName === 'MonthlyBoundary') {
|
||||||
|
$turntime = $environment['turntime'] ?? null;
|
||||||
|
if (!is_int($turntime)) {
|
||||||
|
throw new \InvalidArgumentException('MonthlyBoundary requires turntime');
|
||||||
|
}
|
||||||
|
$gameStorage = KVStorage::getStorage($db, 'game_env');
|
||||||
|
$seedYear = (int)$gameStorage->year;
|
||||||
|
$seedMonth = (int)$gameStorage->month;
|
||||||
|
TurnExecutionHelper::runEventHandler($db, $gameStorage, Enums\EventTarget::PreMonth);
|
||||||
|
if (!preUpdateMonthly()) {
|
||||||
|
throw new \RuntimeException('preUpdateMonthly failed');
|
||||||
|
}
|
||||||
|
turnDate($turntime);
|
||||||
|
$gameStorage->resetCache();
|
||||||
|
if ((int)$gameStorage->month === 1) {
|
||||||
|
checkStatistic();
|
||||||
|
}
|
||||||
|
TurnExecutionHelper::runEventHandler($db, $gameStorage, Enums\EventTarget::Month);
|
||||||
|
postUpdateMonthly(new RandUtil(new LiteHashDRBG(Util::simpleSerialize(
|
||||||
|
UniqueConst::$hiddenSeed,
|
||||||
|
'monthly',
|
||||||
|
$seedYear,
|
||||||
|
$seedMonth,
|
||||||
|
))));
|
||||||
|
} else {
|
||||||
|
$action->run($actionEnvironment);
|
||||||
|
}
|
||||||
|
unset($action);
|
||||||
|
gc_collect_cycles();
|
||||||
|
|
||||||
|
$after = comparisonTurnStateSnapshot($snapshotRequest);
|
||||||
|
$afterDetails = comparisonMonthlyDetails($snapshotRequest['observe'], $worldHistoryAfterId);
|
||||||
|
$createdGenerals = [];
|
||||||
|
if (in_array($actionName, ['CreateManyNPC', 'RegNPC', 'RegNeutralNPC', 'RaiseNPCNation', 'RaiseInvader', 'ProvideNPCTroopLeader'], true)) {
|
||||||
|
foreach ($db->query('SELECT * FROM general WHERE no > %i ORDER BY no', $generalIdBeforeAction) as $row) {
|
||||||
|
$generalId = (int)$row['no'];
|
||||||
|
$turnRows = $db->query(
|
||||||
|
'SELECT turn_idx, action, arg FROM general_turn WHERE general_id = %i ORDER BY turn_idx',
|
||||||
|
$generalId,
|
||||||
|
);
|
||||||
|
$createdGenerals[] = comparisonPickRow(
|
||||||
|
$row,
|
||||||
|
[
|
||||||
|
'id' => 'no',
|
||||||
|
'name' => 'name',
|
||||||
|
'nationId' => 'nation',
|
||||||
|
'cityId' => 'city',
|
||||||
|
'leadership' => 'leadership',
|
||||||
|
'strength' => 'strength',
|
||||||
|
'intelligence' => 'intel',
|
||||||
|
'experience' => 'experience',
|
||||||
|
'dedication' => 'dedication',
|
||||||
|
'officerLevel' => 'officer_level',
|
||||||
|
'gold' => 'gold',
|
||||||
|
'rice' => 'rice',
|
||||||
|
'crew' => 'crew',
|
||||||
|
'crewTypeId' => 'crewtype',
|
||||||
|
'train' => 'train',
|
||||||
|
'atmos' => 'atmos',
|
||||||
|
'turnTime' => 'turntime',
|
||||||
|
'killturn' => 'killturn',
|
||||||
|
'age' => 'age',
|
||||||
|
'npcState' => 'npc',
|
||||||
|
'npcOriginalState' => 'npc_org',
|
||||||
|
'affinity' => 'affinity',
|
||||||
|
'personality' => 'personal',
|
||||||
|
'specialDomestic' => 'special',
|
||||||
|
'specialWar' => 'special2',
|
||||||
|
'specAge' => 'specage',
|
||||||
|
'specAge2' => 'specage2',
|
||||||
|
'bornYear' => 'bornyear',
|
||||||
|
'deadYear' => 'deadyear',
|
||||||
|
'picture' => 'picture',
|
||||||
|
'dex1' => 'dex1',
|
||||||
|
'dex2' => 'dex2',
|
||||||
|
'dex3' => 'dex3',
|
||||||
|
'dex4' => 'dex4',
|
||||||
|
'dex5' => 'dex5',
|
||||||
|
],
|
||||||
|
) + [
|
||||||
|
'turnCount' => count($turnRows),
|
||||||
|
'turnActions' => array_values(array_unique(array_column($turnRows, 'action'))),
|
||||||
|
'rankCount' => (int)$db->queryFirstField(
|
||||||
|
'SELECT count(*) FROM rank_data WHERE general_id = %i',
|
||||||
|
$generalId,
|
||||||
|
),
|
||||||
|
'nonZeroRankCount' => (int)$db->queryFirstField(
|
||||||
|
'SELECT count(*) FROM rank_data WHERE general_id = %i AND value != 0',
|
||||||
|
$generalId,
|
||||||
|
),
|
||||||
|
'troop' => $db->queryFirstRow(
|
||||||
|
'SELECT troop_leader AS id, nation AS nationId, name FROM troop WHERE troop_leader = %i',
|
||||||
|
$generalId,
|
||||||
|
) ?: null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$createdNations = [];
|
||||||
|
if (in_array($actionName, ['RaiseNPCNation', 'RaiseInvader'], true)) {
|
||||||
|
foreach ($db->query('SELECT * FROM nation WHERE nation > %i ORDER BY nation', $nationIdBeforeAction) as $row) {
|
||||||
|
$nationId = (int)$row['nation'];
|
||||||
|
$createdNations[] = comparisonPickRow(
|
||||||
|
$row,
|
||||||
|
[
|
||||||
|
'id' => 'nation',
|
||||||
|
'name' => 'name',
|
||||||
|
'color' => 'color',
|
||||||
|
'capitalCityId' => 'capital',
|
||||||
|
'generalCount' => 'gennum',
|
||||||
|
'gold' => 'gold',
|
||||||
|
'rice' => 'rice',
|
||||||
|
'tech' => 'tech',
|
||||||
|
'level' => 'level',
|
||||||
|
'typeCode' => 'type',
|
||||||
|
'bill' => 'bill',
|
||||||
|
'rate' => 'rate',
|
||||||
|
'scout' => 'scout',
|
||||||
|
'war' => 'war',
|
||||||
|
'strategicCommandLimit' => 'strategic_cmd_limit',
|
||||||
|
'surrenderLimit' => 'surlimit',
|
||||||
|
'aux' => 'aux',
|
||||||
|
],
|
||||||
|
['aux'],
|
||||||
|
) + [
|
||||||
|
'nationTurnCount' => (int)$db->queryFirstField(
|
||||||
|
'SELECT count(*) FROM nation_turn WHERE nation_id = %i',
|
||||||
|
$nationId,
|
||||||
|
),
|
||||||
|
'diplomacyCount' => (int)$db->queryFirstField(
|
||||||
|
'SELECT count(*) FROM diplomacy WHERE me = %i OR you = %i',
|
||||||
|
$nationId,
|
||||||
|
$nationId,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$response = [
|
||||||
|
'schemaVersion' => 1,
|
||||||
|
'engine' => 'ref',
|
||||||
|
'action' => $actionName,
|
||||||
|
'before' => $before,
|
||||||
|
'beforeDetails' => $beforeDetails,
|
||||||
|
'after' => $after,
|
||||||
|
'afterDetails' => $afterDetails,
|
||||||
|
];
|
||||||
|
if (in_array($actionName, ['CreateManyNPC', 'RegNPC', 'RegNeutralNPC'], true)) {
|
||||||
|
$response['createdGenerals'] = $createdGenerals;
|
||||||
|
}
|
||||||
|
if (in_array($actionName, ['RaiseNPCNation', 'RaiseInvader'], true)) {
|
||||||
|
$response['createdGenerals'] = $createdGenerals;
|
||||||
|
$response['createdNations'] = $createdNations;
|
||||||
|
}
|
||||||
|
if ($actionName === 'ProvideNPCTroopLeader') {
|
||||||
|
$response['createdGenerals'] = $createdGenerals;
|
||||||
|
$response['gameEnvironment'] = KVStorage::getStorage(DB::db(), 'game_env')->getValues([
|
||||||
|
'lastNPCTroopLeaderID',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
if ($actionName === 'PreUpdateMonthly') {
|
||||||
|
$response['gameEnvironment'] = KVStorage::getStorage(DB::db(), 'game_env')->getValues([
|
||||||
|
'develcost',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
if ($actionName === 'PostUpdateMonthly') {
|
||||||
|
$response['gameEnvironment'] = KVStorage::getStorage(DB::db(), 'game_env')->getValues([
|
||||||
|
'tournament',
|
||||||
|
'tnmt_trig',
|
||||||
|
'tnmt_pattern',
|
||||||
|
'tnmt_type',
|
||||||
|
'tnmt_auto',
|
||||||
|
'phase',
|
||||||
|
]);
|
||||||
|
$response['createdNeutralAuctions'] = array_map(
|
||||||
|
static fn(array $row): array => [
|
||||||
|
'id' => (int)$row['id'],
|
||||||
|
'type' => $row['type'],
|
||||||
|
'target' => $row['target'],
|
||||||
|
'detail' => Json::decode($row['detail']),
|
||||||
|
],
|
||||||
|
iterator_to_array(
|
||||||
|
$db->query(
|
||||||
|
'SELECT id, `type`, target, detail FROM ng_auction WHERE id > %i AND host_general_id = 0 ORDER BY id',
|
||||||
|
$auctionIdBeforeAction,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (in_array($actionName, ['OpenNationBetting', 'FinishNationBetting'], true)) {
|
||||||
|
$bettingStorage = KVStorage::getStorage(DB::db(), 'betting');
|
||||||
|
$response['betting'] = $bettingStorage->getAll();
|
||||||
|
$response['bets'] = iterator_to_array(
|
||||||
|
$db->query(
|
||||||
|
'SELECT betting_id AS bettingId, general_id AS generalId, user_id AS userId, betting_type AS selectionKey, amount FROM ng_betting ORDER BY id',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
$response['createdEvents'] = array_map(
|
||||||
|
static fn(array $row): array => comparisonPickRow(
|
||||||
|
$row,
|
||||||
|
[
|
||||||
|
'id' => 'id',
|
||||||
|
'targetCode' => 'target',
|
||||||
|
'priority' => 'priority',
|
||||||
|
'condition' => 'condition',
|
||||||
|
'action' => 'action',
|
||||||
|
],
|
||||||
|
['condition', 'action'],
|
||||||
|
),
|
||||||
|
iterator_to_array(
|
||||||
|
$db->query(
|
||||||
|
'SELECT id, target, priority, `condition`, action FROM event WHERE id > %i ORDER BY id',
|
||||||
|
$eventIdBeforeAction,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
$response['gameEnvironment'] = KVStorage::getStorage(DB::db(), 'game_env')->getValues([
|
||||||
|
'last_betting_id',
|
||||||
|
]);
|
||||||
|
$response['inheritancePrevious'] = [];
|
||||||
|
foreach (comparisonIntegerList($request['observe']['ownerIds'] ?? [], 'ownerIds') as $ownerId) {
|
||||||
|
$value = KVStorage::getStorage(DB::db(), "inheritance_{$ownerId}")->getValue('previous');
|
||||||
|
$response['inheritancePrevious'][(string)$ownerId] = is_array($value) ? ($value[0] ?? 0) : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (in_array($actionName, ['BlockScoutAction', 'UnblockScoutAction'], true)) {
|
||||||
|
$response['gameEnvironment'] = KVStorage::getStorage(DB::db(), 'game_env')->getValues([
|
||||||
|
'block_change_scout',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
if ($actionName === 'RaiseInvader') {
|
||||||
|
$response['createdEvents'] = array_map(
|
||||||
|
static fn(array $row): array => comparisonPickRow(
|
||||||
|
$row,
|
||||||
|
[
|
||||||
|
'id' => 'id',
|
||||||
|
'targetCode' => 'target',
|
||||||
|
'priority' => 'priority',
|
||||||
|
'condition' => 'condition',
|
||||||
|
'action' => 'action',
|
||||||
|
],
|
||||||
|
['condition', 'action'],
|
||||||
|
),
|
||||||
|
iterator_to_array(
|
||||||
|
$db->query(
|
||||||
|
'SELECT id, target, priority, `condition`, action FROM event WHERE id > %i ORDER BY id',
|
||||||
|
$eventIdBeforeAction,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
$response['createdDiplomacy'] = iterator_to_array(
|
||||||
|
$db->query(
|
||||||
|
'SELECT me AS fromNationId, you AS toNationId, state, term FROM diplomacy WHERE me > %i OR you > %i ORDER BY me, you',
|
||||||
|
$nationIdBeforeAction,
|
||||||
|
$nationIdBeforeAction,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
$gameStorage = KVStorage::getStorage(DB::db(), 'game_env');
|
||||||
|
$response['gameEnvironment'] = $gameStorage->getValues([
|
||||||
|
'isunited',
|
||||||
|
'turnterm',
|
||||||
|
'block_change_scout',
|
||||||
|
'refreshLimit',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
if (in_array($actionName, ['AutoDeleteInvader', 'InvaderEnding'], true)) {
|
||||||
|
$gameStorage = KVStorage::getStorage(DB::db(), 'game_env');
|
||||||
|
$response['gameEnvironment'] = $gameStorage->getValues([
|
||||||
|
'isunited',
|
||||||
|
'turnterm',
|
||||||
|
'block_change_scout',
|
||||||
|
'refreshLimit',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
if (($request['compactOutput'] ?? false) === true) {
|
||||||
|
$observedGeneralTurns = [];
|
||||||
|
foreach (comparisonIntegerList($request['observe']['generalIds'] ?? [], 'generalIds') as $generalId) {
|
||||||
|
$observedGeneralTurns[(string)$generalId] = iterator_to_array(
|
||||||
|
$db->query(
|
||||||
|
'SELECT turn_idx AS turnIndex, action, arg FROM general_turn WHERE general_id = %i ORDER BY turn_idx',
|
||||||
|
$generalId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$response = [
|
||||||
|
'schemaVersion' => 1,
|
||||||
|
'engine' => 'ref',
|
||||||
|
'action' => $actionName,
|
||||||
|
'afterDetails' => $afterDetails,
|
||||||
|
'createdGeneralCount' => count($createdGenerals),
|
||||||
|
'firstCreatedGenerals' => array_slice($createdGenerals, 0, 3),
|
||||||
|
'createdNations' => $response['createdNations'] ?? [],
|
||||||
|
'createdEvents' => $response['createdEvents'] ?? [],
|
||||||
|
'createdDiplomacy' => $response['createdDiplomacy'] ?? [],
|
||||||
|
'gameEnvironment' => $response['gameEnvironment'] ?? [],
|
||||||
|
'logs' => $after['logs'],
|
||||||
|
'observedGeneralTurns' => $observedGeneralTurns,
|
||||||
|
'remainingEventIds' => array_map(
|
||||||
|
'intval',
|
||||||
|
$db->queryFirstColumn('SELECT id FROM event ORDER BY id'),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
echo json_encode(
|
||||||
|
$response,
|
||||||
|
JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION,
|
||||||
|
), PHP_EOL;
|
||||||
|
} catch (\Throwable $throwable) {
|
||||||
|
fwrite(STDERR, $throwable::class . ': ' . $throwable->getMessage() . PHP_EOL);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
comparisonMonthlyEventTraceMain();
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
<?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();
|
||||||
|
$clock = GameClock::fromStorage($game);
|
||||||
|
$world = $game->getValues([
|
||||||
|
'scenario', 'year', 'month', 'turnterm', 'turntime', 'starttime',
|
||||||
|
'init_year', 'init_month', 'develcost', 'killturn', 'isunited',
|
||||||
|
]);
|
||||||
|
$world['turntime'] = $clock->formatTick(Util::toInt($world['turntime']), true);
|
||||||
|
$world['starttime'] = $clock->formatTick(Util::toInt($world['starttime']), true);
|
||||||
|
|
||||||
|
$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', 'experienceLevel' => 'explevel',
|
||||||
|
'dedication' => 'dedication', 'dedicationLevel' => 'dedlevel',
|
||||||
|
'leadershipExp' => 'leadership_exp', 'strengthExp' => 'strength_exp', 'intelligenceExp' => 'intel_exp',
|
||||||
|
'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', 'experienceLevel', 'dedication', 'dedicationLevel', 'leadershipExp',
|
||||||
|
'strengthExp', 'intelligenceExp', '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];
|
||||||
|
}
|
||||||
|
$general['turnTick'] = Util::toInt($general['turnTime']);
|
||||||
|
$general['turnTime'] = $clock->formatTick($general['turnTick'], true);
|
||||||
|
$general['recentWarTick'] = $general['recentWarTime'] === null
|
||||||
|
? null
|
||||||
|
: Util::toInt($general['recentWarTime']);
|
||||||
|
$general['recentWarTime'] = $general['recentWarTime'] === null
|
||||||
|
? null
|
||||||
|
: $clock->formatTick(Util::toInt($general['recentWarTime']), true);
|
||||||
|
$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 = [];
|
||||||
|
$lastAttackableByNation = KVStorage::getValuesFromInterNamespace($db, 'nation_env', 'last_attackable');
|
||||||
|
$lastCapitalMoveTrialByNation = KVStorage::getValuesFromInterNamespace($db, 'nation_env', 'last천도Trial');
|
||||||
|
foreach ($db->query('SELECT *, CAST(tech AS DOUBLE) AS tech_raw 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'];
|
||||||
|
$nation['techRaw'] = (float)$row['tech_raw'];
|
||||||
|
$nation['lastAttackable'] = (int)($lastAttackableByNation[$nation['id']] ?? 0);
|
||||||
|
$lastCapitalMoveTrial = $lastCapitalMoveTrialByNation[$nation['id']] ?? null;
|
||||||
|
$nation['lastCapitalMoveTrial'] = is_array($lastCapitalMoveTrial)
|
||||||
|
? [(int)($lastCapitalMoveTrial[0] ?? 0), (int)($lastCapitalMoveTrial[1] ?? 0)]
|
||||||
|
: null;
|
||||||
|
$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;
|
||||||
@@ -0,0 +1,312 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace sammo;
|
||||||
|
|
||||||
|
if (PHP_SAPI !== 'cli' || getenv('TURN_DIFFERENTIAL_ENABLED') !== '1') {
|
||||||
|
http_response_code(404);
|
||||||
|
fwrite(STDERR, "NPC possession comparison is disabled.\n");
|
||||||
|
exit(64);
|
||||||
|
}
|
||||||
|
|
||||||
|
chdir(dirname(__DIR__));
|
||||||
|
$_SERVER['REMOTE_ADDR'] ??= '127.0.0.1';
|
||||||
|
error_reporting(E_ALL & ~E_DEPRECATED);
|
||||||
|
require_once 'lib.php';
|
||||||
|
require_once 'func.php';
|
||||||
|
|
||||||
|
final class ComparisonNpcTracingRandUtil extends RandUtil
|
||||||
|
{
|
||||||
|
/** @var list<float> */
|
||||||
|
public array $floatDraws = [];
|
||||||
|
|
||||||
|
public function nextFloat1(): float
|
||||||
|
{
|
||||||
|
$value = parent::nextFloat1();
|
||||||
|
$this->floatDraws[] = $value;
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, mixed> */
|
||||||
|
function comparisonNpcRequest(): array
|
||||||
|
{
|
||||||
|
$request = json_decode(stream_get_contents(STDIN), true, flags: JSON_THROW_ON_ERROR);
|
||||||
|
if (!is_array($request)) {
|
||||||
|
throw new \InvalidArgumentException('fixture must be a JSON object');
|
||||||
|
}
|
||||||
|
return $request;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return list<int> */
|
||||||
|
function comparisonNpcIntegerList(mixed $value, string $name): array
|
||||||
|
{
|
||||||
|
if (!is_array($value)) {
|
||||||
|
throw new \InvalidArgumentException("{$name} must be an array");
|
||||||
|
}
|
||||||
|
$result = [];
|
||||||
|
foreach ($value as $item) {
|
||||||
|
if (!is_int($item) || $item <= 0) {
|
||||||
|
throw new \InvalidArgumentException("{$name} must contain positive integers");
|
||||||
|
}
|
||||||
|
$result[] = $item;
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<int, int> */
|
||||||
|
function comparisonNpcPreviousPick(mixed $value): array
|
||||||
|
{
|
||||||
|
if ($value === null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (!is_array($value)) {
|
||||||
|
throw new \InvalidArgumentException('previousPick must be an array');
|
||||||
|
}
|
||||||
|
$result = [];
|
||||||
|
foreach ($value as $item) {
|
||||||
|
if (
|
||||||
|
!is_array($item)
|
||||||
|
|| !isset($item['id'], $item['keepCount'])
|
||||||
|
|| !is_int($item['id'])
|
||||||
|
|| $item['id'] <= 0
|
||||||
|
|| !is_int($item['keepCount'])
|
||||||
|
|| $item['keepCount'] < 0
|
||||||
|
) {
|
||||||
|
throw new \InvalidArgumentException('previousPick entries require positive id and non-negative keepCount');
|
||||||
|
}
|
||||||
|
$result[$item['id']] = $item['keepCount'];
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return list<array{id: int, leadership: int, strength: int, intel: int}> */
|
||||||
|
function comparisonNpcCandidateRows(): array
|
||||||
|
{
|
||||||
|
return array_map(
|
||||||
|
static fn(array $row): array => [
|
||||||
|
'id' => (int)$row['no'],
|
||||||
|
'leadership' => (int)$row['leadership'],
|
||||||
|
'strength' => (int)$row['strength'],
|
||||||
|
'intel' => (int)$row['intel'],
|
||||||
|
],
|
||||||
|
DB::db()->query('SELECT `no`, leadership, strength, intel FROM general WHERE npc=2'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, mixed> */
|
||||||
|
function comparisonNpcState(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'candidates' => comparisonNpcCandidateRows(),
|
||||||
|
'tokens' => DB::db()->query(
|
||||||
|
'SELECT owner, valid_until, pick_more_from, pick_result, nonce FROM select_npc_token ORDER BY id',
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $case
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
function comparisonNpcRunCase(array $case, string|int $hiddenSeed, int $owner, int $now, GameClock $clock): array
|
||||||
|
{
|
||||||
|
$db = DB::db();
|
||||||
|
$db->delete('select_npc_token', '1=1');
|
||||||
|
|
||||||
|
$insertReserved = static function (int $reservedOwner, array $reservedIds, int $validUntil) use ($db, $now): void {
|
||||||
|
if ($reservedIds === []) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$reservedPick = [];
|
||||||
|
foreach ($reservedIds as $id) {
|
||||||
|
$reservedPick[$id] = ['keepCnt' => 3];
|
||||||
|
}
|
||||||
|
$db->insert('select_npc_token', [
|
||||||
|
'owner' => $reservedOwner,
|
||||||
|
'valid_until' => $validUntil,
|
||||||
|
'pick_more_from' => $now,
|
||||||
|
'pick_result' => json_encode((object)$reservedPick, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE),
|
||||||
|
'nonce' => 1,
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
$reservedIds = comparisonNpcIntegerList($case['reservedIds'] ?? [], 'reservedIds');
|
||||||
|
$boundaryReservedIds = comparisonNpcIntegerList(
|
||||||
|
$case['boundaryReservedIds'] ?? [],
|
||||||
|
'boundaryReservedIds',
|
||||||
|
);
|
||||||
|
$expiredReservedIds = comparisonNpcIntegerList(
|
||||||
|
$case['expiredReservedIds'] ?? [],
|
||||||
|
'expiredReservedIds',
|
||||||
|
);
|
||||||
|
$expiredAt = $now - $clock->ticksFromSeconds(1);
|
||||||
|
$insertReserved($owner + 1, $reservedIds, GameClock::MAX_SAFE_TICK);
|
||||||
|
$insertReserved($owner + 2, $boundaryReservedIds, $now);
|
||||||
|
$insertReserved($owner + 3, $expiredReservedIds, $expiredAt);
|
||||||
|
|
||||||
|
$candidates = [];
|
||||||
|
$weights = [];
|
||||||
|
foreach (comparisonNpcCandidateRows() as $row) {
|
||||||
|
$id = $row['id'];
|
||||||
|
$candidates[$id] = $row + ['keepCnt' => 3];
|
||||||
|
$weights[$id] = NpcPossessionSelector::weight($row);
|
||||||
|
}
|
||||||
|
NpcPossessionSelector::removeReserved(
|
||||||
|
$candidates,
|
||||||
|
$weights,
|
||||||
|
$db->queryFirstColumn(
|
||||||
|
'SELECT pick_result FROM select_npc_token WHERE `owner`!=%i AND valid_until >=%i',
|
||||||
|
$owner,
|
||||||
|
$now,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
$previousPick = comparisonNpcPreviousPick($case['previousPick'] ?? null);
|
||||||
|
$keepIds = comparisonNpcIntegerList($case['keepIds'] ?? [], 'keepIds');
|
||||||
|
$oldPick = [];
|
||||||
|
foreach ($previousPick as $id => $keepCount) {
|
||||||
|
if (!isset($candidates[$id])) {
|
||||||
|
throw new \InvalidArgumentException("previous candidate {$id} is not selectable");
|
||||||
|
}
|
||||||
|
$oldPick[$id] = $candidates[$id];
|
||||||
|
$oldPick[$id]['keepCnt'] = $keepCount;
|
||||||
|
}
|
||||||
|
$hasPreviousToken = ($case['hasPreviousToken'] ?? false) === true || $oldPick !== [];
|
||||||
|
if ($hasPreviousToken) {
|
||||||
|
$db->insert('select_npc_token', [
|
||||||
|
'owner' => $owner,
|
||||||
|
'valid_until' => GameClock::MAX_SAFE_TICK,
|
||||||
|
'pick_more_from' => $now,
|
||||||
|
'pick_result' => json_encode((object)$oldPick, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE),
|
||||||
|
'nonce' => 2,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
$before = comparisonNpcState();
|
||||||
|
$keepSelection = NpcPossessionSelector::applyKeep($oldPick, $keepIds);
|
||||||
|
$picked = $keepSelection['picked'];
|
||||||
|
if ($hasPreviousToken && $keepSelection['cancelled']) {
|
||||||
|
return [
|
||||||
|
'name' => (string)($case['name'] ?? ''),
|
||||||
|
'cancelled' => true,
|
||||||
|
'seed' => null,
|
||||||
|
'candidateOrder' => array_keys($candidates),
|
||||||
|
'randomDraws' => [],
|
||||||
|
'draws' => [],
|
||||||
|
'pick' => array_map(
|
||||||
|
static fn(int $id, array $candidate): array => [
|
||||||
|
'id' => $id,
|
||||||
|
'keepCount' => (int)$candidate['keepCnt'],
|
||||||
|
],
|
||||||
|
array_keys($oldPick),
|
||||||
|
array_values($oldPick),
|
||||||
|
),
|
||||||
|
'selectionStateUnchanged' => comparisonNpcState() === $before,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$seed = NpcPossessionSelector::buildSeed($hiddenSeed, $owner, $now);
|
||||||
|
$rng = new ComparisonNpcTracingRandUtil(new LiteHashDRBG($seed));
|
||||||
|
$draws = [];
|
||||||
|
$picked = NpcPossessionSelector::select(
|
||||||
|
$candidates,
|
||||||
|
$weights,
|
||||||
|
$picked,
|
||||||
|
$rng,
|
||||||
|
static function (int|string $id) use (&$draws): void {
|
||||||
|
$draws[] = (int)$id;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'name' => (string)($case['name'] ?? ''),
|
||||||
|
'cancelled' => false,
|
||||||
|
'seed' => $seed,
|
||||||
|
'candidateOrder' => array_keys($candidates),
|
||||||
|
'randomDraws' => $rng->floatDraws,
|
||||||
|
'draws' => $draws,
|
||||||
|
'pick' => array_map(
|
||||||
|
static fn(int $id, array $candidate): array => [
|
||||||
|
'id' => $id,
|
||||||
|
'keepCount' => (int)$candidate['keepCnt'],
|
||||||
|
],
|
||||||
|
array_keys($picked),
|
||||||
|
array_values($picked),
|
||||||
|
),
|
||||||
|
'selectionStateUnchanged' => comparisonNpcState() === $before,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$request = comparisonNpcRequest();
|
||||||
|
$hiddenSeed = $request['hiddenSeed'] ?? null;
|
||||||
|
$owner = $request['owner'] ?? null;
|
||||||
|
$now = $request['now'] ?? null;
|
||||||
|
$candidateFixtures = $request['candidates'] ?? null;
|
||||||
|
$cases = $request['cases'] ?? null;
|
||||||
|
if (
|
||||||
|
(!is_string($hiddenSeed) && !is_int($hiddenSeed))
|
||||||
|
|| !is_int($owner)
|
||||||
|
|| $owner <= 0
|
||||||
|
|| !is_string($now)
|
||||||
|
|| !is_array($candidateFixtures)
|
||||||
|
|| !is_array($cases)
|
||||||
|
) {
|
||||||
|
throw new \InvalidArgumentException('fixture requires hiddenSeed, positive owner, now, candidates and cases');
|
||||||
|
}
|
||||||
|
$date = \DateTimeImmutable::createFromFormat('!Y-m-d H:i:s', $now);
|
||||||
|
if ($date === false || $date->format('Y-m-d H:i:s') !== $now) {
|
||||||
|
throw new \InvalidArgumentException('now must use Y-m-d H:i:s');
|
||||||
|
}
|
||||||
|
|
||||||
|
$db = DB::db();
|
||||||
|
$gameStorage = KVStorage::getStorage($db, 'game_env');
|
||||||
|
$clock = GameClock::fromStorage($gameStorage);
|
||||||
|
$nowTick = $clock->dateTimeToTick($date);
|
||||||
|
$db->update('general', ['npc' => 1], '1=1');
|
||||||
|
$fixtureIds = [];
|
||||||
|
foreach ($candidateFixtures as $candidate) {
|
||||||
|
if (
|
||||||
|
!is_array($candidate)
|
||||||
|
|| !isset($candidate['id'], $candidate['leadership'], $candidate['strength'], $candidate['intel'])
|
||||||
|
|| !is_int($candidate['id'])
|
||||||
|
|| $candidate['id'] <= 0
|
||||||
|
|| !is_int($candidate['leadership'])
|
||||||
|
|| !is_int($candidate['strength'])
|
||||||
|
|| !is_int($candidate['intel'])
|
||||||
|
) {
|
||||||
|
throw new \InvalidArgumentException('candidate entries require integer id and stats');
|
||||||
|
}
|
||||||
|
if ($db->queryFirstField('SELECT `no` FROM general WHERE `no`=%i', $candidate['id']) === null) {
|
||||||
|
throw new \RuntimeException("fixture general {$candidate['id']} does not exist");
|
||||||
|
}
|
||||||
|
$db->update(
|
||||||
|
'general',
|
||||||
|
[
|
||||||
|
'npc' => 2,
|
||||||
|
'leadership' => $candidate['leadership'],
|
||||||
|
'strength' => $candidate['strength'],
|
||||||
|
'intel' => $candidate['intel'],
|
||||||
|
],
|
||||||
|
'`no`=%i',
|
||||||
|
$candidate['id'],
|
||||||
|
);
|
||||||
|
$fixtureIds[] = $candidate['id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$results = [];
|
||||||
|
foreach ($cases as $case) {
|
||||||
|
if (!is_array($case)) {
|
||||||
|
throw new \InvalidArgumentException('case must be an object');
|
||||||
|
}
|
||||||
|
$results[] = comparisonNpcRunCase($case, $hiddenSeed, $owner, $nowTick, $clock);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(
|
||||||
|
[
|
||||||
|
'fixtureGeneralIds' => $fixtureIds,
|
||||||
|
'observedSqlOrder' => array_column(comparisonNpcCandidateRows(), 'id'),
|
||||||
|
'cases' => $results,
|
||||||
|
],
|
||||||
|
JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT,
|
||||||
|
);
|
||||||
|
echo PHP_EOL;
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use sammo\DB;
|
||||||
|
use sammo\Event\Action\OpenNationBetting;
|
||||||
|
use sammo\GameClock;
|
||||||
|
use sammo\Json;
|
||||||
|
use sammo\KVStorage;
|
||||||
|
|
||||||
|
if (PHP_SAPI !== 'cli') {
|
||||||
|
http_response_code(404);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
chdir(dirname(__DIR__));
|
||||||
|
require 'lib.php';
|
||||||
|
require 'func.php';
|
||||||
|
|
||||||
|
$db = DB::db();
|
||||||
|
$game = KVStorage::getStorage($db, 'game_env')->getAll();
|
||||||
|
$contract = [
|
||||||
|
'scenario' => (int)($game['scenario'] ?? 0),
|
||||||
|
'year' => (int)($game['year'] ?? 0),
|
||||||
|
'month' => (int)($game['month'] ?? 0),
|
||||||
|
'turnterm' => (int)($game['turnterm'] ?? 0),
|
||||||
|
'npcmode' => (int)($game['npcmode'] ?? 0),
|
||||||
|
];
|
||||||
|
if ($contract !== ['scenario' => 2601, 'year' => 186, 'month' => 1, 'turnterm' => 10, 'npcmode' => 2]) {
|
||||||
|
throw new RuntimeException('Ref GUI parity conditional fixture requires scenario2601 at 186-01.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$nationBettingCount = (int)$db->queryFirstField(
|
||||||
|
"SELECT COUNT(*) FROM storage
|
||||||
|
WHERE namespace = 'betting'
|
||||||
|
AND JSON_UNQUOTE(JSON_EXTRACT(value, '$.type')) = 'bettingNation'"
|
||||||
|
);
|
||||||
|
if ($nationBettingCount === 0) {
|
||||||
|
(new OpenNationBetting(4, 5000))->run($game);
|
||||||
|
}
|
||||||
|
|
||||||
|
$uniqueCount = (int)$db->queryFirstField(
|
||||||
|
"SELECT COUNT(*) FROM ng_auction WHERE type = 'uniqueItem' AND finished = 0"
|
||||||
|
);
|
||||||
|
if ($uniqueCount === 0) {
|
||||||
|
$generalID = (int)$db->queryFirstField('SELECT no FROM general WHERE npc <= 2 ORDER BY no LIMIT 1');
|
||||||
|
if ($generalID <= 0) {
|
||||||
|
throw new RuntimeException('Ref GUI parity fixture requires at least one general.');
|
||||||
|
}
|
||||||
|
$gameStorage = KVStorage::getStorage($db, 'game_env');
|
||||||
|
$clock = GameClock::fromStorage($gameStorage);
|
||||||
|
$openTick = $clock->nowTick();
|
||||||
|
$closeTick = GameClock::addTicks($openTick, $clock->ticksFromMinutes(24 * 60));
|
||||||
|
$latestBidCloseTick = GameClock::addTicks($openTick, $clock->ticksFromMinutes(2 * 24 * 60));
|
||||||
|
$openDate = $clock->tickToDateTime($openTick);
|
||||||
|
$detail = [
|
||||||
|
'title' => '손자병법 경매',
|
||||||
|
'hostName' => '청룡',
|
||||||
|
'amount' => 1,
|
||||||
|
'isReverse' => false,
|
||||||
|
'startBidAmount' => 1000,
|
||||||
|
'finishBidAmount' => null,
|
||||||
|
'remainCloseDateExtensionCnt' => 1,
|
||||||
|
'availableLatestBidCloseTick' => $latestBidCloseTick,
|
||||||
|
];
|
||||||
|
$db->insert('ng_auction', [
|
||||||
|
'type' => 'uniqueItem',
|
||||||
|
'finished' => 0,
|
||||||
|
'target' => 'che_서적_15_손자병법',
|
||||||
|
'host_general_id' => $generalID,
|
||||||
|
'req_resource' => 'inheritPoint',
|
||||||
|
'open_tick' => $openTick,
|
||||||
|
'close_tick' => $closeTick,
|
||||||
|
'detail' => Json::encode($detail),
|
||||||
|
]);
|
||||||
|
$auctionID = (int)$db->insertId();
|
||||||
|
$db->insert('ng_auction_bid', [
|
||||||
|
'auction_id' => $auctionID,
|
||||||
|
'owner' => null,
|
||||||
|
'general_id' => $generalID,
|
||||||
|
'amount' => 1000,
|
||||||
|
'date' => $openDate->format('Y-m-d H:i:s'),
|
||||||
|
'aux' => Json::encode(['generalName' => '청룡']),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
printf(
|
||||||
|
"Ref conditional capture fixtures: nation_betting=%d unique_auction=%d\n",
|
||||||
|
(int)$db->queryFirstField(
|
||||||
|
"SELECT COUNT(*) FROM storage
|
||||||
|
WHERE namespace = 'betting'
|
||||||
|
AND JSON_UNQUOTE(JSON_EXTRACT(value, '$.type')) = 'bettingNation'"
|
||||||
|
),
|
||||||
|
(int)$db->queryFirstField("SELECT COUNT(*) FROM ng_auction WHERE type = 'uniqueItem'")
|
||||||
|
);
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace sammo;
|
||||||
|
|
||||||
|
if (PHP_SAPI !== 'cli') {
|
||||||
|
http_response_code(404);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
chdir(dirname(__DIR__));
|
||||||
|
require_once 'lib.php';
|
||||||
|
require_once 'func.php';
|
||||||
|
|
||||||
|
$groups = [
|
||||||
|
'nation' => ['ActionNationType', 'ActionNationType'],
|
||||||
|
'domestic' => ['ActionSpecialDomestic', 'ActionSpecialDomestic'],
|
||||||
|
'war' => ['ActionSpecialWar', 'ActionSpecialWar'],
|
||||||
|
'personality' => ['ActionPersonality', 'ActionPersonality'],
|
||||||
|
];
|
||||||
|
|
||||||
|
$result = [];
|
||||||
|
foreach ($groups as $kind => [$directory, $namespace]) {
|
||||||
|
foreach (glob(__DIR__ . "/../sammo/{$directory}/*.php") ?: [] as $path) {
|
||||||
|
$key = basename($path, '.php');
|
||||||
|
if ($key === 'None' || str_starts_with($key, 'che_event_') || $key === 'che_거상') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$className = "\\sammo\\{$namespace}\\{$key}";
|
||||||
|
$trait = new $className();
|
||||||
|
$result[$kind][$key] = [
|
||||||
|
'name' => $trait->getName(),
|
||||||
|
'info' => $trait->getInfo(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
ksort($result[$kind]);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo Json::encode($result);
|
||||||
@@ -0,0 +1,804 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace sammo;
|
||||||
|
|
||||||
|
use sammo\Enums\RankColumn;
|
||||||
|
|
||||||
|
if (PHP_SAPI !== 'cli') {
|
||||||
|
http_response_code(404);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
chdir(dirname(__DIR__));
|
||||||
|
$_SERVER['REMOTE_ADDR'] ??= '127.0.0.1';
|
||||||
|
error_reporting(E_ALL & ~E_DEPRECATED);
|
||||||
|
require_once 'lib.php';
|
||||||
|
require_once 'func.php';
|
||||||
|
require_once __DIR__ . '/turn_state_snapshot.php';
|
||||||
|
|
||||||
|
final class TurnComparisonTracingRNG implements RNG
|
||||||
|
{
|
||||||
|
private int $sequence = 0;
|
||||||
|
|
||||||
|
/** @var list<array<string, mixed>> */
|
||||||
|
public array $calls = [];
|
||||||
|
|
||||||
|
public function __construct(private readonly RNG $inner)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getMaxInt(): int
|
||||||
|
{
|
||||||
|
return LiteHashDRBG::getMaxInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function nextBytes(int $bytes): string
|
||||||
|
{
|
||||||
|
$value = $this->inner->nextBytes($bytes);
|
||||||
|
$this->record('nextBytes', ['bytes' => $bytes], bin2hex($value));
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function nextBits(int $bits): string
|
||||||
|
{
|
||||||
|
$value = $this->inner->nextBits($bits);
|
||||||
|
$this->record('nextBits', ['bits' => $bits], bin2hex($value));
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function nextInt(?int $max = null): int
|
||||||
|
{
|
||||||
|
$value = $this->inner->nextInt($max);
|
||||||
|
$this->record('nextInt', ['maxInclusive' => $max], $value);
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function nextFloat1(): float
|
||||||
|
{
|
||||||
|
$value = $this->inner->nextFloat1();
|
||||||
|
$this->record('nextFloat1', [], $value);
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function record(string $operation, array $arguments, mixed $result): void
|
||||||
|
{
|
||||||
|
$this->calls[] = [
|
||||||
|
'seq' => $this->sequence++,
|
||||||
|
'operation' => $operation,
|
||||||
|
'arguments' => $arguments === [] ? (object)[] : $arguments,
|
||||||
|
'result' => $result,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, mixed> */
|
||||||
|
function comparisonMappedPatch(array $row, array $mapping): array
|
||||||
|
{
|
||||||
|
$patch = [];
|
||||||
|
foreach ($mapping as $canonical => $legacy) {
|
||||||
|
if (array_key_exists($canonical, $row)) {
|
||||||
|
$patch[$legacy] = $row[$canonical];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $patch;
|
||||||
|
}
|
||||||
|
|
||||||
|
function comparisonApplyTurnFixtureSetup(mixed $setup): void
|
||||||
|
{
|
||||||
|
if ($setup === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!is_array($setup)) {
|
||||||
|
throw new \InvalidArgumentException('setup must be an object');
|
||||||
|
}
|
||||||
|
$db = DB::db();
|
||||||
|
$world = $setup['world'] ?? null;
|
||||||
|
if ($world !== null) {
|
||||||
|
if (!is_array($world)) {
|
||||||
|
throw new \InvalidArgumentException('setup.world must be an object');
|
||||||
|
}
|
||||||
|
$game = KVStorage::getStorage($db, 'game_env');
|
||||||
|
foreach (['year', 'month', 'startyear', 'init_year', 'init_month'] as $key) {
|
||||||
|
$fixtureKey = match ($key) {
|
||||||
|
'startyear' => 'startYear',
|
||||||
|
'init_year' => 'initYear',
|
||||||
|
'init_month' => 'initMonth',
|
||||||
|
default => $key,
|
||||||
|
};
|
||||||
|
if (!array_key_exists($fixtureKey, $world)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$value = $world[$fixtureKey];
|
||||||
|
if (
|
||||||
|
!is_int($value)
|
||||||
|
|| $value < 1
|
||||||
|
|| (in_array($key, ['month', 'init_month'], true) && $value > 12)
|
||||||
|
) {
|
||||||
|
throw new \InvalidArgumentException("setup.world.{$fixtureKey} is invalid");
|
||||||
|
}
|
||||||
|
$game->setValue($key, $value);
|
||||||
|
}
|
||||||
|
$game->resetCache();
|
||||||
|
if (array_key_exists('hiddenSeed', $world)) {
|
||||||
|
$hiddenSeed = $world['hiddenSeed'];
|
||||||
|
if (!is_string($hiddenSeed) || $hiddenSeed === '' || strlen($hiddenSeed) > 256) {
|
||||||
|
throw new \InvalidArgumentException('setup.world.hiddenSeed is invalid');
|
||||||
|
}
|
||||||
|
UniqueConst::$hiddenSeed = $hiddenSeed;
|
||||||
|
}
|
||||||
|
// 비교 fixture는 전역 상태에 의존하지 않도록 effect 기본값을 null로 고정하고,
|
||||||
|
// 명시된 경우에만 해당 effect를 활성화합니다.
|
||||||
|
GameConst::$scenarioEffect = null;
|
||||||
|
if (array_key_exists('scenarioEffect', $world)) {
|
||||||
|
$scenarioEffect = $world['scenarioEffect'];
|
||||||
|
if ($scenarioEffect === '' || $scenarioEffect === 'None') {
|
||||||
|
$scenarioEffect = null;
|
||||||
|
}
|
||||||
|
if ($scenarioEffect !== null) {
|
||||||
|
if (!is_string($scenarioEffect) || getScenarioEffectClass($scenarioEffect) === null) {
|
||||||
|
throw new \InvalidArgumentException('setup.world.scenarioEffect is invalid');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
GameConst::$scenarioEffect = $scenarioEffect;
|
||||||
|
}
|
||||||
|
if (array_key_exists('staticEventHandlers', $world)) {
|
||||||
|
$handlersByEvent = $world['staticEventHandlers'];
|
||||||
|
if (!is_array($handlersByEvent)) {
|
||||||
|
throw new \InvalidArgumentException('setup.world.staticEventHandlers must be an object');
|
||||||
|
}
|
||||||
|
foreach ($handlersByEvent as $eventType => $handlers) {
|
||||||
|
if (!is_string($eventType) || $eventType === '' || !is_array($handlers)) {
|
||||||
|
throw new \InvalidArgumentException('setup.world.staticEventHandlers is invalid');
|
||||||
|
}
|
||||||
|
foreach ($handlers as $handler) {
|
||||||
|
if (!is_string($handler) || $handler === '') {
|
||||||
|
throw new \InvalidArgumentException('setup.world.staticEventHandlers is invalid');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
GameConst::$staticEventHandlers = $handlersByEvent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (['nations', 'cities', 'generals', 'troops', 'diplomacy'] as $collection) {
|
||||||
|
if (isset($setup[$collection]) && !is_array($setup[$collection])) {
|
||||||
|
throw new \InvalidArgumentException("setup.{$collection} must be an array");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$generalCooldowns = comparisonGeneralCooldownSelectors(
|
||||||
|
$setup['generalCooldowns'] ?? [],
|
||||||
|
'setup.generalCooldowns',
|
||||||
|
);
|
||||||
|
if ($generalCooldowns !== []) {
|
||||||
|
$nextExecuteStorage = KVStorage::getStorage($db, 'next_execute');
|
||||||
|
foreach ($setup['generalCooldowns'] as $entry) {
|
||||||
|
$nextAvailableTurn = $entry['nextAvailableTurn'] ?? null;
|
||||||
|
if (!is_int($nextAvailableTurn) || $nextAvailableTurn < 0) {
|
||||||
|
throw new \InvalidArgumentException(
|
||||||
|
'setup.generalCooldowns entries require a non-negative nextAvailableTurn',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$key = "next_execute_{$entry['generalId']}_{$entry['actionName']}";
|
||||||
|
$nextExecuteStorage->setValue($key, $nextAvailableTurn);
|
||||||
|
}
|
||||||
|
$nextExecuteStorage->resetCache();
|
||||||
|
}
|
||||||
|
if (isset($setup['randomFoundingCandidateCityIds'])) {
|
||||||
|
$candidateCityIds = comparisonIntegerList(
|
||||||
|
$setup['randomFoundingCandidateCityIds'],
|
||||||
|
'randomFoundingCandidateCityIds',
|
||||||
|
);
|
||||||
|
$db->update('city', ['level' => 4], '1 = 1');
|
||||||
|
if ($candidateCityIds !== []) {
|
||||||
|
$db->update('city', ['level' => 5], 'city IN %li', $candidateCityIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (($setup['isolateWorld'] ?? false) === true) {
|
||||||
|
$generalIds = array_values(array_map(
|
||||||
|
static fn(array $row): int => (int)$row['id'],
|
||||||
|
$setup['generals'] ?? [],
|
||||||
|
));
|
||||||
|
$nationIds = array_values(array_map(
|
||||||
|
static fn(array $row): int => (int)$row['id'],
|
||||||
|
$setup['nations'] ?? [],
|
||||||
|
));
|
||||||
|
if ($generalIds === [] || $nationIds === []) {
|
||||||
|
throw new \InvalidArgumentException('isolateWorld requires generals and nations');
|
||||||
|
}
|
||||||
|
$generalTemplate = $db->queryFirstRow(
|
||||||
|
'SELECT * FROM general WHERE no IN %li ORDER BY no LIMIT 1',
|
||||||
|
$generalIds,
|
||||||
|
);
|
||||||
|
if ($generalTemplate === null) {
|
||||||
|
throw new \InvalidArgumentException('isolateWorld requires at least one existing fixture general');
|
||||||
|
}
|
||||||
|
$generalTemplateId = (int)$generalTemplate['no'];
|
||||||
|
$rankDataTemplate = $db->query(
|
||||||
|
'SELECT `type`, `value` FROM rank_data WHERE general_id = %i',
|
||||||
|
$generalTemplateId,
|
||||||
|
);
|
||||||
|
$db->delete('general_turn', 'general_id NOT IN %li', $generalIds);
|
||||||
|
$db->delete('rank_data', 'general_id NOT IN %li', $generalIds);
|
||||||
|
$db->delete('nation_turn', 'nation_id NOT IN %li', $nationIds);
|
||||||
|
$db->delete('troop', '1 = 1');
|
||||||
|
$db->delete('diplomacy', '1 = 1');
|
||||||
|
$db->delete('general', 'no NOT IN %li', $generalIds);
|
||||||
|
foreach ($generalIds as $generalId) {
|
||||||
|
if ($db->queryFirstField('SELECT no FROM general WHERE no = %i', $generalId) !== null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$newGeneral = $generalTemplate;
|
||||||
|
$newGeneral['no'] = $generalId;
|
||||||
|
$newGeneral['owner'] = 0;
|
||||||
|
$newGeneral['name'] = "fixture-general-{$generalId}";
|
||||||
|
$db->insert('general', $newGeneral);
|
||||||
|
}
|
||||||
|
foreach ($generalIds as $generalId) {
|
||||||
|
foreach ($rankDataTemplate as $rankRow) {
|
||||||
|
$rankData = [
|
||||||
|
'general_id' => $generalId,
|
||||||
|
'type' => $rankRow['type'],
|
||||||
|
'value' => $rankRow['value'],
|
||||||
|
];
|
||||||
|
$db->insertUpdate('rank_data', $rankData, $rankData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$db->delete('nation', 'nation NOT IN %li', $nationIds);
|
||||||
|
$db->update('city', [
|
||||||
|
'nation' => 0,
|
||||||
|
'supply' => 0,
|
||||||
|
'front' => 0,
|
||||||
|
'state' => 0,
|
||||||
|
'term' => 0,
|
||||||
|
'conflict' => '{}',
|
||||||
|
'officer_set' => 0,
|
||||||
|
], '1 = 1');
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($setup['nations'] ?? [] as $row) {
|
||||||
|
if (!is_array($row) || !is_int($row['id'] ?? null) || $row['id'] < 1) {
|
||||||
|
throw new \InvalidArgumentException('setup.nations requires positive integer ids');
|
||||||
|
}
|
||||||
|
$data = comparisonMappedPatch($row, [
|
||||||
|
'id' => 'nation',
|
||||||
|
'name' => 'name',
|
||||||
|
'color' => 'color',
|
||||||
|
'capitalCityId' => 'capital',
|
||||||
|
'gold' => 'gold',
|
||||||
|
'rice' => 'rice',
|
||||||
|
'tech' => 'tech',
|
||||||
|
'level' => 'level',
|
||||||
|
'typeCode' => 'type',
|
||||||
|
'war' => 'war',
|
||||||
|
'diplomacyLimit' => 'surlimit',
|
||||||
|
'generalCount' => 'gennum',
|
||||||
|
'power' => 'power',
|
||||||
|
'capitalRevision' => 'capset',
|
||||||
|
'strategicCommandLimit' => 'strategic_cmd_limit',
|
||||||
|
]);
|
||||||
|
if (isset($row['meta'])) {
|
||||||
|
$data['aux'] = Json::encode($row['meta']);
|
||||||
|
}
|
||||||
|
$data += [
|
||||||
|
'name' => "fixture-nation-{$row['id']}",
|
||||||
|
'color' => '#777777',
|
||||||
|
'capital' => 0,
|
||||||
|
'gold' => 0,
|
||||||
|
'rice' => 0,
|
||||||
|
'level' => 1,
|
||||||
|
'type' => 'che_중립',
|
||||||
|
'aux' => '{}',
|
||||||
|
];
|
||||||
|
$db->insertUpdate('nation', $data, $data);
|
||||||
|
$nationStor = KVStorage::getStorage($db, $row['id'], 'nation_env');
|
||||||
|
if (($setup['isolateWorld'] ?? false) === true) {
|
||||||
|
$nationStor->deleteValue('recv_assist');
|
||||||
|
$nationStor->deleteValue('resp_assist');
|
||||||
|
}
|
||||||
|
if (array_key_exists('nationEnv', $row)) {
|
||||||
|
if (!is_array($row['nationEnv'])) {
|
||||||
|
throw new \InvalidArgumentException('setup.nations.nationEnv must be an object');
|
||||||
|
}
|
||||||
|
foreach ($row['nationEnv'] as $key => $value) {
|
||||||
|
if (!is_string($key) || $key === '') {
|
||||||
|
throw new \InvalidArgumentException('setup.nations.nationEnv keys must be non-empty strings');
|
||||||
|
}
|
||||||
|
$nationStor->setValue($key, $value);
|
||||||
|
}
|
||||||
|
$nationStor->resetCache();
|
||||||
|
}
|
||||||
|
if (array_key_exists('turnLastByOfficerLevel', $row)) {
|
||||||
|
$turnLastByOfficerLevel = $row['turnLastByOfficerLevel'];
|
||||||
|
if (!is_array($turnLastByOfficerLevel)) {
|
||||||
|
throw new \InvalidArgumentException('setup.nations.turnLastByOfficerLevel must be an object');
|
||||||
|
}
|
||||||
|
foreach ($turnLastByOfficerLevel as $officerLevel => $lastTurn) {
|
||||||
|
$officerLevel = filter_var($officerLevel, FILTER_VALIDATE_INT);
|
||||||
|
if (
|
||||||
|
$officerLevel === false
|
||||||
|
|| $officerLevel < 5
|
||||||
|
|| $officerLevel > 12
|
||||||
|
|| !is_array($lastTurn)
|
||||||
|
) {
|
||||||
|
throw new \InvalidArgumentException(
|
||||||
|
'setup.nations.turnLastByOfficerLevel requires officer levels 5 through 12'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$nationStor->setValue("turn_last_{$officerLevel}", $lastTurn);
|
||||||
|
}
|
||||||
|
$nationStor->resetCache();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (($setup['isolateWorld'] ?? false) === true && ($setup['nations'] ?? []) !== []) {
|
||||||
|
$maxNationId = max(array_map(
|
||||||
|
static fn(array $row): int => (int)$row['id'],
|
||||||
|
$setup['nations'],
|
||||||
|
));
|
||||||
|
$db->query('ALTER TABLE nation AUTO_INCREMENT = %i', $maxNationId + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($setup['cities'] ?? [] as $row) {
|
||||||
|
if (!is_array($row) || !is_int($row['id'] ?? null) || $row['id'] < 1) {
|
||||||
|
throw new \InvalidArgumentException('setup.cities requires positive integer ids');
|
||||||
|
}
|
||||||
|
$patch = comparisonMappedPatch($row, [
|
||||||
|
'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',
|
||||||
|
'officerSet' => 'officer_set',
|
||||||
|
]);
|
||||||
|
if (array_key_exists('conflictEntries', $row)) {
|
||||||
|
if (!is_array($row['conflictEntries'])) {
|
||||||
|
throw new \InvalidArgumentException('setup.cities.conflictEntries must be an array');
|
||||||
|
}
|
||||||
|
$orderedConflict = [];
|
||||||
|
foreach ($row['conflictEntries'] as $entry) {
|
||||||
|
if (
|
||||||
|
!is_array($entry)
|
||||||
|
|| count($entry) !== 2
|
||||||
|
|| !is_int($entry[0] ?? null)
|
||||||
|
|| $entry[0] < 1
|
||||||
|
|| !is_numeric($entry[1] ?? null)
|
||||||
|
) {
|
||||||
|
throw new \InvalidArgumentException(
|
||||||
|
'setup.cities.conflictEntries requires [positive nation id, numeric value] pairs'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$orderedConflict[$entry[0]] = $entry[1];
|
||||||
|
}
|
||||||
|
$patch['conflict'] = Json::encode($orderedConflict);
|
||||||
|
} elseif (array_key_exists('conflict', $row)) {
|
||||||
|
if (!is_array($row['conflict'])) {
|
||||||
|
throw new \InvalidArgumentException('setup.cities.conflict must be an object');
|
||||||
|
}
|
||||||
|
$patch['conflict'] = Json::encode($row['conflict']);
|
||||||
|
}
|
||||||
|
if ($patch !== []) {
|
||||||
|
$db->update('city', $patch, 'city = %i', $row['id']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($setup['generals'] ?? [] as $row) {
|
||||||
|
if (!is_array($row) || !is_int($row['id'] ?? null) || $row['id'] < 1) {
|
||||||
|
throw new \InvalidArgumentException('setup.generals requires positive integer ids');
|
||||||
|
}
|
||||||
|
$patch = comparisonMappedPatch($row, [
|
||||||
|
'name' => 'name',
|
||||||
|
'nationId' => 'nation',
|
||||||
|
'cityId' => 'city',
|
||||||
|
'troopId' => 'troop',
|
||||||
|
'leadership' => 'leadership',
|
||||||
|
'strength' => 'strength',
|
||||||
|
'intelligence' => 'intel',
|
||||||
|
'leadershipExp' => 'leadership_exp',
|
||||||
|
'strengthExp' => 'strength_exp',
|
||||||
|
'intelExp' => 'intel_exp',
|
||||||
|
'experience' => 'experience',
|
||||||
|
'dedication' => 'dedication',
|
||||||
|
'expLevel' => 'explevel',
|
||||||
|
'officerLevel' => 'officer_level',
|
||||||
|
'officerCityId' => 'officer_city',
|
||||||
|
'belong' => 'belong',
|
||||||
|
'permission' => 'permission',
|
||||||
|
'betray' => 'betray',
|
||||||
|
'makeLimit' => 'makelimit',
|
||||||
|
'injury' => 'injury',
|
||||||
|
'age' => 'age',
|
||||||
|
'gold' => 'gold',
|
||||||
|
'rice' => 'rice',
|
||||||
|
'crew' => 'crew',
|
||||||
|
'crewTypeId' => 'crewtype',
|
||||||
|
'train' => 'train',
|
||||||
|
'atmos' => 'atmos',
|
||||||
|
'dex1' => 'dex1',
|
||||||
|
'dex2' => 'dex2',
|
||||||
|
'dex3' => 'dex3',
|
||||||
|
'dex4' => 'dex4',
|
||||||
|
'dex5' => 'dex5',
|
||||||
|
'specAge' => 'specage',
|
||||||
|
'specAge2' => 'specage2',
|
||||||
|
'killTurn' => 'killturn',
|
||||||
|
'npcState' => 'npc',
|
||||||
|
'blockState' => 'block',
|
||||||
|
'specialDomestic' => 'special',
|
||||||
|
'specialWar' => 'special2',
|
||||||
|
'personality' => 'personal',
|
||||||
|
'itemHorse' => 'horse',
|
||||||
|
'itemWeapon' => 'weapon',
|
||||||
|
'itemBook' => 'book',
|
||||||
|
'itemExtra' => 'item',
|
||||||
|
]);
|
||||||
|
if (isset($row['meta'])) {
|
||||||
|
$patch['aux'] = Json::encode($row['meta']);
|
||||||
|
}
|
||||||
|
if (array_key_exists('penalty', $row)) {
|
||||||
|
if (!is_array($row['penalty'])) {
|
||||||
|
throw new \InvalidArgumentException('setup.generals.penalty must be an object');
|
||||||
|
}
|
||||||
|
$patch['penalty'] = Json::encode($row['penalty']);
|
||||||
|
}
|
||||||
|
if (isset($row['lastTurn']) && is_array($row['lastTurn'])) {
|
||||||
|
$patch['last_turn'] = Json::encode($row['lastTurn']);
|
||||||
|
}
|
||||||
|
if ($patch !== []) {
|
||||||
|
$db->update('general', $patch, 'no = %i', $row['id']);
|
||||||
|
}
|
||||||
|
if (array_key_exists('nationId', $row)) {
|
||||||
|
$db->update('rank_data', [
|
||||||
|
'nation_id' => $row['nationId'],
|
||||||
|
], 'general_id = %i', $row['id']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach ($setup['rankData'] ?? [] as $row) {
|
||||||
|
if (
|
||||||
|
!is_array($row)
|
||||||
|
|| !is_int($row['generalId'] ?? null)
|
||||||
|
|| !is_string($row['type'] ?? null)
|
||||||
|
|| RankColumn::tryFrom($row['type']) === null
|
||||||
|
|| !is_int($row['value'] ?? null)
|
||||||
|
) {
|
||||||
|
throw new \InvalidArgumentException('invalid setup.rankData row');
|
||||||
|
}
|
||||||
|
$nationId = (int)($db->queryFirstField(
|
||||||
|
'SELECT nation FROM general WHERE no = %i',
|
||||||
|
$row['generalId'],
|
||||||
|
) ?? 0);
|
||||||
|
$db->insertUpdate('rank_data', [
|
||||||
|
'general_id' => $row['generalId'],
|
||||||
|
'nation_id' => $nationId,
|
||||||
|
'type' => $row['type'],
|
||||||
|
'value' => $row['value'],
|
||||||
|
], [
|
||||||
|
'nation_id' => $nationId,
|
||||||
|
'value' => $row['value'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
if (($setup['isolateWorld'] ?? false) === true && ($setup['generals'] ?? []) !== []) {
|
||||||
|
$maxGeneralId = max(array_map(
|
||||||
|
static fn(array $row): int => (int)$row['id'],
|
||||||
|
$setup['generals'],
|
||||||
|
));
|
||||||
|
$db->query('ALTER TABLE general AUTO_INCREMENT = %i', $maxGeneralId + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($setup['troops'] ?? [] as $row) {
|
||||||
|
if (
|
||||||
|
!is_array($row)
|
||||||
|
|| !is_int($row['id'] ?? null)
|
||||||
|
|| !is_int($row['nationId'] ?? null)
|
||||||
|
|| !is_string($row['name'] ?? null)
|
||||||
|
|| $row['id'] < 1
|
||||||
|
|| $row['nationId'] < 1
|
||||||
|
|| $row['name'] === ''
|
||||||
|
) {
|
||||||
|
throw new \InvalidArgumentException('setup.troops requires positive id/nationId and non-empty name');
|
||||||
|
}
|
||||||
|
$data = [
|
||||||
|
'troop_leader' => $row['id'],
|
||||||
|
'nation' => $row['nationId'],
|
||||||
|
'name' => $row['name'],
|
||||||
|
];
|
||||||
|
$db->insertUpdate('troop', $data, $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($setup['diplomacy'] ?? [] as $row) {
|
||||||
|
if (
|
||||||
|
!is_array($row) ||
|
||||||
|
!is_int($row['fromNationId'] ?? null) ||
|
||||||
|
!is_int($row['toNationId'] ?? null) ||
|
||||||
|
$row['fromNationId'] < 1 ||
|
||||||
|
$row['toNationId'] < 1
|
||||||
|
) {
|
||||||
|
throw new \InvalidArgumentException('setup.diplomacy requires positive integer nation ids');
|
||||||
|
}
|
||||||
|
$data = comparisonMappedPatch($row, [
|
||||||
|
'fromNationId' => 'me',
|
||||||
|
'toNationId' => 'you',
|
||||||
|
'state' => 'state',
|
||||||
|
'term' => 'term',
|
||||||
|
'dead' => 'dead',
|
||||||
|
]);
|
||||||
|
$data += ['state' => 3, 'term' => 0, 'dead' => 0];
|
||||||
|
$db->insertUpdate('diplomacy', $data, $data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function comparisonRunTurnCommand(array $request): array
|
||||||
|
{
|
||||||
|
if (getenv('TURN_DIFFERENTIAL_ENABLED') !== '1') {
|
||||||
|
throw new \RuntimeException('TURN_DIFFERENTIAL_ENABLED=1 is required');
|
||||||
|
}
|
||||||
|
$kind = $request['kind'] ?? null;
|
||||||
|
if (!in_array($kind, ['general', 'nation', 'instantNation', 'troopJoinStaticEvent'], true)) {
|
||||||
|
throw new \InvalidArgumentException(
|
||||||
|
'kind must be general, nation, instantNation, or troopJoinStaticEvent',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$actorGeneralId = $request['actorGeneralId'] ?? null;
|
||||||
|
$action = $request['action'] ?? null;
|
||||||
|
$args = $request['args'] ?? null;
|
||||||
|
$includeLifecycle = $request['includeLifecycle'] ?? false;
|
||||||
|
if (!is_int($actorGeneralId) || $actorGeneralId < 1) {
|
||||||
|
throw new \InvalidArgumentException('actorGeneralId must be a positive integer');
|
||||||
|
}
|
||||||
|
if (!is_string($action) || $action === '') {
|
||||||
|
throw new \InvalidArgumentException('action must be a non-empty string');
|
||||||
|
}
|
||||||
|
if (!is_bool($includeLifecycle)) {
|
||||||
|
throw new \InvalidArgumentException('includeLifecycle must be a boolean');
|
||||||
|
}
|
||||||
|
if ($includeLifecycle && $kind !== 'general') {
|
||||||
|
throw new \InvalidArgumentException('includeLifecycle currently supports general commands only');
|
||||||
|
}
|
||||||
|
|
||||||
|
comparisonApplyTurnFixtureSetup($request['setup'] ?? null);
|
||||||
|
if ($includeLifecycle) {
|
||||||
|
DB::db()->insertUpdate('general_turn', [
|
||||||
|
'general_id' => $actorGeneralId,
|
||||||
|
'turn_idx' => 0,
|
||||||
|
'action' => $action,
|
||||||
|
'arg' => Json::encode($args),
|
||||||
|
'brief' => $action,
|
||||||
|
], [
|
||||||
|
'action' => $action,
|
||||||
|
'arg' => Json::encode($args),
|
||||||
|
'brief' => $action,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
$snapshotRequest = ['observe' => $request['observe'] ?? []];
|
||||||
|
$before = comparisonTurnStateSnapshot($snapshotRequest);
|
||||||
|
$db = DB::db();
|
||||||
|
$gameStorage = KVStorage::getStorage($db, 'game_env');
|
||||||
|
$gameStorage->resetCache();
|
||||||
|
$environment = $gameStorage->getAll();
|
||||||
|
if ($kind === 'troopJoinStaticEvent') {
|
||||||
|
if ($action !== \sammo\API\Troop\JoinTroop::class) {
|
||||||
|
throw new \InvalidArgumentException('troopJoinStaticEvent requires the JoinTroop API action');
|
||||||
|
}
|
||||||
|
$troopId = is_array($args) ? ($args['troopID'] ?? null) : null;
|
||||||
|
if (!is_int($troopId) || $troopId < 1) {
|
||||||
|
throw new \InvalidArgumentException('troopJoinStaticEvent requires a positive integer troopID');
|
||||||
|
}
|
||||||
|
$general = GeneralLite::createObjFromDB(
|
||||||
|
$actorGeneralId,
|
||||||
|
['troop'],
|
||||||
|
Enums\GeneralLiteQueryMode::Lite,
|
||||||
|
);
|
||||||
|
if ($general === null) {
|
||||||
|
throw new \RuntimeException('장수 정보를 불러올 수 없습니다.');
|
||||||
|
}
|
||||||
|
if ($general->getVar('troop') != 0) {
|
||||||
|
throw new \RuntimeException('이미 부대에 소속되어 있습니다.');
|
||||||
|
}
|
||||||
|
$nationId = $general->getNationID();
|
||||||
|
if ($nationId == 0) {
|
||||||
|
throw new \RuntimeException('국가에 소속되어 있지 않습니다.');
|
||||||
|
}
|
||||||
|
$troopExists = $db->queryFirstField(
|
||||||
|
'SELECT `troop_leader` FROM `troop` WHERE `troop_leader` = %i AND `nation` = %i',
|
||||||
|
$troopId,
|
||||||
|
$nationId,
|
||||||
|
);
|
||||||
|
if (!$troopExists) {
|
||||||
|
throw new \RuntimeException('부대가 올바르지 않습니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
GameConst::$staticEventHandlers = [
|
||||||
|
\sammo\API\Troop\JoinTroop::class => ['event_부대탑승즉시이동'],
|
||||||
|
];
|
||||||
|
$general->setVar('troop', $troopId);
|
||||||
|
StaticEventHandler::handleEvent($general, null, \sammo\API\Troop\JoinTroop::class, [], $args);
|
||||||
|
$general->applyDB($db);
|
||||||
|
$after = comparisonTurnStateSnapshot($snapshotRequest);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'schemaVersion' => 1,
|
||||||
|
'engine' => 'ref',
|
||||||
|
'execution' => [
|
||||||
|
'kind' => $kind,
|
||||||
|
'actorGeneralId' => $actorGeneralId,
|
||||||
|
'action' => $action,
|
||||||
|
'args' => $args,
|
||||||
|
'seedDomain' => 'none',
|
||||||
|
'outcome' => ['completed' => true],
|
||||||
|
],
|
||||||
|
'before' => $before,
|
||||||
|
'after' => $after,
|
||||||
|
'rng' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$general = General::createObjFromDB($actorGeneralId);
|
||||||
|
if ($kind === 'instantNation') {
|
||||||
|
$command = buildNationCommandClass($action, $general, $environment, new LastTurn(), $args);
|
||||||
|
if (!$command->hasFullConditionMet()) {
|
||||||
|
throw new \RuntimeException($command->getFailString());
|
||||||
|
}
|
||||||
|
$completed = $command->run(NoRNG::rngInstance());
|
||||||
|
$command->setNextAvailable();
|
||||||
|
$general->getLogger()->flush();
|
||||||
|
$after = comparisonTurnStateSnapshot($snapshotRequest);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'schemaVersion' => 1,
|
||||||
|
'engine' => 'ref',
|
||||||
|
'execution' => [
|
||||||
|
'kind' => $kind,
|
||||||
|
'actorGeneralId' => $actorGeneralId,
|
||||||
|
'action' => $action,
|
||||||
|
'args' => $args,
|
||||||
|
'seedDomain' => 'none',
|
||||||
|
'outcome' => [
|
||||||
|
'lastTurn' => (object)[],
|
||||||
|
'commandName' => $command->getName(),
|
||||||
|
'completed' => $completed,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'before' => $before,
|
||||||
|
'after' => $after,
|
||||||
|
'rng' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$turn = new TurnExecutionHelper($general);
|
||||||
|
$general->increaseInheritancePoint(Enums\InheritanceKey::lived_month, 1);
|
||||||
|
$preprocessRng = new RandUtil(new LiteHashDRBG(Util::simpleSerialize(
|
||||||
|
UniqueConst::$hiddenSeed,
|
||||||
|
'preprocess',
|
||||||
|
(int)$environment['year'],
|
||||||
|
(int)$environment['month'],
|
||||||
|
$actorGeneralId,
|
||||||
|
)));
|
||||||
|
$turn->preprocessCommand($preprocessRng, $environment);
|
||||||
|
$seedDomain = $kind === 'general' ? 'generalCommand' : 'nationCommand';
|
||||||
|
$seed = Util::simpleSerialize(
|
||||||
|
UniqueConst::$hiddenSeed,
|
||||||
|
$seedDomain,
|
||||||
|
(int)$environment['year'],
|
||||||
|
(int)$environment['month'],
|
||||||
|
$actorGeneralId,
|
||||||
|
$action,
|
||||||
|
);
|
||||||
|
$tracingRng = new TurnComparisonTracingRNG(new LiteHashDRBG($seed));
|
||||||
|
$rng = new RandUtil($tracingRng);
|
||||||
|
if ($kind === 'general' && $action === 'che_견문') {
|
||||||
|
\sammo\TextDecoration\SightseeingMessage::setComparisonRng($rng);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($kind === 'general') {
|
||||||
|
$previousLastTurn = $general->getLastTurn();
|
||||||
|
$previousLastTurnRaw = $previousLastTurn->toRaw();
|
||||||
|
$command = buildGeneralCommandClass($action, $general, $environment, $args);
|
||||||
|
$commandFullConditionMet = $command->hasFullConditionMet();
|
||||||
|
$resultTurn = $turn->processCommand($rng, $command, false);
|
||||||
|
} else {
|
||||||
|
$nationStor = KVStorage::getStorage($db, $general->getNationID(), 'nation_env');
|
||||||
|
$lastNationTurnKey = "turn_last_{$general->getVar('officer_level')}";
|
||||||
|
$previousLastTurn = LastTurn::fromRaw($nationStor->getValue($lastNationTurnKey));
|
||||||
|
$previousLastTurnRaw = $previousLastTurn->toRaw();
|
||||||
|
$command = buildNationCommandClass($action, $general, $environment, $previousLastTurn, $args);
|
||||||
|
$commandFullConditionMet = $command->hasFullConditionMet();
|
||||||
|
$resultTurn = $turn->processNationCommand($rng, $command);
|
||||||
|
$nationStor->setValue($lastNationTurnKey, $resultTurn->toRaw());
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($includeLifecycle) {
|
||||||
|
pullGeneralCommand($general->getID());
|
||||||
|
$general->increaseVarWithLimit(
|
||||||
|
'myset',
|
||||||
|
GameConst::$incDefSettingChange,
|
||||||
|
null,
|
||||||
|
GameConst::$maxDefSettingChange,
|
||||||
|
);
|
||||||
|
$turn->updateTurnTime();
|
||||||
|
}
|
||||||
|
$general->getLogger()->flush();
|
||||||
|
$turn->applyDB();
|
||||||
|
unset($turn);
|
||||||
|
$after = comparisonTurnStateSnapshot($snapshotRequest);
|
||||||
|
$resultTurnRaw = $resultTurn->toRaw();
|
||||||
|
$resultTerm = (int)($resultTurnRaw['term'] ?? 0);
|
||||||
|
$preReqTurn = $command->getPreReqTurn();
|
||||||
|
$acceptScoutCompleted = (
|
||||||
|
$action === 'che_등용수락'
|
||||||
|
&& is_array($args)
|
||||||
|
&& is_int($args['destNationID'] ?? null)
|
||||||
|
&& $general->getNationID() === $args['destNationID']
|
||||||
|
);
|
||||||
|
$foundNationCompleted = (
|
||||||
|
in_array($action, ['che_건국', 'cr_건국', 'che_무작위건국'], true)
|
||||||
|
&& is_array($args)
|
||||||
|
&& is_string($args['nationName'] ?? null)
|
||||||
|
&& DB::db()->queryFirstField(
|
||||||
|
'SELECT name FROM nation WHERE nation = %i',
|
||||||
|
$general->getNationID(),
|
||||||
|
) === $args['nationName']
|
||||||
|
);
|
||||||
|
$completedByState = (
|
||||||
|
$action === 'che_접경귀환'
|
||||||
|
&& $tracingRng->calls !== []
|
||||||
|
) || $acceptScoutCompleted || $foundNationCompleted || (($resultTurnRaw['command'] ?? null) === $command->getName()
|
||||||
|
&& (
|
||||||
|
($preReqTurn === 0 && $resultTerm === 0)
|
||||||
|
|| (
|
||||||
|
$preReqTurn > 0
|
||||||
|
&& ($previousLastTurnRaw['command'] ?? null) === $command->getName()
|
||||||
|
&& ($previousLastTurnRaw['arg'] ?? null) === $command->getArg()
|
||||||
|
&& (int)($previousLastTurnRaw['term'] ?? 0) === $preReqTurn
|
||||||
|
)
|
||||||
|
));
|
||||||
|
$completed = $commandFullConditionMet && $completedByState;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'schemaVersion' => 1,
|
||||||
|
'engine' => 'ref',
|
||||||
|
'execution' => [
|
||||||
|
'kind' => $kind,
|
||||||
|
'actorGeneralId' => $actorGeneralId,
|
||||||
|
'action' => $action,
|
||||||
|
'args' => $args,
|
||||||
|
'seedDomain' => $seedDomain,
|
||||||
|
'outcome' => [
|
||||||
|
'lastTurn' => $resultTurnRaw,
|
||||||
|
'commandName' => $command->getName(),
|
||||||
|
'completed' => $completed,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'before' => $before,
|
||||||
|
'after' => $after,
|
||||||
|
'rng' => $tracingRng->calls,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$input = stream_get_contents(STDIN);
|
||||||
|
$request = json_decode($input === '' ? '{}' : $input, true, flags: JSON_THROW_ON_ERROR);
|
||||||
|
if (!is_array($request)) {
|
||||||
|
throw new \InvalidArgumentException('request must be an object');
|
||||||
|
}
|
||||||
|
echo json_encode(
|
||||||
|
comparisonRunTurnCommand($request),
|
||||||
|
JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION,
|
||||||
|
), PHP_EOL;
|
||||||
|
} catch (\Throwable $throwable) {
|
||||||
|
fwrite(STDERR, $throwable::class . ': ' . $throwable->getMessage() . PHP_EOL);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
@@ -0,0 +1,661 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace sammo;
|
||||||
|
|
||||||
|
use sammo\Enums\RankColumn;
|
||||||
|
|
||||||
|
if (PHP_SAPI !== 'cli') {
|
||||||
|
http_response_code(404);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
chdir(dirname(__DIR__));
|
||||||
|
require_once 'lib.php';
|
||||||
|
require_once 'func.php';
|
||||||
|
|
||||||
|
/** @return list<int> */
|
||||||
|
function comparisonIntegerList(mixed $value, string $label): array
|
||||||
|
{
|
||||||
|
if ($value === null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (!is_array($value)) {
|
||||||
|
throw new \InvalidArgumentException("{$label} must be an array");
|
||||||
|
}
|
||||||
|
$result = [];
|
||||||
|
foreach ($value as $entry) {
|
||||||
|
if (!is_int($entry) || $entry < 0) {
|
||||||
|
throw new \InvalidArgumentException("{$label} entries must be non-negative integers");
|
||||||
|
}
|
||||||
|
$result[$entry] = $entry;
|
||||||
|
}
|
||||||
|
ksort($result, SORT_NUMERIC);
|
||||||
|
return array_values($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
function comparisonJsonValue(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function comparisonOptionalCode(mixed $value): ?string
|
||||||
|
{
|
||||||
|
if (!is_string($value) || $value === '' || $value === 'None') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return list<array{generalId: int, actionName: string}> */
|
||||||
|
function comparisonGeneralCooldownSelectors(mixed $value, string $label): array
|
||||||
|
{
|
||||||
|
if ($value === null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (!is_array($value)) {
|
||||||
|
throw new \InvalidArgumentException("{$label} must be an array");
|
||||||
|
}
|
||||||
|
$result = [];
|
||||||
|
foreach ($value as $entry) {
|
||||||
|
if (!is_array($entry)) {
|
||||||
|
throw new \InvalidArgumentException("{$label} entries must be objects");
|
||||||
|
}
|
||||||
|
$generalId = $entry['generalId'] ?? null;
|
||||||
|
$actionName = $entry['actionName'] ?? null;
|
||||||
|
if (!is_int($generalId) || $generalId < 1 || !is_string($actionName) || $actionName === '') {
|
||||||
|
throw new \InvalidArgumentException(
|
||||||
|
"{$label} entries require a positive generalId and non-empty actionName",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$result["{$generalId}:{$actionName}"] = [
|
||||||
|
'generalId' => $generalId,
|
||||||
|
'actionName' => $actionName,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
ksort($result, SORT_STRING);
|
||||||
|
return array_values($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return list<array{nationId: int, actionName: string}> */
|
||||||
|
function comparisonNationCooldownSelectors(mixed $value, string $label): array
|
||||||
|
{
|
||||||
|
if ($value === null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (!is_array($value)) {
|
||||||
|
throw new \InvalidArgumentException("{$label} must be an array");
|
||||||
|
}
|
||||||
|
$result = [];
|
||||||
|
foreach ($value as $entry) {
|
||||||
|
if (!is_array($entry)) {
|
||||||
|
throw new \InvalidArgumentException("{$label} entries must be objects");
|
||||||
|
}
|
||||||
|
$nationId = $entry['nationId'] ?? null;
|
||||||
|
$actionName = $entry['actionName'] ?? null;
|
||||||
|
if (!is_int($nationId) || $nationId < 1 || !is_string($actionName) || $actionName === '') {
|
||||||
|
throw new \InvalidArgumentException(
|
||||||
|
"{$label} entries require a positive nationId and non-empty actionName",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$result["{$nationId}:{$actionName}"] = [
|
||||||
|
'nationId' => $nationId,
|
||||||
|
'actionName' => $actionName,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
ksort($result, SORT_STRING);
|
||||||
|
return array_values($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return list<array{fromNationId: int, toNationId: int}> */
|
||||||
|
function comparisonDiplomacyPairSelectors(mixed $value, string $label): array
|
||||||
|
{
|
||||||
|
if ($value === null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (!is_array($value)) {
|
||||||
|
throw new \InvalidArgumentException("{$label} must be an array");
|
||||||
|
}
|
||||||
|
$result = [];
|
||||||
|
foreach ($value as $entry) {
|
||||||
|
if (!is_array($entry)) {
|
||||||
|
throw new \InvalidArgumentException("{$label} entries must be objects");
|
||||||
|
}
|
||||||
|
$fromNationId = $entry['fromNationId'] ?? null;
|
||||||
|
$toNationId = $entry['toNationId'] ?? null;
|
||||||
|
if (!is_int($fromNationId) || $fromNationId < 1 || !is_int($toNationId) || $toNationId < 1) {
|
||||||
|
throw new \InvalidArgumentException(
|
||||||
|
"{$label} entries require positive fromNationId and toNationId",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$result["{$fromNationId}:{$toNationId}"] = [
|
||||||
|
'fromNationId' => $fromNationId,
|
||||||
|
'toNationId' => $toNationId,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
ksort($result, SORT_STRING);
|
||||||
|
return array_values($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<string, mixed> $row */
|
||||||
|
function comparisonPickRow(array $row, array $mapping, array $jsonKeys = []): array
|
||||||
|
{
|
||||||
|
$result = [];
|
||||||
|
foreach ($mapping as $canonical => $legacy) {
|
||||||
|
if (!array_key_exists($legacy, $row)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$value = $row[$legacy];
|
||||||
|
if (in_array($legacy, $jsonKeys, true)) {
|
||||||
|
$value = comparisonJsonValue($value);
|
||||||
|
}
|
||||||
|
$result[$canonical] = $value;
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param list<int> $ids */
|
||||||
|
function comparisonRowsById(string $table, string $idColumn, array $ids): array
|
||||||
|
{
|
||||||
|
$db = DB::db();
|
||||||
|
$rows = [];
|
||||||
|
foreach ($ids as $id) {
|
||||||
|
$row = $db->queryFirstRow(
|
||||||
|
"SELECT * FROM `{$table}` WHERE `{$idColumn}` = %i",
|
||||||
|
$id,
|
||||||
|
);
|
||||||
|
if ($row !== null) {
|
||||||
|
$rows[] = $row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
function comparisonTurnStateSnapshot(array $request): array
|
||||||
|
{
|
||||||
|
$db = DB::db();
|
||||||
|
$observe = $request['observe'] ?? [];
|
||||||
|
if (!is_array($observe)) {
|
||||||
|
throw new \InvalidArgumentException('observe must be an object');
|
||||||
|
}
|
||||||
|
$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;
|
||||||
|
$includeGlobalHistoryLogs = $observe['includeGlobalHistoryLogs'] ?? false;
|
||||||
|
$generalCooldownSelectors = comparisonGeneralCooldownSelectors(
|
||||||
|
$observe['generalCooldowns'] ?? [],
|
||||||
|
'generalCooldowns',
|
||||||
|
);
|
||||||
|
$nationCooldownSelectors = comparisonNationCooldownSelectors(
|
||||||
|
$observe['nationCooldowns'] ?? [],
|
||||||
|
'nationCooldowns',
|
||||||
|
);
|
||||||
|
$diplomacyPairSelectors = comparisonDiplomacyPairSelectors(
|
||||||
|
$observe['diplomacyPairs'] ?? [],
|
||||||
|
'diplomacyPairs',
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
!is_int($logAfterId)
|
||||||
|
|| $logAfterId < 0
|
||||||
|
|| !is_int($messageAfterId)
|
||||||
|
|| $messageAfterId < 0
|
||||||
|
) {
|
||||||
|
throw new \InvalidArgumentException('logAfterId and messageAfterId must be non-negative integers');
|
||||||
|
}
|
||||||
|
if (!is_bool($includeNationHistoryLogs)) {
|
||||||
|
throw new \InvalidArgumentException('includeNationHistoryLogs must be a boolean');
|
||||||
|
}
|
||||||
|
if (!is_bool($includeGlobalHistoryLogs)) {
|
||||||
|
throw new \InvalidArgumentException('includeGlobalHistoryLogs must be a boolean');
|
||||||
|
}
|
||||||
|
|
||||||
|
$game = KVStorage::getStorage($db, 'game_env');
|
||||||
|
$game->resetCache();
|
||||||
|
$clock = GameClock::fromStorage($game);
|
||||||
|
$worldValues = $game->getValues([
|
||||||
|
'year',
|
||||||
|
'month',
|
||||||
|
'turnterm',
|
||||||
|
'turntime',
|
||||||
|
'isunited',
|
||||||
|
'scenario',
|
||||||
|
'init_year',
|
||||||
|
'init_month',
|
||||||
|
'develcost',
|
||||||
|
'killturn',
|
||||||
|
]);
|
||||||
|
$nextExecuteStorage = KVStorage::getStorage($db, 'next_execute');
|
||||||
|
$nextExecuteStorage->resetCache();
|
||||||
|
$generalCooldowns = array_map(
|
||||||
|
static function (array $selector) use ($nextExecuteStorage): array {
|
||||||
|
$key = "next_execute_{$selector['generalId']}_{$selector['actionName']}";
|
||||||
|
$value = $nextExecuteStorage->getValue($key);
|
||||||
|
return [
|
||||||
|
...$selector,
|
||||||
|
'nextAvailableTurn' => is_int($value) ? $value : null,
|
||||||
|
];
|
||||||
|
},
|
||||||
|
$generalCooldownSelectors,
|
||||||
|
);
|
||||||
|
$nationCooldowns = array_map(
|
||||||
|
static function (array $selector) use ($db): array {
|
||||||
|
$nationStorage = KVStorage::getStorage($db, $selector['nationId'], 'nation_env');
|
||||||
|
$nationStorage->resetCache();
|
||||||
|
$value = $nationStorage->getValue("next_execute_{$selector['actionName']}");
|
||||||
|
return [
|
||||||
|
...$selector,
|
||||||
|
'nextAvailableTurn' => is_int($value) ? $value : null,
|
||||||
|
];
|
||||||
|
},
|
||||||
|
$nationCooldownSelectors,
|
||||||
|
);
|
||||||
|
|
||||||
|
$generals = array_map(
|
||||||
|
static function (array $row) use ($clock): array {
|
||||||
|
$result = comparisonPickRow(
|
||||||
|
$row,
|
||||||
|
[
|
||||||
|
'id' => 'no',
|
||||||
|
'name' => 'name',
|
||||||
|
'nationId' => 'nation',
|
||||||
|
'cityId' => 'city',
|
||||||
|
'troopId' => 'troop',
|
||||||
|
'leadership' => 'leadership',
|
||||||
|
'strength' => 'strength',
|
||||||
|
'intelligence' => 'intel',
|
||||||
|
'experience' => 'experience',
|
||||||
|
'dedication' => 'dedication',
|
||||||
|
'expLevel' => 'explevel',
|
||||||
|
'officerLevel' => 'officer_level',
|
||||||
|
'officerCityId' => 'officer_city',
|
||||||
|
'belong' => 'belong',
|
||||||
|
'permission' => 'permission',
|
||||||
|
'betray' => 'betray',
|
||||||
|
'makeLimit' => 'makelimit',
|
||||||
|
'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',
|
||||||
|
'penalty' => 'penalty',
|
||||||
|
'leadershipExp' => 'leadership_exp',
|
||||||
|
'strengthExp' => 'strength_exp',
|
||||||
|
'intelExp' => 'intel_exp',
|
||||||
|
'dex1' => 'dex1',
|
||||||
|
'dex2' => 'dex2',
|
||||||
|
'dex3' => 'dex3',
|
||||||
|
'dex4' => 'dex4',
|
||||||
|
'dex5' => 'dex5',
|
||||||
|
'specAge' => 'specage',
|
||||||
|
'specAge2' => 'specage2',
|
||||||
|
'killTurn' => 'killturn',
|
||||||
|
'mySet' => 'myset',
|
||||||
|
'specialDomestic' => 'special',
|
||||||
|
'specialWar' => 'special2',
|
||||||
|
'personality' => 'personal',
|
||||||
|
'itemHorse' => 'horse',
|
||||||
|
'itemWeapon' => 'weapon',
|
||||||
|
'itemBook' => 'book',
|
||||||
|
'itemExtra' => 'item',
|
||||||
|
],
|
||||||
|
['last_turn', 'aux', 'penalty'],
|
||||||
|
);
|
||||||
|
foreach ([
|
||||||
|
'specialDomestic',
|
||||||
|
'specialWar',
|
||||||
|
'personality',
|
||||||
|
'itemHorse',
|
||||||
|
'itemWeapon',
|
||||||
|
'itemBook',
|
||||||
|
'itemExtra',
|
||||||
|
] as $key) {
|
||||||
|
$result[$key] = comparisonOptionalCode($result[$key] ?? null);
|
||||||
|
}
|
||||||
|
$result['turnTime'] = $clock->formatTick(Util::toInt($row['turntime']), true);
|
||||||
|
$result['recentWarTime'] = $row['recent_war'] === null
|
||||||
|
? null
|
||||||
|
: $clock->formatTick(Util::toInt($row['recent_war']), true);
|
||||||
|
$result['maxBelong'] = (int)($result['meta']['max_belong'] ?? 0);
|
||||||
|
$ownerId = (int)($row['owner'] ?? 0);
|
||||||
|
$activeActionPoints = 0;
|
||||||
|
if ($ownerId > 0) {
|
||||||
|
$inheritanceStorage = KVStorage::getStorage(DB::db(), "inheritance_{$ownerId}");
|
||||||
|
$inheritanceStorage->resetCache();
|
||||||
|
$stored = $inheritanceStorage->getValue('active_action');
|
||||||
|
if (is_array($stored) && (is_int($stored[0] ?? null) || is_float($stored[0] ?? null))) {
|
||||||
|
$activeActionPoints = $stored[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$result['hasOwner'] = $ownerId > 0;
|
||||||
|
$result['inheritActiveActionPoints'] = $activeActionPoints;
|
||||||
|
return $result;
|
||||||
|
},
|
||||||
|
comparisonRowsById('general', 'no', $generalIds),
|
||||||
|
);
|
||||||
|
$rankTypes = array_map(
|
||||||
|
static fn(RankColumn $column): string => $column->value,
|
||||||
|
RankColumn::cases(),
|
||||||
|
);
|
||||||
|
$rankData = $generalIds === []
|
||||||
|
? []
|
||||||
|
: iterator_to_array($db->query(
|
||||||
|
'SELECT general_id AS generalId, nation_id AS nationId, `type`, `value`'
|
||||||
|
. ' FROM rank_data WHERE general_id IN %li AND `type` IN %ls'
|
||||||
|
. ' ORDER BY general_id, `type`',
|
||||||
|
$generalIds,
|
||||||
|
$rankTypes,
|
||||||
|
));
|
||||||
|
|
||||||
|
$cities = array_map(
|
||||||
|
static fn(array $row): array => comparisonPickRow(
|
||||||
|
$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',
|
||||||
|
'officerSet' => 'officer_set',
|
||||||
|
],
|
||||||
|
['conflict'],
|
||||||
|
),
|
||||||
|
comparisonRowsById('city', 'city', $cityIds),
|
||||||
|
);
|
||||||
|
|
||||||
|
$nations = array_map(
|
||||||
|
static function (array $row) use ($db): array {
|
||||||
|
$projected = comparisonPickRow(
|
||||||
|
$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',
|
||||||
|
'meta' => 'aux',
|
||||||
|
],
|
||||||
|
['aux'],
|
||||||
|
);
|
||||||
|
$nationStor = KVStorage::getStorage($db, (int)$row['nation'], 'nation_env');
|
||||||
|
$receivedAssist = $nationStor->getValue('recv_assist');
|
||||||
|
if (is_array($receivedAssist) && $receivedAssist !== []) {
|
||||||
|
$projected['meta']['recv_assist'] = $receivedAssist;
|
||||||
|
}
|
||||||
|
$respondedAssist = $nationStor->getValue('resp_assist');
|
||||||
|
if (is_array($respondedAssist) && $respondedAssist !== []) {
|
||||||
|
$projected['meta']['resp_assist'] = $respondedAssist;
|
||||||
|
}
|
||||||
|
return $projected;
|
||||||
|
},
|
||||||
|
comparisonRowsById('nation', 'nation', $nationIds),
|
||||||
|
);
|
||||||
|
|
||||||
|
$diplomacyPairs = [];
|
||||||
|
foreach ($nationIds as $fromNationId) {
|
||||||
|
foreach ($nationIds as $toNationId) {
|
||||||
|
if ($fromNationId === $toNationId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$diplomacyPairs["{$fromNationId}:{$toNationId}"] = [
|
||||||
|
'fromNationId' => $fromNationId,
|
||||||
|
'toNationId' => $toNationId,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach ($diplomacyPairSelectors as $pair) {
|
||||||
|
$diplomacyPairs["{$pair['fromNationId']}:{$pair['toNationId']}"] = $pair;
|
||||||
|
}
|
||||||
|
ksort($diplomacyPairs, SORT_STRING);
|
||||||
|
|
||||||
|
$diplomacy = [];
|
||||||
|
foreach ($diplomacyPairs as $pair) {
|
||||||
|
$row = $db->queryFirstRow(
|
||||||
|
'SELECT me, you, state, term, dead FROM diplomacy WHERE me = %i AND you = %i',
|
||||||
|
$pair['fromNationId'],
|
||||||
|
$pair['toNationId'],
|
||||||
|
);
|
||||||
|
if ($row !== null) {
|
||||||
|
$diplomacy[] = comparisonPickRow($row, [
|
||||||
|
'fromNationId' => 'me',
|
||||||
|
'toNationId' => 'you',
|
||||||
|
'state' => 'state',
|
||||||
|
'term' => 'term',
|
||||||
|
'dead' => 'dead',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$generalTurns = [];
|
||||||
|
foreach ($generalIds as $generalId) {
|
||||||
|
foreach ($db->query(
|
||||||
|
'SELECT general_id, turn_idx, action, arg FROM general_turn WHERE general_id = %i ORDER BY turn_idx',
|
||||||
|
$generalId,
|
||||||
|
) as $row) {
|
||||||
|
$generalTurns[] = comparisonPickRow(
|
||||||
|
$row,
|
||||||
|
['generalId' => 'general_id', 'turnIndex' => 'turn_idx', 'action' => 'action', 'args' => 'arg'],
|
||||||
|
['arg'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$nationTurns = [];
|
||||||
|
foreach ($nationIds as $nationId) {
|
||||||
|
foreach ($db->query(
|
||||||
|
'SELECT nation_id, officer_level, turn_idx, action, arg FROM nation_turn WHERE nation_id = %i ORDER BY officer_level, turn_idx',
|
||||||
|
$nationId,
|
||||||
|
) as $row) {
|
||||||
|
$nationTurns[] = comparisonPickRow(
|
||||||
|
$row,
|
||||||
|
[
|
||||||
|
'nationId' => 'nation_id',
|
||||||
|
'officerLevel' => 'officer_level',
|
||||||
|
'turnIndex' => 'turn_idx',
|
||||||
|
'action' => 'action',
|
||||||
|
'args' => 'arg',
|
||||||
|
],
|
||||||
|
['arg'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$logs = [];
|
||||||
|
if ($generalIds !== []) {
|
||||||
|
foreach ($db->query(
|
||||||
|
'SELECT id, general_id, log_type, year, month, text FROM general_record WHERE id > %i ORDER BY id',
|
||||||
|
$logAfterId,
|
||||||
|
) as $row) {
|
||||||
|
if ((int)$row['general_id'] !== 0 && !in_array((int)$row['general_id'], $generalIds, true)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$generalId = (int)$row['general_id'];
|
||||||
|
$category = (string)$row['log_type'];
|
||||||
|
if ($category === 'battle') {
|
||||||
|
$category = 'battle_detail';
|
||||||
|
} elseif ($generalId === 0) {
|
||||||
|
$category = 'summary';
|
||||||
|
}
|
||||||
|
$logs[] = comparisonPickRow($row, [
|
||||||
|
'id' => 'id',
|
||||||
|
'generalId' => 'general_id',
|
||||||
|
'year' => 'year',
|
||||||
|
'month' => 'month',
|
||||||
|
'text' => 'text',
|
||||||
|
]) + [
|
||||||
|
'scope' => $generalId === 0 ? 'system' : 'general',
|
||||||
|
'category' => $category,
|
||||||
|
'nationId' => null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($includeNationHistoryLogs && $nationIds !== []) {
|
||||||
|
foreach ($db->query(
|
||||||
|
'SELECT id, nation_id, year, month, text FROM world_history WHERE nation_id IN %li ORDER BY id',
|
||||||
|
$nationIds,
|
||||||
|
) as $row) {
|
||||||
|
$logs[] = comparisonPickRow($row, [
|
||||||
|
'id' => 'id',
|
||||||
|
'nationId' => 'nation_id',
|
||||||
|
'year' => 'year',
|
||||||
|
'month' => 'month',
|
||||||
|
'text' => 'text',
|
||||||
|
]) + [
|
||||||
|
'scope' => 'nation',
|
||||||
|
'category' => 'history',
|
||||||
|
'generalId' => null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($includeGlobalHistoryLogs) {
|
||||||
|
foreach ($db->query(
|
||||||
|
'SELECT id, nation_id, year, month, text FROM world_history WHERE nation_id = 0 ORDER BY id',
|
||||||
|
) as $row) {
|
||||||
|
$logs[] = comparisonPickRow($row, [
|
||||||
|
'id' => 'id',
|
||||||
|
'nationId' => 'nation_id',
|
||||||
|
'year' => 'year',
|
||||||
|
'month' => 'month',
|
||||||
|
'text' => 'text',
|
||||||
|
]) + [
|
||||||
|
'scope' => 'system',
|
||||||
|
'category' => 'history',
|
||||||
|
'generalId' => null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$messages = array_map(
|
||||||
|
static function (array $row) use ($clock): array {
|
||||||
|
$result = comparisonPickRow(
|
||||||
|
$row,
|
||||||
|
[
|
||||||
|
'id' => 'id',
|
||||||
|
'mailbox' => 'mailbox',
|
||||||
|
'type' => 'type',
|
||||||
|
'sourceId' => 'src',
|
||||||
|
'destinationId' => 'dest',
|
||||||
|
'createdAt' => 'time',
|
||||||
|
'payload' => 'message',
|
||||||
|
],
|
||||||
|
['message'],
|
||||||
|
);
|
||||||
|
$result['createdAt'] = $clock->formatTick(Util::toInt($row['time']), true);
|
||||||
|
return $result;
|
||||||
|
},
|
||||||
|
$db->query(
|
||||||
|
'SELECT id, mailbox, type, src, dest, time, message FROM message WHERE id > %i ORDER BY id',
|
||||||
|
$messageAfterId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'schemaVersion' => 1,
|
||||||
|
'engine' => 'ref',
|
||||||
|
'world' => [
|
||||||
|
'year' => (int)$worldValues['year'],
|
||||||
|
'month' => (int)$worldValues['month'],
|
||||||
|
'tickMinutes' => (int)$worldValues['turnterm'],
|
||||||
|
'turnTime' => $clock->formatTick(Util::toInt($worldValues['turntime']), true),
|
||||||
|
'isUnited' => (int)$worldValues['isunited'],
|
||||||
|
'scenarioId' => (int)$worldValues['scenario'],
|
||||||
|
'initYear' => (int)$worldValues['init_year'],
|
||||||
|
'initMonth' => (int)$worldValues['init_month'],
|
||||||
|
'develCost' => (int)$worldValues['develcost'],
|
||||||
|
'killTurn' => (int)$worldValues['killturn'],
|
||||||
|
'generalCooldowns' => $generalCooldowns,
|
||||||
|
'nationCooldowns' => $nationCooldowns,
|
||||||
|
],
|
||||||
|
'generals' => $generals,
|
||||||
|
'rankData' => $rankData,
|
||||||
|
'cities' => $cities,
|
||||||
|
'nations' => $nations,
|
||||||
|
'diplomacy' => $diplomacy,
|
||||||
|
'generalTurns' => $generalTurns,
|
||||||
|
'nationTurns' => $nationTurns,
|
||||||
|
'logs' => $logs,
|
||||||
|
'messages' => $messages,
|
||||||
|
'watermarks' => [
|
||||||
|
'logId' => (int)($db->queryFirstField('SELECT COALESCE(MAX(id), 0) FROM general_record') ?? 0),
|
||||||
|
'historyLogId' => (int)($db->queryFirstField('SELECT COALESCE(MAX(id), 0) FROM world_history') ?? 0),
|
||||||
|
'messageId' => (int)($db->queryFirstField('SELECT COALESCE(MAX(id), 0) FROM message') ?? 0),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function comparisonTurnStateSnapshotMain(): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$input = stream_get_contents(STDIN);
|
||||||
|
$request = json_decode($input === '' ? '{}' : $input, true, flags: JSON_THROW_ON_ERROR);
|
||||||
|
if (!is_array($request)) {
|
||||||
|
throw new \InvalidArgumentException('request must be an object');
|
||||||
|
}
|
||||||
|
echo json_encode(
|
||||||
|
comparisonTurnStateSnapshot($request),
|
||||||
|
JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION,
|
||||||
|
), PHP_EOL;
|
||||||
|
} catch (\Throwable $throwable) {
|
||||||
|
fwrite(STDERR, $throwable::class . ': ' . $throwable->getMessage() . PHP_EOL);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (realpath((string)($_SERVER['SCRIPT_FILENAME'] ?? '')) === __FILE__) {
|
||||||
|
comparisonTurnStateSnapshotMain();
|
||||||
|
}
|
||||||
+1
-1
@@ -1734,7 +1734,7 @@ function deleteNation(General $lord, bool $applyDB): array
|
|||||||
|
|
||||||
$nationGeneralList = General::createObjListFromDB(
|
$nationGeneralList = General::createObjListFromDB(
|
||||||
$db->queryFirstColumn(
|
$db->queryFirstColumn(
|
||||||
'SELECT `no` FROM general WHERE nation=%i AND no != %i',
|
'SELECT `no` FROM general WHERE nation=%i AND no != %i ORDER BY no ASC',
|
||||||
$nationID,
|
$nationID,
|
||||||
$lordID
|
$lordID
|
||||||
),
|
),
|
||||||
|
|||||||
+32
-1
@@ -319,7 +319,38 @@ function postUpdateMonthly(RandUtil $rng)
|
|||||||
|
|
||||||
//약간의 랜덤치 부여 (95% ~ 105%)
|
//약간의 랜덤치 부여 (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['maxPower'] = max($powerValues['maxPower'] ?? 0, $nation['power']);
|
||||||
$powerValues['maxCrew'] = max($powerValues['maxCrew'] ?? 0, Util::toInt($nation['totalCrew']));
|
$powerValues['maxCrew'] = max($powerValues['maxCrew'] ?? 0, Util::toInt($nation['totalCrew']));
|
||||||
|
|
||||||
|
|||||||
@@ -367,7 +367,7 @@ function disaster(RandUtil $rng) {
|
|||||||
[$queryColumns,,] = General::mergeQueryColumn();
|
[$queryColumns,,] = General::mergeQueryColumn();
|
||||||
$generalListByCity = Util::arrayGroupBy(
|
$generalListByCity = Util::arrayGroupBy(
|
||||||
$db->query(
|
$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::formatListOfBackticks($queryColumns),
|
||||||
Util::squeezeFromArray($targetCityList, 'city')
|
Util::squeezeFromArray($targetCityList, 'city')
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -15,6 +15,15 @@ function calcTournamentTerm(int $turnTerm): int{
|
|||||||
|
|
||||||
function processTournament()
|
function processTournament()
|
||||||
{
|
{
|
||||||
|
if (
|
||||||
|
getenv('REF_DETERMINISTIC_INSTALL_ENABLED') === '1'
|
||||||
|
&& getenv('REF_DETERMINISTIC_TOURNAMENT_MODE') === 'hold'
|
||||||
|
) {
|
||||||
|
// Tournament randomness is independent from the monthly hidden-seed
|
||||||
|
// stream. Keep it out of deterministic monthly comparisons.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$db = DB::db();
|
$db = DB::db();
|
||||||
$gameStor = KVStorage::getStorage($db, 'game_env');
|
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||||
$clock = GameClock::fromStorage($gameStor);
|
$clock = GameClock::fromStorage($gameStor);
|
||||||
|
|||||||
@@ -52,14 +52,9 @@ if($token && $refresh){
|
|||||||
if($now >= $pickMoreFrom){
|
if($now >= $pickMoreFrom){
|
||||||
$oldPickResult = Json::decode($token['pick_result']);
|
$oldPickResult = Json::decode($token['pick_result']);
|
||||||
|
|
||||||
foreach($keepResult as $keepId){
|
$keepSelection = NpcPossessionSelector::applyKeep($oldPickResult, $keepResult);
|
||||||
if(\key_exists($keepId, $oldPickResult) && $oldPickResult[$keepId]['keepCnt'] > 0){
|
$pickResult = $keepSelection['picked'];
|
||||||
$pickResult[$keepId] = $oldPickResult[$keepId];
|
if($keepSelection['cancelled']){
|
||||||
$pickResult[$keepId]['keepCnt']-=1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(count($pickResult) == count($oldPickResult)){
|
|
||||||
$refresh = false;
|
$refresh = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -94,35 +89,22 @@ foreach($db->query('SELECT `no`, `name`, leadership, strength, intel, nation, im
|
|||||||
$general['personal'] = buildPersonalityClass($general['personal'])->getName();
|
$general['personal'] = buildPersonalityClass($general['personal'])->getName();
|
||||||
$general['nation'] = getNationStaticInfo($general['nation'])['name'];
|
$general['nation'] = getNationStaticInfo($general['nation'])['name'];
|
||||||
$candidates[$general['no']] = $general + ['keepCnt'=>KEEP_CNT];
|
$candidates[$general['no']] = $general + ['keepCnt'=>KEEP_CNT];
|
||||||
$allStat = $general['leadership'] + $general['strength'] + $general['intel'];
|
$weight[$general['no']] = NpcPossessionSelector::weight($general);
|
||||||
$weight[$general['no']] = pow($allStat, 1.5);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach($db->queryFirstColumn('SELECT pick_result FROM select_npc_token WHERE `owner`!=%i AND valid_until >=%i', $userID, $now) as $reserved){
|
NpcPossessionSelector::removeReserved(
|
||||||
$reserved = Json::decode($reserved);
|
$candidates,
|
||||||
foreach(array_keys($reserved) as $reservedNPC){
|
$weight,
|
||||||
if(key_exists($reservedNPC, $weight)){
|
$db->queryFirstColumn('SELECT pick_result FROM select_npc_token WHERE `owner`!=%i AND valid_until >=%i', $userID, $now),
|
||||||
unset($candidates[$reservedNPC]);
|
);
|
||||||
unset($weight[$reservedNPC]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$rng = new RandUtil(new LiteHashDRBG(Util::simpleSerialize(
|
$rng = new RandUtil(new LiteHashDRBG(NpcPossessionSelector::buildSeed(
|
||||||
UniqueConst::$hiddenSeed,
|
UniqueConst::$hiddenSeed,
|
||||||
'SelectNPCToken',
|
|
||||||
$userID,
|
$userID,
|
||||||
$now,
|
$now,
|
||||||
)));
|
)));
|
||||||
|
|
||||||
$pickLimit = min(count($candidates), 5);
|
$pickResult = NpcPossessionSelector::select($candidates, $weight, $pickResult, $rng);
|
||||||
|
|
||||||
while(count($pickResult) < $pickLimit){
|
|
||||||
$generalID = $rng->choiceUsingWeight($weight);
|
|
||||||
if(!key_exists($generalID, $pickResult)){
|
|
||||||
$pickResult[$generalID] = $candidates[$generalID];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$newNonce = random_int(0, 0xfffffff);
|
$newNonce = random_int(0, 0xfffffff);
|
||||||
|
|
||||||
|
|||||||
+11
-3
@@ -199,8 +199,16 @@ if($reserve_open){
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
try{
|
try{
|
||||||
Json::die(ResetHelper::buildScenario(
|
$installTime = TimeUtil::format(GameClock::readWallTime(), false);
|
||||||
|
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,
|
$turnterm,
|
||||||
$sync,
|
$sync,
|
||||||
$scenario,
|
$scenario,
|
||||||
@@ -211,7 +219,7 @@ try{
|
|||||||
$show_img_level,
|
$show_img_level,
|
||||||
!!$tournament_trig,
|
!!$tournament_trig,
|
||||||
$join_mode,
|
$join_mode,
|
||||||
TimeUtil::format(GameClock::readWallTime(), false),
|
$installTime,
|
||||||
$autorun_user
|
$autorun_user
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|||||||
+198
-3
@@ -37,15 +37,44 @@ function processWar(string $warSeed, General $attackerGeneral, array $rawAttacke
|
|||||||
|
|
||||||
$city = new WarUnitCity($rng, $rawDefenderCity, $rawDefenderNation, $year, $month, $startYear);
|
$city = new WarUnitCity($rng, $rawDefenderCity, $rawDefenderNation, $year, $month, $startYear);
|
||||||
|
|
||||||
$defenderCityGeneralIDList = $db->queryFirstColumn('SELECT no FROM general WHERE nation=%i AND city=%i AND nation!=0', $city->getVar('nation'), $city->getVar('city'));
|
$defenderCityGeneralIDList = $db->queryFirstColumn('SELECT no FROM general WHERE nation=%i AND city=%i AND nation!=0 ORDER BY no', $city->getVar('nation'), $city->getVar('city'));
|
||||||
$defenderCityGeneralList = General::createObjListFromDB($defenderCityGeneralIDList, null);
|
$defenderCityGeneralList = General::createObjListFromDB($defenderCityGeneralIDList, null);
|
||||||
|
|
||||||
|
$captureBattleFixture =
|
||||||
|
PHP_SAPI === 'cli'
|
||||||
|
&& getenv('REF_DETERMINISTIC_INSTALL_ENABLED') === '1'
|
||||||
|
&& getenv('REF_BATTLE_FIXTURE_TRACE') === '1';
|
||||||
|
if ($captureBattleFixture) {
|
||||||
|
require_once __DIR__ . '/compare/battle_fixture_capture.php';
|
||||||
|
fwrite(STDOUT, 'AI_WAR_FIXTURE_REF ' . Json::encode(comparisonBuildBattleFixture(
|
||||||
|
$warSeed,
|
||||||
|
(int)$year,
|
||||||
|
(int)$month,
|
||||||
|
(int)$startYear,
|
||||||
|
$attackerGeneral,
|
||||||
|
$rawAttackerNation,
|
||||||
|
$defenderCityGeneralList,
|
||||||
|
$rawDefenderCity,
|
||||||
|
$rawDefenderNation,
|
||||||
|
)) . "\n");
|
||||||
|
}
|
||||||
|
|
||||||
/** @var WarUnit[] */
|
/** @var WarUnit[] */
|
||||||
$defenderList = [];
|
$defenderList = [];
|
||||||
|
$defenderCandidateTrace = [];
|
||||||
foreach($defenderCityGeneralList as $defenderGeneral){
|
foreach($defenderCityGeneralList as $defenderGeneral){
|
||||||
$defenderGeneral->setRawCity($rawDefenderCity);
|
$defenderGeneral->setRawCity($rawDefenderCity);
|
||||||
$defenderCandidate = new WarUnitGeneral($rng, $defenderGeneral, $rawDefenderNation, false);
|
$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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,6 +85,14 @@ function processWar(string $warSeed, General $attackerGeneral, array $rawAttacke
|
|||||||
$defenderList[] = $city;
|
$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) {
|
usort($defenderList, function (WarUnit $lhs, WarUnit $rhs) use ($attacker) {
|
||||||
return - (extractBattleOrder($lhs, $attacker) <=> extractBattleOrder($rhs, $attacker));
|
return - (extractBattleOrder($lhs, $attacker) <=> extractBattleOrder($rhs, $attacker));
|
||||||
});
|
});
|
||||||
@@ -85,7 +122,39 @@ function processWar(string $warSeed, General $attackerGeneral, array $rawAttacke
|
|||||||
return $nextDefender;
|
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);
|
$attacker->applyDB($db);
|
||||||
|
|
||||||
@@ -157,6 +226,32 @@ function processWar(string $warSeed, General $attackerGeneral, array $rawAttacke
|
|||||||
$defenderIncTech /= 4;
|
$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(
|
$updateAttackerNation['tech'] = $db->sqleval(
|
||||||
'tech + %d',
|
'tech + %d',
|
||||||
$attackerIncTech / max(GameConst::$initialNationGenLimit, $rawAttackerNation['gennum'])
|
$attackerIncTech / max(GameConst::$initialNationGenLimit, $rawAttackerNation['gennum'])
|
||||||
@@ -230,6 +325,7 @@ function processWar_NG(
|
|||||||
WarUnitGeneral $attacker,
|
WarUnitGeneral $attacker,
|
||||||
callable $getNextDefender,
|
callable $getNextDefender,
|
||||||
WarUnitCity $city,
|
WarUnitCity $city,
|
||||||
|
?callable $trace = null,
|
||||||
): bool {
|
): bool {
|
||||||
$templates = new \League\Plates\Engine(__DIR__ . '/templates');
|
$templates = new \League\Plates\Engine(__DIR__ . '/templates');
|
||||||
|
|
||||||
@@ -245,6 +341,26 @@ function processWar_NG(
|
|||||||
/** @var WarUnit */
|
/** @var WarUnit */
|
||||||
$defender = ($getNextDefender)(null, true);
|
$defender = ($getNextDefender)(null, true);
|
||||||
$conquerCity = false;
|
$conquerCity = false;
|
||||||
|
$traceSeq = 0;
|
||||||
|
$emitTrace = function (string $event, ?WarUnit $currentDefender, array $details = []) use (
|
||||||
|
$trace,
|
||||||
|
&$traceSeq,
|
||||||
|
$attacker,
|
||||||
|
$city
|
||||||
|
): void {
|
||||||
|
if ($trace === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$trace([
|
||||||
|
'seq' => $traceSeq++,
|
||||||
|
'event' => $event,
|
||||||
|
'attacker' => buildWarTraceUnitSnapshot($attacker),
|
||||||
|
'defender' => $currentDefender === null ? null : buildWarTraceUnitSnapshot($currentDefender),
|
||||||
|
'city' => buildWarTraceUnitSnapshot($city),
|
||||||
|
'details' => $details,
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
$emitTrace('battle_start', $defender, ['seed' => $warSeed]);
|
||||||
|
|
||||||
$josaRo = JosaUtil::pick($city->getName(), '로');
|
$josaRo = JosaUtil::pick($city->getName(), '로');
|
||||||
$josaYi = JosaUtil::pick($attacker->getName(), '이');
|
$josaYi = JosaUtil::pick($attacker->getName(), '이');
|
||||||
@@ -277,6 +393,7 @@ function processWar_NG(
|
|||||||
$logger->pushGlobalHistoryLog("<M><b>【패퇴】</b></><D><b>{$defender->getNationVar('name')}</b></>{$josaYi} 병량 부족으로 <G><b>{$defender->getName()}</b></>{$josaUl} 뺏기고 말았습니다.");
|
$logger->pushGlobalHistoryLog("<M><b>【패퇴】</b></><D><b>{$defender->getNationVar('name')}</b></>{$josaYi} 병량 부족으로 <G><b>{$defender->getName()}</b></>{$josaUl} 뺏기고 말았습니다.");
|
||||||
|
|
||||||
$conquerCity = true;
|
$conquerCity = true;
|
||||||
|
$emitTrace('supply_retreat', $defender);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -332,18 +449,34 @@ function processWar_NG(
|
|||||||
$initCaller->merge($defender->getGeneral()->getBattleInitSkillTriggerList($defender));
|
$initCaller->merge($defender->getGeneral()->getBattleInitSkillTriggerList($defender));
|
||||||
|
|
||||||
$initCaller->fire($attacker->rng, [], [$attacker, $defender]);
|
$initCaller->fire($attacker->rng, [], [$attacker, $defender]);
|
||||||
|
$emitTrace('opponent_initialized', $defender);
|
||||||
}
|
}
|
||||||
|
|
||||||
$attacker->beginPhase();
|
$attacker->beginPhase();
|
||||||
$defender->beginPhase();
|
$defender->beginPhase();
|
||||||
|
if ($trace !== null) {
|
||||||
|
$emitTrace('phase_power', $defender, [
|
||||||
|
'attackerAttack' => $attacker->getComputedAttack(),
|
||||||
|
'attackerDefence' => $attacker->getComputedDefence(),
|
||||||
|
'attackerTrain' => $attacker->getComputedTrain(),
|
||||||
|
'attackerAtmos' => $attacker->getComputedAtmos(),
|
||||||
|
'defenderAttack' => $defender->getComputedAttack(),
|
||||||
|
'defenderDefence' => $defender->getComputedDefence(),
|
||||||
|
'defenderTrain' => $defender->getComputedTrain(),
|
||||||
|
'defenderAtmos' => $defender->getComputedAtmos(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
$battleCaller = $attacker->getGeneral()->getBattlePhaseSkillTriggerList($attacker);
|
$battleCaller = $attacker->getGeneral()->getBattlePhaseSkillTriggerList($attacker);
|
||||||
$battleCaller->merge($defender->getGeneral()->getBattlePhaseSkillTriggerList($defender));
|
$battleCaller->merge($defender->getGeneral()->getBattlePhaseSkillTriggerList($defender));
|
||||||
|
|
||||||
$battleCaller->fire($attacker->rng, [], [$attacker, $defender]);
|
$battleCaller->fire($attacker->rng, [], [$attacker, $defender]);
|
||||||
|
$emitTrace('phase_triggered', $defender);
|
||||||
|
|
||||||
$deadDefender = $attacker->calcDamage();
|
$deadDefender = $attacker->calcDamage();
|
||||||
$deadAttacker = $defender->calcDamage();
|
$deadAttacker = $defender->calcDamage();
|
||||||
|
$rawDeadAttacker = $deadAttacker;
|
||||||
|
$rawDeadDefender = $deadDefender;
|
||||||
|
|
||||||
$attackerHP = $attacker->getHP();
|
$attackerHP = $attacker->getHP();
|
||||||
$defenderHP = $defender->getHP();
|
$defenderHP = $defender->getHP();
|
||||||
@@ -371,6 +504,14 @@ function processWar_NG(
|
|||||||
|
|
||||||
$attacker->increaseKilled($deadDefender);
|
$attacker->increaseKilled($deadDefender);
|
||||||
$defender->increaseKilled($deadAttacker);
|
$defender->increaseKilled($deadAttacker);
|
||||||
|
$emitTrace('phase_damage', $defender, [
|
||||||
|
'rawDeadAttacker' => $rawDeadAttacker,
|
||||||
|
'rawDeadDefender' => $rawDeadDefender,
|
||||||
|
'deadAttacker' => $deadAttacker,
|
||||||
|
'deadDefender' => $deadDefender,
|
||||||
|
'attackerHpBefore' => $attackerHP,
|
||||||
|
'defenderHpBefore' => $defenderHP,
|
||||||
|
]);
|
||||||
|
|
||||||
if($defender->getPhase() < 0){
|
if($defender->getPhase() < 0){
|
||||||
$phaseNickname = '先';
|
$phaseNickname = '先';
|
||||||
@@ -394,8 +535,10 @@ function processWar_NG(
|
|||||||
|
|
||||||
$attacker->addPhase();
|
$attacker->addPhase();
|
||||||
$defender->addPhase();
|
$defender->addPhase();
|
||||||
|
$emitTrace('phase_end', $defender);
|
||||||
|
|
||||||
if (!$attacker->continueWar($noRice)) {
|
if (!$attacker->continueWar($noRice)) {
|
||||||
|
$emitTrace('attacker_stopped', $defender, ['noRice' => (bool)$noRice]);
|
||||||
$logWritten = true;
|
$logWritten = true;
|
||||||
|
|
||||||
$attacker->logBattleResult();
|
$attacker->logBattleResult();
|
||||||
@@ -420,6 +563,7 @@ function processWar_NG(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!$defender->continueWar($noRice)) {
|
if (!$defender->continueWar($noRice)) {
|
||||||
|
$emitTrace('defender_stopped', $defender, ['noRice' => (bool)$noRice]);
|
||||||
$logWritten = true;
|
$logWritten = true;
|
||||||
|
|
||||||
$attacker->logBattleResult();
|
$attacker->logBattleResult();
|
||||||
@@ -462,6 +606,7 @@ function processWar_NG(
|
|||||||
|
|
||||||
$defender->finishBattle();
|
$defender->finishBattle();
|
||||||
$defender = ($getNextDefender)($defender, true);
|
$defender = ($getNextDefender)($defender, true);
|
||||||
|
$emitTrace('opponent_switched', $defender);
|
||||||
|
|
||||||
if ($defender !== null && !($defender instanceof WarUnitGeneral)) {
|
if ($defender !== null && !($defender instanceof WarUnitGeneral)) {
|
||||||
throw new \RuntimeException('다음 수비자를 받아오는데 실패');
|
throw new \RuntimeException('다음 수비자를 받아오는데 실패');
|
||||||
@@ -497,10 +642,60 @@ function processWar_NG(
|
|||||||
}
|
}
|
||||||
|
|
||||||
($getNextDefender)($defender, false);
|
($getNextDefender)($defender, false);
|
||||||
|
$emitTrace('battle_end', $defender, ['conquered' => $conquerCity]);
|
||||||
|
|
||||||
return $conquerCity;
|
return $conquerCity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildWarTraceUnitSnapshot(WarUnit $unit): array
|
||||||
|
{
|
||||||
|
$snapshot = [
|
||||||
|
'kind' => $unit instanceof WarUnitGeneral ? 'general' : 'city',
|
||||||
|
'id' => $unit instanceof WarUnitGeneral
|
||||||
|
? $unit->getGeneral()->getID()
|
||||||
|
: $unit->getVar('city'),
|
||||||
|
'name' => $unit->getName(),
|
||||||
|
'isAttacker' => $unit->isAttacker(),
|
||||||
|
'crewTypeId' => $unit->getCrewType()->id,
|
||||||
|
'phase' => $unit->getPhase(),
|
||||||
|
'realPhase' => $unit->getRealPhase(),
|
||||||
|
'maxPhase' => $unit->getMaxPhase(),
|
||||||
|
'hp' => $unit->getHP(),
|
||||||
|
'rawWarPower' => $unit->getRawWarPower(),
|
||||||
|
'warPower' => $unit->getWarPower(),
|
||||||
|
'warPowerMultiplier' => $unit->getWarPowerMultiply(),
|
||||||
|
'killed' => $unit->getKilled(),
|
||||||
|
'dead' => $unit->getDead(),
|
||||||
|
'activatedSkills' => $unit->getActivatedSkillLog(),
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($unit instanceof WarUnitGeneral) {
|
||||||
|
$general = $unit->getGeneral();
|
||||||
|
$snapshot['general'] = [
|
||||||
|
'crew' => $general->getVar('crew'),
|
||||||
|
'rice' => $general->getVar('rice'),
|
||||||
|
'train' => $general->getVar('train'),
|
||||||
|
'atmos' => $general->getVar('atmos'),
|
||||||
|
'injury' => $general->getVar('injury'),
|
||||||
|
'experience' => $general->getVar('experience'),
|
||||||
|
'dedication' => $general->getVar('dedication'),
|
||||||
|
'dex1' => $general->getVar('dex1'),
|
||||||
|
'dex2' => $general->getVar('dex2'),
|
||||||
|
'dex3' => $general->getVar('dex3'),
|
||||||
|
'dex4' => $general->getVar('dex4'),
|
||||||
|
'dex5' => $general->getVar('dex5'),
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
$snapshot['cityState'] = [
|
||||||
|
'defence' => $unit->getVar('def'),
|
||||||
|
'wall' => $unit->getVar('wall'),
|
||||||
|
'population' => $unit->getVar('pop'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
function DeleteConflict($nation)
|
function DeleteConflict($nation)
|
||||||
{
|
{
|
||||||
$db = DB::db();
|
$db = DB::db();
|
||||||
|
|||||||
@@ -64,7 +64,9 @@ class che_견문 extends Command\GeneralCommand{
|
|||||||
|
|
||||||
$sightseeing = new SightseeingMessage();
|
$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;
|
$exp = 0;
|
||||||
|
|
||||||
@@ -122,4 +124,4 @@ class che_견문 extends Command\GeneralCommand{
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
namespace sammo\Command\General;
|
namespace sammo\Command\General;
|
||||||
|
|
||||||
use \sammo\{
|
use \sammo\{
|
||||||
DB, Util, JosaUtil,
|
DB, Util, JosaUtil, Json,
|
||||||
General,
|
General,
|
||||||
ActionLogger,
|
ActionLogger,
|
||||||
LastTurn,
|
LastTurn,
|
||||||
@@ -118,12 +118,35 @@ class che_기술연구 extends che_상업투자{
|
|||||||
$score /= 4;
|
$score /= 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
$genCount = Util::valueFit(
|
$genCount = Util::valueFit(
|
||||||
$db->queryFirstField('SELECT gennum FROM nation WHERE nation=%i', $general->getVar('nation')),
|
$db->queryFirstField('SELECT gennum FROM nation WHERE nation=%i', $general->getVar('nation')),
|
||||||
GameConst::$initialNationGenLimit
|
GameConst::$initialNationGenLimit
|
||||||
);
|
);
|
||||||
|
|
||||||
$nationUpdated = [
|
$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
|
'tech' => $this->nation['tech'] + $score/$genCount
|
||||||
];
|
];
|
||||||
$db->update('nation', $nationUpdated, 'nation=%i', $general->getVar('nation'));
|
$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 `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`
|
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
|
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,
|
$genLimit,
|
||||||
$notIn
|
$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 `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`
|
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
|
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
|
$genLimit
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ class che_상업투자 extends Command\GeneralCommand{
|
|||||||
static protected $actionName = '상업 투자';
|
static protected $actionName = '상업 투자';
|
||||||
static protected $debuffFront = 0.5;
|
static protected $debuffFront = 0.5;
|
||||||
|
|
||||||
protected $reqGold;
|
protected $reqGold;
|
||||||
|
protected $baseTrace = [];
|
||||||
|
|
||||||
protected function argTest():bool{
|
protected function argTest():bool{
|
||||||
$this->arg = null;
|
$this->arg = null;
|
||||||
@@ -118,10 +119,21 @@ class che_상업투자 extends Command\GeneralCommand{
|
|||||||
throw new \sammo\MustNotBeReachedException();
|
throw new \sammo\MustNotBeReachedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
$score *= $trust / 100;
|
$adjustedStat = $score;
|
||||||
$score *= getDomesticExpLevelBonus($general->getVar('explevel'));
|
$score *= $trust / 100;
|
||||||
$score *= $rng->nextRange(0.8, 1.2);
|
$expLevel = $general->getVar('explevel');
|
||||||
$score = $general->onCalcDomestic(static::$actionKey, 'score', $score);
|
$expBonus = getDomesticExpLevelBonus($expLevel);
|
||||||
|
$score *= $expBonus;
|
||||||
|
$randomMultiplier = $rng->nextRange(0.8, 1.2);
|
||||||
|
$score *= $randomMultiplier;
|
||||||
|
$score = $general->onCalcDomestic(static::$actionKey, 'score', $score);
|
||||||
|
$this->baseTrace = [
|
||||||
|
'adjustedStat' => $adjustedStat,
|
||||||
|
'expLevel' => $expLevel,
|
||||||
|
'expBonus' => $expBonus,
|
||||||
|
'randomMultiplier' => $randomMultiplier,
|
||||||
|
'domesticScore' => $score,
|
||||||
|
];
|
||||||
|
|
||||||
return $score;
|
return $score;
|
||||||
}
|
}
|
||||||
@@ -137,7 +149,8 @@ class che_상업투자 extends Command\GeneralCommand{
|
|||||||
|
|
||||||
$trust = Util::valueFit($this->city['trust'], 50);
|
$trust = Util::valueFit($this->city['trust'], 50);
|
||||||
|
|
||||||
$score = Util::valueFit($this->calcBaseScore($rng), 1);
|
$calculatedBaseScore = $this->calcBaseScore($rng);
|
||||||
|
$score = Util::valueFit($calculatedBaseScore, 1);
|
||||||
|
|
||||||
['success'=>$successRatio, 'fail'=>$failRatio] = CriticalRatioDomestic($general, static::$statKey);
|
['success'=>$successRatio, 'fail'=>$failRatio] = CriticalRatioDomestic($general, static::$statKey);
|
||||||
if($trust < 80){
|
if($trust < 80){
|
||||||
@@ -160,11 +173,31 @@ class che_상업투자 extends Command\GeneralCommand{
|
|||||||
|
|
||||||
$date = $general->getTurnTime($general::TURNTIME_HM);
|
$date = $general->getTurnTime($general::TURNTIME_HM);
|
||||||
|
|
||||||
$score *= CriticalScoreEx($rng, $pick);
|
$criticalMultiplier = CriticalScoreEx($rng, $pick);
|
||||||
$score = Util::round($score);
|
$score *= $criticalMultiplier;
|
||||||
|
$score = Util::round($score);
|
||||||
|
|
||||||
$exp = $score * 0.7;
|
$exp = $score * 0.7;
|
||||||
$ded = $score * 1.0;
|
$ded = $score * 1.0;
|
||||||
|
|
||||||
|
if (in_array((string)$general->getID(), array_filter(explode(',', (string)getenv('REF_AI_TRACE_GENERAL_IDS')), 'strlen'), true)) {
|
||||||
|
fwrite(STDOUT, 'DOMESTIC_CALC_TRACE ' . json_encode([
|
||||||
|
'engine' => 'ref',
|
||||||
|
'generalId' => $general->getID(),
|
||||||
|
'action' => static::class,
|
||||||
|
'trust' => $trust,
|
||||||
|
'calculatedBaseScore' => $calculatedBaseScore,
|
||||||
|
'baseTrace' => $this->baseTrace,
|
||||||
|
'successRatio' => $successRatio,
|
||||||
|
'failRatio' => $failRatio,
|
||||||
|
'normalRatio' => $normalRatio,
|
||||||
|
'pick' => $pick,
|
||||||
|
'criticalMultiplier' => $criticalMultiplier,
|
||||||
|
'rewardScore' => $score,
|
||||||
|
'exp' => $exp,
|
||||||
|
'dedication' => $ded,
|
||||||
|
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n");
|
||||||
|
}
|
||||||
|
|
||||||
if($pick == 'success'){
|
if($pick == 'success'){
|
||||||
updateMaxDomesticCritical($general, $score);
|
updateMaxDomesticCritical($general, $score);
|
||||||
@@ -225,4 +258,4 @@ class che_상업투자 extends Command\GeneralCommand{
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ class che_주민선정 extends Command\GeneralCommand{
|
|||||||
static protected $actionKey = '민심';
|
static protected $actionKey = '민심';
|
||||||
static protected $actionName = '주민 선정';
|
static protected $actionName = '주민 선정';
|
||||||
|
|
||||||
protected $reqRice;
|
protected $reqRice;
|
||||||
|
protected $baseTrace = [];
|
||||||
|
|
||||||
protected function argTest():bool{
|
protected function argTest():bool{
|
||||||
$this->arg = null;
|
$this->arg = null;
|
||||||
@@ -109,10 +110,21 @@ class che_주민선정 extends Command\GeneralCommand{
|
|||||||
throw new \sammo\MustNotBeReachedException();
|
throw new \sammo\MustNotBeReachedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
$score *= getDomesticExpLevelBonus($general->getVar('explevel'));
|
$adjustedStat = $score;
|
||||||
$score *= $rng->nextRange(0.8, 1.2);
|
$expLevel = $general->getVar('explevel');
|
||||||
$score = $general->onCalcDomestic(static::$actionKey, 'score', $score);
|
$expBonus = getDomesticExpLevelBonus($expLevel);
|
||||||
$score = Util::valueFit($score, 1);
|
$score *= $expBonus;
|
||||||
|
$randomMultiplier = $rng->nextRange(0.8, 1.2);
|
||||||
|
$score *= $randomMultiplier;
|
||||||
|
$score = $general->onCalcDomestic(static::$actionKey, 'score', $score);
|
||||||
|
$score = Util::valueFit($score, 1);
|
||||||
|
$this->baseTrace = [
|
||||||
|
'adjustedStat' => $adjustedStat,
|
||||||
|
'expLevel' => $expLevel,
|
||||||
|
'expBonus' => $expBonus,
|
||||||
|
'randomMultiplier' => $randomMultiplier,
|
||||||
|
'domesticScore' => $score,
|
||||||
|
];
|
||||||
|
|
||||||
return $score;
|
return $score;
|
||||||
}
|
}
|
||||||
@@ -126,7 +138,8 @@ class che_주민선정 extends Command\GeneralCommand{
|
|||||||
|
|
||||||
$general = $this->generalObj;
|
$general = $this->generalObj;
|
||||||
|
|
||||||
$score = Util::valueFit($this->calcBaseScore($rng), 1);
|
$calculatedBaseScore = $this->calcBaseScore($rng);
|
||||||
|
$score = Util::valueFit($calculatedBaseScore, 1);
|
||||||
|
|
||||||
['success'=>$successRatio, 'fail'=>$failRatio] = CriticalRatioDomestic($general, static::$statKey);
|
['success'=>$successRatio, 'fail'=>$failRatio] = CriticalRatioDomestic($general, static::$statKey);
|
||||||
$successRatio = $general->onCalcDomestic(static::$actionKey, 'success', $successRatio);
|
$successRatio = $general->onCalcDomestic(static::$actionKey, 'success', $successRatio);
|
||||||
@@ -146,10 +159,30 @@ class che_주민선정 extends Command\GeneralCommand{
|
|||||||
|
|
||||||
$date = $general->getTurnTime($general::TURNTIME_HM);
|
$date = $general->getTurnTime($general::TURNTIME_HM);
|
||||||
|
|
||||||
$score *= CriticalScoreEx($rng, $pick);
|
$criticalMultiplier = CriticalScoreEx($rng, $pick);
|
||||||
|
$score *= $criticalMultiplier;
|
||||||
|
|
||||||
$exp = $score * 0.7;
|
$exp = $score * 0.7;
|
||||||
$ded = $score * 1.0;
|
$ded = $score * 1.0;
|
||||||
|
|
||||||
|
if (in_array((string)$general->getID(), array_filter(explode(',', (string)getenv('REF_AI_TRACE_GENERAL_IDS')), 'strlen'), true)) {
|
||||||
|
fwrite(STDOUT, 'DOMESTIC_CALC_TRACE ' . json_encode([
|
||||||
|
'engine' => 'ref',
|
||||||
|
'generalId' => $general->getID(),
|
||||||
|
'action' => static::class,
|
||||||
|
'calculatedBaseScore' => $calculatedBaseScore,
|
||||||
|
'baseTrace' => $this->baseTrace,
|
||||||
|
'successRatio' => $successRatio,
|
||||||
|
'failRatio' => $failRatio,
|
||||||
|
'normalRatio' => $normalRatio,
|
||||||
|
'pick' => $pick,
|
||||||
|
'criticalMultiplier' => $criticalMultiplier,
|
||||||
|
'rewardScore' => $score,
|
||||||
|
'appliedScore' => $score / 10,
|
||||||
|
'exp' => $exp,
|
||||||
|
'dedication' => $ded,
|
||||||
|
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n");
|
||||||
|
}
|
||||||
|
|
||||||
if($pick == 'success'){
|
if($pick == 'success'){
|
||||||
updateMaxDomesticCritical($general, $score);
|
updateMaxDomesticCritical($general, $score);
|
||||||
@@ -197,4 +230,4 @@ class che_주민선정 extends Command\GeneralCommand{
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ class AssignGeneralSpeciality extends \sammo\Event\Action
|
|||||||
$month,
|
$month,
|
||||||
)));
|
)));
|
||||||
|
|
||||||
foreach ($db->query('SELECT no,name,nation,leadership,strength,intel,aux from general where specage<=age and special=%s', GameConst::$defaultSpecialDomestic) as $general) {
|
foreach ($db->query('SELECT no,name,nation,leadership,strength,intel,aux from general where specage<=age and special=%s ORDER BY no ASC', GameConst::$defaultSpecialDomestic) as $general) {
|
||||||
$generalID = $general['no'];
|
$generalID = $general['no'];
|
||||||
$special = SpecialityHelper::pickSpecialDomestic(
|
$special = SpecialityHelper::pickSpecialDomestic(
|
||||||
$rng,
|
$rng,
|
||||||
@@ -57,7 +57,7 @@ class AssignGeneralSpeciality extends \sammo\Event\Action
|
|||||||
$logger->pushGeneralHistoryLog("특기 【<b><C>{$specialText}</></b>】{$josaUl} 습득");
|
$logger->pushGeneralHistoryLog("특기 【<b><C>{$specialText}</></b>】{$josaUl} 습득");
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($db->query('SELECT no,name,nation,leadership,strength,intel,npc,dex1,dex2,dex3,dex4,dex5,aux from general where specage2<=age and special2=%s', GameConst::$defaultSpecialWar) as $general) {
|
foreach ($db->query('SELECT no,name,nation,leadership,strength,intel,npc,dex1,dex2,dex3,dex4,dex5,aux from general where specage2<=age and special2=%s ORDER BY no ASC', GameConst::$defaultSpecialWar) as $general) {
|
||||||
$generalID = $general['no'];
|
$generalID = $general['no'];
|
||||||
$generalAux = Json::decode($general['aux']);
|
$generalAux = Json::decode($general['aux']);
|
||||||
|
|
||||||
|
|||||||
@@ -94,6 +94,21 @@ class ProcessIncome extends \sammo\Event\Action
|
|||||||
foreach ($generalRawList as $rawGeneral) {
|
foreach ($generalRawList as $rawGeneral) {
|
||||||
$generalObj = new General($rawGeneral, null, null, null, null, $year, $month, false);
|
$generalObj = new General($rawGeneral, null, null, null, null, $year, $month, false);
|
||||||
$gold = Util::round(getBill($generalObj->getVar('dedication')) * $ratio);
|
$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);
|
$generalObj->increaseVar('gold', $gold);
|
||||||
|
|
||||||
$logger = $generalObj->getLogger();
|
$logger = $generalObj->getLogger();
|
||||||
|
|||||||
@@ -71,6 +71,21 @@ class ProcessSemiAnnual extends \sammo\Event\Action
|
|||||||
|
|
||||||
$resource = $this->resource;
|
$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% 감소
|
// 내정 1% 감소
|
||||||
$db->update('city', [
|
$db->update('city', [
|
||||||
'dead' => 0,
|
'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)
|
$resource => $db->sqleval('IF(%b > 10000, %b * 0.97, %b * 0.99)', $resource, $resource, $resource)
|
||||||
], '%b > 1000', $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%
|
// > 100000 유지비 5%, > 100000 유지비 3%, > 1000 유지비 1%
|
||||||
$db->update('nation', [
|
$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)
|
$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) {
|
if (!$isGood) {
|
||||||
//FIXME: factory 형태로 바꿔야함
|
//FIXME: factory 형태로 바꿔야함
|
||||||
$generalListByCity = Util::arrayGroupBy($db->query(
|
$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::formatListOfBackticks(General::mergeQueryColumn()[0]),
|
||||||
Util::squeezeFromArray($targetCityList, 'city')),
|
Util::squeezeFromArray($targetCityList, 'city')),
|
||||||
'city');
|
'city');
|
||||||
@@ -133,6 +133,19 @@ class RaiseDisaster extends \sammo\Event\Action
|
|||||||
'def' => $db->sqleval('def * %d', $affectRatio),
|
'def' => $db->sqleval('def * %d', $affectRatio),
|
||||||
'wall' => $db->sqleval('wall * %d', $affectRatio),
|
'wall' => $db->sqleval('wall * %d', $affectRatio),
|
||||||
], 'city = %i', $city['city']);
|
], '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(
|
$generalList = array_map(
|
||||||
function ($rawGeneral) use ($city, $year, $month) {
|
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),
|
'def' => $db->sqleval('least(def * %d, def_max)', $affectRatio),
|
||||||
'wall' => $db->sqleval('least(wall * %d, wall_max)', $affectRatio),
|
'wall' => $db->sqleval('least(wall * %d, wall_max)', $affectRatio),
|
||||||
], 'city = %i', $city['city']);
|
], '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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+462
-157
@@ -9,8 +9,30 @@ use sammo\Enums\PenaltyKey;
|
|||||||
use sammo\Enums\RankColumn;
|
use sammo\Enums\RankColumn;
|
||||||
use sammo\Scenario\NPC;
|
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 RandUtil $rng;
|
||||||
|
|
||||||
protected array $city;
|
protected array $city;
|
||||||
@@ -150,13 +172,25 @@ class GeneralAI
|
|||||||
$gameStor = KVStorage::getStorage($db, 'game_env');
|
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||||
$this->env = $gameStor->getAll(true);
|
$this->env = $gameStor->getAll(true);
|
||||||
|
|
||||||
$this->rng = new RandUtil(new LiteHashDRBG(Util::simpleSerialize(
|
$serializedSeed = Util::simpleSerialize(
|
||||||
UniqueConst::$hiddenSeed,
|
UniqueConst::$hiddenSeed,
|
||||||
'GeneralAI',
|
'GeneralAI',
|
||||||
$this->env['year'],
|
$this->env['year'],
|
||||||
$this->env['month'],
|
$this->env['month'],
|
||||||
$general->getID(),
|
$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->leadership = $general->getLeadership();
|
||||||
$this->strength = $general->getStrength();
|
$this->strength = $general->getStrength();
|
||||||
@@ -172,17 +206,21 @@ class GeneralAI
|
|||||||
return $this->general;
|
return $this->general;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function calcGenType(General $general)
|
protected function calcGenType(General $general)
|
||||||
{
|
{
|
||||||
$leadership = $general->getLeadership(false);
|
$leadership = $general->getLeadership(false);
|
||||||
$strength = Util::valueFit($general->getStrength(false), 1);
|
$strength = Util::valueFit($general->getStrength(false), 1);
|
||||||
$intel = Util::valueFit($general->getIntel(false), 1);
|
$intel = Util::valueFit($general->getIntel(false), 1);
|
||||||
|
$mixedDraw = null;
|
||||||
|
$mixedProbability = null;
|
||||||
|
|
||||||
//무장
|
//무장
|
||||||
if ($strength >= $intel) {
|
if ($strength >= $intel) {
|
||||||
$genType = self::t무장;
|
$genType = self::t무장;
|
||||||
if ($intel >= $strength * 0.8) { //무지장
|
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지장;
|
$genType |= self::t지장;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -190,17 +228,32 @@ class GeneralAI
|
|||||||
} else {
|
} else {
|
||||||
$genType = self::t지장;
|
$genType = self::t지장;
|
||||||
if ($strength >= $intel * 0.8) { //지무장
|
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무장;
|
$genType |= self::t무장;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//통솔
|
//통솔
|
||||||
if ($leadership >= $this->nationPolicy->minNPCWarLeadership) {
|
if ($leadership >= $this->nationPolicy->minNPCWarLeadership) {
|
||||||
$genType |= self::t통솔장;
|
$genType |= self::t통솔장;
|
||||||
}
|
}
|
||||||
return $genType;
|
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()
|
protected function calcDiplomacyState()
|
||||||
@@ -857,12 +910,20 @@ class GeneralAI
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$cityCandidates = [];
|
$cityCandidates = [];
|
||||||
foreach ($this->frontCities as $frontCity) {
|
foreach ($this->frontCities as $frontCity) {
|
||||||
$cityCandidates[$frontCity['city']] = $frontCity['important'];
|
$cityCandidates[$frontCity['city']] = $frontCity['important'];
|
||||||
}
|
}
|
||||||
|
|
||||||
$cmd = buildNationCommandClass('che_발령', $this->general, $this->env, $lastTurn, [
|
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(),
|
'destGeneralID' => $this->rng->choice($generalCandidates)->getID(),
|
||||||
'destCityID' => $this->rng->choiceUsingWeight($cityCandidates)
|
'destCityID' => $this->rng->choiceUsingWeight($cityCandidates)
|
||||||
]);
|
]);
|
||||||
@@ -1135,12 +1196,20 @@ class GeneralAI
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$cityCandidates = [];
|
$cityCandidates = [];
|
||||||
foreach ($this->frontCities as $frontCity) {
|
foreach ($this->frontCities as $frontCity) {
|
||||||
$cityCandidates[$frontCity['city']] = $frontCity['important'];
|
$cityCandidates[$frontCity['city']] = $frontCity['important'];
|
||||||
}
|
}
|
||||||
|
|
||||||
$cmd = buildNationCommandClass('che_발령', $this->general, $this->env, $lastTurn, [
|
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(),
|
'destGeneralID' => $this->rng->choice($generalCandidates)->getID(),
|
||||||
'destCityID' => $this->rng->choiceUsingWeight($cityCandidates)
|
'destCityID' => $this->rng->choiceUsingWeight($cityCandidates)
|
||||||
]);
|
]);
|
||||||
@@ -1564,18 +1633,33 @@ class GeneralAI
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
//국고와 '충분한 금액'의 기하평균
|
//국고와 '충분한 금액'의 기하평균
|
||||||
$payAmount = sqrt(($enoughMoney - $targetNPCGeneral->getVar($resName)) * $resVal);
|
$payAmount = sqrt(($enoughMoney - $targetNPCGeneral->getVar($resName)) * $resVal);
|
||||||
$payAmount = Util::valueFit($payAmount, $resVal - $reqNationRes, $enoughMoney - $targetNPCGeneral->getVar($resName));
|
$payAmount = Util::valueFit($payAmount, $resVal - $reqNationRes, $enoughMoney - $targetNPCGeneral->getVar($resName));
|
||||||
|
|
||||||
if ($resVal < $payAmount / 2) {
|
if ($resVal < $payAmount / 2) {
|
||||||
continue;
|
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(),
|
'destGeneralID' => $targetNPCGeneral->getID(),
|
||||||
'isGold' => $resName == 'gold',
|
'isGold' => $resName == 'gold',
|
||||||
'amount' => Util::valueFit($payAmount, 100, $this->maxResourceActionAmount)
|
'amount' => $payAmount
|
||||||
],
|
],
|
||||||
max(count($npcWarGenerals), count($npcCivilGenerals)) - $idx
|
max(count($npcWarGenerals), count($npcCivilGenerals)) - $idx
|
||||||
];
|
];
|
||||||
@@ -2211,7 +2295,28 @@ class GeneralAI
|
|||||||
return null;
|
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
|
protected function do긴급내정(): ?GeneralCommand
|
||||||
@@ -2356,18 +2461,54 @@ class GeneralAI
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$cmd = $this->rng->choiceUsingWeightPair($cmdList);
|
$cmd = $this->rng->choiceUsingWeightPair($cmdList);
|
||||||
return $cmd;
|
$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
|
protected function do금쌀구매(): ?GeneralCommand
|
||||||
{
|
{
|
||||||
$general = $this->general;
|
$general = $this->general;
|
||||||
|
$traceGeneralIDs = array_filter(explode(',', (string)getenv('REF_AI_TRACE_GENERAL_IDS')), 'strlen');
|
||||||
if ($this->city['trade'] === null && !$this->generalPolicy->can상인무시) {
|
$traceEnabled = in_array((string)$general->getID(), $traceGeneralIDs, true);
|
||||||
return null;
|
$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;
|
$kill = $general->getRankVar(RankColumn::killcrew) + 50000;
|
||||||
$death = $general->getRankVar(RankColumn::deathcrew) + 50000;
|
$death = $general->getRankVar(RankColumn::deathcrew) + 50000;
|
||||||
@@ -2376,12 +2517,24 @@ class GeneralAI
|
|||||||
$absGold = $general->getVar('gold');
|
$absGold = $general->getVar('gold');
|
||||||
$absRice = $general->getVar('rice');
|
$absRice = $general->getVar('rice');
|
||||||
|
|
||||||
$relGold = $absGold;
|
$relGold = $absGold;
|
||||||
$relRice = $absRice * $deathRate;
|
$relRice = $absRice * $deathRate;
|
||||||
|
|
||||||
if ($absGold + $absRice < $this->baseDevelCost * 2) {
|
$trace('resources', [
|
||||||
return null;
|
'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();
|
$crewType = $general->getCrewTypeObj();
|
||||||
if ($this->generalPolicy->can모병) {
|
if ($this->generalPolicy->can모병) {
|
||||||
@@ -2397,10 +2550,19 @@ class GeneralAI
|
|||||||
}
|
}
|
||||||
|
|
||||||
$goldCost = $costCmd->getCost()[0];
|
$goldCost = $costCmd->getCost()[0];
|
||||||
$riceCost = $crewType->riceWithTech(
|
$riceCost = $crewType->riceWithTech(
|
||||||
$this->nation['tech'],
|
$this->nation['tech'],
|
||||||
Util::toInt($this->fullLeadership * 100)
|
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) {
|
if (($relGold + $relRice) * 1.5 <= $goldCost + $riceCost) {
|
||||||
return null;
|
return null;
|
||||||
@@ -2434,10 +2596,16 @@ class GeneralAI
|
|||||||
'buyRice' => true,
|
'buyRice' => true,
|
||||||
'amount' => $amount
|
'amount' => $amount
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
if ($cmd->hasFullConditionMet()) {
|
$conditionMet = $cmd->hasFullConditionMet();
|
||||||
return $cmd;
|
$trace('buy', [
|
||||||
}
|
'amount' => $amount,
|
||||||
|
'minimumResourceActionAmount' => $this->nationPolicy->minimumResourceActionAmount,
|
||||||
|
'conditionMet' => $conditionMet,
|
||||||
|
]);
|
||||||
|
if ($conditionMet) {
|
||||||
|
return $cmd;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2477,14 +2645,28 @@ class GeneralAI
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function do징병(): ?GeneralCommand
|
protected function do징병(): ?GeneralCommand
|
||||||
{
|
{
|
||||||
if (in_array($this->dipState, [self::d평화, self::d선포])) {
|
$traceGeneralIds = array_filter(explode(',', (string)getenv('REF_AI_TRACE_GENERAL_IDS')), 'strlen');
|
||||||
return null;
|
$traceEnabled = in_array((string)$this->general->getID(), $traceGeneralIds, true);
|
||||||
}
|
$trace = function (string $stage, array $values = []) use ($traceEnabled): void {
|
||||||
|
if (!$traceEnabled) {
|
||||||
if (!($this->genType & self::t통솔장)) {
|
return;
|
||||||
return null;
|
}
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -2494,21 +2676,32 @@ class GeneralAI
|
|||||||
$nation = $this->nation;
|
$nation = $this->nation;
|
||||||
$env = $this->env;
|
$env = $this->env;
|
||||||
|
|
||||||
if ($general->getVar('crew') >= $this->nationPolicy->minWarCrew) {
|
if ($general->getVar('crew') >= $this->nationPolicy->minWarCrew) {
|
||||||
return null;
|
$trace('existing-crew', ['crew' => $general->getVar('crew'), 'minWarCrew' => $this->nationPolicy->minWarCrew]);
|
||||||
}
|
return null;
|
||||||
|
}
|
||||||
if (!$this->generalPolicy->can한계징병) {
|
|
||||||
|
$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;
|
$remainPop = $city['pop'] - $this->nationPolicy->minNPCRecruitCityPopulation - $this->fullLeadership * 100;
|
||||||
if ($remainPop <= 0) {
|
if ($remainPop <= 0) {
|
||||||
return null;
|
$trace('population-floor', ['remainPop' => $remainPop, 'fullLeadership' => $this->fullLeadership]);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$maxPop = $city['pop_max'] - $this->nationPolicy->minNPCRecruitCityPopulation;
|
$maxPop = $city['pop_max'] - $this->nationPolicy->minNPCRecruitCityPopulation;
|
||||||
if (($city['pop'] / $city['pop_max'] < $this->nationPolicy->safeRecruitCityPopulationRatio) &&
|
if (($city['pop'] / $city['pop_max'] < $this->nationPolicy->safeRecruitCityPopulationRatio) &&
|
||||||
($this->rng->nextBool($remainPop / $maxPop))
|
($this->rng->nextBool($remainPop / $maxPop))
|
||||||
) {
|
) {
|
||||||
return null;
|
$trace('population-random', ['remainPop' => $remainPop, 'maxPop' => $maxPop, 'fullLeadership' => $this->fullLeadership]);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2529,7 +2722,7 @@ class GeneralAI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$armType) {
|
if (!$armType) {
|
||||||
$dex = [
|
$dex = [
|
||||||
GameUnitConst::T_FOOTMAN => sqrt($general->getVar('dex1') + 500),
|
GameUnitConst::T_FOOTMAN => sqrt($general->getVar('dex1') + 500),
|
||||||
GameUnitConst::T_ARCHER => sqrt($general->getVar('dex2') + 500),
|
GameUnitConst::T_ARCHER => sqrt($general->getVar('dex2') + 500),
|
||||||
@@ -2548,8 +2741,26 @@ class GeneralAI
|
|||||||
$availableArmType[GameUnitConst::T_WIZARD] = $dex[GameUnitConst::T_WIZARD] * $this->fullIntel * 3;
|
$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 = [];
|
$cities = [];
|
||||||
@@ -2573,9 +2784,10 @@ class GeneralAI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($types) {
|
if ($types) {
|
||||||
$type = $this->rng->choiceUsingWeight($types);
|
$type = $this->rng->choiceUsingWeight($types);
|
||||||
} else {
|
$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));
|
throw new MustNotBeReachedException('에러:' . print_r([$general->getName(), $general->getAuxVar('armType'), $armType, $cities, $regions, $relYear, $tech], true));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2601,8 +2813,9 @@ class GeneralAI
|
|||||||
$rice = $general->getVar('rice');
|
$rice = $general->getVar('rice');
|
||||||
$rice -= $this->fullLeadership * 4;
|
$rice -= $this->fullLeadership * 4;
|
||||||
|
|
||||||
if ($gold <= 0 || $rice <= 0) {
|
if ($gold <= 0 || $rice <= 0) {
|
||||||
return null;
|
$trace('reserve-floor', ['remainingGold' => $gold, 'remainingRice' => $rice, 'fullLeadership' => $this->fullLeadership]);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$crew = $this->fullLeadership * 100;
|
$crew = $this->fullLeadership * 100;
|
||||||
@@ -2636,15 +2849,18 @@ class GeneralAI
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$this->generalPolicy->can한계징병 && $rice * 1.1 <= $riceCost) {
|
if (!$this->generalPolicy->can한계징병 && $rice * 1.1 <= $riceCost) {
|
||||||
//이 쌀도 없어?
|
//이 쌀도 없어?
|
||||||
return null;
|
$trace('rice-cost', ['remainingGold' => $gold, 'remainingRice' => $rice, 'goldCost' => $cost, 'riceCost' => $riceCost, 'crewAmount' => $crew, 'crewTypeId' => $type]);
|
||||||
}
|
return null;
|
||||||
|
}
|
||||||
if (!$cmd->hasFullConditionMet()) {
|
|
||||||
return null;
|
if (!$cmd->hasFullConditionMet()) {
|
||||||
}
|
$trace('constraint', ['remainingGold' => $gold, 'remainingRice' => $rice, 'goldCost' => $cost, 'riceCost' => $riceCost, 'crewAmount' => $crew, 'crewTypeId' => $type]);
|
||||||
return $cmd;
|
return null;
|
||||||
|
}
|
||||||
|
$trace('selected', ['remainingGold' => $gold, 'remainingRice' => $rice, 'goldCost' => $cost, 'riceCost' => $riceCost, 'crewAmount' => $crew, 'crewTypeId' => $type]);
|
||||||
|
return $cmd;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function do전투준비(): ?GeneralCommand
|
protected function do전투준비(): ?GeneralCommand
|
||||||
@@ -2700,22 +2916,40 @@ class GeneralAI
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protected function do출병(): ?GeneralCommand
|
protected function do출병(): ?GeneralCommand
|
||||||
{
|
{
|
||||||
if (!$this->attackable) {
|
$traceEnabled = in_array((string)$this->general->getID(), array_filter(explode(',', (string)getenv('REF_AI_TRACE_GENERAL_IDS')), 'strlen'), true);
|
||||||
return null;
|
$trace = static function (string $stage, array $values = []) use ($traceEnabled): void {
|
||||||
}
|
if ($traceEnabled) {
|
||||||
|
fwrite(STDOUT, 'AI_WAR_TRACE ' . Json::encode(['engine' => 'ref', 'stage' => $stage] + $values) . "\n");
|
||||||
if ($this->dipState !== self::d전쟁) {
|
}
|
||||||
return null;
|
};
|
||||||
|
$trace('start', [
|
||||||
|
'generalId' => $this->general->getID(), 'attackable' => $this->attackable,
|
||||||
|
'dipState' => $this->dipState, 'nationRice' => $this->nation['rice'],
|
||||||
|
'baseRice' => GameConst::$baserice, 'train' => $this->general->getVar('train'),
|
||||||
|
'atmos' => $this->general->getVar('atmos'), 'crew' => $this->general->getVar('crew'),
|
||||||
|
'fullLeadership' => $this->fullLeadership, 'minWarCrew' => $this->nationPolicy->minWarCrew,
|
||||||
|
'cityID' => $this->city['city'], 'front' => $this->city['front'],
|
||||||
|
'warTargetNation' => $this->warTargetNation,
|
||||||
|
]);
|
||||||
|
if (!$this->attackable) {
|
||||||
|
$trace('not-attackable');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->dipState !== self::d전쟁) {
|
||||||
|
$trace('not-at-war');
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$general = $this->getGeneralObj();
|
$general = $this->getGeneralObj();
|
||||||
$city = $this->city;
|
$city = $this->city;
|
||||||
$nation = $this->nation;
|
$nation = $this->nation;
|
||||||
|
|
||||||
if (($nation['rice'] < GameConst::$baserice) && $general->getNPCType() >= 2 && $this->rng->nextBool(0.7)) {
|
if (($nation['rice'] < GameConst::$baserice) && $general->getNPCType() >= 2 && $this->rng->nextBool(0.7)) {
|
||||||
return null;
|
$trace('low-nation-rice');
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$cityID = $city['city'];
|
$cityID = $city['city'];
|
||||||
@@ -2723,22 +2957,27 @@ class GeneralAI
|
|||||||
|
|
||||||
$db = DB::db();
|
$db = DB::db();
|
||||||
|
|
||||||
if ($general->getVar('train') < min(100, $this->nationPolicy->properWarTrainAtmos)) {
|
if ($general->getVar('train') < min(100, $this->nationPolicy->properWarTrainAtmos)) {
|
||||||
return null;
|
$trace('low-train');
|
||||||
}
|
return null;
|
||||||
if ($general->getVar('atmos') < min(100, $this->nationPolicy->properWarTrainAtmos)) {
|
}
|
||||||
return null;
|
if ($general->getVar('atmos') < min(100, $this->nationPolicy->properWarTrainAtmos)) {
|
||||||
}
|
$trace('low-atmos');
|
||||||
if ($general->getVar('crew') < min(($this->fullLeadership - 2) * 100, $this->nationPolicy->minWarCrew)) {
|
return null;
|
||||||
return null;
|
}
|
||||||
}
|
if ($general->getVar('crew') < min(($this->fullLeadership - 2) * 100, $this->nationPolicy->minWarCrew)) {
|
||||||
|
$trace('low-crew');
|
||||||
if ($city['front'] === 0) {
|
return null;
|
||||||
return null;
|
}
|
||||||
}
|
|
||||||
|
if ($city['front'] === 0) {
|
||||||
if ($city['front'] === 1) {
|
$trace('front-zero');
|
||||||
return null;
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($city['front'] === 1) {
|
||||||
|
$trace('front-one');
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$attackableNations = [];
|
$attackableNations = [];
|
||||||
@@ -2753,20 +2992,29 @@ class GeneralAI
|
|||||||
}
|
}
|
||||||
$nearCities = array_keys(CityConst::byID($cityID)->path);
|
$nearCities = array_keys(CityConst::byID($cityID)->path);
|
||||||
|
|
||||||
$attackableCities = $db->queryFirstColumn(
|
$attackableCities = $db->queryFirstColumn(
|
||||||
'SELECT city, nation FROM city WHERE nation IN %li AND city IN %li',
|
'SELECT city, nation FROM city WHERE nation IN %li AND city IN %li',
|
||||||
$attackableNations,
|
$attackableNations,
|
||||||
$nearCities
|
$nearCities
|
||||||
);
|
);
|
||||||
|
$trace('destinations', ['attackableNations' => $attackableNations, 'nearCities' => $nearCities, 'attackableCities' => $attackableCities]);
|
||||||
|
|
||||||
if (count($attackableCities) == 0) {
|
if (count($attackableCities) == 0) {
|
||||||
throw new \RuntimeException('출병 불가' . $cityID . var_export($attackableNations, true) . var_export($nearCities, true));
|
throw new \RuntimeException('출병 불가' . $cityID . var_export($attackableNations, true) . var_export($nearCities, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
$cmd = buildGeneralCommandClass('che_출병', $general, $this->env, ['destCityID' => $this->rng->choice($attackableCities)]);
|
$cmd = buildGeneralCommandClass('che_출병', $general, $this->env, ['destCityID' => $this->rng->choice($attackableCities)]);
|
||||||
if (!$cmd->hasFullConditionMet()) {
|
if (!$cmd->hasFullConditionMet()) {
|
||||||
return null;
|
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;
|
return $cmd;
|
||||||
}
|
}
|
||||||
@@ -2946,13 +3194,23 @@ class GeneralAI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$recruitableCityList) {
|
if (!$recruitableCityList) {
|
||||||
LogText("{$this->general->getName()}, {$this->general->getID()} 후방워프 불가: 배후 도시", [count($this->backupCities), count($this->supplyCities)]);
|
LogText("{$this->general->getName()}, {$this->general->getID()} 후방워프 불가: 배후 도시", [count($this->backupCities), count($this->supplyCities)]);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (in_array((string) $this->general->getID(), array_filter(explode(',', (string) getenv('REF_AI_TRACE_GENERAL_IDS')), 'strlen'), true)) {
|
||||||
$cmd = buildGeneralCommandClass('che_NPC능동', $this->general, $this->env, [
|
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' => '순간이동',
|
'optionText' => '순간이동',
|
||||||
'destCityID' => $this->rng->choiceUsingWeight($recruitableCityList),
|
'destCityID' => $this->rng->choiceUsingWeight($recruitableCityList),
|
||||||
]);
|
]);
|
||||||
@@ -3480,7 +3738,13 @@ class GeneralAI
|
|||||||
$supplyCities = [];
|
$supplyCities = [];
|
||||||
$backupCities = [];
|
$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();
|
$nationCity['generals'] = new \ArrayObject();
|
||||||
$cityID = $nationCity['city'];
|
$cityID = $nationCity['city'];
|
||||||
$dev =
|
$dev =
|
||||||
@@ -3531,7 +3795,11 @@ class GeneralAI
|
|||||||
$nationCities = &$this->nationCities;
|
$nationCities = &$this->nationCities;
|
||||||
|
|
||||||
$db = DB::db();
|
$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);
|
$nationGenerals = General::createObjListFromDB($generalIDList);
|
||||||
|
|
||||||
@@ -3610,9 +3878,10 @@ class GeneralAI
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public function chooseNationTurn(NationCommand $reservedCommand): NationCommand
|
public function chooseNationTurn(NationCommand $reservedCommand): NationCommand
|
||||||
{
|
{
|
||||||
$this->updateInstance();
|
$this->updateInstance();
|
||||||
|
$this->traceNationRng('after-update');
|
||||||
|
|
||||||
//TODO: NationTurn과 InstantNationTurn 구분 필요
|
//TODO: NationTurn과 InstantNationTurn 구분 필요
|
||||||
$lastTurn = $reservedCommand->getLastTurn();
|
$lastTurn = $reservedCommand->getLastTurn();
|
||||||
@@ -3655,7 +3924,7 @@ class GeneralAI
|
|||||||
$general->getLogger()->pushGeneralActionLog($text);
|
$general->getLogger()->pushGeneralActionLog($text);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($this->nationPolicy->priority as $actionName) {
|
foreach ($this->nationPolicy->priority as $actionName) {
|
||||||
|
|
||||||
if (!property_exists($this->nationPolicy, 'can' . $actionName)) {
|
if (!property_exists($this->nationPolicy, 'can' . $actionName)) {
|
||||||
trigger_error("can{$actionName}이 없음", E_USER_NOTICE);
|
trigger_error("can{$actionName}이 없음", E_USER_NOTICE);
|
||||||
@@ -3668,7 +3937,9 @@ class GeneralAI
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
/** @var ?NationCommand */
|
/** @var ?NationCommand */
|
||||||
$result = $this->{'do' . $actionName}($lastTurn);
|
$this->traceNationRng("before-{$actionName}");
|
||||||
|
$result = $this->{'do' . $actionName}($lastTurn);
|
||||||
|
$this->traceNationRng("after-{$actionName}");
|
||||||
if ($result !== null) {
|
if ($result !== null) {
|
||||||
$result->reason = 'do' . $actionName;
|
$result->reason = 'do' . $actionName;
|
||||||
return $result;
|
return $result;
|
||||||
@@ -3823,17 +4094,33 @@ class GeneralAI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($this->generalPolicy->priority as $actionName) {
|
foreach ($this->generalPolicy->priority as $actionName) {
|
||||||
if (!property_exists($this->generalPolicy, 'can' . $actionName)) {
|
if (!property_exists($this->generalPolicy, 'can' . $actionName)) {
|
||||||
trigger_error("can{$actionName}이 없음", E_USER_NOTICE);
|
trigger_error("can{$actionName}이 없음", E_USER_NOTICE);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!($this->generalPolicy->{'can' . $actionName})) {
|
if (!($this->generalPolicy->{'can' . $actionName})) {
|
||||||
continue;
|
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([
|
||||||
/** @var ?GeneralCommand */
|
'generalId' => $general->getID(),
|
||||||
$result = $this->{'do' . $actionName}();
|
'actionName' => $actionName,
|
||||||
if ($result !== null) {
|
'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;
|
$result->reason = 'do' . $actionName;
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
@@ -3944,9 +4231,18 @@ class GeneralAI
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$randGeneral->setVar('officer_level', $chiefLevel);
|
$randGeneral->setVar('officer_level', $chiefLevel);
|
||||||
$randGeneral->setVar('officer_city', 0);
|
$randGeneral->setVar('officer_city', 0);
|
||||||
$randGeneral->applyDB($db);
|
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);
|
$this->nation['chief_set'] |= doOfficerSet(0, $chiefLevel);
|
||||||
$setChiefLevel |= doOfficerSet(0, $chiefLevel);
|
$setChiefLevel |= doOfficerSet(0, $chiefLevel);
|
||||||
$this->chiefGenerals[$chiefLevel] = $randGeneral;
|
$this->chiefGenerals[$chiefLevel] = $randGeneral;
|
||||||
@@ -4142,8 +4438,17 @@ class GeneralAI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$nextChiefs[$chiefLevel] = $newChief;
|
$nextChiefs[$chiefLevel] = $newChief;
|
||||||
$newChief->setVar('officer_level', $chiefLevel);
|
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);
|
$newChief->setVar('officer_city', 0);
|
||||||
$nation['chief_set'] |= doOfficerSet(0, $chiefLevel);
|
$nation['chief_set'] |= doOfficerSet(0, $chiefLevel);
|
||||||
$updatedChiefSet |= doOfficerSet(0, $chiefLevel);
|
$updatedChiefSet |= doOfficerSet(0, $chiefLevel);
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class che_도시치료 extends BaseGeneralTrigger
|
|||||||
if ($general->getNationID() == 0) {
|
if ($general->getNationID() == 0) {
|
||||||
/** @var array{int,string,string}[] $patients */
|
/** @var array{int,string,string}[] $patients */
|
||||||
$patients = $db->queryAllLists(
|
$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(),
|
$general->getCityID(),
|
||||||
0,
|
0,
|
||||||
$general->getID()
|
$general->getID()
|
||||||
@@ -40,7 +40,7 @@ class che_도시치료 extends BaseGeneralTrigger
|
|||||||
else {
|
else {
|
||||||
/** @var array{int,string,string}[] $patients */
|
/** @var array{int,string,string}[] $patients */
|
||||||
$patients = $db->queryAllLists(
|
$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->getCityID(),
|
||||||
$general->getID()
|
$general->getID()
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -93,7 +93,14 @@ class ResetHelper{
|
|||||||
$gameStor->resetCache();
|
$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(
|
$result = Util::generateFileUsingSimpleTemplate(
|
||||||
$servRoot.'/d_setting/UniqueConst.orig.php',
|
$servRoot.'/d_setting/UniqueConst.orig.php',
|
||||||
@@ -320,7 +327,11 @@ class ResetHelper{
|
|||||||
'server_cnt'=>$serverCnt,
|
'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', [
|
$db->insert('general', [
|
||||||
'owner'=>$admin['no'],
|
'owner'=>$admin['no'],
|
||||||
'name'=>$admin['name'],
|
'name'=>$admin['name'],
|
||||||
|
|||||||
@@ -17,6 +17,15 @@ class SightseeingMessage{
|
|||||||
const HeavyWounded = 0x2000;
|
const HeavyWounded = 0x2000;
|
||||||
|
|
||||||
static protected $messages = null;
|
static protected $messages = null;
|
||||||
|
static protected ?\sammo\RandUtil $comparisonRng = null;
|
||||||
|
|
||||||
|
public static function setComparisonRng(?\sammo\RandUtil $rng): void
|
||||||
|
{
|
||||||
|
if ($rng !== null && getenv('TURN_DIFFERENTIAL_ENABLED') !== '1') {
|
||||||
|
throw new \RuntimeException('comparison RNG is test-only');
|
||||||
|
}
|
||||||
|
static::$comparisonRng = $rng;
|
||||||
|
}
|
||||||
|
|
||||||
protected static function getMessageList():array{
|
protected static function getMessageList():array{
|
||||||
/* [[속성, [
|
/* [[속성, [
|
||||||
@@ -103,10 +112,16 @@ class SightseeingMessage{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function pickAction():array{
|
public function pickAction(?\sammo\RandUtil $rng = null):array{
|
||||||
[$type, $texts] = Util::choiceRandomUsingWeightPair(static::$messages??[]);
|
$actionRng = $rng ?? static::$comparisonRng;
|
||||||
$text = Util::choiceRandom($texts);
|
if ($actionRng !== null) {
|
||||||
|
[$type, $texts] = $actionRng->choiceUsingWeightPair(static::$messages ?? []);
|
||||||
|
$text = $actionRng->choice($texts);
|
||||||
|
} else {
|
||||||
|
[$type, $texts] = Util::choiceRandomUsingWeightPair(static::$messages??[]);
|
||||||
|
$text = Util::choiceRandom($texts);
|
||||||
|
}
|
||||||
|
|
||||||
return [$type, $text];
|
return [$type, $text];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,25 @@ 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*/
|
/** @var General*/
|
||||||
protected $generalObj;
|
protected $generalObj;
|
||||||
|
|
||||||
@@ -244,16 +263,51 @@ class TurnExecutionHelper
|
|||||||
|
|
||||||
$currentTurn = null;
|
$currentTurn = null;
|
||||||
|
|
||||||
$gameStor = KVStorage::getStorage($db, 'game_env');
|
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||||
$autorun_user = $gameStor->autorun_user;
|
$autorun_user = $gameStor->autorun_user;
|
||||||
|
|
||||||
foreach ($generalsTodo as $rawGeneral) {
|
$traceCityChanges = PHP_SAPI === 'cli'
|
||||||
$currActionTime = GameClock::readWallTime();
|
&& getenv('REF_DETERMINISTIC_INSTALL_ENABLED') === '1'
|
||||||
if ($currActionTime > $limitActionTime) {
|
&& getenv('REF_AI_TRACE_CITY_CHANGES') === '1';
|
||||||
return [true, $currentTurn];
|
$observedCityStates = [];
|
||||||
}
|
if ($traceCityChanges) {
|
||||||
|
foreach ($db->query('SELECT city, nation, front, def, wall, pop, state, term, trust FROM city ORDER BY city') as $row) {
|
||||||
$general = General::createObjFromDB($rawGeneral['no']);
|
$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) {
|
||||||
|
if (getenv('REF_DETERMINISTIC_INSTALL_ENABLED') !== '1') {
|
||||||
|
$currActionTime = GameClock::readWallTime();
|
||||||
|
if ($currActionTime > $limitActionTime) {
|
||||||
|
return [true, $currentTurn];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$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');
|
$nationStor = KVStorage::getStorage($db, $general->getNationID(), 'nation_env');
|
||||||
$turnObj = new static($general);
|
$turnObj = new static($general);
|
||||||
|
|
||||||
@@ -306,9 +360,26 @@ class TurnExecutionHelper
|
|||||||
if (!($nationCommandObj instanceof Command\Nation\휴식)) {
|
if (!($nationCommandObj instanceof Command\Nation\휴식)) {
|
||||||
$hasReservedTurn = true;
|
$hasReservedTurn = true;
|
||||||
}
|
}
|
||||||
if ($ai && ($general->getAuxVar('use_auto_nation_turn') ?? 1)) {
|
if ($ai && ($general->getAuxVar('use_auto_nation_turn') ?? 1)) {
|
||||||
$nationCommandObj = $ai->chooseNationTurn($nationCommandObj);
|
$nationCommandObj = $ai->chooseNationTurn($nationCommandObj);
|
||||||
$cityName = CityConst::byID($general->getCityID())->name;
|
$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}, ");
|
LogText("NationTurn", "General, {$general->getName()}, {$general->getID()}, {$cityName}, {$general->getStaticNation()['name']}, {$nationCommandObj->getBrief()}, {$nationCommandObj->reason}, ");
|
||||||
}
|
}
|
||||||
$rng = new RandUtil(new LiteHashDRBG(Util::simpleSerialize(
|
$rng = new RandUtil(new LiteHashDRBG(Util::simpleSerialize(
|
||||||
@@ -322,9 +393,21 @@ class TurnExecutionHelper
|
|||||||
$resultNationTurn = $turnObj->processNationCommand(
|
$resultNationTurn = $turnObj->processNationCommand(
|
||||||
$rng,
|
$rng,
|
||||||
$nationCommandObj
|
$nationCommandObj
|
||||||
);
|
);
|
||||||
$nationStor->setValue($lastNationTurnKey, $resultNationTurn->toRaw());
|
$nationStor->setValue($lastNationTurnKey, $resultNationTurn->toRaw());
|
||||||
$general->setRawCity(null);
|
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);
|
$generalCommandObj = $general->getReservedTurn(0, $env);
|
||||||
@@ -334,14 +417,55 @@ class TurnExecutionHelper
|
|||||||
|
|
||||||
if ($ai) {
|
if ($ai) {
|
||||||
$newGeneralCommandObj = $ai->chooseGeneralTurn($generalCommandObj); // npc AI 처리
|
$newGeneralCommandObj = $ai->chooseGeneralTurn($generalCommandObj); // npc AI 처리
|
||||||
if ($generalCommandObj !== $newGeneralCommandObj) {
|
if ($generalCommandObj !== $newGeneralCommandObj) {
|
||||||
$autorunMode = true;
|
$autorunMode = true;
|
||||||
$generalCommandObj = $newGeneralCommandObj;
|
$generalCommandObj = $newGeneralCommandObj;
|
||||||
}
|
}
|
||||||
$cityName = CityConst::byID($general->getCityID())->name;
|
$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}, ");
|
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,
|
UniqueConst::$hiddenSeed,
|
||||||
'generalCommand',
|
'generalCommand',
|
||||||
$year,
|
$year,
|
||||||
@@ -349,8 +473,82 @@ class TurnExecutionHelper
|
|||||||
$general->getID(),
|
$general->getID(),
|
||||||
$generalCommandObj->getRawClassName()
|
$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'));
|
pullNationCommand($general->getVar('nation'), $general->getVar('officer_level'));
|
||||||
pullGeneralCommand($general->getID());
|
pullGeneralCommand($general->getID());
|
||||||
|
|
||||||
@@ -364,8 +562,31 @@ class TurnExecutionHelper
|
|||||||
$general->setAuxVar('autorun_limit', $autorun_limit);
|
$general->setAuxVar('autorun_limit', $autorun_limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
$turnObj->updateTurnTime();
|
$turnObj->updateTurnTime();
|
||||||
$turnObj->applyDB();
|
$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];
|
return [false, $currentTurn];
|
||||||
@@ -466,27 +687,54 @@ class TurnExecutionHelper
|
|||||||
return $gameStor->turntime;
|
return $gameStor->turntime;
|
||||||
}
|
}
|
||||||
|
|
||||||
$monthlyRng = new RandUtil(new LiteHashDRBG(Util::simpleSerialize(
|
$monthlyRng = new RandUtil(new LiteHashDRBG(Util::simpleSerialize(
|
||||||
UniqueConst::$hiddenSeed,
|
UniqueConst::$hiddenSeed,
|
||||||
'monthly',
|
'monthly',
|
||||||
$gameStor->year,
|
$gameStor->year,
|
||||||
$gameStor->month
|
$gameStor->month
|
||||||
)));
|
)));
|
||||||
|
|
||||||
// 1달마다 처리하는 것들, 벌점 감소 및 건국,전턴,합병 -1, 군량 소모
|
$traceMonthlyGeneral = static function (string $phase) use ($db): void {
|
||||||
static::runEventHandler($db, $gameStor, EventTarget::PreMonth);
|
$traceGeneralIds = getenv('REF_AI_TRACE_GENERAL_IDS');
|
||||||
if (!preUpdateMonthly()) {
|
if (
|
||||||
unlock();
|
PHP_SAPI !== 'cli'
|
||||||
throw new \RuntimeException('preUpdateMonthly() 처리 에러');
|
|| getenv('REF_DETERMINISTIC_INSTALL_ENABLED') !== '1'
|
||||||
}
|
|| !is_string($traceGeneralIds)
|
||||||
turnDate($nextTurn);
|
) {
|
||||||
|
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() 처리 에러');
|
||||||
|
}
|
||||||
|
$traceMonthlyGeneral('after-pre-update');
|
||||||
|
turnDate($nextTurn);
|
||||||
|
$traceMonthlyGeneral('after-turn-date');
|
||||||
|
|
||||||
// 분기계산. 장수들 턴보다 먼저 있다면 먼저처리
|
// 분기계산. 장수들 턴보다 먼저 있다면 먼저처리
|
||||||
if ($gameStor->month == 1) {
|
if ($gameStor->month == 1) {
|
||||||
checkStatistic();
|
checkStatistic();
|
||||||
}
|
}
|
||||||
static::runEventHandler($db, $gameStor, EventTarget::Month);
|
static::runEventHandler($db, $gameStor, EventTarget::Month);
|
||||||
postUpdateMonthly($monthlyRng);
|
$traceMonthlyGeneral('after-month-event');
|
||||||
|
postUpdateMonthly($monthlyRng);
|
||||||
|
$traceMonthlyGeneral('after-post-update');
|
||||||
|
|
||||||
// 다음달로 넘김
|
// 다음달로 넘김
|
||||||
$prevTurn = $nextTurn;
|
$prevTurn = $nextTurn;
|
||||||
|
|||||||
@@ -371,6 +371,34 @@ class WarUnitGeneral extends WarUnit
|
|||||||
|
|
||||||
function applyDB(\MeekroDB $db): bool
|
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);
|
$affected = $this->getGeneral()->applyDB($db);
|
||||||
$this->getLogger()->flush();
|
$this->getLogger()->flush();
|
||||||
return $affected;
|
return $affected;
|
||||||
|
|||||||
@@ -124,9 +124,17 @@ function fillTickColumn(
|
|||||||
$nullSql = $nullable ? ' NULL DEFAULT NULL' : ' NOT NULL';
|
$nullSql = $nullable ? ' NULL DEFAULT NULL' : ' NOT NULL';
|
||||||
$db->query("ALTER TABLE %b ADD COLUMN %b BIGINT{$nullSql}", $table, $tickColumn);
|
$db->query("ALTER TABLE %b ADD COLUMN %b BIGINT{$nullSql}", $table, $tickColumn);
|
||||||
$db->query(
|
$db->query(
|
||||||
"UPDATE %b SET %b = (TIMESTAMPDIFF(MICROSECOND, %s, %b) * %i) DIV 1000000",
|
"UPDATE %b SET %b = CASE "
|
||||||
|
. "WHEN YEAR(%b) >= 9000 THEN %i "
|
||||||
|
. "ELSE (TIMESTAMPDIFF(MICROSECOND, %s, %b) DIV 1000000) * %i "
|
||||||
|
. "+ (MOD(TIMESTAMPDIFF(MICROSECOND, %s, %b), 1000000) * %i) DIV 1000000 END",
|
||||||
$table,
|
$table,
|
||||||
$tickColumn,
|
$tickColumn,
|
||||||
|
$dateColumn,
|
||||||
|
GameClock::MAX_SAFE_TICK,
|
||||||
|
$baseTime,
|
||||||
|
$dateColumn,
|
||||||
|
$ticksPerSecond,
|
||||||
$baseTime,
|
$baseTime,
|
||||||
$dateColumn,
|
$dateColumn,
|
||||||
$ticksPerSecond,
|
$ticksPerSecond,
|
||||||
@@ -206,10 +214,6 @@ try {
|
|||||||
fillTickColumn($db, 'general_access_log', 'last_refresh', 'last_refresh_game_tick', $baseTimeString, $ticksPerSecond, true);
|
fillTickColumn($db, 'general_access_log', 'last_refresh', 'last_refresh_game_tick', $baseTimeString, $ticksPerSecond, true);
|
||||||
fillTickColumn($db, 'message', 'time', 'time_game_tick', $baseTimeString, $ticksPerSecond, false);
|
fillTickColumn($db, 'message', 'time', 'time_game_tick', $baseTimeString, $ticksPerSecond, false);
|
||||||
fillTickColumn($db, 'message', 'valid_until', 'valid_until_game_tick', $baseTimeString, $ticksPerSecond, false);
|
fillTickColumn($db, 'message', 'valid_until', 'valid_until_game_tick', $baseTimeString, $ticksPerSecond, false);
|
||||||
$db->query(
|
|
||||||
'UPDATE message SET valid_until_game_tick = %i WHERE YEAR(valid_until) >= 9000',
|
|
||||||
GameClock::MAX_SAFE_TICK,
|
|
||||||
);
|
|
||||||
fillTickColumn($db, 'select_npc_token', 'valid_until', 'valid_until_game_tick', $baseTimeString, $ticksPerSecond, false);
|
fillTickColumn($db, 'select_npc_token', 'valid_until', 'valid_until_game_tick', $baseTimeString, $ticksPerSecond, false);
|
||||||
fillTickColumn($db, 'select_npc_token', 'pick_more_from', 'pick_more_from_game_tick', $baseTimeString, $ticksPerSecond, false);
|
fillTickColumn($db, 'select_npc_token', 'pick_more_from', 'pick_more_from_game_tick', $baseTimeString, $ticksPerSecond, false);
|
||||||
fillTickColumn($db, 'select_pool', 'reserved_until', 'reserved_until_game_tick', $baseTimeString, $ticksPerSecond, true);
|
fillTickColumn($db, 'select_pool', 'reserved_until', 'reserved_until_game_tick', $baseTimeString, $ticksPerSecond, true);
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace sammo;
|
||||||
|
|
||||||
|
final class NpcPossessionSelector
|
||||||
|
{
|
||||||
|
public static function buildSeed(string|int $hiddenSeed, int $owner, string|int $now): string
|
||||||
|
{
|
||||||
|
return Util::simpleSerialize($hiddenSeed, 'SelectNPCToken', $owner, $now);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<string, mixed> $general */
|
||||||
|
public static function weight(array $general): float
|
||||||
|
{
|
||||||
|
return pow(
|
||||||
|
(int)$general['leadership'] + (int)$general['strength'] + (int)$general['intel'],
|
||||||
|
1.5,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int|string, array<string, mixed>> $oldPick
|
||||||
|
* @param list<int> $keepIds
|
||||||
|
* @return array{picked: array<int|string, array<string, mixed>>, cancelled: bool}
|
||||||
|
*/
|
||||||
|
public static function applyKeep(array $oldPick, array $keepIds): array
|
||||||
|
{
|
||||||
|
$picked = [];
|
||||||
|
foreach ($keepIds as $keepId) {
|
||||||
|
if (array_key_exists($keepId, $oldPick) && (int)$oldPick[$keepId]['keepCnt'] > 0) {
|
||||||
|
$picked[$keepId] = $oldPick[$keepId];
|
||||||
|
$picked[$keepId]['keepCnt'] = (int)$picked[$keepId]['keepCnt'] - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
'picked' => $picked,
|
||||||
|
'cancelled' => count($picked) === count($oldPick),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int|string, array<string, mixed>> $candidates
|
||||||
|
* @param array<int|string, float> $weights
|
||||||
|
* @param iterable<string> $reservedPayloads
|
||||||
|
*/
|
||||||
|
public static function removeReserved(array &$candidates, array &$weights, iterable $reservedPayloads): void
|
||||||
|
{
|
||||||
|
foreach ($reservedPayloads as $reservedPayload) {
|
||||||
|
$reserved = Json::decode($reservedPayload);
|
||||||
|
foreach (array_keys($reserved) as $reservedNpc) {
|
||||||
|
if (array_key_exists($reservedNpc, $weights)) {
|
||||||
|
unset($candidates[$reservedNpc], $weights[$reservedNpc]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int|string, array<string, mixed>> $candidates
|
||||||
|
* @param array<int|string, float> $weights
|
||||||
|
* @param array<int|string, array<string, mixed>> $picked
|
||||||
|
* @param null|callable(int|string): void $onDraw
|
||||||
|
* @return array<int|string, array<string, mixed>>
|
||||||
|
*/
|
||||||
|
public static function select(
|
||||||
|
array $candidates,
|
||||||
|
array $weights,
|
||||||
|
array $picked,
|
||||||
|
RandUtil $rng,
|
||||||
|
?callable $onDraw = null,
|
||||||
|
): array {
|
||||||
|
$pickLimit = min(count($candidates), 5);
|
||||||
|
while (count($picked) < $pickLimit) {
|
||||||
|
$generalId = $rng->choiceUsingWeight($weights);
|
||||||
|
if ($onDraw !== null) {
|
||||||
|
$onDraw($generalId);
|
||||||
|
}
|
||||||
|
if (!array_key_exists($generalId, $picked)) {
|
||||||
|
$picked[$generalId] = $candidates[$generalId];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $picked;
|
||||||
|
}
|
||||||
|
}
|
||||||
+34
-5
@@ -4,13 +4,36 @@ namespace sammo;
|
|||||||
|
|
||||||
class RandUtil
|
class RandUtil
|
||||||
{
|
{
|
||||||
|
private ?int $traceGeneralId = null;
|
||||||
|
private int $traceSequence = 0;
|
||||||
|
|
||||||
public function __construct(public readonly RNG $rng)
|
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
|
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
|
public function nextRange(int|float $min, int|float $max): float
|
||||||
@@ -25,16 +48,22 @@ class RandUtil
|
|||||||
if ($range > $this->rng->getMaxInt()) {
|
if ($range > $this->rng->getMaxInt()) {
|
||||||
throw new \InvalidArgumentException("Invalid random int range");
|
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{
|
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
|
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
|
public function nextBool(int|float $prob = 0.5): bool
|
||||||
@@ -97,7 +126,7 @@ class RandUtil
|
|||||||
if(!$keys){
|
if(!$keys){
|
||||||
throw new \InvalidArgumentException();
|
throw new \InvalidArgumentException();
|
||||||
}
|
}
|
||||||
$keyIdx = $this->rng->nextInt(count($keys) - 1);
|
$keyIdx = $this->nextInt(count($keys) - 1);
|
||||||
return $items[$keys[$keyIdx]];
|
return $items[$keys[$keyIdx]];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+22
-1
@@ -7,6 +7,19 @@ use phpDocumentor\Reflection\Types\Boolean;
|
|||||||
class TimeUtil
|
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 */
|
/** @deprecated */
|
||||||
public static function DateToday()
|
public static function DateToday()
|
||||||
{
|
{
|
||||||
@@ -87,7 +100,7 @@ class TimeUtil
|
|||||||
|
|
||||||
public static function now(bool $withFraction = false): string
|
public static function now(bool $withFraction = false): string
|
||||||
{
|
{
|
||||||
$obj = new \DateTime();
|
$obj = static::comparisonNow() ?? new \DateTimeImmutable();
|
||||||
return static::format($obj, $withFraction);
|
return static::format($obj, $withFraction);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,11 +195,19 @@ class TimeUtil
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static function nowDateTime(): \DateTime{
|
public static function nowDateTime(): \DateTime{
|
||||||
|
$comparisonNow = static::comparisonNow();
|
||||||
|
if ($comparisonNow !== null) {
|
||||||
|
return \DateTime::createFromImmutable($comparisonNow);
|
||||||
|
}
|
||||||
$now = time();
|
$now = time();
|
||||||
return static::secondsToDateTime($now, false, true);
|
return static::secondsToDateTime($now, false, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function nowDateTimeImmutable(): \DateTimeImmutable{
|
public static function nowDateTimeImmutable(): \DateTimeImmutable{
|
||||||
|
$comparisonNow = static::comparisonNow();
|
||||||
|
if ($comparisonNow !== null) {
|
||||||
|
return $comparisonNow;
|
||||||
|
}
|
||||||
$now = time();
|
$now = time();
|
||||||
return static::secondsToDateTime($now, true, true);
|
return static::secondsToDateTime($now, true, true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use sammo\Json;
|
||||||
|
use sammo\LiteHashDRBG;
|
||||||
|
use sammo\NpcPossessionSelector;
|
||||||
|
use sammo\RandUtil;
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../src/sammo/NpcPossessionSelector.php';
|
||||||
|
|
||||||
|
final class NpcPossessionSelectorTest extends TestCase
|
||||||
|
{
|
||||||
|
/** @return array<int, array{id: int, leadership: int, strength: int, intel: int, keepCnt: int}> */
|
||||||
|
private function candidates(): array
|
||||||
|
{
|
||||||
|
$rows = [
|
||||||
|
['id' => 1, 'leadership' => 40, 'strength' => 50, 'intel' => 60, 'keepCnt' => 3],
|
||||||
|
['id' => 2, 'leadership' => 73, 'strength' => 44, 'intel' => 81, 'keepCnt' => 3],
|
||||||
|
['id' => 3, 'leadership' => 91, 'strength' => 32, 'intel' => 47, 'keepCnt' => 3],
|
||||||
|
['id' => 4, 'leadership' => 55, 'strength' => 88, 'intel' => 38, 'keepCnt' => 3],
|
||||||
|
['id' => 5, 'leadership' => 66, 'strength' => 67, 'intel' => 68, 'keepCnt' => 3],
|
||||||
|
['id' => 6, 'leadership' => 99, 'strength' => 20, 'intel' => 21, 'keepCnt' => 3],
|
||||||
|
['id' => 7, 'leadership' => 35, 'strength' => 93, 'intel' => 72, 'keepCnt' => 3],
|
||||||
|
['id' => 8, 'leadership' => 84, 'strength' => 75, 'intel' => 69, 'keepCnt' => 3],
|
||||||
|
];
|
||||||
|
$result = [];
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$result[$row['id']] = $row;
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSharedKernelPreservesThePreviousEndpointLoop(): void
|
||||||
|
{
|
||||||
|
$candidates = $this->candidates();
|
||||||
|
$weights = [];
|
||||||
|
foreach ($candidates as $id => $candidate) {
|
||||||
|
$weights[$id] = pow(
|
||||||
|
$candidate['leadership'] + $candidate['strength'] + $candidate['intel'],
|
||||||
|
1.5,
|
||||||
|
);
|
||||||
|
self::assertSame($weights[$id], NpcPossessionSelector::weight($candidate));
|
||||||
|
}
|
||||||
|
$seed = NpcPossessionSelector::buildSeed(
|
||||||
|
'npc-possession-differential-seed',
|
||||||
|
7701,
|
||||||
|
'2026-08-01 00:00:01',
|
||||||
|
);
|
||||||
|
|
||||||
|
$legacyRng = new RandUtil(new LiteHashDRBG($seed));
|
||||||
|
$legacyPicked = [];
|
||||||
|
$legacyDraws = [];
|
||||||
|
$pickLimit = min(count($candidates), 5);
|
||||||
|
while (count($legacyPicked) < $pickLimit) {
|
||||||
|
$id = $legacyRng->choiceUsingWeight($weights);
|
||||||
|
$legacyDraws[] = (int)$id;
|
||||||
|
if (!array_key_exists($id, $legacyPicked)) {
|
||||||
|
$legacyPicked[$id] = $candidates[$id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$sharedDraws = [];
|
||||||
|
$sharedPicked = NpcPossessionSelector::select(
|
||||||
|
$candidates,
|
||||||
|
$weights,
|
||||||
|
[],
|
||||||
|
new RandUtil(new LiteHashDRBG($seed)),
|
||||||
|
static function (int|string $id) use (&$sharedDraws): void {
|
||||||
|
$sharedDraws[] = (int)$id;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertSame([1, 8, 1, 4, 5, 5, 8, 8, 5, 3], $legacyDraws);
|
||||||
|
self::assertSame($legacyDraws, $sharedDraws);
|
||||||
|
self::assertSame($legacyPicked, $sharedPicked);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testKeepAndReservedHelpersPreserveEndpointSemantics(): void
|
||||||
|
{
|
||||||
|
$candidates = $this->candidates();
|
||||||
|
$weights = array_map(
|
||||||
|
static fn(array $candidate): float => NpcPossessionSelector::weight($candidate),
|
||||||
|
$candidates,
|
||||||
|
);
|
||||||
|
NpcPossessionSelector::removeReserved(
|
||||||
|
$candidates,
|
||||||
|
$weights,
|
||||||
|
[
|
||||||
|
Json::encode((object)[2 => ['keepCnt' => 3], 7 => ['keepCnt' => 3]]),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
self::assertSame([1, 3, 4, 5, 6, 8], array_keys($candidates));
|
||||||
|
self::assertSame([1, 3, 4, 5, 6, 8], array_keys($weights));
|
||||||
|
|
||||||
|
$oldPick = array_intersect_key($this->candidates(), array_flip([1, 8, 4, 5, 3]));
|
||||||
|
$partial = NpcPossessionSelector::applyKeep($oldPick, [1, 4]);
|
||||||
|
self::assertFalse($partial['cancelled']);
|
||||||
|
self::assertSame(2, $partial['picked'][1]['keepCnt']);
|
||||||
|
self::assertSame(2, $partial['picked'][4]['keepCnt']);
|
||||||
|
|
||||||
|
$all = NpcPossessionSelector::applyKeep($oldPick, [1, 8, 4, 5, 3]);
|
||||||
|
self::assertTrue($all['cancelled']);
|
||||||
|
self::assertSame([1, 8, 4, 5, 3], array_keys($all['picked']));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user