fix(simulation): prosperity in basis points + shared log10_floor helper (#954)

Address PR #145 review. Convert prosperity from f32 to integer basis
points (matching the existing prosperity_bps, D-010 integer-only),
removing both float-determinism bugs by construction:

- New server/src/bps.rs: log10_floor (integer order-of-magnitude) +
  bps_to_f32 edge helper, with boundary tests.
- city_context_reader: prosperity_baseline_bps (u32, 0-10000). role base
  + pop bonus (400*log10_floor, cap 1200) + symmetric noise via integer
  modulo (fixes the always-negative bug) + clamp [1000,9500]. No floats.
- CityGenerationContext.prosperity_baseline -> prosperity_baseline_bps;
  updated the two test context builders. Not serialized — no wire break.
- district_mix population_tier now uses log10_floor (same determinism
  bug class as the comment claimed to avoid).
- Tests in bps + assert positive noise is achievable (the case the old
  test hid).

cargo check/clippy --all-targets -D warnings clean; 1291 lib tests pass;
fmt clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-31 14:31:27 +02:00
co-authored by Claude Opus 4.8
parent a854b89aa0
commit d6e37c0c0e
7 changed files with 304 additions and 114 deletions
+5 -4
View File
@@ -485,7 +485,7 @@ pub enum BuildingEntryClass {
/// Construction era of a building block (D-229).
///
/// Derived from `founding_age_years + prosperity_baseline + seed`.
/// Derived from `founding_age_years + prosperity_baseline_bps + seed`.
/// Reads primarily as **age/wear** via the condition layer (D-217/D-198);
/// not a material-technology ladder (era = maintenance signal, not style signal).
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
@@ -710,7 +710,7 @@ pub struct BuildingPropertyTag {
pub era: ConstructionEra,
/// Cause of this block's era classification.
pub era_cause: EraCause,
/// Frozen-amber condition snapshot from `prosperity_baseline` (D-197/D-217).
/// Frozen-amber condition snapshot from `prosperity_baseline_bps` (D-197/D-217).
/// The rolling condition overlay (D-198) paints over this; never mutates the tag.
pub initial_condition: crate::atlas::tile_condition::TileCondition,
/// Doors into / out of this building (D-231). At least one `Main` door.
@@ -933,8 +933,9 @@ pub struct CityGenerationContext {
/// Foreign key into atlas_city_names.id
pub city_id: u64,
pub political_archetype: PoliticalArchetype,
/// Starting economic health seed (0.01.0). Derived per D-197.
pub prosperity_baseline: f32,
/// Starting economic health seed in basis points (010_000; 10_000 = 1.0).
/// Derived per D-197. Integer to avoid f32 non-determinism (D-010).
pub prosperity_baseline_bps: u32,
pub surrounding_biome: SettingType,
/// Compass octants (0=N, 1=NE … 7=NW) where roads enter the city footprint.
pub road_entry_directions: Vec<u8>,