TypeScript & React & Firebase で何かつくってみる13 Firestore 4
第11回目 でカードを裏返せるようにしたが, その時の伏線を回収する.
カードの裏面を非公開にしたい
「我々は伏せられたカードの下側の面を知ることはできない」
これは多くのカードゲームに共通する大前提だ.
ポーカーもブラックジャックも 裏面を知ることができたら 賭けは成立しない.
カードは 裏返すまで裏に何が描かれているかわからない.
観測されないものは無いのと同じである.
つまりカードの裏面とは それを裏返すまで存在しないのだ.
・・冗談はさておき, この大前提を firebase で実現したい.
DeckView
このために新しく DeckView 型 を用意した.
type DeckView = {
name: string
cards: number[]
allowedDest: string[]
}
Deck との違いは cards が 数値型の配列である点だ.
過去に定義した型を 再掲する.
type CardSurface = {
name: string
style: React.CSSProperties
isFace: boolean
}
type Card = {
face: number
back: number
}
type Deck = {
name: string
cards: Card[]
allowedDest: string[]
}
CardSurface はカード片面のデザインを定義する.
表か裏かは isFace で判別する. 本設計では 「表か裏か」というのは カードの状態ではなく, デザイン上の区別に過ぎない. そもそも システム的には区別する必要がないかもしれない.
この型のインスタンスはゲーム開始前に完全に確定し, ゲーム中に増減することはない. 配列に格納され, 全参加者で共有する.
Card は 表と裏の2つの面を持つ.
各面を CardSurface 配列のインデックス番号(CardSurface番号) で管理する.
Card インスタンスは その総数がゲーム中に変わることはないが, 保存場所が変わったり表と裏が入れ替わったりする.
そして Deck とは Card の集合, つまり カードの山 を示していた.
ただ, Deck は カードの表も裏も知っている 神の目のオブジェクトだった.
それと異なり, DeckView は 上面の情報のみを持つ.
上面のみの CardSurface 番号 をカードの枚数分持つ.
神の目と人の目を同期的に管理する
Deck の集合は firestore の fields コレクションで保持し,共有している.
これの DeckView 版として fieldViews コレクションを用意する.
fieldViews は fields の内容を常に反映しつつも 上側の面しか保持しない.
まずクライアントコードを修正し, fields を更新するときは必ず fieldViews も同じID に対して更新するようにする. この処理は自動化すべきだが, 仮コードなので動けば良しとする.
const fieldsRef = firestore.collection('fields');
const viewsRef = firestore.collection('fieldViews');
const initialize = () => {
const cards: Card[] = Array.from(Array(53).keys()).map((i) => { return { face: 0, back: i + 1 }; });
const deck: Deck = {
name: 'spawn', cards: cards, allowedDest: ['*'], owner: ''
};
fieldsRef.doc('spawn').set(deck);
viewsRef.doc('spawn').set(createDeckView(deck));
};
const createDeck = async () => {
const deck : Deck = {
name: '', cards: [], owner: '', allowedDest: ['*']
};
const doc = await fieldsRef.add(deck);
viewsRef.doc(doc.id).set(createDeckView(deck));
}
const deleteDeck = (id: string) => {
viewsRef.doc(id).delete();
fieldsRef.doc(id).delete();
}
初期値としてこれまで使ってきた deck, alice, bob をやめ, 初期デックは spawn 1つにする.
これは「新品を箱から出した状態」をイメージした. ここにこれから使うすべてのカードが整列した状態で格納されている.
本当は initialize() で fields 内の既存デックをすべて破棄したいところだが, 面倒らしいので後回しにする.
次は Cloud Functions だ.
こちらはもう少しまともに作る. 新たなアクションが追加されると, アクションに応じて fields の内容を更新し, 同じ修正を fieldViews にも適用する.
const runDeckTransaction = (deckNames: string[], updateFun: (decks: Deck[])=>boolean) => {
const fields = fireStore.collection('fields');
const deckRefs = deckNames.map(d => fields.doc(d));
return fireStore.runTransaction(transaction => {
return transaction.getAll(...deckRefs).then(snapshot => {
const decks = snapshot.map(s => s.data() as Deck);
if (updateFun(decks)) {
const viewsRef = fireStore.collection('fieldViews');
for (let i = 0; i < deckRefs.length; ++i) {
transaction.update(deckRefs[i], decks[i]);
transaction.update(viewsRef.doc(deckNames[i]), createDeckView(decks[i]));
}
}
});
});
}
各種アクションに対応する処理はコピペが横行したので 共通化してみた.
上の関数は 「任意の数の現在のデックを取得して updateFun() を実行し, 成功したらその内容を fields と fieldViews に適用する. これらは トランザクション により アトミックに処理する.
何の説明もなく createDeckView() 関数を利用しているが, これは Deck から DeckView を作る. 説明の必要はなかろう.
最後に firestore.rules だ.
match /fields/{deckId} {
allow create, update: if true;
allow delete: if resource.data.cards.size() == 0;
}
match /fieldViews/{deckId} {
allow read, create, update: if true;
allow delete: if resource.data.cards.size() == 0;
}
fieldViews に これまでの fields と同じアクセス権を与えて, fields からは read 権限を削除した.
この修正で, アプリがこれまでと同じように動作することを確認する.
動作はおなじに見えるが, 今リッスンしている fieldViews には表の情報しか書かれていない.
cards に 0 が並んでいるが, 0 は「裏面デザイン」を指す CardSurface番号だ.
もし Card 型に表裏の状態を持たせていたら こんなスッキリした形にはならないだろう.
これが isFace を Card ではなく CardSurface に持たせた理由だ. (第11回の伏線回収)
少しずつ過去に付けた名前を見直しているが Deck や fields なども現状に合わなくなってきている. 後で付け直したい.
この記事が気に入ったらサポートをしてみませんか?