夏の真っ青な空の入道雲を見てると、ふとあの頃を思いだしました。
イギリスのF1チームの車に、日本のホンダのターボエンジンが載せられ、 ブラジル人とフランス人のドライバーが同じチームで激しくぶつかってたあの頃。
当時、日本橋で中古の格安NEC PC-8801mk2SRを手に入れた僕は、 とりつかれた様に覚えたてのC言語で、巷に出始めたゲーム「テトリス」を模したプログラムを書いてました。
PC-88なので、CPUは8ビットZ80。 ストレージは5インチのフロッピードライブが2機。 OSはコンピュータクラブの先輩が改造を施したCP/Mのディスクをコピーしてもらい。 エディタはWord Masterでキーバインドはviでもemacsでもなく、ダイヤモンドカーソル。 コンパイラはBDS C。
8ビットCPUでメモリ空間は64KB、フロッピーディスクは両面倍密2Dの300KBが2枚。 この資源でC言語のプログラミングを楽しめてました。
完成したプログラムを「テテリス」と名付け、同じ機械科の友人の飯田くんに披露してみるとなかなか好評。 「ゲーセンではハマらないはずの斜下の空間に、ワープしたかのようにブロックがハマる」 というバグがあったのですが、「これが逆にスカっとして良い」と言われたのが印象的でした。
後にクラブの後輩のふくちゃんがバイトで作った某OSがあるのですが、 クラブの先輩小前さんが、部室に転がってたこの「テテリス」のソースを発掘し、 某OSでも動作するように改造して、デモに使ったとお聞きしました。
もう、元のソースはどこにあるのやら? 実家の押入れに5インチのフロッピーが残っているはずですが、果たして?
ふと「今作ってみたらどうなるか?」などと思い立ち、ちょろっとPythonで書いてみました。
いざ書いてみると、単に整数1つのグローバル変数とかが、意外に面倒な事に気付きました。
「その世界に一つ」保持しておけば良い「状態」であれば、グローバル変数として持たすとシンプルで良いですよね。
ですが、Pythonの関数でグローバル変数への代入は、注意が必要です。
グローバル変数を参照するだけなら、他の言語と同様に普通に参照できるのですが、 グローバル変数に値を代入しようとすると、関数内のローカル変数が生成され、そちらに代入されます。 グローバル変数へ値を代入する前には、あらかじめ次のように宣言せねばなりません。
global 変数名
これを書いてまわれば、まぁ問題ないのですが、、、面倒なものです。 書いてまわるのを避けて、なんとかしたいなー。
ぱっと思いつくのは辞書。
gdic = { '変数名': 値, ... }
のような辞書を用意しておけば、関数の中から
代入は gdic['変数名'] = 値 参照は gdic['変数名'] や gdic.get('変数名', デフォルト値)
グローバル変数 gdic は辞書のオブジェクトを指したままで、 代入の操作も、参照の操作も、同じ辞書のオブジェクトにアクセスしてるだけです。
ならばと、次に思いついたのはリスト。
foo = [ 値 ] bar = [ 値 ] 関数の中から 代入は foo[0] = 値 bar[0] = 値 参照は foo[0] bar[0]
グローバル変数が指しているリストオブジェクトを参照してるだけで、 グローバル変数自身の値は、同じリストオブジェクトを指したままです。
でも、これはこれで[0]を付けてまわるのも面倒なものです。
後で見て「何でリストにしてたんだろか?」とかなって、 とってみてエラー。「ああ、そうかグローバル変数!」
判りにくそうです。
ならば「クロージャ」ならばどうじゃろ?
こんな関数を用意してみました。
def gval(v=None): keep = [ v ] def set(v): keep[0] = v return lambda v='get': keep[0] if v == 'get' else set(v)
初期値を指定して呼び出すと、ローカル変数に保持しておいて、 保持してる変数にアクセスするためのクロージャ(内部関数)を返します。
内部関数は、引数なし、あるいは'get'という文字列を与えて呼び出すと、設定されてる値を返します。 'get'以外の引数を指定して呼び出すと、その値を変数に設定して更新します。
ここでもまた内部関数から、外側の変数への代入の問題があります。 この場合も単にglobal宣言すれば良いでしょうか?
内部関数からみたら外ですが、外側の関数のローカル変数なのできっとダメ。 ということで、要素1つのリストにして保持します。
このgval()を1つ用意しておいて、次のように使ってみます。
foo = gval(123) bar = gval('hoge') def fuga(): print foo(), bar() foo(999) bar( bar() + str( foo() ) ) print bar()
そんな感じで書き進めていくと、 無駄にクロージャを多用するプログラムになってしまいました。
if __name__ == "__main__": if '-h' in sys.argv: print 'Usage: {} [w10] [h20] [bw] [warp]'.format( sys.argv[0] ) sys.exit(0) try: restore = term_raw() screen_save() cursor_save() cursor(False) work() finally: cursor(True) cursor_load() screen_load() restore() # EOF
ENTERキーを押さずとも、標準入力のキー入力を取得できるよう、端末をrawモードに設定してます。
Pythonのtermioのドキュメントをあたってみると、 try節、finally節を使った例が載ってたので、そのまま従ってみました。
キー入力については以前に CUI14「基礎工事」 で、 C言語で設定してました。
また、 CUI14「ネットワーク対応」 でも、 sttyコマンドを使って、stty raw -echo とか、stty -raw echo とか書いてました。
試してないですが、Pythonのプログラムからsttyコマンドを呼び出すようにしても、うまくいったかも知れませんね。
噛み砕いてterm_raw()の実装を。
def term_raw(sigint=True): fd = sys.stdin.fileno() a = termios.tcgetattr(fd) bak = a[:] f = termios.ECHO | termios.ECHONL | termios.FLUSHO | termios.ICANON | termios.IEXTEN if not sigint: f |= termios.ISIG a[3] &= ~f termios.tcsetattr(fd, termios.TCSADRAIN, a) return lambda : termios.tcsetattr(fd, termios.TCSADRAIN, bak)
bakをグローバル変数にとって、restore用の関数を別に用意するのが、まぁ王道でしょう。 でもここは、嬉しがって無理にでもクロージャを使い、復帰させる情報は関数内のローカル変数に隠してみました。
端末の設定の後は、work()で本題の処理です。
def work(): cls() lines_add(0) drop() th_start_loop( th_key ) (sec, interval, rate) = ( gval(1.0), 3.0, 0.99 ) def move(): try_move(0, 1, 0) time.sleep( sec() ) th_start_loop(move) def accel(): time.sleep(interval) sec( sec() * rate ) th_start_loop(accel) while not over.wait(1.0): time.sleep(1.0) # for ^C
キー入力の関連箇所を少々。上流から順に追ってみます。
def work(): : th_start_loop( th_key ) : def th_key(): d = { 'l': (-1, 0, 0), 'r': (1, 0, 0), 'd': (0, 1, 0), 'u': (0, 0, 1), 'U': (0, 0, -1), } prm = d.get( keydir(1.0), [] ) if prm: try_move(*prm)
def keydir(tmout=None): ctl = lambda s: chr( 1 + ord(s) - ord('a') ) lst = [ # up, down, left, right [ esc('A'), esc('B'), esc('D'), esc('C') ], # allow [ ctl('p'), ctl('n'), ctl('b'), ctl('f') ], # emacs [ 'k', 'j', 'h', 'l' ], # vi ] d = dict( sum( map( lambda k4: zip(k4, ['u','d','l','r']), lst ), [] ) ) d.update({ '\n': 'u', '\t': 'U', ' ': 'U' }) keys = d.keys() buf = getkey(tmout) while buf and any( map( lambda k: k.startswith(buf), keys ) ): if buf in keys: return d.get(buf) buf += getkey(tmout) return '' esc = lambda s: chr(0x1b) + '[' + s
(Pythonのプログラムで書くとシンプルなのに、日本語に起こすと複雑)
def getkey(tmout=None): fd = sys.stdin.fileno() return os.read(fd, 1) if readable(fd, tmout) else '' def readable(fd, tmout=None): (r, _, _) = select.select( [fd], [], [], tmout ) return r
work() のところで、 drop()で最初のブロックを「こけら落し」て、 などと書いてた drop() の処理についてです。
起動して最初の「蹴り込み」のときと、 ブロックが着地して「固まった」ときに、 次のブロックを落しはじめる drop() を呼び出してます。
def drop(): vs = [ W/2, 0, next_v(), 0 ] curr(vs) try_move(0, 2, 0) if cy() > 0: next_v( rand_v() ) vs[2] = next_v() lst = map( lambda (x, y): ( x, y, next_v() ), pos4(*vs) ) show_lst(lst)
順に深入りしていくと、Wとは?
(W, H) = ( arg('w10'), arg('h20') )
arg()による指定は
digit_idx = lambda s: next( ( i for i in range(len(s)) if s[i].isdigit() ), len(s) ) arg_k = lambda s: s[ :digit_idx(s) ] arg_v = lambda s: s[ digit_idx(s): ] arg = lambda dv: int( arg_v( filter( lambda s: arg_k(s) == arg_k(dv) and arg_v(s), sys.argv + [dv] )[0] ) )
指定の文字列は'w10'や'h20'など、名前 + 整数 のフォーマットを期待してて、 整数の部分がデフォルト値になります。
print 'Usage: {} [w10] [h20] [bw] [warp]'.format( sys.argv[0] )
省略時は幅10、高さ20のサイズ。 例えば幅を2倍の20にしたければ、起動時に
$ ./tete17.py w20
などと指定します。
巻戻して、
vs = [ W/2, 0, next_v(), 0 ]
next_v()とは?
rand_v = lambda : random.randrange( 1, len(blk_inf) ) next_v = gval( rand_v() )
例によって「gval()の中に値を保持してて操作するクロージャを返す」 というものですが、まぁグローバル変数みたいなもんです。
で、値の初期値は乱数 rand_v() の値を指定。
rand_v() は 1 から blk_inf[] のサイズ -1 までの整数の乱数を返します。
blk_inf[] とは?
blk_inf = [ [ [], 1, '' ], [ [ (-1, 0), (1, 0), (2, 0) ], 2, 'red' ], # bar [ [ (1, 0), (0, 1), (1, 1) ], 1, 'yellow' ], # square [ [ (-1, 0), (1, 0), (0, 1) ], 4, 'cyan' ], # T [ [ (-1, 0), (0, 1), (1, 1) ], 2, 'green' ], # z [ [ (1, 0), (0, 1), (-1, 1) ], 2, 'magenta' ], # s [ [ (1, 0), (2, 0), (0, 1) ], 4, 'white' ], # L [ [ (-1, 0), (-2, 0), (0, 1) ], 4, 'blue' ],# J ]
curr(vs)
curr()とは?
curr = gvals( [ 0 ] * 4 ) (cx, cy, cv, cr) = curr('getf')
gval()じゃなくてgvals()
def gvals(lst): keep = map(gval, lst) to_lst = lambda v: v if type(v) in [ list, tuple ] else [ v ] * len(keep) return lambda v='get': keep if v == 'getf' else map( lambda (f, v): f(v), zip( keep, to_lst(v) ) )
といっても、gval()を複数個生成してリストで保持してるようなものです。
で、curr とは?
curr = gvals( [ 0 ] * 4 ) (cx, cy, cv, cr) = curr('getf')
を組にして「現在の状態」としてcurrに持たせてます。
そして、組にしておきながら、バラでも扱いやすいように、 'getf'を与えて、個別のクロージャを、 cx, cy, cv, cr としても扱えるように取り出してます。
巻戻して、
def drop(): vs = [ W/2, 0, next_v(), 0 ] curr(vs) try_move(0, 2, 0) :
ということで、最初のブロックの状態をcurr()に設定した後、 try_move(0, 2, 0)で、そいつをy方向に2コマ「ガクン」と移動させようとしてます。
if cy() > 0: next_v( rand_v() ) vs[2] = next_v() lst = map( lambda (x, y): ( x, y, next_v() ), pos4(*vs) ) show_lst(lst)
ブロックを表示する処理を、底の方から見てみます。
def out(s): sys.stdout.write(s) def flush(): sys.stdout.flush()
cls = lambda : out( esc('2J') ) loc = lambda x, y: out( esc( '{};{}H'.format(y+1, x+1) ) ) rev = lambda v: out( esc( '7m' if v else '0m' ) ) cursor = lambda v: out( esc_ex(25, v) ) cursor_save = lambda : out( esc('s') ) cursor_load = lambda : out( esc('u') ) screen_save = lambda : out( esc_ex(47, 1) ) screen_load = lambda : out( esc_ex(47, 0) ) def color(v): lst = [ 'black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white' ] if v in lst: out( esc( '{}m'.format( 30 + lst.index(v) ) ) ) esc = lambda s: chr(0x1b) + '[' + s resc_ex = lambda d, v: esc( '?{}{}'.format(d, 'h' if v else 'l' ) )
def show(x, y, v): loc( x*2, y ) rev(v > 0) s = ' ' if v > 0: if BW: s = col[v][0].upper() + ' ' else: color( col[v] ) out(s)
BW とは?
(BW, WARP) = map( lambda k: k in sys.argv, ['bw', 'warp'] ) : if '-h' in sys.argv: print 'Usage: {} [w10] [h20] [bw] [warp]'.format( sys.argv[0] ) sys.exit(0)
if BW: s = col[v][0].upper() + ' ' else: color( col[v] )
して、col とは?
blk_inf = [ [ [], 1, '' ], [ [ (-1, 0), (1, 0), (2, 0) ], 2, 'red' ], # bar [ [ (1, 0), (0, 1), (1, 1) ], 1, 'yellow' ], # square [ [ (-1, 0), (1, 0), (0, 1) ], 4, 'cyan' ], # T [ [ (-1, 0), (0, 1), (1, 1) ], 2, 'green' ], # z [ [ (1, 0), (0, 1), (-1, 1) ], 2, 'magenta' ], # s [ [ (1, 0), (2, 0), (0, 1) ], 4, 'white' ], # L [ [ (-1, 0), (-2, 0), (0, 1) ], 4, 'blue' ],# J ] (pos3, rot_n, col) = zip(*blk_inf)
ずるずるっと、大物が引き揚がりました。
col[v] だけ見るならば、ブロックの種類 v (1から7) の、 色の文字列 ( 'red', 'yellow', ... 'blue' ) です。
ついでに blk_inf = [ ... ] 全体について
このblk_infを直接使って、赤い棒のヤツの色ならば、
v = 1 blk_inf[v][2]
などと参照できますが、、、
(pos3, rot_n, col) = zip(*blk_inf)
として、columnごとに1次元の配列になおしておいて
col[v]
としても参照できるようにしてます。
で、で、巻戻して、
if BW: s = col[v][0].upper() + ' ' else: color( col[v] )
では color() ?
def color(v): lst = [ 'black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white' ] if v in lst: out( esc( '{}m'.format( 30 + lst.index(v) ) ) )
またしても、巻戻して、
def show(x, y, v): loc( x*2, y ) rev(v > 0) s = ' ' if v > 0: if BW: s = col[v][0].upper() + ' ' else: color( col[v] ) out(s)
out(s) で2文字を表示してshow()おしまい。
というshow()の処理の全貌です。
そして、この show() を呼び出して使っているのが、直後の show_lst()。
def show_lst(lst): map( lambda (x, y, v): show(x, y, v), lst ) flush()
show_lst()を呼び出しているのは2箇所で、update()からと、drop()から。
drop()からは、
def drop(): vs = [ W/2, 0, next_v(), 0 ] curr(vs) try_move(0, 2, 0) if cy() > 0: next_v( rand_v() ) vs[2] = next_v() lst = map( lambda (x, y): ( x, y, next_v() ), pos4(*vs) ) show_lst(lst)
そして、満を持して show_lst(lst) でブロックを表示。
つまり、pos4(x, y, ブロックの種類, 回転の状態) は、その状態のブロックを構成する、4つの四角について (x, y)座標をリストで返します。
pos4()の中は?
def pos4(x, y, v, r): r %= rot_n[v] rot = lambda (x, y), n=r: (x, y) if n <= 0 else rot( (-y, x), n-1 ) p4 = map( rot, pos3[v] ) + [ (0, 0) ] return map( lambda (px, py): ( x + px, y + py ), p4 )
rot_n[], pos3[] については
blk_inf = [ [ [], 1, '' ], [ [ (-1, 0), (1, 0), (2, 0) ], 2, 'red' ], # bar [ [ (1, 0), (0, 1), (1, 1) ], 1, 'yellow' ], # square [ [ (-1, 0), (1, 0), (0, 1) ], 4, 'cyan' ], # T [ [ (-1, 0), (0, 1), (1, 1) ], 2, 'green' ], # z [ [ (1, 0), (0, 1), (-1, 1) ], 2, 'magenta' ], # s [ [ (1, 0), (2, 0), (0, 1) ], 4, 'white' ], # L [ [ (-1, 0), (-2, 0), (0, 1) ], 4, 'blue' ],# J ] (pos3, rot_n, col) = zip(*blk_inf)
だったので、2カラム目の回転の状態の数と、1カラム目の 「原点以外の3つの四角の、原点からの相対座標」になります。
pos4()に戻って
show_lst()を呼び出しているのは2箇所のうち、update()からの方は?
def update(p4, n4): lst = map( lambda (x, y): ( x, y, cv() ), filter( lambda a: a not in p4, n4 ) ) lst += map( lambda (x, y): (x, y, 0), filter( lambda a: a not in n4, p4 ) ) lst = sorted( lst, key=lambda (x, y, v): x ) show_lst(lst) arr_p4(p4, EMP) arr_p4(n4, CURR)
arr_p4() とは?
ブロックを消去したり描画したりして移動するにあたり、 既存のブロックがどのように積み上がっているかが判らないと、 正しく移動したり、新たにブロックを積むことができません。
8 bitパソコンの時代ならば、 「表示するために直接VRAMにライトし」 「表示の状態を取得するために、直接VRAMをリードする」 てな事をマシン後でゴリゴリ書いてたのでしょうが、、、
まぁ表示の状態を記録する「画面のバッファ」を用意しておいて、 描画と同時に、画面のバッファにも書き込み、 状態を取得したければ、画面のバッファを読み出せばいいでしょう。
その「画面のバッファ」へのアクセスが arr_p4() です。
(EMP, CURR) = (0, -1) garr = map( lambda _: [ EMP ] * W, range(H) ) def arr(x, y, v='get'): if not ( x in range(W) and y in range(H) ): return None if v == 'get': return garr[y][x] garr[y][x] = v arr_p4 = lambda p4, v='get': map( lambda (x, y): arr(x, y, v), p4 )
巻戻して
def update(p4, n4): lst = map( lambda (x, y): ( x, y, cv() ), filter( lambda a: a not in p4, n4 ) ) lst += map( lambda (x, y): (x, y, 0), filter( lambda a: a not in n4, p4 ) ) lst = sorted( lst, key=lambda (x, y, v): x ) show_lst(lst) arr_p4(p4, EMP) arr_p4(n4, CURR)
udpate() で show_lst() で表示を更新した後の、 最後に arr_p4(p4, EMP), arr_p4(n4, CURR)は、 画面バッファ側の更新になります。
移動前の4つの四角の位置に EMP (空)、 値0 を設定してから、 移動後の4つの四角の位置に CURR (現在)、 値-1 を設定してます。
こちらはバッファなので画面のチラツキは関係ありません。 なので、ゴニョゴニョ複雑な事をして更新しなくてもいいですね。
この update() を呼び出しているのは一箇所で try_move() からです。
def try_move(dx, dy, dr): move_lock.acquire() if WARP and dx == 0 and pend_dx(): try_move( dx + pend_dx(), dy, dr ) p4 = curr_pos4() n4 = curr_pos4(dx, dy, dr) if all( map( lambda v: v in [ 0, -1 ], arr_p4(n4) ) ): update(p4, n4) curr( curr_after(dx, dy, dr) ) timer('cancel') pend_dx(0) elif dy > 0: timer('start') elif dx != 0: pend_dx(dx) move_lock.release()
def curr_after(dx, dy, dr): (x, y, v, r) = curr() rn = rot_n[v] return ( x + dx, y + dy, v, (r + dr + rn) % rn ) curr_pos4 = lambda dx=0, dy=0, dr=0: pos4( *curr_after(dx, dy, dr) )
下請けのcurr_after(dx, dy, dr)では
巻戻して
: if all( map( lambda v: v in [ 0, -1 ], arr_p4(n4) ) ): update(p4, n4) curr( curr_after(dx, dy, dr) ) timer('cancel') pend_dx(0) elif dy > 0: timer('start') elif dx != 0: pend_dx(dx)
try_move()からの2つの謎、timer()と、pend_dx()ですが、 pend_dx()はあまり本質では無いので保留して、timer()をば。
def timer_new(sec, f): tmr = gval() def tmr_f(): tmr(None) f() def ret_f(cmd=''): if not tmr() and cmd == 'start': tmr( threading.Timer(sec, tmr_f) ) tmr().start() elif tmr() and cmd == 'cancel': tmr().cancel() tmr(None) return tmr() return ret_f timer = timer_new( 1.0, fix )
try_move()から、移動不可で、かつ落下方向の移動をしようとしていたのであれば、 「謎のtimer('start')呼び出し」で、1.0秒後にfix()を呼び出すたタイマーをスタートしてます。
これはつまり、ブロックが下に落ちて、動かない状態に固まるまで1秒の猶予を与えております。
この猶予期間中に、左右キーでブロックを横にズラすなどして移動すれば、 timer('cancel')呼び出しで、タイマーをキャンセルして、元の状態に戻ります。
動けないまま1秒が経過してfix()の呼び出しがなされると、ブロックがその位置で固まり、 次のブロックの「こけら落とし」となるわけです。
def fix(): move_lock.acquire() arr_p4( curr_pos4(), cv() ) ys = filter( lambda y: all( map( lambda x: arr(x, y) > 0, range(W) ) ), range(2, H) ) if ys: bikabika(ys) map(dosun, ys) lines_add( len(ys) ) drop() if cy() == 0: over.set() move_lock.release()
def bikabika(ys): draw = lambda y: ( loc(0, y), out( ' ' * W ), flush() )[-1] bika = lambda i: ( rev(i%2), map(draw, ys), time.sleep(0.1) )[-1] map( bika, range(3*2) )
def dosun(y): if y >= 2: xvs = map( lambda x: (x, arr(x, y-1) if y > 2 else 0 ), range(W) ) map( lambda (x, v): ( arr(x, y, v), show(x, y, v) )[-1], xvs ) dosun(y-1)
「th_start_loop()でスレッドを生成して、上記の関数xxx()を繰り返し実行します」 的な前ふりが多々ありましたので、その実体をば。
def th_start_loop(f): def loop(): while True: f() th = threading.Thread( target=loop ) th.daemon = True th.start()
daemon とは?
デフォルトは False で、メインスレッドは False。 「デーモンでない生存中のスレッドが全てなくなると、Pythonプログラム全体が終了します。」 とう事らしいので、、、
逆に、生成するスレッドが daemon False のままだと、 生成したスレッドが生きてる限り、プログラム全体が終了できないってことですよね。
今回の場合メインスレッドが終了すれば、プログラム全体が終了して欲しいので、 追加で生成するスレッドは、全て daemon True に設定してます。