# 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. """寝ぼけた巨人・教材1/5。各ファイルは単独実行できます。""" from dataclasses import dataclass, field, asdict, replace from copy import deepcopy import math, json, hashlib @dataclass(frozen=True) class Level: name: str start: tuple stones: tuple=() left_sweep: bool=True LEVELS=(Level('起きた左手',(78.,78.)),Level('横に空いた庭',(80.,78.),((24,54,72,64),(88,54,136,64))),Level('片手を眠らせておく',(80.,86.),((72,20,88,60),))) @dataclass(frozen=True) class Input: dx: int=0 dy: int=0 z: bool=False @dataclass class WorldState: tick: int = 0 x: float = 78 y: float = 78 facing: tuple = (0, -1) status: str = 'playing' events: tuple = () def initial(level):return WorldState(x=level.start[0],y=level.start[1]) def blocked(l,x,y,r=4): if not 4+r<=x<=156-r or not 4+r<=y<=104-r:return True return any(a-r= 3: arm, age = slot(visual) it = s.intent if it and 0 <= age < 54 and s.hands[it.arm].hp > 0: color = 8 if age >= 48 else 4 if it.kind == 'circle': x,y,r=it.region; xx,yy=xy(x,y);p.circ(xx,yy,r*2,color);p.circb(xx,yy,r*2,10) else: a,c,b,d=it.region; xx,yy=xy(a,c);p.rect(xx,yy,(b-a)*2,(d-c)*2,color) for y in range(int(c),int(d),4): x1,y1=xy(a,y);x2,y2=xy(b,y);p.line(x1,y1,x2,y2,10) for a,c,b,d in self.level.stones: xx,yy=xy(a,c);p.rect(xx,yy,(b-a)*2,(d-c)*2,5);p.rectb(xx,yy,(b-a)*2,(d-c)*2,13) hx,hy=xy(80,17);p.elli(hx-35,hy-20,70,36,13);p.line(hx-18,hy-4,hx-8,hy-4,1);p.line(hx+8,hy-4,hx+18,hy-4,1) if STAGE >= 2: for i,((x,y),h) in enumerate(zip(HANDS,s.hands)): xx,yy=xy(x,y);p.circ(xx,yy,18,9 if h.hp else 5) if getattr(h,'awake',False):p.circ(xx-5,yy-3,3,7) else:p.line(xx-9,yy-2,xx-2,yy-2,1) if getattr(h,'hit_lock',False): for dy in (-8,0,8):p.line(xx-15,yy+dy,xx+15,yy+dy,7) p.text(xx-16,yy-27,'LR'[i]+' HP '+str(h.hp),7) xx,yy=xy(s.x,s.y);p.circ(xx,yy,8,12);p.line(xx,yy,xx+s.facing[0]*12,yy+s.facing[1]*12,10) if STAGE >= 2 and 6 <= s.sword_age-1 < 12: fx,fy=s.facing; corners=[(xx+2*(u*fx-v*fy),yy+2*(u*fy+v*fx)) for u,v in ((0,-8),(18,-8),(18,8),(0,8))] for j in range(4):p.line(*corners[j],*corners[(j+1)%4],10) p.clip() if STAGE >= 3: arm, age=slot(visual) phase='PREPARE' if arm<0 else 'LR'[arm]+' '+arm_phase(visual,arm).upper()+' AGE '+str(age) p.text(12,306,phase,10) p.text(12,323,'INTENT '+(s.intent.kind.upper()+' SEEN '+str(s.intent.seen) if s.intent else 'NONE'),6) else:p.text(12,306,'PRACTICE: '+('MOVE ONLY' if STAGE==1 else 'SWORD ONLY / NO ENEMY ATTACK OR HIT LOCK'),14) if STAGE>=4:p.text(12,340,'YOUR HP '+str(s.hp)+' / INVULNERABLE '+str(s.invuln),11) elif STAGE==3:p.text(12,340,'PRACTICE: ENEMY DAMAGE DISABLED',14) if STAGE>=2:p.text(12,357,'SWORD AGE '+str(s.sword_age)+' / ID '+str(s.sword_id),7) p.text(12,380,'ARROWS: MOVE / P: RUN-PAUSE',7) p.text(12,397,'PAUSED SPACE: ONE TICK / R: RESET / ESC: QUIT',7) p.text(12,414,'1-3: GARDENS' if STAGE==5 else 'HOLD ARROWS + TAP SPACE TO MEASURE EXACT TICKS',6) def make_demo_inputs(): # 実際の初期状態から進める。完成状態を捏造して描きません。 if STAGE == 1: return [Input(dx=-1) for _ in range(10)] result = [Input(dx=-1) for _ in range(21)] result += [Input(dy=-1) for _ in range(20)] result += [Input(z=True)] + [Input() for _ in range(6)] if STAGE >= 3: result += [Input() for _ in range(13)] return result def main(headless=False, screenshot=None, demo=False): import pyxel as p p.init(464,436,title='Sleepy giant lesson '+str(STAGE),fps=30,display_scale=2,headless=headless) app=App(); sequence=make_demo_inputs() if demo else [] keymap={k:getattr(p,'KEY_'+k) for k in ('LEFT','RIGHT','UP','DOWN','Z','SPACE','P','R','1','2','3')} limit=max(12,2*len(sequence)+4) def update(): if p.btnp(p.KEY_ESCAPE) or headless and p.frame_count>=limit: if screenshot:app.draw(p);p.screen.save(screenshot,1) p.quit();return if headless: index=p.frame_count//2 if demo and p.frame_count%2==0 and index0 else 'LEFT') if inp.dy:held.add('DOWN' if inp.dy>0 else 'UP') if inp.z:held.add('Z') app.control({'SPACE'},held) else:app.control(set(),set()) else:app.control({k for k,v in keymap.items() if p.btnp(v)}, {k for k,v in keymap.items() if p.btn(v)}) 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') args=parser.parse_args() if args.demo_checkpoint and not args.headless_smoke:parser.error('demo requires headless mode') if args.screenshot and not args.headless_smoke:parser.error('screenshot requires headless mode') if args.screenshot and (Path(args.screenshot).suffix.lower()!='.png' or not Path(args.screenshot).parent.is_dir() or Path(args.screenshot).exists()):parser.error('use a new .png in an existing directory') # Pyxel may change cwd during init; preserve the caller's output destination. if args.screenshot: args.screenshot = str(Path(args.screenshot).resolve()) main(args.headless_smoke,args.screenshot,args.demo_checkpoint)