人対人対戦をサポート
ユーザー入力を受け付けるプレーヤーを用意して、人対人で対戦できるようにしました。
- プレーヤーの仕様が「盤の情報を受け取って次に石を置く場所を返す関数」だと処理が永遠にUIスレッドに移らず上手く動かないので、次に置く石の位置はコールバック関数に引数として渡す形にしました。
- ただこれだと、コールバック関数をコールしないことによって、試合を凍結できるので対策が必要だな。(一定時間後に強制スキップとか。)
サンプル
青い下線があるマスが現在のプレーヤーが置ける石の位置です。クリックで「石を置く」。
→サンプル
ソース
if ( typeof reversi == "undefined" ) { reversi = {}; } /** * ゲーム * @param {Function} white プレーヤー1 * @param {Function} black プレーヤー2 */ reversi.Game = function( white, black ) { this.white = white; this.black = black; this.board = reversi.inner.create(); this.color = reversi.inner.Color.WHITE; this.current = white; this.skipCount = 0; } reversi.Game.prototype = { /** * ゲームする * @param {Function} callback コールバック関数。 */ play: function( callback ) { var self = this; if ( reversi.inner.hasNext( this.board ) ) { if ( reversi.inner.enable( this.board, this.color )) { this.skipCount = 0; var b = new reversi.Board( reversi.inner.clone(this.board), this.color ); var callbacked = false; this.current( b, function ( position ) { if ( callbacked ) { // 2重Put 負け扱いにする。 } callbacked = true; try { self.board = reversi.inner.put( self.board, position, self.color ); self.next(callback); } catch ( exception ) { // 負け扱いにする。TODO } } ); } else { if ( this.skipCount == 0 ) { this.skipCount++; this.next(callback); // 次のターン } else { // 両者共にスキップ this.end(callback ); } } } else { this.end(callback); } }, /** * 次のターンに進む * @param {Function} callback コールバック関数。 */ next: function( callback ) { // プレーヤーを変更 this.color = this.color == reversi.inner.Color.WHITE ? reversi.inner.Color.BLACK : reversi.inner.Color.WHITE; this.current = this.color == reversi.inner.Color.WHITE ? this.white : this.black; var self = this; setTimeout( function(){ self.play(callback); }, 100 ); }, /** * 終了処理 * @param {Function} callback コールバック関数。 */ end: function( callback ) { // 結果をカウントして表示。 var result = { white:0, black:0, board:new reversi.Board(this.board, this.color) }; reversi.inner.each( this.board, function( p, color ) { if ( color == reversi.inner.Color.WHITE ) { result.white ++; } else if ( color == reversi.inner.Color.BLACK ) { result.black++; } } ) ; if ( result.white > result.black ) { result.winner = "white"; } else if ( result.white < result.black ) { result.winner = "black"; } else { result.winner = "none"; } callback( result ); }, /** * 終了処理 * @param {Function} callback コールバック関数。 */ end: function( callback ) { // 結果をカウントして表示。 var result = { white:0, black:0, board:new reversi.Board(this.board, this.color) }; reversi.inner.each( this.board, function( p, color ) { if ( color == reversi.inner.Color.WHITE ) { result.white ++; } else if ( color == reversi.inner.Color.BLACK ) { result.black++; } } ) ; if ( result.white > result.black ) { result.winner = "white"; } else if ( result.white < result.black ) { result.winner = "black"; } else { result.winner = "none"; } callback( result ); } } // マスの状態 reversi.GridState = { // 自分の MINE: 1, // 敵の ENEMIES: 2, // 何もおかれていない。 EMPTY: 0 } /** * プレイヤーに渡される盤の情報 */ reversi.Board = function( board, color ) { this.board = board; this.color = color; } reversi.Board.prototype = { /** * 指定した位置のマスの状態を得る。 * @param {Hash} position 位置 */ getGridState : function( position ) { var value = this.board[position.x][position.y]; if ( value == this.color ) { return reversi.GridState.MINE; } else if ( value == reversi.inner.Color.EMPTY ) { return reversi.GridState.EMPTY; } else { return reversi.GridState.ENEMIES; } }, /** * 指定した位置に石を配置できるか評価する。 * @param {Hash} position 位置 */ enablePut : function( position ) { return reversi.inner.enablePut( this.board, this.color, position); } } // ユーティリティ関数 reversi.inner = { /**色*/ Color: { WHITE: 1, BLACK: 2, EMPTY: 0 }, /**方向*/ DIRECTIONS: [ [-1,-1],[ 0,-1],[1,-1], [-1, 0], [1, 0], [-1, 1],[ 0, 1],[1, 1] ], /** * ボードを反転し反転した石の数を返す。 * メソッドから復帰後、ボードも反転している。 * @param {Array} board ボード * @param {int} color 色 * @param {Hash} position 位置 * @param {Array} direction 方向 * @return {int} 反転した数 */ flip: function( board, color, position, direction ) { var enemyColor = ( color == reversi.inner.Color.WHITE ? reversi.inner.Color.BLACK : reversi.inner.Color.WHITE ); var fliped = []; var count = 0; var current = { x: position.x + direction[0], y: position.y + direction[1] }; while ( current.x >= 0 && current.x < board[0].length && current.y >= 0 && current.y < board.length ) { if ( board[current.x][current.y] == enemyColor ) { board[current.x][current.y] = color; fliped.push( current ); count++; } else if ( board[current.x][current.y] == color ) { return count; } else { break; } current = { x:current.x + direction[0], y:current.y + direction[1] }; } for ( var i = 0; i < fliped.length; i++ ) { board[fliped[i].x][fliped[i].y] = enemyColor; } return 0; }, /** * ボードを複製する。 * @param {Array} board ボード * @return {Array} 複製したボード */ clone: function( board ) { var newBoard = []; reversi.inner.each( board, function( p, color ) { if ( !newBoard[p.x] ) { newBoard[p.x] = [];} newBoard[p.x][p.y] = color; } ); return newBoard; }, /** * ボードを列挙する。 * @param {Array} board ボード * @param {Function} f ハンドラ */ each: function( board, f ) { for ( var i=0;i<board.length;i++ ) { for ( var j=0;j<board[i].length;j++ ) { if ( f( {x:i, y:j}, board[i][j] )) { return; } } } }, /** * ボードを生成する。 * @param {Array} ボード */ create: function( board, f ) { var board = []; for ( var i=0;i<8;i++ ) { board[i] = []; for ( var j=0;j<8;j++ ) { if ( (i==3 && j==3) || (i==4 && j==4) ) { board[i][j] = reversi.inner.Color.WHITE; } else if ( i==3 && j==4 || i==4 && j==3 ) { board[i][j] = reversi.inner.Color.BLACK; } else { board[i][j] = reversi.inner.Color.EMPTY; } } } return board; }, /** * 石を置く * @param {Array} board ボード * @param {Hash} position 位置 * @param {int} color 色 */ put : function( board, position, color ) { if ( board[position.x][position.y] != reversi.inner.Color.EMPTY ) { throw { "color":color, reason:"grid is not empty." } } board[position.x][position.y] = color; var count = 0; for (var i=0;i<reversi.inner.DIRECTIONS.length; i++) { count += reversi.inner.flip(board, color, position, reversi.inner.DIRECTIONS[i] ); } if ( count == 0 ) { throw { "color":color, reason:"disable put to the position." } } return board; }, /** * 次のターンがあるか評価する。 * @param {Array} board ボード */ hasNext: function( board ) { var hasNext = false; reversi.inner.each( board, function( p, color ) { if ( color == reversi.inner.Color.EMPTY) { hasNext = true; return true; } }); return hasNext; }, /** * 石を置く場所があるか評価する。 * @param {Array} board ボード */ enable: function( board, color ) { var hasNext = false; reversi.inner.each( board, function( p, c ) { if ( reversi.inner.enablePut( board, color, p ) ) { hasNext = true; return true; } }); return hasNext; }, /** * 石を置けるか評価する。 * @param {Array} board ボード * @param {int} color 色 * @param {Hash} position 位置 */ enablePut: function( board, color, position ) { if ( board[position.x][position.y] != reversi.inner.Color.EMPTY) { return false; } var count = 0; var tmp = reversi.inner.clone( board ); for (var i=0;i<reversi.inner.DIRECTIONS.length; i++) { count += reversi.inner.flip(tmp, color, position, reversi.inner.DIRECTIONS[i] ); if ( count > 0 ) { return true; } } return false; } }