Closed 2 on the reference pair and 4 on pair 2, regressed 0, with no operator input. Measured, both pairs, never netted. - S00 stamps PvSav from Sav on every live player, before any phase can move it. 0 closed on pair 1 (a no-op there), 2 on pair 2. - T31 recovers the per-player difficulty column by recomputing BnkEl from the colony state the input save was written from and comparing against the BnkEl the save carries. That removes the --ai-player flag as a blocker and turns the phase's self-check into a real one: it used to compare its POST-turn result against the PRE-turn stored value, so its 6-of-8 only ever covered the six players whose limit does not move. The load-time check passes for every live player of all eleven corpus saves. T31 Blocked -> Partial; BnkPr still needs the tuning constant. - BANKRUPTCY_PROTECTION_LIMIT_FACTOR is multiplied in as fmul dword ptr, so it is a float32 in the image; the engine narrows it now. Zero leaves move on this corpus -- all seven of its BnkPr records land where the two constants agree -- and three hand-written test expectations moved (rule 23). docs/PL-players-residual.md carries the decomposition, the predictions written before the build, where they were wrong, and the ranked remainder.
88 lines
3.6 KiB
C++
88 lines
3.6 KiB
C++
#include "game/sim/player_turn.h"
|
|
|
|
#include "check.h"
|
|
|
|
using namespace sots::sim;
|
|
|
|
static TuningTable tuning() {
|
|
TuningTable t;
|
|
t.BANKRUPTCY_PROTECTION_LIMIT_FACTOR = 3.3;
|
|
return t;
|
|
}
|
|
|
|
static void test_snapshot() {
|
|
// S00 is a plain copy, and the point of the test is that it is a copy of the value
|
|
// BEFORE the turn, so the identity has to hold for the negative and the saturated cases
|
|
// too rather than only for the corpus' positive treasuries.
|
|
CHECK_EQ(SnapshotPreviousTurn(50000), 50000);
|
|
CHECK_EQ(SnapshotPreviousTurn(0), 0);
|
|
CHECK_EQ(SnapshotPreviousTurn(-1234), -1234);
|
|
CHECK_EQ(SnapshotPreviousTurn(2000000000), 2000000000);
|
|
}
|
|
|
|
// The corpus' two real empires, with the max incomes recovered by inverting the `BnkEl` each
|
|
// save carries. Player 0 is not AI; player 1 is, and its income therefore carries the x1.1.
|
|
static void test_column_identified_from_the_stored_limit() {
|
|
const TuningTable t = tuning();
|
|
|
|
// turn1-state, player 0 ("re"): stored BnkEl -1,590,613, max income 238,592.
|
|
DifficultyColumnEvidence e =
|
|
IdentifyDifficultyColumn(-1590613, 238592, 262451, t);
|
|
CHECK(e.column == DifficultyColumn::NonAI);
|
|
CHECK(!e.IsAI());
|
|
CHECK(e.Reproduced());
|
|
|
|
// turn1-state, player 1 ("Fane Lao"): stored -1,811,273, max income 271,691 -- which is
|
|
// what the AI column produces; the non-AI column would produce 271691 / 1.1.
|
|
e = IdentifyDifficultyColumn(-1811273, 246992, 271691, t);
|
|
CHECK(e.column == DifficultyColumn::AI);
|
|
CHECK(e.IsAI());
|
|
CHECK(e.Reproduced());
|
|
}
|
|
|
|
static void test_zero_income_is_ambiguous_not_identified() {
|
|
const TuningTable t = tuning();
|
|
// Five of the corpus' eight players own nothing. Both columns reproduce the stored zero,
|
|
// and the honest answer is "the column is unknown and cannot matter" -- not "non-AI".
|
|
const DifficultyColumnEvidence e = IdentifyDifficultyColumn(0, 0, 0, t);
|
|
CHECK(e.column == DifficultyColumn::Ambiguous);
|
|
CHECK(!e.IsAI());
|
|
CHECK(e.Reproduced());
|
|
CHECK_EQ(e.nonAiLimit, 0);
|
|
CHECK_EQ(e.aiLimit, 0);
|
|
}
|
|
|
|
static void test_neither_column_fits_is_unidentified() {
|
|
const TuningTable t = tuning();
|
|
// A stored limit no column reproduces means the max-income chain is wrong for this
|
|
// player, and the caller must abstain rather than pick the nearer of two wrong answers.
|
|
const DifficultyColumnEvidence e = IdentifyDifficultyColumn(-1000000, 238592, 262451, t);
|
|
CHECK(e.column == DifficultyColumn::Unidentified);
|
|
CHECK(!e.IsAI());
|
|
CHECK(!e.Reproduced());
|
|
}
|
|
|
|
// The identification only works because the two columns are far apart. A tenth of the income
|
|
// is thousands of units of BnkEl at any empire size that matters, so a truncation can never
|
|
// make the wrong column fit -- but at a max income of a handful of units they collapse, and
|
|
// the answer there is Ambiguous, which is correct and is worth pinning.
|
|
static void test_the_two_columns_collapse_only_at_a_trivial_income() {
|
|
const TuningTable t = tuning();
|
|
DifficultyColumnEvidence e = IdentifyDifficultyColumn(-6, 1, 1, t);
|
|
CHECK(e.column == DifficultyColumn::Ambiguous);
|
|
|
|
// one unit apart, and already separable
|
|
e = IdentifyDifficultyColumn(-133, 20, 22, t);
|
|
CHECK(e.column == DifficultyColumn::NonAI);
|
|
e = IdentifyDifficultyColumn(-146, 20, 22, t);
|
|
CHECK(e.column == DifficultyColumn::AI);
|
|
}
|
|
|
|
int main() {
|
|
test_snapshot();
|
|
test_column_identified_from_the_stored_limit();
|
|
test_zero_income_is_ambiguous_not_identified();
|
|
test_neither_column_fits_is_unidentified();
|
|
test_the_two_columns_collapse_only_at_a_trivial_income();
|
|
return simtest::finish("player_turn");
|
|
}
|