sots-engine/tests/app/test_catalog.cpp
alex 39c01422f7 src/app: the standalone -- load a save, run a turn, write a save
`sots_turn` loads a save through the engine's own reader, walks the published
phase order of all three turn drivers, runs what we hold, prints what we do
not, and writes the result back through the engine's own writer.

The phase catalog carries all 32 + 12 + 37 phases whether or not they are
implemented, so an unimplemented phase is a named no-op that appears in the run
log rather than a silent absence. 14 of the 44 turn-driver phases are modelled,
7 commit anything, 2 of the 37 tail phases are modelled.

Modelled but NOT committed is a first-class state. A phase whose formula we hold
and whose inputs we do not is evaluated, reported, and left unwritten unless
--commit-blocked is passed. That distinction was earned: committing phase 31's
player-status restore regressed two leaves that had agreed with the oracle
before the turn, because the phase writes 1 and the file carries 4.

Measured against the game's own post-turn saves, leaves localised by
state_checksum.py with coverage proved by re-serialisation:

  turn1-state -> turn2-state   209 -> 204 diverging, closed 5, regressed 0
  turn2-state -> turn3-state   108 -> 103 diverging, closed 5, regressed 0

Two tests: app_catalog (the tables stay complete and nothing claims to be
verified against a live game) and app_turn (11 saves driven; an untouched load
re-serialises byte-identically, a turn leaves the file re-readable, and no
blocked or stub phase writes anything). Skips cleanly without SOTS_SAVES_DIR.

ctest 38/38, clean-room OK. src/shim untouched. docs/S-standalone.md has the
full gap list.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARBgSooAfokKUy6wKUKEyZ
2026-09-08 10:35:45 -04:00

79 lines
3.3 KiB
C++

// The phase catalog is a published claim about the turn: it must stay complete, ordered and
// self-consistent, because the completion metric is computed from it.
#include <cstdio>
#include <cstring>
#include <set>
#include <string>
#include "app/phase_catalog.h"
static int failures = 0;
#define CHECK(c) \
do { \
if (!(c)) { \
std::printf("FAIL %s:%d %s\n", __FILE__, __LINE__, #c); \
++failures; \
} \
} while (0)
using namespace sots::app;
static void CheckTable(const PhaseDesc* p, size_t n, Driver d, int firstIndex,
std::set<std::string>& ids, std::set<std::string>& names) {
for (size_t i = 0; i < n; ++i) {
CHECK(p[i].driver == d);
// Indices are contiguous and in execution order -- a gap means a phase was dropped.
CHECK(p[i].index == firstIndex + static_cast<int>(i));
CHECK(p[i].id && std::strlen(p[i].id) == 3);
CHECK(p[i].name && p[i].name[0] != '\0');
CHECK(ids.insert(p[i].id).second);
CHECK(names.insert(p[i].name).second);
// Anything not a stub must carry a note saying what it does or what is missing.
if (p[i].status != PhaseStatus::Stub) CHECK(p[i].note && p[i].note[0] != '\0');
}
}
int main() {
std::set<std::string> ids, names;
size_t nh = 0, ns = 0, np = 0, nt = 0;
const PhaseDesc* h = HostPhases(nh);
const PhaseDesc* s = StrategicPhases(ns);
const PhaseDesc* p = PlayerPhases(np);
const PhaseDesc* t = TailPhases(nt);
// The three published phase counts. These are load-bearing: the milestone is stated as
// "N of 44", and 44 is 32 + 12.
CHECK(ns == 32);
CHECK(np == 12);
CHECK(nt == 37);
CHECK(static_cast<int>(ns + np) == kSpinePhaseCount);
// The host steps are deliberately NOT part of the milestone's denominator.
CHECK(nh == 2);
CheckTable(h, nh, Driver::Host, 0, ids, names);
CheckTable(s, ns, Driver::Strategic, 0, ids, names);
CheckTable(p, np, Driver::Player, 1, ids, names);
CheckTable(t, nt, Driver::Tail, 0, ids, names);
const PhaseTally spine = TallySpine();
CHECK(spine.total == kSpinePhaseCount);
CHECK(spine.verified + spine.implemented + spine.partial + spine.blocked + spine.stub ==
spine.total);
CHECK(spine.modelled() <= spine.total);
CHECK(spine.committed() <= spine.modelled());
const PhaseTally tail = TallyTail();
CHECK(tail.total == 37);
CHECK(tail.verified + tail.implemented + tail.partial + tail.blocked + tail.stub ==
tail.total);
// A phase that does nothing must not claim to be verified: `Verified` in this table means
// "compared against the live game", and nothing here has been.
CHECK(spine.verified == 0);
CHECK(tail.verified == 0);
std::printf("catalog: spine %d/%d modelled (%d committed), tail %d/%d modelled\n",
spine.modelled(), spine.total, spine.committed(), tail.modelled(), tail.total);
std::printf(failures ? "FAILED (%d)\n" : "ok (%d failures)\n", failures);
return failures ? 1 : 0;
}