Skip to content

Linux headless: -years/-seed/-threads/-dump, and hand the auto-picked player nation back to the AI - #2320

Open
ruggsea wants to merge 2 commits into
schombert:mainfrom
ruggsea:headless-batch-runs
Open

ruggsea wants to merge 2 commits into
schombert:mainfrom
ruggsea:headless-batch-runs

Conversation

@ruggsea

@ruggsea ruggsea commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Two small commits for running AI-only campaigns on Linux without a window.

  1. headless: -years, -seed, -threads and -dump. With -headless, -years N runs N game years with no speed cap and exits, printing seconds per game year. -seed N fixes the game seed, -threads N caps the worker threads (with -threads 1 the same seed gives the same campaign), and -dump DIR writes monthly nations.csv, prices.csv and provinces.csv. The driver is a new header, headless_run.hpp; the only other changes are the flag parsing in entry_point_nix.cpp and reading the seed in serialization.cpp. Without these flags nothing changes.
  2. headless: hand the auto-picked player nation back to the AI (bug fix). In headless mode network::init still picks the top-ranked nation for the local player and sets is_player_controlled on it. Headless then only resets local_player_nation, so that nation keeps the flag all game: its armies are never put under AI control and the AI never declares a war for it. That is ENG in 1836 vanilla and RUS in GFM. Over 100-year headless campaigns ENG declared 0 wars in 40 of 40, and GFM Russia lost its starting war against Dagestan in 109 of 109. With the fix ENG declares 15.8 wars per campaign (112 campaigns) and GFM Russia beats Circassia and Dagestan by 1832. Three lines in each entry point.

Tested on Linux (clang, Release): 3-year vanilla run, 29 s on one core with -threads 1; two runs with the same seed write byte-identical CSVs.

-years N (with -headless) runs N game years with no speed cap and exits; -seed N fixes
the game seed; -threads N caps the worker threads (-threads 1 makes runs reproducible);
-dump DIR writes monthly nations/prices/provinces CSVs. Nothing changes without the flags.
network::init picks the first non-player nation by rank for the local player and
create_mp_player marks it is_player_controlled. Headless only cleared
local_player_nation, so that nation (ENG in 1836 vanilla, RUS in GFM) kept the
player flag: its armies never went on guard, assign_targets never found them,
and the AI never declared a war for it.
@schombert

Copy link
Copy Markdown
Owner

You should probably discuss new features in the PA discord. While I might be amenable to additional logging behind a build flag, I am not a fan of fprintf calls directly in the logic of functions, even if they are behind an if.

@schombert

Copy link
Copy Markdown
Owner

Have you benchmarked the new fused_sum_over_demographics ? It doesn't use multiple threads and the way it access memory makes me think it would be strictly slower

@ruggsea
ruggsea force-pushed the headless-batch-runs branch from 4d5633b to fe0f2e5 Compare October 5, 2026 00:14
@ruggsea ruggsea changed the title Linux headless batch runs: -years/-seed/-dump/-threads, a player-flag fix, optional map screenshots, a faster demographics pass, and tools for ensembles Linux headless: -years/-seed/-threads/-dump, and hand the auto-picked player nation back to the AI Oct 5, 2026
@ruggsea

ruggsea commented Oct 5, 2026

Copy link
Copy Markdown
Contributor Author

Sorry such a big PR was a mistake, I have now tried to reduce it to a minimal state

@ruggsea

ruggsea commented Oct 5, 2026

Copy link
Copy Markdown
Contributor Author

Have you benchmarked the new fused_sum_over_demographics ? It doesn't use multiple threads and the way it access memory makes me think it would be strictly slower

I would make a separate PR for it but yes you are right it is slower on multiple threads, but my new versions is still optimal for single threaded runs therefore I would use it for headless runs that need to be deterministic

@schombert

Copy link
Copy Markdown
Owner

probably best not to describe it as "fast" then, as it would probably confuse normal players

[&](uint8_t const* ptr_in, uint32_t length) { read_save_section(ptr_in, ptr_in + length, state); });

state.game_seed = uint32_t(std::random_device()());
state.game_seed = getenv("ALICE_SEED") ? uint32_t(strtoul(getenv("ALICE_SEED"), nullptr, 10)) : uint32_t(std::random_device()()); // -seed N sets ALICE_SEED

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea of being able to specify a seed, but I think that it is probably a bad practice to pass that value around in a global variable, and definitely a bad idea to do it by setting an environment variable. Instead I suggest passing the seed value, if present, in a std::optional<uint32_t> through to the function creating the new scenario from scratch

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants