# 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. """おばけ洗濯日和・教材 2/5。原作を簡略化した独立実行ファイル。 実行: python 02_catch.py コード・図形: MIT。原作者と条件は隣の LICENSE を参照。 """ from dataclasses import dataclass WIDTH, HEIGHT = 240, 180 GROUND = 136 FPS = 30 CAPACITY = 8 SHEET_POSITIONS = (120, 150, 90, 60, 180) @dataclass class Drop: x: float y: float = 24 class Game: def __init__(self): self.x = WIDTH / 2 self.frames = 0 self.cargo = 0 self.drops = [] def step(self, direction=0): self.frames += 1 # 左右を同時に押すと direction は0。画面の外に出さない。 self.x = max(10, min(WIDTH - 10, self.x + direction * 2)) if self.frames % 30 == 1: index = ((self.frames - 1) // 30) % len(SHEET_POSITIONS) self.drops.append(Drop(SHEET_POSITIONS[index])) # 元リストから削除するため、コピーを使って全ての落下物を調べる。 for drop in self.drops[:]: drop.y += 1.5 if drop.y >= GROUND: if abs(self.x - drop.x) < 12: self.cargo = min(CAPACITY, self.cargo + 1) self.drops.remove(drop) def main(headless=False, screenshot=None): # 純粋な Game のテストではPyxelを読み込まない。 import pyxel game = Game() def update(): nonlocal game if headless and pyxel.frame_count >= 12: if screenshot: pyxel.screenshot(screenshot, scale=2) pyxel.quit() return if pyxel.btnp(pyxel.KEY_R): game = Game() return direction = int(pyxel.btn(pyxel.KEY_RIGHT)) - int(pyxel.btn(pyxel.KEY_LEFT)) game.step(direction) def draw(): pyxel.cls(1) pyxel.text(8, 8, "GHOST LAUNDRY / STEP 2", 10) pyxel.line(0, GROUND + 8, WIDTH - 1, GROUND + 8, 3) pyxel.text(8, 20, f"CARRY {game.cargo}/{CAPACITY}", 7) for drop in game.drops: pyxel.rect(drop.x - 5, drop.y - 3, 10, 6, 7) x = game.x pyxel.circ(x, GROUND - 6, 7, 7) pyxel.rect(x - 7, GROUND - 6, 15, 11, 7) pyxel.pset(x - 3, GROUND - 7, 1) pyxel.pset(x + 3, GROUND - 7, 1) for i in range(game.cargo): pyxel.rect(x - 5, GROUND - 18 - i * 4, 10, 3, 6) pyxel.text(8, 164, "LEFT/RIGHT: MOVE R: RESET ESC: QUIT", 6) pyxel.init(WIDTH, HEIGHT, title="Ghost Laundry - Stage 2", fps=FPS, 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)