# 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. """風まかせ郵便気球・教材4/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)) LETTERS = (4,9) DEADLINE = 24 @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') if action == 'cut': if s.moored != 0: return (s, 'home') return (replace(s, outcome='returned'), 'cut') h = s.h fuel = s.fuel spent = s.spent if s.moored >= 0: if action == 'wait': ns = replace(s, t=s.t + 1) if ns.t >= DEADLINE: ns = replace(ns, outcome='late') 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(LETTERS): if x == pos: delivered |= 1 << i 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') elif moored == 0 and delivered == (1 << len(LETTERS)) - 1: ns = replace(ns, outcome='won') elif ns.t >= DEADLINE: ns = replace(ns, outcome='late') 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","C":"cut"}.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 4",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) p.text(245,36,"MAIL "+str(s.delivered.bit_count())+"/2 LIMIT "+str(DEADLINE),10) 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) for i,pos in enumerate(LETTERS): x=40+pos*30 p.rectb(x-4,139,9,7,11 if s.delivered&(1<=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 C SAFE RETURN",6) p.text(8,306,"ESC QUIT",6) DEMO_KEYS = ('Z', 'RIGHT', 'DOWN', 'RIGHT', 'UP', 'RIGHT', 'DOWN', 'Z', 'UP', 'DOWN', 'DOWN', 'UP', 'DOWN', 'RIGHT', '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