lane T2: the treaty-turn stamp; 76 leaves closed across five pairs, 0 regressed
H02 StampTreatyTurns -- the diplomacy ledger's "this treaty was last in force on turn N" stamp, over every ordered pair of players that holds one, creating the entry on demand. It is a host step, not a phase of either turn driver: its sole caller is the command-application step, which runs after the frame counter has advanced and before both drivers. On a turn with no combat and no diplomatic command it is the only writer of these fields, which is why a whole per-player dipstats vector is its output. Read from the instruction stream; three things an earlier reading had wrong are corrected in findings/subsystems/treaty-turn-stamp.md: the stamped value is the TURN and not the modification counter, the relation codes are 3=allied / 2=NAP / 1=cease-fire and not the reverse, and the bit is the player's index field rather than its position in the player vector -- the opposite convention from the shared-vision mask two files away. Measured, closed and regressed reported separately and never netted: turn1 -> turn2 (reference) 209 -> 132 closed 77 (was 51), regressed 0 turn2 -> turn3 108 -> 73 closed 35 (was 21), regressed 0 human-turn2 -> turn3 closed 76 (was 64), regressed 0 zuul-turn15 -> turn16 closed 30 (was 18), regressed 0 zuul-turn16 -> turn17 closed 29 (was 17), regressed 0 The last three are pairs from a different game at turns 2, 15 and 16 that the model was never fitted to, and it closes exactly the twelve ordered treaty pairs each of them holds. The rule also reproduces the ledger of ten of the eleven corpus saves entry for entry, including each entry's order and every stamped value; the eleventh is the turn-1 save whose ledger no turn has yet written, and it is the reference pair's input. app_test_treaty pins the five things the corpus cannot separate: the relation codes (no save exercises cease-fire), the bit's source, the missing alliance-id guard, the -1 initialiser on a fresh entry, and the append-at-the-end order that makes re-running the step idempotent instead of duplicating rows. The betrayal half of the same function needs the turn's diplomatic commands; with no command stream it is provably a no-op and is not modelled.
This commit is contained in:
parent
0f1c007f00
commit
5d29ee60e4
8 changed files with 369 additions and 5 deletions
|
|
@ -7,6 +7,7 @@ add_library(sots_app STATIC
|
||||||
alliance.cpp
|
alliance.cpp
|
||||||
phase_catalog.cpp
|
phase_catalog.cpp
|
||||||
trade_raid.cpp
|
trade_raid.cpp
|
||||||
|
treaty.cpp
|
||||||
turn_record.cpp
|
turn_record.cpp
|
||||||
visibility_phase.cpp
|
visibility_phase.cpp
|
||||||
turn.cpp
|
turn.cpp
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,13 @@ constexpr PhaseDesc kHost[] = {
|
||||||
{Driver::Host, 0, "H00", "BeginProcessTurn", PhaseStatus::Implemented,
|
{Driver::Host, 0, "H00", "BeginProcessTurn", PhaseStatus::Implemented,
|
||||||
"advances the frame counter, which is the turn number the whole game displays; runs "
|
"advances the frame counter, which is the turn number the whole game displays; runs "
|
||||||
"before the spine and is where the 'Begin processing turn N' line comes from"},
|
"before the spine and is where the 'Begin processing turn N' line comes from"},
|
||||||
{Driver::Host, 1, "H01", "SaveWriterInvariants", PhaseStatus::Implemented,
|
{Driver::Host, 1, "H02", "StampTreatyTurns", PhaseStatus::Implemented,
|
||||||
|
"the diplomacy ledger's 'this treaty was last in force on turn N' stamp, over every "
|
||||||
|
"ORDERED pair of players that holds one, creating the entry on demand. Runs in the "
|
||||||
|
"command-application step -- after the frame counter has advanced and before either "
|
||||||
|
"turn driver -- so it stamps the NEW turn. It is the only writer of these fields on a "
|
||||||
|
"turn with no combat and no diplomatic command"},
|
||||||
|
{Driver::Host, 2, "H01", "SaveWriterInvariants", PhaseStatus::Implemented,
|
||||||
"the summary's turn number is the simulation's frame counter -- an identity that holds "
|
"the summary's turn number is the simulation's frame counter -- an identity that holds "
|
||||||
"across every save in the corpus and belongs to the writer, not to a turn phase"},
|
"across every save in the corpus and belongs to the writer, not to a turn phase"},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
88
src/app/treaty.cpp
Normal file
88
src/app/treaty.cpp
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
#include "app/treaty.h"
|
||||||
|
|
||||||
|
namespace sots::app {
|
||||||
|
|
||||||
|
using mars::stream::shapes::DipStat;
|
||||||
|
using mars::stream::shapes::Player;
|
||||||
|
using mars::stream::shapes::PlayerEntry;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// "This kind of treaty was last in force on turn N", one field per treaty kind. A fresh
|
||||||
|
// entry starts all three at -1, which is what distinguishes "never" from "on turn 0".
|
||||||
|
constexpr std::int32_t kNever = -1;
|
||||||
|
|
||||||
|
std::int32_t* StampField(DipStat& e, Relation r) {
|
||||||
|
switch (r) {
|
||||||
|
case Relation::Allied: return &e.lastally;
|
||||||
|
case Relation::NonAggression: return &e.lastnap;
|
||||||
|
case Relation::CeaseFire: return &e.lastcf;
|
||||||
|
case Relation::War: return nullptr;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Relation RelationTo(const Player& self, const Player& other) {
|
||||||
|
// Self first, and by the index field -- two players with the same index are the same
|
||||||
|
// player as far as this test is concerned, which is how the original short-circuits.
|
||||||
|
if (self.plyrIdx == other.plyrIdx) return Relation::Allied;
|
||||||
|
// x86 masks a variable shift count to five bits. Reproduced rather than corrected, for
|
||||||
|
// the same reason `alliance.cpp` reproduces it: the point is to be the original.
|
||||||
|
const std::uint32_t bit = 1u << (static_cast<unsigned>(other.plyrIdx) & 31u);
|
||||||
|
// The alliance mask is tested UNCONDITIONALLY here -- there is no alliance-id guard, and
|
||||||
|
// that is the difference from the shared-vision mask in alliance.cpp.
|
||||||
|
if (static_cast<std::uint32_t>(self.alliances.al) & bit) return Relation::Allied;
|
||||||
|
if (static_cast<std::uint32_t>(self.alliances.na) & bit) return Relation::NonAggression;
|
||||||
|
if (static_cast<std::uint32_t>(self.alliances.cf) & bit) return Relation::CeaseFire;
|
||||||
|
return Relation::War;
|
||||||
|
}
|
||||||
|
|
||||||
|
DipStat NewDipStat(std::int32_t otherPlayerId) {
|
||||||
|
DipStat e;
|
||||||
|
e.other = otherPlayerId;
|
||||||
|
e.lastnap = kNever;
|
||||||
|
e.lastally = kNever;
|
||||||
|
e.lastcf = kNever;
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
|
TreatyStampResult StampTreatyTurns(std::vector<PlayerEntry>& players, std::int32_t frame) {
|
||||||
|
TreatyStampResult r;
|
||||||
|
const std::size_t n = players.size();
|
||||||
|
for (std::size_t i = 0; i < n; ++i) {
|
||||||
|
Player& a = players[i].player;
|
||||||
|
for (std::size_t j = 0; j < n; ++j) {
|
||||||
|
if (i == j) continue; // the original compares the POINTERS, not the indices
|
||||||
|
const Player& b = players[j].player;
|
||||||
|
const Relation rel = RelationTo(a, b);
|
||||||
|
if (rel == Relation::War) continue;
|
||||||
|
++r.pairsStamped;
|
||||||
|
|
||||||
|
const std::int32_t otherId = players[j].playerID;
|
||||||
|
DipStat* e = nullptr;
|
||||||
|
for (auto& cand : a.dipstats) // linear, first match, exactly as the original
|
||||||
|
if (cand.other == otherId) {
|
||||||
|
e = &cand;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!e) {
|
||||||
|
a.dipstats.push_back(NewDipStat(otherId)); // appended at the END
|
||||||
|
e = &a.dipstats.back();
|
||||||
|
++r.entriesCreated;
|
||||||
|
}
|
||||||
|
std::int32_t* field = StampField(*e, rel);
|
||||||
|
if (field && *field != frame) ++r.fieldsWritten;
|
||||||
|
if (field) *field = frame;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// A created entry puts a whole record on the wire, not one value: its own leaf plus the
|
||||||
|
// element leaf the container gains. A stamp into an entry that already existed moves one
|
||||||
|
// leaf. Counting it this way keeps the phase's self-report in the same currency as the
|
||||||
|
// divergence report rather than in a currency of its own.
|
||||||
|
r.leafWrites = r.fieldsWritten + r.entriesCreated;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace sots::app
|
||||||
76
src/app/treaty.h
Normal file
76
src/app/treaty.h
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
// The treaty-turn stamp -- a host step, not a phase of either turn driver.
|
||||||
|
//
|
||||||
|
// Once per turn, after the turn's commands have been applied and before either turn driver
|
||||||
|
// runs, the server walks every ORDERED pair of players and, for each pair that currently
|
||||||
|
// holds a treaty, stamps the current turn number into that player's diplomacy-stats entry for
|
||||||
|
// the other. The entry is created on demand. Nothing else in a normal turn writes those
|
||||||
|
// fields, so the whole per-player `dipstats` vector of a save is this step's output.
|
||||||
|
//
|
||||||
|
// It is the diplomacy ledger the AI reads. There is no attitude score in the simulation: what
|
||||||
|
// a player knows about another's diplomatic history is thirteen counters, and three of them
|
||||||
|
// are "the turn the treaty of this kind was last in force".
|
||||||
|
//
|
||||||
|
// Four things about it are worth stating here because each is a place a reimplementation
|
||||||
|
// goes quietly wrong.
|
||||||
|
//
|
||||||
|
// 1. The stamped value is the TURN, not the modification counter. The two are easy to
|
||||||
|
// confuse -- they are adjacent words on the same object and an earlier reading of this
|
||||||
|
// step named the wrong one -- but they differ by an order of magnitude in every save: on
|
||||||
|
// the corpus's turn-23 save the counter is in the hundreds and the stamp is 23.
|
||||||
|
// 2. The relation codes are 3 = allied (and self), 2 = non-aggression, 1 = cease-fire,
|
||||||
|
// 0 = war. An earlier reading had 1 and 3 the other way round, which puts the stamp in
|
||||||
|
// the cease-fire field of an allied pair.
|
||||||
|
// 3. The relation's bit is the player's own INDEX FIELD, not its position in the player
|
||||||
|
// vector. This is the exact opposite of the shared-vision mask in `alliance.h`, which
|
||||||
|
// uses the vector position and never loads the index field. Both live in the same
|
||||||
|
// subsystem and they disagree. No save in the corpus separates them.
|
||||||
|
// 4. The alliance mask is consulted unconditionally. Unlike the shared-vision mask, it is
|
||||||
|
// NOT gated on the player carrying an alliance id.
|
||||||
|
//
|
||||||
|
// Existing entries are found by a linear first-match scan on the other player's id, and a new
|
||||||
|
// entry is appended at the END -- so the vector's order is the order in which pairs were
|
||||||
|
// first stamped, which is player-vector order.
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "mars/stream/shapes.h"
|
||||||
|
|
||||||
|
namespace sots::app {
|
||||||
|
|
||||||
|
// What one player currently is to another. The values are the original's, and they are the
|
||||||
|
// values the rest of the game tests against (`== 3` gates allied resupply; `< 1` is war).
|
||||||
|
enum class Relation : int {
|
||||||
|
War = 0,
|
||||||
|
CeaseFire = 1,
|
||||||
|
NonAggression = 2,
|
||||||
|
Allied = 3, // and self
|
||||||
|
};
|
||||||
|
|
||||||
|
// `self` and `other` are read from the wire's player records. `other`'s INDEX FIELD supplies
|
||||||
|
// the bit; a player is always `Allied` to itself.
|
||||||
|
Relation RelationTo(const mars::stream::shapes::Player& self,
|
||||||
|
const mars::stream::shapes::Player& other);
|
||||||
|
|
||||||
|
// The value a fresh diplomacy-stats entry starts at: the three "last in force" fields at -1
|
||||||
|
// and every counter at 0. Held as a function rather than as a default member initialiser
|
||||||
|
// because the wire shape's own default is all-zero, and -1 is this step's, not the wire's.
|
||||||
|
mars::stream::shapes::DipStat NewDipStat(std::int32_t otherPlayerId);
|
||||||
|
|
||||||
|
// What the step did, so the phase log can report it without the phase re-deriving it.
|
||||||
|
struct TreatyStampResult {
|
||||||
|
int pairsStamped = 0; // ordered pairs that held a treaty
|
||||||
|
int entriesCreated = 0; // entries that did not exist before this turn
|
||||||
|
int fieldsWritten = 0; // stamps applied; equals pairsStamped
|
||||||
|
int leafWrites = 0; // save leaves this moved, counted the way the report counts them
|
||||||
|
};
|
||||||
|
|
||||||
|
// The whole step, in place. `frame` is the turn number AFTER the host has advanced it --
|
||||||
|
// this runs downstream of that increment, so a caller that has not advanced the frame yet
|
||||||
|
// will stamp every entry one turn short.
|
||||||
|
TreatyStampResult StampTreatyTurns(std::vector<mars::stream::shapes::PlayerEntry>& players,
|
||||||
|
std::int32_t frame);
|
||||||
|
|
||||||
|
} // namespace sots::app
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
|
|
||||||
#include "app/alliance.h"
|
#include "app/alliance.h"
|
||||||
#include "app/trade_raid.h"
|
#include "app/trade_raid.h"
|
||||||
|
#include "app/treaty.h"
|
||||||
#include "app/turn_record.h"
|
#include "app/turn_record.h"
|
||||||
#include "app/visibility_phase.h"
|
#include "app/visibility_phase.h"
|
||||||
#include "game/sim/colony.h"
|
#include "game/sim/colony.h"
|
||||||
|
|
@ -722,6 +723,30 @@ TurnResult RunStrategicTurn(SaveGame& game, const TurnOptions& opt) {
|
||||||
r.records.push_back(rec);
|
r.records.push_back(rec);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// H02 StampTreatyTurns: the diplomacy ledger's "last in force on turn N" stamp. It runs
|
||||||
|
// in the command-application step, which is AFTER the frame counter above and before
|
||||||
|
// either turn driver -- the ordering is load-bearing, because the value stamped is the
|
||||||
|
// new turn and a step placed before H00 would stamp every entry one turn short.
|
||||||
|
{
|
||||||
|
PhaseRecord rec;
|
||||||
|
rec.desc = &hp[1];
|
||||||
|
const TreatyStampResult ts = StampTreatyTurns(game.sim.players, game.sim.frame);
|
||||||
|
rec.invocations = ts.pairsStamped;
|
||||||
|
rec.leafWrites = ts.leafWrites;
|
||||||
|
rec.committed = true;
|
||||||
|
rec.notes.push_back(fmt("%d ordered pair(s) hold a treaty; %d entry(s) created, "
|
||||||
|
"%d stamp(s) written at turn %d",
|
||||||
|
ts.pairsStamped, ts.entriesCreated, ts.fieldsWritten,
|
||||||
|
game.sim.frame));
|
||||||
|
if (ts.pairsStamped == 0)
|
||||||
|
rec.notes.push_back("no player in this save holds any treaty, so this run "
|
||||||
|
"exercises the relation test only and writes nothing");
|
||||||
|
rec.notes.push_back("the betrayal half of the same step needs the turn's diplomatic "
|
||||||
|
"commands (the alliance/NAP/cease-fire broken masks); with no "
|
||||||
|
"command stream it is provably a no-op and is not modelled");
|
||||||
|
r.records.push_back(rec);
|
||||||
|
}
|
||||||
|
|
||||||
PlayerPhaseTotals pt;
|
PlayerPhaseTotals pt;
|
||||||
SystemTotals st;
|
SystemTotals st;
|
||||||
bool playerDriverRan = false;
|
bool playerDriverRan = false;
|
||||||
|
|
@ -930,7 +955,7 @@ TurnResult RunStrategicTurn(SaveGame& game, const TurnOptions& opt) {
|
||||||
// H01 SaveWriterInvariants.
|
// H01 SaveWriterInvariants.
|
||||||
{
|
{
|
||||||
PhaseRecord rec;
|
PhaseRecord rec;
|
||||||
rec.desc = &hp[1];
|
rec.desc = &hp[2];
|
||||||
const int before = game.summary.turn;
|
const int before = game.summary.turn;
|
||||||
ApplySaveWriterInvariants(game, r);
|
ApplySaveWriterInvariants(game, r);
|
||||||
rec.invocations = 1;
|
rec.invocations = 1;
|
||||||
|
|
|
||||||
|
|
@ -22,13 +22,20 @@ add_executable(app_test_alliance test_alliance.cpp)
|
||||||
target_link_libraries(app_test_alliance PRIVATE sots_app)
|
target_link_libraries(app_test_alliance PRIVATE sots_app)
|
||||||
add_test(NAME app_alliance COMMAND app_test_alliance)
|
add_test(NAME app_alliance COMMAND app_test_alliance)
|
||||||
|
|
||||||
|
# The treaty-turn stamp, as a rule. Pins the five things the corpus cannot separate -- the
|
||||||
|
# relation codes, the bit's source, the missing alliance-id guard, the -1 initialiser and the
|
||||||
|
# append order -- so it always runs.
|
||||||
|
add_executable(app_test_treaty test_treaty.cpp)
|
||||||
|
target_link_libraries(app_test_treaty PRIVATE sots_app)
|
||||||
|
add_test(NAME app_treaty COMMAND app_test_treaty)
|
||||||
|
|
||||||
# The turn-record model against the record the game itself archived; needs the owner's saves.
|
# The turn-record model against the record the game itself archived; needs the owner's saves.
|
||||||
add_executable(app_test_turn_record test_turn_record.cpp)
|
add_executable(app_test_turn_record test_turn_record.cpp)
|
||||||
target_link_libraries(app_test_turn_record PRIVATE sots_app)
|
target_link_libraries(app_test_turn_record PRIVATE sots_app)
|
||||||
add_test(NAME app_turn_record COMMAND app_test_turn_record)
|
add_test(NAME app_turn_record COMMAND app_test_turn_record)
|
||||||
|
|
||||||
foreach(_t app_test_catalog app_test_turn app_test_trade_raid app_test_alliance
|
foreach(_t app_test_catalog app_test_turn app_test_trade_raid app_test_alliance
|
||||||
app_test_turn_record)
|
app_test_treaty app_test_turn_record)
|
||||||
target_include_directories(${_t} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
target_include_directories(${_t} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
target_compile_options(${_t} PRIVATE -Wall -Wextra -Wpedantic)
|
target_compile_options(${_t} PRIVATE -Wall -Wextra -Wpedantic)
|
||||||
endforeach()
|
endforeach()
|
||||||
|
|
|
||||||
|
|
@ -48,8 +48,10 @@ int main() {
|
||||||
CHECK(nt == 37);
|
CHECK(nt == 37);
|
||||||
CHECK(static_cast<int>(ns + np) == kSpinePhaseCount);
|
CHECK(static_cast<int>(ns + np) == kSpinePhaseCount);
|
||||||
|
|
||||||
// The host steps are deliberately NOT part of the milestone's denominator.
|
// The host steps are deliberately NOT part of the milestone's denominator. Their ids run
|
||||||
CHECK(nh == 2);
|
// H00, H02, H01 because the table is in EXECUTION order and H02 was read later than the
|
||||||
|
// step it runs before; the id is the stable name, the index is the ordinal.
|
||||||
|
CHECK(nh == 3);
|
||||||
CheckTable(h, nh, Driver::Host, 0, ids, names);
|
CheckTable(h, nh, Driver::Host, 0, ids, names);
|
||||||
CheckTable(s, ns, Driver::Strategic, 0, ids, names);
|
CheckTable(s, ns, Driver::Strategic, 0, ids, names);
|
||||||
CheckTable(p, np, Driver::Player, 1, ids, names);
|
CheckTable(p, np, Driver::Player, 1, ids, names);
|
||||||
|
|
|
||||||
159
tests/app/test_treaty.cpp
Normal file
159
tests/app/test_treaty.cpp
Normal file
|
|
@ -0,0 +1,159 @@
|
||||||
|
// 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 <cstdio>
|
||||||
|
|
||||||
|
#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<PlayerEntry> MakePlayers(std::size_t n) {
|
||||||
|
std::vector<PlayerEntry> v(n);
|
||||||
|
for (std::size_t i = 0; i < n; ++i) {
|
||||||
|
v[i].playerID = static_cast<std::int32_t>(16 * (i + 1));
|
||||||
|
v[i].player.plyrIdx = static_cast<std::int32_t>(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<PlayerEntry> 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<PlayerEntry> 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<PlayerEntry> 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<PlayerEntry> 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<PlayerEntry> 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<PlayerEntry> 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<PlayerEntry> 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;
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue