// The treaty-turn stamp, pinned as a rule rather than as a table of observations. // // The corpus agrees with this rule on ten of eleven saves -- two games, species 0/2/4/5, // turns 2 through 23 -- reproducing every entry, every `other` id, every entry's ORDER and // every stamped value. (The eleventh is a turn-1 save on which no turn has been processed, // so its ledger is empty; it is the input of the reference pair and the rule predicts what // it becomes.) That comparison lives against the owner's saves. What is pinned HERE is what // the corpus cannot separate: // // * the relation codes. Every save's treaties are either alliances or non-aggression // pacts, so the cease-fire arm has never been exercised and an implementation that // swapped 1 and 3 would look right on ten of the eleven if it also swapped the fields. // * the bit is the player's INDEX FIELD, not its vector position. Every save in the corpus // has the two equal. // * the alliance mask is consulted with no alliance-id guard, unlike the shared-vision // mask -- in the corpus no player holds an alliance mask without an alliance id. // * a fresh entry's three "last in force" fields start at -1, not 0. A save cannot show // this for a field that was then stamped; it shows only for the two that were not. // * the entry is appended at the END and found by first match, so re-running the step is // idempotent rather than appending duplicates. #include #include "app/treaty.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::PlayerEntry; using sots::app::Relation; using sots::app::RelationTo; using sots::app::StampTreatyTurns; static std::vector MakePlayers(std::size_t n) { std::vector v(n); for (std::size_t i = 0; i < n; ++i) { v[i].playerID = static_cast(16 * (i + 1)); v[i].player.plyrIdx = static_cast(i); } return v; } int main() { // 1. The relation codes, and their precedence. Alliance beats NAP beats cease-fire, and // a player is allied to itself whatever the masks say. { std::vector p = MakePlayers(3); p[0].player.alliances.al = 1 << 1; p[0].player.alliances.na = (1 << 1) | (1 << 2); p[0].player.alliances.cf = (1 << 1) | (1 << 2); CHECK_EQ((int)RelationTo(p[0].player, p[1].player), (int)Relation::Allied); CHECK_EQ((int)RelationTo(p[0].player, p[2].player), (int)Relation::NonAggression); CHECK_EQ((int)RelationTo(p[0].player, p[0].player), (int)Relation::Allied); CHECK_EQ((int)RelationTo(p[1].player, p[0].player), (int)Relation::War); // not symmetric p[0].player.alliances.na = 0; CHECK_EQ((int)RelationTo(p[0].player, p[2].player), (int)Relation::CeaseFire); p[0].player.alliances.cf = 0; CHECK_EQ((int)RelationTo(p[0].player, p[2].player), (int)Relation::War); } // 2. The bit is the INDEX FIELD. Give the vector's second element index 5 and the mask // bit that reaches it is bit 5, not bit 1. { std::vector p = MakePlayers(2); p[1].player.plyrIdx = 5; p[0].player.alliances.na = 1 << 5; CHECK_EQ((int)RelationTo(p[0].player, p[1].player), (int)Relation::NonAggression); p[0].player.alliances.na = 1 << 1; CHECK_EQ((int)RelationTo(p[0].player, p[1].player), (int)Relation::War); } // 3. No alliance-id guard: an alliance mask with no alliance id still means allied. { std::vector p = MakePlayers(2); p[0].player.alliances.alid = -1; p[0].player.alliances.al = 1 << 1; CHECK_EQ((int)RelationTo(p[0].player, p[1].player), (int)Relation::Allied); } // 4. A created entry: the right field stamped, the other two at -1, every counter 0, // and `other` the OTHER PLAYER'S ID -- not its index. { std::vector p = MakePlayers(2); p[0].player.alliances.na = 1 << 1; p[1].player.alliances.na = 1 << 0; const sots::app::TreatyStampResult r = StampTreatyTurns(p, 7); CHECK_EQ(r.pairsStamped, 2); CHECK_EQ(r.entriesCreated, 2); CHECK_EQ(r.fieldsWritten, 2); CHECK_EQ(p[0].player.dipstats.size(), 1u); const auto& e = p[0].player.dipstats[0]; CHECK_EQ(e.other, 32); CHECK_EQ(e.lastnap, 7); CHECK_EQ(e.lastally, -1); CHECK_EQ(e.lastcf, -1); CHECK_EQ(e.lastnapbty, 0); CHECK_EQ(e.bknnap, 0); CHECK_EQ(e.btynap, 0); CHECK_EQ(e.deadhome, 0); CHECK_EQ(p[1].player.dipstats[0].other, 16); } // 5. Re-running does not duplicate: the entry is found by first match on `other` and the // stamp is overwritten in place. { std::vector p = MakePlayers(2); p[0].player.alliances.na = 1 << 1; (void)StampTreatyTurns(p, 7); const sots::app::TreatyStampResult r = StampTreatyTurns(p, 8); CHECK_EQ(p[0].player.dipstats.size(), 1u); CHECK_EQ(p[0].player.dipstats[0].lastnap, 8); CHECK_EQ(r.entriesCreated, 0); CHECK_EQ(r.fieldsWritten, 1); // Stamping the same turn twice writes nothing new. const sots::app::TreatyStampResult again = StampTreatyTurns(p, 8); CHECK_EQ(again.fieldsWritten, 0); CHECK_EQ(again.pairsStamped, 1); // the pair still holds a treaty; it just did not move } // 6. Entries are appended in the order pairs are first stamped, which is player-vector // order -- and a later turn that adds a new treaty appends after the existing ones. { std::vector p = MakePlayers(4); p[0].player.alliances.na = (1 << 2) | (1 << 3); (void)StampTreatyTurns(p, 4); CHECK_EQ(p[0].player.dipstats.size(), 2u); CHECK_EQ(p[0].player.dipstats[0].other, 48); // index 2 CHECK_EQ(p[0].player.dipstats[1].other, 64); // index 3 p[0].player.alliances.al = 1 << 1; (void)StampTreatyTurns(p, 5); CHECK_EQ(p[0].player.dipstats.size(), 3u); CHECK_EQ(p[0].player.dipstats[2].other, 32); // appended at the END, not sorted in CHECK_EQ(p[0].player.dipstats[2].lastally, 5); CHECK_EQ(p[0].player.dipstats[2].lastnap, -1); CHECK_EQ(p[0].player.dipstats[0].lastnap, 5); // and the older ones were re-stamped } // 7. War with everyone writes nothing at all -- the ledger of a player with no treaty // stays empty, which is what both real empires in the reference save look like. { std::vector p = MakePlayers(4); const sots::app::TreatyStampResult r = StampTreatyTurns(p, 9); CHECK_EQ(r.pairsStamped, 0); CHECK_EQ(r.leafWrites, 0); for (const auto& e : p) CHECK_EQ(e.player.dipstats.size(), 0u); } std::printf("app_test_treaty: %d failure(s)\n", failures); return failures ? 1 : 0; }