# 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. # # Exception: assets/glyphs.json contains rasterized glyph data derived from # Noto Sans CJK JP (SIL Open Font License 1.1). See assets/NOTO-LICENSE.txt. # Pyxel is an external dependency under its own MIT license. """最後の一匹・教材 2/5:距離と張力を分ける。 原作を簡略化した独立実行ファイル。実行: python 02_meters.py 01〜04はメーター観察の練習、05は小魚1匹を釣るゲームです。 日本語フォント素材は使わず、標準フォントと図形だけで描きます。 """ WIDTH, HEIGHT = 280, 210 FPS = 30 class Game: def __init__(self): self.distance = 70.0 self.held = False self.reeled = 0 self.released = 0 self.tension = 20.0 def step(self, held=False): self.held = held # btnでは長押しを読む。離したフレームで反対のカウンターを0にする。 self.reeled = self.reeled + 1 if held else 0 self.released = 0 if held else self.released + 1 if held: self.distance -= 0.42 self.tension += 0.60 else: self.distance += 0.45 self.tension = max(0.0, self.tension - 1.30) # 01〜04は練習用。端に来ても終了せずメーターを範囲内に止める。 self.distance = max(0.0, min(100.0, self.distance)) self.tension = min(100.0, self.tension) def main(headless=False, screenshot=None): # importしてGameを試すだけならPyxelは不要。描画のときだけ読み込む。 import pyxel from pathlib import Path # Pyxel初期化より前に、呼び出した場所からの保存先を確定する。 if screenshot: screenshot = str(Path(screenshot).expanduser().resolve()) game = Game() frames = 0 screenshot_saved = False def update(): nonlocal game, frames # 12回の更新と12回の描画の後、次の更新で終了する。 if headless and frames >= 12: pyxel.quit() return frames += 1 if pyxel.btnp(pyxel.KEY_R): game = Game() return game.step(pyxel.btn(pyxel.KEY_SPACE)) def bar(y, label, value, color): shown = max(0.0, min(100.0, value)) pyxel.text(10, y, label, 7) pyxel.rect(64, y, 164, 7, 0) pyxel.rect(65, y + 1, int(shown * 1.62), 5, color) pyxel.text(237, y, f"{shown:5.1f}", color) def draw(): nonlocal screenshot_saved pyxel.cls(1) pyxel.text(10, 8, "THE LAST FISH / STEP 2", 10) pyxel.text(10, 19, "PRACTICE: METERS DO NOT END THE GAME", 6) pyxel.text(10, 35, "SPACE: " + ("HELD / REEL" if game.held else "RELEASED / SLACK"), 7) pyxel.text(10, 46, f"HELD {game.reeled:3}f RELEASED {game.released:3}f", 6) bar(60, "DIST", game.distance, 12) bar(74, "TENSION", game.tension, 8 if game.tension >= 75 else 10) # 距離が0なら岸側、100なら画面右。状態の計算はここでしない。 x = 54 + max(0.0, min(100.0, game.distance)) * 1.90 pyxel.rect(0, 134, WIDTH, 38, 5) pyxel.line(0, 140, WIDTH - 1, 140, 6) pyxel.rect(0, 131, 38, 9, 4) pyxel.line(24, 133, 42, 122, 9) pyxel.line(42, 122, x, 151, 7) pyxel.elli(x - 9, 146, 18, 10, 10) pyxel.tri(x + 8, 151, x + 16, 145, x + 16, 157, 10) pyxel.pset(x - 5, 149, 0) pyxel.text(10, 104, "ONE BUTTON, TWO DIFFERENT ACTIONS", 10) pyxel.text(10, 178, "R RESTARTS THE SAME PRACTICE", 6) pyxel.text(10, 196, "SPACE: REEL R: RESET ESC: QUIT", 6) # 12回目に描いた本物のPyxel画面を保存。自動プレイ画像ではない。 if screenshot and frames >= 12 and not screenshot_saved: pyxel.screenshot(screenshot, scale=2) screenshot_saved = True pyxel.init(WIDTH, HEIGHT, title="The Last Fish - Stage 2", fps=FPS, headless=headless) pyxel.run(update, draw) if __name__ == "__main__": import argparse parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--headless-smoke", action="store_true", help="Render 12 real Pyxel frames then quit; not a full playtest") parser.add_argument("--screenshot", metavar="PATH", help="Save the 12th rendered frame as a PNG") args = parser.parse_args() main(headless=args.headless_smoke, screenshot=args.screenshot)