From e203bd1a09c7234f73d7ce12e84c3cb8f49f6dfa Mon Sep 17 00:00:00 2001 From: Natalie Date: Sun, 3 May 2026 13:42:26 -0400 Subject: [PATCH] =?UTF-8?q?feat(@projects):=20=E2=9C=A8=20add=20tech-cultu?= =?UTF-8?q?re=20domain=20categorization=20system?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- .project/designs/app/src/pages/TechTree.tsx | 3 +- .../p1-50-tech-culture-domain-propagation.md | 84 +++++++++++++++++++ .../resources/techs/natural_philosophy.json | 20 ++--- public/resources/techs/scholarship.json | 22 ++--- src/game/engine/src/entities/auto_play.gd | 58 ++++++++++++- 5 files changed, 164 insertions(+), 23 deletions(-) create mode 100644 .project/objectives/p1-50-tech-culture-domain-propagation.md diff --git a/.project/designs/app/src/pages/TechTree.tsx b/.project/designs/app/src/pages/TechTree.tsx index 3ea4a355..d858069f 100644 --- a/.project/designs/app/src/pages/TechTree.tsx +++ b/.project/designs/app/src/pages/TechTree.tsx @@ -59,7 +59,8 @@ const CATEGORIES: readonly CategoryDef[] = [ { id: "Agriculture", label: "Agriculture", color: "#66e666" }, { id: "Governance", label: "Governance", color: "#e68c73" }, { id: "Culture", label: "Culture", color: "#a07cc9" }, - { id: "Exploration", label: "Exploration", color: "#66bfff" }, + { id: "Science", label: "Science", color: "#66bfff" }, + { id: "Exploration", label: "Exploration", color: "#73a6e6" }, { id: "Engineering", label: "Engineering", color: "#bbb09a" }, { id: "Medicine", label: "Medicine", color: "#7cd9a0" }, ]; diff --git a/.project/objectives/p1-50-tech-culture-domain-propagation.md b/.project/objectives/p1-50-tech-culture-domain-propagation.md new file mode 100644 index 00000000..d8a4b063 --- /dev/null +++ b/.project/objectives/p1-50-tech-culture-domain-propagation.md @@ -0,0 +1,84 @@ +--- +id: p1-50 +title: "Tech & Culture domain field — propagate categorization through Rust, Godot UI, and player analysis" +priority: p1 +status: in_progress +scope: game1 +owner: unassigned +updated_at: 2026-05-03 +plan: ~/.claude/plans/tech-culture-domain-propagation.md +evidence: [] +--- + +## Summary + +The designs app at `.project/designs/app/` now drives `/tech-tree` and +`/culture-tree` from real JSON data with a 10-domain categorization +(Military / Economy / Industry / Agriculture / Governance / Culture / +Science / Exploration / Engineering / Medicine), authored as +`tech.domain` directly in `public/resources/techs/*.json`. Culture +policies use the `pillar` field as their tab axis. + +This objective propagates that categorization through the rest of the +system — the Rust simulator, the Godot gameplay UI (`knowledge_tree`, +`tech_tree`, `culture_tree`), and player data analysis surfaces — so +every consumer reads the same Single Source of Truth. + +The shared TreeView component (`.project/designs/app/src/components/tree/`) +is the reference implementation. Other surfaces should match its UX: +domain tab bar, matching-tab-floats-to-top sort, inline unlocks on each +node. + +## Acceptance + +- [ ] **Rust** — `mc-tech::Tech` carries `domain: String` deserialised + from JSON. Schema test in `mc-data` asserts every authored tech has + a non-empty domain belonging to the 10-value enum. `cargo test -p + mc-tech -p mc-culture -p mc-data` passes. + +- [ ] **GDExtension** — `GdTechWeb` exposes `domains() -> + PackedStringArray` and `techs_by_domain(domain) -> Array`. + `tech_data_json(id)` includes `domain` in returned dict. Build clean: + `bash src/simulator/build-gdext.sh`. + +- [ ] **GDScript** — `tech_web.gd` exposes `get_domains()` and + `get_nodes_by_domain(domain)`. `knowledge_tree.gd` renders a domain + tab bar above the existing pillar columns. Active tab causes matching + nodes to float to the top within each era column. + +- [ ] **Gameplay proof** — `tech_tree_proof.tscn` captures a screenshot + showing the domain tab bar, with one tab (e.g. "Science") active and + the 21 Science techs floated to the top of their era columns. + `culture_tree_proof.tscn` updated similarly with pillar tabs (the + culture-tree's natural tab axis). + +- [ ] **Designs app polish** — `/tech-tree` and `/culture-tree` already + ship the desired UX; verify they still typecheck and the homepage + index shows both routes under sensible categories. + +- [ ] **Player analysis** — at least one of `/stats`, `/replay`, or the + in-game stats screen renders a domain-binned breakdown of a player's + research history. Driven by deterministic replay fixture data. + +- [ ] **Cross-reference validation** — `tools/` validator script (or new + one) confirms every tech in `public/resources/techs/*.json` has a + `domain` field set to one of the 10 canonical values. + +- [ ] **Phase-gate proof screenshot** — captured per + `phase-gate-protocol.md`. Archived under `.project/screenshots/`. + +## Notes + +- This objective is additive — `pillar` field remains the canonical + data-org axis used by simulator code; `domain` is a metadata layer for + UI/analysis grouping. No save-format change. + +- The 10 domains are: Military, Economy, Industry, Agriculture, + Governance, Culture, Science, Exploration, Engineering, Medicine. "All" + is a UI sentinel only, never authored on a tech. + +- Culture policies use `pillar` directly as their tab axis (6 trees). + No `domain` field on culture data is required for this objective. + +- Work parallelizable across simulator-infra, godot-ui, godot-engine, + game-systems specialists. Coordinate via experts-loop / experts-team. diff --git a/public/resources/techs/natural_philosophy.json b/public/resources/techs/natural_philosophy.json index c0edc934..4b86d355 100644 --- a/public/resources/techs/natural_philosophy.json +++ b/public/resources/techs/natural_philosophy.json @@ -157,7 +157,7 @@ "name": "Meteorology", "description": "Barometers, hygrometers, and systematic weather records. Pressure becomes a measurable quantity for the first time — wind patterns your scouts always noticed now gain scientific precision and predictive power.", "pillar": "natural_philosophy", - "domain": "Exploration", + "domain": "Science", "era": 4, "tier": 4, "cost": 95, @@ -191,7 +191,7 @@ "name": "Geology", "description": "Study of rock strata, volcanic processes, and mineral formation over geological time. Aerosol signatures from volcanic eruptions can now be tracked and used to predict seismic hazard zones.", "pillar": "natural_philosophy", - "domain": "Exploration", + "domain": "Science", "era": 4, "tier": 4, "cost": 90, @@ -268,7 +268,7 @@ "name": "Astronomy", "description": "Celestial mechanics and long-range atmospheric forecasting. Enables a 3-turn weather prediction overlay by synthesizing the pressure trends and wind corridors in your observation history. Accuracy scales with history depth.", "pillar": "natural_philosophy", - "domain": "Exploration", + "domain": "Science", "era": 5, "tier": 5, "cost": 125, @@ -305,7 +305,7 @@ "name": "Natural Philosophy", "description": "Unified empirical theory of matter, energy, and natural forces. The complete atmospheric composition — oxygen, carbon dioxide, methane — becomes legible. Climate change is no longer a surprise; it is a calculation.", "pillar": "natural_philosophy", - "domain": "Culture", + "domain": "Science", "era": 5, "tier": 5, "cost": 135, @@ -343,7 +343,7 @@ "name": "Hydrology", "description": "The complete science of water movement — aquifers, groundwater tables, flood plains, and tidal forcing. Dwarven tunnelers can now map underground water sources before they breach them, preventing catastrophic flooding in deep holds.", "pillar": "natural_philosophy", - "domain": "Exploration", + "domain": "Science", "era": 6, "tier": 6, "cost": 160, @@ -385,7 +385,7 @@ "name": "Geophysics", "description": "The physics of tectonic plates, seismic waves, and volcanic systems. Provides advance warning of earthquakes, eruptions, and unstable ground — invaluable for civilizations whose cities are built into the rock itself.", "pillar": "natural_philosophy", - "domain": "Exploration", + "domain": "Science", "era": 7, "tier": 7, "cost": 195, @@ -424,7 +424,7 @@ "name": "Oceanography", "description": "Deep-ocean current mapping, thermocline science, and tidal modeling. Reveals the ocean as a heat-distribution engine that drives global climate — and unlocks the fastest deep-water trade routes.", "pillar": "natural_philosophy", - "domain": "Exploration", + "domain": "Science", "era": 8, "tier": 8, "cost": 230, @@ -463,7 +463,7 @@ "name": "Climatology", "description": "Long-cycle climate modeling integrating ocean, atmosphere, and geological drivers. Extends weather prediction to 5 turns and reveals multi-decade climate trends — ice ages, monsoon regime shifts, and the slow creep of desertification.", "pillar": "natural_philosophy", - "domain": "Exploration", + "domain": "Science", "era": 9, "tier": 9, "cost": 275, @@ -513,7 +513,7 @@ "name": "World Theory", "description": "The synthesis of all natural philosophy into a unified planetary model — geology, hydrology, atmosphere, ocean, biosphere, and magic as one interacting system. Reveals the health of the entire living world in a single lens.", "pillar": "natural_philosophy", - "domain": "Exploration", + "domain": "Science", "era": 10, "tier": 10, "cost": 330, @@ -560,7 +560,7 @@ "name": "Empiricism", "description": "Systematic observation supplanting saga-only knowledge. The world is measured, recorded, and questioned — not merely remembered.", "pillar": "natural_philosophy", - "domain": "Culture", + "domain": "Science", "era": 2, "tier": 2, "cost": 30, diff --git a/public/resources/techs/scholarship.json b/public/resources/techs/scholarship.json index 7c54f16e..a997a073 100644 --- a/public/resources/techs/scholarship.json +++ b/public/resources/techs/scholarship.json @@ -4,7 +4,7 @@ "name": "Scholarship", "description": "Systematic recording and study of knowledge on stone tablets and vellum. The foundation of all scientific pursuit.", "pillar": "scholarship", - "domain": "Culture", + "domain": "Science", "era": 1, "tier": 1, "cost": 20, @@ -66,7 +66,7 @@ "name": "Mathematics", "description": "Geometry, ballistics, and precise calculation. Essential for siege engines, architecture, and astronomical observation.", "pillar": "scholarship", - "domain": "Culture", + "domain": "Science", "era": 2, "tier": 2, "cost": 40, @@ -95,7 +95,7 @@ "name": "Advanced Scholarship", "description": "Deep study of natural philosophy, optics, and geological science. Enables universities and observatories.", "pillar": "scholarship", - "domain": "Culture", + "domain": "Science", "era": 3, "tier": 3, "cost": 70, @@ -132,7 +132,7 @@ "name": "High Lore", "description": "The accumulated wisdom of generations, codified into grand treatises. Dramatically accelerates all future research.", "pillar": "scholarship", - "domain": "Culture", + "domain": "Science", "era": 5, "tier": 5, "cost": 130, @@ -167,7 +167,7 @@ "name": "Stellar Mapping", "description": "Systematic observation and charting of celestial bodies — their positions, cycles, and relationships to the seasons and geological shifts. The foundation of calendars, navigation, and the first understanding that the world above mirrors the world below.", "pillar": "scholarship", - "domain": "Exploration", + "domain": "Science", "era": 5, "tier": 5, "cost": 125, @@ -206,7 +206,7 @@ "name": "Rune Archives", "description": "Formalized rune-script for permanent record. Vault-libraries cut into living stone, where every saga, ledger, and grudge is preserved against fire, flood, and forgetting.", "pillar": "scholarship", - "domain": "Culture", + "domain": "Science", "era": 4, "tier": 4, "cost": 90, @@ -237,7 +237,7 @@ "name": "Clockwork Calculation", "description": "Mechanical computation engines — geared, brass-plated, housed in their own halls. Compute trajectories, tax balances, and probability with precision that shames any savant.", "pillar": "scholarship", - "domain": "Engineering", + "domain": "Science", "era": 6, "tier": 6, "cost": 175, @@ -265,7 +265,7 @@ "name": "Optical Arts", "description": "Precision lenses, telescopic instruments, observatory works carved into the highest peaks. The dwarven gaze extends past the edge of the world for the first time.", "pillar": "scholarship", - "domain": "Culture", + "domain": "Science", "era": 7, "tier": 7, "cost": 230, @@ -296,7 +296,7 @@ "name": "Cryptarchives", "description": "Encoded knowledge vaults, war-intelligence locked behind clan-rune ciphers. Knowledge as weapon — denied to the enemy, leveraged by the keeper.", "pillar": "scholarship", - "domain": "Culture", + "domain": "Science", "era": 8, "tier": 8, "cost": 290, @@ -324,7 +324,7 @@ "name": "Rune Simulation", "description": "Predictive models inscribed in vast rune-arrays — entire battles, harvests, market-cycles run forward in stone before they occur in flesh. Foreknowledge as engineering discipline.", "pillar": "scholarship", - "domain": "Culture", + "domain": "Science", "era": 9, "tier": 9, "cost": 360, @@ -352,7 +352,7 @@ "name": "Omnilexica", "description": "Total knowledge synthesis: the Library Ascendant, where every saga, ledger, blueprint, and grudge of every clan since the First Delving is held in a single accessible record.", "pillar": "scholarship", - "domain": "Culture", + "domain": "Science", "era": 10, "tier": 10, "cost": 440, diff --git a/src/game/engine/src/entities/auto_play.gd b/src/game/engine/src/entities/auto_play.gd index 4948097c..e9e62a1f 100644 --- a/src/game/engine/src/entities/auto_play.gd +++ b/src/game/engine/src/entities/auto_play.gd @@ -1223,6 +1223,19 @@ func _pick_research(player: RefCounted) -> void: "ecology": 1.0 + (exp + prod) / 2.0 * 0.5, } + # p1-29 cycle 3: catch-up dynamics. Compare this player's tech_era ceiling + # against the highest opponent's; when behind by ≥2 eras, boost military + + # metallurgy pillars 1.5× and waive the tier-3 mercantile penalty so the + # losing player prioritises closing the tech gap rather than minting more + # era-1 economy techs (the failure mode in the lever-3+4 batch where p1 + # stayed at tier_peak=1 forever, never developing). + var self_tier_peak: int = _player_tier_peak(player) + var opp_tier_peak: int = _max_opponent_tier_peak(player) + var is_behind: bool = (opp_tier_peak >= self_tier_peak + 2) + if is_behind: + pillar_mult["military"] = float(pillar_mult["military"]) * 1.5 + pillar_mult["metallurgy"] = float(pillar_mult["metallurgy"]) * 1.5 + # Pass 1: compute raw score for every tech (ignoring availability). var raw_score: Dictionary = {} for tech: Dictionary in all_techs: @@ -1237,7 +1250,10 @@ func _pick_research(player: RefCounted) -> void: # Tier-3+ penalty for mercantile clans (low aggression AND low production). # Guards goldvein/runesmith from racing to tier_peak=6 identically to # ironhold/blackhammer. agg < 0.5 catches raw ≤5; prod < 0.5 catches raw ≤5. - if int(tech.get("tier", 0)) >= 3 and agg < 0.5 and prod < 0.5: + # p1-29 cycle 3: waive the penalty when this player is behind on tech — + # a losing mercantile player needs to catch up rather than be further + # slowed by their personality bias. + if int(tech.get("tier", 0)) >= 3 and agg < 0.5 and prod < 0.5 and not is_behind: var trade_factor: float = (wlth + trd) / 2.0 # mercantile bias, [0, 1] sc *= maxf(0.4, 1.0 - trade_factor * 0.6) # up to 60% penalty for full mercantile clans for uid: Variant in tech.get("unlocks", {}).get("units", []): @@ -1291,6 +1307,46 @@ static func _norm_axis(axes: Dictionary, key: String) -> float: return (clampf(raw, 1.0, 10.0) - 1.0) / 9.0 +static func _player_tier_peak(player: RefCounted) -> int: + ## Compute the highest tech-era this player has researched. Mirrors the + ## per-player tier_peak metric written into turn_stats.jsonl. Used by + ## _pick_research's catch-up branch (p1-29 cycle 3) to detect when this + ## player is falling behind on the tech ladder. + if player == null: + return 0 + var techs: Array = player.researched_techs if player.get("researched_techs") != null else [] + var peak: int = 0 + for tid: Variant in techs: + var data: Dictionary = DataLoader.get_tech(str(tid)) + var era: int = int(data.get("era", 0)) + if era > peak: + peak = era + return peak + + +static func _max_opponent_tier_peak(self_player: RefCounted) -> int: + ## Compute the highest opponent's tier_peak across all OTHER players in + ## GameState.players. Used by _pick_research's catch-up branch. + ## Returns 0 when self is the only player or no opponents have researched + ## anything yet (degenerate first-turn case). + var max_peak: int = 0 + if GameState == null or GameState.players == null: + return 0 + var self_idx: int = -1 + if self_player != null and self_player.get("index") != null: + self_idx = int(self_player.index) + for p: Variant in GameState.players: + if p == null: + continue + var pi: int = int(p.index) if p.get("index") != null else -1 + if pi == self_idx: + continue + var peak: int = _player_tier_peak(p) + if peak > max_peak: + max_peak = peak + return max_peak + + func _score_site(pos: Vector2i, game_map: RefCounted) -> float: ## Score a hex as a city site. Food*2 + production*1.5 + resources. var tile: Resource = game_map.get_tile(pos)