The milestone needs a second input. The AI is a client, not part of the sim: it decides once,
on one machine, and its decisions reach the server as commands. A save carries the board and
half the input, which is why our turn wrote ModCount 14 where the original writes 24 -- the
missing ten ARE the turn's command stream.
* `game/ai/apply_order` -- the thirty-step schedule the original drains a batch in: twenty-
seven per-LIST steps (every player's elements of one list before the next list starts) with
three per-PLAYER gate loops spliced in at step 10, 29 and 30. Neither list order nor member
order, and both facts are asserted so a port that sorted cannot pass.
* `game/ai/command_capture` -- a `.tcb` recorded turn: gates, list lengths, elements in wire
order, per-client seeds, and `?` for a field the instrument could not read. An element count
that disagrees with its declaration is REJECTED, because a counter quietly one short is
indistinguishable from a turn that issued one fewer command.
* `app/command_replay` -- applies it before the drivers, where the End-Turn dispatcher does.
Every command is CHARGED; only the ones whose subsystem we hold are APPLIED; the rest are
declined with the named gap, or marked incomplete when the capture itself lacks the payload.
* `--turn-commands`, `--replay-count-only`, `--replay-recorded-names`, `--ai-seed`.
Measured on a fresh build directory, canonical pair turn2-state -> turn3-state:
108 -> 62, closed 46, regressed 0 (was 108 -> 63, closed 45) -- /Sim/ModCount now reads the
original's 24, decomposed as 2 drivers + 4 research-rate gates + build + rates + list 10 +
two list-14 + fleet move, with the list-23 population element free.
turn1-state replayed against the SAME run's autosave closes 7 (ModCount and all six research
leaves); against the historical turn2-state it closes 6 and leaves player 512's research pick
diverging -- which is correct, because that recording is from a process that picked differently.
One prediction was falsified and it paid for itself: the first run regressed two leaves because
the rates element's MEMORY field order is not its wire order. The converter no longer claims a
mapping it cannot support.
Two new addresses (the second and third gate-loop heads) via ghidra/addresses.d/lane-rb.json;
header regenerated, never hand-resolved.
55 lines
2.9 KiB
CMake
55 lines
2.9 KiB
CMake
# Standalone turn-driver tests.
|
|
#
|
|
# test_catalog needs nothing but the catalog and always runs.
|
|
# test_turn reads the owner's saves via SOTS_SAVES_DIR at run time and skips cleanly when it
|
|
# is unset -- no .sav ever enters this repo.
|
|
add_executable(app_test_catalog test_catalog.cpp)
|
|
target_link_libraries(app_test_catalog PRIVATE sots_app)
|
|
add_test(NAME app_catalog COMMAND app_test_catalog)
|
|
|
|
add_executable(app_test_turn test_turn.cpp)
|
|
target_link_libraries(app_test_turn PRIVATE sots_app)
|
|
add_test(NAME app_turn COMMAND app_test_turn)
|
|
|
|
# The trade-raid word count is pinned against a scripted generator, so it always runs.
|
|
add_executable(app_test_trade_raid test_trade_raid.cpp)
|
|
target_link_libraries(app_test_trade_raid PRIVATE sots_app)
|
|
add_test(NAME app_trade_raid COMMAND app_test_trade_raid)
|
|
|
|
# The alliance mask, as a rule. Pins the three things the corpus cannot separate -- the bit
|
|
# index, the OR, and the guard -- so it always runs.
|
|
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 no-research event, as a rule. Pins what the corpus cannot separate -- the
|
|
# researched-this-turn test, the state-2 reading, the FLT_MAX sentinel, EvAct=1 and the id
|
|
# sequence from a fresh EvNxID -- so it always runs.
|
|
add_executable(app_test_event_phase test_event_phase.cpp)
|
|
target_link_libraries(app_test_event_phase PRIVATE sots_app)
|
|
add_test(NAME app_event_phase COMMAND app_test_event_phase)
|
|
|
|
# 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_treaty app_test_event_phase app_test_turn_record)
|
|
target_include_directories(${_t} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
|
target_compile_options(${_t} PRIVATE -Wall -Wextra -Wpedantic)
|
|
endforeach()
|
|
|
|
# Replaying a recorded command stream: the count is charged for every command, a command we
|
|
# cannot apply is counted and declined rather than guessed, and the count-only control proves
|
|
# the modelled handlers write where they claim to. Builds its own board, so it always runs.
|
|
add_executable(app_test_command_replay test_command_replay.cpp)
|
|
target_link_libraries(app_test_command_replay PRIVATE sots_app)
|
|
add_test(NAME app_command_replay COMMAND app_test_command_replay)
|