Skip to content

Commit 400778c

Browse files
committed
Programmatical play API now supports buying metro lines
1 parent 7e8da05 commit 400778c

4 files changed

Lines changed: 57 additions & 0 deletions

File tree

PROGRESS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,4 @@
5555
- Updated game rules docs for purchase-based line unlocks and the new locked-button hover/click behavior.
5656
- Changed UI font usage to `courier` via shared `font_name` config.
5757
- Expanded `README.md` programmatic play docs to list the `MiniMetroEnv` API, action schemas, valid input constraints, and observation/step return fields.
58+
- Added programmatic `buy_line` action to purchase metro lines through `env.step(...)`, with optional `path_index` targeting and validation.

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,13 @@ obs, reward, done, info = env.step({"type": "remove_path", "path_index": 0})
6464
- Valid only when `0 <= k < len(observation["structured"]["paths"])`.
6565
- `{"type": "remove_path", "path_id": "..."}`
6666
- Removes an existing path by path id string from `observation["structured"]["paths"][*]["id"]`.
67+
- `{"type": "buy_line"}`
68+
- Buys the next locked line if affordable.
69+
- Price follows configured incremental unlock costs (derived from `path_unlock_milestones`).
70+
- `{"type": "buy_line", "path_index": k}`
71+
- Attempts to buy a specific locked line button index.
72+
- Must be the next purchasable locked index (sequential purchase rule); otherwise fails.
73+
- `path_index` must be an integer in `[0, num_paths - 1]`.
6774
- `{"type": "pause"}`
6875
- Pauses simulation updates.
6976
- `{"type": "resume"}`

src/mediator.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,17 @@ def try_purchase_path_button(self, button: PathButton) -> bool:
206206
self.update_unlocked_num_paths()
207207
return True
208208

209+
def try_purchase_path_button_by_index(
210+
self, button_idx: int | None = None
211+
) -> bool:
212+
if button_idx is None:
213+
button_idx = self.get_next_path_button_idx_to_purchase()
214+
if button_idx is None:
215+
return False
216+
if button_idx < 0 or button_idx >= len(self.path_buttons):
217+
return False
218+
return self.try_purchase_path_button(self.path_buttons[button_idx])
219+
209220
def step_time(self, dt_ms: int) -> None:
210221
self.increment_time(dt_ms)
211222

@@ -524,6 +535,11 @@ def apply_action(self, action: Dict) -> bool:
524535
stations = action.get("stations", [])
525536
loop = bool(action.get("loop", False))
526537
return self.create_path_from_station_indices(stations, loop) is not None
538+
if action_type == "buy_line":
539+
button_idx = action.get("path_index")
540+
if button_idx is not None and not isinstance(button_idx, int):
541+
return False
542+
return self.try_purchase_path_button_by_index(button_idx)
527543
if action_type == "remove_path":
528544
if "path_id" in action:
529545
return self.remove_path_by_id(action["path_id"])

test/test_env.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,39 @@ def test_path_unlock_progression_uses_score_purchases(self):
264264
self.assertTrue(env.mediator.try_purchase_path_button(second_button))
265265
self.assertEqual(env.mediator.unlocked_num_paths, 2)
266266

267+
def test_step_buy_line_purchases_next_locked_line(self):
268+
env = MiniMetroEnv()
269+
env.reset(seed=17)
270+
env.mediator.score = env.mediator.path_purchase_prices[0]
271+
272+
_, _, _, info = env.step({"type": "buy_line"})
273+
274+
self.assertTrue(info["action_ok"])
275+
self.assertEqual(env.mediator.unlocked_num_paths, 2)
276+
self.assertFalse(env.mediator.path_buttons[1].is_locked)
277+
self.assertEqual(env.mediator.score, 0)
278+
279+
def test_step_buy_line_supports_path_index(self):
280+
env = MiniMetroEnv()
281+
env.reset(seed=18)
282+
env.mediator.score = env.mediator.path_purchase_prices[0]
283+
284+
_, _, _, info = env.step({"type": "buy_line", "path_index": 1})
285+
286+
self.assertTrue(info["action_ok"])
287+
self.assertEqual(env.mediator.unlocked_num_paths, 2)
288+
289+
def test_step_buy_line_invalid_index_returns_false(self):
290+
env = MiniMetroEnv()
291+
env.reset(seed=19)
292+
env.mediator.score = 10_000
293+
294+
_, _, _, info = env.step({"type": "buy_line", "path_index": "1"})
295+
self.assertFalse(info["action_ok"])
296+
297+
_, _, _, info = env.step({"type": "buy_line", "path_index": 99})
298+
self.assertFalse(info["action_ok"])
299+
267300
def test_reward_increments_on_passenger_delivery(self):
268301
env = MiniMetroEnv()
269302
env.reset(seed=13)

0 commit comments

Comments
 (0)