Download code/environments/snake/task.py from markuz89/minijev-snake: direct link, hf CLI and curl.
- Browser
- Download file 4.8 kB
-
https://huggingface.co/markuz89/minijev-snake/resolve/main/code/environments/snake/task.py
- Command line
-
hf download hf://markuz89/minijev-snake/code/environments/snake/task.py
-
curl -L -o task.py https://huggingface.co/markuz89/minijev-snake/resolve/main/code/environments/snake/task.py
4.8 kB
| """Snake as a MiniJEV task: SnakeState -> multi-channel board image. | |
| Channels (board padded by a 1-cell wall border; smaller boards in a batch are | |
| padded with more wall, which is semantically exact): | |
| 0 wall 1 head 2 body (excl. head) 3 tail 4 food | |
| 5 time-until-free: (L - i) / area for body segment i (head=0) | |
| 6 row offset from the head (r - r_head) / max(H, W) | |
| 7 col offset from the head (c - c_head) / max(H, W) | |
| Channel 5 lets the model reason about which cells will open up as the snake | |
| moves - the same information the oracle's time-aware search uses. Channels 6-7 | |
| make every cell egocentric ("the food is 3 up, 2 left"), which removes the | |
| long learning plateau of discovering relative geometry from absolute positions. | |
| """ | |
| from __future__ import annotations | |
| from typing import Sequence | |
| import numpy as np | |
| from minijev.tasks import Task, ragged_pack, ragged_take | |
| from .env import ACTIONS, SnakeState | |
| N_CHANNELS = 8 | |
| DIRS = {a: i for i, a in enumerate(ACTIONS)} | |
| SEMANTIC_CHOICES = {"UP": "move up", "DOWN": "move down", "LEFT": "move left", "RIGHT": "move right"} | |
| class SnakeTask(Task): | |
| """task.choice_style: "semantic" -> choices "move up/down/left/right" (v2 models); | |
| "legacy" (default, what pre-v2 checkpoints were trained with) -> "UP/DOWN/LEFT/RIGHT".""" | |
| name = "snake" | |
| state_encoder = "grid" | |
| default_question = "Choose the next move" | |
| def choice_alias(self) -> dict[str, str]: | |
| return SEMANTIC_CHOICES if self.cfg.get("choice_style", "legacy") == "semantic" else {} | |
| def default_choices(self) -> tuple[str, ...]: | |
| return tuple(self.model_label(a) for a in ACTIONS) | |
| def encoder_class(self): | |
| from .encoder import SnakeStateEncoder | |
| return SnakeStateEncoder | |
| def encoder_config(self) -> dict: | |
| return {"in_channels": N_CHANNELS} | |
| def states_to_arrays(self, states: Sequence[SnakeState]) -> dict[str, np.ndarray]: | |
| bodies = [[r * s.width + c for r, c in s.body] for s in states] | |
| flat, offsets = ragged_pack(bodies, np.int16) | |
| return { | |
| "body": flat, | |
| "body_offsets": offsets, | |
| "food": np.array([s.food[0] * s.width + s.food[1] if s.food else -1 for s in states], np.int16), | |
| "height": np.array([s.height for s in states], np.uint8), | |
| "width": np.array([s.width for s in states], np.uint8), | |
| "direction": np.array([DIRS[s.direction] for s in states], np.uint8), | |
| } | |
| def example_states(self, n=4, seed=0): | |
| from .agents import OracleAgent | |
| from .env import SnakeConfig, SnakeEnv | |
| states, oracle = [], OracleAgent() | |
| env = SnakeEnv(SnakeConfig.from_dict({"board_size": self.cfg.get("board_size", 10)}), seed=seed) | |
| while len(states) < n: | |
| states.append(env.state()) | |
| for _ in range(7): # spread samples along the game | |
| env.step(oracle.act(env.state())) | |
| if env.done: | |
| env.reset() | |
| return states | |
| def encode_arrays(self, arrays, idx): | |
| return {"state_grid": encode_grid(arrays, np.asarray(idx))} | |
| def encode_grid(arrays: dict[str, np.ndarray], idx: np.ndarray) -> np.ndarray: | |
| B = len(idx) | |
| hs = arrays["height"][idx].astype(np.int64) | |
| ws = arrays["width"][idx].astype(np.int64) | |
| H, W = int(hs.max()) + 2, int(ws.max()) + 2 | |
| x = np.zeros((B, N_CHANNELS, H, W), dtype=np.float32) | |
| x[:, 0] = 1.0 | |
| for h, w in set(zip(hs.tolist(), ws.tolist())): | |
| sel = np.nonzero((hs == h) & (ws == w))[0] | |
| x[sel, 0, 1:h + 1, 1:w + 1] = 0.0 | |
| cells, lengths = ragged_take(arrays["body"], arrays["body_offsets"], idx) | |
| cells = cells.astype(np.int64) | |
| sample = np.repeat(np.arange(B), lengths) | |
| pos = np.arange(len(cells)) - np.repeat(np.cumsum(lengths) - lengths, lengths) # index from head | |
| Ls = np.repeat(lengths, lengths) | |
| w_s = ws[sample] | |
| r, c = cells // w_s + 1, cells % w_s + 1 | |
| head = pos == 0 | |
| x[sample[head], 1, r[head], c[head]] = 1.0 | |
| x[sample[~head], 2, r[~head], c[~head]] = 1.0 | |
| tail = pos == Ls - 1 | |
| x[sample[tail], 3, r[tail], c[tail]] = 1.0 | |
| x[sample, 5, r, c] = (Ls - pos) / (hs[sample] * w_s) | |
| hr, hc = np.zeros(B, np.int64), np.zeros(B, np.int64) | |
| hr[sample[head]], hc[sample[head]] = r[head], c[head] | |
| scale = np.maximum(hs, ws).astype(np.float32)[:, None, None] | |
| x[:, 6] = (np.arange(H)[None, :, None] - hr[:, None, None]) / scale | |
| x[:, 7] = (np.arange(W)[None, None, :] - hc[:, None, None]) / scale | |
| food = arrays["food"][idx].astype(np.int64) | |
| has = np.nonzero(food >= 0)[0] | |
| x[has, 4, food[has] // ws[has] + 1, food[has] % ws[has] + 1] = 1.0 | |
| return x | |
| def make_task(cfg: dict) -> SnakeTask: | |
| return SnakeTask(cfg) | |