完成サンプルをブラウザーで遊ぶ
この講座のもとになった完成版のゲームを、ブラウザーでそのまま遊べます。キーボードで操作するので、PCで開いてください(スマホのタッチ操作には対応していません)。初めて開くときは、読み込みに少し時間がかかります。
完成版の主な操作:Spaceで1刻進める(Pで自動)、矢印キーで変える場所を選ぶ、Zで詳しく見る、Enterで変更して再実行、Xで1手戻す、Rで何も変えずに確認
ボタンを押すと、ゲームの読み込みが始まります。
画面に「CLICK TO START」と出たらクリックします。続けてゲーム画面を一度クリックすると、キー操作が効くようになります。ゲームを終えるときは、枠の外にある「閉じる」ボタンを使ってください(Escキーの働きはゲームごとに違います)。ゲーム内のセーブは、閉じる・ページを移る・再読み込みすると消えます。
うまく表示されないとき
ゲームを動かすためのプログラム(Pyxel 2.9.9)は、外部の配信サービス(jsDelivr)から読み込みます。表示されないときは「最初から再読込」を押すか、完成版のソースをダウンロードしてPCで実行してください。ブラウザー版の録画機能は止めています。ブラウザー版で確認できている範囲は検証状況にまとめています。
変えるところを1つにしぼって比べる。
いきなり完成版のコードを全部読む必要はありません。小さなゲームにルールを1つずつ足しながら、5つのステップで作っていきます。どのステップのファイルもそれだけで動くので、動きを確かめてから次へ進みましょう。
- 必要な環境
- Python 3.11以上 / Pyxel 2.9.9
- 知っておくとよいこと
- 変数・if・関数の基本
- 教材に入っているもの
- ステップごとに動くPythonファイル5本・説明・動作確認用のテスト
- 完成した教材の操作
- Space:1刻進める / 左右:変える場所を選ぶ / Enter:変えて再実行 / X:1手戻す / R:最初に戻す
- 難易度の目安
- 初級このページで使うプログラムの考え方の数と難しさから決めた目安です。難易度について
- 学習順に迷ったら
- おすすめの学ぶ順番を見る
コードを書き換えたら、ファイルを保存し、動いているゲームを閉じてから、もう一度同じ.pyを実行します。ゲーム中にRキーなどで「やり直し」をしても、書き換えたコードは読み込まれません。
教材のゲーム画面は、日本語フォントを用意しなくても動くように英語で表示しています。ファイルには動作確認用のコードも入っていますが、最初は Game / State・update・draw の3つに注目すれば十分です。わからない言葉は用語集で調べられます。class・self・dataclass はPythonの状態、キー入力と時間の扱いは入力・更新・描画、マス目とピクセルの違いは画面とマップの座標のページで詳しく説明しています。
水を足してから、品を作る
決まった順番で進む「1刻」の処理。
State は、水の量・完成した品の数・刻(経過した回数)を持つ、書き換えられないデータです。Space を押すたびに、まずポンプで水を1足し、上限の4を超えないようにします。そのあと、水が2あればそれを使って品を1つ作ります。水が足りなければ、水を減らさずに待ちます。時間は実際の時計ではなく、操作した回数で進みます。
下のコードは 01_water.py の一部です。前のステップから追加・変更したところだけを抜き出しています。
supplied = min(capacity, state.water + pump)
produced = int(supplied >= cost)
water = supplied - cost if produced else supplied実行して確かめる
教材のZIPを展開したフォルダーで、次のコマンドを実行します。
.venv\Scripts\python.exe 01_water.py.venv/bin/python 01_water.py- 水を足してから、品を作る
- 水が足りなければ使わない
- R キーで同じ最初の状態に戻る
水0から始めたとき、最初の1刻で品を作れるか予想してみましょう。次に、最初の水の量だけを1に変えて比べてみましょう。
このステップのコード全文(89行)
# MIT License
#
# Copyright (c) 2026
#
# 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.
#
# Third-party exception: bitmap glyph data derived from Noto Sans CJK is licensed
# under the SIL Open Font License 1.1. See assets/NOTO-LICENSE.txt.
"""未来からの修理票・教材1/5。架空の工場Aの仕組みを段階化。
実行: python 01_water.py
"""
from dataclasses import dataclass
@dataclass(frozen=True)
class State:
water: int = 2
output: int = 0
tick: int = 0
def step(state):
pump, capacity, cost = 1, 4, 2
# 補給を先に行う。同じ刻の補給を、その刻の生産で使える。
supplied = min(capacity, state.water + pump)
produced = int(supplied >= cost)
water = supplied - cost if produced else supplied
return State(water=water, output=state.output + produced, tick=state.tick + 1)
def main(headless=False, screenshot=None):
import pyxel
state = State()
def update():
nonlocal state
if headless and pyxel.frame_count >= 12:
if screenshot:
pyxel.screenshot(screenshot, scale=2)
pyxel.quit()
return
if pyxel.btnp(pyxel.KEY_R):
state = State()
return
current = state
if pyxel.btnp(pyxel.KEY_SPACE):
state = step(state)
def draw():
current = state
pyxel.cls(1)
pyxel.text(10, 10, "FUTURE REPAIR / STEP 1", 10)
pyxel.text(10, 25, f"TICK {current.tick} WATER {current.water} OUTPUT {current.output}", 7)
pyxel.rect(10, 44, 130, 14, 5)
pyxel.rect(10, 44, min(current.water, 6) * 20, 14, 12)
pyxel.text(155, 48, "WATER", 6)
pyxel.text(10, 66, "PUMP +1 CAP 4 COST 2", 11)
pyxel.text(10, 93, "FILL FIRST, THEN TRY TO MAKE ONE.", 6)
pyxel.text(10, 107, "NOT ENOUGH WATER = WAIT", 6)
pyxel.text(10, 121, "WAIT DOES NOT USE WATER", 6)
pyxel.text(10, 248, "SPACE: ONE TICK R: RESET ESC: QUIT", 6)
pyxel.init(310, 280, title="Future Repair - Stage 1", fps=30, 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="12 frames then quit; not a playtest")
parser.add_argument("--screenshot", help="PNG path used with --headless-smoke")
args = parser.parse_args()
main(headless=args.headless_smoke, screenshot=args.screenshot)六刻で結果を止める
「終わる条件」と「成功の条件」を分ける。
1回の試運転は6刻です。done は6刻まで進んだか、success は終わったときに目標の5個以上できたかを表します。何も変えないと、4個で終わります。この段階ではまだ変更はできず、動かして問題を観察する練習です。
下のコードは 02_six_ticks.py の一部です。前のステップから追加・変更したところだけを抜き出しています。
def done(self):
return self.tick >= TICKS実行して確かめる
教材のZIPを展開したフォルダーで、次のコマンドを実行します。
.venv\Scripts\python.exe 02_six_ticks.py.venv/bin/python 02_six_ticks.py- 6刻で止まる
- 何も変えないと4個
- 失敗してもやり直せる
TARGET だけを4に変えて、機械が作る数は変わらず、結果の評価だけが変わることを確かめましょう。確かめたら元に戻して、次へ進んでください。
このステップのコード全文(106行)
# MIT License
#
# Copyright (c) 2026
#
# 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.
#
# Third-party exception: bitmap glyph data derived from Noto Sans CJK is licensed
# under the SIL Open Font License 1.1. See assets/NOTO-LICENSE.txt.
"""未来からの修理票・教材2/5。架空の工場Aの仕組みを段階化。
実行: python 02_six_ticks.py
"""
from dataclasses import dataclass
TICKS = 6
TARGET = 5
@dataclass(frozen=True)
class State:
water: int = 2
output: int = 0
tick: int = 0
@property
def done(self):
return self.tick >= TICKS
@property
def success(self):
return self.done and self.output >= TARGET
def step(state):
if state.done:
return state
pump, capacity, cost = 1, 4, 2
# 補給を先に行う。同じ刻の補給を、その刻の生産で使える。
supplied = min(capacity, state.water + pump)
produced = int(supplied >= cost)
water = supplied - cost if produced else supplied
return State(water=water, output=state.output + produced, tick=state.tick + 1)
def main(headless=False, screenshot=None):
import pyxel
state = State()
def update():
nonlocal state
if headless and pyxel.frame_count >= 12:
if screenshot:
pyxel.screenshot(screenshot, scale=2)
pyxel.quit()
return
if pyxel.btnp(pyxel.KEY_R):
state = State()
return
current = state
if pyxel.btnp(pyxel.KEY_SPACE):
state = step(state)
def draw():
current = state
pyxel.cls(1)
pyxel.text(10, 10, "FUTURE REPAIR / STEP 2", 10)
pyxel.text(10, 25, f"TICK {current.tick} WATER {current.water} OUTPUT {current.output}", 7)
pyxel.rect(10, 44, 130, 14, 5)
pyxel.rect(10, 44, min(current.water, 6) * 20, 14, 12)
pyxel.text(155, 48, "WATER", 6)
pyxel.text(10, 66, "PUMP +1 CAP 4 COST 2", 11)
pyxel.text(10, 93, "FILL FIRST, THEN TRY TO MAKE ONE.", 6)
pyxel.text(10, 107, "NOT ENOUGH WATER = WAIT", 6)
pyxel.text(10, 121, "WAIT DOES NOT USE WATER", 6)
if current.done:
pyxel.text(10, 181, "GOAL!" if current.success else "OUTPUT BELOW TARGET", 10)
pyxel.text(10, 193, f"RESULT {current.output} / TARGET {TARGET}", 7)
pyxel.text(10, 209, "R: OBSERVE THE SAME RUN AGAIN", 6)
pyxel.text(10, 248, "SPACE: ONE TICK R: RESET ESC: QUIT", 6)
pyxel.init(310, 280, title="Future Repair - Stage 2", fps=30, 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="12 frames then quit; not a playtest")
parser.add_argument("--screenshot", help="PNG path used with --headless-smoke")
args = parser.parse_args()
main(headless=args.headless_smoke, screenshot=args.screenshot)いつ止まったかを残す
状態の移り変わりを、時間順の記録(ログ)に残す。
それぞれの刻について、「始めの水」「足したあとの水」「使ったあとの水」「品を作ったか」を1行(entry)にまとめます。timeline は、タプルを1つ追加した新しい値として保存します。完成した数だけでは見えない、「3刻目と5刻目は待っていた」ということが、表で確かめられます。
下のコードは 03_timeline.py の一部です。前のステップから追加・変更したところだけを抜き出しています。
entry = (state.tick + 1, state.water, supplied, water, produced)実行して確かめる
教材のZIPを展開したフォルダーで、次のコマンドを実行します。
.venv\Scripts\python.exe 03_timeline.py.venv/bin/python 03_timeline.py- 6刻分の行を順番に読める
- 待っていた刻がわかる
- 記録してもルールは変わらない
3刻目に品を作れない原因を、「足す水の量」「タンクの大きさ」「使う水の量」のどれかと結びつけて説明してみましょう。まだ2つを同時には変えません。
このステップのコード全文(110行)
# MIT License
#
# Copyright (c) 2026
#
# 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.
#
# Third-party exception: bitmap glyph data derived from Noto Sans CJK is licensed
# under the SIL Open Font License 1.1. See assets/NOTO-LICENSE.txt.
"""未来からの修理票・教材3/5。架空の工場Aの仕組みを段階化。
実行: python 03_timeline.py
"""
from dataclasses import dataclass
TICKS = 6
TARGET = 5
@dataclass(frozen=True)
class State:
water: int = 2
output: int = 0
tick: int = 0
timeline: tuple = ()
@property
def done(self):
return self.tick >= TICKS
@property
def success(self):
return self.done and self.output >= TARGET
def step(state):
if state.done:
return state
pump, capacity, cost = 1, 4, 2
# 補給を先に行う。同じ刻の補給を、その刻の生産で使える。
supplied = min(capacity, state.water + pump)
produced = int(supplied >= cost)
water = supplied - cost if produced else supplied
entry = (state.tick + 1, state.water, supplied, water, produced)
return State(water=water, output=state.output + produced, tick=state.tick + 1,
timeline=state.timeline + (entry,))
def main(headless=False, screenshot=None):
import pyxel
state = State()
def update():
nonlocal state
if headless and pyxel.frame_count >= 12:
if screenshot:
pyxel.screenshot(screenshot, scale=2)
pyxel.quit()
return
if pyxel.btnp(pyxel.KEY_R):
state = State()
return
current = state
if pyxel.btnp(pyxel.KEY_SPACE):
state = step(state)
def draw():
current = state
pyxel.cls(1)
pyxel.text(10, 10, "FUTURE REPAIR / STEP 3", 10)
pyxel.text(10, 25, f"TICK {current.tick} WATER {current.water} OUTPUT {current.output}", 7)
pyxel.rect(10, 44, 130, 14, 5)
pyxel.rect(10, 44, min(current.water, 6) * 20, 14, 12)
pyxel.text(155, 48, "WATER", 6)
pyxel.text(10, 66, "PUMP +1 CAP 4 COST 2", 11)
pyxel.text(10, 86, "TICK BEFORE FILLED LEFT MADE", 6)
for row, (tick, before, filled, left, made) in enumerate(current.timeline):
pyxel.text(10, 100 + row * 12,
f"{tick:4} {before:6} {filled:6} {left:4} {made:4}", 7 if made else 8)
if current.done:
pyxel.text(10, 181, "GOAL!" if current.success else "OUTPUT BELOW TARGET", 10)
pyxel.text(10, 193, f"RESULT {current.output} / TARGET {TARGET}", 7)
pyxel.text(10, 209, "R: OBSERVE THE SAME RUN AGAIN", 6)
pyxel.text(10, 248, "SPACE: ONE TICK R: RESET ESC: QUIT", 6)
pyxel.init(310, 280, title="Future Repair - Stage 3", fps=30, 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="12 frames then quit; not a playtest")
parser.add_argument("--screenshot", help="PNG path used with --headless-smoke")
args = parser.parse_args()
main(headless=args.headless_smoke, screenshot=args.screenshot)一か所だけ変えて再実行
設定を足し合わせず、1つ選んで置きかえる。
まず、何も変えずに6刻観察します。結果が出たら左右キーで変える場所の候補を選び、Enter で刻0からやり直します。settings は、足す水の量・タンクの大きさ・使う水の量・最初の水の4つの値を返し、どの候補もそのうち1つだけを変えます。前に変えたところを残したまま、さらに変更を積み重ねることはしません。
下のコードは 04_one_change.py の一部です。前のステップから追加・変更したところだけを抜き出しています。
return (2 if choice == 1 else 1, 6 if choice == 2 else 4,
1 if choice == 3 else 2, 4 if choice == 4 else 2)実行して確かめる
教材のZIPを展開したフォルダーで、次のコマンドを実行します。
.venv\Scripts\python.exe 04_one_change.py.venv/bin/python 04_one_change.py- 結果が出たあとだけ候補を選べる
- 一度に変わるのは1つの値だけ
- 同じ6刻で比べられる
new(choice) で、毎回元の条件から作り直します。「タンクを大きくすれば、たくさん作れる」という予想を、PUMP 2 と CAPACITY 6 のログで比べてみましょう。2つを同時に選べないようにしている理由も説明してみましょう。
このステップのコード全文(137行)
# MIT License
#
# Copyright (c) 2026
#
# 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.
#
# Third-party exception: bitmap glyph data derived from Noto Sans CJK is licensed
# under the SIL Open Font License 1.1. See assets/NOTO-LICENSE.txt.
"""未来からの修理票・教材4/5。架空の工場Aの仕組みを段階化。
実行: python 04_one_change.py
"""
from dataclasses import dataclass
TICKS = 6
TARGET = 5
CHOICES = ("NO CHANGE", "PUMP 2", "CAPACITY 6", "COST 1", "INITIAL WATER 4")
def settings(choice):
if choice not in range(5):
raise ValueError("choice must be 0..4")
return (2 if choice == 1 else 1, 6 if choice == 2 else 4,
1 if choice == 3 else 2, 4 if choice == 4 else 2)
@dataclass(frozen=True)
class State:
water: int = 2
output: int = 0
tick: int = 0
timeline: tuple = ()
choice: int = 0
@property
def done(self):
return self.tick >= TICKS
@property
def success(self):
return self.done and self.output >= TARGET
def new(choice=0):
pump, capacity, cost, initial_water = settings(choice)
return State(water=initial_water, choice=choice)
def step(state):
if state.done:
return state
pump, capacity, cost, initial_water = settings(state.choice)
# 補給を先に行う。同じ刻の補給を、その刻の生産で使える。
supplied = min(capacity, state.water + pump)
produced = int(supplied >= cost)
water = supplied - cost if produced else supplied
entry = (state.tick + 1, state.water, supplied, water, produced)
return State(water=water, output=state.output + produced, tick=state.tick + 1,
timeline=state.timeline + (entry,), choice=state.choice)
def main(headless=False, screenshot=None):
import pyxel
state = new()
selected = 0
def update():
nonlocal state, selected
if headless and pyxel.frame_count >= 12:
if screenshot:
pyxel.screenshot(screenshot, scale=2)
pyxel.quit()
return
if pyxel.btnp(pyxel.KEY_R):
state = new()
selected = 0
return
current = state
if current.done:
if pyxel.btnp(pyxel.KEY_LEFT):
selected = (selected - 1) % len(CHOICES)
elif pyxel.btnp(pyxel.KEY_RIGHT):
selected = (selected + 1) % len(CHOICES)
elif pyxel.btnp(pyxel.KEY_RETURN):
state = new(selected)
return
if pyxel.btnp(pyxel.KEY_SPACE):
state = step(state)
def draw():
current = state
pyxel.cls(1)
pyxel.text(10, 10, "FUTURE REPAIR / STEP 4", 10)
pyxel.text(10, 25, f"TICK {current.tick} WATER {current.water} OUTPUT {current.output}", 7)
pyxel.rect(10, 44, 130, 14, 5)
pyxel.rect(10, 44, min(current.water, 6) * 20, 14, 12)
pyxel.text(155, 48, "WATER", 6)
pyxel.text(10, 66, "ACTIVE: " + CHOICES[current.choice], 11)
pyxel.text(10, 86, "TICK BEFORE FILLED LEFT MADE", 6)
for row, (tick, before, filled, left, made) in enumerate(current.timeline):
pyxel.text(10, 100 + row * 12,
f"{tick:4} {before:6} {filled:6} {left:4} {made:4}", 7 if made else 8)
if current.done:
pyxel.text(10, 181, "GOAL!" if current.success else "OUTPUT BELOW TARGET", 10)
pyxel.text(10, 193, f"RESULT {current.output} / TARGET {TARGET}", 7)
pyxel.text(10, 209, "TRY: " + CHOICES[selected], 11)
pyxel.text(10, 222, "LEFT/RIGHT: CHOOSE ENTER: RESTART", 6)
pyxel.text(10, 248, "SPACE: ONE TICK R: RESET ESC: QUIT", 6)
pyxel.init(310, 280, title="Future Repair - Stage 4", fps=30, 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="12 frames then quit; not a playtest")
parser.add_argument("--screenshot", help="PNG path used with --headless-smoke")
args = parser.parse_args()
main(headless=args.headless_smoke, screenshot=args.screenshot)一刻と一回の変更を戻す
移り変わってきた状態を、履歴に保存する。
Session は、進める前の State を history に追加します。X キーで、刻・水の量・完成した数・設定・ログがまとめて戻ります。変更してやり直した直後の刻0で X を押すと、直前の試運転の結果に戻れるので、別の候補を選び直せます。R キーは何も変えない最初の状態に戻し、履歴も消します。
下のコードは 05_undo.py の一部です。前のステップから追加・変更したところだけを抜き出しています。
def undo(self):
if self.history:
self.state = self.history.pop()実行して確かめる
教材のZIPを展開したフォルダーで、次のコマンドを実行します。
.venv\Scripts\python.exe 05_undo.py.venv/bin/python 05_undo.py- 1刻前に戻る
- 変更した直後なら、前の結果に戻る
- R キーで履歴も消える
State をまるごと戻します。PUMP 2 で成功したあと、X でその運転の途中に戻り、同じ操作をくり返して、結果が同じになるか確かめましょう。
このステップのコード全文(167行)
# MIT License
#
# Copyright (c) 2026
#
# 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.
#
# Third-party exception: bitmap glyph data derived from Noto Sans CJK is licensed
# under the SIL Open Font License 1.1. See assets/NOTO-LICENSE.txt.
"""未来からの修理票・教材5/5。架空の工場Aの仕組みを段階化。
実行: python 05_undo.py
"""
from dataclasses import dataclass
TICKS = 6
TARGET = 5
CHOICES = ("NO CHANGE", "PUMP 2", "CAPACITY 6", "COST 1", "INITIAL WATER 4")
def settings(choice):
if choice not in range(5):
raise ValueError("choice must be 0..4")
return (2 if choice == 1 else 1, 6 if choice == 2 else 4,
1 if choice == 3 else 2, 4 if choice == 4 else 2)
@dataclass(frozen=True)
class State:
water: int = 2
output: int = 0
tick: int = 0
timeline: tuple = ()
choice: int = 0
@property
def done(self):
return self.tick >= TICKS
@property
def success(self):
return self.done and self.output >= TARGET
def new(choice=0):
pump, capacity, cost, initial_water = settings(choice)
return State(water=initial_water, choice=choice)
def step(state):
if state.done:
return state
pump, capacity, cost, initial_water = settings(state.choice)
# 補給を先に行う。同じ刻の補給を、その刻の生産で使える。
supplied = min(capacity, state.water + pump)
produced = int(supplied >= cost)
water = supplied - cost if produced else supplied
entry = (state.tick + 1, state.water, supplied, water, produced)
return State(water=water, output=state.output + produced, tick=state.tick + 1,
timeline=state.timeline + (entry,), choice=state.choice)
class Session:
def __init__(self):
self.state = new()
self.history = []
def advance(self):
result = step(self.state)
if result != self.state:
self.history.append(self.state)
self.state = result
def change(self, choice):
if self.state.done and choice in range(5):
self.history.append(self.state)
self.state = new(choice) # 介入を積み増さず、毎回ひとつへ置き換える。
def undo(self):
if self.history:
self.state = self.history.pop()
def restart(self):
self.state = new()
self.history.clear()
def main(headless=False, screenshot=None):
import pyxel
session = Session()
selected = 0
def update():
nonlocal selected
if headless and pyxel.frame_count >= 12:
if screenshot:
pyxel.screenshot(screenshot, scale=2)
pyxel.quit()
return
if pyxel.btnp(pyxel.KEY_R):
session.restart()
selected = 0
return
if pyxel.btnp(pyxel.KEY_X):
session.undo()
selected = session.state.choice
return
current = session.state
if current.done:
if pyxel.btnp(pyxel.KEY_LEFT):
selected = (selected - 1) % len(CHOICES)
elif pyxel.btnp(pyxel.KEY_RIGHT):
selected = (selected + 1) % len(CHOICES)
elif pyxel.btnp(pyxel.KEY_RETURN):
session.change(selected)
return
if pyxel.btnp(pyxel.KEY_SPACE):
session.advance()
def draw():
current = session.state
pyxel.cls(1)
pyxel.text(10, 10, "FUTURE REPAIR / STEP 5", 10)
pyxel.text(10, 25, f"TICK {current.tick} WATER {current.water} OUTPUT {current.output}", 7)
pyxel.rect(10, 44, 130, 14, 5)
pyxel.rect(10, 44, min(current.water, 6) * 20, 14, 12)
pyxel.text(155, 48, "WATER", 6)
pyxel.text(10, 66, "ACTIVE: " + CHOICES[current.choice], 11)
pyxel.text(10, 86, "TICK BEFORE FILLED LEFT MADE", 6)
for row, (tick, before, filled, left, made) in enumerate(current.timeline):
pyxel.text(10, 100 + row * 12,
f"{tick:4} {before:6} {filled:6} {left:4} {made:4}", 7 if made else 8)
if current.done:
pyxel.text(10, 181, "GOAL!" if current.success else "OUTPUT BELOW TARGET", 10)
pyxel.text(10, 193, f"RESULT {current.output} / TARGET {TARGET}", 7)
pyxel.text(10, 209, "TRY: " + CHOICES[selected], 11)
pyxel.text(10, 222, "LEFT/RIGHT: CHOOSE ENTER: RESTART", 6)
pyxel.text(10, 248, "SPACE: ONE TICK R: RESET ESC: QUIT", 6)
pyxel.text(10, 260, "X: UNDO A TICK / RETURN TO PRIOR RUN", 6)
pyxel.init(310, 280, title="Future Repair - Stage 5", fps=30, 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="12 frames then quit; not a playtest")
parser.add_argument("--screenshot", help="PNG path used with --headless-smoke")
args = parser.parse_args()
main(headless=args.headless_smoke, screenshot=args.screenshot)仕組みがわかったら、元の完成版を読んでみよう。
元の作品では、A・B・C の3つの事例を6刻ずつ観察し、1か所だけを変えて同じ未来をもう一度動かします。A は水と生産、B は電力の取り合い、C は運ぶ速さと置ける量がテーマです。条件を満たして終われば、どの結末も同じ「基本の成功」として扱います。
ここまでに作ったのは、元のゲームの仕組みだけを取り出して作り直した「簡略版」です。元のゲームのソースは手を加えずに、別のZIP(元の完成版ソース)に入れてあります。
| 比べる点 | 5ステップの教材 | 元の完成版 |
|---|---|---|
| 事例 | 水のタンクと品作りのAだけ | A・B・C、それぞれ4台の機械と5つの候補 |
| 比べ方 | 1か所の変更・6刻・ログ・undo | 同じルール+修理票・比較の表示・記録 |
| 操作と表示 | 手動で1刻ずつ・英語・図形 | 日本語・自動運転・説明と確認の画面 |
完成版のコードで、次に読むところ
model.pyのnewとstepで、A の5つの設定を教材と見比べましょう。stepの B で、「充電→部品→運ぶ」の順番と、取り合いになる資源を読みましょう。stepの C で、戻ってきた品を同じ刻にもう一度流さないようにする処理を読みましょう。controller.pyで、1刻分の undo と、変更を選び直すための履歴を読みましょう。
最初は、数字かルールを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の実際のパソコンでの操作や、音が出るかどうかはまだ確認していません。人がすべての操作を試したわけでもありません。ブラウザー版の確認範囲は確認状況をご覧ください。
うまく動かないときは:起動やエラーの対処法 / 質問用のメモを作る
