あなたが,勝つことも引き分けることもできない三目並べ (jQueryプラグイン「jQuery.fakeTicTacToe.js」によるマルバツ・ゲーム)
三目並べゲームには,必勝法は存在しない。
したがって○×ゲームでは,お互いが最善の手を尽くすと,必ず引き分ける。
ところが,下記の三目並べは,CPUが必ず勝つ。あなたは必ず負ける。
三目並べゲームに必勝法が無いはずにも関わらず,ここではCPU必勝なのである。

ウソだと思ったら,下記の動作サンプルで,何ゲームでも戦ってみてほしい。
毎回かならず負けるから…。
jQueryプラグイン「jQuery.fakeTicTacToe.js」 動作デモページ
http://jquery-fake-tic-tac-toe-js.goo...
※スマートフォン・タブレットでも遊べる。
このjQueryプラグインの使用法は,リンク先のGoogle Codeのプロジェクトページに詳しく書いてある。
自由に改変したり,自分のWebサイトに埋め込んだりすることができる。
オープンソース・MITライセンス。
下記では,このjQueryプラグインのソースコードをかいつまんで追いかけることにより,
CPUを必勝とするアルゴリズムの概要を手短に説明する。
プラグインのソースコードの概要
下記のURLから,ソースコード全体を閲覧できる。
jquery.fakeTicTacToe.js(Google CodeのGitリポジトリ)
https://code.google.com/p/jquery-fake...
プロジェクトのトップページ
https://code.google.com/p/jquery-fake...
まず,冒頭部分。
(function(jQuery){ // 公開メソッド jQuery.fn.fakeTicTacToe = function() { //return this.each(function() { setupOneBoard(this); //}); }; jQuery.fakeTicTacToeRetry = function() { resetOneBoard(); };
ここでは,jQueryプラグインとして外部に公開するメソッドを定義している。
ゲームの開始メソッドは,$("セレクタ").fakeTicTacToe() という用途を想定しているので,プロトタイプチェイン上に設置する。
一方,ゲームの終了メソッドは,セレクタを介さずにどこからでも呼ばれうるため,静的メソッドとしてある。
1ページ内でゲーム盤の個数は1個でかまわないだろう,という考えから,eachによるイテレーションは実装しないでおいた。
jQuery プラグインの定義パターンについて調べてみた(by id:cyokodog氏)
http://d.hatena.ne.jp/cyokodog/200911...
- jQueryオブジェクト($)に対して直接メソッドをくっつけるのが,関数 API 系。プロトタイプチェイン上(fn)にメソッドを乗っけるのが,メソッド API 系。
- メソッドAPI系:プラグイン定義の際に使用される jQuery.fn はjQuery.prototype の別名。jQuery セレクタで要素を取得すると・・・jQuery インスタンスは、jQuery.prototype に定義された関数郡を、自身が保有するメソッドのように扱う
jQuery Pluginの書き方
http://tech.kayac.com/archive/jquery-...
- メソッドチェインを切らさないようにするには,最後にreturn this;する
- 複数要素が指定されている場合,thisをeachで処理
JavaScriptの動かないコード (中級編) オブジェクトのprototypeを変更した時のエラー
http://language-and-engineering.hatenablog.jp/entry/20080922/1222010471
- prototypeとは,new後に「困ったときのデフォルトの参照先」
- JSの言語仕様として,オブジェクトの__proto__([ [Prototype] ] )にデフォルトで代入されるのがprototype
/* ------- メッセージ ------- */
var FTMSG = {
msg_en : {
このゲームは,ブラウザの利用言語が英語の場合にも対応している。
そのため,文言を国際化し,ブラウザの言語設定から利用メッセージを判定・取得している。
/* ------- ゲーム全体 ------- */
// セットアップ
var setupOneBoard = function( divElement )
{
ここでは,ゲーム盤を生成し,各セルに押下イベントをセットして,
ユーザ側に最初の一手を促すところまで。
そして,次の関数以降は,ゲーム版全体の状態をデータ構造として裏側で保持しておくためのこまごました処理が続く。
// 特定のコマタイプがリーチ状態にあるかどうかの詳細情報を返す。
// 第二引数には,検査したいマップを渡す。
var getReachInfo = function( pieceType, targetMap ){
この関数は重要である。
ゲーム盤全体を満遍なくスキャンすることにより,
ユーザが勝ちそうな状態か,すなわち「CPUにとってピンチの事態が発生しているかどうか」を判定するメソッドなのだから・・・。
/* ------- ユーザ ------- */
// ユーザが一手を打ったとき
var onRvCellClicked = function( x, y )
{
if( ! isTurnOfUser ) return;
if( ! isBlankCell( x, y ) ) return;
execTurnOfUser(x, y);
};
var execTurnOfUser = function(x, y){
// コマを置く
recordPieceOnBoard({ is_user : true, x : x, y : y });
// ゆずる
isTurnOfUser = false;
alertRv(FTMSG._("cpus_turn"));
setTimeout(
execTurnOfCPU,
2000
);
};
ユーザがセルをクリック(タップ)した時のイベントである。
そのセルにコマを設置し,CPUに手を譲るだけ。
ここから先のコードに,次第に,うさんくさい怪しさが・・・。
/* ------- CPU ------- */
// CPUの一手
var execTurnOfCPU = function(){
var stra = createCPUStrategy();
stra.exec();
};
// CPUの戦略を考え,JSONで返す
var createCPUStrategy = function(){
// 最善の手を考える
var normal_stra = getNormalBestStrategy();
// 最善の手を打っても負けるか引き分ける?
if( requiresFakeOn( normal_stra ) )
{
// 危機に瀕しているので,インチキをする
return {
exec : function(){ execFakeOnPinch(); }
};
}
else
if( ( countAllPieces() <= 9 - 3 - 2 ) && ( Math.random() < 0.2 ) )
{
// 一定の確率で,危機に瀕していなくてもインチキをする。
// 条件は,3倍速で動いた後に,ラリーが一回続くこと。
return {
exec : function(){ execFakeSpeed( normal_stra ); }
};
}
else
{
// インチキなしで続行
return normal_stra;
}
};
// まっとうな最善の戦略を考える
var getNormalBestStrategy = function(){
// NOTE: 3目並べは,お互いに最善の手を打った場合,必ず引き分けになる。
// 自分が3つ並ぶならそうする
var reachInfoCPU = getReachInfo( 0, cellsMap );
if( reachInfoCPU.flagged )
{
return {
exec : function(){ putPieceOfCPU( reachInfoCPU.flaggedCell ); },
target : reachInfoCPU.flaggedCell
};
}
// 相手が3つ並ぶなら妨害する
var reachInfoUser = getReachInfo( 1, cellsMap );
if( reachInfoUser.flagged )
{
return {
exec : function(){ putPieceOfCPU( reachInfoUser.flaggedCell ); },
target : reachInfoUser.flaggedCell
};
}
// できれば中央に置く
if( isBlankCell( 1, 1 ) ){
return {
exec : function(){ putPieceOfCPU( { x : 1, y : 1 } ); },
target : { x : 1, y : 1 }
};
}
// できればどれか隅に置く
var stra_corner = null;
$.each( [ 0, 2 ], function(){
var x_corner = this;
$.each( [ 0, 2 ], function(){
var y_corner = this;
if( isBlankCell( x_corner, y_corner ) ){
stra_corner = {
exec : function(){ putPieceOfCPU( { x : x_corner, y : y_corner } ); },
target : { x : x_corner, y : y_corner }
};
}
} );
} );
if( stra_corner ) return stra_corner;
// 何でもいいのでランダムな空きマスに置く
for( var i = 0; i < 3; i ++ )
{
for( var j = 0; j < 3; j ++ )
{
if( isBlankCell( i, j ) ){
return {
exec : function(){ putPieceOfCPU( { x : i, y : j } ); },
target : { x : i, y : j }
};
}
}
}
};
まず,CPUは,最初から邪悪なわけではない。
できるだけまっとうな戦略を採ろうとするのだ。
このコード中で示されているアルゴリズムこそ,三目並べの「最善の手」を打つための確立された方法である。
この戦略をとる限り,決して負ける事はない,という事が知られている。
もし双方のプレーヤがこの戦略でプレーしたら,かならず引き分けとなることが証明されている。
この手順をミスった場合にのみ,三目並べには「負け」という状態が存在しうるのだ。
ちなみに,何気なくStrategyパターンが使用されていることにお気づきだろうか?
createCPUStrategy も getNormalBestStrategy も,戦略(ストラテジー)オブジェクトを返すメソッドである。
返却されたJSONオブジェクトは,exec() でその戦略を実行可能,という点で共通したインタフェースを持つようにしている。
Javaのデザインパターンと異なるのは,各戦略オブジェクトに対して,exec() の実装を言語的に強制はしていない(インタフェースオブジェクトの指定とかがない)という点だけだ。
GoFの23のデザインパターンを,Javaで活用するための一覧表 (パターンごとの要約コメント付き)
http://language-and-engineering.hatenablog.jp/entry/20120330/p1
- Strategy: アルゴリズム切り替え。アルゴリズム実装のための専用オブジェクト
で,問題は,この先の
// 危機に瀕しているので,インチキをする // 一定の確率で,危機に瀕していなくてもインチキをする。
の部分。
ここでCPUが牙をむき始める。
/* ------- CPUによるインチキ工作 ------- */
// インチキが必要か判定
var requiresFakeOn = function( strat ){
// NOTE: 8手目までに必ず勝つ必要がある。9マス目をユーザが打つことはない。
// 最善の手を打っても負けるか引き分けるか,を判定。
// 現在のボードのコピー
//var mapCopy = $.merge( [], cellsMap ); > 参照コピーになって干渉してしまう
var mapCopy = getNewNullMap();
for(var i = 0; i < 3; i ++)
{
for( var j = 0; j < 3; j ++ )
{
mapCopy[i][j] = cellsMap[i][j];
}
}
// CPUが一手打ったシミュレーション
mapCopy[ strat.target.x ][ strat.target.y ] = 0;
debug("count = " + countAllPieces());
// この仮想状況でもユーザにリーチの可能性があるか,または8手目でゲーム完了しないか?
if(
( getReachInfo( 1, mapCopy ).flagged )
||
( ( countAllPieces() == 7 ) && ( ! isCPUCompleted( mapCopy ) ) )
)
{
return true;
}
else
{
return false;
}
};
まっとうな方法では勝てない,ということを確認するために,
CPUは一度,まっとうな手をシミュレーションする。
仮想的なマップ上で,一手打ってみるのである。
それでもダメだったら,いよいよ裏工作に走る。
具体的なインチキ工作については,
エフェクトで「むなしさ」を表現することを心がけた。
この部分はコードよりも,実際のゲームにおいて,負け続ける楽しみをぜひ味わって頂きたい。
結び
「何と不毛なプラグイン・・・」と,
呆れて口がふさがらないリアクションを引き出すことができたら,
このプラグインの目的は果たせたことになる。
プレーしてみると意外とはまる。お試しあれ。
補足
下記のページで,ソースコードリーディングの対象として紹介されている。
jQuery.fakeTicTacToe.js | 「三目並べ(ゲーム)」カテゴリー | JavaScriptデモ
http://javascript-demo.e1blue.net/js/...
関連する記事:
わずか1.7キロバイトのJavaScript マリオ風のゲーム (脱力系)
http://language-and-engineering.hatenablog.jp/entry/20081006/1223209263
jQuery をSQLの「select文」のように使う方法
http://language-and-engineering.hatenablog.jp/entry/20081004/1223056859
あなたが理解できない,たった一行のRubyのコード (動的言語に対する静的解析の限界)
http://language-and-engineering.hatenablog.jp/entry/20120619/p1