# MIT License # # Copyright (c) 2026 # # 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. # # Third-party exception: bitmap glyph data derived from Noto Sans CJK is licensed # under the SIL Open Font License 1.1. See assets/NOTO-LICENSE.txt. """小さな進化島・教材1/5。現実の進化を再現しない資源計画ゲーム。""" from dataclasses import dataclass, replace WEATHER = (0, 0, 0, 2, 2, 2) WEATHER_NAMES = ("WET", "DRY", "COLD") BASE_HARVEST = ((2, 0, 0), (3, 2, 1)) BASE_USE = (2, 3, 4) ENERGY_CAP = 18 @dataclass(frozen=True) class State: day: int = 0 energy: int = 6 outcome: str = "running" def forecast(state, field): if state.day >= 6: return {"harvest": 0, "base": 0, "upkeep": 0, "change": 0, "after": state.energy} weather = WEATHER[state.day] harvest = BASE_HARVEST[field][weather] base = BASE_USE[weather] upkeep = 0 paid_change = 0 # paid_changeは表示する支払済み費用。ここでは二重に引かない。 return {"harvest": harvest, "base": base, "upkeep": upkeep, "change": paid_change, "after": min(ENERGY_CAP, state.energy+harvest-base-upkeep)} def move(state, action): if state.outcome != "running": return state, "ENDED" if action not in ("forage:0", "forage:1"): return state, "INVALID" field = int(action[-1]) detail = forecast(state, field) energy = detail["after"] day = state.day+1 outcome = "failed" if energy <= 0 else "won" if day == 6 else "running" return replace(state, day=day, energy=energy, outcome=outcome), "DAY CONFIRMED" class App: def __init__(self): self.reset() def reset(self): self.state = State() self.field = 0 self.gated = False self.message = "UP/DOWN FIELD, SPACE CONFIRM" def control(self, pressed, held): # 新しいRはいつでも優先。ただし、その後は全解放を待つ。 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 = "ONE KEY AT A TIME / RELEASE ALL" return if len(pressed) != 1: return key = next(iter(pressed)) if self.state.outcome != "running": return if key in ("UP", "DOWN"): self.field = 0 if key == "UP" else 1 self.gated = True return action = "forage:"+str(self.field) if key == "SPACE" else None if action is None: return self.state, self.message = move(self.state, action) self.gated = True def draw(self, p): p.cls(1) p.text(8, 8, "TRAIT ISLAND / STAGE 1", 10) p.text(8, 22, "DAY "+str(self.state.day)+" / 6 ENERGY "+str(self.state.energy), 7) for i in range(ENERGY_CAP): p.rect(8+i*10, 34, 8, 10, 11 if i=5 else "NO EGG") p.text(8, 182, result, 10) p.text(8, 200, self.message, 7) p.text(8, 220, "UP/DOWN FIELD SPACE FORAGE R RESET", 6) p.text(8, 244, "ESC QUIT", 6) DEMO_KEYS = ('DOWN', 'SPACE') CONTROL_KEYS = ('UP', 'DOWN', 'SPACE', '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, 260, title="Trait Island", fps=30, display_scale=3, headless=headless) keys = {key: getattr(p, "KEY_"+key) for key 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=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