# 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. """橋を渡る楽団・教材3/5。整数の玩具で、現実の橋の安全計算ではありません。""" from dataclasses import dataclass,replace,asdict WEIGHTS = (2, 1, 3) STRENGTHS = (11, 9, 16) @dataclass(frozen=True) class Level: id: str title: str widths: tuple capacity: int entries: tuple beats: tuple limit: int = 32 LEVELS = (Level('A', 'そろい足の三人', (1, 2, 1), 6, (1, 5, 9), (4, 4, 4)), Level('B', 'ばらけた足拍子', (1, 2, 1), 6, (1, 4, 7), (3, 3, 3)), Level('C', '中央が二枚の橋', (1, 2, 2, 1), 8, (1, 5, 9), (4, 4, 4))) @dataclass(frozen=True) class State: boards: tuple t: int = 0 positions: tuple = (0,0,0) loads: tuple = () status: str = 'edit' vectors: tuple = () strikes: tuple = () peak: int = 0 def initial(level): n=len(level.widths) return State((0,)*n,loads=(2,)*n,vectors=((0,0),)*n,strikes=(0,)*n) def total(s): return sum((WEIGHTS[b] for b in s.boards)) def damping(v): return v - (v > 0) + (v < 0) def rotate(pair, t): x, y = pair y, x = (-x, y) return (damping(x), damping(y)) if t % 4 == 0 else (x, y) def advance(level,s,impact=4): if s.status!='run': return s t=s.t+1 n=len(s.boards) positions=tuple(0 if t=level.limit else 'run') return replace(s,t=t,positions=positions,loads=loads,status=status,vectors=vectors,strikes=strikes,peak=max(s.peak,*amplitudes)) STAGE = 3 LEVEL = LEVELS[0] BOARD_LABELS = ("PLAIN","LIGHT","BRACE") class App: def __init__(self): self.reset() def reset(self): self.state=initial(LEVEL) self.cursor=0 self.pending=0 self.gated=False self.message='READY' def commit(self,action): before=self.state state=self.state if action=='start' and state.status=='edit' and total(state)<=LEVEL.capacity: self.state=replace(state,status='run') elif action=='tick': self.state=advance(LEVEL,state) elif action.startswith('board:') and state.status=='edit': _,index,board=action.split(':') index,board=int(index),int(board) if 0<=indexLEVEL.capacity: self.message='TOO HEAVY: LIGHTEN A BOARD' 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));self.gated=True if key=='RETURN':self.commit('start') elif key=='SPACE':self.commit('tick') elif self.state.status=='edit': if key in ('LEFT','RIGHT'): self.cursor=(self.cursor+(-1 if key=='LEFT' else 1))%len(self.state.boards) self.pending=self.state.boards[self.cursor] elif key in ('UP','DOWN'): self.pending=(self.pending+(1 if key=='UP' else -1))%3 elif key=='Z':self.commit('board:'+str(self.cursor)+':'+str(self.pending)) def draw(self,p): s=self.state;n=len(s.boards) p.cls(1) p.text(12,10,'BRIDGE ORCHESTRA / STAGE '+str(STAGE)+' / A',10) p.text(12,27,'TICK '+str(s.t)+' / STATUS '+s.status.upper(),7) p.text(12,44,'BOARD WEIGHT '+str(total(s))+' / CAP '+str(LEVEL.capacity),8 if total(s)>LEVEL.capacity else 11) p.text(12,62,'ENTRY 1,5,9 / WALK EVERY 4 TICKS / PEOPLE WEIGHT 2',6) for i,board in enumerate(s.boards): x=72+i*120 p.rect(x,145,116,18,(5,6,9)[board]);p.rectb(x,145,116,18,10 if i==self.cursor else 7) p.text(x+8,175,str(i)+': '+BOARD_LABELS[board],7) p.text(x+8,192,'LOAD '+str(s.loads[i]),7) if hasattr(s,'vectors'): p.text(x+8,209,'V '+str(s.vectors[i]),14) p.text(x+8,226,'HITS '+str(s.strikes[i]),14) if STAGE>=4: p.text(x+8,243,'LIMIT '+str(limits(LEVEL,s)[i]),8 if i in s.faults else 11) p.rect(12,145,54,80,3);p.rect(438,145,54,80,3) for i,position in enumerate(s.positions): x=35 if position==0 else 465 if position==n+1 else 125+(position-1)*120 y=130-i*14 p.circ(x,y,5,(8,10,12)[i]);p.text(x-1,y-2,str(i+1),1) if STAGE>=2:p.text(12,275,'CANDIDATE '+str(self.cursor)+': '+BOARD_LABELS[self.pending]+' (Z TO COMMIT)',7) if STAGE>=4: q=forecast(LEVEL,s) p.text(12,294,'NEXT TICK LOADS '+str(q.loads) if q else 'NO NEXT-TICK FORECAST',6) p.text(12,311,'FAULT SEGMENTS '+str(s.faults)+' / PEAK '+str(s.peak),8 if s.faults else 6) elif STAGE==3:p.text(12,294,'PRACTICE: LOADS ONLY / STRENGTH NOT CHECKED YET',14) else:p.text(12,294,'PRACTICE: QUIET FOOTSTEPS / NO STRENGTH CHECK',14) p.text(12,350,self.message,10) p.text(12,372,'ENTER: START / SPACE: ONE TICK / R: RESET / ESC: QUIT',7) if STAGE>=2:p.text(12,389,'ARROWS: SEGMENT + CANDIDATE / Z: COMMIT IN EDIT',7) if STAGE==5:p.text(12,406,'X: UNDO (DECISION COUNT DOES NOT REWIND)',7) def make_demo_keys(): return ('RIGHT','UP','UP','Z','RIGHT','UP','Z','RETURN')+('SPACE',)*9 def main(headless=False,screenshot=None,demo=False): import pyxel as p p.init(508,432,title='Bridge orchestra lesson '+str(STAGE),fps=30,display_scale=2,headless=headless) app=App();sequence=make_demo_keys() if demo else () keys={'LEFT':p.KEY_LEFT,'RIGHT':p.KEY_RIGHT,'UP':p.KEY_UP,'DOWN':p.KEY_DOWN,'RETURN':p.KEY_RETURN,'SPACE':p.KEY_SPACE,'Z':p.KEY_Z,'X':p.KEY_X,'R':p.KEY_R} limit=max(20,2*len(sequence)+8) 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() if headless: if demo: index=p.frame_count//2 pressed={sequence[index]} if p.frame_count%2==0 and index