# 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。独立して動く朝と夕方の模型。""" from dataclasses import dataclass, replace START_POTS = (2, 0, -1, -1, -1, -1) WEATHER = ("W", "W", "W", "W") START_SEEDS = 0 DEADLINES = ((4, 2),) SEED_GOAL = 1 @dataclass(frozen=True) class State: day: int = 0 pots: tuple = START_POTS seeds: int = START_SEEDS food: int = 0 delivered: int = 0 planted: int = 0 leaves: int = 0 pods: int = 0 status: str = "play" def shadows(state): if state.day >= len(WEATHER) or WEATHER[state.day] == "C": return frozenset() dx = 1 if WEATHER[state.day] == "W" else -1 # index%3は列番号。影を次の行へ回り込ませない。 return frozenset(i+dx for i, value in enumerate(state.pots) if value >= 2 and 0 <= i%3+dx < 3) def forecast(state): if state.day >= len(WEATHER) or WEATHER[state.day] == "C": return state.pots shade = shadows(state) return tuple(value+1 if 0 <= value < 3 and i not in shade else value for i, value in enumerate(state.pots)) def step(state, action): if state.status != "play": return state if action == "D": return replace(state, food=0, delivered=state.delivered+state.food) if state.food else state if action == "W": missed = any(day <= state.day and state.delivered < goal for day, goal in DEADLINES) if state.day == len(WEATHER): status = "won" if not missed and state.seeds >= SEED_GOAL else "short" return replace(state, status=status) if missed: return replace(state, status="short") return replace(state, day=state.day+1, pots=forecast(state)) if not isinstance(action, str) or len(action) != 2 or action[0] != "C" or action[1] not in "012345": return state index = int(action[1]) value = state.pots[index] pots = list(state.pots) if value == 2: pots[index] = -1 return replace(state, pots=tuple(pots), food=state.food+2, leaves=state.leaves+1) if value == 3: pots[index] = -1 return replace(state, pots=tuple(pots), seeds=state.seeds+2, pods=state.pods+1) if value == -1 and state.seeds > 0: pots[index] = 0 return replace(state, pots=tuple(pots), seeds=state.seeds-1, planted=state.planted+1) return state @dataclass(frozen=True) class Timeline: state: State = State() history: tuple = () def apply(timeline, action): if action == "undo": return Timeline(timeline.history[-1], timeline.history[:-1]) if timeline.history else timeline state = step(timeline.state, action) return timeline if state == timeline.state else Timeline(state, timeline.history+(timeline.state,)) class App: def __init__(self): self.reset() def reset(self): self.timeline = Timeline() self.selected = 0 self.locked = False self.message = "SPACE: EVENING GROWTH" @property def state(self): return self.timeline.state def control(self, pressed, held): if self.locked: if not held: self.locked = False return if "R" in pressed: self.reset() self.locked = True return if "X" in pressed: before = self.state self.timeline = apply(self.timeline, "undo") self.message = "UNDO" if self.state != before else "NOTHING TO UNDO" self.locked = True return if self.state.status != "play": return if len(pressed) != 1: return key = next(iter(pressed)) x, y = self.selected%3, self.selected//3 if key in ("LEFT", "RIGHT", "UP", "DOWN"): x = max(0, min(2, x+(key=="RIGHT")-(key=="LEFT"))) y = max(0, min(1, y+(key=="DOWN")-(key=="UP"))) self.selected = y*3+x self.locked = True return action = "W" if key == "SPACE" else None if key == "Z": action = "C"+str(self.selected) if key == "C": action = "D" if action is None: return before = self.state self.timeline = apply(self.timeline, action) self.message = ("EVENING / NEXT MORNING" if action=="W" else "CARE / SAME MORNING") if self.state!=before else "NO CHANGE" if action == "D" and self.state != before: self.message = "DELIVERED / SAME MORNING" if self.state.status != "play": self.message = self.state.status.upper()+" / R TO RESTART" self.locked = True def draw(self, p): p.cls(1) p.text(8, 8, "ROOFTOP GARDEN / STAGE 5", 10) weather = WEATHER[self.state.day] if self.state.day < len(WEATHER) else "END" p.text(8, 22, "MORNING "+str(self.state.day)+" / "+str(len(WEATHER))+" SUN "+weather, 7) p.text(8, 34, "SEEDS "+str(self.state.seeds)+" FOOD "+str(self.state.food), 11) p.text(148, 34, "DELIVERED "+str(self.state.delivered), 11) deadline = " / ".join("D"+str(day)+":"+str(goal) for day, goal in DEADLINES) p.text(8, 46, "DELIVERY "+deadline+" FINAL SEEDS "+str(SEED_GOAL), 6) shade = shadows(self.state) following = forecast(self.state) for i, value in enumerate(self.state.pots): x, y = 12+(i%3)*100, 64+(i//3)*58 p.rectb(x, y, 92, 53, 10 if i==self.selected else 5) p.text(x+4, y+4, "POT "+str(i+1), 7) if i in shade: p.text(x+48, y+4, "SHADE", 6) p.rect(x+30, y+33, 24, 12, 4) if value >= 0: p.line(x+42, y+32, x+42, y+24-3*value, 3) if value == 0: p.circ(x+42, y+30, 2, 10) elif value == 1: p.circ(x+37, y+27, 3, 11) elif value == 2: p.circ(x+35, y+24, 6, 3) p.circ(x+49, y+21, 6, 11) else: p.circ(x+36, y+19, 4, 9) p.circ(x+47, y+22, 4, 10) p.text(x+4, y+42, str(value)+" -> "+str(following[i]), 7) p.text(8, 188, "-1 EMPTY 0 SEED 1 YOUNG 2 LEAF 3 POD", 6) p.text(8, 200, "ARROW IS EVENING FORECAST, NOT EXTRA GROWTH", 6) p.text(8, 216, self.message, 10) p.text(8, 230, 'SPACE GROW ARROWS SELECT Z CARE C DELIVER', 7) p.text(8, 241, "R RESET X UNDO ESC QUIT", 6) DEMO_KEYS = ('Z', 'SPACE', 'SPACE', 'SPACE', 'RIGHT', 'Z', 'C', 'SPACE', 'SPACE') 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(320, 256, title="Rooftop Garden", fps=30, display_scale=3, headless=headless) keys = {k: getattr(p, "KEY_"+k) for k in ("UP", "DOWN", "LEFT", "RIGHT", "SPACE", "Z", "C", "X", "R")} 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=3) p.quit() return if headless and demo: index = p.frame_count//2 pressed = {DEMO_KEYS[index]} if p.frame_count%2==0 and index