H02 StampTreatyTurns -- the diplomacy ledger's "this treaty was last in force on turn N" stamp, over every ordered pair of players that holds one, creating the entry on demand. It is a host step, not a phase of either turn driver: its sole caller is the command-application step, which runs after the frame counter has advanced and before both drivers. On a turn with no combat and no diplomatic command it is the only writer of these fields, which is why a whole per-player dipstats vector is its output. Read from the instruction stream; three things an earlier reading had wrong are corrected in findings/subsystems/treaty-turn-stamp.md: the stamped value is the TURN and not the modification counter, the relation codes are 3=allied / 2=NAP / 1=cease-fire and not the reverse, and the bit is the player's index field rather than its position in the player vector -- the opposite convention from the shared-vision mask two files away. Measured, closed and regressed reported separately and never netted: turn1 -> turn2 (reference) 209 -> 132 closed 77 (was 51), regressed 0 turn2 -> turn3 108 -> 73 closed 35 (was 21), regressed 0 human-turn2 -> turn3 closed 76 (was 64), regressed 0 zuul-turn15 -> turn16 closed 30 (was 18), regressed 0 zuul-turn16 -> turn17 closed 29 (was 17), regressed 0 The last three are pairs from a different game at turns 2, 15 and 16 that the model was never fitted to, and it closes exactly the twelve ordered treaty pairs each of them holds. The rule also reproduces the ledger of ten of the eleven corpus saves entry for entry, including each entry's order and every stamped value; the eleventh is the turn-1 save whose ledger no turn has yet written, and it is the reference pair's input. app_test_treaty pins the five things the corpus cannot separate: the relation codes (no save exercises cease-fire), the bit's source, the missing alliance-id guard, the -1 initialiser on a fresh entry, and the append-at-the-end order that makes re-running the step idempotent instead of duplicating rows. The betrayal half of the same function needs the turn's diplomatic commands; with no command stream it is provably a no-op and is not modelled.
81 lines
3.5 KiB
C++
81 lines
3.5 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. Their ids run
|
|
// H00, H02, H01 because the table is in EXECUTION order and H02 was read later than the
|
|
// step it runs before; the id is the stable name, the index is the ordinal.
|
|
CHECK(nh == 3);
|
|
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;
|
|
}
|