以前、JavaScript でオセロを実装していたのですが、この実装には一つ大きな問題がありました。
AI相手にゲームをするのは、それはそれで楽しいものの、
やはりこの手のゲームは人間同士で対戦したくなるものです。
一応、あの実装は人間同士で対戦できると言えばできるのですが、
同じPCの前に座って交代しながら操作する形になので、色々と不便です。
インターネット全盛のこの時代、やはりネット対戦できるようにしたいですよね。
しかしプレイヤー間の通信やプレイ中のゲームの状態の共有は一体どうすれば良いのやら。
オセロのようなターン制の単純なゲームでさえネット対戦対応するには課題が山盛りです。
どうにかして簡単にサクサクっとネット対戦できるようにできないものでしょうか。
実はFirebaseを使えば簡単にサクサクっと対応できます。
これは
という超便利なサービスです。
例えば
var ref = new Firebase('https://<foobarbaz>.firebaseio-demo.com/');
ref.push({...});
でデータを追加できて
ref.on('child_added', function (snapshot) {
...
});
でデータの追加される度に何かすることが出来ます。
なので、
$('#text').keypress(function (e) {
if (e.keyCode == 13) {
var name = $('#name').val();
var text = $('#text').val();
ref.push({name: name, text: text});
$('#text').val('');
}
});
ref.on('child_added', function (snapshot) {
var message = snapshot.val();
showChatMessage(message.name, message.text);
});
のようなコードを書いてフォームをちょこっと書き足せば、
何とこれだけでリアルタイムのチャットアプリの出来上がりという訳です。
Firebase を使って本格的なアプリやサービスを提供するなら有料プランを使う必要がありますが、
ちょっとしたアプリであれば無料プランで何とかなります。
と言う訳で Firebase を使えば超簡単にネット対戦版オセロが出来上がるのでは……?!
まずどういう風にネット対戦するのかイメージを固めておきましょう。
画面としては以下の2つが必要でしょう:
ネット対戦しようと思ったら当然接続している人を識別する必要があります。
ゲームを遊びたい人はTwitterアカウントでログインしてもらう事にしましょう。
Firebaseはユーザー認証も簡単にできる
ようなので楽勝そうです。
後はデータをどう保存するかですね。
FirebaseはRDBではないのでSQLと同じようにデータが取れると思ったら大間違いです。
その点も考慮すると以下のようすると良さそうです:
{
gameOutlines: {
<ゲームID>: {
blackId: <ユーザーID>,
blackName: <ユーザー名>,
whiteId: <ユーザーID>,
whiteName: <ユーザー名>,
state: <ゲームの状態(準備中/プレイ中/終了)>
},
...
},
gameDetails: {
<ゲームID>: {
moves: {
<着手ID>: <手(石の座標orパス)>,
...
}
},
...
},
}
moves
を元に「再生」すれば分かる話ですからね。users
にまとめた方が良いと思いますが、moves
の値は配列です。ですが、画面側も含めて書き始めると道に迷いそうなので、
個々の機能 = パーツをどう実装するか考えて、
後で出来上がったパーツを組み合わせる事にしましょう。
なお、各パーツのコードは以下の変数が定義済みとして記述しています:
var ref = new Firebase('https://<foobarbaz>.firebaseio-demo.com/');
/gameOutlines
以下のデータを全部取得するだけです。
これは以下のコードで実現できます:
ref.child('gameOutlines').on('value', function (snapshot) {
updateGameListView(snapshot.val() || []);
});
function updateGameListView(gameOutlines) {
// TODO: 何か良い感じに画面を更新する。
}
「新しいゲームが作成された」時や「あるゲームに誰かが参加した」時にも随時画面を更新する必要がありますが、value
イベントは既存データの初回取得に加えてデータの変更がある度に発生するので、
上記のコードで十分です。
ただ、ゲーム数が100や1000や10000になった場合、この方法だと大変なことになるので、
きちんとするならページングすべきですね。
Firebaseがサポートしているログイン方式は色々ありますが、どれを使うにしてもいくらか前準備が必要です。
https://auth.firebase.com/v2//auth/twitter/callback
を設定しておく。以上の前準備が完了していればログイン関連のAPIが使えるようになります
(全ての設定が完了していないと、Twitter側での認証画面で認証を行ってもアプリ側に制御が戻った時にエラーになります)。
肝心のログインを行うAPIは何種類かあるのですが、今回の場合は authWithOAuthPopup
を使うのがベストだと思います:
$('#loginButton').click(function () {
ref.authWithOAuthPopup('twitter', function(error, auth) {
if (error) {
console.log('ログイン駄目です', error);
} else {
updateCurrentUserView(auth);
}
});
});
ログアウトは unauth
で実行できます:
ref.unauth();
ref.getAuth() === null; //==> true
ログイン済みかどうかは getAuth
で判定できます:
var auth = ref.getAuth();
if (auth === null) {
alert('ログインしてない');
} else {
alert('ログイン済み: ようこそ @' + auth.twitter.username + ' さん!');
}
ゲームの新規作成は
gameOutlines
に追加するだけなので、以下のようなコードで実現できるでしょう:
$('#newGameButton').click(function () {
var go = ref.child('gameOutlines').push({
state: 'preparing',
created_at: Firebase.ServerValue.TIMESTAMP
});
var gameId = go.key();
updateGameDetailView(gameId);
});
ゲームの概要情報 outline
とFirebase内におけるキー gameId
が既知なら以下のコードでできそうです:
var outline = ...;
var gameId = ...;
$('#joinAsBlackButton').click(function () {
var auth = ref.getAuth();
if (auth) {
outline.blackId = auth.uid;
outline.blackName = auth.twitter.username;
if (outline.blackId && outline.whiteId)
outline.state = 'playing';
ref.child('gameOutlines').child(gameId).update(outline);
}
});
$('#joinAsWhiteButton').click(function () {
// blackと同様。
});
一応これでも動きはするでしょうが、
「非ログイン状態で参加ボタンを押下した場合はまずログインさせる」
や
「複数人が同時に参加ボタンを押下しても破綻しないようにする」
といった処置は必要そうです。
gameId
gameTree
があると仮定したら話は簡単そうです:
var gameId = ...;
var gameTree = makeInitialGameTree();
var moves = ref.child('gameDetails').child(gameId).child('moves');
moves.on('child_added', function (snapshot) {
play(snapshot.val());
});
function play(moveName) {
var validMoveNames =
gameTree.moves.map(function (m) {return nameMove(m);});
var i = validMoveNames.indexOf(moveName);
if (0 <= i) {
gameTree = force(gameTree.moves[i].gameTreePromise);
} else {
throw new Error(
'Error: Unexpected move "' + moveName + '" is chosen\n' +
'but valid moves are ' + validMoveNames.join(', ') + '.'
);
}
}
child_added
イベントは既存の各データに対して1回づつ発生し、 その後は新しいデータが追加される度に発生するので、 これだけで
なお、全てのプレイヤーがルールに則って手を打ってくれれば良いのですが、 不正なクライアントを作ってあり得ない手を打つプレイヤーが出てくる (もしくは公式クライアントのバグであり得ない手が打ててしまう) 可能性があるので、打たれた手が正当かどうかチェックする必要があります。
後は
ができればOKでしょう。
でもこれはオフライン版と大して違いが無いので省略します。
ただしここにペーストするには全貌が長過ぎます。
と言う訳で GitHubで公開していますので、そちらを参照してください。
ファイル名が online
で始まっているものになります。
http://kana.github.io/othello-js/online で遊べるのでご自由にどうぞ。