# 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. """片手の料理人・教材3/5。ASCII表示、Python 3.11+、単独実行。""" from dataclasses import dataclass, asdict STAGE = 3 STATIONS = ('SUPPLY', 'GRILL', 'SHELF') FPS = 30 COOK = (60, 120) WARM_TICKS = 180 @dataclass class Plate: id: int kind: int heat: int = 0 ready_at: int | None = None burnt: bool = False class Model: def __init__(self): self.pos = 0 self.hand = None self.shelf = None self.message = "SPACE: TAKE A PLATE" self.oven = None self.choice = 0 self.orders = [1, 0] self.t = 0 def owned(self): return [p for p in (self.hand, self.oven, self.shelf) if p is not None] def available(self): owned_ids = [p.id for p in self.owned()] return [i for i in range(len(self.orders)) if i not in owned_ids] def invariant(self): ids = [p.id for p in self.owned()] assert len(ids) == len(set(ids)), "duplicate plate ownership" assert all(0 <= i < len(self.orders) for i in ids) def move(self, direction): self.pos = max(0, min(len(STATIONS) - 1, self.pos + direction)) def tick(self): self.t += 1 if self.oven is not None: plate = self.oven plate.heat += 1 if plate.ready_at is None and plate.heat >= COOK[plate.kind]: plate.ready_at = self.t # 完成した世界時刻を一度だけ記録 self.message = "READY: TAKE IT OFF THE GRILL" if plate.heat >= COOK[plate.kind] + 60 and not plate.burnt: plate.burnt = True self.message = "BURNT: REMOVE THEN HOLD X" self.message = "BURNT: R TO RESTART THIS PRACTICE" def temperature(self, plate): if plate.burnt: return "BURNT" if plate.ready_at is None: return "RAW" return "HOT" if self.t - plate.ready_at <= WARM_TICKS else "COLD" def action(self): ok = False if self.pos == 0: available = self.available() if self.hand is not None: self.message = "HAND FULL: USE THE SHELF" elif not available: self.message = "NO AVAILABLE ORDER" else: index = available[self.choice % len(available)] self.hand = Plate(index, self.orders[index]) self.message = "TAKEN: ORDER " + str(index + 1) ok = True elif self.pos == 1: if self.hand is not None and self.oven is not None: self.message = "HAND AND GRILL FULL: USE SHELF" elif self.hand is not None: self.oven = self.hand self.hand = None # 移動元を必ず空ける self.message = "PLACED ON GRILL" ok = True elif self.oven is not None: self.hand = self.oven self.oven = None self.message = "REMOVED FROM GRILL" ok = True else: self.message = "GRILL EMPTY: BRING A PLATE" elif self.pos == 2: if self.hand is None and self.shelf is None: self.message = "SHELF EMPTY" else: self.hand, self.shelf = self.shelf, self.hand self.message = "HAND AND SHELF SWAPPED" ok = True self.invariant() return ok class App: def __init__(self): self.reset() def reset(self): self.m = Model() def control(self, pressed, held): # pressedは今フレームの押下、heldは押しているキー。原作の処理順。 if "R" in pressed: self.reset() return for key, direction in (("A", -1), ("LEFT", -1), ("D", 1), ("RIGHT", 1)): if key in pressed: self.m.move(direction) if "W" in pressed or "UP" in pressed: self.m.choice -= 1 if "S" in pressed or "DOWN" in pressed: self.m.choice += 1 if "SPACE" in pressed: self.m.action() self.m.tick() # 操作、廃棄、その後に時間を1つ進める def draw(self, p): m = self.m p.cls(1) p.text(10, 8, "ONE HAND COOK / STAGE " + str(STAGE), 10) p.text(10, 20, "FICTIONAL GAME - NOT FOOD SAFETY GUIDANCE", 6) p.text(10, 36, "TIME " + str(m.t) + " TICKS 30 TICKS = 1 SECOND", 7) p.text(10, 48, "CLOCK PRACTICE / R RESETS A BURNT PLATE", 11) for i, name in enumerate(STATIONS): x = 10 + i * 100 p.rect(x, 74, 92, 100, 4) p.rectb(x, 74, 92, 100, 10 if i == m.pos else 5) p.text(x + 4, 80, name, 7) available = m.available() if available: selected = available[m.choice % len(available)] p.text(14, 104, "ORDER " + str(selected + 1), 10) p.text(14, 116, ("TOAST", "THICK")[m.orders[selected]], 7) else: p.text(14, 104, "NO ORDER", 6) self.draw_plate(p, 114, 101, m.oven) self.draw_plate(p, 214, 101, m.shelf) x = 54 + m.pos * 100 p.circ(x, 190, 6, 15) p.text(10, 208, "HAND", 6) self.draw_plate(p, 64, 204, m.hand) p.text(10, 268, m.message, 10) p.text(10, 282, "A/D OR ARROWS: MOVE SPACE: TRANSFER R: RESET", 6) p.text(10, 294, "W/S OR UP/DOWN: ORDER ESC: QUIT", 6) def draw_plate(self, p, x, y, plate): if plate is None: p.text(x, y, "EMPTY", 6) return p.elli(x + 20, y + 9, 38, 10, 6) color = 0 if plate.burnt else 9 if plate.ready_at is not None else 15 p.rect(x + 27, y, 22, 13, color) p.text(x, y + 23, "ORDER " + str(plate.id + 1), 7) p.text(x, y + 33, ("TOAST", "THICK")[plate.kind], 7) p.text(x, y + 43, f"HEAT {plate.heat} {self.m.temperature(plate)}", 10) if plate.ready_at is not None: p.text(x, y + 53, "AGE " + str(self.m.t - plate.ready_at), 6) def demo_plan(): # 自動入力をcontrolへ渡す。人のキーボード入力を確認するものではない。 return [("act",), ("go", 1), ("act",), ("ready",), ("act",), ("go", 2), ("act",), ("wait", 181)] class Demo: def __init__(self): self.plan = demo_plan() self.release = False self.wait = 0 def keys(self, app): if self.release: self.release = False return set() if self.wait: self.wait -= 1 return set() if not self.plan: return set() m = app.m if getattr(m, "moving", 0): return set() command = self.plan[0] if command[0] == "go" and m.pos != command[1]: self.release = True return {"D" if command[1] > m.pos else "A"} if command[0] == "ready" and (m.oven is None or m.oven.ready_at is None): return set() self.plan.pop(0) if command[0] == "act": self.release = True return {"SPACE"} if command[0] == "wait": self.wait = command[1] return set() def main(): import argparse import json from pathlib import Path import pyxel parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--headless", action="store_true") parser.add_argument("--demo", action="store_true") parser.add_argument("--frames", type=int, default=0) parser.add_argument("--capture", type=Path) parser.add_argument("--state", type=Path) args = parser.parse_args() app = App() demo = Demo() frames = 0 mapping = {name: getattr(pyxel, "KEY_" + name) for name in ("A", "D", "LEFT", "RIGHT", "W", "S", "UP", "DOWN", "SPACE", "R", "X", "Z", "P")} pyxel.init(420, 340, title="006 One Hand Cook - Stage " + str(STAGE), fps=FPS, display_scale=2, quit_key=pyxel.KEY_NONE, headless=args.headless) def update(): nonlocal frames if pyxel.btnp(pyxel.KEY_ESCAPE): pyxel.quit() return if args.demo: pressed = demo.keys(app) held = pressed else: pressed = {name for name, key in mapping.items() if pyxel.btnp(key)} held = {name for name, key in mapping.items() if pyxel.btn(key)} app.control(pressed, held) frames += 1 def draw(): app.draw(pyxel) if args.demo: pyxel.text(250, 8, "AUTOMATIC CONTROLLER DEMO", 8) if args.frames and frames >= args.frames: if args.capture: args.capture.parent.mkdir(parents=True, exist_ok=True) pyxel.screenshot(str(args.capture), scale=2) if args.state: data = {key: value for key, value in vars(app.m).items() if key != "history"} data = json.loads(json.dumps(data, default=lambda value: asdict(value))) data["history_count"] = len(getattr(app.m, "history", [])) data["automatic_controller_demo"] = args.demo args.state.parent.mkdir(parents=True, exist_ok=True) args.state.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") pyxel.quit() pyxel.run(update, draw) if __name__ == "__main__": main()