Skip to content

Commit fa5b43f

Browse files
fix: unblock Crashlanded research scoring (seed bench + brief observability) (#75)
* fix: seed Crashlanded SimpleResearchBench and queue Smithing Research 1.2 floors at 7/31 on Crashlanded because the save had no research bench and currentProj was unset. Agents never finish tech; research_target is rejected as bench/prereqs missing. That floor is the starting tree, not sigma. Bake a player-owned wood SimpleResearchBench at (128, 136) and queue Smithing. Re-pin save_sha256. Do not recalibrate the baseline sidecar. Co-authored-by: Jason <jkbennitt@users.noreply.github.com> * fix: surface research_bench_present and treat current project as done Add research_bench_present to the harness-neutral state snapshot and a MAP_SUMMARY cue when no bench is visible. Prefer research benches in the 50-structure sample so ruin walls cannot hide the seeded bench. research_target on the already-queued current project is success-by-state (no rewrite), matching the growing-zone already-covered gate. Tests cover the 7/31 floor math, brief flag true/false, save contract, and the current-project path. Co-authored-by: Jason <jkbennitt@users.noreply.github.com> * fix: scripted smoke targets Smithing so research_target still writes Mock state already has Electricity queued. After treating the current project as success-by-state, the MCP round-trip smoke issued zero research POSTs. Smithing is available and not current, so the write path is still exercised. Co-authored-by: Jason <jkbennitt@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Jason <jkbennitt@users.noreply.github.com>
1 parent 07d6f43 commit fa5b43f

19 files changed

Lines changed: 217 additions & 16 deletions

‎CLAUDE.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ Tick-specific priorities injected into all Felix agents (other harnesses get no
272272
### Save Loading + Item Setup
273273

274274
`run_scenario.py` automatically:
275-
1. Loads the scenario's save file (`rle_crashlanded_v1`, etc.)
275+
1. Loads the scenario's save file (`rle_crashlanded_v1` seeds a built `SimpleResearchBench` at (128,136) and queues `Smithing`; the other five saves are derived from this base)
276276
2. Polls until game is ready (colonist_count > 0)
277277
3. Unforbids all starting items (via `POST /api/v1/things/set-forbidden`)
278278
4. Runs any `setup_commands` declared in the scenario YAML (spawn_pawn, spawn_item, change_weather, drop_pod)

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ You need four things set up:
101101
1. **RimWorld** (Steam) with **Harmony** and **[RIMAPI](https://github.com/IlyaChichkov/RIMAPI)** mods subscribed and **enabled** in the Mods menu. Load order: Harmony → Core → (DLCs) → RIMAPI.
102102
2. **LLM provider** — [LM Studio](https://lmstudio.ai/) (local, free) or [OpenRouter](https://openrouter.ai/) (cloud)
103103
3. **Python 3.14+** with [uv](https://docs.astral.sh/uv/)
104-
4. **Save file** — `rle_crashlanded_v1` in RimWorld's save folder (the scenario auto-loads it)
104+
4. **Save file** — `rle_crashlanded_v1` in RimWorld's save folder (the scenario auto-loads it). The Crashlanded seed includes a built `SimpleResearchBench` so research can leave the 7/31 starting floor.
105105

106106
> **RIMAPI note:** The Workshop version may not have our contributed endpoints yet. See [CLAUDE.md](CLAUDE.md) for instructions on building and deploying our fork DLL. AppSprout runs set `RIMAPI_DLL_PATH` and `RIMAPI_FORK_PATH` to the compiled checkout; summaries record that path, the DLL SHA, and the fork commit. Workshop is not source of truth.
107107

‎docker/saves/rle_crashlanded_v1.rws‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@
185185
<letterQueue />
186186
</letterStack>
187187
<researchManager>
188+
<currentProj>Smithing</currentProj>
188189
<progress>
189190
<keys>
190191
<li>CataphractArmor</li>
@@ -2652,7 +2653,7 @@ AAAAAA==
26522653
<lastSeason>Spring</lastSeason>
26532654
</dateNotifier>
26542655
<uniqueIDsManager>
2655-
<nextThingID>37771</nextThingID>
2656+
<nextThingID>37772</nextThingID>
26562657
<nextFactionID>12</nextFactionID>
26572658
<nextTaleID>4</nextTaleID>
26582659
<nextWorldObjectID>93</nextWorldObjectID>
@@ -458687,6 +458688,22 @@ Hw==
458687458688
<duplicate IsNull="True" />
458688458689
<flight />
458689458690
</thing>
458691+
<thing Class="Building_ResearchBench">
458692+
<def>SimpleResearchBench</def>
458693+
<id>SimpleResearchBench37771</id>
458694+
<map>0</map>
458695+
<pos>(128, 0, 136)</pos>
458696+
<health>180</health>
458697+
<stuff>WoodLog</stuff>
458698+
<faction>Faction_11</faction>
458699+
<questTags IsNull="True" />
458700+
<spawnedTick>0</spawnedTick>
458701+
<despawnedTick>-1</despawnedTick>
458702+
<beenRevealed>True</beenRevealed>
458703+
<billStack>
458704+
<bills />
458705+
</billStack>
458706+
</thing>
458690458707
</things>
458691458708
</li>
458692458709
</maps>

‎saves/README.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22

33
Compressed RimWorld save files used by the benchmark scenarios.
44

5+
`rle_crashlanded_v1` seeds a built, player-owned `SimpleResearchBench`
6+
(wood, `(128, 0, 136)`, no power required) and queues `currentProj=Smithing`.
7+
Without the bench, scoring 1.2 research floors at **7/31 ≈ 0.2258** — the
8+
starting finished/available ratio — because colonists cannot finish tech and
9+
`research_target` is rejected as bench/prereqs missing. That floor is not σ.
10+
The other five scenario saves are derived from this base; rebuild them via
11+
`scripts/create_scenario_saves.py` if they need the same bench.
12+
513
## Install
614

715
Extract to your RimWorld saves folder:

‎saves/rle_crashlanded_v1.rws.gz‎

-68.2 KB
Binary file not shown.

‎scripts/create_scenario_saves.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
Prerequisites:
88
- RimWorld running with RIMAPI mod loaded
99
- Base save `rle_crashlanded_v1` exists and loads cleanly
10+
(seeded SimpleResearchBench + currentProj=Smithing)
1011
1112
Usage:
1213
python scripts/create_scenario_saves.py # build all
@@ -83,6 +84,8 @@ def _default_rimworld_save_dir() -> Path:
8384

8485
# Per-scenario setup recipe. Each scenario is a sequence of API calls.
8586
# `items` entries are (def_name, amount[, stuff_def]) tuples.
87+
# The Crashlanded base save seeds a built SimpleResearchBench and queues
88+
# currentProj=Smithing so research can leave the 7/31 starting floor.
8689
# NOTE on "day advancement": the plan (and issue #7) originally called for
8790
# building saves at day 30 / 60 / etc with shelter, food, and research
8891
# progress. RIMAPI does not currently expose an endpoint to fast-forward

‎src/rle/harness/brief.py‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from pydantic import BaseModel, ConfigDict
1616

1717
from rle.rimapi.api_catalog import visible_write_catalog
18-
from rle.rimapi.schemas import GameState
18+
from rle.rimapi.schemas import GameState, is_research_bench_def
1919
from rle.rimapi.sse_client import RimAPIEvent
2020
from rle.scenarios.schema import ScenarioConfig
2121

@@ -25,6 +25,17 @@
2525
)
2626
_MAX_EVENTS = 12
2727

28+
_MISSING_BENCH_CUE = (
29+
"RESEARCH: no research bench on the map — colonists cannot finish tech "
30+
"(research score stays at the seed floor). Blueprint SimpleResearchBench "
31+
"before research_target, or the write fails with bench/prereqs missing."
32+
)
33+
34+
35+
def research_bench_present(state: GameState) -> bool:
36+
"""True when GameState.structures includes a research bench."""
37+
return any(is_research_bench_def(s.def_name) for s in state.map.structures)
38+
2839

2940
def build_map_summary(state: GameState) -> str | None:
3041
"""Compact (~500 token) spatial summary from terrain + zone + room data.
@@ -102,6 +113,9 @@ def build_map_summary(state: GameState) -> str | None:
102113
f"{fs.harvestable_cells} harvestable."
103114
)
104115

116+
if not research_bench_present(state):
117+
lines.append(_MISSING_BENCH_CUE)
118+
105119
return "\n".join(lines)
106120

107121

@@ -143,6 +157,7 @@ def state_snapshot(state: GameState) -> dict[str, Any]:
143157
],
144158
"resources": state.resources.model_dump(),
145159
"research": state.research.model_dump(),
160+
"research_bench_present": research_bench_present(state),
146161
"threats": [t.model_dump() for t in state.threats],
147162
"weather": state.weather.model_dump(),
148163
"map": {

‎src/rle/orchestration/action_executor.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,9 @@ async def _h_research_target(self, cid: str, params: dict[str, Any]) -> None:
362362
research = None if self._tick_state is None else self._tick_state.research
363363
if research is not None and not force:
364364
status = research_target_status(project, research)
365+
if status == "current":
366+
# Already queued — same success-by-state as a covered growing zone.
367+
return
365368
if status == "finished":
366369
raise ValueError(
367370
f"Research project '{project}' is already finished."

‎src/rle/orchestration/preflight.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
77
* growing-zone cells already covered → success-by-state
88
* work_priority payloads aligned with WorkTypeDef / ``/api/v1/work-list``
9-
* research_target only when the project is available (prereqs / bench)
9+
* research_target only when the project is available (prereqs / bench);
10+
current project is already-satisfied (no rewrite)
1011
* tend requires a living doctor + patient pair
1112
* stockpile_delete is quarantined when the endpoint is missing
1213
"""

‎src/rle/rimapi/client.py‎

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,36 @@
3030
ThreatData,
3131
WeatherData,
3232
ZoneData,
33+
is_research_bench_def,
3334
)
3435

3536
logger = logging.getLogger(__name__)
3637

38+
# GameState.structures is a prompt-sized sample. Research benches must stay
39+
# in that sample even when the map has 400+ ruin walls, or the brief flag
40+
# research_bench_present is a false negative.
41+
_STRUCTURE_SAMPLE_LIMIT = 50
42+
43+
44+
def _building_def_name(building: dict[str, Any]) -> str:
45+
return str(building.get("def_name", building.get("label", "Unknown")))
46+
47+
48+
def _select_buildings_for_state(buildings: Any) -> list[Any]:
49+
"""Research benches first, then other buildings, capped for the snapshot."""
50+
if not isinstance(buildings, list):
51+
return []
52+
benches: list[Any] = []
53+
rest: list[Any] = []
54+
for raw in buildings:
55+
if not isinstance(raw, dict):
56+
continue
57+
if is_research_bench_def(_building_def_name(raw)):
58+
benches.append(raw)
59+
else:
60+
rest.append(raw)
61+
return benches + rest
62+
3763

3864
class RimAPIError(Exception):
3965
"""Base exception for RIMAPI errors."""
@@ -586,7 +612,7 @@ async def get_map(self) -> MapData:
586612
except (RimAPIResponseError, RimAPIConnectionError):
587613
temperature = 15.0
588614
structures = []
589-
for b in buildings[:50]:
615+
for b in _select_buildings_for_state(buildings)[:_STRUCTURE_SAMPLE_LIMIT]:
590616
pos = b.get("position", [0, 0])
591617
if isinstance(pos, dict):
592618
pos = (pos.get("x", 0), pos.get("z", 0))
@@ -596,7 +622,7 @@ async def get_map(self) -> MapData:
596622
pos = (0, 0)
597623
structures.append(StructureData(
598624
structure_id=str(b.get("id", b.get("thing_id", ""))),
599-
def_name=b.get("def_name", b.get("label", "Unknown")),
625+
def_name=_building_def_name(b),
600626
position=pos,
601627
hit_points=float(b.get("hit_points", 100)),
602628
max_hit_points=float(b.get("max_hit_points", 100)),

0 commit comments

Comments
 (0)