# 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,asdict WEATHER = {'A': ((3, -1, 1, 0), (4, -1, 2, 1), (4, -1, 2, 2), (3, -1, 1, 0)), 'B': ((3, -1, 1, 0), (4, 1, 2, 1), (5, -1, 3, 3), (3, 1, 1, 0)), 'C': ((5, 1, 1, 0), (2, 1, 2, 2), (4, 1, 2, 2), (4, -1, 1, 0))} @dataclass(frozen=True) class State: season: int = 0 battery: int = 4 capacity: int = 8 def forecast(s): if s.season>=4:raise ValueError('four seasons finished') wind=WEATHER['A'][s.season][0] return {'power':wind,'available':min(s.capacity,s.battery+wind), 'spill':max(0,s.battery+wind-s.capacity)} def step(s): f=forecast(s) return replace(s,season=s.season+1,battery=f['available']) STAGE = 1 SEASON_LABELS=("AUTUMN","EARLY WINTER","DEEP WINTER","THAW") class App: def __init__(self): self.reset() def reset(self): self.state=State() self.house=0 self.choice=0 self.confirm=False self.gated=False self.message='READY' def press(self,key): if key=='R':self.reset();return if self.state.season>=4:return if key=="SPACE":self.state=step(self.state) def control(self,pressed,held): 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 self.press(next(iter(pressed)));self.gated=True def draw(self,p): s=self.state;p.cls(1) p.text(12,12,'WINDMILL VILLAGE / STAGE '+str(STAGE)+' / A',10) p.text(12,29,'FICTIONAL TOY / NOT REAL HEATING OR GENERATOR DESIGN',6) p.text(12,51,'SEASON '+str(s.season)+' / 4 '+(SEASON_LABELS[s.season] if s.season<4 else 'FINISHED'),7) p.text(12,68,'BATTERY '+str(s.battery)+' / CAPACITY '+str(s.capacity),11) for i,w in enumerate(WEATHER['A']): x=12+i*145;p.rectb(x,88,138,36,10 if i==s.season else 5) p.text(x+6,95,SEASON_LABELS[i],7) p.text(x+6,110,'WIND '+str(w[0])+' / '+('W -> E' if w[1]==-1 else 'E -> W'),6) if s.season<4: f=forecast(s) p.text(12,150,'GENERATION '+str(f['power'])+' / NEXT BATTERY '+str(f['available'])+' / SPILL '+str(f['spill']),11) p.text(12,186,'STAGE 1 HAS NO HOUSES, HEATING, MATERIAL COST OR SCORE.',7) p.text(12,206,'SPACE ADVANCES EXACTLY ONE SEASON. HELD KEYS DO NOT REPEAT.',7) p.text(12,432,'SPACE: NEXT SEASON / R: RESET / ESC: QUIT',7) p.text(12,382,self.message,10) def demo_keys(): return ('SPACE','SPACE','SPACE') def main(headless=False,screenshot=None,demo=False): import pyxel as p p.init(600,464,title='Windmill village lesson '+str(STAGE),fps=30,display_scale=2,headless=headless) app=App();sequence=demo_keys() if demo else () keys={'LEFT':p.KEY_LEFT,'RIGHT':p.KEY_RIGHT,'UP':p.KEY_UP,'DOWN':p.KEY_DOWN,'TAB':p.KEY_TAB,'RETURN':p.KEY_RETURN,'SPACE':p.KEY_SPACE,'Z':p.KEY_Z,'X':p.KEY_X,'R':p.KEY_R} limit=max(20,2*len(sequence)+8) def update(): if p.btnp(p.KEY_ESCAPE) or headless and p.frame_count>=limit: if screenshot:app.draw(p);p.screen.save(screenshot,1) p.quit() if headless: if demo: index=p.frame_count//2 pressed={sequence[index]} if p.frame_count%2==0 and index