The spine's fourth phase, read byte-for-byte and implemented:
almem[i] = (1 << i) | (ALid != -1 ? AL : 0)
with i the player's POSITION IN THE PLAYER VECTOR, not its index field. Both
inputs are on the wire and so is the output, through the turn-record archive,
so the phase is checkable against bytes the original wrote:
app_turn_record: 11 saves, 80 player-records, 560 fields, 0 mismatches
(was 480 fields over six fields; almem is the seventh)
The eight zero masks of the corpus's earliest archived turn are PREDICTED, not
excluded: the archiving phase also runs on load, and the load path does not run
the spine. BuildTurnRecord takes spineRan and models it, so all 80 records are
compared.
Three parts of the rule the corpus cannot separate -- the bit index, the OR,
and the ALid guard -- are pinned in app_alliance with the separating inputs no
save provides, and app_turn_record prints that it could not separate them.
Divergence, closed and regressed reported separately:
turn1->turn2 default 209 -> 204 closed 5, regressed 0
turn1->turn2 --commit-blocked 209 -> 189 closed 29, regressed 9 (was 17)
turn2->turn3 default 108 -> 103 closed 5, regressed 0
turn2->turn3 --commit-blocked 108 -> 106 closed 13, regressed 11 (was 19)
T36 stays blocked: nine leaves would still be wrong (inc x3, sav x3 behind the
budget; three census leaves behind the design catalogue). It now closes all 24
turnstats leaves on the reference pair, so it becomes a clean +24 once those
two land.
Prediction and falsification committed first in 49ae628.
Gates run separately: clean-room OK; host ctest 43/43. No src/shim touched.
110 lines
5.4 KiB
C++
110 lines
5.4 KiB
C++
// The alliance / shared-vision mask, pinned as a rule rather than as a table of observations.
|
|
//
|
|
// The corpus agrees with this rule on 72 of 80 player-records and predicts the other 8 (the
|
|
// zero masks of every save's earliest archived turn, which the load path wrote without a
|
|
// spine). That comparison lives in app_test_turn_record, where the stored bytes are. What is
|
|
// pinned HERE is the part the corpus cannot separate:
|
|
//
|
|
// * the bit is the player's position in the vector, not its own index field. Every save in
|
|
// the corpus has the two equal, so no comparison against stored bytes can tell them
|
|
// apart -- only the instruction stream can, and this test holds the reading.
|
|
// * the mask is an OR of the self bit with the alliance's member mask. In the corpus every
|
|
// observed alliance mask already contains its member's own bit, so `self | AL` and `AL`
|
|
// are indistinguishable there.
|
|
// * the guard is on the alliance id, not on the member mask being non-zero. In the corpus
|
|
// the two always agree, because a player with no alliance id also has a zero mask.
|
|
//
|
|
// Three cases that no save exercises, each held here so that a later save which does
|
|
// exercise one has something to disagree with.
|
|
#include <cstdio>
|
|
|
|
#include "app/alliance.h"
|
|
#include "mars/stream/shapes.h"
|
|
|
|
static int failures = 0;
|
|
#define CHECK_EQ(a, b) \
|
|
do { \
|
|
const long long va = (long long)(a), vb = (long long)(b); \
|
|
if (va != vb) { \
|
|
std::printf("FAIL %s:%d %s == %s (%lld != %lld)\n", __FILE__, \
|
|
__LINE__, #a, #b, va, vb); \
|
|
++failures; \
|
|
} \
|
|
} while (0)
|
|
|
|
using mars::stream::shapes::Player;
|
|
using mars::stream::shapes::PlayerEntry;
|
|
|
|
static Player MakePlayer(int plyrIdx, int alid, int al) {
|
|
Player p;
|
|
p.plyrIdx = plyrIdx;
|
|
p.alliances.alid = alid;
|
|
p.alliances.al = al;
|
|
return p;
|
|
}
|
|
|
|
int main() {
|
|
// 1. No alliance: the self bit alone, at the VECTOR index.
|
|
for (std::size_t i = 0; i < 8; ++i) {
|
|
const Player p = MakePlayer(static_cast<int>(i), sots::app::kNoAlliance, 0);
|
|
CHECK_EQ(sots::app::AllianceMask(p, i), 1 << i);
|
|
}
|
|
|
|
// 2. The index field disagrees with the vector position. The original shifts by the loop
|
|
// variable and never loads the index field, so the vector position wins. No save in the
|
|
// corpus has these two apart; this is the instruction-stream reading, pinned.
|
|
{
|
|
const Player p = MakePlayer(/*plyrIdx=*/7, sots::app::kNoAlliance, 0);
|
|
CHECK_EQ(sots::app::AllianceMask(p, 2), 1 << 2);
|
|
}
|
|
|
|
// 3. In an alliance whose member mask does NOT already contain the player's own bit.
|
|
// Every alliance in the corpus does contain it, so `self | AL` and `AL` are the same
|
|
// number there. Here they are not, and the OR is what is being held.
|
|
{
|
|
const Player p = MakePlayer(/*plyrIdx=*/1, /*alid=*/0, /*al=*/0x0c); // bits 2 and 3
|
|
CHECK_EQ(sots::app::AllianceMask(p, 1), 0x0e); // 1|2|3
|
|
}
|
|
|
|
// 4. The guard is the alliance id, not the member mask. A stale member mask with no
|
|
// alliance id contributes nothing. Unobserved in the corpus, where the two never differ.
|
|
{
|
|
const Player p = MakePlayer(/*plyrIdx=*/1, sots::app::kNoAlliance, /*al=*/0x0c);
|
|
CHECK_EQ(sots::app::AllianceMask(p, 1), 0x02);
|
|
}
|
|
|
|
// 5. An alliance id of zero is a real alliance. -1 is the only "none", and a check
|
|
// written as `if (alid)` would drop every player of alliance 0 -- which is the only
|
|
// alliance id that appears anywhere in the corpus.
|
|
{
|
|
const Player p = MakePlayer(/*plyrIdx=*/2, /*alid=*/0, /*al=*/0x0c);
|
|
CHECK_EQ(sots::app::AllianceMask(p, 2), 0x0c);
|
|
}
|
|
|
|
// 6. The whole phase, in vector order, and the shape the corpus actually shows: two
|
|
// allied players carrying the same member mask, the rest alone.
|
|
{
|
|
std::vector<PlayerEntry> players(8);
|
|
for (std::size_t i = 0; i < players.size(); ++i)
|
|
players[i].player = MakePlayer(static_cast<int>(i), sots::app::kNoAlliance, 0);
|
|
players[2].player = MakePlayer(2, 0, 0x0c);
|
|
players[3].player = MakePlayer(3, 0, 0x0c);
|
|
const std::vector<std::int32_t> m = sots::app::RebuildAllianceMasks(players);
|
|
CHECK_EQ(m.size(), players.size());
|
|
const std::int32_t want[8] = {1, 2, 0x0c, 0x0c, 0x10, 0x20, 0x40, 0x80};
|
|
for (std::size_t i = 0; i < 8; ++i) CHECK_EQ(m[i], want[i]);
|
|
}
|
|
|
|
// 7. The shift wraps above 31, because x86 masks a variable shift count to five bits.
|
|
// Untested against any game -- no save has more than eight players -- and reproduced
|
|
// rather than corrected, so that a reimplementation running a 32-player game diverges the
|
|
// same way the original does instead of diverging differently.
|
|
{
|
|
const Player p = MakePlayer(0, sots::app::kNoAlliance, 0);
|
|
CHECK_EQ(sots::app::AllianceMask(p, 32), 1);
|
|
CHECK_EQ(sots::app::AllianceMask(p, 33), 2);
|
|
}
|
|
|
|
std::printf("app_test_alliance: %d failure(s)\n", failures);
|
|
return failures ? 1 : 0;
|
|
}
|