# 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。独立して動く朝と夕方の模型。""" from dataclasses import dataclass, replace START_POTS = (2, 0, -1, -1, -1, -1) WEATHER = ("W", "W", "W", "W") @dataclass(frozen=True) class State: day: int = 0 pots: tuple = START_POTS 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 action == "W": if state.day >= len(WEATHER): return state return replace(state, day=state.day+1, pots=forecast(state)) return state class App: def __init__(self): self.reset() def reset(self): self.state = State() self.selected = 0 self.locked = False self.message = "SPACE: EVENING GROWTH" 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 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 action is None: return before = self.state self.state = step(self.state, action) self.message = ("EVENING / NEXT MORNING" if action=="W" else "CARE / SAME MORNING") if self.state!=before else "NO CHANGE" self.locked = True def draw(self, p): p.cls(1) p.text(8, 8, "ROOFTOP GARDEN / STAGE 2", 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) 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', 7) p.text(8, 241, "R RESET ESC QUIT", 6) DEMO_KEYS = ('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