これまたいつものことだけど、それなりに一通り考えたつもりでも、実際に作ってみると見落としがが結構あるんだよな。 ま、それはそれで楽しいのだが。
ところで、spider の場合に問題になるのはテスト。 テストのためにゲームを実行しているはずが、ゲームに夢中になってテストが疎かになってしまうのだ。 割と頻繁に本末転倒。 って、これは spider じゃなくて俺の問題か。
さて、コード。
// カード供給元
function CardSet() {
var parent = document.getElementById( 'field' );
var cards = [];
for ( var i = 0; i < 2; i++ ) { // カード2セット
for ( var mark in CARD.MARKS ) {
for ( var n = 0; n < CARD.NUMBERS.length; n++ ) {
cards.push( ( new Card( mark, n ) ).setParent( parent ) );
}
}
}
this.getCards = function () {
return cards.random();
};
this.getDummy = function () {
return ( new Card() ).setParent( parent );
};
}
カードを供給するためのオブジェクト。
難易度最大で固定とするため、カードも ♥ ♠ ♦ ♣ 各13枚を2セットで固定している。 これを最初にまとめて作って、以後は新規作成せず使い回す。 カードの親要素を field にしているのは、列を親にすると管理が面倒になりそうだから。 ゲーム中、カードは頻繁に列を移動することになるからね。
シャッフルしたカードを返す。
ダミーカード(マークも数字も無いカード)を1枚返す。
// 列
function Line( lineNo, dummyHeadCard, controller ) {
var self = this;
var POS = {
left : 10 + lineNo * 70,
top : 120,
vGap : 24,
nextTop : function () {
return POS.top + ( myCards.length - 1 ) * POS.vGap;
}
};
var myCards = [ dummyHeadCard.setPosition( POS.left, POS.top ).close() ];
this.clear = function () {
while ( myCards.length > 1 ) {
var card = myCards.pop();
if ( card.isOpen() ) {
card.close().removeEventListener( 'click', _select, false );
}
}
};
this.init = function ( cards ) {
cards.each( function ( card ) {
myCards.push( card.close().setPosition( POS.left, POS.nextTop() ).setZIndex( myCards.length ) );
} );
};
this.isEmpty = function () {
return myCards.length === 1;
};
this.isOpen = function ( no ) {
return myCards[ myCards.length - no ].isOpen();
};
this.canAdd = function ( cards ) {
if ( self.isEmpty() ) {
return cards.length;
}
var nextNumber = myCards.tail().getNumber() - 1;
return cards.length - cards.doWhile( function ( card ) {
return card.getNumber() !== nextNumber;
} );
};
this.add = function ( cards ) {
cards.each( function ( card ) {
card.setPosition( POS.left, POS.nextTop() ).setZIndex( myCards.length );
myCards.push( card.open().addEventListener( 'click', _select, false ) );
} );
if ( self.canGet(1).length === 13 ) {
controller.fin( lineNo );
}
};
this.canGet = function ( order ) {
if ( self.isEmpty() ) {
return [];
}
var selected = myCards.slice( order ? order : myCards.length - 1 ).reverse();
var getable = [ selected.shift() ];
selected.doWhile( function ( card ) {
if ( card.isOpen() &&
getable[ 0 ].getMark() === card.getMark() &&
getable[ 0 ].getNumber() === card.getNumber() - 1 ) {
getable.unshift( card );
return true;
}
return false;
} );
return getable;
};
this.get = function ( n ) {
var cards = [];
for ( var i = 0; i < n; i++ ) {
cards.unshift( myCards.pop().removeEventListener( 'click', _select, false ) );
}
if ( !myCards.tail().isOpen() ) {
myCards.tail().open().addEventListener( 'click', _select, false );
}
return cards;
}
this.focus = function ( focused ) {
myCards.tail().focus( focused );
};
this.closeTop = function () {
if ( myCards.tail().isOpen() ) {
myCards.tail().close().removeEventListener( 'click', _select, false );
}
};
function _select( e ) {
controller.select( lineNo, this.getZIndex() );
}
}
列を表現するオブジェクト。 何列目なのかを lineNo で指定する。
列内のカードに対し、列番号と列位置とで表示する座標を決める。 また、置いた順番と zIndex が一致するように設定し、これによってクリックされたカードの列内位置を取得する。
表向きのカードにはクリックイベントを設定し、イベント処理の中で、列番号と列内位置をコントローラーに通知する。 列の末尾(一番奥)にはダミーカードを置いて、列が空になったときの処理をさせる。
列にある全てのカードを削除する。 削除するカードにイベントが設定されている場合は解除する。
ゲーム初期化時に実行する。 引数は、裏向きに初期配置するカード。
列が空(ダミーカードだけ)のとき true を返す。
列の先頭から no 番目のカードが表向きのとき true を返す。
列への追加候補として渡されたカードがこの列に置けるかどうかを判定し、判定結果として置ける枚数を返す。
列が空なら全て置ける。 空ではない場合は 「自分のカードの一番上 - 1 = 移動候補のカードのどれか」 が成り立てば、追加候補の先頭からその位置までのカードを置けるものとする。
列にカードを追加する。 このとき、カードの一番下を1として、連番になるように zIndex を設定する。
カードを追加したら、先頭から13枚揃っているかどうかを確認する。 揃っている場合は、コントローラーに通知する。
列から削除可能なカードを返す。
削除できるのは、カードの先頭から同じマークで連番の続く範囲。 引数 order を指定した場合は、列の先頭からその位置までを検査対象とする。 order は、列のカードに設定した zIndex と同じで、1が一番奥を意味する。
先頭から n 枚のカードを削除し、削除したカードを返す。 このとき、削除するカードに設定されているイベントは解除する。 また、削除後の列の先頭のカードが裏向きだった場合は、表向きにしてイベントを設定する。
列の先頭のカードの選択中表示を切り替える。
列の先頭のカードが表向きの場合、裏向きにしてイベントを解除する。 一手戻すとき、カードを裏向きにする必要がある場合に呼ばれる。
// 未配カード用
function Yet( controller ) {
var POS = {
left : 640,
top : 10
};
var myCards = [];
this.clear = function () {
if ( myCards.length ) {
myCards.tail().removeEventListener( 'click', controller.deal, false );
myCards = [];
}
}
this.init = function ( cards ) {
cards.each( function ( card ) {
myCards.push( card.setPosition( POS.left, POS.top ).close().setZIndex( myCards.length ) );
} );
myCards.tail().addEventListener( 'click', controller.deal, false );
};
this.get = function ( n ) {
myCards.tail().removeEventListener( 'click', controller.deal, false );
var cards = myCards.slice( myCards.length - n );
myCards.splice( myCards.length - n, n );
if ( myCards.length ) {
myCards.tail().addEventListener( 'click', controller.deal, false );
}
return cards;
};
}
列に配っていないカードを持たせるオブジェクト。
裏向きに積んだカードの一番上に、クリックイベントを設定する。 このイベント処理の中で、コントローラーのカードを配る処理を呼ぶ。
残っている全てのカードを削除する。 カードの先頭に設定しているイベントは解除する。
ゲーム初期化時に実行する。 引数は、裏向きに初期配置するカード。 カードの先頭にイベントを設定する。
先頭から n 枚のカードを取り出す。 取り出したカードの先頭に設定していたイベントは解除する。 取り出した後、まだ残りがあれば、その先頭にイベントを設定する。
コントローラーのカードを配る処理の中で呼ばれるのだが、呼び出しが相互になってしまうのが微妙。
// 完了カード用
function Fin( controller ) {
var POS = {
left : 10,
top : 10,
hGap : 30,
nextLeft : function () {
return POS.left + POS.hGap * Math.floor( myCards.length / 13 );
}
};
var myCards = [];
this.clear = function () {
myCards = [];
};
this.add = function ( cards ) {
cards.reverse().each( function ( card ) {
card.setPosition( POS.nextLeft(), POS.top ).setZIndex( myCards.length );
myCards.push( card );
} );
}
}
13枚揃ったカードを置いておくオブジェクト。 何をするでもない、ただカードを保持しておくだけ。
持っている全てのカードを削除する。
カードを追加する。
ここのカードは全て表向きなのだが、渡されるカードはどこかの列で13枚揃ったものなので、既に表向きになっているはず。
// 勝敗管理
function Score() {
var win = new _score( 'success' );
var lose = new _score( 'fail' );
win.set( win.get() );
lose.set( lose.get() );
this.win = function () {
win.set( win.get() + 1 );
};
this.lose = function () {
lose.set( lose.get() + 1 );
};
this.clear = function () {
win.set( 0 );
lose.set( 0 );
};
function _score( id ) {
var key = 'spider_' + id;
this.get = function () {
return parseInt( window.localStorage[ key ] || 0 );
};
this.set = function ( value ) {
document.getElementById( id ).firstChild.nodeValue = window.localStorage[ key ] = value;
};
}
}
勝敗を管理するオブジェクト。
勝敗の記録を保持するのに、折角なので HTML5 の localStrage を使ってみた。 localStorage は key-value タイプで、key という名前で値 value を保存するには
window.localStrage[ key ] = value;
また、保存した key に対応する値を取得するには
value = window.localStrage[ key ];
とまあ簡単で使いやすい。 key が重複しないように気をつける必要があるが、それはまあ頭に 「spider_」 とか付けておけばいいだろう。 欠点は、ちょっと古いブラウザ(主にIE)が対応していないことだが、それはすぐに問題無くなるはず。 だよな?
勝ち数を1増やす。
負け数を1増やす。
勝ち負けともに0にする。
// コントローラー
function Controller() {
var self = this;
var cardSet = new CardSet();
var yet = new Yet( this );
var fin = new Fin( this );
var score = new Score();
var lines = [];
for ( var l = 0; l < 10; l++ ) {
lines[ l ] = new Line( l, cardSet.getDummy(), this );
}
var lastLine = -1;
var lastOrder = 0;
var history = [];
var win = false;
this.clearScore = function () {
confirm( 'これまでの勝敗を無しにする' ) && score.clear();
};
this.clear = function () {
lines.each( function ( line ) {
line.clear();
} );
fin.clear();
yet.clear();
if ( win ) {
win = false;
} else {
score.lose();
}
return self;
};
this.init = function () {
yet.init( cardSet.getCards() );
for ( var i = 0; i < lines.length; i++ ) {
lines[ i ].init( yet.get( i < 4 ? 5 : 4 ) );
}
return self;
};
this.deal = function () {
if ( lines.all( function ( line ) {
return !line.isEmpty();
} ) ) {
if ( lastLine !== -1 ) {
lines[ lastLine ].focus( false );
lastLine = -1;
lastOrder = 0;
}
lines.each( function ( line ) {
line.add( yet.get( 1 ) );
} );
history = [];
} else {
alert( '空列があるとカードを配れない' );
}
};
this.select = function ( lineNo, order ) {
if ( lastLine === -1 ) {
lastLine = lineNo;
lastOrder = order;
lines[ lineNo ].focus( true );
} else if ( lastLine === lineNo ) {
lines[ lineNo ].focus( false );
lastLine = -1;
lastOrder = 0;
} else {
lines[ lastLine ].focus( false );
var movable = lines[ lineNo ].canAdd( lines[ lastLine ].canGet( lastOrder ) );
if ( movable > 0 ) {
history.push( {
from : lastLine,
to : lineNo,
moved : movable,
opened : lines[ lastLine ].isOpen( movable + 1 )
} );
lines[ lineNo ].add( lines[ lastLine ].get( movable ) );
}
lastLine = -1;
}
}
this.fin = function( lineNo ) {
fin.add( lines[ lineNo ].get( 13 ) );
if ( self.isFinished() ) {
alert( '完成' );
score.win();
win = true;
}
}
this.isFinished = function () {
return lines.all( function ( line ) {
return line.isEmpty();
} );
}
this.back = function () {
if ( lastLine !== -1 ) {
lines[ lastLine ].focus( false );
lastLine = -1;
}
if ( history.length ) {
var command = history.pop();
if ( !command.opened ) {
lines[ command.from ].closeTop();
}
lines[ command.from ].add( lines[ command.to ].get( command.moved ) );
}
};
}
ゲームの進行とカードの移動を制御する。 コントロールを訳して制御と言ってるけど、実際にやってることは各種オブジェクト間の仲介に近い。
コントローラー生成時、仲介される側である列や未配カードなどのオブジェクトを生成する。
これまでの勝敗の記録をクリアする。 一応確認する。
各オブジェクトを一旦空にする。
新規ゲーム開始前のクリア処理なので、呼ばれるのは、勝った後か現状を破棄してやり直しかのどちらか。 なので、直前の結果が勝利でない場合は、やり直しによる呼び出しとして負け数を1増やす。
列と未配カードの初期配置。
初期配置する数は決まっているのだが、わざわざカードを全て一旦未配として、そこから各列に必要な数を取って置いている。
空き列が無い場合、未配から各列に1枚ずつカードを配る。 このとき、選択状態があれば解除する。 履歴スタックも空にする。
空き列がある場合は、その旨メッセージ表示する。
列のカードがクリックされたときのイベント処理で呼び出される。 引数として列番号と列内位置が渡される。
列が選択されていない場合は、今回の列を選択状態とする。
列が選択されていて、かつその列が今回と同じなら、選択状態を解除する。
その他の場合、選択列の先頭から選択位置までで、今回の列に移動できるカードがあるかどうかを調べる。 移動できるカードがあった場合は、その最大分を移動する。 併せて履歴スタックにもカードの移動記録を積んでおく。 移動できるカードが有ろうが無かろうが、選択状態は解除する。
13枚揃った場合に呼ばれる。
引数で指定された列番号の列の先頭から13枚を、完了分に移動する。
完成、つまり全列が空になっていれば true を返す。
戻す操作で呼ばれる。
選択状態を解除し、履歴スタックに移動記録が有れば1つ取り出して、逆の移動を行う。
// 初期処理
window.onload = function () {
var controller = new Controller();
controller.init().deal();
document.getElementById( 'score' ).addEventListener( 'click', controller.clearScore, false );
document.getElementById( 'back' ).addEventListener( 'click', controller.back, false );
document.getElementById( 'init' ).addEventListener( 'click', function () {
if ( controller.isFinished() || confirm( 'このゲームを捨てて新しく始める' ) ) {
controller.clear().init().deal();
}
}, false );
};
最後に、ロードイベントとして実行する全体の初期処理。 コントローラーを生成してゲーム実行のための準備をするとともに、メニューの各項目用のイベント処理を設定する。
以上でとりあえず動くのだが、完成したときの挙動がちょっと不自然。 まず完成のダイアログが出て、それから最後のカードが移動するのだ。 処理の順番は、カードの移動の方が先になるように書いたつもりなのだが。
あと、ブラウザのリロードは、やり直しとして負けでカウントすべきなのだが、今は放置。 なので、できないと思ったらリロードして、勝ち数だけを積み上げることが出来てしまう。 まあ、こんなので勝率を上げても虚しいだけだが。
そして何より、カードの移動のアニメーションが無い。 アニメーションなどしないでシュパッと移動する方が好みなのだが、それはそれとして、アニメーションを実装しないと何かちょっと負けた気がする。
ということで、明日はカード移動のアニメーションを考える。