merge lane E3: nve visibility record; 46 leaves closed 0 regressed; the gate is AFlags
This commit is contained in:
commit
c51b8d0c39
10 changed files with 696 additions and 5 deletions
|
|
@ -8,6 +8,7 @@ add_library(sots_app STATIC
|
||||||
phase_catalog.cpp
|
phase_catalog.cpp
|
||||||
trade_raid.cpp
|
trade_raid.cpp
|
||||||
turn_record.cpp
|
turn_record.cpp
|
||||||
|
visibility_phase.cpp
|
||||||
turn.cpp
|
turn.cpp
|
||||||
report.cpp)
|
report.cpp)
|
||||||
target_include_directories(sots_app PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/..)
|
target_include_directories(sots_app PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/..)
|
||||||
|
|
|
||||||
|
|
@ -109,7 +109,10 @@ constexpr PhaseDesc kStrategic[] = {
|
||||||
{Driver::Strategic, 28, "S28", "GrantMetSpeciesTechs", PhaseStatus::Stub,
|
{Driver::Strategic, 28, "S28", "GrantMetSpeciesTechs", PhaseStatus::Stub,
|
||||||
"the 'you have met this race, its racial tech appears in your tree' rule -- small, pure, "
|
"the 'you have met this race, its racial tech appears in your tree' rule -- small, pure, "
|
||||||
"draw-free, and the cheapest unimplemented phase in this table"},
|
"draw-free, and the cheapest unimplemented phase in this table"},
|
||||||
{Driver::Strategic, 29, "S29", "SystemTailFixup", PhaseStatus::Stub, ""},
|
{Driver::Strategic, 29, "S29", "SystemObservedStamp", PhaseStatus::Implemented,
|
||||||
|
"the system's own last-observed turn, written wherever some player can currently see "
|
||||||
|
"the system and left alone everywhere else. Whole function modelled; the gate is the "
|
||||||
|
"active-presence mask, which falls when the last fleet leaves"},
|
||||||
{Driver::Strategic, 30, "S30", "BuildTeamPartition", PhaseStatus::Stub, ""},
|
{Driver::Strategic, 30, "S30", "BuildTeamPartition", PhaseStatus::Stub, ""},
|
||||||
{Driver::Strategic, 31, "S31", "EncounterDetectionAndStatusRestore", PhaseStatus::Partial,
|
{Driver::Strategic, 31, "S31", "EncounterDetectionAndStatusRestore", PhaseStatus::Partial,
|
||||||
"trade-raid generation runs first here and IS modelled: two chances per player, one word "
|
"trade-raid generation runs first here and IS modelled: two chances per player, one word "
|
||||||
|
|
@ -192,11 +195,19 @@ constexpr PhaseDesc kTail[] = {
|
||||||
"the decision function is modelled in game::sim; the per-player state it reads is not "
|
"the decision function is modelled in game::sim; the per-player state it reads is not "
|
||||||
"assembled here"},
|
"assembled here"},
|
||||||
{Driver::Tail, 16, "T16", "ResolveArrivedColonizers", PhaseStatus::Stub, ""},
|
{Driver::Tail, 16, "T16", "ResolveArrivedColonizers", PhaseStatus::Stub, ""},
|
||||||
{Driver::Tail, 17, "T17", "RebuildPlayerViewTree", PhaseStatus::Stub, ""},
|
{Driver::Tail, 17, "T17", "RebuildPlayerViewTree", PhaseStatus::Partial,
|
||||||
|
"the per-(system, player) observation record IS modelled and committed -- who saw "
|
||||||
|
"the system, on what turn, and what encounter was there. The colony-numbers view "
|
||||||
|
"the same phase rebuilds beside it is NOT: its list is empty on both reference "
|
||||||
|
"pairs, so nothing here has evidence to build it against"},
|
||||||
{Driver::Tail, 18, "T18", "PostFleetWarnings", PhaseStatus::Stub, ""},
|
{Driver::Tail, 18, "T18", "PostFleetWarnings", PhaseStatus::Stub, ""},
|
||||||
{Driver::Tail, 19, "T19", "DrainInfraTerraformQueue", PhaseStatus::Stub, ""},
|
{Driver::Tail, 19, "T19", "DrainInfraTerraformQueue", PhaseStatus::Stub, ""},
|
||||||
{Driver::Tail, 20, "T20", "ScriptHooksTurnEnd", PhaseStatus::Stub, ""},
|
{Driver::Tail, 20, "T20", "ScriptHooksTurnEnd", PhaseStatus::Stub, ""},
|
||||||
{Driver::Tail, 21, "T21", "UpdateSurveyAndSystemStats", PhaseStatus::Stub, ""},
|
{Driver::Tail, 21, "T21", "UpdateSurveyAndSystemStats", PhaseStatus::Partial,
|
||||||
|
"the explored sweep IS modelled and committed: every player who can currently see a "
|
||||||
|
"system has now surveyed it. The event this owes per newly-surveyed pair is not "
|
||||||
|
"posted, and the derived per-system defence figure the same phase computes is not "
|
||||||
|
"modelled"},
|
||||||
{Driver::Tail, 22, "T22", "TradeSliderFinalisationSecondPass", PhaseStatus::Stub, ""},
|
{Driver::Tail, 22, "T22", "TradeSliderFinalisationSecondPass", PhaseStatus::Stub, ""},
|
||||||
{Driver::Tail, 23, "T23", "TradeManagerEndOfTurnHooks", PhaseStatus::Stub,
|
{Driver::Tail, 23, "T23", "TradeManagerEndOfTurnHooks", PhaseStatus::Stub,
|
||||||
"eight vtable calls, wholly unidentified"},
|
"eight vtable calls, wholly unidentified"},
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
#include "app/alliance.h"
|
#include "app/alliance.h"
|
||||||
#include "app/trade_raid.h"
|
#include "app/trade_raid.h"
|
||||||
#include "app/turn_record.h"
|
#include "app/turn_record.h"
|
||||||
|
#include "app/visibility_phase.h"
|
||||||
#include "game/sim/colony.h"
|
#include "game/sim/colony.h"
|
||||||
#include "game/sim/economy.h"
|
#include "game/sim/economy.h"
|
||||||
#include "game/sim/numeric.h"
|
#include "game/sim/numeric.h"
|
||||||
|
|
@ -594,6 +595,14 @@ TurnResult RunStrategicTurn(SaveGame& game, const TurnOptions& opt) {
|
||||||
playerDriverRan = true;
|
playerDriverRan = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 29: { // S29 SystemObservedStamp
|
||||||
|
const VisibilityPhaseResult v = RunSystemObservedStamp(game);
|
||||||
|
rec.invocations = v.systemsVisited;
|
||||||
|
rec.leafWrites = v.leafWrites;
|
||||||
|
rec.committed = v.leafWrites > 0;
|
||||||
|
rec.notes = v.notes;
|
||||||
|
break;
|
||||||
|
}
|
||||||
case 31: { // S31 EncounterDetectionAndStatusRestore
|
case 31: { // S31 EncounterDetectionAndStatusRestore
|
||||||
// Trade-raid generation runs FIRST inside this phase, before detection
|
// Trade-raid generation runs FIRST inside this phase, before detection
|
||||||
// proper, and it is the turn's dominant RNG consumer: two chances per entry
|
// proper, and it is the turn's dominant RNG consumer: two chances per entry
|
||||||
|
|
@ -693,6 +702,18 @@ TurnResult RunStrategicTurn(SaveGame& game, const TurnOptions& opt) {
|
||||||
rec.leafWrites = 1;
|
rec.leafWrites = 1;
|
||||||
rec.committed = true;
|
rec.committed = true;
|
||||||
rec.notes.push_back(fmt("ModCount -> %d", game.sim.modCount));
|
rec.notes.push_back(fmt("ModCount -> %d", game.sim.modCount));
|
||||||
|
} else if (tp[i].index == 17) {
|
||||||
|
const VisibilityPhaseResult v = RunObservationRecords(game);
|
||||||
|
rec.invocations = v.systemsVisited;
|
||||||
|
rec.leafWrites = v.leafWrites;
|
||||||
|
rec.committed = v.leafWrites > 0;
|
||||||
|
rec.notes = v.notes;
|
||||||
|
} else if (tp[i].index == 21) {
|
||||||
|
const VisibilityPhaseResult v = RunExploredSweep(game);
|
||||||
|
rec.invocations = v.systemsVisited;
|
||||||
|
rec.leafWrites = v.leafWrites;
|
||||||
|
rec.committed = v.leafWrites > 0;
|
||||||
|
rec.notes = v.notes;
|
||||||
} else if (tp[i].index == 36) {
|
} else if (tp[i].index == 36) {
|
||||||
RunFinalizeTurnRecords(game, opt, rec, recordAudit, allianceMasks);
|
RunFinalizeTurnRecords(game, opt, rec, recordAudit, allianceMasks);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
175
src/app/visibility_phase.cpp
Normal file
175
src/app/visibility_phase.cpp
Normal file
|
|
@ -0,0 +1,175 @@
|
||||||
|
#include "app/visibility_phase.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstdarg>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "game/sim/visibility.h"
|
||||||
|
|
||||||
|
namespace sots::app {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using mars::stream::shapes::SaveGame;
|
||||||
|
using mars::stream::shapes::Sys;
|
||||||
|
|
||||||
|
std::string fmt(const char* f, ...) {
|
||||||
|
char buf[512];
|
||||||
|
va_list ap;
|
||||||
|
va_start(ap, f);
|
||||||
|
std::vsnprintf(buf, sizeof buf, f, ap);
|
||||||
|
va_end(ap);
|
||||||
|
return std::string(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
sim::SystemMasks MasksOf(const Sys& s) {
|
||||||
|
sim::SystemMasks m;
|
||||||
|
m.seen = s.vFlags;
|
||||||
|
m.explored = s.eFlags;
|
||||||
|
m.active = s.aFlags;
|
||||||
|
m.presence = s.fFlags;
|
||||||
|
m.gate = s.gFlags;
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
// slot -> player handle id, sized to the highest slot the save uses. A slot no player
|
||||||
|
// occupies maps to 0 and is never observed, because no mask bit can be set for it.
|
||||||
|
std::vector<std::int32_t> SlotToPlayerId(const SaveGame& game) {
|
||||||
|
int maxSlot = -1;
|
||||||
|
for (const auto& e : game.sim.players) maxSlot = std::max(maxSlot, e.player.plyrIdx);
|
||||||
|
if (maxSlot < 0) return {};
|
||||||
|
std::vector<std::int32_t> out(static_cast<std::size_t>(maxSlot) + 1, 0);
|
||||||
|
for (const auto& e : game.sim.players) {
|
||||||
|
if (!sim::SlotRepresentable(e.player.plyrIdx)) continue;
|
||||||
|
out[static_cast<std::size_t>(e.player.plyrIdx)] = e.playerID;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
std::int32_t EncounterAtSystem(const SaveGame& game, std::int32_t systemId) {
|
||||||
|
for (const auto& f : game.sim.fleets) {
|
||||||
|
if (f.flt.locID != systemId) continue;
|
||||||
|
if (f.flt.ftEnc == 0) continue; // "no encounter"; the system defence fleet uses it
|
||||||
|
return f.flt.ftEnc;
|
||||||
|
}
|
||||||
|
return sim::kNoEncounter;
|
||||||
|
}
|
||||||
|
|
||||||
|
VisibilityPhaseResult RunSystemObservedStamp(SaveGame& game) {
|
||||||
|
VisibilityPhaseResult r;
|
||||||
|
int watched = 0;
|
||||||
|
for (auto& e : game.sim.systems) {
|
||||||
|
++r.systemsVisited;
|
||||||
|
if (e.sys.aFlags != 0) ++watched;
|
||||||
|
const sim::SystemStampResult s =
|
||||||
|
sim::UpdateSystemStamp(e.sys.ltis, e.sys.aFlags, game.sim.frame);
|
||||||
|
if (!s.changed) continue;
|
||||||
|
e.sys.ltis = s.stamp;
|
||||||
|
++r.systemsTouched;
|
||||||
|
++r.leafWrites;
|
||||||
|
}
|
||||||
|
r.notes.push_back(fmt("%d of %d system(s) are watched by someone; %d stamp(s) moved to %d",
|
||||||
|
watched, r.systemsVisited, r.leafWrites, game.sim.frame));
|
||||||
|
if (watched != r.systemsVisited)
|
||||||
|
r.notes.push_back("an unwatched system keeps the turn it was last watched -- the gate "
|
||||||
|
"is the ACTIVE mask, which falls when the last fleet leaves, not the "
|
||||||
|
"sticky one");
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
VisibilityPhaseResult RunExploredSweep(SaveGame& game) {
|
||||||
|
VisibilityPhaseResult r;
|
||||||
|
int newlyExplored = 0;
|
||||||
|
for (auto& e : game.sim.systems) {
|
||||||
|
++r.systemsVisited;
|
||||||
|
const std::int32_t next = sim::ApplyExploredSweep(e.sys.eFlags, e.sys.aFlags);
|
||||||
|
if (next == e.sys.eFlags) continue;
|
||||||
|
newlyExplored += static_cast<int>(
|
||||||
|
sim::NewlyExploredSlots(e.sys.eFlags, e.sys.aFlags).size());
|
||||||
|
e.sys.eFlags = next;
|
||||||
|
++r.systemsTouched;
|
||||||
|
++r.leafWrites;
|
||||||
|
}
|
||||||
|
r.notes.push_back(fmt("%d system(s) newly surveyed, across %d (system, player) pair(s)",
|
||||||
|
r.systemsTouched, newlyExplored));
|
||||||
|
if (newlyExplored)
|
||||||
|
r.notes.push_back(fmt("the original posts one event per pair, so this phase owes %d "
|
||||||
|
"event(s) it does not post",
|
||||||
|
newlyExplored));
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
VisibilityPhaseResult RunObservationRecords(SaveGame& game) {
|
||||||
|
VisibilityPhaseResult r;
|
||||||
|
const std::vector<std::int32_t> slotToId = SlotToPlayerId(game);
|
||||||
|
int created = 0, refreshed = 0, derivedEncounters = 0;
|
||||||
|
|
||||||
|
for (auto& e : game.sim.systems) {
|
||||||
|
++r.systemsVisited;
|
||||||
|
|
||||||
|
std::vector<sim::Observation> before;
|
||||||
|
before.reserve(e.sys.nve.size());
|
||||||
|
for (const auto& n : e.sys.nve) {
|
||||||
|
sim::Observation o;
|
||||||
|
// The wire carries the handle id; the map key is the slot. Recover the slot so
|
||||||
|
// the ordering the container imposes is reproducible.
|
||||||
|
o.playerId = n.ePid;
|
||||||
|
o.playerSlot = -1;
|
||||||
|
for (std::size_t slot = 0; slot < slotToId.size(); ++slot)
|
||||||
|
if (slotToId[slot] == n.ePid) o.playerSlot = static_cast<int>(slot);
|
||||||
|
o.turnSeen = n.ets;
|
||||||
|
o.encounterId = n.eid;
|
||||||
|
before.push_back(o);
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::int32_t enc = EncounterAtSystem(game, e.sysID);
|
||||||
|
if (enc != sim::kNoEncounter) ++derivedEncounters;
|
||||||
|
|
||||||
|
const sim::ObservationUpdate up = sim::UpdateObservations(
|
||||||
|
before, MasksOf(e.sys), slotToId, enc, game.sim.frame);
|
||||||
|
created += up.created;
|
||||||
|
refreshed += up.refreshed;
|
||||||
|
|
||||||
|
// Count the leaves that actually move. A created record adds four (the count and its
|
||||||
|
// three fields); a refreshed one moves only the fields that changed.
|
||||||
|
int moved = 0;
|
||||||
|
if (up.records.size() != e.sys.nve.size()) ++moved; // the count leaf
|
||||||
|
for (std::size_t i = 0; i < up.records.size(); ++i) {
|
||||||
|
const sim::Observation& o = up.records[i];
|
||||||
|
if (i >= e.sys.nve.size()) {
|
||||||
|
moved += 3;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const auto& old = e.sys.nve[i];
|
||||||
|
if (old.ePid != o.playerId) ++moved;
|
||||||
|
if (old.ets != o.turnSeen) ++moved;
|
||||||
|
if (old.eid != o.encounterId) ++moved;
|
||||||
|
}
|
||||||
|
if (moved == 0) continue;
|
||||||
|
|
||||||
|
e.sys.nve.clear();
|
||||||
|
e.sys.nve.reserve(up.records.size());
|
||||||
|
for (const auto& o : up.records) {
|
||||||
|
mars::stream::shapes::NveEntry n;
|
||||||
|
n.ePid = o.playerId;
|
||||||
|
n.ets = o.turnSeen;
|
||||||
|
n.eid = o.encounterId;
|
||||||
|
e.sys.nve.push_back(n);
|
||||||
|
}
|
||||||
|
++r.systemsTouched;
|
||||||
|
r.leafWrites += moved;
|
||||||
|
}
|
||||||
|
|
||||||
|
r.notes.push_back(fmt("%d record(s) created, %d refreshed, over %d system(s); %d leaf/leaves",
|
||||||
|
created, refreshed, r.systemsTouched, r.leafWrites));
|
||||||
|
r.notes.push_back(fmt("%d system(s) carry an encounter fleet whose id the record copies -- "
|
||||||
|
"the field the original reads is NOT on the wire, so this derivation "
|
||||||
|
"is a HYPOTHESIS and no save in the corpus can separate it from the "
|
||||||
|
"rule it stands in for",
|
||||||
|
derivedEncounters));
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace sots::app
|
||||||
55
src/app/visibility_phase.h
Normal file
55
src/app/visibility_phase.h
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
// The visibility phases, wired to the save shapes.
|
||||||
|
//
|
||||||
|
// Three phases of the turn touch the per-system visibility cluster, in two different
|
||||||
|
// drivers, and they are listed here together because they share one input -- the system's
|
||||||
|
// active-presence mask -- and nothing else in the turn reads it.
|
||||||
|
//
|
||||||
|
// S29 the system's own last-observed stamp
|
||||||
|
// T17 the per-(system, player) observation record
|
||||||
|
// T21 the explored sweep
|
||||||
|
//
|
||||||
|
// The mask itself is READ FROM THE SAVE and never rebuilt. The original recomputes it on
|
||||||
|
// every fleet arrival and departure, which the standalone does not model; but neither
|
||||||
|
// reference pair moves a mask leaf, so the loaded value is the value these phases would
|
||||||
|
// see, and rebuilding it from an unmodelled movement pass would be a change with no
|
||||||
|
// evidence behind it. When that stops being true it will show up as a regression here
|
||||||
|
// first, which is the point of saying it out loud.
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "mars/stream/shapes.h"
|
||||||
|
|
||||||
|
namespace sots::app {
|
||||||
|
|
||||||
|
// What one visibility phase did, in the terms the run log prints.
|
||||||
|
struct VisibilityPhaseResult {
|
||||||
|
int systemsVisited = 0; // systems the phase looked at
|
||||||
|
int systemsTouched = 0; // systems where something moved
|
||||||
|
int leafWrites = 0; // save leaves this phase changed
|
||||||
|
std::vector<std::string> notes;
|
||||||
|
};
|
||||||
|
|
||||||
|
// S29: `ltis`, the system's own last-observed turn. Written on every system some player can
|
||||||
|
// currently see; left alone on the rest.
|
||||||
|
VisibilityPhaseResult RunSystemObservedStamp(mars::stream::shapes::SaveGame& game);
|
||||||
|
|
||||||
|
// T21: the explored sweep -- every player who can see a system has now surveyed it.
|
||||||
|
VisibilityPhaseResult RunExploredSweep(mars::stream::shapes::SaveGame& game);
|
||||||
|
|
||||||
|
// T17: the per-(system, player) observation record.
|
||||||
|
VisibilityPhaseResult RunObservationRecords(mars::stream::shapes::SaveGame& game);
|
||||||
|
|
||||||
|
// The encounter parked at a system, as this model can recover it.
|
||||||
|
//
|
||||||
|
// The field the original reads is a star-system member that is NOT serialised: it is set
|
||||||
|
// once when the map is generated and never moves. The only observable that carries the same
|
||||||
|
// id is the encounter fleet sitting at the system, so that is what this reads -- and it is a
|
||||||
|
// HYPOTHESIS, not a reading. It agrees with the original on all six encounter fleets in the
|
||||||
|
// corpus and no save can separate it from the true rule, because no save kills an encounter
|
||||||
|
// while leaving its system visible. Returns -1 when there is none.
|
||||||
|
std::int32_t EncounterAtSystem(const mars::stream::shapes::SaveGame& game,
|
||||||
|
std::int32_t systemId);
|
||||||
|
|
||||||
|
} // namespace sots::app
|
||||||
|
|
@ -7,6 +7,7 @@ add_library(sots_game_sim STATIC
|
||||||
research.cpp
|
research.cpp
|
||||||
colony.cpp
|
colony.cpp
|
||||||
movement.cpp
|
movement.cpp
|
||||||
|
visibility.cpp
|
||||||
techgraph.cpp)
|
techgraph.cpp)
|
||||||
target_include_directories(sots_game_sim PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/../..)
|
target_include_directories(sots_game_sim PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/../..)
|
||||||
target_compile_features(sots_game_sim PUBLIC cxx_std_17)
|
target_compile_features(sots_game_sim PUBLIC cxx_std_17)
|
||||||
|
|
@ -17,7 +18,7 @@ endif()
|
||||||
option(SOTS_GAME_SIM_TESTS "Build the game/sim unit tests" OFF)
|
option(SOTS_GAME_SIM_TESTS "Build the game/sim unit tests" OFF)
|
||||||
if(SOTS_GAME_SIM_TESTS)
|
if(SOTS_GAME_SIM_TESTS)
|
||||||
enable_testing()
|
enable_testing()
|
||||||
set(_sim_tests economy research colony movement techgraph)
|
set(_sim_tests economy research colony movement techgraph visibility)
|
||||||
foreach(_t IN LISTS _sim_tests)
|
foreach(_t IN LISTS _sim_tests)
|
||||||
add_executable(game_sim_test_${_t} ${CMAKE_CURRENT_SOURCE_DIR}/../../../tests/game_sim/test_${_t}.cpp)
|
add_executable(game_sim_test_${_t} ${CMAKE_CURRENT_SOURCE_DIR}/../../../tests/game_sim/test_${_t}.cpp)
|
||||||
target_link_libraries(game_sim_test_${_t} PRIVATE sots_game_sim)
|
target_link_libraries(game_sim_test_${_t} PRIVATE sots_game_sim)
|
||||||
|
|
|
||||||
78
src/game/sim/visibility.cpp
Normal file
78
src/game/sim/visibility.cpp
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
#include "game/sim/visibility.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace sots::sim {
|
||||||
|
|
||||||
|
std::vector<int> NewlyExploredSlots(std::int32_t explored, std::int32_t active) {
|
||||||
|
std::vector<int> out;
|
||||||
|
const std::uint32_t newly = static_cast<std::uint32_t>(active) & ~static_cast<std::uint32_t>(explored);
|
||||||
|
for (int slot = 0; slot < 32; ++slot)
|
||||||
|
if ((newly >> slot) & 1u) out.push_back(slot);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
SystemStampResult UpdateSystemStamp(std::int32_t currentStamp, std::int32_t activeMask,
|
||||||
|
std::int32_t turn) {
|
||||||
|
SystemStampResult r;
|
||||||
|
r.stamp = currentStamp;
|
||||||
|
if (activeMask == 0) return r; // nobody is watching; the old stamp stands
|
||||||
|
r.stamp = turn;
|
||||||
|
r.changed = (turn != currentStamp);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
ObservationUpdate UpdateObservations(const std::vector<Observation>& existing,
|
||||||
|
const SystemMasks& masks,
|
||||||
|
const std::vector<std::int32_t>& slotToId,
|
||||||
|
std::int32_t encounterId, std::int32_t turn) {
|
||||||
|
ObservationUpdate up;
|
||||||
|
up.records = existing;
|
||||||
|
|
||||||
|
const int slotCount = static_cast<int>(slotToId.size());
|
||||||
|
for (int slot = 0; slot < slotCount; ++slot) {
|
||||||
|
if (!IsObservedBy(masks, slot)) continue;
|
||||||
|
|
||||||
|
auto it = std::find_if(up.records.begin(), up.records.end(),
|
||||||
|
[slot](const Observation& o) { return o.playerSlot == slot; });
|
||||||
|
if (it == up.records.end()) {
|
||||||
|
Observation o;
|
||||||
|
o.playerSlot = slot;
|
||||||
|
o.playerId = slotToId[static_cast<std::size_t>(slot)];
|
||||||
|
o.turnSeen = turn;
|
||||||
|
o.encounterId = encounterId;
|
||||||
|
up.records.push_back(o);
|
||||||
|
++up.created;
|
||||||
|
++up.refreshed;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// The encounter id is a property of the system, fixed for the life of the game, so a
|
||||||
|
// record that already carries one keeps it. Re-deriving it here from what happens to
|
||||||
|
// be at the system this turn would be a different rule with the same result on every
|
||||||
|
// save we hold, and it would silently disagree the first time an encounter dies.
|
||||||
|
const std::int32_t keptEncounter =
|
||||||
|
it->encounterId != kNoEncounter ? it->encounterId : encounterId;
|
||||||
|
if (it->turnSeen == turn && it->encounterId == keptEncounter) {
|
||||||
|
++up.alreadyCurrent;
|
||||||
|
} else {
|
||||||
|
it->turnSeen = turn;
|
||||||
|
it->encounterId = keptEncounter;
|
||||||
|
++up.refreshed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
up.untouched = static_cast<int>(up.records.size()) - up.refreshed - up.alreadyCurrent;
|
||||||
|
|
||||||
|
std::stable_sort(up.records.begin(), up.records.end(),
|
||||||
|
[](const Observation& a, const Observation& b) {
|
||||||
|
return a.playerSlot < b.playerSlot;
|
||||||
|
});
|
||||||
|
return up;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ShouldShareObservation(const Observation* from, const Observation* to) {
|
||||||
|
if (from == nullptr) return false;
|
||||||
|
if (to == nullptr) return true;
|
||||||
|
return to->turnSeen < from->turnSeen;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace sots::sim
|
||||||
146
src/game/sim/visibility.h
Normal file
146
src/game/sim/visibility.h
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
// Per-system visibility: who can see a system this turn, what they remember about it, and
|
||||||
|
// the two turn stamps that record it.
|
||||||
|
//
|
||||||
|
// The game keeps three per-(system, player) memories side by side on the star system --
|
||||||
|
// what the player last saw of the OWNER, what they last saw of the ENCOUNTER there, and
|
||||||
|
// what they last saw of the colony's NUMBERS. This module models the second, plus the two
|
||||||
|
// masks and the system-level stamp that move with it. It is pure: no save shapes, no I/O.
|
||||||
|
//
|
||||||
|
// The load-bearing fact, and the one the corpus can check, is the GATE. Four per-player
|
||||||
|
// masks live on a star system and three of them are equal on every system of every save
|
||||||
|
// this project holds, so a model built on the wrong one looks correct until it does not.
|
||||||
|
//
|
||||||
|
// presence -- the player has a fleet at the system
|
||||||
|
// gate -- the player has a gate there
|
||||||
|
// active -- the derived union `presence | gate | isOwner`, recomputed on every fleet
|
||||||
|
// arrival and departure, so it FALLS when the last fleet leaves
|
||||||
|
// seen -- the same union but sticky: only ever OR'd, never cleared
|
||||||
|
// explored -- ever surveyed
|
||||||
|
//
|
||||||
|
// The observation record is refreshed under `active`, not under `seen`. One save in the
|
||||||
|
// corpus separates them: a system whose last fleet left carries `seen` set, `active` clear,
|
||||||
|
// and a stamp frozen at the previous turn. That save is the test.
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace sots::sim {
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------
|
||||||
|
// The masks
|
||||||
|
// ---------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// The per-player masks a star system carries. Bit index is the player's slot index, not its
|
||||||
|
// handle id, and not its position in the server's player vector.
|
||||||
|
struct SystemMasks {
|
||||||
|
std::int32_t seen = 0; // sticky union
|
||||||
|
std::int32_t explored = 0; // ever surveyed
|
||||||
|
std::int32_t active = 0; // presence | gate | isOwner, recomputed, non-sticky
|
||||||
|
std::int32_t presence = 0; // a fleet is here
|
||||||
|
std::int32_t gate = 0; // a gate is here
|
||||||
|
};
|
||||||
|
|
||||||
|
// Bit for a player slot. Slots at or above 32 cannot be represented; the original packs the
|
||||||
|
// same masks into a 32-bit int and separately caps a runtime companion at 15 players, so a
|
||||||
|
// slot outside the range is not a case this can encode and the caller is told so.
|
||||||
|
constexpr bool SlotRepresentable(int playerSlot) { return playerSlot >= 0 && playerSlot < 32; }
|
||||||
|
constexpr std::int32_t SlotBit(int playerSlot) {
|
||||||
|
return SlotRepresentable(playerSlot) ? static_cast<std::int32_t>(1u << playerSlot) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is the system currently observed by this player? This is the gate on the observation
|
||||||
|
// record and on the system stamp: the ACTIVE mask, not the sticky one.
|
||||||
|
constexpr bool IsObservedBy(const SystemMasks& m, int playerSlot) {
|
||||||
|
return SlotRepresentable(playerSlot) && (m.active & SlotBit(playerSlot)) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is the system explored by this player?
|
||||||
|
constexpr bool IsExploredBy(const SystemMasks& m, int playerSlot) {
|
||||||
|
return SlotRepresentable(playerSlot) && (m.explored & SlotBit(playerSlot)) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The active mask as the original recomputes it on every fleet arrival and departure. Kept
|
||||||
|
// here so the invariant is stated in one place even though the standalone reads the mask
|
||||||
|
// from the save rather than rebuilding it -- the two reference pairs move no mask leaf, so
|
||||||
|
// the loaded value is the value the phase would see, and a rebuild would be a change with
|
||||||
|
// no evidence behind it.
|
||||||
|
constexpr std::int32_t RecomputeActive(std::int32_t presence, std::int32_t gate,
|
||||||
|
std::int32_t ownerBit) {
|
||||||
|
return presence | gate | ownerBit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The explored sweep, once per turn: every player who can currently see the system has now
|
||||||
|
// surveyed it. Returns the new mask; the caller compares to know whether a leaf moved.
|
||||||
|
constexpr std::int32_t ApplyExploredSweep(std::int32_t explored, std::int32_t active) {
|
||||||
|
return explored | active;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Which slots this sweep newly sets -- the ones that would each post an "explored" event.
|
||||||
|
std::vector<int> NewlyExploredSlots(std::int32_t explored, std::int32_t active);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------
|
||||||
|
// The system-level stamp
|
||||||
|
// ---------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// The system's own "last observed" turn. Written when ANY player can see it -- the gate is
|
||||||
|
// the whole active mask being non-zero, not a particular player's bit -- and left alone
|
||||||
|
// otherwise, so a system nobody watches keeps the turn it was last watched.
|
||||||
|
struct SystemStampResult {
|
||||||
|
std::int32_t stamp = 0;
|
||||||
|
bool changed = false;
|
||||||
|
};
|
||||||
|
SystemStampResult UpdateSystemStamp(std::int32_t currentStamp, std::int32_t activeMask,
|
||||||
|
std::int32_t turn);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------
|
||||||
|
// The per-(system, player) observation record
|
||||||
|
// ---------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// No encounter is present. The system field this mirrors is constructed to this value and
|
||||||
|
// the map lookup returns it for a player who has no record.
|
||||||
|
constexpr std::int32_t kNoEncounter = -1;
|
||||||
|
|
||||||
|
// One remembered sighting. `playerSlot` is the map key and orders the list; `playerId` is
|
||||||
|
// what the wire carries in its place.
|
||||||
|
struct Observation {
|
||||||
|
int playerSlot = 0;
|
||||||
|
std::int32_t playerId = 0;
|
||||||
|
std::int32_t turnSeen = 0;
|
||||||
|
std::int32_t encounterId = kNoEncounter;
|
||||||
|
};
|
||||||
|
|
||||||
|
// What one system's observation list should be after this turn's pass.
|
||||||
|
//
|
||||||
|
// `existing` is the list as loaded, `encounterId` the encounter parked at the system, and
|
||||||
|
// `slotToId` maps a player slot to the handle id the wire carries. A player who can see the
|
||||||
|
// system gets a refreshed record; a player who cannot keeps whatever it had. Nothing is ever
|
||||||
|
// removed: the original's map has exactly three callers of `operator[]` and none of them
|
||||||
|
// erases, which is what makes this a memory rather than a state.
|
||||||
|
//
|
||||||
|
// The result is ordered by player slot ascending, because the original stores it in a tree
|
||||||
|
// keyed by that slot. Every system in the corpus has at most one entry, so the ordering is
|
||||||
|
// asserted from the container and not from evidence.
|
||||||
|
struct ObservationUpdate {
|
||||||
|
std::vector<Observation> records;
|
||||||
|
int refreshed = 0; // records whose stamp was rewritten
|
||||||
|
int created = 0; // of those, ones that did not exist before
|
||||||
|
int alreadyCurrent = 0; // observed this turn and already carrying this turn's stamp
|
||||||
|
int untouched = 0; // not observed this turn; kept exactly as loaded
|
||||||
|
};
|
||||||
|
|
||||||
|
ObservationUpdate UpdateObservations(const std::vector<Observation>& existing,
|
||||||
|
const SystemMasks& masks,
|
||||||
|
const std::vector<std::int32_t>& slotToId,
|
||||||
|
std::int32_t encounterId, std::int32_t turn);
|
||||||
|
|
||||||
|
// The intel-sharing rule: a player may be handed another player's record when the other's
|
||||||
|
// sighting is newer, or when it has none of its own. The stamp that is copied is the
|
||||||
|
// SIGHTING stamp, not the current turn -- the receiver learns what the donor saw, and when.
|
||||||
|
//
|
||||||
|
// Nothing in the corpus has ever executed this: no save has two players in an alliance. It
|
||||||
|
// is here because the rule is short, it is read, and leaving it out would misrepresent the
|
||||||
|
// record as single-writer.
|
||||||
|
bool ShouldShareObservation(const Observation* from, const Observation* to);
|
||||||
|
|
||||||
|
} // namespace sots::sim
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
# game/sim tests: four hand-computed suites + a real-save smoke test (skips unless SOTS_SAVES_JSON).
|
# game/sim tests: four hand-computed suites + a real-save smoke test (skips unless SOTS_SAVES_JSON).
|
||||||
foreach(_t economy research colony movement techgraph)
|
foreach(_t economy research colony movement techgraph visibility)
|
||||||
add_executable(game_sim_test_${_t} test_${_t}.cpp)
|
add_executable(game_sim_test_${_t} test_${_t}.cpp)
|
||||||
target_link_libraries(game_sim_test_${_t} PRIVATE sots_game_sim)
|
target_link_libraries(game_sim_test_${_t} PRIVATE sots_game_sim)
|
||||||
target_include_directories(game_sim_test_${_t} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
target_include_directories(game_sim_test_${_t} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
|
|
|
||||||
203
tests/game_sim/test_visibility.cpp
Normal file
203
tests/game_sim/test_visibility.cpp
Normal file
|
|
@ -0,0 +1,203 @@
|
||||||
|
#include "game/sim/visibility.h"
|
||||||
|
|
||||||
|
#include "check.h"
|
||||||
|
|
||||||
|
using namespace sots::sim;
|
||||||
|
|
||||||
|
static SystemMasks masks(std::int32_t seen, std::int32_t explored, std::int32_t active,
|
||||||
|
std::int32_t presence, std::int32_t gate) {
|
||||||
|
SystemMasks m;
|
||||||
|
m.seen = seen;
|
||||||
|
m.explored = explored;
|
||||||
|
m.active = active;
|
||||||
|
m.presence = presence;
|
||||||
|
m.gate = gate;
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_bits() {
|
||||||
|
CHECK(SlotRepresentable(0));
|
||||||
|
CHECK(SlotRepresentable(31));
|
||||||
|
CHECK(!SlotRepresentable(-1));
|
||||||
|
CHECK(!SlotRepresentable(32));
|
||||||
|
CHECK_EQ(SlotBit(0), std::int32_t{1});
|
||||||
|
CHECK_EQ(SlotBit(4), std::int32_t{16});
|
||||||
|
CHECK_EQ(SlotBit(7), std::int32_t{128});
|
||||||
|
CHECK_EQ(SlotBit(32), std::int32_t{0});
|
||||||
|
}
|
||||||
|
|
||||||
|
// The gate is the ACTIVE mask. Three masks agree on nearly every system of every save the
|
||||||
|
// project holds, so this pins the one thing the corpus almost cannot show.
|
||||||
|
static void test_gate_is_the_active_mask() {
|
||||||
|
// seen and explored set, active clear: this is the shape a system takes after its last
|
||||||
|
// visiting fleet leaves. It must NOT be treated as observed.
|
||||||
|
const SystemMasks m = masks(/*seen*/ 2, /*explored*/ 2, /*active*/ 0, 0, 0);
|
||||||
|
CHECK(!IsObservedBy(m, 1));
|
||||||
|
CHECK(IsExploredBy(m, 1));
|
||||||
|
|
||||||
|
const SystemMasks live = masks(2, 2, 2, 2, 0);
|
||||||
|
CHECK(IsObservedBy(live, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_active_recompute() {
|
||||||
|
// presence | gate | owner, and the owner term is a bit, not a flag.
|
||||||
|
CHECK_EQ(RecomputeActive(2, 0, 0), std::int32_t{2});
|
||||||
|
CHECK_EQ(RecomputeActive(0, 4, 0), std::int32_t{4});
|
||||||
|
CHECK_EQ(RecomputeActive(0, 0, 16), std::int32_t{16});
|
||||||
|
CHECK_EQ(RecomputeActive(2, 4, 16), std::int32_t{22});
|
||||||
|
CHECK_EQ(RecomputeActive(0, 0, 0), std::int32_t{0});
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_explored_sweep() {
|
||||||
|
CHECK_EQ(ApplyExploredSweep(0, 16), std::int32_t{16});
|
||||||
|
CHECK_EQ(ApplyExploredSweep(1, 1), std::int32_t{1}); // idempotent
|
||||||
|
CHECK_EQ(ApplyExploredSweep(2, 0), std::int32_t{2}); // never cleared
|
||||||
|
CHECK_EQ(ApplyExploredSweep(1, 16), std::int32_t{17}); // accumulates
|
||||||
|
|
||||||
|
const std::vector<int> newly = NewlyExploredSlots(1, 0x11);
|
||||||
|
CHECK_EQ(newly.size(), std::size_t{1});
|
||||||
|
CHECK_EQ(newly[0], 4);
|
||||||
|
CHECK_EQ(NewlyExploredSlots(0x11, 0x11).size(), std::size_t{0});
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_system_stamp() {
|
||||||
|
// Watched: the stamp becomes this turn.
|
||||||
|
SystemStampResult r = UpdateSystemStamp(1, /*active*/ 16, /*turn*/ 2);
|
||||||
|
CHECK_EQ(r.stamp, std::int32_t{2});
|
||||||
|
CHECK(r.changed);
|
||||||
|
|
||||||
|
// Already current: no leaf moves.
|
||||||
|
r = UpdateSystemStamp(2, 16, 2);
|
||||||
|
CHECK_EQ(r.stamp, std::int32_t{2});
|
||||||
|
CHECK(!r.changed);
|
||||||
|
|
||||||
|
// Unwatched: the old stamp stands. This is the Bismol case at the system level.
|
||||||
|
r = UpdateSystemStamp(22, /*active*/ 0, /*turn*/ 23);
|
||||||
|
CHECK_EQ(r.stamp, std::int32_t{22});
|
||||||
|
CHECK(!r.changed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The corpus's one discriminating row, replayed as a rule.
|
||||||
|
//
|
||||||
|
// `zuul-turn23-fleet23.sav`, system "Bismol": the visiting fleet has gone, so the sticky and
|
||||||
|
// explored masks still carry the player's bit while the active mask does not, and the saved
|
||||||
|
// record's stamp is 22 while the save is turn 23. A model gated on the sticky mask -- or on
|
||||||
|
// explored -- refreshes it to 23 and is wrong. A model gated on active leaves it alone.
|
||||||
|
static void test_bismol_freeze() {
|
||||||
|
std::vector<Observation> before;
|
||||||
|
Observation o;
|
||||||
|
o.playerSlot = 1;
|
||||||
|
o.playerId = 32;
|
||||||
|
o.turnSeen = 22;
|
||||||
|
o.encounterId = kNoEncounter;
|
||||||
|
before.push_back(o);
|
||||||
|
|
||||||
|
const std::vector<std::int32_t> slots = {16, 32, 0, 512, 0, 0, 0};
|
||||||
|
const ObservationUpdate up =
|
||||||
|
UpdateObservations(before, masks(/*seen*/ 2, /*explored*/ 2, /*active*/ 0, 0, 0), slots,
|
||||||
|
kNoEncounter, /*turn*/ 23);
|
||||||
|
|
||||||
|
CHECK_EQ(up.records.size(), std::size_t{1});
|
||||||
|
CHECK_EQ(up.records[0].turnSeen, std::int32_t{22}); // FROZEN
|
||||||
|
CHECK_EQ(up.refreshed, 0);
|
||||||
|
CHECK_EQ(up.created, 0);
|
||||||
|
CHECK_EQ(up.untouched, 1);
|
||||||
|
|
||||||
|
// The same row under the wrong gate, stated so the failure mode is visible: had the model
|
||||||
|
// used the sticky mask the record would have moved to 23.
|
||||||
|
const ObservationUpdate wrong =
|
||||||
|
UpdateObservations(before, masks(2, 2, /*active*/ 2, 0, 0), slots, kNoEncounter, 23);
|
||||||
|
CHECK_EQ(wrong.records[0].turnSeen, std::int32_t{23});
|
||||||
|
CHECK_EQ(wrong.refreshed, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_creation_and_refresh() {
|
||||||
|
const std::vector<std::int32_t> slots = {16, 32, 0, 512, 528, 0, 0, 576};
|
||||||
|
|
||||||
|
// Turn 1 -> 2 at Hyperion: no record, active mask carries slot 4, an encounter is there.
|
||||||
|
ObservationUpdate up = UpdateObservations({}, masks(16, 0, 16, 16, 0), slots,
|
||||||
|
/*encounter*/ 5, /*turn*/ 2);
|
||||||
|
CHECK_EQ(up.records.size(), std::size_t{1});
|
||||||
|
CHECK_EQ(up.created, 1);
|
||||||
|
CHECK_EQ(up.records[0].playerSlot, 4);
|
||||||
|
CHECK_EQ(up.records[0].playerId, std::int32_t{528}); // the HANDLE id, not the slot
|
||||||
|
CHECK_EQ(up.records[0].turnSeen, std::int32_t{2});
|
||||||
|
CHECK_EQ(up.records[0].encounterId, std::int32_t{5});
|
||||||
|
|
||||||
|
// Turn 2 -> 3 at the same system: the stamp moves, the encounter id does not.
|
||||||
|
const ObservationUpdate next =
|
||||||
|
UpdateObservations(up.records, masks(16, 16, 16, 16, 0), slots, 5, 3);
|
||||||
|
CHECK_EQ(next.records.size(), std::size_t{1});
|
||||||
|
CHECK_EQ(next.created, 0);
|
||||||
|
CHECK_EQ(next.refreshed, 1);
|
||||||
|
CHECK_EQ(next.records[0].turnSeen, std::int32_t{3});
|
||||||
|
CHECK_EQ(next.records[0].encounterId, std::int32_t{5});
|
||||||
|
CHECK_EQ(next.records[0].playerId, std::int32_t{528});
|
||||||
|
|
||||||
|
// A system nobody can see gains nothing. This is Spica: a colony record exists, the
|
||||||
|
// active mask is zero, and no observation record is ever created.
|
||||||
|
const ObservationUpdate none =
|
||||||
|
UpdateObservations({}, masks(0, 0, 0, 0, 0), slots, kNoEncounter, 2);
|
||||||
|
CHECK_EQ(none.records.size(), std::size_t{0});
|
||||||
|
CHECK_EQ(none.created, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// An existing record keeps its encounter id even if nothing derivable is at the system now.
|
||||||
|
// The field the original reads is fixed at map generation; re-deriving it every turn is a
|
||||||
|
// different rule that no save can separate, so the one that cannot lose information is used.
|
||||||
|
static void test_encounter_id_is_sticky() {
|
||||||
|
std::vector<Observation> before;
|
||||||
|
Observation o;
|
||||||
|
o.playerSlot = 4;
|
||||||
|
o.playerId = 528;
|
||||||
|
o.turnSeen = 2;
|
||||||
|
o.encounterId = 5;
|
||||||
|
before.push_back(o);
|
||||||
|
const std::vector<std::int32_t> slots = {16, 32, 0, 512, 528};
|
||||||
|
|
||||||
|
const ObservationUpdate up = UpdateObservations(
|
||||||
|
before, masks(16, 16, 16, 16, 0), slots, /*nothing derivable now*/ kNoEncounter, 3);
|
||||||
|
CHECK_EQ(up.records[0].encounterId, std::int32_t{5});
|
||||||
|
CHECK_EQ(up.records[0].turnSeen, std::int32_t{3});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Two observers of one system. Never exercised by any save in the corpus -- every system in
|
||||||
|
// it has a single-bit active mask -- so this pins the ordering the container imposes rather
|
||||||
|
// than a behaviour anyone has seen.
|
||||||
|
static void test_two_observers_order_by_slot() {
|
||||||
|
const std::vector<std::int32_t> slots = {16, 32, 0, 512, 528, 0, 0, 576};
|
||||||
|
// slots 7 and 1 both observe; the list must come out 1 then 7.
|
||||||
|
const ObservationUpdate up =
|
||||||
|
UpdateObservations({}, masks(0x82, 0, 0x82, 0x82, 0), slots, kNoEncounter, 4);
|
||||||
|
CHECK_EQ(up.records.size(), std::size_t{2});
|
||||||
|
CHECK_EQ(up.records[0].playerSlot, 1);
|
||||||
|
CHECK_EQ(up.records[0].playerId, std::int32_t{32});
|
||||||
|
CHECK_EQ(up.records[1].playerSlot, 7);
|
||||||
|
CHECK_EQ(up.records[1].playerId, std::int32_t{576});
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_sharing_rule() {
|
||||||
|
Observation a, b;
|
||||||
|
a.turnSeen = 10;
|
||||||
|
b.turnSeen = 8;
|
||||||
|
CHECK(ShouldShareObservation(&a, &b)); // newer donor wins
|
||||||
|
CHECK(!ShouldShareObservation(&b, &a)); // older donor does not
|
||||||
|
CHECK(ShouldShareObservation(&a, nullptr));
|
||||||
|
CHECK(!ShouldShareObservation(nullptr, &a));
|
||||||
|
b.turnSeen = 10;
|
||||||
|
CHECK(!ShouldShareObservation(&a, &b)); // equal is not newer
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
test_bits();
|
||||||
|
test_gate_is_the_active_mask();
|
||||||
|
test_active_recompute();
|
||||||
|
test_explored_sweep();
|
||||||
|
test_system_stamp();
|
||||||
|
test_bismol_freeze();
|
||||||
|
test_creation_and_refresh();
|
||||||
|
test_encounter_id_is_sticky();
|
||||||
|
test_two_observers_order_by_slot();
|
||||||
|
test_sharing_rule();
|
||||||
|
return simtest::finish("visibility");
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue