# -*- coding: utf-8 -*- # LICENSE.txt (unchanged text): # 022 Word Bridge / ことばの橋 # Original code and pixel shapes: provided for the user's game project. # Bundled Japanese bitmap glyphs were rendered from Noto Sans CJK Regular. # Noto font data is licensed under SIL Open Font License 1.1. # See assets/NOTO-LICENSE.txt for copyright and the complete license. # Glyph generation: tools/build_font.py. Pillow is a development-only dependency. # Runtime engine: Pyxel 2.9.9 (MIT license, installed separately by pip). # No downloaded sprites, music, sound recordings, network services or LLM models. # DISTRIBUTION_PERMISSION.txt (unchanged text): # 022 ことばの橋:公開・教材配布の補足 # 記録日:2026年10月6日 # # この作品のコードと自作図形、および本講座用の段階教材は、プロジェクト所有者の承認に基づき、 # 「Pyxel入門 Pythonゲームの作り方」で学習・改造用のソース付き教材として配布しています。 # 教材をダウンロードし、内容を読んで学び、ご自身の制作のために変更して試すことができます。 # # 原納品物の LICENSE.txt は変更せずに同梱しています。 # この補足は今回の公開・教材配布と学習利用の許諾を記録するものであり、原LICENSEをMIT本文へ # 置き換えたり、元からMITの記載があったものとして扱ったりするものではありません。 # 本作品と原作の権利表示を、ほかの作品の権利表示で上書きしないでください。 # # コード・図形と外部ライブラリー・字形データの条件は区別します。 # ・ゲームの図形は、Pyxelの矩形・線・円・三角形などで描いたものです。 # ・元の完成版の日本語字形はNoto Sans CJK Regular由来です。 # SIL Open Font License 1.1の原文を assets/NOTO-LICENSE.txt に保持しています。 # ・Pyxelは別途インストールする外部ライブラリーです。Pyxel自身のライセンスが適用されます。 # ・段階教材の画面は標準ASCII描画を使い、Noto字形データの追加導入を必要としません。 # # 元コード・図形の提供に関する記載、外部由来の字形の著作権とライセンスは、原本のまま残しています。 """022 Word Bridge: a standalone, incrementally runnable teaching stage. Original notice: LICENSE.txt. Teaching distribution: DISTRIBUTION_PERMISSION.txt. No image/font file is needed. Pyxel is imported only when the window starts. """ from dataclasses import dataclass, replace, asdict from pathlib import Path import json STAGE = 5 STAGE_TITLE = 'UNDO AND THREE ORIGINAL LAYOUTS' CONTROLS = ['ARROWS MOVE / SPACE PREVIEW, RELEASE, SPACE CONFIRM', 'Z CANCEL OR UNDO / R RESET / P PAUSE / Q TITLE', 'AFTER SUCCESS N NEXT / ESC QUIT / RELEASE ALL KEYS'] # Original pure model, retained verbatim below. """Finite, immutable river model. No renderer, clock, randomness, or network.""" from dataclasses import dataclass, replace, asdict import hashlib, json DIRECTIONS={'UP':(0,-1),'DOWN':(0,1),'LEFT':(-1,0),'RIGHT':(1,0)} ACTIONS=tuple(DIRECTIONS)+('SWAP',) REASONS={'ready':'荷物を旗まで届けよう','move':'浮いている石だけ歩ける', 'water':'水には進めません','sunk':'その石は沈んでいます','bank':'陸で文を替えよう', 'swap':'同じ印の石が、全部入れ替わった','success':'配達できた!','invalid':'その文は作れません', 'terminal':'配達済みです','cancel':'文を替えずに戻りました','same':'文はそのまま','undo':'一手戻しました'} @dataclass(frozen=True) class Level: title:str width:int height:int dry:frozenset light:frozenset heavy:frozenset start:tuple goal:tuple floating:str def __post_init__(self): if self.floating not in ('L','H'):raise ValueError('unknown category') if self.dry&self.light or self.dry&self.heavy or self.light&self.heavy:raise ValueError('overlapping terrain') if self.start not in self.dry or self.goal not in self.dry:raise ValueError('endpoints must be dry') if self.start==self.goal:raise ValueError('endpoints must differ') if any(not(0<=x= 4: app.draw(p) if screenshot: p.screen.save(screenshot, 1) if state_json: with Path(state_json).open('x', encoding='utf-8') as stream: json.dump(snapshot(app), stream, ensure_ascii=False, indent=2) stream.write('\n') p.quit() return app.feed(set() if headless else {key for key, code in keymap.items() if p.btn(code)}) p.run(update, lambda: app.draw(p)) if __name__ == '__main__': import argparse parser = argparse.ArgumentParser(description='Standalone Word Bridge teaching stage') parser.add_argument('--headless-smoke', action='store_true') parser.add_argument('--demo-checkpoint', action='store_true') parser.add_argument('--screenshot') parser.add_argument('--state-json') parser.add_argument('--case', choices=('A', 'B', 'C')) args = parser.parse_args() if (args.demo_checkpoint or args.screenshot is not None or args.state_json is not None or args.case) and not args.headless_smoke: parser.error('diagnostic options require --headless-smoke') if args.case and STAGE != 5: parser.error('--case is available only in stage 05') for field, suffix in (('screenshot', '.png'), ('state_json', '.json')): value = getattr(args, field) if value is not None: if not value.strip(): parser.error('output path must not be empty') target = Path(value).absolute() if target.suffix.lower() != suffix or not target.parent.is_dir() or target.exists() or target.is_symlink(): parser.error('output must be a new ' + suffix + ' in an existing directory') setattr(args, field, str(target)) main(args.headless_smoke, args.screenshot, args.state_json, args.demo_checkpoint, 'ABC'.index(args.case) if args.case else None)