テトリスを作り直すの2
昨日の続き。
フィールドもテトリミノも1次元で扱うとき、困るのは壁際の判定。
2次元版は、行をオブジェクトとして持っていたから、その行内に収まっているかどうかで移動可能かどうかの判断ができた。 でも、1次元だと全部繋がっているから、行を跨がったかどうかを自分で判断しなきゃいけないんだよな。 やっぱり、壁となるセルを導入するか。 いわゆる番兵。 でも、壁が厚いのは見た目が気に入らないんだよな。 前回、壁を作らなかったのも、それが理由だし。
と、ちょっと悩んで、壁を表示しなきゃいいだけだってことに気がついた。 うーん…何でこの程度のことに気付かなかったんだろう。 呆けたのか。 ま、今更だけど、気付いたんだからいいか。
壁は、上下の有効範囲判定にも使える。 テトリミノがフィールドの有効範囲からはみ出すのは、I型の回転の有効判定で最大2つ分。 よって、上下には2行分の壁があればいい。 左右は、壁さえあれば厚さは関係無いから、1列あればいいだろう。 幅、高さ、上下と左右の壁の厚さ、それぞれ定義する。 ついでに、最初の有効行の先頭位置と、テトリミノ初期表示用のオフセット位置も。
// 定数定義
C = {
WIDTH : 12, // 壁を含めた幅
HEIGHT : 24, // 壁を含めた高さ
V_WALL : 2, // 上下の壁の厚さ
H_WALL : 1 // 左右の壁の厚さ
};
C.FIRSTLINE_HEAD = C.WIDTH * C.V_WALL;
C.INITIAL_OFFSET = Math.floor( C.FIRSTLINE_HEAD + C.WIDTH * 1.5 ) - 1;
この定数を使って、テトリミノのパターンも再定義する。
// テトリミノのパターン定義
TETRIMINOS = [
{ cells : [ -1, 0, 1, 2 ], color : 'cyan' }, // I
{ cells : [ 0, 1, C.WIDTH, C.WIDTH + 1 ], color : 'yellow' }, // O
{ cells : [ 0, 1, C.WIDTH - 1, C.WIDTH ], color : 'green' }, // S
{ cells : [ -1, 0, C.WIDTH, C.WIDTH + 1 ], color : 'red' }, // Z
{ cells : [ -1, 0, 1, C.WIDTH + 1 ], color : 'blue' }, // J
{ cells : [ -1, 0, 1, C.WIDTH - 1 ], color : 'orange' }, // L
{ cells : [ -1, 0, 1, C.WIDTH ], color : 'magenta' } // T
];
TETRIMINOS.MAX_R = TETRIMINOS[ 0 ].cells.length - 1;
TETRIMINOS.get = function () {
return this[ Math.floor( Math.random() * TETRIMINOS.length ) ];
};
TETRIMINOS.get は、ランダムにパターンを返すメソッド。 新しいテトリミノが必要になった時に呼ぶ。
そしてフィールド。 処理を1次元配列用に書き直し、ついでに分け過ぎっぽい関数を纏め直したり。
// フィールド
function Field( controller ) {
var self = this;
var STATE = {
EMPTY : 0,
BLOCK : 1,
WALL : 2
};
this.field = [];
Array.prototype.each.call( document.getElementById( 'field' ).getElementsByTagName( 'div' ), function ( div, i ) {
div.get = function () {
return {
state : this.state,
color : this.style.backgroundColor
};
};
div.set = function ( value ) {
this.state = value.state || STATE.EMPTY;
this.style.backgroundColor = value.color || 'black';
return this;
};
div.is = function ( state ) {
return this.state === state;
};
div.lineIs = i % C.WIDTH !== 0 ? null : function ( state ) {
for ( var j = C.H_WALL; j < C.WIDTH - C.H_WALL; j++ ) {
if ( !self.field[ i + j ].is( state ) ) {
return false;
}
}
return true;
};
div.lineCopy = i % C.WIDTH !== 0 ? null : function ( sourceLineHead ) {
for ( var j = C.H_WALL; j < C.WIDTH - C.H_WALL; j++ ) {
self.field[ i + j ].set( sourceLineHead + j > C.FIRSTLINE_HEAD ? self.field[ sourceLineHead + j ].get() : {} );
}
return this;
};
self.field.push( div.set( { state : div.parentNode.nodeName.toUpperCase() == 'TH' ? STATE.WALL : STATE.EMPTY } ) );
} );
this.tetrimino = TETRIMINOS.get();
this.offset = C.INITIAL_OFFSET;
this.init = function () {
self.field.each( function ( cell ) {
if ( !cell.is( STATE.WALL ) ) {
cell.set( {} );
}
} );
_setNewTetrimino();
};
this.turn = function () {
var turned = {
cells : [],
color : self.tetrimino.color
};
self.tetrimino.cells.each( function ( cell ) {
turned.cells.push( Math.floor( ( cell + TETRIMINOS.MAX_R ) / C.WIDTH ) - ( C.WIDTH * ( ( cell + TETRIMINOS.MAX_R * C.WIDTH + TETRIMINOS.MAX_R ) % C.WIDTH ) ) + TETRIMINOS.MAX_R * C.WIDTH );
} );
_move( turned, self.offset );
};
this.left = function () {
_move( self.tetrimino, self.offset - 1 );
};
this.right = function () {
_move( self.tetrimino, self.offset + 1 );
};
this.down = function () {
if ( !_move( self.tetrimino, self.offset + C.WIDTH ) ) {
_block();
}
};
this.drop = function () {
while ( _move( self.tetrimino, self.offset + C.WIDTH ) ) ;
_block();
};
function _setNewTetrimino() {
if ( !_move( self.tetrimino = TETRIMINOS.get(), self.offset = C.INITIAL_OFFSET ) ) {
controller.stop();
}
};
function _move( tetrimino, offset ) {
for ( var i = 0; i < tetrimino.cells.length; i++ ) {
if ( !self.field[ tetrimino.cells[ i ] + offset ].is( STATE.EMPTY ) ) {
return false;
}
}
_letTetrimino( self.tetrimino, self.offset, STATE.EMPTY, 'black' );
_letTetrimino( self.tetrimino = tetrimino, self.offset = offset, STATE.EMPTY );
return true;
}
function _block() {
_letTetrimino( self.tetrimino, self.offset, STATE.BLOCK );
_deleteLines();
_setNewTetrimino();
}
function _letTetrimino( tetrimino, offset, state, color ) {
tetrimino.cells.each( function ( cell ) {
self.field[ cell + offset ].set( {
state : state,
color : color || tetrimino.color
} );
} );
}
function _deleteLines() {
var head = self.tetrimino.cells.sort().reverse()[ 0 ] + self.offset;
for ( var count = 0, checked = 0, empty = 0, lineHead = head - head % C.WIDTH; lineHead > C.FIRSTLINE_HEAD; checked++ ) {
var checkLineHead = lineHead - C.WIDTH * count;
if ( checked < self.tetrimino.cells.length && checkLineHead > C.FIRSTLINE_HEAD && self.field[ checkLineHead ].lineIs( STATE.BLOCK ) ) {
count++;
continue;
}
if ( count ) {
if ( ( empty = self.field[ lineHead ].lineCopy( checkLineHead ).lineIs( STATE.EMPTY ) ? empty + 1 : 0 ) > count ) {
break;
}
}
lineHead -= C.WIDTH;
}
if ( count ) {
controller.report( count );
}
}
};
1次元配列にする他に大きく変えたのが、行を削除する _deleteLines() の処理。
前のは、テトリミノの各要素が存在している行がブロックで埋まっているかどうかを調べ、埋まっている各行に対し、その行より上を全て一つ下にずらすという処理を繰り返していた。 今度のは、テトリミノの一番下の行から上に向かって、各行1回だけ、埋まっているかどうかの判定とずらす処理をするようにしている。 ちなみに 「ずらす」 のは、上の行を下の行にコピーすることで実現している。 判り難いのが、無駄を省くための部分。
checked は、行が埋まっているかどうかの判定をした回数。 埋まっているかどうかの判定が不要な行にまで判定しないために使う。 消える行は最大でテトリミノの要素数と同じだから、判定はこの数だけやればいいのだな。
empty は、コピーした行が空白行だったとき、何行連続して空白行だったかの数。 下にずらす処理をどこまで続けるかの判定に使う。 上が全部空白行になったら、ずらす意味が無いからしないのだ。
これらの処理は、いずれも 「テトリミノは隣接した要素から成る」 という前提で成り立っている。 いや、4×4の領域内であれば別に隣接してなくてもいいのか。 この領域内からランダムに4つ選んだ特殊テトリミノが、不定期に混ざるのも面白いかもしれない。
しかし、やたら長い行がちらほらあるのが気になるな。 もっとすっきり書けないものか。 でも、ちまちま小分けするのも趣味じゃないんだよなぁ。
得点表示は前回のままなので省略。 コントローラーもほぼそのままなのだが、若干くどかった記述と、ゲーム終了付近の処理をちょっと見直した。
// コントローラー
function Controller() {
var self = this;
var INTERVAL = {
INIT : 1000,
MIN : 100,
DIFF : 10
};
var field = new Field( this );
var score = new Score();
var timerId = null;
var started = false;
var interval = INTERVAL.INIT;
this.start = function () {
if ( !started ) {
document.getElementById( 'start' ).disabled = true;
score.init();
field.init();
document.onkeyup = function ( e ) {
switch ( String.fromCharCode( e ? e.keyCode : event.keyCode ).toUpperCase() ) {
case 'H' : field.left(); break;
case 'K' : field.right(); break;
case 'J' : field.turn(); break;
case ' ' : field.drop(); break;
} };
interval = INTERVAL.INIT;
started = true;
_proceed();
}
};
this.stop = function () {
document.onkeyup = null;
clearTimeout( timerId );
alert( "終了" );
document.getElementById( 'start' ).disabled = false;
started = false;
};
this.report = function ( deletedLines ) {
score.countUp( deletedLines );
if ( ( interval -= ( INTERVAL.DIFF * deletedLines ) ) < INTERVAL.MIN ) {
interval = INTERVAL.INIT;
}
};
function _proceed() {
if ( started ) field.down();
if ( started ) timerId = setTimeout( _proceed, interval );
}
}
テトリミノのパターン定義を除いた処理が、前のに比べて50行ぐらい短くなった。 が、やたら長い行が増えてもいて、あんまりすっきりした気がしない。 どうしたものか。