# 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. """風車村の冬支度・教材2/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))} TITLES = {'A': '西からの初雪', 'B': '向きの変わる冬', 'C': '古い蓄電池'} @dataclass(frozen=True) class State: case: str = 'A' season: int = 0 mill: int = 1 shields: tuple = (False, False, False) materials: int = 5 battery: int = 4 capacity: int = 8 comfort: tuple = (3, 3, 3) shelter: int = 0 history: tuple = () blocking: bool = True @dataclass(frozen=True) class Plan: build: str = 'none' heat: tuple = (0, 0, 0) def new(case='A', blocking=True): return State(case=case, battery={'A': 4, 'B': 5, 'C': 7}[case], comfort=(2, 3, 2) if case == 'C' else (3, 3, 3), blocking=blocking) def forecast(s,b='none'): if b!='none' or s.season>=4:raise ValueError('no construction in this stage') wind,direction,heat,snow=WEATHER[s.case][s.season] return {'shields':s.shields,'mill':s.mill,'capacity':s.capacity,'cost':0, 'wind':wind,'direction':direction,'snow':snow,'blocked':0, 'power':wind,'needs':(heat,heat,heat), 'available':min(s.capacity,s.battery+wind), 'spill':max(0,s.battery+wind-s.capacity)} def predict(s, p): f = forecast(s, p.build) if not isinstance(p.heat, tuple) or len(p.heat) != 3 or any((type(h) != int or not 0 <= h <= n for h, n in zip(p.heat, f['needs']))): raise ValueError('配分は必要量までです') if sum(p.heat) > f['available']: raise ValueError('電力が足りません') f['comfort'] = tuple((min(3, c + 1) if h >= n else max(0, c - (n - h)) for c, h, n in zip(s.comfort, p.heat, f['needs']))) f['shelter'] = any((c == 0 for c in f['comfort'])) f['remaining'] = f['available'] - sum(p.heat) return f def step(s, p): f = predict(s, p) entry = (s.season, p.build, p.heat, f['power'], f['spill'], f['comfort'], f['remaining'], f['shelter']) return replace(s, season=s.season + 1, mill=f['mill'], shields=f['shields'], capacity=f['capacity'], materials=s.materials - f['cost'] + 1, battery=f['remaining'], comfort=f['comfort'], shelter=s.shelter + int(f['shelter']), history=s.history + (entry,)) def success(s): return s.season == 4 and min(s.comfort) >= 1 STAGE = 2 SEASON_LABELS=("AUTUMN","EARLY WINTER","DEEP WINTER","THAW") class App: def __init__(self): self.reset() def reset(self): self.state=new("A",blocking=True) self.plan=Plan() 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.confirm: if key=='X':self.confirm=False;self.message='DRAFT KEPT';return if key=='RETURN': self.state=step(self.state,self.plan);self.plan=Plan() self.confirm=False;self.message='SEASON COMMITTED' return if self.state.season>=4:return if key=='TAB':self.house=(self.house+1)%3 if key in ('UP','DOWN'): f=forecast(self.state,self.plan.build);heat=list(self.plan.heat) heat[self.house]=max(0,min(f['needs'][self.house],heat[self.house]+(1 if key=='UP' else -1))) self.plan=Plan(self.plan.build,tuple(heat));self.message='FREE DRAFT' if key=='RETURN': try:predict(self.state,self.plan);self.confirm=True;self.message='PREVIEW / ENTER AGAIN TO COMMIT' except ValueError:self.message='NOT ENOUGH ENERGY / LOWER HEAT' 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) p.text(12,139,'MATERIALS '+str(s.materials)+' / SHELTER SEASONS '+str(s.shelter),6) if s.season<4: f=forecast(s,self.plan.build) p.text(12,157,'POWER '+str(f['power'])+' / AVAILABLE '+str(f['available'])+' / SPILL '+str(f['spill'])+' / DRAFT COST '+str(f['cost']),11) for i in range(3): x=12+i*193;p.rectb(x,187,181,65,10 if i==self.house else 5) p.text(x+7,196,('LEFT','MIDDLE','RIGHT')[i]+' HOUSE',7) p.text(x+7,213,'HEAT '+str(self.plan.heat[i])+' / NEED '+str(f['needs'][i]),11) p.text(x+7,230,'COMFORT '+str(s.comfort[i])+' / DRAFT SHIELD '+str(int(f['shields'][i])),6) try: prediction=predict(s,self.plan) p.text(12,268,'NEXT COMFORT '+str(prediction['comfort'])+' / BATTERY '+str(prediction['remaining']),10) except ValueError:p.text(12,268,'HEAT TOTAL EXCEEDS AVAILABLE ENERGY',8) else: p.text(12,188,'FINAL COMFORT '+str(s.comfort)+' / '+('SUCCESS' if success(s) else 'TRY ANOTHER PLAN'),10) p.text(12,213,'SHELTER IS NOT PERMANENT FAILURE; ONLY FINAL COMFORT MATTERS.',7) p.text(12,342,'SHIELDS '+('DO NOT BLOCK WIND IN THIS SIMPLIFIED STAGE' if STAGE==3 else 'BLOCK UPSTREAM WIND' if STAGE>=4 else 'NOT INTRODUCED'),14) p.text(12,359,'SEASONS FINISHED / R TO RESTART' if s.season>=4 else 'PREVIEW: ENTER TO COMMIT / X TO CANCEL' if self.confirm else 'EDITING: NO RESOURCE HAS BEEN CHARGED',10) p.text(12,403,'TAB: HOUSE / UP/DOWN: HEAT / ENTER: PREVIEW, THEN COMMIT',7) p.text(12,437,'R: RESET / ESC: QUIT',7) p.text(12,382,self.message,10) def demo_keys(): return ('UP','TAB','UP','TAB','UP','RETURN','RETURN') 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