The trade-raid block is 16 of a measured turn's 18-22 generator words and it is the first thing the standalone can model as a COUNT rather than as a formula: two chance rolls per entry of the player vector, neither site inside a back edge, both probabilities strictly inside (0,1) so neither early-out fires. src/app/trade_raid implements it with the word cost reported rather than assumed, so a tuning table that pushes an odds value to 0 or 1 removes the draw and the ledger says so. Against lane Z's two calibrated oracle pairs the standalone now consumes 16 words and lands 4 and 2 short, which is exactly the per-call-site ledger's split for those two turns. It does NOT match the oracle's state, and the report says which sites are missing instead of netting them off the total. The tail's last phase -- the per-player turn record -- is modelled for the six fields that are recoverable from the wire, and self-checked every run against the record the input save already carries for its own turn: 480 fields over 80 player-records across the corpus, 0 mismatches. It is not committed. Under --commit-blocked it closes 24 container-shaped divergences on the reference pair and opens 17 leaf-shaped ones, all of them in the five fields the model does not hold, so the block is a measurement now rather than an argument. phases 14/44 of the two turn drivers (8 committed), 3/37 of the tail. divergence unchanged: 209->204 and 108->103, 5 closed / 0 regressed on both.
124 lines
5.4 KiB
C++
124 lines
5.4 KiB
C++
#include "app/report.h"
|
|
|
|
#include <fstream>
|
|
|
|
namespace sots::app {
|
|
namespace {
|
|
|
|
std::string JsonEscape(const std::string& s) {
|
|
std::string o;
|
|
for (char c : s) {
|
|
switch (c) {
|
|
case '"': o += "\\\""; break;
|
|
case '\\': o += "\\\\"; break;
|
|
case '\n': o += "\\n"; break;
|
|
case '\r': o += "\\r"; break;
|
|
case '\t': o += "\\t"; break;
|
|
default:
|
|
if (static_cast<unsigned char>(c) < 0x20) {
|
|
char b[8];
|
|
std::snprintf(b, sizeof b, "\\u%04x", c);
|
|
o += b;
|
|
} else {
|
|
o += c;
|
|
}
|
|
}
|
|
}
|
|
return o;
|
|
}
|
|
|
|
void TallyJson(std::ofstream& f, const char* key, const PhaseTally& t) {
|
|
f << " \"" << key << "\": {\"total\": " << t.total << ", \"verified\": " << t.verified
|
|
<< ", \"implemented\": " << t.implemented << ", \"partial\": " << t.partial
|
|
<< ", \"blocked\": " << t.blocked << ", \"stub\": " << t.stub
|
|
<< ", \"modelled\": " << t.modelled() << ", \"committed\": " << t.committed() << "}";
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void PrintPhaseLog(std::FILE* out, const TurnResult& r, bool verbose) {
|
|
Driver current = static_cast<Driver>(-1);
|
|
for (const auto& rec : r.records) {
|
|
if (rec.desc->driver != current) {
|
|
current = rec.desc->driver;
|
|
std::fprintf(out, "\n%s\n", DriverName(current));
|
|
}
|
|
const char* indent = current == Driver::Player ? " " : " ";
|
|
std::fprintf(out, "%s%c %-4s %-38s %-12s", indent, StatusGlyph(rec.desc->status),
|
|
rec.desc->id, rec.desc->name, StatusName(rec.desc->status));
|
|
if (rec.invocations || rec.leafWrites || rec.wouldWrite || rec.rngWords) {
|
|
std::fprintf(out, " ran=%-5d writes=%-5d", rec.invocations, rec.leafWrites);
|
|
if (rec.wouldWrite) std::fprintf(out, " would=%-4d", rec.wouldWrite);
|
|
if (rec.rngWords) std::fprintf(out, " rng=%d", rec.rngWords);
|
|
}
|
|
std::fprintf(out, "\n");
|
|
for (const auto& n : rec.notes) std::fprintf(out, "%s - %s\n", indent, n.c_str());
|
|
if (verbose && rec.desc->note[0])
|
|
std::fprintf(out, "%s # %s\n", indent, rec.desc->note);
|
|
}
|
|
}
|
|
|
|
void PrintSummary(std::FILE* out, const TurnResult& r) {
|
|
const PhaseTally s = TallySpine();
|
|
const PhaseTally t = TallyTail();
|
|
std::fprintf(out, "\nphases\n");
|
|
std::fprintf(out,
|
|
" turn drivers (the milestone's denominator): %d of %d modelled, %d committed\n"
|
|
" verified %d implemented %d partial %d blocked %d stub %d\n",
|
|
s.modelled(), s.total, s.committed(), s.verified, s.implemented, s.partial,
|
|
s.blocked, s.stub);
|
|
std::fprintf(out,
|
|
" post-combat tail (written to the autosave, tracked separately): %d of %d "
|
|
"modelled\n"
|
|
" verified %d implemented %d partial %d blocked %d stub %d\n",
|
|
t.modelled(), t.total, t.verified, t.implemented, t.partial, t.blocked, t.stub);
|
|
std::fprintf(out, "\nthis run\n");
|
|
std::fprintf(out, " leaves written %d\n", r.leafWrites);
|
|
std::fprintf(out, " leaves NOT written by a blocked phase %d\n", r.wouldWrite);
|
|
std::fprintf(out, " generator words consumed %d (state %s, %s)\n", r.rngWords,
|
|
r.rngLoaded ? "loaded" : "UNREADABLE",
|
|
r.rngCommitted ? "WRITTEN BACK" : "left untouched");
|
|
if (!r.rngUnaccounted.empty()) {
|
|
std::fprintf(out, " generator words NOT accounted (never netted off the above):\n");
|
|
for (const auto& u : r.rngUnaccounted) std::fprintf(out, " - %s\n", u.c_str());
|
|
}
|
|
for (const auto& w : r.warnings) std::fprintf(out, " ! %s\n", w.c_str());
|
|
}
|
|
|
|
bool WriteMetricJson(const std::string& path, const TurnResult& r, const std::string& inputName,
|
|
const std::string& outputName) {
|
|
std::ofstream f(path);
|
|
if (!f) return false;
|
|
const PhaseTally s = TallySpine();
|
|
const PhaseTally t = TallyTail();
|
|
f << "{\n";
|
|
f << " \"schema\": \"sots-standalone-metric/1\",\n";
|
|
f << " \"input\": \"" << JsonEscape(inputName) << "\",\n";
|
|
f << " \"output\": \"" << JsonEscape(outputName) << "\",\n";
|
|
TallyJson(f, "spine", s);
|
|
f << ",\n";
|
|
TallyJson(f, "tail", t);
|
|
f << ",\n";
|
|
f << " \"run\": {\"leafWrites\": " << r.leafWrites << ", \"blockedLeafWrites\": "
|
|
<< r.wouldWrite << ", \"rngWords\": " << r.rngWords << ", \"rngLoaded\": "
|
|
<< (r.rngLoaded ? "true" : "false") << ", \"rngCommitted\": "
|
|
<< (r.rngCommitted ? "true" : "false") << ", \"rngUnaccounted\": [";
|
|
for (std::size_t i = 0; i < r.rngUnaccounted.size(); ++i)
|
|
f << (i ? ", " : "") << '"' << JsonEscape(r.rngUnaccounted[i]) << '"';
|
|
f << "]},\n";
|
|
f << " \"phases\": [\n";
|
|
bool first = true;
|
|
for (const auto& rec : r.records) {
|
|
if (!first) f << ",\n";
|
|
first = false;
|
|
f << " {\"driver\": \"" << DriverName(rec.desc->driver) << "\", \"id\": \""
|
|
<< rec.desc->id << "\", \"name\": \"" << rec.desc->name << "\", \"status\": \""
|
|
<< StatusName(rec.desc->status) << "\", \"ran\": " << rec.invocations
|
|
<< ", \"writes\": " << rec.leafWrites << ", \"blockedWrites\": " << rec.wouldWrite
|
|
<< ", \"rng\": " << rec.rngWords << "}";
|
|
}
|
|
f << "\n ]\n}\n";
|
|
return static_cast<bool>(f);
|
|
}
|
|
|
|
} // namespace sots::app
|