# MIT License # # Copyright (c) 2026 Tidepost contributors # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # # Exception: assets/glyphs.json contains rasterized glyph data derived from # Noto Sans CJK JP (SIL Open Font License 1.1). See assets/NOTO-LICENSE.txt. # Pyxel is an external dependency under its own MIT license. """一歩貸してください・教材 1/5。原作1部屋目の仕組みを段階化。 実行: python 01_grid.py コード・図形: MIT。原作者と条件は隣の LICENSE を参照。 """ from dataclasses import dataclass ROWS = ("#######", "#P..G.#", "#.....#", "#E.S..#", "#######") DIRECTIONS = {"U": (0, -1), "D": (0, 1), "L": (-1, 0), "R": (1, 0), "W": (0, 0)} TILE = 24 def find(char): return next((x, y) for y, row in enumerate(ROWS) for x, cell in enumerate(row) if cell == char) def move(position, command): dx, dy = DIRECTIONS[command] x, y = position[0] + dx, position[1] + dy if 0 <= y < len(ROWS) and 0 <= x < len(ROWS[y]) and ROWS[y][x] != "#": return x, y return position @dataclass(frozen=True) class State: player: tuple = find("P") turns: int = 0 def step(state, command): if command not in DIRECTIONS: return state player = move(state.player, command) return State(player=player, turns=state.turns + 1) def main(headless=False, screenshot=None): import pyxel state = State() keys = ((pyxel.KEY_UP, "U"), (pyxel.KEY_DOWN, "D"), (pyxel.KEY_LEFT, "L"), (pyxel.KEY_RIGHT, "R"), (pyxel.KEY_SPACE, "W")) def update(): if headless and pyxel.frame_count >= 12: if screenshot: pyxel.screenshot(screenshot, scale=2) pyxel.quit() return nonlocal state if pyxel.btnp(pyxel.KEY_R): state = State() return for key, command in keys: if pyxel.btnp(key): state = step(state, command) break # 同じフレームに複数の一手を進めない。 def draw(): current = state pyxel.cls(1) pyxel.text(10, 8, "ECHO STEPS / STEP 1", 10) pyxel.text(10, 20, f"TURNS {current.turns}", 7) for y, row in enumerate(ROWS): for x, char in enumerate(row): px, py = 10 + x * TILE, 40 + y * TILE pyxel.rect(px, py, TILE - 1, TILE - 1, 5 if char == "#" else 0) x, y = current.player pyxel.rect(16 + x * TILE, 46 + y * TILE, 12, 12, 12) pyxel.text(10, 190, "ARROWS: STEP SPACE: WAIT", 6) pyxel.text(10, 202, "R: RESET ESC: QUIT", 6) pyxel.init(188, 218, title="Echo Steps - Stage 1", fps=30, headless=headless) pyxel.run(update, draw) if __name__ == "__main__": import argparse parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--headless-smoke", action="store_true", help="12 frames then quit; not a playtest") parser.add_argument("--screenshot", help="PNG path used with --headless-smoke") args = parser.parse_args() main(headless=args.headless_smoke, screenshot=args.screenshot)