Compare commits
4 commits
db99971efb
...
eafbc5f03a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eafbc5f03a | ||
|
|
c5d4b45fe7 | ||
|
|
fa5e190bce | ||
|
|
5d29ee60e4 |
8 changed files with 370 additions and 5 deletions
|
|
@ -7,6 +7,7 @@ add_library(sots_app STATIC
|
|||
alliance.cpp
|
||||
phase_catalog.cpp
|
||||
trade_raid.cpp
|
||||
treaty.cpp
|
||||
turn_record.cpp
|
||||
construction_phase.cpp
|
||||
visibility_phase.cpp
|
||||
|
|
|
|||
|
|
@ -44,7 +44,13 @@ constexpr PhaseDesc kHost[] = {
|
|||
{Driver::Host, 0, "H00", "BeginProcessTurn", PhaseStatus::Implemented,
|
||||
"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"},
|
||||
{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 "
|
||||
"across every save in the corpus and belongs to the writer, not to a turn phase"},
|
||||
};
|
||||
|
|
|
|||
89
src/app/treaty.cpp
Normal file
89
src/app/treaty.cpp
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
#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;
|
||||
}
|
||||
}
|
||||
// The phase's own accounting: one for each stamp that moved a value, plus one for each
|
||||
// record that did not exist before. It is deliberately NOT an attempt to predict the
|
||||
// divergence report's leaf count, which also charges two container leaves to each player
|
||||
// whose ledger goes from empty to non-empty -- on the reference pair this reports 28
|
||||
// where the report closes 26. The report is the authority; this is what the phase did.
|
||||
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
|
||||
|
|
@ -10,6 +10,7 @@
|
|||
#include "app/alliance.h"
|
||||
#include "app/construction_phase.h"
|
||||
#include "app/trade_raid.h"
|
||||
#include "app/treaty.h"
|
||||
#include "app/turn_record.h"
|
||||
#include "app/visibility_phase.h"
|
||||
#include "game/sim/colony.h"
|
||||
|
|
@ -860,6 +861,30 @@ TurnResult RunStrategicTurn(SaveGame& game, const TurnOptions& opt) {
|
|||
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;
|
||||
SystemTotals st;
|
||||
bool playerDriverRan = false;
|
||||
|
|
@ -1100,7 +1125,7 @@ TurnResult RunStrategicTurn(SaveGame& game, const TurnOptions& opt) {
|
|||
// H01 SaveWriterInvariants.
|
||||
{
|
||||
PhaseRecord rec;
|
||||
rec.desc = &hp[1];
|
||||
rec.desc = &hp[2];
|
||||
const int before = game.summary.turn;
|
||||
ApplySaveWriterInvariants(game, r);
|
||||
rec.invocations = 1;
|
||||
|
|
|
|||
|
|
@ -22,13 +22,20 @@ add_executable(app_test_alliance test_alliance.cpp)
|
|||
target_link_libraries(app_test_alliance PRIVATE sots_app)
|
||||
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.
|
||||
add_executable(app_test_turn_record test_turn_record.cpp)
|
||||
target_link_libraries(app_test_turn_record PRIVATE sots_app)
|
||||
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
|
||||
app_test_turn_record)
|
||||
app_test_treaty app_test_turn_record)
|
||||
target_include_directories(${_t} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_compile_options(${_t} PRIVATE -Wall -Wextra -Wpedantic)
|
||||
endforeach()
|
||||
|
|
|
|||
|
|
@ -48,8 +48,10 @@ int main() {
|
|||
CHECK(nt == 37);
|
||||
CHECK(static_cast<int>(ns + np) == kSpinePhaseCount);
|
||||
|
||||
// The host steps are deliberately NOT part of the milestone's denominator.
|
||||
CHECK(nh == 2);
|
||||
// The host steps are deliberately NOT part of the milestone's denominator. Their ids run
|
||||
// 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(s, ns, Driver::Strategic, 0, 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