問題
以前、 JavaScript でオセロを実装 していたのですが、 この実装には一つ大きな問題がありました。
AI相手にゲームをするのは、それはそれで楽しいものの、 やはりこの手のゲームは人間同士で対戦したくなるものです。 一応、あの実装は人間同士で対戦できると言えばできるのですが、 同じPCの前に座って交代しながら操作する形になので、色々と不便です。
インターネット全盛のこの時代、やはりネット対戦できるようにしたいですよね。 しかしプレイヤー間の通信やプレイ中のゲームの状態の共有は一体どうすれば良いのやら。 オセロのようなターン制の単純なゲームでさえネット対戦対応するには課題が山盛りです。
どうにかして簡単にサクサクっとネット対戦できるようにできないものでしょうか。
回答
実は Firebase を使えば簡単にサクサクっと対応できます。 これは
- JSONなデータを読み書きできるストレージサービスだけど
- データの変更がリアルタイムで全てのクライアントに同期される
という超便利なサービスです。
例えば
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
にまとめた方が良いと思いますが、 実装が面倒臭くなるので、 今回はユーザーIDだけでなく名前も個々のゲーム情報に含める事にします。 moves
の値は配列です。ですが、 Firebase はその理念上、配列をそのまま保存させてくれない ので、少々まどろっこしい書き方をしています。
実装
画面側も含めて書き始めると道に迷いそうなので、 個々の機能 = パーツをどう実装するか考えて、 後で出来上がったパーツを組み合わせる事にしましょう。
なお、各パーツのコードは以下の変数が定義済みとして記述しています:
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がサポートしているログイン方式は色々ありますが、どれを使うにしてもいくらか前準備が必要です。
- Twitterアプリを作成する。
- Callback URL は
https://auth.firebase.com/v2/<YOUR-FIREBASE>/auth/twitter/callback
を設定しておく。
- Callback URL は
- Firebaseアプリの管理画面の「Login & Auth」タブを開いて各種設定を行う:
- 「Enable Twitter Authentication」にチェックを入れる。
- 「Twitter API Key」にTwitterアプリの「Consumer Key (API Key)」を入力する。
- 「Twitter API Secret」にTwitterアプリの「Consumer Secret (API Secret)」を入力する。
- 「Authorized Domains for OAuth Redirects」にアプリをホストするドメイン名を入力する(このフォームはEnterを押下するまで入力内容を確定してくれないので注意)
以上の前準備が完了していればログイン関連の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と同様。
});
一応これでも動きはするでしょうが、 「非ログイン状態で参加ボタンを押下した場合はまずログインさせる」 や 「複数人が同時に参加ボタンを押下しても破綻しないようにする」 といった処置は必要そうです。
ゲームのプレイ
- ゲームID
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回づつ発生し、
その後は新しいデータが追加される度に発生するので、
これだけで
- これまでの棋譜から現在の状態を再現する
- 新しい手が打たれる度にゲームの状態を更新する
の両方が賄えます。
なお、全てのプレイヤーがルールに則って手を打ってくれれば良いのですが、 不正なクライアントを作ってあり得ない手を打つプレイヤーが出てくる (もしくは公式クライアントのバグであり得ない手が打ててしまう) 可能性があるので、打たれた手が正当かどうかチェックする必要があります。
後は
- ゲームの状態の表示
- 手を打つUIの提供
ができればOKでしょう。 でもこれはオフライン版と大して違いが無いので省略します。
全体像
以上のパーツを上手いこと調整して組み合わせればオンライン版オセロの完成です。
ただしここにペーストするには全貌が長過ぎます。
と言う訳で
GitHubで公開していますので、そちらを参照してください。
ファイル名が online
で始まっているものになります。
実際に遊んでみたい!
http://kana.github.io/othello-js/online で遊べるのでご自由にどうぞ。
コメントする