« Django勉強会とか日記 |Main| エスパー日記 »

« 一般化したハノイの塔問題にひそむ規則性 | Python | Re: 先頭から2文字ずつ取る »

1行でテトリス

今日のDjango勉強会で もっと短いPythonのテトリス ― TRIVIAL TECHNOLOGIES 2.0 を教えてもらったけど、15行で書かれたテトリスがリンク切れで読めません。

なぜ15行なのか。なぜ1行にしなかったのか。できなかったのか。 15行版のソースコードが見られないので謎です。

そこで自分で作ってみることにしました(ぇ)

追記。これはNikolaj Baer: Stepping it up a notch: Tetris in 100のコードを1行に縮める過程です。


= 2006-11-23 01:26:55 pygame - python game development をインストール。 Windows用インストーラがある。ダウンロード中。 Enterを5回押すだけでインストール完了。 とりあえず100行テトリスを コピペして動かしてみる。動いた。 X印で終了しようとしたら固まった。 まぁいいや。
= 2006-11-23 01:32:36 とりあえずsvnリポジトリ作成。37分。 ソースを眺める。 いろいろなものがglobalに直に置かれている。 gという変数は使われていないようにみえるのでこれを「globals()」 (グローバル名前空間)にしよう。
= 2006-11-23 01:45:43 最初の代入文のかたまりをさっそく今日覚えた 辞書のupdateのキーワード付き引数で変更。 次はrender関数の中へ。うわー、インデントがスペース3文字だ。 elseの後に改行なしでコード書いたりしてるー。 こういうコードは許せないのでとりあえず整形(どうせつぶすのに)

さて、ここで使われている変数cはローカル変数だ。 if文で変数の値をセットして後で1回使うだけ、ということは、 これは変数を使っている部分に条件込みの式を一つ入れれば済む話。

renderの中身は無事1行に。

現在1時55分。


