# 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. """風まかせ郵便気球・教材3/5。原作Aの有限航路。""" from dataclasses import dataclass, replace WINDS = ((-1,-1,1,1),(0,1,0,-1),(2,2,-2,-2)) START = 1 INITIAL_FUEL = 4 DOCKS = ((1,0),(6,3)) @dataclass(frozen=True) class State: t: int x: int h: int moored: int fuel: int stocks: tuple delivered: int = 0 spent: int = 0 outcome: str = 'flying' def initial(): return State(0,START,0,0,INITIAL_FUEL,tuple(n for _,n in DOCKS)) def step(s, action): if s.outcome != 'flying': return (s, 'end') h = s.h fuel = s.fuel spent = s.spent if s.moored >= 0: if action == 'wait': ns = replace(s, t=s.t + 1) return (ns, 'wait') if action != 'launch': return (s, 'moored') if fuel < 1: return (s, 'fuel') h = 1 fuel -= 1 spent += 1 else: if action not in ('up', 'down', 'stay'): return (s, 'air') h += {'up': 1, 'down': -1, 'stay': 0}[action] if not 0 <= h <= 2: return (s, 'height') if action == 'up': if fuel < 1: return (s, 'fuel') fuel -= 1 spent += 1 wind = WINDS[h][s.t % len(WINDS[0])] x = s.x + wind delivered = s.delivered stocks = list(s.stocks) moored = -1 if h == 0 and 0 <= x <= 12: for i, (pos, _) in enumerate(DOCKS): if x == pos: moored = i transfer = min(stocks[i], 6 - fuel) fuel += transfer stocks[i] -= transfer break ns = State(s.t + 1, x, h, moored, fuel, tuple(stocks), delivered, spent) if not 0 <= x <= 12: ns = replace(ns, outcome='drift') return (ns, 'ok') class App: def __init__(self): self.reset() def reset(self): self.state = initial() self.gated = False self.message = "READY" def control(self, pressed, held): 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 = "RELEASE ALL KEYS" return if len(pressed)!=1: return key = next(iter(pressed)) if self.state.outcome!="flying": return action = {"UP":"up","DOWN":"down","RIGHT":"stay","Z":"launch","SPACE":"wait"}.get(key) if action is None: return self.state,self.message = step(self.state,action) self.gated = True def draw(self, p): p.cls(1) p.text(8,8,"WIND POST / STAGE 3",10) s = self.state p.text(8,23,"T "+str(s.t)+" X "+str(s.x)+" H "+str(s.h)+" "+s.outcome.upper(),7) p.text(8,36,"FUEL "+str(s.fuel)+"/6 SPENT "+str(s.spent)+" "+("MOORED D"+str(s.moored) if s.moored>=0 else "IN AIR"),11) for h in range(3): y = 150-h*36 p.line(40,y,400,y,5) p.text(8,y-3,"H"+str(h),6) for x in range(13): p.text(39+x*30,165,str(x),6) for i,(pos,_) in enumerate(DOCKS): x=40+pos*30 p.rect(x-5,145,11,9,6) p.text(x-7,180,"D"+str(i)+":"+str(s.stocks[i]),7) if 0<=s.x<=12: x=40+s.x*30;y=150-s.h*36 p.circ(x,y-9,5,10) p.line(x,y-4,x,y,7) p.rect(x-2,y,5,3,9) phase=s.t%len(WINDS[0]) p.text(8,199,"WIND BY TURN COLUMN / NOW "+str(phase),7) for h in range(3): p.text(8,214+h*14,"H"+str(h),6) for col,wind in enumerate(WINDS[h]): x=38+col*45 if col==phase:p.rectb(x-2,211+h*14,30,11,10) p.text(x,214+h*14,format(wind,"+d"),7) preview_actions = (('UP', 'up'), ('RIGHT', 'stay'), ('DOWN', 'down')) if s.moored>=0: preview_actions = (("Z","launch"),("SPACE","wait")) for row,(key,action) in enumerate(preview_actions): after,reason=step(s,action) text=key+": X"+str(after.x)+" H"+str(after.h)+" T"+str(after.t) if hasattr(after,"fuel"):text+=" F"+str(after.fuel) if after==s:text=key+": "+reason.upper() p.text(235,214+row*14,text,11 if after!=s else 6) p.text(8,263,self.message.upper(),7) p.text(8,280,"Z LAUNCH SPACE MOORED WAIT R RESET",6) p.text(8,292,"UP / DOWN / RIGHT: FLY",6) p.text(8,306,"ESC QUIT",6) DEMO_KEYS = ('Z', 'RIGHT', 'DOWN', 'RIGHT', 'UP', 'RIGHT', 'DOWN') KEYS = ("UP","DOWN","RIGHT","Z","SPACE","C","X","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(448,320,title="Wind Post",fps=30,display_scale=2,headless=headless) keys={k:getattr(p,"KEY_"+k) for k in 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=2) p.quit();return if headless and demo: index=p.frame_count//2 pressed={DEMO_KEYS[index]} if p.frame_count%2==0 and index