# 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. # 上記は原作の通知全文です。この教材にNoto由来の画像・文字資産は含めません。 """026 音のないピアノ / 教材 1/5。単独で実行できます。""" import argparse from pathlib import Path # 1桁のbitは0(消灯)か1(点灯)。左から重み4, 2, 1です。 # 例: 110は4+2=6。0b110はPythonで2進数を書く記法です。 # XOR(^)は各桁を比較し、違うなら1、同じなら0にします。 # 灯り ^ 配線maskでは、maskが1の場所だけ0<->1が反転します。 # 000 ^ 110 = 110、110 ^ 110 = 000。足し算ではありません。 MASKS = (0b110, 0b011, 0b001) WIDTH, HEIGHT = 384, 360 FPS = 30 def bits(value): """整数を左→右の3桁へ変換。100は左だけ点灯。""" return format(value, "03b") class Game: def __init__(self): self.lamps = 0 def press(self, key): if not isinstance(key, int) or isinstance(key, bool) or not 0 <= key < 3: return False before = self.lamps mask = MASKS[key] self.lamps = before ^ mask return True def main(headless_smoke=False, screenshot=None, demo=False): import pyxel # 純粋なGameのテストだけならPyxelを読み込みません。 game = Game() selected = 0 frames = 0 locked = False message = "LEFT/RIGHT select. SPACE presses one key." controls = [pyxel.KEY_LEFT, pyxel.KEY_RIGHT, pyxel.KEY_SPACE, pyxel.KEY_R] pyxel.init(WIDTH, HEIGHT, title="026 SILENT PIANO - STEP 1", fps=FPS, display_scale=2, headless=headless_smoke) def update(): nonlocal game, selected, frames, locked, message frames += 1 if demo: if frames in (2, 4): game.press(0) elif frames == 6: selected = 1 game.press(1) message = "AUTO DEMO: scripted actions, not a human test." return # R/Z/Nの直後に押しっぱなしの別キーが流入しないよう全解放を待つ。 if locked: if not any(pyxel.btn(key) for key in controls): locked = False return if pyxel.btnp(pyxel.KEY_R): game = Game() selected = 0 locked = True message = "NEW PRACTICE: everything reset." return left = pyxel.btnp(pyxel.KEY_LEFT) right = pyxel.btnp(pyxel.KEY_RIGHT) if (left or right) and pyxel.btn(pyxel.KEY_LEFT) and pyxel.btn(pyxel.KEY_RIGHT): message = "Choose one direction at a time." return if left or right: selected = (selected + (-1 if left else 1)) % 3 return if pyxel.btnp(pyxel.KEY_SPACE): game.press(selected) message = "Pressed " + "ABC"[selected] + ": compare before and after." def draw_lamps(x, y, value): for index, digit in enumerate(bits(value)): xx = x + index * 30 pyxel.circ(xx, y, 10, 10 if digit == "1" else 0) pyxel.circb(xx, y, 10, 6) pyxel.text(xx - 2, y - 2, digit, 1 if digit == "1" else 7) def draw(): pyxel.cls(1) pyxel.text(12, 10, "026 SILENT PIANO / STEP 1", 7) pyxel.text(12, 26, "NOW " + bits(game.lamps) + " LEFT -> RIGHT", 6) draw_lamps(40, 57, game.lamps) pyxel.text(224, 44, "PRACTICE: NO GOAL YET", 13) for key in range(3): x = 12 + key * 120 pyxel.rect(x, 79, 110, 30, 7) pyxel.rectb(x, 79, 110, 30, 10 if key == selected else 5) pyxel.text(x + 12, 92, "ABC"[key] + (" " if key == selected else ""), 1) pyxel.text(12, 118, message, 7) pyxel.text(12, 142, " ".join("ABC"[i] + "=" + bits(mask) for i, mask in enumerate(MASKS)) + " : 1 means TOGGLE", 10) pyxel.text(12, 166, "XOR per bit: same -> 0, different -> 1", 6) pyxel.text(12, 184, "000 XOR 110 = 110", 7) pyxel.text(12, 202, "110 XOR 110 = 000", 7) pyxel.text(12, 230, "Press the same key twice: lamps come back.", 13) pyxel.text(12, 310, "LEFT/RIGHT select SPACE press", 7) pyxel.text(12, 326, "R new practice ESC quit", 7) pyxel.text(12, 344, "SILENT / ASCII UI / 30 FPS", 5) # 12回目の描画後に保存・終了。実際のPyxel描画を使います。 if frames == 12 and screenshot: path = Path(screenshot) path.parent.mkdir(parents=True, exist_ok=True) pyxel.screenshot(str(path), scale=2) if frames == 12 and headless_smoke: print("headless-smoke: 12 frames rendered", flush=True) pyxel.quit() pyxel.run(update, draw) if __name__ == "__main__": parser = argparse.ArgumentParser(description="026 silent piano teaching stage") parser.add_argument("--headless-smoke", action="store_true") parser.add_argument("--screenshot", metavar="PATH") parser.add_argument("--demo", action="store_true", help="run deterministic scripted actions") args = parser.parse_args() main(args.headless_smoke, args.screenshot, args.demo)