完成サンプルをブラウザーで遊ぶ
この講座のもとになった完成版のゲームを、ブラウザーでそのまま遊べます。キーボードで操作するので、PCで開いてください(スマホのタッチ操作には対応していません)。初めて開くときは、読み込みに少し時間がかかります。
完成版の主な操作:矢印キーで鉢を選ぶ、Zで植える・収穫する、Cで納品、Spaceで夕方へ進める・締め切る、Xで1手戻す、Rでやり直し、Pで一時停止
ボタンを押すと、ゲームの読み込みが始まります。
画面に「CLICK TO START」と出たらクリックします。続けてゲーム画面を一度クリックすると、キー操作が効くようになります。ゲームを終えるときは、枠の外にある「閉じる」ボタンを使ってください(Escキーの働きはゲームごとに違います)。ゲーム内のセーブは、閉じる・ページを移る・再読み込みすると消えます。
うまく表示されないとき
ゲームを動かすためのプログラム(Pyxel 2.9.9)は、外部の配信サービス(jsDelivr)から読み込みます。表示されないときは「最初から再読込」を押すか、完成版のソースをダウンロードしてPCで実行してください。ブラウザー版の録画機能は止めています。ブラウザー版で確認できている範囲は検証状況にまとめています。
朝の状態から、夕方の状態をまとめて求める。
いきなり完成版のコードを全部読む必要はありません。小さなゲームにルールを1つずつ足しながら、5つのステップで作っていきます。どのステップのファイルもそれだけで動くので、動きを確かめてから次へ進みましょう。
- 必要な環境
- Python 3.11以上 / Pyxel 2.9.9
- 知っておくとよいこと
- 変数・if・for・関数 / タプルとリスト / 割り算の余り・集合・dataclass は本文で説明します
- 教材に入っているもの
- ステップごとに動くPythonファイル5本・説明・動作確認用のテスト
- 完成した教材の操作
- 矢印:鉢を選ぶ / Z:手入れ / Space:夕方へ進む・最後の朝を締め切る / C:食材を全部納品 / X:05の Undo / R:最初に戻す / Esc:終了
- 難易度の目安
- 初級このページで使うプログラムの考え方の数と難しさから決めた目安です。難易度について
- 学習順に迷ったら
- おすすめの学ぶ順番を見る
コードを書き換えたら、ファイルを保存し、動いているゲームを閉じてから、もう一度同じ.pyを実行します。ゲーム中にRキーなどで「やり直し」をしても、書き換えたコードは読み込まれません。
教材のゲーム画面は、日本語フォントを用意しなくても動くように英語で表示しています。ファイルには動作確認用のコードも入っていますが、最初は Game / State・update・draw の3つに注目すれば十分です。わからない言葉は用語集で調べられます。class・self・dataclass はPythonの状態、キー入力と時間の扱いは入力・更新・描画、マス目とピクセルの違いは画面とマップの座標のページで詳しく説明しています。
朝から夕方の成長を作る
古い並びから、新しい並びを作る。
まず、6つの鉢と4回の夕方を作ります。値は、-1が空、0が苗、1が若葉、2が大きな葉、3が種のさやです。最初は1番が2、2番が0、ほかは空です。Space を1回押すと夕方の成長が確定し、次の朝になります。考えている間に日付が進むことはありません。
State は、day(日付)と pots(鉢)をまとめた、書き換えられない箱です。pots はタプルで、forecast が新しいタプルを返します。0〜2の株だけを1増やし、空の-1と、もう完成した3は変えません。replace で新しい State を作るので、朝の値はそのまま残ります。
画面の「2 -> 3」は、「今は2で、次の夕方に3になる」という予告です。画面を描くたびに育てるという意味ではありません。draw は読むだけにして、確定は step の W に分けておけば、何回画面を描いても時間は進みません。
ここでは、影と収穫を省いた練習です。4回の夕方で止まりますが、勝ち負けはまだありません。
下のコードは 01_growth.py の一部です。前のステップから追加・変更したところだけを抜き出しています。
return tuple(value+1 if 0 <= value < 3 else value
for i, value in enumerate(state.pots))実行して確かめる
教材のZIPを展開したフォルダーで、次のコマンドを実行します。
.venv\Scripts\python.exe 01_growth.py.venv/bin/python 01_growth.py- 空の鉢は育たない
- 3で止まる
- 予告では状態が変わらない
draw で pots を書き換えると、画面を描いた回数だけ育ってしまいます。予告は読むだけにして、確定は W のときだけ行います。WEATHER の最初の W を C に変えて、起動し直してみましょう。最初の Space で朝1になりますが、くもり(C)なので株は2・0のままです。確かめたら W に戻します。
このステップのコード全文(158行)
# 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
START_POTS = (2, 0, -1, -1, -1, -1)
WEATHER = ("W", "W", "W", "W")
@dataclass(frozen=True)
class State:
day: int = 0
pots: tuple = START_POTS
def forecast(state):
if state.day >= len(WEATHER) or WEATHER[state.day] == "C":
return state.pots
return tuple(value+1 if 0 <= value < 3 else value
for i, value in enumerate(state.pots))
def step(state, action):
if action == "W":
if state.day >= len(WEATHER):
return state
return replace(state, day=state.day+1, pots=forecast(state))
return state
class App:
def __init__(self):
self.reset()
def reset(self):
self.state = State()
self.selected = 0
self.locked = False
self.message = "SPACE: EVENING GROWTH"
def control(self, pressed, held):
if self.locked:
if not held:
self.locked = False
return
if "R" in pressed:
self.reset()
self.locked = True
return
if len(pressed) != 1:
return
key = next(iter(pressed))
x, y = self.selected%3, self.selected//3
if key in ("LEFT", "RIGHT", "UP", "DOWN"):
x = max(0, min(2, x+(key=="RIGHT")-(key=="LEFT")))
y = max(0, min(1, y+(key=="DOWN")-(key=="UP")))
self.selected = y*3+x
self.locked = True
return
action = "W" if key == "SPACE" else None
if action is None:
return
before = self.state
self.state = step(self.state, action)
self.message = ("EVENING / NEXT MORNING" if action=="W" else "CARE / SAME MORNING") if self.state!=before else "NO CHANGE"
self.locked = True
def draw(self, p):
p.cls(1)
p.text(8, 8, "ROOFTOP GARDEN / STAGE 1", 10)
weather = WEATHER[self.state.day] if self.state.day < len(WEATHER) else "END"
p.text(8, 22, "MORNING "+str(self.state.day)+" / "+str(len(WEATHER))+" SUN "+weather, 7)
shade = frozenset()
following = forecast(self.state)
for i, value in enumerate(self.state.pots):
x, y = 12+(i%3)*100, 64+(i//3)*58
p.rectb(x, y, 92, 53, 10 if i==self.selected else 5)
p.text(x+4, y+4, "POT "+str(i+1), 7)
if i in shade:
p.text(x+48, y+4, "SHADE", 6)
p.rect(x+30, y+33, 24, 12, 4)
if value >= 0:
p.line(x+42, y+32, x+42, y+24-3*value, 3)
if value == 0:
p.circ(x+42, y+30, 2, 10)
elif value == 1:
p.circ(x+37, y+27, 3, 11)
elif value == 2:
p.circ(x+35, y+24, 6, 3)
p.circ(x+49, y+21, 6, 11)
else:
p.circ(x+36, y+19, 4, 9)
p.circ(x+47, y+22, 4, 10)
p.text(x+4, y+42, str(value)+" -> "+str(following[i]), 7)
p.text(8, 188, "-1 EMPTY 0 SEED 1 YOUNG 2 LEAF 3 POD", 6)
p.text(8, 200, "ARROW IS EVENING FORECAST, NOT EXTRA GROWTH", 6)
p.text(8, 216, self.message, 10)
p.text(8, 230, 'SPACE GROW', 7)
p.text(8, 241, "R RESET ESC QUIT", 6)
DEMO_KEYS = ('SPACE',)
def main(headless=False, screenshot=None, demo=False):
from pathlib import Path
import pyxel as p
screenshot = str(Path(screenshot).resolve()) if screenshot else None
if screenshot:
Path(screenshot).parent.mkdir(parents=True, exist_ok=True)
app = App()
p.init(320, 256, title="Rooftop Garden", fps=30, display_scale=3, headless=headless)
keys = {k: getattr(p, "KEY_"+k) for k in ("UP", "DOWN", "LEFT", "RIGHT", "SPACE", "Z", "C", "X", "R")}
limit = max(12, 2*len(DEMO_KEYS)+3) if demo else 12
def update():
if headless and p.frame_count >= limit:
if screenshot:
p.screenshot(screenshot, scale=3)
p.quit()
return
if headless and demo:
index = p.frame_count//2
pressed = {DEMO_KEYS[index]} if p.frame_count%2==0 and index<len(DEMO_KEYS) 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))
return app
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--headless-smoke", action="store_true")
parser.add_argument("--demo-checkpoint", action="store_true")
parser.add_argument("--screenshot")
args = parser.parse_args()
main(args.headless_smoke, args.screenshot, args.demo_checkpoint)大きな葉の影を加える
影を一度だけ計算して、いっせいに育てる。
大きな葉(2)と種のさや(3)は、同じ行のとなりの鉢に影を落とします。W は西からの光なので東どなりに、E なら西どなりに影ができます。くもりの C では、成長も影もありません。
プログラムの中の番号は0〜5で、画面の1〜6より1小さくなります。index%3 で列(0・1・2)を求め、0 <= 列+dx < 3 を満たす相手だけを影の集合に入れます。右はしの影が、次の行の左はしに回りこむまちがいを防ぐためです。
forecast は、朝の pots から shade(影になる鉢)を一度だけ求め、その集合を見てすべての株の夕方の値を作ります。その夕方に若葉(1)が大きな葉(2)に育っても、となりの成長をあとから取り消したりはしません。鉢を1つずつ書き換えながら影を計算し直すと、処理する順番で答えが変わってしまいます。
最初の状態では、1番が2番を影にするので、前の段階とちがって苗が育ちません。まだ収穫の操作がない練習なので、影を取り除く手入れは次の段階で加えます。
下のコードは 02_shadows.py の一部です。前のステップから追加・変更したところだけを抜き出しています。
shade = shadows(state)
return tuple(value+1 if 0 <= value < 3 and i not in shade else value
for i, value in enumerate(state.pots))実行して確かめる
教材のZIPを展開したフォルダーで、次のコマンドを実行します。
.venv\Scripts\python.exe 02_shadows.py.venv/bin/python 02_shadows.py- 影は同じ行のとなりだけ
- W と E で影の向きがちがう
- 新しく育った葉で、すでに決まった成長を止めない
pots を作る前に、shade を固定しておきます。WEATHER を4つとも E に変えると、西のはしの1番は影を落とす相手がなく、2番が1に育ちます。確かめたら W に戻します。
このステップのコード全文(167行)
# 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.
"""屋上菜園・教材2/5。独立して動く朝と夕方の模型。"""
from dataclasses import dataclass, replace
START_POTS = (2, 0, -1, -1, -1, -1)
WEATHER = ("W", "W", "W", "W")
@dataclass(frozen=True)
class State:
day: int = 0
pots: tuple = START_POTS
def shadows(state):
if state.day >= len(WEATHER) or WEATHER[state.day] == "C":
return frozenset()
dx = 1 if WEATHER[state.day] == "W" else -1
# index%3は列番号。影を次の行へ回り込ませない。
return frozenset(i+dx for i, value in enumerate(state.pots)
if value >= 2 and 0 <= i%3+dx < 3)
def forecast(state):
if state.day >= len(WEATHER) or WEATHER[state.day] == "C":
return state.pots
shade = shadows(state)
return tuple(value+1 if 0 <= value < 3 and i not in shade else value
for i, value in enumerate(state.pots))
def step(state, action):
if action == "W":
if state.day >= len(WEATHER):
return state
return replace(state, day=state.day+1, pots=forecast(state))
return state
class App:
def __init__(self):
self.reset()
def reset(self):
self.state = State()
self.selected = 0
self.locked = False
self.message = "SPACE: EVENING GROWTH"
def control(self, pressed, held):
if self.locked:
if not held:
self.locked = False
return
if "R" in pressed:
self.reset()
self.locked = True
return
if len(pressed) != 1:
return
key = next(iter(pressed))
x, y = self.selected%3, self.selected//3
if key in ("LEFT", "RIGHT", "UP", "DOWN"):
x = max(0, min(2, x+(key=="RIGHT")-(key=="LEFT")))
y = max(0, min(1, y+(key=="DOWN")-(key=="UP")))
self.selected = y*3+x
self.locked = True
return
action = "W" if key == "SPACE" else None
if action is None:
return
before = self.state
self.state = step(self.state, action)
self.message = ("EVENING / NEXT MORNING" if action=="W" else "CARE / SAME MORNING") if self.state!=before else "NO CHANGE"
self.locked = True
def draw(self, p):
p.cls(1)
p.text(8, 8, "ROOFTOP GARDEN / STAGE 2", 10)
weather = WEATHER[self.state.day] if self.state.day < len(WEATHER) else "END"
p.text(8, 22, "MORNING "+str(self.state.day)+" / "+str(len(WEATHER))+" SUN "+weather, 7)
shade = shadows(self.state)
following = forecast(self.state)
for i, value in enumerate(self.state.pots):
x, y = 12+(i%3)*100, 64+(i//3)*58
p.rectb(x, y, 92, 53, 10 if i==self.selected else 5)
p.text(x+4, y+4, "POT "+str(i+1), 7)
if i in shade:
p.text(x+48, y+4, "SHADE", 6)
p.rect(x+30, y+33, 24, 12, 4)
if value >= 0:
p.line(x+42, y+32, x+42, y+24-3*value, 3)
if value == 0:
p.circ(x+42, y+30, 2, 10)
elif value == 1:
p.circ(x+37, y+27, 3, 11)
elif value == 2:
p.circ(x+35, y+24, 6, 3)
p.circ(x+49, y+21, 6, 11)
else:
p.circ(x+36, y+19, 4, 9)
p.circ(x+47, y+22, 4, 10)
p.text(x+4, y+42, str(value)+" -> "+str(following[i]), 7)
p.text(8, 188, "-1 EMPTY 0 SEED 1 YOUNG 2 LEAF 3 POD", 6)
p.text(8, 200, "ARROW IS EVENING FORECAST, NOT EXTRA GROWTH", 6)
p.text(8, 216, self.message, 10)
p.text(8, 230, 'SPACE GROW', 7)
p.text(8, 241, "R RESET ESC QUIT", 6)
DEMO_KEYS = ('SPACE',)
def main(headless=False, screenshot=None, demo=False):
from pathlib import Path
import pyxel as p
screenshot = str(Path(screenshot).resolve()) if screenshot else None
if screenshot:
Path(screenshot).parent.mkdir(parents=True, exist_ok=True)
app = App()
p.init(320, 256, title="Rooftop Garden", fps=30, display_scale=3, headless=headless)
keys = {k: getattr(p, "KEY_"+k) for k in ("UP", "DOWN", "LEFT", "RIGHT", "SPACE", "Z", "C", "X", "R")}
limit = max(12, 2*len(DEMO_KEYS)+3) if demo else 12
def update():
if headless and p.frame_count >= limit:
if screenshot:
p.screenshot(screenshot, scale=3)
p.quit()
return
if headless and demo:
index = p.frame_count//2
pressed = {DEMO_KEYS[index]} if p.frame_count%2==0 and index<len(DEMO_KEYS) 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))
return app
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--headless-smoke", action="store_true")
parser.add_argument("--demo-checkpoint", action="store_true")
parser.add_argument("--screenshot")
args = parser.parse_args()
main(args.headless_smoke, args.screenshot, args.demo_checkpoint)食材と次の種を選んで収穫
株と資源を一緒に更新する。
矢印キーで鉢を選び、Z で手入れをします。大きな葉(2)なら食材が2、種のさや(3)なら種が2手に入り、その鉢は空(-1)になります。空の鉢では、種を1つ使って苗(0)を植えます。苗(0)と若葉(1)は、まだ収穫できません。
手入れには時間がかかりません。同じ朝に何鉢でも手入れでき、Space で夕方を確定したときだけ日付が進みます。最初に大きな葉を摘めば、その朝の影も消えるので、2番の苗がその日の夕方から育ちます。
今、食材として摘むか、種ができるまで残すか。同じ株を両方に数えないように、収穫したら必ず鉢を空にします。leaves・pods・planted は、葉の収穫・さやの収穫・植えた回数です。種の数が「最初の種+2×さやの収穫−植えた数」になっているかを、テストで確かめられます。
ここでは、納品や期限はまだ加えていません。食材は傷まないので、資源の変わり方を落ち着いて試せます。
下のコードは 03_harvest.py の一部です。前のステップから追加・変更したところだけを抜き出しています。
return replace(state, pots=tuple(pots), food=state.food+2, leaves=state.leaves+1)実行して確かめる
教材のZIPを展開したフォルダーで、次のコマンドを実行します。
.venv\Scripts\python.exe 03_harvest.py.venv/bin/python 03_harvest.py- 手入れでは日付が進まない
- 葉は食材に、さやは種になる
- 植えるには種を1つ使う
replace で一緒に更新します。R から Space で1番を3まで育て、Z で種を2つ取り、もう一度 Z で同じ鉢に植えてみましょう。朝1・苗0・種1・食材0 になることを確かめます。
このステップのコード全文(190行)
# 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.
"""屋上菜園・教材3/5。独立して動く朝と夕方の模型。"""
from dataclasses import dataclass, replace
START_POTS = (2, 0, -1, -1, -1, -1)
WEATHER = ("W", "W", "W", "W")
START_SEEDS = 0
@dataclass(frozen=True)
class State:
day: int = 0
pots: tuple = START_POTS
seeds: int = START_SEEDS
food: int = 0
planted: int = 0
leaves: int = 0
pods: int = 0
def shadows(state):
if state.day >= len(WEATHER) or WEATHER[state.day] == "C":
return frozenset()
dx = 1 if WEATHER[state.day] == "W" else -1
# index%3は列番号。影を次の行へ回り込ませない。
return frozenset(i+dx for i, value in enumerate(state.pots)
if value >= 2 and 0 <= i%3+dx < 3)
def forecast(state):
if state.day >= len(WEATHER) or WEATHER[state.day] == "C":
return state.pots
shade = shadows(state)
return tuple(value+1 if 0 <= value < 3 and i not in shade else value
for i, value in enumerate(state.pots))
def step(state, action):
if action == "W":
if state.day >= len(WEATHER):
return state
return replace(state, day=state.day+1, pots=forecast(state))
if not isinstance(action, str) or len(action) != 2 or action[0] != "C" or action[1] not in "012345":
return state
index = int(action[1])
value = state.pots[index]
pots = list(state.pots)
if value == 2:
pots[index] = -1
return replace(state, pots=tuple(pots), food=state.food+2, leaves=state.leaves+1)
if value == 3:
pots[index] = -1
return replace(state, pots=tuple(pots), seeds=state.seeds+2, pods=state.pods+1)
if value == -1 and state.seeds > 0:
pots[index] = 0
return replace(state, pots=tuple(pots), seeds=state.seeds-1, planted=state.planted+1)
return state
class App:
def __init__(self):
self.reset()
def reset(self):
self.state = State()
self.selected = 0
self.locked = False
self.message = "SPACE: EVENING GROWTH"
def control(self, pressed, held):
if self.locked:
if not held:
self.locked = False
return
if "R" in pressed:
self.reset()
self.locked = True
return
if len(pressed) != 1:
return
key = next(iter(pressed))
x, y = self.selected%3, self.selected//3
if key in ("LEFT", "RIGHT", "UP", "DOWN"):
x = max(0, min(2, x+(key=="RIGHT")-(key=="LEFT")))
y = max(0, min(1, y+(key=="DOWN")-(key=="UP")))
self.selected = y*3+x
self.locked = True
return
action = "W" if key == "SPACE" else None
if key == "Z":
action = "C"+str(self.selected)
if action is None:
return
before = self.state
self.state = step(self.state, action)
self.message = ("EVENING / NEXT MORNING" if action=="W" else "CARE / SAME MORNING") if self.state!=before else "NO CHANGE"
self.locked = True
def draw(self, p):
p.cls(1)
p.text(8, 8, "ROOFTOP GARDEN / STAGE 3", 10)
weather = WEATHER[self.state.day] if self.state.day < len(WEATHER) else "END"
p.text(8, 22, "MORNING "+str(self.state.day)+" / "+str(len(WEATHER))+" SUN "+weather, 7)
p.text(8, 34, "SEEDS "+str(self.state.seeds)+" FOOD "+str(self.state.food), 11)
shade = shadows(self.state)
following = forecast(self.state)
for i, value in enumerate(self.state.pots):
x, y = 12+(i%3)*100, 64+(i//3)*58
p.rectb(x, y, 92, 53, 10 if i==self.selected else 5)
p.text(x+4, y+4, "POT "+str(i+1), 7)
if i in shade:
p.text(x+48, y+4, "SHADE", 6)
p.rect(x+30, y+33, 24, 12, 4)
if value >= 0:
p.line(x+42, y+32, x+42, y+24-3*value, 3)
if value == 0:
p.circ(x+42, y+30, 2, 10)
elif value == 1:
p.circ(x+37, y+27, 3, 11)
elif value == 2:
p.circ(x+35, y+24, 6, 3)
p.circ(x+49, y+21, 6, 11)
else:
p.circ(x+36, y+19, 4, 9)
p.circ(x+47, y+22, 4, 10)
p.text(x+4, y+42, str(value)+" -> "+str(following[i]), 7)
p.text(8, 188, "-1 EMPTY 0 SEED 1 YOUNG 2 LEAF 3 POD", 6)
p.text(8, 200, "ARROW IS EVENING FORECAST, NOT EXTRA GROWTH", 6)
p.text(8, 216, self.message, 10)
p.text(8, 230, 'SPACE GROW ARROWS SELECT Z CARE', 7)
p.text(8, 241, "R RESET ESC QUIT", 6)
DEMO_KEYS = ('Z', 'SPACE', 'SPACE', 'SPACE', 'RIGHT', 'Z')
def main(headless=False, screenshot=None, demo=False):
from pathlib import Path
import pyxel as p
screenshot = str(Path(screenshot).resolve()) if screenshot else None
if screenshot:
Path(screenshot).parent.mkdir(parents=True, exist_ok=True)
app = App()
p.init(320, 256, title="Rooftop Garden", fps=30, display_scale=3, headless=headless)
keys = {k: getattr(p, "KEY_"+k) for k in ("UP", "DOWN", "LEFT", "RIGHT", "SPACE", "Z", "C", "X", "R")}
limit = max(12, 2*len(DEMO_KEYS)+3) if demo else 12
def update():
if headless and p.frame_count >= limit:
if screenshot:
p.screenshot(screenshot, scale=3)
p.quit()
return
if headless and demo:
index = p.frame_count//2
pressed = {DEMO_KEYS[index]} if p.frame_count%2==0 and index<len(DEMO_KEYS) 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))
return app
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--headless-smoke", action="store_true")
parser.add_argument("--demo-checkpoint", action="store_true")
parser.add_argument("--screenshot")
args = parser.parse_args()
main(args.headless_smoke, args.screenshot, args.demo_checkpoint)期限の朝に納品して締める
期限を確かめるのは、確定する操作のときにする。
元のゲームの A「葉を残す一日」の目標を加えます。朝4までに食材を合計2つ以上納品し、種を1つ以上残すことです。C は、持っている食材を全部渡すキーです。納品も、手入れと同じく時間がかかりません。
Space で朝3から朝4に進んでも、まだ最後の朝の作業ができます。次の Space でその日を締め切り、期限と種を確かめます。朝4に入った瞬間に失敗にすると、元のゲームの「期限の朝も作業できる」というルールが変わってしまいます。
DEADLINES の1組は、(朝, 合計の納品数) です。日を進める・締め切る W のときに、もう期限が来た目標を確かめます。food(持っている食材)ではなく delivered(納品した数)を見るので、収穫しただけでは達成になりません。
WON(成功)か SHORT(足りない)になったら、ふつうの操作は止まります。ここで、元のゲームの A を最後まで遊べる、小さな完成版になります。元のゲームの B と C にある、何世代も育てる仕組みや、途中の期限は、元の完成版を読む発展の課題にします。
下のコードは 04_delivery.py の一部です。前のステップから追加・変更したところだけを抜き出しています。
missed = any(day <= state.day and state.delivered < goal
for day, goal in DEADLINES)実行して確かめる
教材のZIPを展開したフォルダーで、次のコマンドを実行します。
.venv\Scripts\python.exe 04_delivery.py.venv/bin/python 04_delivery.py- 収穫と納品は別の操作
- 最後の朝も作業できる
- 残った種の数も確かめる
DEADLINES を ((2, 2),) に変えて起動し直してみましょう。Space 2回では朝2の PLAY(遊んでいる途中)、3回目は納品していないので SHORT です。確かめたら期限を4に戻します。
このステップのコード全文(214行)
# 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.
"""屋上菜園・教材4/5。独立して動く朝と夕方の模型。"""
from dataclasses import dataclass, replace
START_POTS = (2, 0, -1, -1, -1, -1)
WEATHER = ("W", "W", "W", "W")
START_SEEDS = 0
DEADLINES = ((4, 2),)
SEED_GOAL = 1
@dataclass(frozen=True)
class State:
day: int = 0
pots: tuple = START_POTS
seeds: int = START_SEEDS
food: int = 0
delivered: int = 0
planted: int = 0
leaves: int = 0
pods: int = 0
status: str = "play"
def shadows(state):
if state.day >= len(WEATHER) or WEATHER[state.day] == "C":
return frozenset()
dx = 1 if WEATHER[state.day] == "W" else -1
# index%3は列番号。影を次の行へ回り込ませない。
return frozenset(i+dx for i, value in enumerate(state.pots)
if value >= 2 and 0 <= i%3+dx < 3)
def forecast(state):
if state.day >= len(WEATHER) or WEATHER[state.day] == "C":
return state.pots
shade = shadows(state)
return tuple(value+1 if 0 <= value < 3 and i not in shade else value
for i, value in enumerate(state.pots))
def step(state, action):
if state.status != "play":
return state
if action == "D":
return replace(state, food=0, delivered=state.delivered+state.food) if state.food else state
if action == "W":
missed = any(day <= state.day and state.delivered < goal
for day, goal in DEADLINES)
if state.day == len(WEATHER):
status = "won" if not missed and state.seeds >= SEED_GOAL else "short"
return replace(state, status=status)
if missed:
return replace(state, status="short")
return replace(state, day=state.day+1, pots=forecast(state))
if not isinstance(action, str) or len(action) != 2 or action[0] != "C" or action[1] not in "012345":
return state
index = int(action[1])
value = state.pots[index]
pots = list(state.pots)
if value == 2:
pots[index] = -1
return replace(state, pots=tuple(pots), food=state.food+2, leaves=state.leaves+1)
if value == 3:
pots[index] = -1
return replace(state, pots=tuple(pots), seeds=state.seeds+2, pods=state.pods+1)
if value == -1 and state.seeds > 0:
pots[index] = 0
return replace(state, pots=tuple(pots), seeds=state.seeds-1, planted=state.planted+1)
return state
class App:
def __init__(self):
self.reset()
def reset(self):
self.state = State()
self.selected = 0
self.locked = False
self.message = "SPACE: EVENING GROWTH"
def control(self, pressed, held):
if self.locked:
if not held:
self.locked = False
return
if "R" in pressed:
self.reset()
self.locked = True
return
if self.state.status != "play":
return
if len(pressed) != 1:
return
key = next(iter(pressed))
x, y = self.selected%3, self.selected//3
if key in ("LEFT", "RIGHT", "UP", "DOWN"):
x = max(0, min(2, x+(key=="RIGHT")-(key=="LEFT")))
y = max(0, min(1, y+(key=="DOWN")-(key=="UP")))
self.selected = y*3+x
self.locked = True
return
action = "W" if key == "SPACE" else None
if key == "Z":
action = "C"+str(self.selected)
if key == "C":
action = "D"
if action is None:
return
before = self.state
self.state = step(self.state, action)
self.message = ("EVENING / NEXT MORNING" if action=="W" else "CARE / SAME MORNING") if self.state!=before else "NO CHANGE"
if action == "D" and self.state != before:
self.message = "DELIVERED / SAME MORNING"
if self.state.status != "play":
self.message = self.state.status.upper()+" / R TO RESTART"
self.locked = True
def draw(self, p):
p.cls(1)
p.text(8, 8, "ROOFTOP GARDEN / STAGE 4", 10)
weather = WEATHER[self.state.day] if self.state.day < len(WEATHER) else "END"
p.text(8, 22, "MORNING "+str(self.state.day)+" / "+str(len(WEATHER))+" SUN "+weather, 7)
p.text(8, 34, "SEEDS "+str(self.state.seeds)+" FOOD "+str(self.state.food), 11)
p.text(148, 34, "DELIVERED "+str(self.state.delivered), 11)
deadline = " / ".join("D"+str(day)+":"+str(goal) for day, goal in DEADLINES)
p.text(8, 46, "DELIVERY "+deadline+" FINAL SEEDS "+str(SEED_GOAL), 6)
shade = shadows(self.state)
following = forecast(self.state)
for i, value in enumerate(self.state.pots):
x, y = 12+(i%3)*100, 64+(i//3)*58
p.rectb(x, y, 92, 53, 10 if i==self.selected else 5)
p.text(x+4, y+4, "POT "+str(i+1), 7)
if i in shade:
p.text(x+48, y+4, "SHADE", 6)
p.rect(x+30, y+33, 24, 12, 4)
if value >= 0:
p.line(x+42, y+32, x+42, y+24-3*value, 3)
if value == 0:
p.circ(x+42, y+30, 2, 10)
elif value == 1:
p.circ(x+37, y+27, 3, 11)
elif value == 2:
p.circ(x+35, y+24, 6, 3)
p.circ(x+49, y+21, 6, 11)
else:
p.circ(x+36, y+19, 4, 9)
p.circ(x+47, y+22, 4, 10)
p.text(x+4, y+42, str(value)+" -> "+str(following[i]), 7)
p.text(8, 188, "-1 EMPTY 0 SEED 1 YOUNG 2 LEAF 3 POD", 6)
p.text(8, 200, "ARROW IS EVENING FORECAST, NOT EXTRA GROWTH", 6)
p.text(8, 216, self.message, 10)
p.text(8, 230, 'SPACE GROW ARROWS SELECT Z CARE C DELIVER', 7)
p.text(8, 241, "R RESET ESC QUIT", 6)
DEMO_KEYS = ('Z', 'SPACE', 'SPACE', 'SPACE', 'RIGHT', 'Z', 'C', 'SPACE', 'SPACE')
def main(headless=False, screenshot=None, demo=False):
from pathlib import Path
import pyxel as p
screenshot = str(Path(screenshot).resolve()) if screenshot else None
if screenshot:
Path(screenshot).parent.mkdir(parents=True, exist_ok=True)
app = App()
p.init(320, 256, title="Rooftop Garden", fps=30, display_scale=3, headless=headless)
keys = {k: getattr(p, "KEY_"+k) for k in ("UP", "DOWN", "LEFT", "RIGHT", "SPACE", "Z", "C", "X", "R")}
limit = max(12, 2*len(DEMO_KEYS)+3) if demo else 12
def update():
if headless and p.frame_count >= limit:
if screenshot:
p.screenshot(screenshot, scale=3)
p.quit()
return
if headless and demo:
index = p.frame_count//2
pressed = {DEMO_KEYS[index]} if p.frame_count%2==0 and index<len(DEMO_KEYS) 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))
return app
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--headless-smoke", action="store_true")
parser.add_argument("--demo-checkpoint", action="store_true")
parser.add_argument("--screenshot")
args = parser.parse_args()
main(args.headless_smoke, args.screenshot, args.demo_checkpoint)朝の判断を丸ごと戻す
時間のかからない操作も、履歴に積む。
X キーで、直前に確定した操作を戻します。Timeline は、今の State と過去の State のタプルを持ち、状態を変える操作の前だけ履歴を追加します。State と pots は書き換えられないので、保存した過去の朝があとから書き換わることはありません。
日付を進めたときだけ記録してはいけません。収穫・植えつけ・納品は、時間はかからなくても大事な判断です。1回の X で、最後の1操作を戻せるようにします。苗を収穫しようとするなど、何も変わらない操作は履歴に入りません。
WON や SHORT のときでも X を先に受け付けるので、締め切った結果を戻すと、朝4の PLAY に戻れます。R は、鉢・資源・日付・履歴を最初に戻します。カーソルは見た目の選択なので、ゲームの状態の Undo とは別にしています。
完成版のふつうのルールは、元のゲームの A と比べてあります。状況の選択、影なしの比較、日本語の文字、記録の保存は省いています。次は、元のゲームの forecast・step・invariant で、いっせいの成長と、資源の帳簿のつけ方を読みましょう。
下のコードは 05_undo.py の一部です。前のステップから追加・変更したところだけを抜き出しています。
return timeline if state == timeline.state else Timeline(state, timeline.history+(timeline.state,))実行して確かめる
教材のZIPを展開したフォルダーで、次のコマンドを実行します。
.venv\Scripts\python.exe 05_undo.py.venv/bin/python 05_undo.py- 時間のかからない操作も Undo できる
- 受け付けられない操作では履歴は変わらない
- 結果の確定も戻せる
State 全体を保存し、何も変わらない操作は積まないようにします。R→Z で収穫→C で納品→X と押してみましょう。日付0のまま、FOOD 2・DELIVERED 0 に戻ることを確かめます。
このステップのコード全文(236行)
# 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.
"""屋上菜園・教材5/5。独立して動く朝と夕方の模型。"""
from dataclasses import dataclass, replace
START_POTS = (2, 0, -1, -1, -1, -1)
WEATHER = ("W", "W", "W", "W")
START_SEEDS = 0
DEADLINES = ((4, 2),)
SEED_GOAL = 1
@dataclass(frozen=True)
class State:
day: int = 0
pots: tuple = START_POTS
seeds: int = START_SEEDS
food: int = 0
delivered: int = 0
planted: int = 0
leaves: int = 0
pods: int = 0
status: str = "play"
def shadows(state):
if state.day >= len(WEATHER) or WEATHER[state.day] == "C":
return frozenset()
dx = 1 if WEATHER[state.day] == "W" else -1
# index%3は列番号。影を次の行へ回り込ませない。
return frozenset(i+dx for i, value in enumerate(state.pots)
if value >= 2 and 0 <= i%3+dx < 3)
def forecast(state):
if state.day >= len(WEATHER) or WEATHER[state.day] == "C":
return state.pots
shade = shadows(state)
return tuple(value+1 if 0 <= value < 3 and i not in shade else value
for i, value in enumerate(state.pots))
def step(state, action):
if state.status != "play":
return state
if action == "D":
return replace(state, food=0, delivered=state.delivered+state.food) if state.food else state
if action == "W":
missed = any(day <= state.day and state.delivered < goal
for day, goal in DEADLINES)
if state.day == len(WEATHER):
status = "won" if not missed and state.seeds >= SEED_GOAL else "short"
return replace(state, status=status)
if missed:
return replace(state, status="short")
return replace(state, day=state.day+1, pots=forecast(state))
if not isinstance(action, str) or len(action) != 2 or action[0] != "C" or action[1] not in "012345":
return state
index = int(action[1])
value = state.pots[index]
pots = list(state.pots)
if value == 2:
pots[index] = -1
return replace(state, pots=tuple(pots), food=state.food+2, leaves=state.leaves+1)
if value == 3:
pots[index] = -1
return replace(state, pots=tuple(pots), seeds=state.seeds+2, pods=state.pods+1)
if value == -1 and state.seeds > 0:
pots[index] = 0
return replace(state, pots=tuple(pots), seeds=state.seeds-1, planted=state.planted+1)
return state
@dataclass(frozen=True)
class Timeline:
state: State = State()
history: tuple = ()
def apply(timeline, action):
if action == "undo":
return Timeline(timeline.history[-1], timeline.history[:-1]) if timeline.history else timeline
state = step(timeline.state, action)
return timeline if state == timeline.state else Timeline(state, timeline.history+(timeline.state,))
class App:
def __init__(self):
self.reset()
def reset(self):
self.timeline = Timeline()
self.selected = 0
self.locked = False
self.message = "SPACE: EVENING GROWTH"
@property
def state(self):
return self.timeline.state
def control(self, pressed, held):
if self.locked:
if not held:
self.locked = False
return
if "R" in pressed:
self.reset()
self.locked = True
return
if "X" in pressed:
before = self.state
self.timeline = apply(self.timeline, "undo")
self.message = "UNDO" if self.state != before else "NOTHING TO UNDO"
self.locked = True
return
if self.state.status != "play":
return
if len(pressed) != 1:
return
key = next(iter(pressed))
x, y = self.selected%3, self.selected//3
if key in ("LEFT", "RIGHT", "UP", "DOWN"):
x = max(0, min(2, x+(key=="RIGHT")-(key=="LEFT")))
y = max(0, min(1, y+(key=="DOWN")-(key=="UP")))
self.selected = y*3+x
self.locked = True
return
action = "W" if key == "SPACE" else None
if key == "Z":
action = "C"+str(self.selected)
if key == "C":
action = "D"
if action is None:
return
before = self.state
self.timeline = apply(self.timeline, action)
self.message = ("EVENING / NEXT MORNING" if action=="W" else "CARE / SAME MORNING") if self.state!=before else "NO CHANGE"
if action == "D" and self.state != before:
self.message = "DELIVERED / SAME MORNING"
if self.state.status != "play":
self.message = self.state.status.upper()+" / R TO RESTART"
self.locked = True
def draw(self, p):
p.cls(1)
p.text(8, 8, "ROOFTOP GARDEN / STAGE 5", 10)
weather = WEATHER[self.state.day] if self.state.day < len(WEATHER) else "END"
p.text(8, 22, "MORNING "+str(self.state.day)+" / "+str(len(WEATHER))+" SUN "+weather, 7)
p.text(8, 34, "SEEDS "+str(self.state.seeds)+" FOOD "+str(self.state.food), 11)
p.text(148, 34, "DELIVERED "+str(self.state.delivered), 11)
deadline = " / ".join("D"+str(day)+":"+str(goal) for day, goal in DEADLINES)
p.text(8, 46, "DELIVERY "+deadline+" FINAL SEEDS "+str(SEED_GOAL), 6)
shade = shadows(self.state)
following = forecast(self.state)
for i, value in enumerate(self.state.pots):
x, y = 12+(i%3)*100, 64+(i//3)*58
p.rectb(x, y, 92, 53, 10 if i==self.selected else 5)
p.text(x+4, y+4, "POT "+str(i+1), 7)
if i in shade:
p.text(x+48, y+4, "SHADE", 6)
p.rect(x+30, y+33, 24, 12, 4)
if value >= 0:
p.line(x+42, y+32, x+42, y+24-3*value, 3)
if value == 0:
p.circ(x+42, y+30, 2, 10)
elif value == 1:
p.circ(x+37, y+27, 3, 11)
elif value == 2:
p.circ(x+35, y+24, 6, 3)
p.circ(x+49, y+21, 6, 11)
else:
p.circ(x+36, y+19, 4, 9)
p.circ(x+47, y+22, 4, 10)
p.text(x+4, y+42, str(value)+" -> "+str(following[i]), 7)
p.text(8, 188, "-1 EMPTY 0 SEED 1 YOUNG 2 LEAF 3 POD", 6)
p.text(8, 200, "ARROW IS EVENING FORECAST, NOT EXTRA GROWTH", 6)
p.text(8, 216, self.message, 10)
p.text(8, 230, 'SPACE GROW ARROWS SELECT Z CARE C DELIVER', 7)
p.text(8, 241, "R RESET X UNDO ESC QUIT", 6)
DEMO_KEYS = ('Z', 'SPACE', 'SPACE', 'SPACE', 'RIGHT', 'Z', 'C', 'SPACE', 'SPACE')
def main(headless=False, screenshot=None, demo=False):
from pathlib import Path
import pyxel as p
screenshot = str(Path(screenshot).resolve()) if screenshot else None
if screenshot:
Path(screenshot).parent.mkdir(parents=True, exist_ok=True)
app = App()
p.init(320, 256, title="Rooftop Garden", fps=30, display_scale=3, headless=headless)
keys = {k: getattr(p, "KEY_"+k) for k in ("UP", "DOWN", "LEFT", "RIGHT", "SPACE", "Z", "C", "X", "R")}
limit = max(12, 2*len(DEMO_KEYS)+3) if demo else 12
def update():
if headless and p.frame_count >= limit:
if screenshot:
p.screenshot(screenshot, scale=3)
p.quit()
return
if headless and demo:
index = p.frame_count//2
pressed = {DEMO_KEYS[index]} if p.frame_count%2==0 and index<len(DEMO_KEYS) 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))
return app
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--headless-smoke", action="store_true")
parser.add_argument("--demo-checkpoint", action="store_true")
parser.add_argument("--screenshot")
args = parser.parse_args()
main(args.headless_smoke, args.screenshot, args.demo_checkpoint)仕組みがわかったら、元の完成版を読んでみよう。
元のゲームの A です。大きな葉(2)から食材2、種のさや(3)から種2、空の鉢に種1で苗(0)を植えます。朝の手入れのあとで影を一度だけ計算し、夕方にいっせいに育ちます。期限の朝も手入れと納品ができ、次の Space で期限を確かめます。
ここまでに作ったのは、元のゲームの仕組みだけを取り出して作り直した「簡略版」です。元のゲームのソースは手を加えずに、別のZIP(元の完成版ソース)に入れてあります。
| 比べる点 | 5ステップの教材 | 元の完成版 |
|---|---|---|
| 段階と状況 | 01は影なしの成長、02は影、03は資源の練習。04と05は元のゲームのA。 | A・B・C。途中の期限・光の向きの反転・くもり・何世代もの育成もある。 |
| 表示と操作 | 英字(ASCII)と簡単な鉢、次の夕方の予告。始めてすぐ遊べ、05だけXのUndoがある。 | 日本語の文字、表紙、状況の選択、説明や一時停止など。 |
| 比較と記録 | ふつうのルールで固定。記録の保存と、影なしの比較の切り替えは省いている。 | 影だけを外した管理用の比較と、再現の記録・補助ツールがある。 |
完成版のコードで、次に読むところ
model.pyのshadows・forecast(朝の古い状態から計算する部分)model.pyのstep(期限と最後の朝の扱い)model.pyのinvariant(種・食材・株の数がいつも合っているかの式)model.pyのapply・undoと、B と C のLevelデータ
最初は、数字かルールを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の実際のパソコンでの操作や、音が出るかどうかはまだ確認していません。人がすべての操作を試したわけでもありません。ブラウザー版の確認範囲は確認状況をご覧ください。
うまく動かないときは:起動やエラーの対処法 / 質問用のメモを作る
