# 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. """片手の料理人・教材4/5。ASCII表示、Python 3.11+、単独実行。""" from dataclasses import dataclass, asdict STAGE = 4 STATIONS = ('SUPPLY', 'GRILL', 'SHELF', 'SERVE') 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, 0, 1] self.delivered = [] self.warm = 0 self.waste = 0 self.status = "play" self.t = 0 def owned(self): return [p for p in (self.hand, self.oven, self.shelf) if p is not None] def active(self): count = min(len(self.orders), len(self.delivered) + 2) return [i for i in range(count) if i not in self.delivered] def available(self): owned_ids = [p.id for p in self.owned()] return [i for i in self.active() if i not in owned_ids] def invariant(self): ids = [p.id for p in self.owned()] + self.delivered assert len(ids) == len(set(ids)), "duplicate plate ownership" assert all(0 <= i < len(self.orders) for i in ids) assert self.warm <= len(self.delivered) def move(self, direction): self.pos = max(0, min(len(STATIONS) - 1, self.pos + direction)) def tick(self): if self.status != "play": return 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" 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): if self.status != "play": return False 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 else: if self.hand is None: self.message = "BRING A PLATE TO SERVE" elif self.hand.burnt: self.message = "BURNT: HOLD X TO DISCARD" elif self.hand.ready_at is None: self.message = "RAW: RETURN IT TO THE GRILL" else: plate = self.hand hot = self.t - plate.ready_at <= WARM_TICKS self.delivered.append(plate.id) self.warm += hot self.hand = None self.message = "HOT DELIVERY" if hot else "COLD DELIVERY IS STILL VALID" ok = True if len(self.delivered) == len(self.orders): self.status = "done" self.invariant() return ok def discard(self): if self.hand is None or not self.hand.burnt: self.message = "ONLY A BURNT HAND PLATE CAN BE DISCARDED" return self.hand = None self.waste += 1 self.message = "ORDER CAN NOW BE TAKEN AGAIN" self.invariant() class App: def __init__(self): self.reset() def reset(self): self.m = Model() self.discard_frames = 0 def control(self, pressed, held): # pressedは今フレームの押下、heldは押しているキー。原作の処理順。 if "R" in pressed: self.reset() return if self.m.status != "play": 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.discard_frames = self.discard_frames + 1 if "X" in held else 0 if self.discard_frames == 24: self.m.discard() 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, f"SERVED {len(m.delivered)}/4 HOT {m.warm} WASTE {m.waste}", 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) p.text(314, 110, "SPACE: SERVE", 10) p.text(314, 126, m.status.upper(), 11) 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) p.text(10, 306, "HOLD X 24 FRAMES: DISCARD BURNT HAND PLATE", 6) p.rect(240, 244, min(160, self.discard_frames * 160 / 24), 4, 8) 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へ渡す。人のキーボード入力を確認するものではない。 pair = [("go", 0), ("act",), ("go", 1), ("act",), ("go", 0), ("act",), ("go", 2), ("act",), ("go", 1), ("ready",), ("act",), ("go", 2), ("act",), ("go", 1), ("act",), ("go", 2), ("act",), ("go", 3), ("act",), ("go", 1), ("ready",), ("act",), ("go", 3), ("act",)] return pair * 2 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()