= 2006-11-23 01:56:00 updateを利用すると、今までglobals().__setitem__("foo", 1)と書いていた代入文が かなり楽に書けるようになる。 たいがいのプログラムにある変数の初期化部分で
globals().update(g=lambda **kw:globals().update(kw), ...
と書いておけば、以降はg(foo=1, bar=2,...)と わりと普通に代入文が書けるようになる。
= 2006-11-23 02:09:27 今回は、他人のコードで、Pygameの仕様にも詳しくないので、 挙動を変えないような書き換えが必要。 たとえばすでに一部書き換え済みだけど下のような例。
   keys=pygame.key.get_pressed()
   keys[pygame.K_LEFT] and px > 0 and g(px=px-1)
   keys[pygame.K_RIGHT] and px+len(piece.split('\n')[0]) < bw and g(px=px+1)
   keys[pygame.K_SPACE] and g(py=drop_piece(piece,px,py))
   keys[pygame.K_RETURN]and g(piece=rotate_piece(piece))
この場合、pygame.key.get_pressed()を1回呼んでいるのを、 4回の呼び出しに変えてもいいかどうかは仕様による。 「前回呼ばれてから次に呼ばれるまでに押されたキーを返す」 という実装になっている可能性も大いにある。 でも調べるのは面倒なので、1回の呼び出しでいいようにする。 1回代入して複数回使うケースを、代入を使わずに実現するにはどうすればいいか…。 うーんと。 ラムダ関数の引数にする手もあるけど、 この場合は使われ方が同じだからデフォルト引数を使う方がスマートかも。 Pythonのデフォルト引数は定義時に評価されるので
   lambda key,keys=pygame.key.get_pressed(): keys[key]
こういう関数は何回呼び出しても関数定義の1回だけget_pressedが呼ばれる。 あとは下のようなリスト閉包で真偽値のリストに変えて…
[
    (lambda key,keys=pygame.key.get_pressed(): keys[key])(key)
    for key in [pygame.K_LEFT, pygame.K_RIGHT, pygame.K_SPACE, pygame.K_RETURN]
]
条件が真だった場合に実行すべき内容を、 評価を遅延させるためにlambdaで囲ったリストとzipして、 条件が真だった場合にその内容を評価するような関数にmapで渡す。
map(lambda (x,y): x and y(),
    zip([
       (lambda key,keys=pygame.key.get_pressed(): keys[key])(key)
       for key in [pygame.K_LEFT, pygame.K_RIGHT, pygame.K_SPACE, pygame.K_RETURN]
    ],[
       lambda :px > 0 and g(px=px-1),
       lambda :px+len(piece.split('\n')[0]) < bw and g(px=px+1),
       lambda :g(py=drop_piece(piece,px,py)),
       lambda :g(piece=rotate_piece(piece))
    ])
)

= 2006-11-23 02:36:07 renderとtickをnext_pieceをlambda関数に変えて、 最初の変数の初期化の行にくっつける。 結局のところ関数の宣言も変数の初期化なんだ。 値が無名関数なだけで。
= 2006-11-23 02:40:49 rotate_pieceは面倒なことをしてあるね。 素直にfor文を二重にすれば楽なのに。
def rotate_piece(p):
   pp,pl="",p.split('\n')
   for i in range(len(pl)*len(pl[0])):
      pp+=pl[len(pl)-1-i%len(pl)][i/len(pl)]
      if i%len(pl)==len(pl)-1: pp+='\n' 
   return pp.rstrip('\n')
こう書かれていたけど、まず下のように直す。
def rotate_piece(p):
    pp,pl="",p.split('\n')
    for i in range(len(pl[0])):
        for j in range(len(pl)):
            pp+=pl[len(pl)-1-j][i]
        pp+='\n' 
    return pp.rstrip('\n')
次にすること: 中のfor文が終わった後改行文字をつけておきながら、 最後にreturnする前で最後の改行文字を取り除いている所に 「"\n".join使えよ!」とつっこむ。
def rotate_piece(p):
    pl=p.split('\n')
    return  "\n".join([
            "".join([pl[len(pl)-1-j][i] for j in range(len(pl))])
            for i in range(len(pl[0]))
    ])
あとはplへの代入を取り除く。どうしようか。 これはlambdaを使うのが一番楽かも。
def rotate_piece(p):
    return  (lambda pl:"\n".join([
            "".join([pl[len(pl)-1-j][i] for j in range(len(pl))])
            for i in range(len(pl[0]))
    ]))(p.split('\n'))
忘れずにコミットしなきゃ。まだリビジョン6なのはコミットし忘れが多いせいだ。
= 2006-11-23 02:52:04 drop_piece。while文が出てきたけど、せいぜい20回程度のループだから、 再帰呼び出しで問題ない。
drop_piece = lambda p,x,y:
   (y <= bh-len(p.split('\n')) and \
           not(collide_piece(p,x,y+1)) and \
           [drop_piece(p,x,y+1)] or [y])[0]
collide_piece。forの中で本当にreturnしようとするとちょっと面倒だけど、 この場合は実行される内容に副作用がない(と思える)ので、全部計算してから 「True in ...」でTrueのものがあるかチェック。
= 2006-11-23 03:11:28 深刻な問題。 飽きてきた。

深刻な問題2。 キムチ鍋をした後ほったらかしていた鍋の表面にびっしり白いものが! 開けたらすごく臭い! どうしよう!

とりあえず中身を捨てたが多少こびりついている。 気持ち悪いから洗いたくないので水を張って台所用漂白剤を多めに入れてみた。 吉と出るか凶と出るか。吉なら、明日の朝には分解されてなくなっている。 凶なら変な反応が起きてさらに気持ち悪いものができている。


= 2006-11-23 03:16:52 fix_piece。それglobal文いらないから、とツッコミ。
= 2006-11-23 03:23:42 そうか、なんでやる気が続かないかわかった。 今回は、改行を詰める作業は手作業でやらずに、 最後にスクリプトで不要な空白文字を取り除かせようと思っていたのだけど、 そのせいで行数が縮まないから達成感がない。今93行。

今まで書いた部分だけ余計な空白文字を取り除いてみた。 36行になった。1画面に収まった。やる気出た。

chk_board。割とめんどくさいなぁ。 そろっている行を消す処理なんだけど。 ただ消すだけならfilterでそろっているのを取り除けばいいんだけど、 まとまって消えたときにスコアが高くなって欲しいそうだ。 しかも1行と2行に分かれて消えたのはまとまって3行消えたのとは違う点数。


= 2006-11-23 03:58:13 続きの代入文は面倒だからセミコロンでつないでしまった。 トップレベルだからこれでも問題なく動く。 最後のwhile文は繰り返し回数の多いループだから itertoolsのcountとifilterを使う。 できあがり。 1行です。2695文字だけど。 お、ちょうど28時ジャスト。 今回は無駄に長い変数名などがあるのでもっと縮めるのも簡単だけど、 それはもう飽きたのでやりません。

__ 15行バージョンが見られました。80*15行の「ワンライナー」だったようです。 変数名を短いものに置き換えたり、オリジナルの関数をそのままワンライナー化しないで可能であれば展開したりという方法でかなり短くできたようです。でも「import random,pygame;」と「P=pygame;」は「import random,pygame as P;」にすればもっと縮まりますね。個人的にはfindで部分文字列が見つからなければ-1が返ってくることと、-1のビットNOTが0であることを利用して「0のない行(全部詰まった行)を取り除く」というのを「[r for r in B if~r.find('0')]」と書いているあたりがかなりクールだと感じました。

トラックバック(Trackback)

Trackback URL: http://www.nishiohirokazu.org/mt/mt-tb.cgi/434

フィードバック

by ジャンル違い | 2006年11月23日 17:34

勉強会ではお世話になりました。

私の方では、15行テトリスの方見ることができました。(23日17時 時点)

多分、たまたま落ちていたのではないでしょうか?

今試したら見ることができました。 これは1行80文字で15行ということなので、実質的にはワンライナーですね。 始める前に見られていたら自分でワンライナー化したりしなかったのに(苦笑)

ご意見・ご感想をお送りください(フィードバック)

(フィードバックはメールで送信され、基本的に表示されませんが、内容によっては公開させていただくこともございます。ご了承ください。Your comment doesn't appear the page immediately. If the comment has value to other people, it will be put on the page or subsequent entries. Thank you.)

上の情報は、いずれも未記入でかまいません。 All of above questions are optional.