feat(@projects/@magic-civilization): ✨ add climate tile data access
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
55550e48e7
commit
7fe43667b8
3 changed files with 147 additions and 0 deletions
|
|
@ -171,6 +171,29 @@ impl GdGridState {
|
|||
None => Dictionary::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return climate axes fields for a single tile as a Dictionary.
|
||||
/// Keys: latitude, continentality, mean_temp, mean_precip, seasonality,
|
||||
/// aridity_index (floats), t_band, p_band (ints).
|
||||
/// Returns an empty Dictionary if the tile coordinates are out of range.
|
||||
#[func]
|
||||
fn tile_climate(&self, col: i64, row: i64) -> Dictionary {
|
||||
match self.inner.tile(col as i32, row as i32) {
|
||||
Some(tile) => {
|
||||
let mut d = Dictionary::new();
|
||||
d.set("latitude", tile.latitude as f64);
|
||||
d.set("continentality", tile.continentality as f64);
|
||||
d.set("mean_temp", tile.mean_temp as f64);
|
||||
d.set("mean_precip", tile.mean_precip as f64);
|
||||
d.set("seasonality", tile.seasonality as f64);
|
||||
d.set("aridity_index", tile.aridity_index as f64);
|
||||
d.set("t_band", tile.t_band as i64);
|
||||
d.set("p_band", tile.p_band as i64);
|
||||
d
|
||||
}
|
||||
None => Dictionary::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── GdClimatePhysics ────────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -225,6 +225,25 @@ impl WasmGrid {
|
|||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Return climate axes fields for a single tile as a JSON string.
|
||||
/// Returns `null` if the coordinates are out of range.
|
||||
#[wasm_bindgen(js_name = "tileClimateJson")]
|
||||
pub fn tile_climate_json(&self, col: i32, row: i32) -> Option<String> {
|
||||
self.inner.tile(col, row).map(|t| {
|
||||
format!(
|
||||
r#"{{"latitude":{latitude},"continentality":{continentality},"mean_temp":{mean_temp},"mean_precip":{mean_precip},"seasonality":{seasonality},"aridity_index":{aridity_index},"t_band":{t_band},"p_band":{p_band}}}"#,
|
||||
latitude = t.latitude,
|
||||
continentality = t.continentality,
|
||||
mean_temp = t.mean_temp,
|
||||
mean_precip = t.mean_precip,
|
||||
seasonality = t.seasonality,
|
||||
aridity_index = t.aridity_index,
|
||||
t_band = t.t_band,
|
||||
p_band = t.p_band,
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// WASM-exposed climate physics engine.
|
||||
|
|
|
|||
105
src/simulator/crates/mc-climate/tests/climate_decomposition.rs
Normal file
105
src/simulator/crates/mc-climate/tests/climate_decomposition.rs
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
//! Climate decomposition golden test (p2-49).
|
||||
//! Derives climate fields for a 10×10 grid and freezes the results.
|
||||
//! Any change to derive logic must update the expected values and add
|
||||
//! a migration note to the commit message.
|
||||
|
||||
use mc_climate::derive::{
|
||||
latitude, t_band, p_band, mean_temp, aridity_index, ClimateParams,
|
||||
classify_terrain_whittaker, derive_climate_fields,
|
||||
};
|
||||
use mc_core::grid::GridState;
|
||||
|
||||
#[test]
|
||||
fn latitude_derivation_golden() {
|
||||
// Row 0 = north pole (+1.0), row 9 = south pole (-1.0)
|
||||
let cases: &[(u32, u32, f32)] = &[
|
||||
(0, 10, 1.0),
|
||||
(5, 10, -0.111), // ≈ 1 - 2*(5/9) = 1 - 1.111 = -0.111
|
||||
(9, 10, -1.0),
|
||||
];
|
||||
for &(row, rows, expected) in cases {
|
||||
let got = latitude(row, rows);
|
||||
assert!((got - expected).abs() < 0.01, "latitude({row},{rows}) = {got}, want ≈{expected}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn band_discretisation_stable() {
|
||||
let t_thresh = [0.20f32, 0.40, 0.60, 0.80];
|
||||
let p_thresh = [0.15f32, 0.35, 0.60, 0.80];
|
||||
// Spot-checks — these values must never change without a data migration
|
||||
assert_eq!(t_band(0.10, &t_thresh), 0, "T polar");
|
||||
assert_eq!(t_band(0.30, &t_thresh), 1, "T cold");
|
||||
assert_eq!(t_band(0.50, &t_thresh), 2, "T temperate");
|
||||
assert_eq!(t_band(0.70, &t_thresh), 3, "T warm");
|
||||
assert_eq!(t_band(0.90, &t_thresh), 4, "T hot");
|
||||
|
||||
assert_eq!(p_band(0.05, &p_thresh), 0, "P hyper_arid");
|
||||
assert_eq!(p_band(0.25, &p_thresh), 1, "P arid");
|
||||
assert_eq!(p_band(0.50, &p_thresh), 2, "P semi_arid");
|
||||
assert_eq!(p_band(0.70, &p_thresh), 3, "P humid");
|
||||
assert_eq!(p_band(0.90, &p_thresh), 4, "P wet");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn whittaker_table_spot_checks() {
|
||||
// Directly from the CLIMATE.md Whittaker table
|
||||
assert_eq!(classify_terrain_whittaker(0, 0, 0.3), "tundra", "polar any → tundra");
|
||||
assert_eq!(classify_terrain_whittaker(1, 0, 0.3), "tundra", "cold arid → tundra");
|
||||
assert_eq!(classify_terrain_whittaker(1, 3, 0.3), "boreal_forest","cold wet → boreal_forest");
|
||||
assert_eq!(classify_terrain_whittaker(2, 0, 0.3), "desert", "temperate arid → desert");
|
||||
assert_eq!(classify_terrain_whittaker(2, 1, 0.3), "grassland", "temperate semi-arid → grassland");
|
||||
assert_eq!(classify_terrain_whittaker(3, 0, 0.3), "desert", "warm arid → desert");
|
||||
assert_eq!(classify_terrain_whittaker(3, 2, 0.3), "plains", "warm semi-arid → plains");
|
||||
assert_eq!(classify_terrain_whittaker(4, 0, 0.3), "desert", "hot hyper-arid → desert");
|
||||
assert_eq!(classify_terrain_whittaker(4, 4, 0.3), "jungle", "hot wet → jungle");
|
||||
// Elevation overrides
|
||||
assert_eq!(classify_terrain_whittaker(4, 4, 0.86), "snow", "elev>0.85 → snow");
|
||||
assert_eq!(classify_terrain_whittaker(4, 4, 0.75), "mountains", "elev>0.70 → mountains");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derive_climate_fields_deterministic_on_10x10() {
|
||||
let params = ClimateParams::default();
|
||||
|
||||
let make_grid = || {
|
||||
let mut g = GridState::new(10, 10);
|
||||
for (i, t) in g.tiles.iter_mut().enumerate() {
|
||||
t.elevation = 0.3 + (i % 5) as f32 * 0.1;
|
||||
t.biome_id = if i % 7 == 0 { "ocean".to_string() } else { "land".to_string() };
|
||||
// Simulate tectonic pass output
|
||||
t.mountain_proximity = if i % 11 == 0 { 0.8 } else { 0.0 };
|
||||
t.coast_proximity = if i % 5 == 0 { 1.0 } else { 0.2 };
|
||||
}
|
||||
g
|
||||
};
|
||||
|
||||
let mut g1 = make_grid();
|
||||
let mut g2 = make_grid();
|
||||
derive_climate_fields(&mut g1, ¶ms);
|
||||
derive_climate_fields(&mut g2, ¶ms);
|
||||
|
||||
for i in 0..g1.tiles.len() {
|
||||
assert_eq!(g1.tiles[i].t_band, g2.tiles[i].t_band, "t_band mismatch idx={i}");
|
||||
assert_eq!(g1.tiles[i].p_band, g2.tiles[i].p_band, "p_band mismatch idx={i}");
|
||||
assert_eq!(g1.tiles[i].mean_temp.to_bits(), g2.tiles[i].mean_temp.to_bits(), "mean_temp mismatch idx={i}");
|
||||
assert_eq!(g1.tiles[i].mean_precip.to_bits(),g2.tiles[i].mean_precip.to_bits(),"mean_precip mismatch idx={i}");
|
||||
assert_eq!(g1.tiles[i].latitude.to_bits(), g2.tiles[i].latitude.to_bits(), "latitude mismatch idx={i}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aridity_index_range_reasonable() {
|
||||
let params = ClimateParams::default();
|
||||
// Tropical equator: should be humid (aridity > 1)
|
||||
let t_tropical = mean_temp(0.0, 0.2, ¶ms);
|
||||
let p_high = 0.85f32;
|
||||
let ai_humid = aridity_index(t_tropical, p_high);
|
||||
assert!(ai_humid > 1.0, "humid tile aridity_index should be >1.0, got {ai_humid}");
|
||||
|
||||
// Dry mid-lat: should be arid (<0.5)
|
||||
let t_mid = mean_temp(0.5, 0.3, ¶ms);
|
||||
let p_low = 0.10f32;
|
||||
let ai_dry = aridity_index(t_mid, p_low);
|
||||
assert!(ai_dry < 0.5, "dry tile aridity_index should be <0.5, got {ai_dry}");
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue