# 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, replace, asdict from itertools import permutations from functools import lru_cache import hashlib, json NAMES = 'ABC' ITEMS = ('チーズ', 'りんご', '木の実') @dataclass(frozen=True) class Observation: witness: int target: int item: int @dataclass(frozen=True) class Card: witness: int target: int answer: int def perceive(item, weakness): return (item + 1) % 3 if item == weakness else item def testimony(observation, weakness): """A witness receives only one observed item, not the full hidden assignment.""" return Card(observation.witness, observation.target, perceive(observation.item, weakness)) STAGE = 1 LABELS = ("CHEESE", "APPLE", "NUT") # 学習用の公開された一品。隠れた配置全体は証言関数へ渡さない。 START_ITEM = 0 START_WEAKNESS = 0 class App: def __init__(self): self.reset() def reset(self): self.item = START_ITEM self.weakness = START_WEAKNESS self.card = None self.gated = False self.message = 'READY: Z RECORDS ONE OBSERVATION' 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.item = (self.item + (-1 if key == 'LEFT' else 1)) % 3 elif key in ('UP', 'DOWN'): self.weakness = (self.weakness + (-1 if key == 'UP' else 1)) % 3 elif key == 'Z': observation = Observation(0, 1, self.item) self.card = testimony(observation, self.weakness) self.message = 'RECORDED: CURSOR EDITS DO NOT REWRITE THIS CARD' def draw(self, p): p.cls(1) p.text(16, 12, 'MOUSE COUNCIL / STAGE 1 / PERCEPTION LAB', 10) p.text(16, 40, 'PUBLIC SAMPLE ITEM: ' + LABELS[self.item], 7) p.text(16, 58, 'WITNESS A WEAKNESS: ' + LABELS[self.weakness], 7) p.text(16, 82, 'ONLY THE WEAK ITEM LOOKS LIKE THE NEXT ITEM.', 6) p.text(16, 98, '0 CHEESE -> 1 APPLE -> 2 NUT -> 0 CHEESE', 6) for item in range(3): y = 139 + item * 36 p.rectb(14, y-7, 462, 27, 10 if item == self.item else 5) p.text(24, y, LABELS[item] + ' -> ' + LABELS[perceive(item, self.weakness)], 7) p.text(16, 266, 'LAST CARD A > B: ' + (LABELS[self.card.answer] if self.card else '(none)'), 11) p.text(16, 290, 'Observation(witness=0, target=1, item=one item)', 13) p.text(16, 308, 'No hidden assignment is passed to testimony().', 13) p.text(16, 362, self.message, 10) p.text(16, 402, 'LEFT/RIGHT: ITEM UP/DOWN: WEAKNESS Z: RECORD', 7) p.text(16, 420, 'R: RESET ESC: QUIT / NO TICKETS OR SOLVING YET', 7) def make_demo_keys(): return ('Z', 'RIGHT', 'Z') def main(headless=False, screenshot=None, demo=False): # Validate and resolve BEFORE importing Pyxel: relative screen.save paths # otherwise refer to the script folder, not the caller's working directory. from pathlib import Path if demo and not headless: raise ValueError('demo requires headless mode') if screenshot: target = Path(screenshot) if not headless or target.suffix.lower() != '.png' or not target.parent.is_dir() or target.exists(): raise ValueError('use a new .png in an existing directory with headless mode') screenshot = str(target.resolve()) import pyxel as p p.init(492, 452, title='Mouse council lesson '+str(STAGE), fps=30, display_scale=2, headless=headless) app = App(); sequence = make_demo_keys() if demo else () names = ('UP','DOWN','LEFT','RIGHT','Z','SPACE','TAB','H','O','X','R','1','2','3') keys = {name:getattr(p, 'KEY_'+name) for name in names} limit = max(20, len(sequence)*2+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(); return if headless: index = p.frame_count//2 pressed = {sequence[index]} if p.frame_count%2 == 0 and index < len(sequence) else set() app.control(pressed, pressed) else: app.control({k for k,v in keys.items() if p.btnp(v)}, {k for k,v in keys.items() if p.btn(v)}) p.run(update, lambda: app.draw(p)) if __name__ == '__main__': import argparse 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() try: main(args.headless_smoke, args.screenshot, args.demo_checkpoint) except ValueError as error: parser.error(str(error))