コンテンツにスキップ

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 notin / not inx if 条件 else y
代入= += -= *= /= //= %= **=、タプルアンパック(a, b = b, a
変数ローカル変数、グローバル変数(global 宣言)
関数def(引数・戻り値あり。デフォルト引数はなし)、呼び出し側のキーワード引数(sorted(xs, key=f)
データ構造listtupledict(リテラル・添字・スライス)、属性アクセス(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 / sort
dict: get / keys / values / items / pop
str: upper / lower / strip / split / join / replace / startswith / endswith / find / count

state["me"]["x"]state.me.xどちらでも書けます。 辞書のかっこと引用符は初めて読む人には記号が多いので、点(.)でも読めるようにしてあります。

禁止されている構文は、実行時ではなく読み込みの時点で断られますimport os と書いた bot は「走って途中で止まる」のではなく、そもそも始まりません。

書けないもの断る理由
import / from ... importサンドボックス
openサンドボックス
eval / execサンドボックス
__ で始まる名前サンドボックス
classサンドボックス/ブロックに変換できない
lambdaブロックに変換できない
リスト内包表記ブロックに変換できない
try / exceptブロックに変換できない
デコレータ、isdelブロックに変換できない

理由は大きく 3 つに分かれます。順に見ていきます。

理由 1:安全性(サンドボックス)

Section titled “理由 1:安全性(サンドボックス)”

CodeTank Arena では、他の人が書いた bot を自分の端末で動かします。 対戦相手が CPU であっても、将来ほかの人の bot と戦うようになっても同じです。 つまり「知らない人が書いたプログラムを、自分のブラウザで実行する」ことになります。

ふつうならこれはとても危ない話です。PyLite はそこを構造で解決しています。

bot のコードが JavaScript として実行されることは、一度もありません。 PyLite はソースを自前の構文木(AST)に読み替え、それをたどって解釈するだけです。 evalFunction も使いません。

そのうえで、bot から見える世界にはゲームのヘルパーと math しか置いていませんglobalThiswindowfetch にたどり着く名前が、そもそも存在しないのです。

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未実装。listin で代用する
乱数random は使えない。試合ごとにシードを固定した rand() / randint() / choice() を使う

set がないのは意地悪ではありません。集合は要素を回す順番が実装に左右されやすく、 順番が変わると試合の結果が変わってしまうからです。

時間で打ち切らず、命令数で打ち切る

Section titled “時間で打ち切らず、命令数で打ち切る”

無限ループの bot を止める必要があります。 しかし「1 秒経ったら打ち切る」という作りにはできません。 速い端末と遅い端末で、止まる場所が変わってしまうからです。それは決定論が壊れるということです。

そこで PyLite は、文や式をいくつ評価したかを数えます。

項目上限超えたら
update 1 回あたり150,000 命令その tick は前回の入力を維持
setup1,000,000 命令受け付けない
1 試合での超過の許容3 回4 回目で行動停止
リスト・文字列の要素数各 100,000MemoryError
再帰の深さ100RecursionError
ソースコードの長さ32 KB受け付けない

数え方は端末によらないので、どこで走らせても同じ場所で止まります

なお 150,000 という上限はかなり余裕があります。 毎 tick A* で全体を探索しても収まる水準なので、 ふつうに書いていてバジェットで事故ることはありません。

共通テスト用プログラム表記との対訳

Section titled “共通テスト用プログラム表記との対訳”

大学入学共通テストの『情報Ⅰ』は、Python そのものではなく疑似言語で出題されます。 CodeTank Arena は同じコードを疑似言語の表示に切り替えられるので、 授業や試験対策でそのまま使えます。

概念Python共通テスト用プログラム表記
代入x = 1x = 1(同じ)
真偽値True / FalseTrue / 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 / notand / 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 xx を返す
関数定義def 名前(引数):関数 名前(引数) を次のように定義する:

+= のような複合代入は、共通テスト表記には無いので x = x - 1 の形にほどいて表示されます。

左が書いた Python、右がアプリの表示を切り替えたときに出るものです。

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 を返す

なお、関数定義の書式は公式には規定されていません。 このアプリでは、問題文でよく見る 関数 名前(引数) を次のように定義する: を採っています。

処理系のエラーをそのまま出しても、学び始めた人には読めません。 PyLite は構文エラーを最初から日本語で返し、実行時のエラーも日本語に直して表示します。 行番号も出ます。

もとのエラー表示
NameError: name 'enmy' isn't definedenmy という名前が見つかりません。enemy の打ち間違いではありませんか?(5 行目)
IndexError: list index out of rangeリストの範囲外を読もうとしています。要素数より大きい番号を使っていませんか?(8 行目)
ZeroDivisionError: divide by zero0 で割ろうとしています。割る数が 0 になっていないか確かめましょう。(22 行目)