Building a memory game with Blocの手習い(Chapter 5)

これはSmalltalk Advent Calendar 2017の記事です。

前回に引き続いて、Bloc のチュートリアルである Building a memory game with Bloc のChapter 5(Adding Interaction) の内容をやっていく。
元ネタは
https://github.com/pharo-graphics/Bloc
の中にある、
http://files.pharo.org/books-pdfs/booklet-Bloc/2017-11-09-memorygame.pdf
である。

このチュートリアル最後の章にきた。Memory game のモデルとビューが出来上がり、この章ではユーザーとのインタラクションを追加する。
このゲームでは、イベントリスナによるイベントのハンドリングと、モデルから送られる通知を用いてインタラクションを実現する(らしい)。

5.1 An event listener

まずイベントリスナを追加する。イベントリスナは BlElementEventListner を継承して作る。

BlElementEventListner subclass: #MgdCardEventListner
  instanceVariableNames: 'memoryGame'
  classVariableNames: ''
  package: 'Bloc-MemoryGame-Demo-Element'.

ゲームのモデルをインスタンス変数として保持し、ゲーム状態を更新できるようにする。セッターメソッドも作成する。

MgdCardEventListner >> memoryGame: aGameModel
  memoryGame := aGameModel.

クリックでデバッガが起動するように clickEvent: メソッドをオーバーライドする。

MgdCardEventListner >> click: anEvent
  self halt.

5.2 Adding event listeners

どのカードがクリックされたかを検出するために、作成したイベントリスナをカードのエレメントに登録しなければならない。カードごとにイベントリスナを作るのは時間もメモリも勿体無いので、ゲーム盤のエレメントでイベントリスナを作成しておき、カードのエレメントの生成時に一括して登録する。

MgdGameElement >> newCardEventListner
  ^ MgdCardEventListner new
MgdGameElement >> memoryGame: aGameModel
  | aCardEventListner |
  memoryGame := aGameModel.
  aCardEventListner := self newCardEventListner memoryGame: memoryGame.
  self layout columnCount: memoryGame gridSize.
  memoryGame availableCards
    do: [ :aCard |
      | cardElement |
      cardElement := self newCardElement card: aCard.
      cardElement addEventHandler: aCardEventListner.
      self addChild: cardElement ].

イベントリスナの動作を確かめるために今までの Playground 内での表示から、ちゃんとしたウィンドウでの表示を行う。

game := MgdGameModel new initializeForSymbols: '12345678'.
grid := MgdGameElement new.
grid memoryGame: game.
space := BlSpace new.
space extent: 420 @ 420.
space root addChild: grid.
space show.

上記を選択して Do it (Command-D または Ctrl-D)を選ぶと下のようにカードが並んだウィンドウが表示される。

適当なカードをクリックすると、self halt のノーティファイアが表示される。

このまま Proceed を押せばノーティファイアは消える。
ただ、ここで Abandon を選んだり、ノーティファイアを閉じたりするとおかしなことになる。おかしなこととは、再度コードを実行すると次のような空のウィンドウになってしまうことだ。

これ以降、何度やっても同じ状態となる。これを解決するのに10分くらいかかってしまった。解決方法は2章の最後に書いた、ヤヴァイ時のおまじないを実行すること。

BlUniverse reset.

これを Do it すれば、元のように表示されるようになる。なるほど。

5.3 Specialize clickEvent:

self halt で止まるようになっている clickEvent: を書き換えてクリックに適切に反応するように修正する。
clickEvent: に渡されるイベントオブジェクトに対して currentTarget メッセージを送るとクリックされたエレメントを得ることができる。2章で作成したモデルでは、chooseCard: メソッドでカードが選ばれた時の処理を行っているので、カードのエレメントからモデルを得て、引数として与えれば良い。

MgdCardEventListner >> clickEvent: anEvent
  memoryGame chooseCard: anEvent currentTarget card.

上記の変更を行えば、モデル上ではクリック時にカードが選ばれた際の処理が行われるが、画面上には何の変化も現れない。モデルの状態変化に対してエレメントの表示を変える方法は次の節で行う。

5.4 Connecting the model to the UI

この節ではモデルからエレメントへの通信について説明している。
モデルは通知(announcement)を出すが、エレメントを直接知っているわけではない。エレメントは通知を受け取るように登録することで適切に反応できるようにする。
まず、通知を受け取るメソッドを作り、トレースできるようトランスクリプトに出力させる。

MgdRawCardElement >> onDisappear
  Transcript show: 'On disappear'; cr.
MgdRawCardElement >> onFlipped
  Transcript show: 'On flipped'; cr.

次にこれらの通知を受け取った時にこれらのメソッドが実行されるように、カードのエレメントのセッターメソッドで登録する。

MgdRawCardElement >> card: aMgCard
  card := aMgCard.
  card announcer when: MgdCardFlippedAnnouncement send: #onFlipped to: self.
  card announcer when: MgdCardDisappearAnnouncement send: #onDisappear to: self.

カードがクリックされると、イベントを受け取ったエレメントがモデルの状態を変え、モデルが送出した通知を受け取ったエレメントがトランスクリプトにログを出力する。
このままではログ出力だけなので、カードを裏返した時に表示を書き換えるようメソッドを修正する。

MgdRawCardElement >> onFlipped
  Transcript show: 'On flipped'; cr.
  self invalidate.

5.5 Handling disappear

揃ったカードを取り除く(ように表示する)方法として2種類が紹介されている。一つはカードを透明にする(透明度を変える)方法である。

MgdRawCardElement >> onDisappear
  Transcript show: 'On disappear'; cr.
  self opacity: 0.

透明にする場合、カードはその場所に残り、イベントを受け取ることになる。
もう一つは非表示に設定する方法である。

MgdRawCardElement >> onDisappear
  Transcript show: 'On disappear'; cr.
  self visiblity: BlVisibility hidden.

非表示に設定した場合、1列(または1行)のカードがなくなると、自動的にレイアウトが詰められてしまう。

5.6 Refreshing on missed pair

この章の内容は特に意味がない。以前の章の作業により完了している。

5.7 Conclusion

これで Memory game の実装は終わった。実際にゲームで遊ぶことができる。
ただ、私の環境ではトラブルが発生した。具体的にはカードを裏返しすると何も表示されなくなるというものである。常に発生するわけではなくゲーム途中から次第に見えなくなっていく。
どうも3章の drawFlippedSideOn: で、文字表示がおかしいのでフォントを変えたのだが、それが悪かったようだ。元の Source Sans Pro に戻したところ大きさも問題なく表示されるようになった。
一応、修正後のメソッドを記しておく。

drawFlippedSideOn: aCanvas
  | font origin textPainter metrics |
  font := aCanvas font
    named: 'Source Sans Pro';
    size: 50;
    build.
  textPainter := aCanvas text
    font: font;
    paint: Color white;
    string: self card symbol asString.
  metrics := textPainter measure.
  origin := (self extent - metrics textMetrics bounds extent) / 2.0.
  origin := origin - metrics textMetrics bounds origin.
  textPainter
    baseline: origin;
    draw.

まだ不安定な部分があるのだろうか。次は自分のプロジェクトで Bloc を使ってみたい。