use chrono::Local;
use log::info;
use std::io::Write;
use std::path::Path;
use navig18xx::game::new_1867;
use navig18xx::prelude::*;
mod output;
use output::Dir;
pub struct GameState {
example: Example,
game: Box<dyn Game>,
companies: Vec<CompanyInfo>,
}
pub struct CompanyInfo {
token_name: &'static str,
trains: Trains,
train_desc: &'static str,
num_paths: usize,
net_revenue: usize,
}
#[test]
#[ignore]
fn test_1867_bc() -> Result<(), Box<dyn std::error::Error>> {
let use_cached_routes = false;
save_1867_bc_routes(use_cached_routes)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let use_cached_routes = true;
save_1867_bc_routes(use_cached_routes)
}
fn init_logging() {
let log_level = "info";
env_logger::Builder::from_env(
env_logger::Env::default().default_filter_or(log_level),
)
.format(|buf, record| {
writeln!(
buf,
"{} [{}] {}",
chrono::Local::now().format("%Y-%m-%dT%H:%M:%S"),
record.level(),
record.args()
)
})
.init();
}
fn save_1867_bc_routes(
use_cached_routes: bool,
) -> Result<(), Box<dyn std::error::Error>> {
let image_dir = Dir::BookRoot;
let json_dir = Dir::Examples;
init_logging();
let mut state = game_state();
let dest_file = json_dir.join("1867_bc.map");
let descr: navig18xx::map::Descr = state.example.map().into();
info!("Writing {} ...", dest_file.display());
navig18xx::io::write_map_descr(dest_file, &descr, true)?;
let state_file = json_dir.join("1867_bc.game");
let game_state = state.game.save(state.example.map());
info!("Writing {} ...", state_file.display());
navig18xx::io::write_game_state(state_file, game_state, true)?;
state.example.draw_map();
let out_file = image_dir.join("1867_bc.png");
save_png(&state.example, &out_file);
for company in &state.companies {
let routes_basename = format!("1867_bc_{}.json", company.token_name);
let routes_file = json_dir.join(routes_basename);
let image_basename = format!("1867_bc_{}.png", company.token_name);
let image_file = image_dir.join(image_basename);
if !routes_file.exists() {
info!("File does not exist: {}", routes_file.display());
}
let cached_opt = read_routes(&routes_file).ok();
let (routes, save_routes) = if let Some(routes) = cached_opt {
info!("Reading {}", routes_file.display());
if use_cached_routes {
info!("Using cached routes for {}", company.token_name);
(routes, false)
} else {
info!("Calculating best routes for {}", company.token_name);
let new_routes =
best_routes(&state.example, &*state.game, company);
if new_routes == routes {
(routes, false)
} else {
info!("Saving routes to {}", routes_file.display());
let pretty = true;
write_routes(routes_file, &new_routes, pretty).unwrap();
panic!("Calculated routes differ from the cached routes")
}
}
} else {
(best_routes(&state.example, &*state.game, company), true)
};
state.example.erase_all()?;
draw_routes(&mut state.example, &*state.game, company, &routes)?;
save_png(&state.example, image_file);
if save_routes {
info!("Saving routes to {}", routes_file.display());
let pretty = true;
write_routes(routes_file, &routes, pretty).unwrap();
}
}
Ok(())
}
fn game_state() -> GameState {
let mut game = new_1867();
let hex_max_diameter = 125.0;
let hex = Hex::new(hex_max_diameter);
let mut example = Example::new_game(&game, hex);
game.set_phase_name(example.map_mut(), "8");
let cnr = "CNR";
let gw = "GW";
let cno = "C&O";
let cpr = "CPR";
let ntr = "NTR";
let tiles = vec![
tile_at("87", "B18").rotate_cw(1),
tile_at("63", "C17").token(0, cno),
tile_at("63", "D16").token(0, cnr),
tile_at("63", "E15").token(0, gw),
tile_at("42", "F14").rotate_acw(2),
tile_at("23", "G13").rotate_cw(1),
tile_at("27", "H12").rotate_acw(2),
tile_at("23", "I11").rotate_cw(1),
tile_at("24", "J10").rotate_acw(2),
tile_at("8", "K9").rotate_cw(1),
tile_at("8", "D18").rotate_acw(2),
tile_at("623", "E17").token(0, cno).token(1, cnr),
tile_at("124", "F16")
.token(0, cno)
.token(1, cnr)
.token(2, cpr),
tile_at("611", "G15").rotate_cw(1).token(0, cpr),
tile_at("204", "H14").rotate_acw(1),
tile_at("8", "I13").rotate_cw(2),
tile_at("X8", "J12").token(0, gw),
tile_at("31", "K11").rotate_acw(1),
tile_at("204", "L10").rotate_acw(2),
tile_at("57", "M9").rotate_cw(1),
tile_at("9", "N8").rotate_cw(1),
tile_at("15", "I15").rotate_cw(1).token(0, ntr),
tile_at("24", "J14").rotate_cw(1),
tile_at("911", "K13").rotate_acw(2),
tile_at("639", "L12")
.token(0, cpr)
.token(1, ntr)
.token(2, gw),
tile_at("58", "M13").rotate_cw(2),
];
example.place_tiles(tiles);
let extra_tiles = vec![
tile_at("16", "D18").rotate_acw(3),
tile_at("7", "C19").rotate_acw(2),
];
example.place_tiles(extra_tiles);
let cnr_trains = Trains::new(vec![*game.train("5"), *game.train("5+5E")]);
let gw_trains = Trains::new(vec![*game.train("5"), *game.train("8")]);
let cno_trains = Trains::new(vec![*game.train("6"), *game.train("8")]);
let companies = vec![
CompanyInfo {
token_name: gw,
trains: gw_trains,
train_desc: "5-train, 8-train",
num_paths: 15_008,
net_revenue: 840,
},
CompanyInfo {
token_name: cno,
trains: cno_trains,
train_desc: "6-train, 8-train",
num_paths: 46_176,
net_revenue: 900,
},
CompanyInfo {
token_name: cnr,
trains: cnr_trains,
train_desc: "5-train, 5+5E-train",
num_paths: 67_948,
net_revenue: 1130,
},
];
let game: Box<dyn Game> = Box::new(game);
GameState {
example,
game,
companies,
}
}
fn best_routes(
example: &Example,
game: &dyn Game,
company: &CompanyInfo,
) -> Routes {
let bonuses = vec![];
let token = example.map().token(company.token_name);
let path_limit = company.trains.path_limit();
let criteria = Criteria {
token,
path_limit,
conflict_rule: game.single_route_conflicts(),
route_conflict_rule: game.multiple_routes_conflicts(),
};
let map = example.map();
let start = Local::now();
let paths = paths_for_token(map, &criteria);
assert_eq!(paths.len(), company.num_paths);
let mid = Local::now() - start;
let routes = company
.trains
.select_routes(paths, bonuses)
.expect("Could not find optimal routes");
let durn = Local::now() - start;
info!(
"Paths duration: {:02}:{:02}.{:03}",
mid.num_minutes(),
mid.num_seconds() % 60,
mid.num_milliseconds() % 1000
);
info!(
"Total duration: {:02}:{:02}.{:03}",
durn.num_minutes(),
durn.num_seconds() % 60,
durn.num_milliseconds() % 1000
);
info!("${}", routes.net_revenue);
for train_route in &routes.train_routes {
info!(" ${}", train_route.revenue);
}
info!("");
assert_eq!(routes.net_revenue, company.net_revenue);
routes
}
fn draw_routes(
example: &mut Example,
game: &dyn Game,
company: &CompanyInfo,
routes: &Routes,
) -> Result<(), Box<dyn std::error::Error>> {
example.draw_map_subset(|addr| {
let row = addr.logical_row();
let col = addr.logical_column();
row + col >= 16
});
let colours = example.theme().highlight_colours();
for (tr, colour) in routes.train_routes.iter().zip(colours) {
example.draw_route(&tr.route, colour)
}
let label_text = format!(
"{}: {} = ${}",
company.token_name, company.train_desc, routes.net_revenue
);
let labeller = example
.text_style()
.font_serif()
.font_size(36.0)
.bold()
.halign_left()
.valign_top()
.labeller(example.context(), example.hex());
let coords = game.coordinate_system();
let m = example.map().prepare_to_draw(
coords.parse("A5").unwrap(),
example.hex(),
example.context(),
);
labeller.draw(&label_text, (0.0, 0.0).into());
example.context().set_matrix(m);
Ok(())
}
fn save_png<S: AsRef<Path>>(example: &Example, filename: S) {
let filename = filename.as_ref();
let bg_rgba = Some(Colour::WHITE);
let margin = 20;
info!("Writing {} ...", filename.display());
example.write_png(margin, bg_rgba, filename);
}