# 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。Python 3.11+ / Pyxel 2.9.9。""" from dataclasses import dataclass, asdict STAGE = 1 START_FUEL = 12 START_LOAD = 0 # Stage 1 exposes the arithmetic, without the route model or a win condition. def move_cost(distance, load): return distance * (1 + load) class App: def __init__(self): self.reset() def reset(self): self.distance = 1 self.load = START_LOAD self.fuel = START_FUEL self.spent = 0 self.last = None self.reason = 'READY' self.gated = False def control(self, pressed, held): if 'R' in pressed: self.reset(); self.gated = True; return if self.gated: if not held and not pressed: self.gated = False return if len(pressed | held) > 1: self.gated = True; return if len(pressed) != 1: return key = next(iter(pressed)); self.gated = True if key in ('LEFT','RIGHT'): self.distance = max(1, min(3, self.distance+(-1 if key=='LEFT' else 1))) elif key in ('UP','DOWN'): self.load = max(0, min(4, self.load+(1 if key=='UP' else -1))) elif key in ('Z','SPACE'): cost = move_cost(self.distance, self.load) if self.fuel < cost: self.reason = 'NOT ENOUGH FUEL: NOTHING CHANGED' return self.fuel -= cost self.spent += cost self.last = (self.distance, self.load, cost) self.reason = 'SPENT: CARGO CHANGES AFFECT ONLY FUTURE MOVES' def snapshot(self): return {'stage':STAGE, 'distance':self.distance, 'load':self.load, 'fuel':self.fuel, 'spent':self.spent, 'last':self.last, 'reason':self.reason} def draw(self,p): p.cls(1) p.text(14,10,'RETURN FUEL / STAGE 1 / PUBLIC COST LAB',10) p.text(14,30,'SIMPLIFIED LEARNING UI: NO MAP, PICKUPS OR WIN CONDITION',7) p.text(28,67,'DISTANCE '+str(self.distance),7) p.text(28,89,'CURRENT LOAD '+str(self.load),7) p.text(28,116,'NEXT COST = '+str(self.distance)+' x (1 + '+str(self.load)+') = '+str(move_cost(self.distance,self.load)),10) for d in range(1,4): y=154+d*30 p.text(30,y,'DISTANCE '+str(d),13) for load in range(5): x=170+load*80 p.rectb(x-8,y-6,65,22,10 if (d,load)==(self.distance,self.load) else 5) p.text(x,y,str(move_cost(d,load)),7) for load in range(5):p.text(164+load*80,162,'LOAD '+str(load),6) p.text(28,291,'FUEL '+str(self.fuel)+' SPENT '+str(self.spent)+' TOTAL '+str(self.fuel+self.spent),11) p.text(28,315,'LAST (DISTANCE, LOAD, COST): '+str(self.last),7) p.text(28,340,self.reason,10) p.text(14,412,'LEFT/RIGHT: DISTANCE UP/DOWN: LOAD Z/SPACE: SPEND',7) p.text(14,429,'R: RESET EXPERIMENT ESC: QUIT NO ROUTE RULES YET',13) p.text(14,446,'EDIT -> SAVE -> QUIT -> RERUN. R DOES NOT RELOAD CODE.',6) def make_demo_keys(): return ('UP','UP','RIGHT','Z') def main(argv=None): import argparse from pathlib import Path import json parser=argparse.ArgumentParser(description=__doc__) parser.add_argument('--headless-smoke', action='store_true', help='bounded actual Pyxel render') parser.add_argument('--demo-checkpoint', action='store_true', help='replay documented keys') parser.add_argument('--frames', type=int, help='bounded headless frames, 1..10000') parser.add_argument('--screenshot', help='new PNG; relative to launch working directory') parser.add_argument('--state-json', help='new JSON; relative to launch working directory') args=parser.parse_args(argv) if (args.demo_checkpoint or args.frames is not None or args.screenshot or args.state_json) and not args.headless_smoke: parser.error('--demo-checkpoint, --frames and outputs require --headless-smoke') if args.frames is not None and not 1<=args.frames<=10000: parser.error('--frames must be an integer from 1 to 10000') sequence=make_demo_keys() if args.demo_checkpoint else () minimum=len(sequence)*2+2 if sequence else 1 frames=args.frames if args.frames is not None else max(8,minimum) if frames=frames: finish(); return index=elapsed//2 pressed={sequence[index]} if elapsed%2==0 and index