315 lines
9.5 KiB
PHP
315 lines
9.5 KiB
PHP
<?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);
|
|
}
|