Sub-Python(PyLite)の構文
bot のコードを動かしているのは PyLite という、このゲームのために書かれた Python の処理系です。 本物の Python(CPython)ではなく、Python の一部だけを実装したものです。 このページでは「どこまで書けるか」と「なぜそこで線を引いたか」を説明します。
| 分類 | 書けるもの |
|---|---|
| 制御構造 | if / elif / else / while / for ... in range() / for ... in <リスト> / break / continue / pass |
| 演算 | + - * / // % **、比較(a < b < c のような連鎖も可)、and or not、in / not in、x if 条件 else y |
| 代入 | = += -= *= /= //= %= **=、タプルアンパック(a, b = b, a) |
| 変数 | ローカル変数、グローバル変数(global 宣言) |
| 関数 | def(引数・戻り値あり。デフォルト引数はなし)、呼び出し側のキーワード引数(sorted(xs, key=f)) |
| データ構造 | list、tuple、dict(リテラル・添字・スライス)、属性アクセス(state.me.x) |
| 組み込み | math.*、len / min / max / abs / sum / range / sorted / int / float / round / str / bool / list / tuple / dict / enumerate / zip / print、ゲーム専用のヘルパー |
| メソッド | list: append / pop / insert / remove / index / count / reverse / sortdict: get / keys / values / items / popstr: upper / lower / strip / split / join / replace / startswith / endswith / find / count |
state["me"]["x"] と state.me.x はどちらでも書けます。
辞書のかっこと引用符は初めて読む人には記号が多いので、点(.)でも読めるようにしてあります。
書けない構文と、その理由
Section titled “書けない構文と、その理由”禁止されている構文は、実行時ではなく読み込みの時点で断られます。
import os と書いた bot は「走って途中で止まる」のではなく、そもそも始まりません。
| 書けないもの | 断る理由 |
|---|---|
import / from ... import | サンドボックス |
open | サンドボックス |
eval / exec | サンドボックス |
__ で始まる名前 | サンドボックス |
class | サンドボックス/ブロックに変換できない |
lambda | ブロックに変換できない |
| リスト内包表記 | ブロックに変換できない |
try / except | ブロックに変換できない |
デコレータ、is、del | ブロックに変換できない |
理由は大きく 3 つに分かれます。順に見ていきます。
理由 1:安全性(サンドボックス)
Section titled “理由 1:安全性(サンドボックス)”CodeTank Arena では、他の人が書いた bot を自分の端末で動かします。 対戦相手が CPU であっても、将来ほかの人の bot と戦うようになっても同じです。 つまり「知らない人が書いたプログラムを、自分のブラウザで実行する」ことになります。
ふつうならこれはとても危ない話です。PyLite はそこを構造で解決しています。
bot のコードが JavaScript として実行されることは、一度もありません。
PyLite はソースを自前の構文木(AST)に読み替え、それをたどって解釈するだけです。
eval も Function も使いません。
そのうえで、bot から見える世界にはゲームのヘルパーと math しか置いていません。
globalThis、window、fetch にたどり着く名前が、そもそも存在しないのです。
import を許してしまうと、この前提が崩れます。
open でファイルを読めたら、eval で好きな文字列を実行できたら、__ 始まりの名前で
内部の仕組みに手が届いたら、その 1 つで囲いが破れます。
だからパーサの時点で拒否します。「あとで検査する」より確実だからです。
import os # ✗ import は使えませんimport math # ✗ これも書けません
math.sqrt(2.0) # ○ math は import なしで、最初から使えますmath.pi # ○import math まで断るのは意地悪に見えるかもしれませんが、理由があります。
math は「読み込むモジュール」ではなく、最初からそこにある道具として置いてあるからです。
import という入口を 1 つも開けないほうが、囲いは単純で確実になります。
理由 2:ブロックと行き来できること
Section titled “理由 2:ブロックと行き来できること”CodeTank Arena では、ブロックで組んだものが Python になり、Python がブロックに戻ります。 画面を切り替えるたびに、コードは構文木から作り直されます。
このとき、ブロックで表せない構文があると往復が壊れます。
Python 側で書いた lambda がブロックに戻せないと、ブロック画面に切り替えた瞬間に消えてしまう。
これは「生徒が書いたコードをアプリが勝手に書き換える」ということで、起きてはいけません。
リスト内包表記や lambda を断っているのは、危ないからではなくこの往復を守るためです。
表現力としては、for 文で書けば同じことができます。
# ✗ リスト内包表記は使えませんdists = [distance(0, 0, p[0], p[1]) for p in points]
# ○ for 文で書きますdists = []for p in points: dists.append(distance(0, 0, p[0], p[1]))エラーメッセージも、この方針に合わせて「代わりにどう書くか」を返します。 たとえば内包表記なら「リスト内包表記は使えません。for 文で書きましょう」と出ます。
理由 3:決定論と、命令数バジェット
Section titled “理由 3:決定論と、命令数バジェット”同じ試合はどの端末でも同じ結果になる必要があります(決定論)。 これを守るために、PyLite は次の点を保証しています。
| 項目 | PyLite でのふるまい |
|---|---|
dict を回す順番 | 入れた順を保証する |
sorted / list.sort | 安定ソートを保証する(同じ値の並びが崩れない) |
set | 未実装。list と in で代用する |
| 乱数 | random は使えない。試合ごとにシードを固定した rand() / randint() / choice() を使う |
set がないのは意地悪ではありません。集合は要素を回す順番が実装に左右されやすく、
順番が変わると試合の結果が変わってしまうからです。
時間で打ち切らず、命令数で打ち切る
Section titled “時間で打ち切らず、命令数で打ち切る”無限ループの bot を止める必要があります。 しかし「1 秒経ったら打ち切る」という作りにはできません。 速い端末と遅い端末で、止まる場所が変わってしまうからです。それは決定論が壊れるということです。
そこで PyLite は、文や式をいくつ評価したかを数えます。
| 項目 | 上限 | 超えたら |
|---|---|---|
update 1 回あたり | 150,000 命令 | その tick は前回の入力を維持 |
setup | 1,000,000 命令 | 受け付けない |
| 1 試合での超過の許容 | 3 回 | 4 回目で行動停止 |
| リスト・文字列の要素数 | 各 100,000 | MemoryError |
| 再帰の深さ | 100 | RecursionError |
| ソースコードの長さ | 32 KB | 受け付けない |
数え方は端末によらないので、どこで走らせても同じ場所で止まります。
なお 150,000 という上限はかなり余裕があります。 毎 tick A* で全体を探索しても収まる水準なので、 ふつうに書いていてバジェットで事故ることはありません。
共通テスト用プログラム表記との対訳
Section titled “共通テスト用プログラム表記との対訳”大学入学共通テストの『情報Ⅰ』は、Python そのものではなく疑似言語で出題されます。 CodeTank Arena は同じコードを疑似言語の表示に切り替えられるので、 授業や試験対策でそのまま使えます。
| 概念 | Python | 共通テスト用プログラム表記 |
|---|---|---|
| 代入 | x = 1 | x = 1(同じ) |
| 真偽値 | True / False | True / False(同じ) |
| 条件分岐 | if 条件: | もし 条件 ならば: |
| 〃(2 つめ以降) | elif 条件: | そうでなくもし 条件 ならば: |
| 〃(どれでもない) | else: | そうでなければ: |
| 反復(回数) | for i in range(a, b): | i を a から b-1 まで 1 ずつ増やしながら繰り返す: |
| 反復(リスト) | for p in points: | p を points の要素それぞれについて繰り返す: |
| 反復(条件) | while 条件: | 条件 の間繰り返す: |
| 反復を抜ける | break | 繰り返しを抜ける |
| 次の反復へ | continue | 次の繰り返しへ進む |
| 論理演算 | and / or / not | and / or / not(同じ) |
| 整数の割り算 | // | ÷ |
| 表示 | print(n) | 表示する(n) |
| 要素数 | len(points) | 要素数(points) |
| 距離を求める | distance(a, b, c, d) | 距離(a, b, c, d) |
| 向きを求める | angle_to(a, b, c, d) | 向き(a, b, c, d) |
| 戻り値 | return x | x を返す |
| 関数定義 | def 名前(引数): | 関数 名前(引数) を次のように定義する: |
+= のような複合代入は、共通テスト表記には無いので x = x - 1 の形にほどいて表示されます。
左が書いた Python、右がアプリの表示を切り替えたときに出るものです。
def sample(points, x, enemy_dist): total = 0 for i in range(0, 10, 1): total = total + i for p in points: if p > 3: break while x > 0: x -= 1 if enemy_dist < 3: fire = True elif enemy_dist < 8: fire = False else: drive = "forward" print(total) return total関数 sample(points, x, enemy_dist) を次のように定義する: total = 0 i を 0 から 9 まで 1 ずつ増やしながら繰り返す: total = total + i p を points の要素それぞれについて繰り返す: もし p > 3 ならば: 繰り返しを抜ける x > 0 の間繰り返す: x = x - 1 もし enemy_dist < 3 ならば: fire = True そうでなくもし enemy_dist < 8 ならば: fire = False そうでなければ: drive = "forward" 表示する(total) total を返すなお、関数定義の書式は公式には規定されていません。
このアプリでは、問題文でよく見る 関数 名前(引数) を次のように定義する: を採っています。
エラーは日本語で出ます
Section titled “エラーは日本語で出ます”処理系のエラーをそのまま出しても、学び始めた人には読めません。 PyLite は構文エラーを最初から日本語で返し、実行時のエラーも日本語に直して表示します。 行番号も出ます。
| もとのエラー | 表示 |
|---|---|
NameError: name 'enmy' isn't defined | enmy という名前が見つかりません。enemy の打ち間違いではありませんか?(5 行目) |
IndexError: list index out of range | リストの範囲外を読もうとしています。要素数より大きい番号を使っていませんか?(8 行目) |
ZeroDivisionError: divide by zero | 0 で割ろうとしています。割る数が 0 になっていないか確かめましょう。(22 行目) |