# MIT License # # Copyright (c) 2026 Pyxel 100 contributors # # 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. # # The bundled Noto-derived bitmap glyphs are separately licensed under SIL OFL. # See assets/NOTO-LICENSE.txt. # 教材画面はASCIIと図形のみ。字形データは使いません。 """棚からぼたもちの独立教材。日本語の説明はLESSON.mdを参照。""" from dataclasses import dataclass, replace, asdict from fractions import Fraction as F import hashlib import json FLOOR = 12 @dataclass(frozen=True) class Body: id: str kind: str x: int y: int w: int = 1 mass: F = F(0) capacity: F | None = None @property def right(self): return self.x + self.w @property def fixed(self): return self.kind in ('anchor','plate') @property def removable(self): return self.kind == 'board' @dataclass(frozen=True) class State: level: int bodies: tuple[Body, ...] removed: tuple[str, ...] = () broken: tuple[str, ...] = () delivered: tuple[str, ...] = () status: str = 'ready' reason: str = 'ready' danger: tuple[str, ...] = () moves: int = 0 def __post_init__(self): object.__setattr__(self,'bodies',tuple(sorted(self.bodies,key=lambda b:b.id))) for name in ('removed','broken','delivered','danger'): object.__setattr__(self,name,tuple(sorted(getattr(self,name)))) @dataclass(frozen=True) class Frame: state: State event: str ids: tuple[str, ...] = () @dataclass(frozen=True) class Result: state: State frames: tuple[Frame, ...] def board(id,x,y,w,capacity=4): return Body(id,'board',x,y,w,F(1),None if capacity is None else F(capacity)) def anchor(id,x,y): return Body(id,'anchor',x,y) def mochi(id,x,y,mass=1): return Body(id,'mochi',x,y,1,F(mass)) def plate(id,x,y): return Body(id,'plate',x,y) # 原作Cの固定配置そのもの。level=2は元作品の0始まりの番号。 def initial(): return State(2, ( mochi('M1',5,2), mochi('M2',10,2), board('A1',4,3,3), board('A2',9,3,3), anchor('a1L',4,4), anchor('a1R',6,4), anchor('a2L',9,4), anchor('a2R',11,4), board('D',3,7,10,None), board('E',2,8,2,F(5,4)), board('F',12,8,2,F(5,4)), anchor('eL',2,9), anchor('eR',3,9), anchor('fL',12,9), anchor('fR',13,9), plate('P1',5,9), plate('P2',10,9))) def overlap(a,b): return min(a.right,b.right)-max(a.x,b.x) >= 1 def contacts(state): """Actual adjacent cell contacts, never authored graph shortcuts.""" return {b.id:tuple(c.id for c in state.bodies if c.id != b.id and c.y == b.y+1 and overlap(b,c)) for b in state.bodies if not b.fixed} def validate_geometry(state): for b in state.bodies: assert 0<=b.x= last_frame: if screenshot: pyxel.screenshot(str(Path(screenshot).resolve()),scale=1) if report: Path(report).write_text(json.dumps({ 'label':'actual Pyxel headless / synthetic controller input / silent', 'stage':STAGE,'frames':frame,'logical_seconds':frame/30, 'program_sha256':hashlib.sha256(Path(__file__).read_bytes()).hexdigest(), 'wall_seconds_before_exit':round(time.perf_counter()-start,6), 'sequence':sequence,'events':events,'snapshot':controller.snapshot() },ensure_ascii=False,indent=2)+'\n',encoding='utf-8') pyxel.quit() pyxel.init(640,460,title='Shelf Mochi - Step '+str(STAGE),fps=30,headless=headless) pyxel.run(update,draw) if __name__ == '__main__': import argparse parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--headless-smoke',action='store_true') parser.add_argument('--headless-replay',help='Synthetic keys: '+str(REPLAY)) parser.add_argument('--screenshot') parser.add_argument('--report') args = parser.parse_args() if args.headless_smoke and args.headless_replay is not None: parser.error('Choose smoke or replay') headless = args.headless_smoke or args.headless_replay is not None if not headless and (args.screenshot or args.report): parser.error('Outputs require headless mode') sequence = args.headless_replay or '' if any(char not in REPLAY for char in sequence): parser.error('Unsupported replay character') main(headless,sequence,args.screenshot,args.report)