// 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 #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(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 players(8); for (std::size_t i = 0; i < players.size(); ++i) players[i].player = MakePlayer(static_cast(i), sots::app::kNoAlliance, 0); players[2].player = MakePlayer(2, 0, 0x0c); players[3].player = MakePlayer(3, 0, 0x0c); const std::vector 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; }