見出し画像

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 には表の情報しか書かれていない.

FireShot Capture 027 - card-game-field - Database - Firebase コンソール - console.firebase.google.com

cards に 0 が並んでいるが, 0 は「裏面デザイン」を指す CardSurface番号だ. 
もし Card 型に表裏の状態を持たせていたら こんなスッキリした形にはならないだろう.

これが isFace を Card ではなく CardSurface に持たせた理由だ. (第11回の伏線回収)


少しずつ過去に付けた名前を見直しているが Deck や fields なども現状に合わなくなってきている.  後で付け直したい.


この記事が気に入ったらサポートをしてみませんか?