# 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. """お弁当列車・教材2/5。原作Bを段階的に組み立てる。""" from dataclasses import dataclass, replace STOCK = ("F", "F", "V", "V", "E", "E") KINDS = "FVE" PRESET = ((4, 0, 1), (3, 2, 5)) ORDERS = ((1, 1, 0), (1, 0, 1), (0, 1, 1)) WORK = (3, 2, 3) GOAL = 5 CAPACITY = 3 @dataclass(frozen=True) class State: shelves: tuple = PRESET hand: int = -1 orders: tuple = (1, 1, 0) sold: tuple = () def initial(): return State() def step(state, action): if not isinstance(action, tuple) or len(action) != 2: return state command, place = action if place not in (0, 1, 2): return state shelves = [list(row) for row in state.shelves] if command == "take": if place == 2 or state.hand >= 0 or not shelves[place]: return state item = shelves[place].pop(0) return replace(state, shelves=tuple(tuple(row) for row in shelves), hand=item) if command == "put": if place == 2 or state.hand < 0 or len(shelves[place]) >= CAPACITY: return state shelves[place].insert(0, state.hand) return replace(state, shelves=tuple(tuple(row) for row in shelves), hand=-1) if command == "sell": item = state.hand if place == 2 else shelves[place][0] if shelves[place] else -1 if item < 0: return state kind = KINDS.index(STOCK[item]) if state.orders[kind] <= 0: return state orders = list(state.orders) orders[kind] -= 1 hand = state.hand if place == 2: hand = -1 else: shelves[place].pop(0) return replace(state, shelves=tuple(tuple(row) for row in shelves), hand=hand, orders=tuple(orders), sold=state.sold+(item,)) return state class App: def __init__(self): self.reset() def reset(self): self.state = initial() self.selection = 0 self.item = 0 self.gated = False self.message = "ONE KEY AT A TIME" def control(self, pressed, held): if "R" in pressed: self.reset() self.gated = True return if self.gated: if not pressed and not held: self.gated = False return if len(pressed | held) > 1: self.gated = True self.message = "RELEASE ALL KEYS" return if len(pressed) != 1: return key = next(iter(pressed)) if key in ("UP", "DOWN"): self.selection = (self.selection+(-1 if key=="UP" else 1))%3 self.gated = True return action = ("take", self.selection) if key=="C" else ("put", self.selection) if key=="V" else ("sell", self.selection) if key=="Z" else None if action is None: return before = self.state self.state = step(self.state, action) self.message = "CONFIRMED" if before != self.state else "BLOCKED / NO COST" self.gated = True def draw(self, p): p.cls(1) p.text(8, 8, "LUNCH TRAIN / STAGE 2", 10) loading = False p.text(8, 23, "HAND PRACTICE"+" / FIRST STATION / NO WORK LIMIT", 7) for row, items in enumerate(self.state.shelves): y = 65+row*47 p.text(8, y+10, ("> " if self.selection==row else " ")+("A" if row==0 else "B"), 10 if self.selection==row else 7) for depth in range(CAPACITY): x = 65+depth*75 p.rectb(x, y, 64, 30, 6) if depth BACK", 6) p.text(8, 170, ("> " if self.selection==2 else " ")+"HAND: "+("-" if self.state.hand<0 else str(self.state.hand+1)+":"+STOCK[self.state.hand]), 10 if self.selection==2 else 7) p.text(8, 202, "ORDERS F/V/E: "+" / ".join(map(str,self.state.orders)), 11) p.text(8, 218, "SOLD: "+str(len(self.state.sold)), 7) p.text(8, 255, self.message, 7) p.text(8, 280, "UP/DOWN SELECT", 6) p.text(8, 301, "Z SELL C TAKE V PUT", 6) p.text(8, 313, "R RESET ESC QUIT", 6) DEMO_KEYS = ('C', 'Z', 'DOWN', 'Z') CONTROL_KEYS = ("UP", "DOWN", "LEFT", "RIGHT", "Z", "C", "V", "SPACE", "X", "R") def main(headless=False, screenshot=None, demo=False): from pathlib import Path import pyxel as p screenshot = str(Path(screenshot).resolve()) if screenshot else None if screenshot: Path(screenshot).parent.mkdir(parents=True, exist_ok=True) app = App() p.init(384, 330, title="Lunch Train", fps=30, display_scale=2, headless=headless) keys = {k: getattr(p, "KEY_"+k) for k in CONTROL_KEYS} limit = max(12, 2*len(DEMO_KEYS)+3) if demo else 12 def update(): if headless and p.frame_count >= limit: if screenshot: p.screenshot(screenshot, scale=2) p.quit() return if headless and demo: index = p.frame_count//2 pressed = {DEMO_KEYS[index]} if p.frame_count%2==0 and index