sots-engine/src/app/construction_phase.cpp
lane-b6 0fcbb69ba5 game/sim + app: ship construction -- the build-queue completion bookkeeping, and S11's sub-pass wired in
game/sim/construction.{h,cpp}: Game::ShipRecords (four per-hull-class arrays plus the
per-design vector, sized by ENUMERATION against the wire, not by what the code touches)
and the completion bookkeeping BuildQueue::ProcessTurn performs -- the per-class built
counter, whose indexed increment has EXACTLY ONE writer in the whole image, and the
find-or-append per-design record keyed by the design's object id. RunSystemConstruction
wraps the point pass and keeps each completion's design id, which the point pass alone
does not report.

game/sim/colony: corrected from the instruction stream -- with points <= 0 the entry test
branches to the epilogue, so the REMOVAL SWEEP IS SKIPPED TOO. Carried as a labelled
hypothesis: no corpus save can reach the state that shows it.

app/construction_phase.{h,cpp}: S11's build-queue sub-pass, reported on its own line
because what blocks it is not what blocks the rest of the colony turn. It is blocked on
the per-system output term for points; it is NOT what the archived ship census waits on.

tests/game_sim/test_construction.cpp: 65 checks, including a corpus oracle the campaign
already owned and had not noticed -- zuul-turn16-noderoute -> zuul-turn17-rollpending is a
real consecutive-turn pair in which six orders complete and one is partially advanced.
The test SOLVES for the point total rather than assuming it, so a non-FIFO order, a
per-order budget or skip-instead-of-stop each falsify it.
2026-09-08 14:39:20 -04:00

96 lines
3.8 KiB
C++

#include "app/construction_phase.h"
#include <cstdarg>
#include <cstdio>
#include <map>
#include "game/sim/construction.h"
namespace sots::app {
namespace {
std::string fmt(const char* f, ...) {
char buf[512];
va_list ap;
va_start(ap, f);
std::vsnprintf(buf, sizeof buf, f, ap);
va_end(ap);
return std::string(buf);
}
} // namespace
ConstructionPhaseResult RunBuildQueues(mars::stream::shapes::SaveGame& game) {
ConstructionPhaseResult r;
// The owner of a queue is the system's `PID`, which is the player's OBJECT id, not its
// position in the player vector. The two are different numbers and confusing them is a
// trap this codebase has already paid for once.
std::map<std::int32_t, std::size_t> playerIndex;
for (std::size_t i = 0; i < game.sim.players.size(); ++i)
playerIndex[game.sim.players[i].playerID] = i;
int shipBorne = 0;
int ordersByOwner = 0;
int unownedQueues = 0;
for (auto& e : game.sim.systems) {
if (!e.sys.bq.has_value()) continue;
++r.queuesVisited;
auto& q = *e.sys.bq;
r.ordersPending += static_cast<int>(q.orders.size());
for (const auto& o : q.orders) r.pointsDemanded += o.conleft;
if (!q.orders.empty()) {
if (playerIndex.count(e.sys.pid))
++ordersByOwner;
else
++unownedQueues;
}
}
// The ship-borne queues. Every one of them is gated behind the ship's `hbq` flag, and
// the count is reported because an order hiding in one would falsify the "no orders on
// the reference pair" reading, which is the load-bearing claim of this phase.
for (const auto& fe : game.sim.fleets)
for (const auto& se : fe.flt.ships)
if (se.ship.hbq) shipBorne += static_cast<int>(se.ship.bq2.orders.size());
r.blockedOnPoints = r.ordersPending > 0;
if (r.queuesVisited == 0) {
r.notes.push_back("no system carries a build queue: a queue is written only for a "
"system with an owner");
return r;
}
r.notes.push_back(fmt("%d system build queue(s), %d pending order(s) demanding %d "
"construction point(s); %d order(s) in ship-borne queues",
r.queuesVisited, r.ordersPending, r.pointsDemanded, shipBorne));
if (r.ordersPending == 0 && shipBorne == 0) {
r.notes.push_back("NOTHING TO BUILD. The pass is faithful and idle: with no order "
"anywhere in the game it cannot create a ship, so it closes no "
"leaf here. On the reference pairs the archived ship census is "
"still one destroyer higher than ours, and the order behind that "
"destroyer is created inside the turn by the AI -- that leaf is "
"blocked on AI order generation, not on this phase");
return r;
}
// Points would come from the system's output vector; that term is the roadmap's item 1
// and is not this lane's. The pass is therefore driven with nothing and reports the
// demand, so the shape of the gap is visible in the run log.
r.notes.push_back(fmt("BLOCKED: construction points come from the per-system output "
"term (out[7] scaled by the shipyard bonus, then out[8] = min(out"
"[7], demand)), which is unmodelled. %d order(s) would be offered "
"points this turn", r.ordersPending));
if (unownedQueues)
r.notes.push_back(fmt("%d queue(s) hold orders but their system's owner id resolves "
"to no player in the save -- reported, not skipped silently",
unownedQueues));
(void)ordersByOwner;
return r;
}
} // namespace sots::app