use crate::world::compile::polities_layer::PolitiesOutput;
use crate::world::types::GeologyOutput;
#[derive(Debug, Clone, PartialEq)]
pub struct TradeRoute {
pub from: usize,
pub to: usize,
pub mode: String,
pub stance: String,
pub distance_km: f64,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct TradeOutput {
pub routes: Vec<TradeRoute>,
}
const DX: [i32; 8] = [1, 1, 0, -1, -1, -1, 0, 1];
const DY: [i32; 8] = [0, 1, 1, 1, 0, -1, -1, -1];
fn is_coastal(geo: &GeologyOutput, x: usize, y: usize) -> bool {
let (w, h) = (geo.width, geo.height);
if w == 0 || h == 0 || geo.heightmap.len() != w * h || x >= w || y >= h {
return false;
}
let sea = geo.sea_level;
if geo.heightmap[y * w + x] <= sea {
return true; }
for d in 0..8 {
let nx = x as i32 + DX[d];
let ny = y as i32 + DY[d];
if nx < 0 || ny < 0 || nx >= w as i32 || ny >= h as i32 {
continue;
}
if geo.heightmap[ny as usize * w + nx as usize] <= sea {
return true;
}
}
false
}
fn stance_of(pol: &PolitiesOutput, a: usize, b: usize) -> &str {
let (lo, hi) = if a < b { (a, b) } else { (b, a) };
pol.relations
.iter()
.find(|r| (r.a, r.b) == (lo, hi) || (r.a, r.b) == (hi, lo))
.map(|r| r.stance.as_str())
.unwrap_or("neutral")
}
const MAX_LINKS_PER_REALM: usize = 3;
pub fn compile_trade(pol: &PolitiesOutput, geo: &GeologyOutput, radius_earth: f64) -> TradeOutput {
let n = pol.polities.len();
if n < 2 {
return TradeOutput::default();
}
let (w, h) = (geo.width, geo.height);
let coastal: Vec<bool> =
pol.polities.iter().map(|p| is_coastal(geo, p.capital_pos.0, p.capital_pos.1)).collect();
use std::collections::BTreeMap;
let mut links: BTreeMap<(usize, usize), TradeRoute> = BTreeMap::new();
for a in 0..n {
let mut nbrs: Vec<(usize, f64, &str)> = (0..n)
.filter(|&b| b != a)
.map(|b| {
let dx = pol.polities[a].capital_pos.0 as f64 - pol.polities[b].capital_pos.0 as f64;
let dy = pol.polities[a].capital_pos.1 as f64 - pol.polities[b].capital_pos.1 as f64;
(b, crate::world::travel::distance_km(radius_earth, w, h, dx, dy), stance_of(pol, a, b))
})
.filter(|(_, _, stance)| *stance != "rival")
.collect();
nbrs.sort_by(|x, y| {
x.1.partial_cmp(&y.1).unwrap_or(std::cmp::Ordering::Equal).then(x.0.cmp(&y.0))
});
for (b, km, stance) in nbrs.into_iter().take(MAX_LINKS_PER_REALM) {
let (lo, hi) = if a < b { (a, b) } else { (b, a) };
let mode = if coastal[lo] && coastal[hi] { "sea" } else { "land" };
links.entry((lo, hi)).or_insert(TradeRoute {
from: lo,
to: hi,
mode: mode.to_string(),
stance: stance.to_string(),
distance_km: km,
});
}
}
TradeOutput { routes: links.into_values().collect() }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::world::compile::polities_layer::{Polity, Relation};
use crate::world::types::GeologyOutput;
fn polity(name: &str, x: usize, y: usize) -> Polity {
Polity {
name: name.into(),
capital: "cap".into(),
capital_pos: (x, y),
member_count: 1,
population: 10_000,
}
}
fn land_geo() -> GeologyOutput {
let (w, h) = (10, 10);
GeologyOutput {
width: w,
height: h,
sea_level: 0.0,
heightmap: vec![1.0; w * h],
..Default::default()
}
}
#[test]
fn rivals_never_get_a_route_non_rivals_nearby_do() {
let pol = PolitiesOutput {
polities: vec![polity("A", 1, 1), polity("B", 2, 1), polity("C", 8, 8)],
relations: vec![
Relation { a: 0, b: 1, stance: "rival".into() }, Relation { a: 0, b: 2, stance: "allied".into() },
Relation { a: 1, b: 2, stance: "neutral".into() },
],
};
let t = compile_trade(&pol, &land_geo(), 1.0);
assert!(!t.routes.iter().any(|r| (r.from, r.to) == (0, 1)));
assert!(t.routes.iter().any(|r| (r.from, r.to) == (0, 2)));
assert!(t.routes.iter().any(|r| (r.from, r.to) == (1, 2)));
assert!(t.routes.iter().all(|r| r.mode == "land"));
}
#[test]
fn coastal_capitals_trade_by_sea() {
let (w, h) = (10, 10);
let mut hm = vec![1.0f32; w * h];
for y in 0..h {
hm[y * w] = -1.0; }
let geo = GeologyOutput { width: w, height: h, sea_level: 0.0, heightmap: hm, ..Default::default() };
let pol = PolitiesOutput {
polities: vec![polity("A", 1, 2), polity("B", 1, 7)], relations: vec![Relation { a: 0, b: 1, stance: "allied".into() }],
};
let t = compile_trade(&pol, &geo, 1.0);
assert_eq!(t.routes.len(), 1);
assert_eq!(t.routes[0].mode, "sea");
assert_eq!(t.routes[0].stance, "allied");
}
#[test]
fn is_deterministic_and_dedups_undirected() {
let pol = PolitiesOutput {
polities: vec![polity("A", 1, 1), polity("B", 2, 2), polity("C", 3, 3)],
relations: Vec::new(),
};
let a = compile_trade(&pol, &land_geo(), 1.0);
let b = compile_trade(&pol, &land_geo(), 1.0);
assert_eq!(a, b);
assert!(a.routes.iter().all(|r| r.from < r.to));
}
}