この記事はtraP Advent Calendar2018 12月4日の記事です。
つかみ
こんにちは。hukuda222です。
本日12/4はご注文はうさぎですか?の香風智乃ちゃんの誕生日です。おめでとうございます!!!
さて、なんの関係もない上に突然ですが、皆さんはリスト内包表記は好きですか?
リスト内包表記とは、Python、Haskell、Nimなどの言語で実装されているリスト内で命令を実行できる機構です。具体的にどんなのかというとこんなのです。
なるべく読みやすくなるように頑張りますが、今回の記事のPythonコードは全てワンライナーかリスト内包表記縛りで書きます。
print([i*3 for i in range(4)])
main = print [i*3 | i <- [0..3]]
import sugar
echo lc[ i*3 | (i <- @[0,1,2,3]) , int]
実行すると、9以下の3の倍数の正整数を列挙することができます。これを見ても、
「はぁ?大したことないやん。便利っちゃ便利だけどそれがなんなの?」
と思われるかもしれませんが、何を隠そうPythonのリスト内包表記はチューリング完全です。(他も多分そうだと思うけど知らない)
チューリング完全ということはなんでもできます。それを示すべくbrainf*ckの実装でもやろうか思ったのですが、別の方が作っていました。そこであるツイートを目にします。
ライフゲームが作れるならチューリング完全だよ
— やまだ (@_ymdtr_) 2018年11月21日
確かにライフゲームはそれ自体がチューリング完全だったような気がするので、ライフゲームを作ることにしました。
Haskellは標準出力が面倒そうで、Nimはほぼ書いたことないので今回はPythonを使います。
小技の紹介
作る前にいくつか今回使う小技の紹介です。
関数定義
Pythonは他の多くの言語と同様にラムダ式を定義することができます。ラムダ式とは雑に言うと一行で書ける関数です。
print([[f(i) for i in [1,2,3]] for f in [lambda x:x+1]]) # [[2,3,4]]
ちょっと文字数が増えますが、ラムダ関数を使うことで複雑な処理もできるようになります。同様にリスト内グローバル変数なども定義できます。
import
Pythonは、必要なライブラリをimport文でインポートします。通常のimportはリスト内部ではできませんが、以下のようにすることでimportすることができます。
もちろんimportしたリストの内部でしか使えませんが、今回は一つのリストの内側で全てが完結するので問題ないです。
print([math.pi for math in [__import__("math")]]) #[3.141592653589793]
時間差出力
[[time.sleep(1) or print(i) for i in [1,2,3]] for time [__import__("time")]]
1秒ごとに、1,2,3を表示します。
Pythonのtime.sleep関数の返り値はNoneなので、Falseと同じ扱いになります。
orにしているため、一つ目の比較文が出力であるNoneを返すまで1秒待ってから、2つ目の比較文であるprint(i)を実行することになります。
値の更新
Pythonで変数に値を代入するのは簡単です。変数aに1を入れるなら、a=1
で終わりです。
しかし、リスト内部で代入はできません(僕が知らないだけで、やる方法があるかもしれませんが)
[[(a.append(i) or a.pop(0)) and print(a) for i in range(2, 10)] for a in [[1]]]
実行すると、[2],[3],...[9]が順番に表示されます。このように、リスト操作を使うことで値を更新することができます。
ちなみに、Pythonは0がFalse、0より大きい正の数はTrue扱いとなるので上のようなコードになります。
無限ループ
何がしたいかというと、こういうことがしたいです。
while True:
{任意の処理}
どうにも闇魔術っぽさがありますが、こんな感じで実装できます。
[[loop.append(None) or print("こころぴょんぴょん") for l in loop] for loop in [[0]]]
実行すると無限にこころぴょんぴょんできます。自力では思いつかなかったので、後述の記事を参考にしました。
スクロールせずにアニメーションさせる
東工大の1年次の選択講義で、anime.rb(anime.py)というコンソールに文字列を出力してアニメーションを作る課題があります。
当時の僕は「どうやってアニメーションさせるんだろう」と思っていたのですが、渡されたサンプルコードは描画するたびに改行していました。
別にそれでもいいですが、せっかくなのでスクロールしないようにします。cursesという標準出力をいじる標準ライブラリを使います。
[[[sc.clear() or [sc.addch(j, i, "#")for j in range(5)]
and sc.refresh() or time.sleep(1) or
for i in range(5)
for sc in [curses.initscr()]
]for time, curses in [(__import__("time"), __import__("curses"))]]
]
縦に並んだ#が右に動いてるように見えると思います。
i==4
の時だけだけ処理をさせたい場合は、(i==4 and {したい処理})
とするとできます。
cursesは全角文字を弄るとうまくいかないことがあるので、使うなら半角文字のみにするのが無難です。
できたやつ
盤面が小さいので基本的に小物しかできないですね。
固定物体はブロック、ボートと蜂の巣、振動子はブリンカーができています。
[[[a.append(get_next(a[0], np.pad(a[0], [(1, 1), (1, 1)], "constant")))
or time.sleep(0.2) or print(arange(a.pop(0))) for i in range(1000)]
for get_next, arange, a in
[(lambda a, b:[[(lambda x, y: 1 if x == 3
else 1 if x == 2 and y == 1 else 0)
(np.sum(b[i:i + 3, j:j + 3]) -
a[i][j], a[i][j])
for j in range(len(a[0]))]
for i in range(len(a))],
lambda a:"\n".join(["".join(["■"if aij else "□" for aij in ai])
for ai in a]) + "\n",
[[[random.randint(0, 1) for j in range(20)] for i in range(20)]]
)]]
for np, time, random in
[(__import__("numpy"), __import__('time'), __import__('random'))]
]
全角文字("■","□")を使いたかったので、cursesは使いませんでした。
読みやすくするためにnumpy、いい感じに表示するためにtime、randomを使いましたが、使わなくても実装はできるのでやっぱり内包表記はチューリング完全なんだなぁと思いました。
読みやすくするために改行やインデントを入れましたが、ただのリストなのでワンライナーで書くこともできます。
np.pad(a[0], [(1, 1), (1, 1)], "constant")
で現在の盤面の二次元配列の周囲1マスを0で埋めた別の二次元配列を作って計算しやすくしています。
おまけ
ところで、cursesの機能は標準出力をいい感じにするだけではありません。標準入力もいい感じにすることができます。標準入力と標準出力がいい感じにできるということは……?
そうです、ゲームを作ることができます!!!
適当なキーを押すとジャンプします。流れてくるブロックに当たるとゲームオーバーです。
跳ねてるのはティッピーです。
[[[[init() or th1().start() or th2().start()
for th1, th2 in
[(lambda: threading.Thread(target=get_key,
args=(inp, [0], upd), daemon=True),
lambda: threading.Thread(
target=main, args=(inp, [0], [19], upd, [0]))
)]]
for get_key, main in [(
lambda inp, loop, upd: [upd(sc.getch(), inp) or loop.append(None)
for l in loop],
lambda inp, ply, ene, upd, loop:
[sc.clear() or (ply[0] > 0 and upd(ply[0] - 1, ply))
or upd(ene[0] - 1, ene) or (ene[0] < -5 and upd(19, ene)) or
[[sc.addch(ply[0] - 3 + i if ply[0] > 4 else 6 + i - ply[0], j, cij)
for j, cij in enumerate(ci)]
for i, ci in enumerate([",^~^.", "{' '}"])] and
ene[0] >= 0 and
[sc.addch(7, ene[0] + i, c) for i, c in enumerate("##")] and
((inp[0] != -1 and ply[0] <= 0) and (upd(8, ply)or upd(-1, inp)))
or sc.addstr(8, 0, " " * 20) or sc.refresh() or time.sleep(0.2)or
((ply[0] == 0 and ene[0] > -2 and ene[0] < 6)
or (loop.append(None))) and curses.reset_shell_mode()
for l in loop])]]
for init, upd, inp, sc in
[(lambda:curses.noecho() or curses.cbreak(),
lambda x, arr:arr.append(x) or arr.pop(0) and False,
[-1], curses.initscr())]]
for curses, threading, time in
[(__import__("curses"), __import__("threading"), __import__("time"))]]
キー入力を非同期で取る必要があるので、今回はマルチスレッドを使って実装しました。
main関数(orでやたら繋がってる部分)で、描画とプレイヤーとブロックの移動、当たり判定、終了処理をしています。しょぼいゲームですが、一応コンソールで動くゲームを作る上で最低限の処理は実装しています。
ですので、やろうと思えば超大作RPGやヌルヌル動くアクション、やり込み要素満点のシミュレーションゲームなど任意のゲームがリスト内包表記縛りで作れるはずです。(イラストは全部AAですが)
おわりに
リスト内包表記は短くかけるので楽しいです。リスト内包表記/ワンライナー縛りGamejamとか楽しそうだなぁ〜というのを一瞬思いましたが、自分を含めて参加者が0になりそうなので多分やらないです。
多少のできないことはあるものの、普通に書いてできることは大概リスト内包表記でもできるので暇な方は試してみてはいかがでしょうか。ひょっとしたら副作用とかを意識する良い機会になるかもしれません。
Qiitaのbrainf*ckや純LISPをリスト内包表記で実装した方の記事を貼っておくので興味がある方はどうぞ。
関連記事
Pythonのリスト内包表記はチューリング完全だから純LISPだって実装できる(Qiita)
明日の担当はSyojuくんです。お楽しみに。
余談ですが、この記事のヘッダ画像にgif使えるかな?って貼ったら普通に使えてびっくりしました。