# MIT License # # Copyright (c) 2026 Pyxel 100 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. # # The bundled Noto-derived bitmap glyphs are separately licensed under SIL OFL. # See assets/NOTO-LICENSE.txt. """盾のない騎士・第5/5段階。詳細はLESSON.md。""" """Pure 30Hz combat, fixed-point tenths of pixels, perception-bounded enemies.""" from dataclasses import dataclass, replace, asdict from math import hypot import json, hashlib, heapq SCALE = 10 @dataclass(frozen=True) class Level: title: str start: tuple enemies: tuple wall: tuple | None = None LEVELS = (Level('横へ、一歩', (300, 520), (('spear', 940, 520),)), Level('槍の向こう', (380, 520), (('spear', 900, 520), ('axe', 1220, 520))), Level('斧のそばで待つ', (300, 760), (('axe', 820, 540), ('spear', 1170, 380)), (550, 200, 670, 470))) @dataclass(frozen=True) class Input: dx: int = 0 dy: int = 0 sword: bool = False dodge: bool = False @dataclass(frozen=True) class Observation: seen: tuple | None @dataclass(frozen=True) class Memory: point: tuple | None = None wait: int = 0 @dataclass(frozen=True) class Intent: goal: tuple | None start: bool direction: tuple memory: Memory @dataclass(frozen=True) class Enemy: id: int kind: str x: int y: int home: tuple hp: int = 3 stage: str = 'idle' timer: int = 0 direction: tuple = (1000, 0) memory: Memory = Memory() attack_id: int = 0 @dataclass(frozen=True) class Attack: id: int owner: int kind: str x: int y: int direction: tuple remaining: int = 6 hit: tuple = () @dataclass(frozen=True) class State: x: int y: int enemies: tuple facing: tuple = (1000, 0) hp: int = 3 invuln: int = 0 dash: int = 0 dash_cd: int = 0 dash_dir: tuple = (1000, 0) sword: int = 0 sword_dir: tuple = (1000, 0) sword_hit: tuple = () prev_sword: bool = False prev_dodge: bool = False attacks: tuple = () tick: int = 0 serial: int = 0 status: str = 'play' friendly_hits: int = 0 sword_hits: int = 0 damage_cause: str = '' observations: tuple = () resolved_attacks: tuple = () def initial(level=0): b = LEVELS[level] if isinstance(level, int) else level return State(*b.start, tuple((Enemy(i, k, x, y, (x, y)) for i, (k, x, y) in enumerate(b.enemies)))) def unit(dx, dy): h = hypot(dx, dy) return (round(dx / h * 1000), round(dy / h * 1000)) if h else (1000, 0) def segment_box(a, z, box): lo, hi = (0.0, 1.0) for av, dv, mn, mx in ((a[0], z[0] - a[0], box[0], box[2]), (a[1], z[1] - a[1], box[1], box[3])): if dv == 0: if not mn <= av <= mx: return False else: u, v = ((mn - av) / dv, (mx - av) / dv) lo = max(lo, min(u, v)) hi = min(hi, max(u, v)) if lo > hi: return False return True def los(b, a, z): return not b.wall or not segment_box(a, z, b.wall) def valid(b, x, y, r=50): if not 80 + r <= x <= 1520 - r or not 80 + r <= y <= 960 - r: return False return not b.wall or not (b.wall[0] - r < x < b.wall[2] + r and b.wall[1] - r < y < b.wall[3] + r) def move(b, x, y, d, speed): xx = x + round(d[0] * speed / 1000) yy = y + round(d[1] * speed / 1000) if valid(b, xx, y): x = xx if valid(b, x, yy): y = yy return (x, y) def waypoint(b, p, target): if not b.wall: return target x1, y1, x2, y2 = b.wall box = (x1 - 55, y1 - 55, x2 + 55, y2 + 55) if not segment_box(p, target, box): return target nodes = (p, target, (box[0] - 2, box[1] - 2), (box[2] + 2, box[1] - 2), (box[0] - 2, box[3] + 2), (box[2] + 2, box[3] + 2)) q = [(0, 0, ())] seen = set() while q: cost, i, route = heapq.heappop(q) if i in seen: continue seen.add(i) if i == 1: return nodes[route[0]] if route else target for j, z in enumerate(nodes): if j != i and j not in seen and valid(b, *z) and (not segment_box(nodes[i], z, box)): heapq.heappush(q, (cost + hypot(z[0] - nodes[i][0], z[1] - nodes[i][1]), j, route + (j,))) return p def observe(b, e, player): return Observation(player if hypot(player[0] - e.x, player[1] - e.y) <= 700 and los(b, (e.x, e.y), player) else None) def policy(o, m, e): if e.stage != 'idle' or e.hp <= 0: return Intent(None, False, e.direction, m) if o.seen is not None: dist = hypot(o.seen[0] - e.x, o.seen[1] - e.y) reach = 600 if e.kind == 'spear' else 370 return Intent(o.seen, dist <= reach, unit(o.seen[0] - e.x, o.seen[1] - e.y), Memory(o.seen, 12)) if m.point is not None: if hypot(m.point[0] - e.x, m.point[1] - e.y) > 10: return Intent(m.point, False, e.direction, m) if m.wait > 0: return Intent(None, False, e.direction, replace(m, wait=m.wait - 1)) return Intent(e.home, False, e.direction, Memory()) def region_hit(kind, origin, d, p, r=50): dx, dy = (p[0] - origin[0], p[1] - origin[1]) if kind == 'axe': return dx * dx + dy * dy <= (290 + r) ** 2 along = (dx * d[0] + dy * d[1]) / 1000 side = abs(dx * d[1] - dy * d[0]) / 1000 length, half = (520, 70) if kind == 'spear' else (180, 100) outside_x = max(-along, 0, along - length) outside_y = max(0, side - half) return outside_x * outside_x + outside_y * outside_y <= r * r def step(b, s, inp=Input(), friend_damage=2): if s.status != 'play': return s enemies = [] obs = [] serial = s.serial new_attacks = [] for e in s.enemies: o = observe(b, e, (s.x, s.y)) if e.stage == 'idle' and e.hp > 0 else Observation(None) obs.append(o) it = policy(o, e.memory, e) if e.hp <= 0: enemies.append(e) continue if e.stage == 'idle': if it.start: serial += 1 e = replace(e, stage='windup', timer=36 if e.kind == 'spear' else 42, direction=it.direction, memory=it.memory, attack_id=serial) else: xx, yy = (e.x, e.y) if it.goal is not None: dest = waypoint(b, (e.x, e.y), it.goal) if hypot(dest[0] - e.x, dest[1] - e.y) > 6: xx, yy = move(b, e.x, e.y, unit(dest[0] - e.x, dest[1] - e.y), 6) e = replace(e, x=xx, y=yy, memory=it.memory) else: timer = e.timer - 1 if timer <= 0: if e.stage == 'windup': new_attacks.append(Attack(e.attack_id, e.id, e.kind, e.x, e.y, e.direction)) e = replace(e, stage='active', timer=6) elif e.stage == 'active': e = replace(e, stage='recover', timer=42) else: e = replace(e, stage='idle', timer=0) else: e = replace(e, timer=timer) enemies.append(e) dx = max(-1, min(1, inp.dx)) dy = max(-1, min(1, inp.dy)) direction = unit(dx, dy) if dx or dy else s.facing facing = direction if dx or dy else s.facing dash = max(0, s.dash - 1) cd = max(0, s.dash_cd - 1) sw = max(0, s.sword - 1) dd = s.dash_dir sd = s.sword_dir sh = s.sword_hit if inp.sword and (not s.prev_sword) and (not s.sword) and (not s.dash): sw = 24 sd = facing sh = () elif inp.dodge and (not s.prev_dodge) and (not s.sword) and (not s.dash) and (not s.dash_cd): dash = 12 cd = 30 dd = direction speed = 28 if dash else 12 if sw else 15 x, y = move(b, s.x, s.y, dd if dash else direction, speed) if dash or dx or dy else (s.x, s.y) damage = [0] * len(enemies) hurt = 0 cause = s.damage_cause fh = s.friendly_hits swh = s.sword_hits if 13 <= sw <= 18: for e in enemies: if e.hp > 0 and e.id not in sh and region_hit('sword', (x, y), sd, (e.x, e.y)) and los(b, (x, y), (e.x, e.y)): damage[e.id] += 1 sh += (e.id,) swh += 1 attacks = [] resolved = [] for a in s.attacks + tuple(new_attacks): hit = a.hit if -1 not in hit and region_hit(a.kind, (a.x, a.y), a.direction, (x, y)) and los(b, (a.x, a.y), (x, y)): hit += (-1,) if s.invuln == 0 and (not hurt): hurt = 1 cause = f'{a.kind}#{a.id}' for e in enemies: if e.hp > 0 and e.id != a.owner and (e.id not in hit) and region_hit(a.kind, (a.x, a.y), a.direction, (e.x, e.y)) and los(b, (a.x, a.y), (e.x, e.y)): hit += (e.id,) damage[e.id] += friend_damage if friend_damage: fh += 1 resolved.append(replace(a, hit=hit)) if a.remaining > 1: attacks.append(replace(a, remaining=a.remaining - 1, hit=hit)) enemies = tuple((replace(e, hp=max(0, e.hp - damage[e.id]), stage='dead' if e.hp - damage[e.id] <= 0 else e.stage) for e in enemies)) hp = max(0, s.hp - hurt) status = 'lost' if hp == 0 else 'won' if all((e.hp == 0 for e in enemies)) else 'play' return State(x, y, enemies, facing, hp, 30 if hurt else max(0, s.invuln - 1), dash, cd, dd, sw, sd, sh, inp.sword, inp.dodge, tuple(attacks), s.tick + 1, serial, status, fh, swh, cause, tuple(obs), tuple(resolved)) def state_hash(s): return hashlib.sha256(json.dumps(asdict(s), sort_keys=True, separators=(',', ':')).encode('utf-8')).hexdigest() # --- 教材用の操作と描画。上のモデルはPyxelを知らない。 --- from pathlib import Path import argparse STAGE = 5 DEFAULT_LEVEL = 'C' LEVEL_NAMES = ('A', 'B', 'C') DEMO_RUNS = { 'A': ( (3, 1, 0, 0, 0), (42, 0, 0, 0, 0), (24, 1, 0, 0, 0), (1, 1, 0, 1, 0), (4, 1, 0, 0, 0), (3, 0, 0, 0, 0), (1, -1, 0, 0, 0), (3, 0, -1, 0, 0), (7, 0, 0, 0, 0), (1, 0, -1, 0, 0), (1, 0, 1, 0, 0), (1, 0, -1, 0, 0), (1, 0, 1, 0, 0), (1, 0, -1, 0, 0), (1, 0, 1, 0, 0), (1, 0, -1, 0, 0), (1, 0, 1, 0, 0), (1, 0, -1, 0, 0), (1, 0, 1, 0, 0), (1, 0, -1, 0, 0), (1, 0, 1, 0, 0), (1, 0, -1, 0, 0), (1, 0, 1, 0, 0), (1, 0, -1, 0, 0), (1, 0, 1, 0, 0), (1, 0, -1, 0, 0), (1, 0, 1, 0, 0), (1, 0, -1, 0, 0), (1, 0, 1, 0, 0), (1, 0, -1, 0, 0), (1, 0, 1, 0, 0), (1, 0, -1, 0, 0), (1, 0, 1, 0, 0), (1, 0, -1, 0, 0), (1, 0, 1, 0, 0), (1, 0, -1, 0, 0), (1, 0, 1, 0, 0), (1, 0, -1, 0, 0), (1, 0, 1, 0, 0), (1, 0, -1, 0, 0), (1, 0, 1, 0, 0), (1, 0, -1, 0, 0), (1, 0, 1, 0, 0), (1, 0, -1, 0, 0), (1, 0, 1, 0, 0), (1, 0, -1, 0, 0), (1, 0, 1, 0, 0), (1, 0, -1, 0, 0), (1, 0, 1, 0, 0), (1, 0, -1, 0, 0), (1, 0, 1, 0, 0), (1, 1, 0, 1, 0), (1, 1, -1, 0, 0), (23, 0, 0, 0, 0), (1, 1, 0, 1, 0), (1, -1, -1, 0, 0), (5, 0, 0, 0, 0), ), 'B': ( (1, 1, 0, 0, 0), (12, 1, -1, 0, 0), (17, 1, 0, 0, 0), (1, 1, -1, 0, 0), (12, 0, 0, 0, 0), (1, 1, 1, 1, 0), (1, -1, 0, 0, 0), (1, -1, 1, 0, 0), (2, 0, 1, 0, 0), (1, -1, 1, 0, 0), (19, 0, 0, 0, 0), (1, 1, 1, 1, 0), (1, -1, 0, 0, 0), (1, -1, 1, 0, 0), (2, 0, 0, 0, 0), (1, 0, -1, 0, 0), (5, 0, 0, 0, 0), (14, 1, 0, 0, 0), (1, 1, 0, 1, 0), (1, 1, 1, 0, 0), (2, 1, 0, 0, 0), (14, 0, 0, 0, 0), (15, -1, -1, 0, 0), (1, 0, -1, 0, 0), (43, 0, 0, 0, 0), (10, 1, 1, 0, 0), (1, 1, 1, 1, 0), (3, 1, 1, 0, 0), (1, 1, 0, 0, 0), (1, 1, -1, 0, 0), (16, 0, 0, 0, 0), (14, -1, -1, 0, 0), (1, -1, 1, 0, 0), (44, 0, 0, 0, 0), (10, 1, 1, 0, 0), (1, 1, 1, 1, 0), (3, 1, 1, 0, 0), (1, 1, 0, 0, 0), (1, 1, -1, 0, 0), (1, 0, 0, 0, 0), ), 'C': ( (5, 1, -1, 0, 0), (1, 1, 0, 0, 0), (1, 1, -1, 0, 0), (3, 1, 0, 0, 0), (1, -1, -1, 0, 0), (1, 0, 1, 0, 0), (47, 0, 0, 0, 0), (2, 1, -1, 0, 0), (1, 1, 0, 0, 0), (1, 1, -1, 0, 0), (1, 1, 0, 0, 0), (1, 1, -1, 0, 0), (2, 1, 0, 0, 0), (1, 1, -1, 0, 0), (1, 1, 0, 0, 0), (1, 1, -1, 0, 0), (1, 1, 0, 1, 0), (1, 1, -1, 0, 0), (1, 1, 0, 0, 0), (1, 1, -1, 0, 0), (1, 1, 1, 0, 0), (16, 0, 0, 0, 0), (14, -1, 1, 0, 0), (1, -1, -1, 0, 0), (4, 0, 0, 0, 0), (2, 1, -1, 0, 0), (4, 1, 0, 0, 0), (18, 1, -1, 0, 0), (1, 1, 0, 0, 0), (1, 1, -1, 0, 0), (1, 1, 0, 0, 0), (1, 1, -1, 0, 0), (2, 1, 0, 0, 0), (1, 1, -1, 0, 0), (1, 1, 0, 0, 0), (1, 1, -1, 0, 0), (1, 1, 0, 0, 0), (1, 1, -1, 0, 0), (1, 1, 0, 0, 0), (1, 1, -1, 0, 0), (2, 1, 0, 0, 0), (1, 1, -1, 1, 0), (1, 1, 0, 0, 0), (1, 1, -1, 0, 0), (1, 1, 0, 0, 0), (1, 0, -1, 0, 0), (5, 0, 0, 0, 0), (1, 0, 1, 0, 0), (1, 0, -1, 0, 0), (1, 0, 1, 0, 0), (1, 0, -1, 0, 0), (1, 0, 1, 0, 0), (1, 0, -1, 0, 0), (1, 0, 1, 0, 0), (1, 0, -1, 0, 0), (1, 0, 1, 0, 0), (1, 0, -1, 0, 0), (1, 0, 1, 0, 0), (1, 0, -1, 0, 0), (1, 0, 1, 0, 0), (1, 0, -1, 0, 0), (1, 0, 1, 0, 0), (1, 0, -1, 0, 0), (1, 0, 1, 0, 0), (1, 0, -1, 0, 0), (1, 0, 1, 0, 0), (1, 0, -1, 0, 0), (1, 0, 1, 0, 0), (1, 0, -1, 0, 0), (1, 0, 1, 0, 0), (1, 0, -1, 0, 0), (1, 0, 1, 0, 0), (1, 0, -1, 0, 0), (1, 0, 1, 0, 0), (1, 0, -1, 0, 0), (1, 0, 1, 0, 0), (1, 0, -1, 0, 0), (1, 0, 1, 0, 0), (1, 0, -1, 0, 0), (1, 0, 1, 0, 0), (1, 0, -1, 0, 0), (1, 0, 1, 0, 0), (1, 0, -1, 0, 0), (1, 0, 1, 0, 0), (1, 0, -1, 0, 0), (1, 0, 1, 0, 0), (1, 0, -1, 0, 0), (1, 0, 1, 0, 0), (1, 0, -1, 0, 0), (1, 1, 0, 1, 0), (1, 1, 1, 0, 0), (1, 0, -1, 0, 0), (22, 0, 0, 0, 0), (1, 1, 0, 1, 0), (1, -1, 0, 0, 0), (5, 0, 0, 0, 0), ), } def demo_input(frame, level): """1始まりのフレーム。各行は(回数, dx, dy, sword, dodge)。""" for count, dx, dy, sword, dodge in DEMO_RUNS[level]: if frame <= count: return Input(dx, dy, bool(sword), bool(dodge)) frame -= count return Input() class Controller: """キー集合を意味入力へ。R/Pのあとだけ全キー解放を待つ。""" def __init__(self, level=DEFAULT_LEVEL): self.b = LEVELS[LEVEL_NAMES.index(level)] self.s = initial(self.b) self.paused = False self.locked = False self.previous = set() def feed(self, held): held = set(held) pressed = held - self.previous self.previous = held if "R" in pressed: self.s = initial(self.b) self.paused = False self.locked = True return if self.locked: if not held: self.locked = False fields = {name: False for name in ("prev_sword", "prev_dodge") if hasattr(self.s, name)} self.s = replace(self.s, **fields) return if "P" in pressed: self.paused = not self.paused self.locked = True return if self.paused: return inp = Input(int("RIGHT" in held) - int("LEFT" in held), int("DOWN" in held) - int("UP" in held), "Z" in held, "X" in held) self.s = step(self.b, self.s, inp) class App: def __init__(self, args, pyxel): self.p = pyxel self.args = args self.c = Controller(args.level) self.frame = 0 self.saved = False self.p.init(384, 320, title="032 Shieldless Knight - Stage " + str(STAGE), fps=30, display_scale=2, headless=args.headless) self.p.run(self.update, self.draw) def update(self): # 最終フレームはdrawで保存し、その次のupdateで終了する。 if self.saved or self.p.btnp(self.p.KEY_ESCAPE): self.p.quit() return self.frame += 1 if self.args.still: return if self.args.demo: self.c.s = step(self.c.b, self.c.s, demo_input(self.frame, self.args.level)) else: names = ("UP", "DOWN", "LEFT", "RIGHT", "Z", "X", "P", "R") self.c.feed({k for k in names if self.p.btn(getattr(self.p, "KEY_" + k))}) def point(self, x, y): return 32 + x / 5, 45 + y / 5 def hazard(self, kind, x, y, direction, color, fill=False): # 原作と同じregion_hitとlosで、予兆と有効範囲を描く。 for px in range(8, 153): for py in range(8, 97): point = (px * 10, py * 10) if region_hit(kind, (x, y), direction, point, 0) and los(self.c.b, (x, y), point): edge = any(not region_hit(kind, (x, y), direction, (point[0] + dx, point[1] + dy), 0) for dx, dy in ((10, 0), (-10, 0), (0, 10), (0, -10))) if fill or edge or (px + py) % 9 == 0: xx, yy = self.point(*point) self.p.rect(xx, yy, 2, 2, color) def draw(self): p, s = self.p, self.c.s p.cls(1) p.text(12, 8, "032 SHIELDLESS KNIGHT STAGE " + str(STAGE), 7) hp = getattr(s, "hp", "-") p.text(12, 22, f"LEVEL {self.args.level} TICK {s.tick} HP {hp} {s.status.upper()}", 10) p.text(12, 34, f"POSITION (tenths): {s.x},{s.y}", 6) p.rect(48, 61, 288, 176, 3) p.rectb(46, 59, 292, 180, 11) for e in s.enemies: if e.hp > 0 and e.stage == "windup": self.hazard(e.kind, e.x, e.y, e.direction, 4) for a in getattr(s, "resolved_attacks", ()): self.hazard(a.kind, a.x, a.y, a.direction, 10, True) if self.c.b.wall: x1, y1, x2, y2 = self.c.b.wall xx, yy = self.point(x1, y1) p.rect(xx, yy, (x2 - x1) / 5, (y2 - y1) / 5, 5) p.rectb(xx, yy, (x2 - x1) / 5, (y2 - y1) / 5, 13) for e in s.enemies: xx, yy = self.point(e.x, e.y) if e.hp <= 0: p.line(xx - 5, yy - 3, xx + 5, yy + 3, 5) continue if e.stage == "recover": p.circb(xx, yy, 14, 7) p.circ(xx, yy, 7, 8 if e.kind == "spear" else 9) p.rect(xx - 9, yy + 12, 14, 9, 1) p.text(xx - 8, yy + 13, str(e.hp), 7) if e.stage == "windup": p.text(xx - 5, yy - 18, str(e.timer), 8) xx, yy = self.point(s.x, s.y) p.circ(xx, yy, 6, 12) d = s.sword_dir if getattr(s, "sword", 0) else s.facing active = 13 <= getattr(s, "sword", 0) <= 18 length = 36 if active else 16 p.line(xx, yy, xx + d[0] * length / 1000, yy + d[1] * length / 1000, 10 if active else 7) if getattr(s, "dash", 0): p.circb(xx, yy, 12, 6) p.rect(0, 242, 384, 78, 1) p.text(12, 246, "ARROWS MOVE / R RESET / P PAUSE / ESC QUIT", 7) p.text(12, 257, 'Z: SWORD / X: DODGE (NO INVINCIBILITY)', 6) p.text(12, 269, f"SWORD {getattr(s,'sword',0)} DASH {getattr(s,'dash',0)} COOLDOWN {getattr(s,'dash_cd',0)}", 7) p.text(12, 281, f"SWORD HITS {getattr(s,'sword_hits',0)} FRIENDLY HITS {getattr(s,'friendly_hits',0)}", 7) p.text(12, 293, "BLUE: YOU / RED: SPEAR / ORANGE: AXE", 13) label = "PAUSED" if self.c.paused else "ALL ENEMIES DEFEATED" if s.status == "won" else "DEFEATED - R TO RESET" if s.status == "lost" else "" if label: p.rect(85, 135, 220, 20, 1) p.text(100, 142, label, 10) if self.args.frames is not None and self.frame >= self.args.frames: if self.args.output: self.args.output.mkdir(parents=True, exist_ok=True) stem = f"stage{STAGE}_{self.args.level}_{self.frame:03}" p.screenshot(str(self.args.output / (stem + ".png")), scale=2) report = {"stage": STAGE, "level": self.args.level, "frames": self.frame, "headless": self.args.headless, "demo": self.args.demo, "label": "Actual Pyxel render, fixed input, silent; not human playtest", "state": asdict(s), "state_hash": state_hash(s)} (self.args.output / (stem + ".json")).write_text( json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8") self.saved = True def parse_args(argv=None): parser = argparse.ArgumentParser(description="盾のない騎士・第" + str(STAGE) + "段階") parser.add_argument("--level", choices=LEVEL_NAMES, default=DEFAULT_LEVEL) parser.add_argument("--headless", action="store_true") parser.add_argument("--demo", action="store_true", help="固定入力を再生する") parser.add_argument("--still", action="store_true", help="状態を進めず描画する(検証用)") parser.add_argument("--frames", type=int, help="描画フレーム数。1以上") parser.add_argument("--output", type=Path, help="画像とUTF-8状態JSONの保存先") args = parser.parse_args(argv) if args.frames is not None and args.frames < 1: parser.error("--frames must be >= 1") if args.headless and args.frames is None: parser.error("--headless requires --frames") if args.output is not None and args.frames is None: parser.error("--output requires --frames") return args def main(argv=None): args = parse_args(argv) # 無効な引数をPyxelのimportより先に拒否。 import pyxel App(args, pyxel) if __name__ == "__main__": main()