完成サンプルをブラウザーで遊ぶ
この講座のもとになった完成版のゲームを、ブラウザーでそのまま遊べます。キーボードで操作するので、PCで開いてください(スマホのタッチ操作には対応していません)。初めて開くときは、読み込みに少し時間がかかります。
完成版の主な操作:Spaceを押している間は巻く・離すと緩める、魚と魚の間に左右で選んでSpaceで決定、PかEscで一時停止、Rでやり直し、Tabで魚の群れを選ぶ、ウィンドウの×で終了
ボタンを押すと、ゲームの読み込みが始まります。
画面に「CLICK TO START」と出たらクリックします。続けてゲーム画面を一度クリックすると、キー操作が効くようになります。ゲームを終えるときは、枠の外にある「閉じる」ボタンを使ってください(Escキーの働きはゲームごとに違います)。ゲーム内のセーブは、閉じる・ページを移る・再読み込みすると消えます。
うまく表示されないとき
ゲームを動かすためのプログラム(Pyxel 2.9.9)は、外部の配信サービス(jsDelivr)から読み込みます。表示されないときは「最初から再読込」を押すか、完成版のソースをダウンロードしてPCで実行してください。ブラウザー版の録画機能は止めています。ブラウザー版で確認できている範囲は検証状況にまとめています。
ボタンを押している時間と、離している時間をルールにする。
いきなり完成版のコードを全部読む必要はありません。小さなゲームにルールを1つずつ足しながら、5つのステップで作っていきます。どのステップのファイルもそれだけで動くので、動きを確かめてから次へ進みましょう。
- 必要な環境
- Python 3.11以上 / Pyxel 2.9.9
- 知っておくとよいこと
- 変数・if・関数の基本
- 教材に入っているもの
- ステップごとに動くPythonファイル5本・説明・動作確認用のテスト
- 完成した教材の操作
- SPACE:押している間は巻く、離すと緩める / R:同じ最初の状態からやり直す / P:一時停止・再開(05のみ) / ESC:終了(元のゲームの Esc は一時停止です)
- 難易度の目安
- 初級このページで使うプログラムの考え方の数と難しさから決めた目安です。難易度について
- 学習順に迷ったら
- おすすめの学ぶ順番を見る
コードを書き換えたら、ファイルを保存し、動いているゲームを閉じてから、もう一度同じ.pyを実行します。ゲーム中にRキーなどで「やり直し」をしても、書き換えたコードは読み込まれません。
教材のゲーム画面は、日本語フォントを用意しなくても動くように英語で表示しています。ファイルには動作確認用のコードも入っていますが、最初は Game / State・update・draw の3つに注目すれば十分です。わからない言葉は用語集で調べられます。class・self・dataclass はPythonの状態、キー入力と時間の扱いは入力・更新・描画、マス目とピクセルの違いは画面とマップの座標のページで詳しく説明しています。
長押しと離した時間
押した瞬間と、押し続けている間を区別する。
Space を押しているフレームの間はずっと held が True、離している間は False になります。btn は押し続けているかどうか、btnp は押した瞬間かどうかを調べる関数です。reeled は続けて押したフレーム数、released は続けて離したフレーム数です。片方が増えるときにもう片方を0に戻すことで、「合計」ではなく「続けて何フレームか」を数えられます。魚までの距離 DIST は、押し続けると減り、離すと増えます。まだ勝ち負けはありません。
下のコードは 01_button.py の一部です。前のステップから追加・変更したところだけを抜き出しています。
self.held = held
# btnでは長押しを読む。離したフレームで反対のカウンターを0にする。
self.reeled = self.reeled + 1 if held else 0
self.released = 0 if held else self.released + 1実行して確かめる
教材のZIPを展開したフォルダーで、次のコマンドを実行します。
.venv\Scripts\python.exe 01_button.py.venv/bin/python 01_button.py- Spaceの長押しでHELDが増える
- 離すとHELDは0・RELEASEDが増える
- Rで同じ初期状態へ戻る
btnp(KEY_SPACE) にすると、押し続けても最初の1フレームしか動きません。また、反対側のカウンターを0に戻し忘れると、続けて押した時間を測れません。step(True) を8回、step(False) を4回、step(True) を1回呼んだとき、それぞれの reeled と released を紙に書いてみましょう。答えは順に (8,0)、(0,4)、(1,0) です。1フレームは1/30秒なので、30回で1秒になります。
このステップのコード全文(127行)
# 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.
"""最後の一匹・教材 1/5:長押しと離した時間。
原作を簡略化した独立実行ファイル。実行: python 01_button.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
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
else:
self.distance += 0.45
# 01〜04は練習用。端に来ても終了せずメーターを範囲内に止める。
self.distance = max(0.0, min(100.0, self.distance))
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 1", 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)
pyxel.text(10, 75, "HOLD: CLOSER / RELEASE: FARTHER", 6)
# 距離が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 1", 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)距離と張力を分ける
1つの入力で、得をする数値と危険な数値を同時に変える。
DIST は魚までの距離、TENSION は糸の張り具合(張力)です。巻くと1フレームごとに距離が0.42減り、張力が0.60増えます。離すと距離が0.45増え、張力が1.30減ります。近づけようと巻き続けると張力が上がりすぎ、緩めてばかりだと魚が遠ざかります。ここは練習なので、値を0〜100の範囲に収めるだけです。勝ち負けは05で追加します。
下のコードは 02_meters.py の一部です。前のステップから追加・変更したところだけを抜き出しています。
if held:
self.distance -= 0.42
self.tension += 0.60
else:
self.distance += 0.45
self.tension = max(0.0, self.tension - 1.30)実行して確かめる
教材のZIPを展開したフォルダーで、次のコマンドを実行します。
.venv\Scripts\python.exe 02_meters.py.venv/bin/python 02_meters.py- 長押しは近づくが張力が上がる
- 離すと張力が下がるが遠ざかる
- 0未満・100超のメーターにならない
max(0.0, ...) を外すと張力がマイナスになり、次に押し続けたときに、不自然に安全な時間ができてしまいます。最初の値から1フレーム巻いたあとの距離69.58・張力20.60と、続けて1フレーム離したあとの距離70.03・張力19.30を、計算で確かめてみましょう。次に、張力の増え方 0.60 だけを 0.30 に変えて、距離の変わり方が同じままか観察してみましょう。
このステップのコード全文(131行)
# 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)本物の反転を予告する
フレームを一定の周期で区切り、最後の1回だけ出来事を起こす。
tick(経過フレーム数)を84で割った余り local を使い、「泳ぐ」36フレーム・「前ぶれ」24フレーム・「休み」24フレームをくり返します。この段階の前ぶれは、すべて本物の反転(魚が向きを変える動き)で、右向きの矢印と REAL TURN IN 24〜1 が表示されます。local=59 で更新するときに1回だけ反転します。そのとき巻いていれば張力が+65、離していれば+16になり、どちらも距離が+8されます。04で作る成功のごほうびは、まだありません。休み中は、巻いたときの張力の増え方が+0.20に下がります。
下のコードは 03_cue.py の一部です。前のステップから追加・変更したところだけを抜き出しています。
# 予兆の最後だけ判定する。24フレーム全てに加算しない。
if self.local == TURN_FRAME:
self.tension += 65 if held else 16
self.distance += 8
self.feedback = "TURN! HELD +65" if held else "TURN! RELEASED +16"
self.feedback_ticks = 35実行して確かめる
教材のZIPを展開したフォルダーで、次のコマンドを実行します。
.venv\Scripts\python.exe 03_cue.py.venv/bin/python 03_cue.py- 右向き矢印で本物の反転が予告される
- 24〜1の予兆の最後だけ大きく変わる
- 休みでは張力の上がり方が小さい
if phase == "warning" の中だけで+65すると、前ぶれの24フレーム分、24回も足されてしまいます。最後の local=59 のときに1回だけ判定します。また、tick を先に増やすと、見えている前ぶれと判定が1フレームずれます。tick が35、36、58、59、60、83、84のときの phase と turn_in を予想してみましょう。36は前ぶれ24、59は前ぶれ1、60は休みです。CRUISE_FRAMES だけを60に変えると、前ぶれの長さはそのままで、泳ぐ時間だけを長くできます。
このステップのコード全文(177行)
# 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.
"""最後の一匹・教材 3/5:本物の反転を予告する。
原作を簡略化した独立実行ファイル。実行: python 03_cue.py
01〜04はメーター観察の練習、05は小魚1匹を釣るゲームです。
日本語フォント素材は使わず、標準フォントと図形だけで描きます。
"""
WIDTH, HEIGHT = 280, 210
FPS = 30
CRUISE_FRAMES = 36
WARNING_FRAMES = 24
REST_FRAMES = 24
CYCLE_FRAMES = CRUISE_FRAMES + WARNING_FRAMES + REST_FRAMES
TURN_FRAME = CRUISE_FRAMES + WARNING_FRAMES - 1 # 0始まりなので59
class Game:
def __init__(self):
self.distance = 70.0
self.held = False
self.reeled = 0
self.released = 0
self.tension = 20.0
self.tick = 0
self.feedback = ""
self.feedback_ticks = 0
@property
def local(self):
return self.tick % CYCLE_FRAMES
@property
def phase(self):
if self.local < CRUISE_FRAMES:
return "cruise"
if self.local < CRUISE_FRAMES + WARNING_FRAMES:
return "warning"
return "rest"
@property
def cue(self):
# この教材にはフェイントがない。予兆は全て本物の外向き矢印。
return "out" if self.phase == "warning" else ""
@property
def turn_in(self):
# 次のstepを含む残り回数。予兆開始で24、判定直前で1。
return TURN_FRAME - self.local + 1 if self.phase == "warning" else 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
self.feedback_ticks = max(0, self.feedback_ticks - 1)
if held:
self.distance -= 0.42
self.tension += 0.20 if self.phase == "rest" else 0.60
else:
self.distance += 0.45
self.tension = max(0.0, self.tension - 1.30)
# 予兆の最後だけ判定する。24フレーム全てに加算しない。
if self.local == TURN_FRAME:
self.tension += 65 if held else 16
self.distance += 8
self.feedback = "TURN! HELD +65" if held else "TURN! RELEASED +16"
self.feedback_ticks = 35
# 01〜04は練習用。端に来ても終了せずメーターを範囲内に止める。
self.distance = max(0.0, min(100.0, self.distance))
self.tension = min(100.0, self.tension)
self.tick += 1
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 3", 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, 92, f"PHASE {game.phase.upper():7} CYCLE {game.local:02}/{CYCLE_FRAMES}", 6)
if game.cue:
pyxel.line(15, 111, 37, 111, 10)
pyxel.tri(43, 111, 34, 106, 34, 116, 10)
pyxel.text(51, 103, f"REAL TURN IN {game.turn_in:02} FRAMES", 10)
pyxel.text(51, 115, "HELD: +65 RELEASED: +16 TENSION", 7)
elif game.feedback_ticks:
pyxel.text(10, 109, game.feedback, 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 3", 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)短く緩めて魚を疲れさせる
ボタンを続けて離していた時間を、成功の条件に使う。
反転を判定するときに、そのフレームも含めて3〜18フレーム続けて離していれば成功です。成功すると距離が24縮まり、魚の疲れ STAMINA が1増えます。疲れは最大3です。巻く速さは「0.42 + 0.12 × 疲れ」なので、成功するほど魚を寄せやすくなります。離していた時間が2フレーム以下なら遅すぎ、19フレーム以上なら早く離しすぎで、どちらも失敗です。失敗すると、03と同じ反転になります。まだ練習なので、距離が0になっても終わりません。
下のコードは 04_release.py の一部です。前のステップから追加・変更したところだけを抜き出しています。
if MIN_RELEASE <= self.released <= MAX_RELEASE:
self.distance -= 24
self.stamina = min(3, self.stamina + 1)
self.good_turns += 1
self.feedback = "GOOD TURN! DISTANCE -24"実行して確かめる
教材のZIPを展開したフォルダーで、次のコマンドを実行します。
.venv\Scripts\python.exe 04_release.py.venv/bin/python 04_release.py- 予兆の最後を3〜18フレーム離すと成功する
- 最初から離しっぱなしでは成功しない
- 疲れは3まで、成功回数はその先も増える
released を増やすと、3フレーム目が2として判定されてしまいます。このコードは、先に離している時間を数えてから判定します。また、疲れと成功回数は別の数値で、3で止まるのは疲れだけです。test_stages.py の duration を1、2、3、18、19、24にしたときの結果を読んでみましょう。成功するのは3と18だけです。MAX_RELEASE だけを12に変えると、3は成功のまま、18は失敗になります。先に予想してから、テストを書き換えて試してみましょう。
このステップのコード全文(187行)
# 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.
"""最後の一匹・教材 4/5:短く緩めて魚を疲れさせる。
原作を簡略化した独立実行ファイル。実行: python 04_release.py
01〜04はメーター観察の練習、05は小魚1匹を釣るゲームです。
日本語フォント素材は使わず、標準フォントと図形だけで描きます。
"""
WIDTH, HEIGHT = 280, 210
FPS = 30
CRUISE_FRAMES = 36
WARNING_FRAMES = 24
REST_FRAMES = 24
CYCLE_FRAMES = CRUISE_FRAMES + WARNING_FRAMES + REST_FRAMES
TURN_FRAME = CRUISE_FRAMES + WARNING_FRAMES - 1 # 0始まりなので59
MIN_RELEASE = 3
MAX_RELEASE = 18
class Game:
def __init__(self):
self.distance = 70.0
self.held = False
self.reeled = 0
self.released = 0
self.tension = 20.0
self.tick = 0
self.feedback = ""
self.feedback_ticks = 0
self.stamina = 0
self.good_turns = 0
@property
def local(self):
return self.tick % CYCLE_FRAMES
@property
def phase(self):
if self.local < CRUISE_FRAMES:
return "cruise"
if self.local < CRUISE_FRAMES + WARNING_FRAMES:
return "warning"
return "rest"
@property
def cue(self):
# この教材にはフェイントがない。予兆は全て本物の外向き矢印。
return "out" if self.phase == "warning" else ""
@property
def turn_in(self):
# 次のstepを含む残り回数。予兆開始で24、判定直前で1。
return TURN_FRAME - self.local + 1 if self.phase == "warning" else 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
self.feedback_ticks = max(0, self.feedback_ticks - 1)
if held:
self.distance -= 0.42 + 0.12 * self.stamina
self.tension += 0.20 if self.phase == "rest" else 0.60
else:
self.distance += 0.45
self.tension = max(0.0, self.tension - 1.30)
if self.local == TURN_FRAME:
# 先に今回の入力を数えたので、releasedには今回も含まれる。
if MIN_RELEASE <= self.released <= MAX_RELEASE:
self.distance -= 24
self.stamina = min(3, self.stamina + 1)
self.good_turns += 1
self.feedback = "GOOD TURN! DISTANCE -24"
else:
self.tension += 65 if held else 16
self.distance += 8
self.feedback = "MISSED TURN!"
self.feedback_ticks = 35
# 01〜04は練習用。端に来ても終了せずメーターを範囲内に止める。
self.distance = max(0.0, min(100.0, self.distance))
self.tension = min(100.0, self.tension)
self.tick += 1
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 4", 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, 92, f"PHASE {game.phase.upper():7} CYCLE {game.local:02}/{CYCLE_FRAMES}", 6)
if game.cue:
pyxel.line(15, 111, 37, 111, 10)
pyxel.tri(43, 111, 34, 106, 34, 116, 10)
pyxel.text(51, 103, f"REAL TURN IN {game.turn_in:02} FRAMES", 10)
pyxel.text(51, 115, "RELEASE FOR 3-18f AT THE TURN", 7)
elif game.feedback_ticks:
pyxel.text(10, 109, game.feedback, 10)
pyxel.text(10, 178, f"STAMINA {game.stamina}/3 GOOD TURNS {game.good_turns}", 14)
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 4", 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)巻き始めと結果・再挑戦
動き出すまでの待ち時間と、止まる状態をはっきり書く。
巻き始めの1〜5フレームは、糸のたるみを取るため距離が減らず、6フレーム目から巻けます。張力は、この待ち時間の間も増えます。距離が0以下で釣り上げ、100以上で逃げられ、張力が100以上で糸が切れます。同時に起きたときは、糸が切れるほうを優先します。結果が出たあとと、P で一時停止している間は、step の最初で処理を戻すので、時間・続けて押した数・ごほうびも動きません。R キーで Game を作り直し、同じ魚にもう一度挑戦できます。
下のコードは 05_result.py の一部です。前のステップから追加・変更したところだけを抜き出しています。
# 1〜5フレーム目はたるみ取り。6フレーム目から距離が減る。
if self.reeled > REEL_DELAY:
self.distance -= 0.42 + 0.12 * self.stamina実行して確かめる
教材のZIPを展開したフォルダーで、次のコマンドを実行します。
.venv\Scripts\python.exe 05_result.py.venv/bin/python 05_result.py- 巻き始めは5フレーム距離が変わらない
- 短い解放で釣れ、押しっぱなし・離しっぱなしは負ける
- 結果・停止中は動かず、Rで全状態が戻る
reeled >= 5 にすると、5フレーム目から巻けてしまい、1フレーム早くなります。また、結果の絵を出すだけではゲームは止まりません。step の最初の return と、勝ち負けを決める if / elif の順番も必要です。step(True) を5回呼んでも距離は70のままで、6回目で69.58になることを確かめましょう。1回離してから巻き直すと、また5回待つことも確かめます。勝ちと負けが同時に起きるテストを読んで、なぜ糸が切れるほうが先なのか説明してみましょう。
このステップのコード全文(217行)
# 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.
"""最後の一匹・教材 5/5:巻き始めと結果・再挑戦。
原作を簡略化した独立実行ファイル。実行: python 05_result.py
01〜04はメーター観察の練習、05は小魚1匹を釣るゲームです。
日本語フォント素材は使わず、標準フォントと図形だけで描きます。
"""
WIDTH, HEIGHT = 280, 210
FPS = 30
CRUISE_FRAMES = 36
WARNING_FRAMES = 24
REST_FRAMES = 24
CYCLE_FRAMES = CRUISE_FRAMES + WARNING_FRAMES + REST_FRAMES
TURN_FRAME = CRUISE_FRAMES + WARNING_FRAMES - 1 # 0始まりなので59
MIN_RELEASE = 3
MAX_RELEASE = 18
REEL_DELAY = 5
class Game:
def __init__(self):
self.distance = 70.0
self.held = False
self.reeled = 0
self.released = 0
self.tension = 20.0
self.tick = 0
self.feedback = ""
self.feedback_ticks = 0
self.stamina = 0
self.good_turns = 0
self.status = "play"
self.paused = False
def toggle_pause(self):
self.paused = not self.paused
@property
def local(self):
return self.tick % CYCLE_FRAMES
@property
def phase(self):
if self.local < CRUISE_FRAMES:
return "cruise"
if self.local < CRUISE_FRAMES + WARNING_FRAMES:
return "warning"
return "rest"
@property
def cue(self):
# この教材にはフェイントがない。予兆は全て本物の外向き矢印。
return "out" if self.phase == "warning" else ""
@property
def turn_in(self):
# 次のstepを含む残り回数。予兆開始で24、判定直前で1。
return TURN_FRAME - self.local + 1 if self.phase == "warning" else 0
def step(self, held=False):
if self.paused or self.status != "play":
return
self.held = held
# btnでは長押しを読む。離したフレームで反対のカウンターを0にする。
self.reeled = self.reeled + 1 if held else 0
self.released = 0 if held else self.released + 1
self.feedback_ticks = max(0, self.feedback_ticks - 1)
if held:
# 1〜5フレーム目はたるみ取り。6フレーム目から距離が減る。
if self.reeled > REEL_DELAY:
self.distance -= 0.42 + 0.12 * self.stamina
self.tension += 0.20 if self.phase == "rest" else 0.60
else:
self.distance += 0.45
self.tension = max(0.0, self.tension - 1.30)
if self.local == TURN_FRAME:
# 先に今回の入力を数えたので、releasedには今回も含まれる。
if MIN_RELEASE <= self.released <= MAX_RELEASE:
self.distance -= 24
self.stamina = min(3, self.stamina + 1)
self.good_turns += 1
self.feedback = "GOOD TURN! DISTANCE -24"
else:
self.tension += 65 if held else 16
self.distance += 8
self.feedback = "MISSED TURN!"
self.feedback_ticks = 35
# 同時に到達したら、糸切れ > 逃走 > 釣り上げの順で決める。
if self.tension >= 100:
self.status = "cut"
elif self.distance >= 100:
self.status = "escape"
elif self.distance <= 0:
self.status = "caught"
self.tick += 1
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
if pyxel.btnp(pyxel.KEY_P):
game.toggle_pause()
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 5", 10)
pyxel.text(10, 19, "CATCH ONE FISH. DISTANCE 0 = WIN", 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, 92, f"PHASE {game.phase.upper():7} CYCLE {game.local:02}/{CYCLE_FRAMES}", 6)
if game.cue:
pyxel.line(15, 111, 37, 111, 10)
pyxel.tri(43, 111, 34, 106, 34, 116, 10)
pyxel.text(51, 103, f"REAL TURN IN {game.turn_in:02} FRAMES", 10)
pyxel.text(51, 115, "RELEASE FOR 3-18f AT THE TURN", 7)
elif game.feedback_ticks:
pyxel.text(10, 109, game.feedback, 10)
pyxel.text(10, 178, f"STAMINA {game.stamina}/3 GOOD TURNS {game.good_turns}", 14)
if 0 < game.reeled <= REEL_DELAY:
pyxel.text(147, 35, "TAKING UP SLACK", 10)
if game.status != "play":
pyxel.rect(8, 89, 264, 54, 0)
pyxel.rectb(8, 89, 264, 54, 10)
label = {"caught": "CAUGHT!", "cut": "LINE CUT!", "escape": "FISH ESCAPED!"}[game.status]
pyxel.text(55, 105, label, 10 if game.status == "caught" else 8)
pyxel.text(55, 120, "R: TRY THE SAME FISH AGAIN", 7)
if game.paused:
pyxel.rect(64, 97, 152, 42, 0)
pyxel.rectb(64, 97, 152, 42, 6)
pyxel.text(78, 108, "PAUSED / P: RESUME", 10)
pyxel.text(78, 121, "INPUT COUNTERS STOP TOO", 7)
pyxel.text(10, 196, "SPACE: REEL R: RETRY P: PAUSE 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 5", 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)仕組みがわかったら、元の完成版を読んでみよう。
元のゲームでは、全部で5匹の魚を釣ります。張力が100で糸が切れ、距離が100で逃げられ、0で釣り上げです。本物の反転のときに3〜18フレーム続けて緩めると、距離が-24、疲れが+1になります。巻き始めの5フレームは距離が減りません。フェイント(見せかけの前ぶれ)のときは巻きます。連続ボーナスを確定するには、次の1匹分の枠を使います。
ここまでに作ったのは、元のゲームの仕組みだけを取り出して作り直した「簡略版」です。元のゲームのソースは手を加えずに、別のZIP(元の完成版ソース)に入れてあります。
| 比べる点 | 5ステップの教材 | 元の完成版 |
|---|---|---|
| 変更点 1 | 最後の段階は small・variant=0 の小さな魚1匹だけ。5匹分の魚の群れ・魚の種類・seed は省いています | |
| 変更点 2 | 前ぶれはすべて本物。フェイントの見分け方は、元のゲームを読む発展課題です | |
| 変更点 3 | 連続ボーナス・確定・魚を見送る選択は省いています | |
| 変更点 4 | 01〜04は0〜100のメーターの練習で、勝ち負けと巻き始めの待ち時間はありません。05で勝ち負けと5フレームの待ちを足します | |
| 変更点 5 | 元のゲームの日本語フォント・タイトル画面・記録・録画は省いています。教材は英字と図形だけです | |
| 変更点 6 | Escで終了。Pの一時停止とRのやり直しは05で作ります | |
完成版のコードで、次に読むところ
model.pyのFish.phase・Fish.event・Fish.cue(魚の動きの段階と前ぶれ)model.pyのFish.step(魚の1フレーム分の更新)model.pyのVoyage.start・Voyage.step・Voyage.bank・Voyage.advance(釣り全体の流れ)app.pyのApp.update・App.commit(入力と画面の更新)tests/test_model.pyのRules(ルールのテスト)
最初は、数字かルールを1つだけ変えてみましょう。遊ぶ前に「どう変わるか」を予想しておくと、仕組みがよくわかります。慣れてきたら、見た目や音も足してみてください。元のファイルは残しておき、コピーした別のフォルダーで改造すると、いつでも元に戻せます。
教材をダウンロードする
無料で、登録なしでダウンロードできます。ZIPファイルは必ず展開してから、中のファイルを実行してください。元の完成版は、同じZIPに入っている assets フォルダー(画像などの素材)も一緒に使います。
コードと図形はMITライセンス、元の完成版に含まれる日本語の文字データ(Noto由来)はSIL OFL 1.1です。配ったり改造したりするときは、ZIPに入っているLICENSEなどの権利表示を残してください。出典・権利について
検証について
どこまで動作を確認したかを見る
2026年10月5日以降、Linux(Python 3.12・Pyxel 2.9.9)で、教材のコードとゲームのルールを自動テストしています。全500ステップの教材が、実際のPyxelで起動して画面を描けることも確かめました。元の完成版は、ルールのテストで確認しています。
この確認は、画面を表示しない自動テストで行ったものです。Windows・Macの実際のパソコンでの操作や、音が出るかどうかはまだ確認していません。人がすべての操作を試したわけでもありません。ブラウザー版の確認範囲は確認状況をご覧ください。
うまく動かないときは:起動やエラーの対処法 / 質問用のメモを作る
