Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions src/entry_point_nix.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
#include "system_state.hpp"
#include "game_scene.hpp"
#include "parsers_declarations.hpp"
#include "headless_run.hpp"
#include <oneapi/tbb/global_control.h>
#include <memory>

static sys::state game_state;
struct scenario_file {
Expand Down Expand Up @@ -204,6 +207,21 @@ void enforce_list_order() {
}

int main(int argc, char* argv[]) {
// headless batch flags: -seed N (fixed game seed), -years N (with -headless: run N game years at full speed, then exit),
// -dump DIR (monthly CSVs, see headless_run.hpp), -threads N (cap worker threads; 1 makes runs reproducible)
int run_years = -1;
std::string dump_dir;
std::unique_ptr<oneapi::tbb::global_control> thread_cap;
for(int i = 1; i + 1 < argc; ++i) {
if(std::string(argv[i]) == "-seed")
setenv("ALICE_SEED", argv[i + 1], 1);
else if(std::string(argv[i]) == "-years")
run_years = std::atoi(argv[i + 1]);
else if(std::string(argv[i]) == "-dump")
dump_dir = argv[i + 1];
else if(std::string(argv[i]) == "-threads")
thread_cap = std::make_unique<oneapi::tbb::global_control>(oneapi::tbb::global_control::max_allowed_parallelism, size_t(std::atoi(argv[i + 1])));
}
add_root(game_state.common_fs, NATIVE("."));
check_mods_folder();
check_scenario_folder();
Expand Down Expand Up @@ -370,6 +388,13 @@ int main(int argc, char* argv[]) {
game_state.ui_pause.store(false, std::memory_order::release);
game_scene::switch_scene(game_state, game_scene::scene_id::in_game_basic);
game_state.local_player_nation = dcon::nation_id{};
// headless: network::init gave the rank-1 nation to a local player; nobody plays it, so hand it back to the AI
for(auto n : game_state.world.in_nation)
n.set_is_player_controlled(false);
if(run_years >= 0) {
headless::run(game_state, run_years, dump_dir);
return EXIT_SUCCESS;
}
game_state.game_loop();
} else {
std::thread update_thread([&]() {
Expand Down
3 changes: 3 additions & 0 deletions src/entry_point_win.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,9 @@ int WINAPI wWinMain(HINSTANCE /*hInstance*/, HINSTANCE /*hPrevInstance*/, LPWSTR
game_state.ui_pause.store(false, std::memory_order::release);
game_scene::switch_scene(game_state, game_scene::scene_id::in_game_basic);
game_state.local_player_nation = dcon::nation_id{};
// headless: network::init gave the rank-1 nation to a local player; nobody plays it, so hand it back to the AI
for(auto n : game_state.world.in_nation)
n.set_is_player_controlled(false);
if(headless_repeat) {
std::thread update_thread([&]() { game_state.game_loop(); });
while(!game_state.quit_signaled.load(std::memory_order::acquire)) {
Expand Down
5 changes: 3 additions & 2 deletions src/gamestate/serialization.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include "dcon_generated_ids.hpp"
#include "system_state.hpp"
#include "serialization.hpp"
#include <cstdlib>
#include <random>
#include <ctime>

Expand Down Expand Up @@ -1169,7 +1170,7 @@ bool try_read_scenario_and_save_file(sys::state& state, native_string_view name)
buffer_pos = with_decompressed_section(buffer_pos,
[&](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


state.on_scenario_load();

Expand Down Expand Up @@ -1219,7 +1220,7 @@ bool try_read_scenario_as_save_file(sys::state& state, native_string_view name)
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


// only load gamerule settings if host or singleplayer. A client would have to load the host' settings anyway
Expand Down
115 changes: 115 additions & 0 deletions src/headless_run.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
#pragma once
// -headless -years N: runs N game years with no speed cap, prints seconds per game year, and with -dump DIR
// writes monthly CSVs (nations.csv, prices.csv, provinces.csv) on the 1st of each month.
#include <chrono>
#include <cstdio>
#include <filesystem>
#include <string>

namespace headless {

inline std::string ymd_str(sys::year_month_day d) {
char buf[16];
snprintf(buf, sizeof(buf), "%04d-%02d-%02d", d.year, int(d.month), int(d.day));
return std::string(buf);
}
inline std::string ymd_of(sys::state& state) {
return ymd_str(state.current_date.to_ymd(state.start_date));
}

inline std::string tag_of(sys::state& state, dcon::nation_id n) {
if(!n)
return "";
return nations::int_to_tag(
state.world.national_identity_get_identifying_int(state.world.nation_get_identity_from_identity_holder(n)));
}

inline void dump_month(sys::state& state, FILE* nations_csv, FILE* prices_csv, FILE* provinces_csv, std::string const& date) {
// province index is Alice's own (land provinces first), not the Vic2 definition.csv number
for(int32_t i = 0; i < state.province_definitions.first_sea_province.index(); ++i) {
dcon::province_id p{dcon::province_id::value_base_t(i)};
fprintf(provinces_csv, "%s,%d,%s,%s,%.6g\n", date.c_str(), i,
tag_of(state, state.world.province_get_nation_from_province_ownership(p)).c_str(),
tag_of(state, state.world.province_get_nation_from_province_control(p)).c_str(),
state.world.province_get_demographics(p, demographics::total));
}
state.world.for_each_nation([&](dcon::nation_id n) {
if(state.world.nation_get_owned_province_count(n) == 0)
return;
fprintf(nations_csv, "%s,%s,%.6g,%.6g,%.6g,%d,%d,%d,%.6g,%.6g,%d,%.6g,%.6g\n", date.c_str(), tag_of(state, n).c_str(),
state.world.nation_get_prestige(n), float(state.world.nation_get_industrial_score(n)),
float(state.world.nation_get_military_score(n)), int(state.world.nation_get_rank(n)),
int(state.world.nation_get_is_great_power(n)), int(state.world.nation_get_is_civilized(n)),
state.world.nation_get_stockpiles(n, economy::money), state.world.nation_get_demographics(n, demographics::total),
int(state.world.nation_get_owned_province_count(n)), state.world.nation_get_infamy(n),
state.world.nation_get_war_exhaustion(n));
});
state.world.for_each_commodity([&](dcon::commodity_id c) {
if(c == economy::money)
return;
float supply = 0.f;
state.world.for_each_market([&](dcon::market_id m) { supply += state.world.market_get_supply(m, c); });
// price = supply-weighted mean over the per-state markets; median_price = median over markets
fprintf(prices_csv, "%s,%s,%.6g,%.6g,%.6g\n", date.c_str(),
text::produce_simple_string(state, state.world.commodity_get_name(c)).c_str(), economy::price(state, c),
economy::median_price(state, c), supply);
});
}

inline void run(sys::state& state, int years, std::string const& dump_dir) {
using clock = std::chrono::steady_clock;
FILE* nations_csv = nullptr;
FILE* prices_csv = nullptr;
FILE* provinces_csv = nullptr;
if(!dump_dir.empty()) {
std::filesystem::create_directories(dump_dir);
nations_csv = fopen((dump_dir + "/nations.csv").c_str(), "w");
prices_csv = fopen((dump_dir + "/prices.csv").c_str(), "w");
provinces_csv = fopen((dump_dir + "/provinces.csv").c_str(), "w");
fprintf(provinces_csv, "date,province,owner,controller,population\n");
fprintf(nations_csv, "date,tag,prestige,industrial_score,military_score,rank,is_gp,is_civilized,treasury,population,provinces,infamy,war_exhaustion\n");
fprintf(prices_csv, "date,good,price,median_price,world_supply\n");
}

auto start = state.current_date.to_ymd(state.start_date);
int const end_year = start.year + years;
int year = start.year;
printf("HEADLESS start=%s years=%d seed=%u pops=%u\n", ymd_str(start).c_str(), years, state.game_seed, state.world.pop_size());
fflush(stdout);
if(nations_csv)
dump_month(state, nations_csv, prices_csv, provinces_csv, ymd_str(start));

state.user_settings.autosaves = sys::autosave_frequency::none; // parallel runs would share one autosave slot set
auto t_start = clock::now();
auto t_year = t_start;
while(years > 0) {
command::execute_pending_commands(state);
state.single_game_tick();
auto ymd = state.current_date.to_ymd(state.start_date);
if(ymd.day == 1 && nations_csv) {
dump_month(state, nations_csv, prices_csv, provinces_csv, ymd_str(ymd));
fflush(nations_csv);
fflush(prices_csv);
fflush(provinces_csv);
}
if(ymd.year != year) {
auto now = clock::now();
printf("YEAR %d secs=%.3f pops=%u wars=%u\n", year, std::chrono::duration<double>(now - t_year).count(),
state.world.pop_size(), state.world.war_size());
fflush(stdout);
t_year = now;
year = ymd.year;
}
if(ymd.year >= end_year)
break;
}
printf("TOTAL years=%d secs=%.3f\n", years, std::chrono::duration<double>(clock::now() - t_start).count());
fflush(stdout);
if(nations_csv) {
fclose(nations_csv);
fclose(prices_csv);
fclose(provinces_csv);
}
}

} // namespace headless
Loading