Skip to content

Commit 0528e46

Browse files
authored
Allow shed actions from locked shed-access tiles in kaggriculture (#1392)
The four shed-access tiles are one per quadrant, and only NW starts unlocked, so three of them begin LOCKED. The LOCKED guard ran before PICKUP/DROP/PLACE, which meant a unit standing on one of those tiles could not use the shed it was standing next to -- including the first hire of each day, whose least-occupied spawn lands on (5,4). Move those three ops above the guard. They only use the tile as a standing position; the shed is not a tile and is never locked. Every op that mutates the standing tile stays below the guard, so PLANT, BUILD_COOP, BUILD_PASTURE, DIG and the rest still no-op on locked ground and consume nothing. PLACE spans both cases. Its animal branch requires a dict tile and a LOCKED tile is the string "LOCKED", so it cannot match there and falls through to the shed path; covered by test rather than changed. Also correct two README lines that claimed all tile actions no-op on locked tiles.
1 parent a4dce5a commit 0528e46

3 files changed

Lines changed: 97 additions & 37 deletions

File tree

kaggle_environments/envs/kaggriculture/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ Each Farmer / Farm Hand can be given an action every turn. Farmer/Farm Hand CAN
4040

4141
#### Movement
4242

43-
- NORTH, SOUTH, EAST, WEST — Move one cell in that direction. Moves off the edge of the board are no-ops. Locked tiles are passable: a unit may move onto and across unbought quadrants, but tile actions (`PLANT`, `WATER`, `BUILD_*`, etc.) all no-op on a locked tile and consume nothing.
43+
- NORTH, SOUTH, EAST, WEST — Move one cell in that direction. Moves off the edge of the board are no-ops. Locked tiles are passable: a unit may move onto and across unbought quadrants, but tile actions (`PLANT`, `WATER`, `BUILD_*`, etc.) all no-op on a locked tile and consume nothing. The exception is the shed actions `PICKUP`, `DROP`, and `PLACE`-into-shed, which work from any shed-access tile even while that tile is locked — they use the tile only as a standing position and never change it.
4444

4545
#### Shed
4646

@@ -146,7 +146,7 @@ Each player has their own farm with a set number of squares. Players are unable
146146
- Farmer and hired farm hands drop their inventory at the end of the day in the shed (if there is room)
147147
- Limited to 100 items, excluding seeds. Once the shed is full, any further items added (via `PLACE` mid-day or end-of-day inventory drop) are discarded — there is no overflow holding area, so stockpiling on farmer/hand inventories does not bypass the cap.
148148

149-
The shed sits at the center of the board and is not a tile — it never appears in the `tiles` array, whose only values are `None`, `"LOCKED"`, and structure dicts. "Orthogonally adjacent to the shed" means standing on one of the four center tiles, `(half-1, half-1)`, `(half, half-1)`, `(half-1, half)`, `(half, half)` for `half = boardSize // 2`. At the default `boardSize = 10` those are `(4,4)`, `(5,4)`, `(4,5)`, and `(5,5)`, one in each quadrant.
149+
The shed sits at the center of the board and is not a tile — it never appears in the `tiles` array, whose only values are `None`, `"LOCKED"`, and structure dicts. "Orthogonally adjacent to the shed" means standing on one of the four center tiles, `(half-1, half-1)`, `(half, half-1)`, `(half-1, half)`, `(half, half)` for `half = boardSize // 2`. At the default `boardSize = 10` those are `(4,4)`, `(5,4)`, `(4,5)`, and `(5,5)`, one in each quadrant. Since only NW starts unlocked, three of those four tiles begin locked; the shed is reachable from all of them regardless, because the shed itself is never locked.
150150

151151
### Farmer/Farm Hand
152152

kaggle_environments/envs/kaggriculture/kaggriculture.py

Lines changed: 44 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -321,9 +321,11 @@ def _apply_unit_action(farm, private, idx, action, board_size, day, turns_per_da
321321
return
322322

323323
tile = farm["tiles"][fy][fx]
324-
if tile == "LOCKED":
325-
return
326324

325+
# Shed operations resolve before the LOCKED guard. They use the tile only as
326+
# a standing position -- the shed itself is always owned -- and three of the
327+
# four shed-access tiles start LOCKED, so guarding them first would make the
328+
# shed unreachable from those tiles.
327329
if op == "DROP":
328330
if not _is_shed_adjacent((fx, fy), board_size):
329331
return
@@ -358,6 +360,46 @@ def _apply_unit_action(farm, private, idx, action, board_size, day, turns_per_da
358360
_inv_add(inv, item, n)
359361
return
360362

363+
if op == "PLACE":
364+
if len(action) < 2:
365+
return
366+
item = action[1]
367+
# Animal placement: standing on a matching unoccupied structure. A LOCKED
368+
# tile is the string "LOCKED", never a dict, so this branch cannot match
369+
# there and PLACE falls through to the shed path below.
370+
if (
371+
item in ANIMALS
372+
and isinstance(tile, dict)
373+
and tile.get("kind") == ANIMALS[item]["structure"]
374+
and "animal" not in tile
375+
):
376+
if _inv_take(inv, item, 1):
377+
farm["tiles"][fy][fx] = _new_animal(item, day)
378+
return
379+
# Shed drop: orthogonally adjacent to the shed; obeys shedCapacity.
380+
if _is_shed_adjacent((fx, fy), board_size):
381+
n = int(action[2]) if len(action) >= 3 else 1
382+
if n <= 0:
383+
return
384+
n = min(n, inv.get(item, 0))
385+
if n <= 0:
386+
return
387+
current = sum(private["shed"].values())
388+
room = max(0, shed_capacity - current)
389+
n = min(n, room)
390+
if n <= 0:
391+
return
392+
inv[item] -= n
393+
if inv[item] == 0:
394+
del inv[item]
395+
private["shed"][item] = private["shed"].get(item, 0) + n
396+
return
397+
398+
# Everything below mutates the tile the unit stands on, so it requires that
399+
# tile to be owned.
400+
if tile == "LOCKED":
401+
return
402+
361403
if op == "PLANT":
362404
if len(action) < 2:
363405
return
@@ -446,39 +488,6 @@ def _apply_unit_action(farm, private, idx, action, board_size, day, turns_per_da
446488
farm["tiles"][fy][fx] = {"kind": "PASTURE"}
447489
return
448490

449-
if op == "PLACE":
450-
if len(action) < 2:
451-
return
452-
item = action[1]
453-
# Animal placement: standing on a matching unoccupied structure.
454-
if (
455-
item in ANIMALS
456-
and isinstance(tile, dict)
457-
and tile.get("kind") == ANIMALS[item]["structure"]
458-
and "animal" not in tile
459-
):
460-
if _inv_take(inv, item, 1):
461-
farm["tiles"][fy][fx] = _new_animal(item, day)
462-
return
463-
# Shed drop: orthogonally adjacent to the shed; obeys shedCapacity.
464-
if _is_shed_adjacent((fx, fy), board_size):
465-
n = int(action[2]) if len(action) >= 3 else 1
466-
if n <= 0:
467-
return
468-
n = min(n, inv.get(item, 0))
469-
if n <= 0:
470-
return
471-
current = sum(private["shed"].values())
472-
room = max(0, shed_capacity - current)
473-
n = min(n, room)
474-
if n <= 0:
475-
return
476-
inv[item] -= n
477-
if inv[item] == 0:
478-
del inv[item]
479-
private["shed"][item] = private["shed"].get(item, 0) + n
480-
return
481-
482491
if op == "FEED":
483492
if not (isinstance(tile, dict) and "animal" in tile):
484493
return

tests/envs/kaggriculture/test_kaggriculture.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,57 @@ def test_movement_blocked_off_map():
162162
assert farm["hands"][0] == [9, 9]
163163

164164

165+
def test_shed_ops_work_from_a_locked_shed_access_tile():
166+
"""Three of the four shed-access tiles start locked; the shed still works."""
167+
farm = _new_farm(10, 100)
168+
private = _new_private()
169+
private["shed"]["WHEAT"] = 5
170+
# (5, 4) is a shed-access tile in the NE quadrant, so it starts locked.
171+
farm["farmer"] = [5, 4]
172+
assert farm["tiles"][4][5] == "LOCKED"
173+
174+
_apply_unit_action(farm, private, 0, ["PICKUP", "WHEAT", 2], 10, 0, 24)
175+
assert private["inventories"][0]["WHEAT"] == 2
176+
assert private["shed"]["WHEAT"] == 3
177+
178+
_apply_unit_action(farm, private, 0, ["PLACE", "WHEAT", 1], 10, 0, 24)
179+
assert private["inventories"][0]["WHEAT"] == 1
180+
assert private["shed"]["WHEAT"] == 4
181+
182+
_apply_unit_action(farm, private, 0, ["DROP"], 10, 0, 24)
183+
assert private["inventories"][0].get("WHEAT", 0) == 0
184+
assert private["shed"]["WHEAT"] == 5
185+
186+
187+
def test_tile_ops_still_noop_on_locked_shed_access_tile():
188+
"""Resolving shed ops early must not let tile ops run on locked ground."""
189+
farm = _new_farm(10, 100)
190+
private = _new_private()
191+
private["seeds"]["CARROT"] = 3
192+
farm["farmer"] = [5, 4]
193+
assert farm["tiles"][4][5] == "LOCKED"
194+
195+
for action in (["PLANT", "CARROT"], ["BUILD_COOP"], ["BUILD_PASTURE"], ["DIG"]):
196+
_apply_unit_action(farm, private, 0, action, 10, 0, 24)
197+
assert farm["tiles"][4][5] == "LOCKED", action
198+
assert private["seeds"]["CARROT"] == 3
199+
200+
201+
def test_place_animal_still_noop_on_locked_tile():
202+
"""PLACE moved above the guard, but animals need an owned structure."""
203+
farm = _new_farm(10, 100)
204+
private = _new_private()
205+
# (5, 4) is locked and shed-adjacent: the animal branch must not match, and
206+
# the shed fallback must not silently swallow the animal either.
207+
farm["farmer"] = [5, 4]
208+
private["inventories"][0]["CHICKEN"] = 1
209+
_apply_unit_action(farm, private, 0, ["PLACE", "CHICKEN"], 10, 0, 24)
210+
assert farm["tiles"][4][5] == "LOCKED"
211+
# It went into the shed as a plain item, not onto the locked tile.
212+
assert private["inventories"][0].get("CHICKEN", 0) == 0
213+
assert private["shed"]["CHICKEN"] == 1
214+
215+
165216
# --- Shed / pickup / inventory ---------------------------------------------
166217

167218
def test_pickup_requires_shed_adjacency():

0 commit comments

Comments
 (0)