test: capture legacy battle parity fixtures
This commit is contained in:
@@ -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,
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -71,6 +71,48 @@ final class ComparisonTracingRNG implements RNG
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
function comparisonExtractRankVar(array $raw): Map
|
||||||
{
|
{
|
||||||
$rankVars = new Map();
|
$rankVars = new Map();
|
||||||
@@ -97,7 +139,7 @@ function comparisonBuildGeneral(
|
|||||||
}
|
}
|
||||||
$raw['aux'] = Json::encode($aux);
|
$raw['aux'] = Json::encode($aux);
|
||||||
$raw['owner'] = 0;
|
$raw['owner'] = 0;
|
||||||
return new General(
|
return new ComparisonGeneral(
|
||||||
$raw,
|
$raw,
|
||||||
comparisonExtractRankVar($raw),
|
comparisonExtractRankVar($raw),
|
||||||
null,
|
null,
|
||||||
@@ -135,7 +177,7 @@ function comparisonRunBattle(array $fixture): array
|
|||||||
GameConst::$scenarioEffect = $scenarioEffect;
|
GameConst::$scenarioEffect = $scenarioEffect;
|
||||||
|
|
||||||
$tracingRng = new ComparisonTracingRNG(new LiteHashDRBG($seed));
|
$tracingRng = new ComparisonTracingRNG(new LiteHashDRBG($seed));
|
||||||
$warRng = new RandUtil($tracingRng);
|
$warRng = new ComparisonTracingRandUtil($tracingRng);
|
||||||
$attacker = new WarUnitGeneral(
|
$attacker = new WarUnitGeneral(
|
||||||
$warRng,
|
$warRng,
|
||||||
comparisonBuildGeneral($rawAttacker, $rawAttackerCity, $rawAttackerNation, $year, $month),
|
comparisonBuildGeneral($rawAttacker, $rawAttackerCity, $rawAttackerNation, $year, $month),
|
||||||
@@ -156,11 +198,17 @@ function comparisonRunBattle(array $fixture): array
|
|||||||
if (count($defenderList) && extractBattleOrder($city, $attacker) > 0) {
|
if (count($defenderList) && extractBattleOrder($city, $attacker) > 0) {
|
||||||
$defenderList[] = $city;
|
$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(
|
usort(
|
||||||
$defenderList,
|
$defenderList,
|
||||||
fn(WarUnit $lhs, WarUnit $rhs): int =>
|
fn(WarUnit $lhs, WarUnit $rhs): int =>
|
||||||
-(extractBattleOrder($lhs, $attacker) <=> extractBattleOrder($rhs, $attacker)),
|
-(extractBattleOrder($lhs, $attacker) <=> extractBattleOrder($rhs, $attacker)),
|
||||||
);
|
);
|
||||||
|
$defenderOrderAfterSort = array_map($summarizeDefenderOrder, $defenderList);
|
||||||
|
|
||||||
$iterDefender = new \ArrayIterator($defenderList);
|
$iterDefender = new \ArrayIterator($defenderList);
|
||||||
$iterDefender->rewind();
|
$iterDefender->rewind();
|
||||||
@@ -213,8 +261,13 @@ function comparisonRunBattle(array $fixture): array
|
|||||||
'attacker' => buildWarTraceUnitSnapshot($attacker),
|
'attacker' => buildWarTraceUnitSnapshot($attacker),
|
||||||
'city' => buildWarTraceUnitSnapshot($city),
|
'city' => buildWarTraceUnitSnapshot($city),
|
||||||
'finishedDefenders' => $finishedDefenders,
|
'finishedDefenders' => $finishedDefenders,
|
||||||
|
'defenderOrder' => [
|
||||||
|
'before' => $defenderOrderBeforeSort,
|
||||||
|
'after' => $defenderOrderAfterSort,
|
||||||
|
],
|
||||||
'events' => $events,
|
'events' => $events,
|
||||||
'rng' => $tracingRng->calls,
|
'rng' => $tracingRng->calls,
|
||||||
|
'boolRng' => $warRng->boolCalls,
|
||||||
'logs' => [
|
'logs' => [
|
||||||
'attacker' => $attackerLogs,
|
'attacker' => $attackerLogs,
|
||||||
'defenders' => $defenderLogs,
|
'defenders' => $defenderLogs,
|
||||||
@@ -224,8 +277,27 @@ function comparisonRunBattle(array $fixture): array
|
|||||||
}
|
}
|
||||||
|
|
||||||
$fixturePath = $argv[1] ?? null;
|
$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))) {
|
if ($fixturePath === null || ($fixturePath !== '-' && !is_file($fixturePath))) {
|
||||||
fwrite(STDERR, "usage: php compare/battle_trace.php <fixture.json|->\n");
|
fwrite(STDERR, "usage: php compare/battle_trace.php <fixture.json|-|--jsonl>\n");
|
||||||
exit(2);
|
exit(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,25 @@ function processWar(string $warSeed, General $attackerGeneral, array $rawAttacke
|
|||||||
$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', $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 = [];
|
$defenderCandidateTrace = [];
|
||||||
|
|||||||
Reference in New Issue
Block a user