명령 상태의 자료형과 로그, 메시지, 순위 그래프를 보존하고 실제 명령 호출을 관찰하는 비교 전용 trace를 추가합니다. 계측은 CLI 및 명시적 차등 테스트 환경에서만 실행되며 제품 동작은 변경하지 않습니다.
190 lines
6.8 KiB
PHP
190 lines
6.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace sammo;
|
|
|
|
use sammo\API\Message\DecideMessageResponse;
|
|
use sammo\Enums\MessageType;
|
|
|
|
if (PHP_SAPI !== 'cli') {
|
|
http_response_code(404);
|
|
exit;
|
|
}
|
|
if (getenv('TURN_DIFFERENTIAL_ENABLED') !== '1') {
|
|
fwrite(STDERR, "TURN_DIFFERENTIAL_ENABLED=1 is required\n");
|
|
exit(1);
|
|
}
|
|
|
|
define('SAMMO_TURN_COMPARISON_LIBRARY_ONLY', true);
|
|
require_once __DIR__ . '/turn_command_trace.php';
|
|
|
|
/** @return array<string, mixed> */
|
|
function comparisonInstantDiplomacyMessageRow(int $messageId): array
|
|
{
|
|
$row = DB::db()->queryFirstRow(
|
|
'SELECT id, mailbox, type, src, dest, time, valid_until, message FROM message WHERE id = %i',
|
|
$messageId,
|
|
);
|
|
if ($row === null) {
|
|
throw new \RuntimeException("message {$messageId} was not persisted");
|
|
}
|
|
return [
|
|
'id' => (int)$row['id'],
|
|
'mailbox' => (int)$row['mailbox'],
|
|
'type' => (string)$row['type'],
|
|
'sourceId' => (int)$row['src'],
|
|
'destinationId' => (int)$row['dest'],
|
|
'timeTick' => (int)$row['time'],
|
|
'validUntilTick' => (int)$row['valid_until'],
|
|
'payload' => comparisonJsonValue($row['message']),
|
|
];
|
|
}
|
|
|
|
/** @return array<string, mixed> */
|
|
function comparisonRunInstantDiplomacyResponse(array $request): array
|
|
{
|
|
$action = $request['action'] ?? null;
|
|
if (!in_array($action, [
|
|
DiplomaticMessage::TYPE_NO_AGGRESSION,
|
|
DiplomaticMessage::TYPE_CANCEL_NA,
|
|
DiplomaticMessage::TYPE_STOP_WAR,
|
|
], true)) {
|
|
throw new \InvalidArgumentException('action must be noAggression, cancelNA, or stopWar');
|
|
}
|
|
$actorGeneralId = $request['actorGeneralId'] ?? null;
|
|
$proposerGeneralId = $request['proposerGeneralId'] ?? null;
|
|
if (!is_int($actorGeneralId) || $actorGeneralId < 1) {
|
|
throw new \InvalidArgumentException('actorGeneralId must be a positive integer');
|
|
}
|
|
if (!is_int($proposerGeneralId) || $proposerGeneralId < 1) {
|
|
throw new \InvalidArgumentException('proposerGeneralId must be a positive integer');
|
|
}
|
|
if (($request['response'] ?? true) !== true) {
|
|
throw new \InvalidArgumentException('this comparison runner currently covers accepted responses only');
|
|
}
|
|
|
|
comparisonApplyTurnFixtureSetup($request['setup'] ?? null);
|
|
$db = DB::db();
|
|
$actor = $db->queryFirstRow(
|
|
'SELECT no, name, nation FROM general WHERE no = %i',
|
|
$actorGeneralId,
|
|
);
|
|
$proposer = $db->queryFirstRow(
|
|
'SELECT no, name, nation FROM general WHERE no = %i',
|
|
$proposerGeneralId,
|
|
);
|
|
if ($actor === null || $proposer === null) {
|
|
throw new \RuntimeException('fixture generals are missing');
|
|
}
|
|
$actorNation = $db->queryFirstRow(
|
|
'SELECT nation, name, color FROM nation WHERE nation = %i',
|
|
(int)$actor['nation'],
|
|
);
|
|
$proposerNation = $db->queryFirstRow(
|
|
'SELECT nation, name, color FROM nation WHERE nation = %i',
|
|
(int)$proposer['nation'],
|
|
);
|
|
if ($actorNation === null || $proposerNation === null) {
|
|
throw new \RuntimeException('fixture nations are missing');
|
|
}
|
|
|
|
$messageBaseline = (int)($db->queryFirstField('SELECT COALESCE(MAX(id), 0) FROM message') ?? 0);
|
|
$logBaseline = (int)($db->queryFirstField('SELECT COALESCE(MAX(id), 0) FROM general_record') ?? 0);
|
|
$src = new MessageTarget(
|
|
$proposerGeneralId,
|
|
(string)$proposer['name'],
|
|
(int)$proposerNation['nation'],
|
|
(string)$proposerNation['name'],
|
|
(string)$proposerNation['color'],
|
|
);
|
|
$dest = new MessageTarget(
|
|
$actorGeneralId,
|
|
(string)$actor['name'],
|
|
(int)$actorNation['nation'],
|
|
(string)$actorNation['name'],
|
|
(string)$actorNation['color'],
|
|
);
|
|
$option = ['action' => $action];
|
|
if ($action === DiplomaticMessage::TYPE_NO_AGGRESSION) {
|
|
$year = $request['year'] ?? null;
|
|
$month = $request['month'] ?? null;
|
|
if (!is_int($year) || !is_int($month)) {
|
|
throw new \InvalidArgumentException('noAggression requires integer year and month');
|
|
}
|
|
$option['year'] = $year;
|
|
$option['month'] = $month;
|
|
}
|
|
$proposal = new DiplomaticMessage(
|
|
MessageType::diplomacy,
|
|
$src,
|
|
$dest,
|
|
'외교 제안',
|
|
Message::gameNow(),
|
|
new \DateTime('9999-12-31'),
|
|
$option,
|
|
);
|
|
$proposalMessageId = $proposal->send();
|
|
$proposalBefore = comparisonInstantDiplomacyMessageRow($proposalMessageId);
|
|
|
|
$observe = $request['observe'] ?? [];
|
|
if (!is_array($observe)) {
|
|
throw new \InvalidArgumentException('observe must be an object');
|
|
}
|
|
$observe['logAfterId'] = $logBaseline;
|
|
$observe['messageAfterId'] = $messageBaseline;
|
|
$snapshotRequest = ['observe' => $observe];
|
|
$before = comparisonTurnStateSnapshot($snapshotRequest);
|
|
|
|
$session = Session::getInstance();
|
|
$_SESSION[UniqueConst::$serverID . Session::GAME_KEY_GENERAL_ID] = $actorGeneralId;
|
|
$_SESSION[UniqueConst::$serverID . Session::GAME_KEY_GENERAL_NAME] = (string)$actor['name'];
|
|
$api = new DecideMessageResponse(dirname(__DIR__), [
|
|
'msgID' => $proposalMessageId,
|
|
'response' => true,
|
|
]);
|
|
$validationError = $api->validateArgs();
|
|
if ($validationError !== null) {
|
|
throw new \RuntimeException("DecideMessageResponse validation failed: {$validationError}");
|
|
}
|
|
$apiResult = $api->launch($session, null, null);
|
|
if (!is_array($apiResult)) {
|
|
throw new \RuntimeException('DecideMessageResponse did not return an array result');
|
|
}
|
|
|
|
return [
|
|
'schemaVersion' => 1,
|
|
'engine' => 'ref',
|
|
'execution' => [
|
|
'kind' => 'instantDiplomacyMessageResponse',
|
|
'entryPoint' => DecideMessageResponse::class,
|
|
'actorGeneralId' => $actorGeneralId,
|
|
'proposerGeneralId' => $proposerGeneralId,
|
|
'action' => $action,
|
|
'response' => true,
|
|
'outcome' => $apiResult,
|
|
'proposalMessageId' => $proposalMessageId,
|
|
'proposalBefore' => $proposalBefore,
|
|
'proposalAfter' => comparisonInstantDiplomacyMessageRow($proposalMessageId),
|
|
],
|
|
'before' => $before,
|
|
'after' => comparisonTurnStateSnapshot($snapshotRequest),
|
|
'rng' => [],
|
|
];
|
|
}
|
|
|
|
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(
|
|
comparisonRunInstantDiplomacyResponse($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);
|
|
}
|