# 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. """057 紙片のラリー / 第1段階。LESSON.md を参照。""" STAGE=1 STAGE_NOTE='FIXED SPEED 1 / STRAIGHT 120 / NO ACCEL, STEER OR CONTACT' """057 deterministic integer rally. No renderer, clock, filesystem or random state.""" from dataclasses import dataclass, asdict, replace import hashlib,json RULE_VERSION='paper-rally-r1' GENERATOR_VERSION='module-bank-g1' ENV_VERSION='integer-10Hz-v1' TICK_RATE=10 @dataclass(frozen=True) class Segment: length:int curve:int width:int module:str @dataclass(frozen=True) class Level: seed:str title:str modules:tuple # Both alternatives connect the same ordered entry/exit sockets; no arbitrary geometry. LEVELS=(Level('LESSON-STRAIGHT','直線練習',(('START',((120,0,14),),None),)),) @dataclass(frozen=True) class State: tick:int=0 segment:int=0 offset:int=0 q:int=0 v:int=0 accel:int=0 brake:int=0 contacts:int=0 stopped:int=0 outcome:str='running' def folds(level):return ('',)+tuple(name for name,_,folded in level.modules if folded is not None) def course(level,fold='',wide=False): if fold not in folds(level):raise ValueError('Unknown fold in finite bank') if wide and (level.seed[0]!='A' or fold!='A'):raise ValueError('Width comparison is only A folded') return tuple(Segment(d,c,14 if wide and name==fold else w,name) for name,opened,folded in level.modules for d,c,w in (folded if name==fold else opened)) def initial():return State(v=1) def distance(s,road):return sum(seg.length for seg in road[:s.segment])+s.offset def total(road):return sum(s.length for s in road) def at(road,d): d=max(0,min(d,total(road)-1)) for i,seg in enumerate(road): if d=limit: if screenshot:app.draw(p);p.screen.save(screenshot,1) if report: from pathlib import Path data={'stage':STAGE,'mode':app.mode,'state':asdict(app.s),'scope':'actual Pyxel; synthetic held input; not OS keyboard or human playtest'} pass Path(report).write_text(json.dumps(data,ensure_ascii=False,indent=2),encoding='utf-8') p.quit();return if headless: held=demo_held(p.frame_count) if demo else set();pressed=held-previous;previous=held else: held={k for k,v in keys.items() if p.btn(v)};pressed={k for k,v in keys.items() if p.btnp(v)} app.control(pressed,held) p.run(update,lambda:app.draw(p)) if __name__=='__main__': import argparse from pathlib import Path parser=argparse.ArgumentParser(description=__doc__) parser.add_argument('--headless-smoke',action='store_true');parser.add_argument('--demo-checkpoint',action='store_true') parser.add_argument('--screenshot');parser.add_argument('--report') pass args=parser.parse_args() pass if (args.demo_checkpoint or args.screenshot or args.report) and not args.headless_smoke:parser.error('demo and output require --headless-smoke') for field,suffix in (('screenshot','.png'),('report','.json')): value=getattr(args,field) if value: target=Path(value).resolve() if target.suffix.lower()!=suffix or not target.parent.is_dir() or target.exists():parser.error('use a new '+suffix+' in an existing directory') setattr(args,field,str(target)) main(args.headless_smoke,args.screenshot,args.report,args.demo_checkpoint)