この記事は
FORK Advent Calendar 2019 18日目の記事です。
- NuxtでのPWA対応Webアプリ開発
- Cloud Firestore(以下Firestore)を使ったリアルタイムなユーザー間通信
の実践入門として、簡単なカードゲームを作ってみたお話です。
背景
実家に帰省するたびに、愛すべき甥っ子(小3)におもちゃやらゲームやらを買い与えては一緒に遊んでいるので
今のところは一緒に遊んでくれる楽しいおじさんとして無邪気になついてくれているのですが、
小学校中学年ともなるとそろそろ半年に一回帰ってくる金ヅルおじさん1になりますよね。
そんな無慈悲な烙印を押される前に、ここらでいっちょゲームを作れる金ヅルおじさんになってやろうじゃありませんか。
おことわり(言い訳)
Firebaseの実践入門ということでなにかと勢いまかせのエイヤッで作ってしまったため、
未完成のアプリケーションなうえ、Sparkプランです。
そのため現時点ではURLの公開等はせずに開発途中の画面、コードでお届けします。
「逆に考えるんだ。“未完成のものでも「NuxtやFirebaseで何か作りたい」という人へのきっかけになれればいいさ”と考えるんだ」
ジョースター卿
構成
- Nuxt 2.9.2
- 静的ジェネレートSPA(PWA対応)
- Typescript
- Vuetify(UIコンポーネント)
- Firebase
- Firebase Hosting(ホスティング)
- Cloud Firestore(データベース)
作るもの(ゲームの内容)
「ザ・マインド」というカードゲームのルールを基に自作します。
全員で1~100の数字を小さい順に出す、というプレイヤー協力型ゲームです。
カードゲームと言ってもルールがシンプルで自作できそうだったのでチャレンジしてみました。
(某こどものおつかいやあらへん番組でも紹介されていました)
詳しいルールについては下記を参考に。
The Mind-ザ・マインド|相談ができない孤独の協力ゲーム | 遊びゴコロ
ルール概要
- プレイ人数:2人以上
- 対象年齢:1から100まで数を数えられればたぶんOK
- プレイヤーには1~100の数字が書かれたカードの中からランダムで1枚配られます
- プレイヤー同士の会話やジェスチャーは一切禁止です2
- 数字の少ない順に全員が手札を場に出せたらクリアです(次のレベルに進む)
- 場に出したカードより少ない数字を誰かがまだ持っていたら失敗です
- 手札の枚数はレベルが上がるたびに1枚ずつ増えます(レベル2では1人2枚)
要は会話やジェスチャーをせずに、顔色や間を窺って数字を順番に出していく、
信頼関係が試されるゲームです。
はたして甥っ子と金銭授受以外の手段で信頼関係を築けるのか・・!
実装していない本家ルール
- ライフポイント(失敗したら-1、0になったら終了)
- 流星カード(各プレイヤーの一番小さな数字のカードを開示して取り除けるアイテム)
- レベルボーナス(特定のレベルクリアでのライフの回復、アイテムの付与など)
できたもの
前置きが長くなりましたね、こちらができたものです
プレイヤー登録画面
(部屋とかルームとか表記が定まってない感)
- ホストとなる人が**部屋を作る(ホスト)**から、参加するプレイヤーの名前を登録して部屋を作ります。
このときに部屋番号となるチャンネルIDが登録されます。 - ゲストとなる人が、ホストの画面のQRコードを読むか、**部屋に入る(ゲスト)**からチャンネルIDを入力します。
- プレイヤー選択画面で全員が選択したらゲームスタートです。
ゲーム画面(成功 / 失敗パターン)
- ゲーム開始時に手札が配布されます。
- カードを出すときは「提示するカードを表示」を押すと、自分の他の手札が見えなくなります。
- 2の画面を他のプレイヤーに見せながら、判定ボタンを押すと、その数字が通ったかどうかが判定されます。
(ほかに小さい数字があれば失敗) - これを全員がカードを出すまで繰り返します。
- 成功の場合は次のステージに進むか終了かを選べます
- 失敗の場合はリトライするか終了かを選べます
環境構築と参考記事
Nuxt + Typescript
以前こちらの記事にまとめました。
Nuxt v2.9 + Typescriptの環境構築
Firebase
こちらを参考にさせていただきました。ありがたや~~
- Nuxt v2とFirebase(CloudFirestore)でPWA対応Webアプリ開発
- 【v2対応】Nuxt.jsとFirebaseを組み合わせて爆速でWebアプリケーションを構築する
- 『Nuxt + Firebase Hosting』で超速deploy
Vuetify
UI FrameWorkにVuetifyを採用しています。以下が参考になりました
データの設計
ゲームに必要なデータは以下の通り設計しました。
カッコ内はVuexのストアモジュールを指します。そのままですね。
- チャンネルID(
store/index.ts
)- プレイヤーをマッチングさせるための部屋番号
- プレイヤー情報(
store/player.ts
)- ID
- 名前
- ホストorゲスト
- プレイヤー選択画面での選択状態
- ゲーム情報(
store/game.ts
)- 全カードの配列
- 各プレイヤーの手札
- 現在のステージ数
- 現在出されているカードの枚数
- 最後に出されたカードの番号
- 次の正解となるカードの番号
- 成功 / 失敗 / ゲームをやめた などの状態
これらをVuexとFirestoreで同期させます!
Vuex
こんな感じ
Firestore
Firestoreは、
mindコレクション > チャンネルIDのドキュメント > gameとplayerのフィールド
という構造でVuexと同期します。
実装
VuexとFirestoreの同期 / 更新
Firestoreが提供するonSnapshot()
メソッドとset()
メソッドを使ってデータの同期と更新を実装します。
- 同期処理 onSnapshot()メソッド
- 更新処理 set()メソッド
これらの処理はすべてVuexのactions
に持たせることで、
コンポーネント(vueファイル)側ではFirestoreを意識しなくていいようにしました。
同期
ゲーム情報の同期部分(Vuexストアモジュール)
import firebase from '@/plugins/firebase'
const db = firebase.firestore()
// ===== 略 =====
export const state = (): GameState => ({
cardList: [],
count: 0,
hand: {},
pushHand: null,
next: null,
stage: null,
success: false,
failed: false,
exit: false
})
// ===== 略 =====
export const mutations = {
gameDataSync(
state: GameState,
payload: GameState
) {
if (payload) {
Object.assign(state, payload)
}
},
// ===== 略 =====
export const actions = {
gameDataFetch({ commit, rootState }) {
/* Firebaseの連携 */
const channelId: string | null = rootState.channel
if(channelId !== null) {
//- コレクションとドキュメント識別子(チャンネルID)を明示的に指定する
db.collection('mind')
.doc(channelId)
//- onSnapshotで対象のドキュメントをリッスン
.onSnapshot((doc) => {
//- コールバックでデータ更新時の処理
const docData = doc.data()
if (docData) {
commit('gameDataSync', docData.game)
} else {
console.error(`エラー:ゲームデータが取得できませんでした`)
}
}, (error) => {
console.error(`エラー:${error}`)
})
}
},
// ===== 略 =====
ゲーム開始などのよきタイミングでgameDataFetch
を一度dispatch
することで
Firestoreの対象のドキュメントに変更があった場合にgame.ts
のstate
も更新されるようになります。
あとはコンポーネント側でcomputed
なりwatch
なりで変更を感知させます。
更新
データ更新のタイミングは
- ステージ開始時(カードをシャッフルして配布)
- プレイヤーがカードを出した時
- 次のステージ/リトライ/終了などのステージ更新時
です。
2を例にすると、Vuexに成否判定後にFirestoreへのマージ処理を担うaction
を用意して、
のclickイベントでdispatch
する設計にしました。
action
// ===== 略 =====
export const actions = {
// ===== 略 =====
/* 手札の初期化action */
gameInit({ state, rootState, rootGetters }) {
const player: Array<Player> = rootGetters['player/getPlayerList']
const playerIds: Array<string> = []
const playerLen: number = player.length
const handLen: number = state.stage !== null ? state.stage : 1
const hand: {} = {}
const cardsLen: number = handLen * playerLen
const handred: Array<number> = []
player.map((e: Player) => {
playerIds.push(e.id)
})
// 1 - 100 の配列を用意
for (let i: number = 0; i < 100; i++) {
handred.push(i + 1)
}
// Fisher-Yates アルゴリズムによるシャッフル
for (let i: number = handred.length - 1; i > 0; i--) {
const j: number = Math.floor(Math.random() * (i + 1))
const tmp: number = handred[i]
handred[i] = handred[j]
handred[j] = tmp
}
const cards: Array<number> = handred.splice(0, cardsLen)
const cardList: Array<number> = [...cards].sort((a, b) => {
return a - b
})
// cardの配布
for (let i = 0; i < playerLen; i++) {
const id: string = playerIds[i]
const handCards: Array<number> = cards.splice(0, handLen)
const sortedhandCards: Array<number> = handCards.sort((a, b) => {
return a - b
})
hand[id] = sortedhandCards
}
const channelId: string = rootState.channel
const initializeData: {
game: GameState
} = {
game: {
stage: handLen,
hand,
pushHand: null,
cardList,
count: 0,
next: cardList[0],
success: false,
failed: false,
exit: false
}
}
//- FirestoreにinitializeDataオブジェクトをマージ
db.collection('mind')
.doc(channelId)
.set(initializeData, { merge: true })
},
/* 手札の開示action */
pushHand({ state, rootState }, payload: number) {
const channelId: string = rootState.channel
const pushData: GameData = {
game: {
pushHand: payload
}
}
/* 中略
* 数字を引数payloadで受け取って、
* 成功や失敗の判定、成功の場合は次の正解の番号を取得などしてから、
* pushDataオブジェクトにデータを追加。
*/
//- FirestoreにpushDataオブジェクトをマージ
db.collection('mind')
.doc(channelId)
.set(pushData, { merge: true })
},
// ===== 略 =====
図にするとこんな感じになります。
感想
今回はFirebase(Firestore)にとにかく触ってみる、
という目的でカードゲームを作ってみました。
正直自分はDBの知識はさっぱりなのですが、
FirestoreのドキュメントはJSONレコードとして扱え3、連携処理もFirebaseのAPIのおかげでとても簡単でした。
簡単すぎるがゆえに、自分の無学に不安を覚える箇所も多々あるので、
これを機にFirebaseやDBについてきちんと学んでいきたいと思います。
ボードゲームやカードゲームは、単純なルールでも奥が深いものが多く、
開発のモチベーションも保ちやすいためチャレンジ課題としておすすめです
(サービスとして公開する場合は著作権にも注意しましょう)
ゲームを作れる金ヅルおじさんはお年玉もしっかりあげます。(金ヅルなので1)
FORK Advent Calendar 2019
17日目 Docker入門 ~Dockerfile編~ @takaday
19日目 Advent Calendar だから Illustrator + JavaScript でお菓子を作ってみる @momo_r
-
ゲームの肝となるルールですが、数字を直接的には言わなければ会話もOK、とかにしても楽しいらしいです(やったことないけど) ↩