2010 02 06

テトリスを作るの6

更にテトリス。

最初はテーブルのtdに対応するようにCellオブジェクトを定義して、その配列の配列をフィールドにすればいいと思っていたのだが、何か無駄っぽいのでやめ。 tdに直接にメソッドを追加することにした。

追加するメソッドも変更。 最初は、 getState() setState( state ) を考えていたのだが、設定は設定したい状態を、取得は判定風にしてみた。 isEmpty() とか。

そんなtdを配列につめて行に。 行をまた配列につめてフィールドに。

コードを書いてるうちに有ると便利だと気付いて、行に copy( sourceLine ) all( method ) というメソッドを追加した。 前者は、引数で指定した行をコピーする。 後者は、引数で指定した判定メソッドが行の全ての画素に対して真であることを判定する。

// フィールド function Field( controller ) { var self = this; var field = []; var STATE = { EMPTY : 0, BLOCK : 1, TETRIMINO : -1 }; Array.prototype.each.call( document.getElementById( 'field' ).getElementsByTagName( 'tr' ), function ( tr ) { var line = []; Array.prototype.each.call( tr.getElementsByTagName( 'div' ), function ( div ) { div.setState = function ( state, color ) { this.state = state; this.style.backgroundColor = ( color || 'black' ); return this; }; div.block = function ( color ) { return this.setState( STATE.BLOCK, color ); }; div.tetrimino = function ( color ) { return this.setState( STATE.TETRIMINO, color ); }; div.empty = function () { return this.setState( STATE.EMPTY ); }; div.isBlock = function () { return this.state === STATE.BLOCK; }; div.isEmpty = function () { return this.state === STATE.EMPTY; }; div.getColor = function () { return this.style.backgroundColor; }; line.push( div.empty() ); } ); line.all = function ( method ) { for ( var i = 0; i < this.length && this[ i ][ method ](); i++ ); return this.length === i; }; line.copy = function ( sourceLine ) { for ( var i = 0; i < this.length; i++ ) { if ( sourceLine && sourceLine[ i ].isBlock() ) { this[ i ].block( sourceLine[ i ].getColor() ); } else { this[ i ].empty(); } } return this; }; field.push( line ); } ); this.field = field; this.tetrimino = null; this.init = function () { _clear(); _setNewTetrimino(); }; this.turn = function () { _moveIfAllowed( self.tetrimino.turn() ); }; this.left = function () { _moveIfAllowed( self.tetrimino.move( [ 0,-1 ] ) ); }; this.right = function () { _moveIfAllowed( self.tetrimino.move( [ 0, 1 ] ) ); }; this.down = function () { if ( !_moveIfAllowed( self.tetrimino.move( [ 1, 0 ] ) ) ) { _block(); _setNewTetrimino(); } }; this.drop = function () { while ( _moveIfAllowed( self.tetrimino.move( [ 1, 0 ] ) ) ) ; _block(); _setNewTetrimino(); }; function _clear() { self.field.each( function ( line ) { line.each( function ( cell ) { cell.empty(); } ); } ); }; var center = Math.floor( field[ 0 ].length / 2 - 1 ); function _setNewTetrimino() { if ( !_moveIfAllowed( self.tetrimino = new Tetrimino( [ 0, center ] ) ) ) { controller.stop(); } }; function _moveIfAllowed( tetrimino ) { if ( _allowed( tetrimino ) ) { _move( tetrimino ); return true; } return false; } function _allowed( tetrimino ) { var allowed = true; try { tetrimino.cells.each( function ( cell ) { if ( self.field[ cell[ 0 ] ][ cell[ 1 ] ].isBlock() ) { allowed = false; } } ); } catch ( e ) { allowed = false; } return allowed; } function _move( tetrimino ) { _letTetrimino( self.tetrimino, 'empty' ); _letTetrimino( self.tetrimino = tetrimino, 'tetrimino' ); } function _block() { _letTetrimino( self.tetrimino, 'block' ); _deleteLine(); } function _letTetrimino( tetrimino, method ) { tetrimino.cells.each( function ( cell ) { self.field[ cell[ 0 ] ][ cell[ 1 ] ][ method ]( tetrimino.color ); } ); } function _deleteLine() { var tetriminoLines = []; self.tetrimino.cells.each( function ( cell ) { tetriminoLines[ cell[ 0 ] ] = self.field[ cell[ 0 ] ].all( 'isBlock' ); } ); var count = 0; tetrminoLines.each( function ( isFilled, line ) { if ( isFilled ) { count++; for ( var i = line; i >= 0 && !self.field[ i ].copy( self.field[ i - 1 ] ).all( 'isEmpty' ); i-- ); } } ); if ( count ) { controller.report( count ); } } };

すっきり書きたいからと導入した each に囚われて、途中で切り上げればいいループを最後までいってたりするのが、ちょっと気になるところだな。 まあ、ここで扱う配列は要素の数が少ないから、途中で切り上げる効果は大したことは無いだろうけど、なんか、ねぇ。

あと、テトリミノが存在できるかどうかの判定に例外処理を併用しているのも、すっきりしない気分。 ここは、

// 例外を使わない判定処理 function _allowed( tetrimino ) { var allowed = true; tetrimino.cells.each( function ( cell ) { if ( self.field[ cell[ 0 ] ] == undefinde || self.field[ cell[ 0 ] ][ cell[ 1 ] ] == undefined || self.field[ cell[ 0 ] ][ cell[ 1 ] ].isBlock() ) { allowed = false; } } ); return allowed; }

とした方がいいんじゃないかって気がする。 だったらそうすりゃいいようなものだが… うーん…

そうそう、each みたいな配列用のユーティリティ関数を作った時によく嵌るんだけど、getElementsByTagName が返すDOMの要素列って、擬似配列なんだよね。 配列として扱えるけど、配列ではない。 なので、Array の prototype にメソッドを追加しても、DOMの要素列からは使えない。 しかたが無いので、

// call で無理矢理に適用 Array.prototype.each.call( document.getElementsByTagName( 'td' ), function ( td ) { … } );

としてやる。 いつだったか、call や apply の使い方が解らなくて悩んだことがあったけど、こーゆーところで使えばいいのだな。

続きはまた明日。