# 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. """まねっこ決闘 教材2/5: 気力の支払い。ASCII表示、音声なし。""" from dataclasses import dataclass, replace, asdict STAGE = 2 NAMES = ("UP SMALL", "DOWN SMALL", "UP BIG", "DOWN BIG", "BREATHE") SIDES = ("up", "down", "up", "down", None) DAMAGE = (1, 1, 3, 3, 0) COST = (0, 0, 2, 2, 0) ENERGY_CAP = 4 INITIAL_ENERGY = 2 INITIAL_CARDS = (2, 2, 1, 1, 1) INITIAL_HP = 8 ROUND_LIMIT = 7 @dataclass(frozen=True) class State: cards: tuple = INITIAL_CARDS hp: int = INITIAL_HP energy: int = INITIAL_ENERGY round: int = 0 status: str = "play" last: int = -1 damage: int = 0 reason: str = "ready" def initial(): return State() def refusal(state, card): if state.status != "play": return "end" if type(card) is not int or card not in range(len(NAMES)): return "invalid" if state.cards[card] == 0: return "empty" if state.energy < COST[card]: return "energy" return "" def step(state, card): if refusal(state, card): return state cards = list(state.cards) cards[card] -= 1 energy = min(ENERGY_CAP, state.energy+2) if card==4 else state.energy-COST[card] damage = DAMAGE[card] hp = max(0, state.hp-damage) turn = state.round+1 available = any(n and energy>=COST[j] for j,n in enumerate(cards)) status = "won" if hp==0 else "lost" if turn>=ROUND_LIMIT or not available else "play" reason = "breath_full" if card==4 and state.energy==ENERGY_CAP else "breath" if card==4 else "hit" if status != "play": reason = "complete" if status=="won" else "remaining" return State(cards=tuple(cards), hp=hp, energy=energy, round=turn, status=status, last=card, damage=damage, reason=reason) class App: def __init__(self): self.reset() def reset(self): self.state = initial() self.selected = 0 self.gated = False self.message = "READY" 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 in ("LEFT", "RIGHT"): self.selected = (self.selected+(-1 if key=="LEFT" else 1))%len(NAMES) self.message = "SELECT ONLY" elif key in tuple(str(n+1) for n in range(len(NAMES))): self.selected = int(key)-1 self.message = "SELECT ONLY" elif key == "Z": why = refusal(self.state, self.selected) self.state = step(self.state, self.selected) self.message = why.upper() if why else self.state.reason.upper() def draw(self, p): s = self.state p.cls(1) p.text(8,8,"MIMIC DUEL / STAGE "+str(STAGE),10) p.text(8,23,"HP "+str(s.hp)+" / "+str(INITIAL_HP)+" ROUND "+str(s.round)+" / "+str(ROUND_LIMIT)+" "+s.status.upper(),7) p.rect(8,37,240,8,5) p.rect(8,37,240*s.hp/INITIAL_HP,8,8) p.text(8,55,"ENERGY "+str(s.energy)+" / "+str(ENERGY_CAP),11) p.text(8,70,"PRACTICE: NO GUARD / NO LEARNING",6) for n,name in enumerate(NAMES): y = 106+n*23 p.rect(8,y,400,20,2 if n==self.selected else 1) if n==self.selected: p.rectb(8,y,400,20,10) p.text(15,y+6,str(n+1)+" "+name+" LEFT "+str(s.cards[n])+" DMG "+str(DAMAGE[n]),7) p.text(276,y+6,"COST "+str(COST[n]),11) preview = step(s,self.selected) why = refusal(s,self.selected) p.text(8,232,"NEXT: "+(why.upper() if why else "HP "+str(preview.hp)+" / "+preview.reason.upper()),10) p.text(8,249,self.message,14) p.text(8,267,"ARROWS / NUMBER: SELECT Z: USE R: RESET",7) p.text(8,282,"ESC: QUIT RELEASE KEYS BETWEEN ACTIONS",7) DEMO_KEYS = ('1', 'Z', '1', 'Z', '3', 'Z', '5', 'Z', '4', 'Z') def main(headless=False, screenshot=None, demo=False): import pyxel as p p.init(420,300,title="Mimic Duel lesson "+str(STAGE),fps=30,display_scale=2,headless=headless) app = App() keys = {"LEFT":p.KEY_LEFT,"RIGHT":p.KEY_RIGHT,"Z":p.KEY_Z,"R":p.KEY_R,"X":p.KEY_X} keys.update({str(n):getattr(p,"KEY_"+str(n)) for n in range(1,6)}) limit = max(16,2*len(DEMO_KEYS)+6) if demo else 16 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: n=p.frame_count//2 pressed={DEMO_KEYS[n]} if p.frame_count%2==0 and n