RxJSを使ってテトリスを実装してみた話

この記事は[「WACUL Advent Calendar 2016」](http://qiita.com/advent-calendar/2016/wacul)の3日目です。
今年の10月からWACULでフロントエンドエンジニアをしている@bokuwebと申します。

本記事では`RxJS`の勉強にテトリスを実装した話しを書いてみます。

## つくったもの

![screenshot.gif](https://qiita-image-store.s3.amazonaws.com/0/91510/68060beb-45d4-05c6-bd44-64d062b32a19.gif)


###### リポジトリ: https://github.com/bokuweb/rxjs-tetris
###### デモ: https://bokuweb.github.io/rxjs-tetris/

### デモの遊び方

`Enter`キーでゲームが開始します。(音が鳴るのでご注意ください。)

`→` : ブロックを右へ移動
`←` : ブロックを左へ移動
`↓` : ブロックを下へ移動
`SPACE` : ブロックを回転

## 実装

### 概要

先に全体の流れを図示すると以下のような構成になりました。
後述しますが、`Timer`/`KeyInput``Straem`から`Action`が発行され、ゲームのステートを更新していきます。更新されたゲームのステートから描画を行うとともに、ゲームオーバー判定や、行削除の有無などの判定を行い`Action`を投げる形となっています。

![Untitled.png](https://qiita-image-store.s3.amazonaws.com/0/91510/aaf872b0-a0c6-88b1-8eda-45161a3a6a81.png)
 
### Action

例えば`ゲームの開始`や`ブロックの回転`、`ブロックの移動`などのイベントを`Action`として定義しています。`Action`は`ActionSource$`を介して次のように発行されます。

`ActionSource$.next(new RemoveAction(removeRows))`

この辺は[Tackling State](https://vsavkin.com/managing-state-in-angular-2-applications-caf78d123d02#.yzvb60f7r)という記事を参考にしています。

``` ruby
// src/actions.ts
import { Subject } from "rxjs/Subject";

export const actionSource$ = new Subject();

export type Action = NextAction |
  StartAction |
  DownAction |
  LeftAction |
  RightAction |
  RotateAction |
  GameoverAction |
  RemoveAction;

export class NextAction {
  constructor () {}
}

export class StartAction {
  constructor () {}
}

export class DownAction {
  constructor () {}
}

export class LeftAction {
  constructor () {}
}

export class RightAction {
  constructor () {}
}

export class RotateAction {
  constructor () {}
}

export class GameoverAction {
  constructor () {}
}

export class RemoveAction {
  constructor (public removeRows: number[]) {}
}
```

### Key Input

キー入力は`fromEvent`で`keydown`イベントを変換`map`で各`Action`にマッピングし`ActionSource$`に渡しています。これで各キー入力に応じて`Action`が発行されることになります。

``` ruby
// src/index.ts
const keyEvent$ = Observable
  .fromEvent(document, 'keydown')
  .map((k: KeyboardEvent) => {
    switch(k.code) {
      case 'ArrowRight': return new RightAction();
      case 'ArrowLeft': return new LeftAction();
      case 'ArrowDown': return new DownAction();
      case 'Enter': return new StartAction();
      case 'Space': return new RotateAction();
      default: return;
    };
  })
  .filter((action) => !!action)
  .subscribe(actionSource$);
```

### Timer 

タイマーは`interval`にて作成しています。以下で`500ms`毎に`DownAction`が発行されることになります。

``` ruby
// src/index.ts
Observable
  .interval(500)
  .map(_ => new DownAction())
  .subscribe(actionSource$);
```

### game State

ゲームのステートとして、`ポーズ中かどうか`、`落下中のブロックのステート`、`フィールドのステート`を保持しています。ここは`Redux`でいうところの`Reducer`に相当しています。

すなわち、`scan`を使用して前述の`Action`を受け取る度に、前回の状態とActionを引数にもらい次回ステートを演算して返す処理を行います。

このように状態をストリーム内に保持するために`scan`はよく使用する`operator`だと思います。(ちなみに`reduce`というoperatorもありますが、こちらは`complete`しないと値が流れないため、`scan`のほうが使用頻度が高い印象があります。) 

このあたりは「`Reactive Programing with RxJS」 *1 `にも`Avoiding External State`、`Using scan`と書いてあります。パイプラインの外にステートを持ってしまうと、アプリケーションが複雑になった際にバグの温床となるということですね。

*1 この書籍はRxJS5には未対応なのでご注意ください。


``` ruby
// src/index.ts

interface AppState {
  isPaused: boolean;
  block: Block;
  field: Field;
}

const gameState$ = actionSource$
  .scan((state: AppState, action: Action) => {
    if (action instanceof StartAction) {
      return assign(state, { isPaused: false } );
    } else if (action instanceof DownAction) {
      if (state.isPaused) return state;
      if (!isLocked(state)) state.block.y += 1;
      return state;
    } else if (action instanceof LeftAction) {
      if (canMoveX(state, -1)) state.block.x -= 1;
      return assign({}, state);
    } else if (action instanceof RightAction) {
      if (canMoveX(state, 1)) state.block.x += 1;
      return state;
    } else if (action instanceof NextAction) {
      const block = getRandomBlock();
      const newField = createNewField(state.field, state.block);
      return assign(state, { block, field: newField });
    } else if (action instanceof RotateAction) {
      const rotatedBlock = assign({}, state.block, { shape: getRotatedShape(state.block.shape) });
      const block = isCollision(state.field, rotatedBlock) ? state.block : rotatedBlock;
      return assign(state, { block });
    } else if (action instanceof RemoveAction) {
      action.removeRows.forEach((rowIndex: number) => {
        state.field.splice(rowIndex, 1);
        state.field.unshift(range(config.ROW).map(() => 99))
      });
      return state;
    } else if (action instanceof GameoverAction) {
      return createInitState();
    } else {
      return state;
    }
  }, createInitState())
  .share()
```

上記の固まりをみてもわかりにくいですが、あるActionに着目してみると非常に単純な処理をおこなっているだけです。

例えば以下は`DownAction`を受け取ったらブロックを1マス下に移動したステートを作成し、次回ステートとして反映するということを行っています。

``` ruby
else if (action instanceof DownAction) {
  if (state.isPaused) return state;
  if (!isLocked(state)) state.block.y += 1;
  return state;

```

### Render

`gameState$`から必要なステートを`select`かつ、ブロックのステートと、フィールドのステートを合成して描画すべきフォールドを生成、`render`に渡しています。

``` ruby
// src/index.ts
gameState$
  .map((state: AppState) => ({
    isPaused: state.isPaused,
    field: createNewField(state.field, state.block)
  }))
  .subscribe(render);
```

`render`は蛇足ですが、`snabbdom/snabbdom`を使用しています。特に理由はないのですが、どうせなら使ったことのないものを触ってみようという意図です。

以下が記述になります。パッと見拒否反応を示す人もいるかもしれませんが、自分は`mithril.js`で遊んでいたこともあり、割りと好みです。

描画は`div`要素を敷き詰めて、`field`と`block`の状態を合成したものをマッピングしています。
ポーズの場合だけは、そのメッセージを表示するようにしています。

``` ruby
import { Renderer, h } from './renderer';
import { colors } from './shape';

const renderer = new Renderer();
renderer.mount(document.getElementById('container'), h('div'));

export const render = (state: any ) => {
  const vnode = h('div', [
    state.isPaused
      ? h('div', 'Press enter key to start')
      : h('div.field', state.field.map((row: number[]) => (
        h('div.raw', row.map(cell => (
          h(`div.cell${cell !== 99 ? '--active' : ''}`,
            { style: { backgroundColor: colors[cell] }})))
         )
      )))
  ]);
  renderer.update(vnode);
};
```

### Judge game state

ここでは流れたきたステートを見て、各種判定を行い、必要に応じて`Action`を発行しています。

まずは、接地判定を行い、接地していたら`NextAction`を発行し次のブロックを用意させます。
テトリスって接地した後も1マス分横にスライドできたりするんですよね。最初はこの辺無視してたんですが、いざ遊んでみると結構重要な要素っぽく、`bufferCount`で`FIFO`つくって対応しました。（この辺もっといいOperatorありそうな気がしますが。。）

また、この接地判定は行が消せるかどうかの判定にも使用したいため`share`でhot化しています。

``` ruby
const isLocked$ = gameState$
  .map((state: AppState) => isLocked(state))
  .bufferCount(2, 1)
  .map(lockedBuffer => (
    lockedBuffer.every(isLocked => isLocked)
  ))
  .share()

isLocked$
  .distinctUntilChanged()
  .filter(isLocked => !!isLocked)
  .map(_ => new NextAction())
  .do(() => beeper.next(100))
  .subscribe(actionSource$);
```

ここではゲームオーバー判定をおこなっています。次のブロックを用意した段階で衝突していたらゲームオーバーとしています。

``` ruby
gameState$
  .filter((state: AppState) => state.block.y === 0)
  .filter((state: AppState) => (
    isCollision(state.field, state.block) || isLocked(state)
  ))
  .map(_ => new GameoverAction())
  .delay(200)
  .subscribe(actionSource$);
```

ゲームのステートと`isLocked$`ストリームをcombineし、接地状態であれば、消せる行がないか走査するようにしています。横一列そろっている消すことが可能な行があった場合、音を鳴らし、`RemoveAction`を発行するようにしています。

`delay(50)`してるのは接地から削除までの隙間時間を設けるために入れました。これがないと本当に接地した瞬間に消えてしまい`ブロックを置いた感`が無かったからです。

``` ruby
gameState$
  .combineLatest(isLocked$)
  .delay(50)
  .filter(([_, isLocked]) => isLocked)
  .map(([state, _]: [AppState, boolean]) => (
    getRemovableRows(state.field)
  ))
  .filter(removeRows => removeRows.length > 0)
  .do(() => beeper.next(400))
  .map(removeRows => new RemoveAction(removeRows))
  .subscribe(actionSource$);
```

### Audio（おまけ）

これは完全に蛇足なんですが、音が全くならないとフィードバックがなくイマイチだったのでWebAudioで矩形波をつくって、適当な周波数を放り込んでいます。これにより、ブロックが消えたときに`ピロ`ってなります。

``` ruby
src/sounder.ts
import { Subject } from 'rxjs';

const audio = new window.AudioContext;
export const beeper = new Subject();
beeper.sampleTime(100).subscribe((hz: number) => {
    const oscillator = audio.createOscillator();
    oscillator.connect(audio.destination);
    oscillator.type = 'square';
    oscillator.frequency.value = hz;
    oscillator.start();
    oscillator.stop(audio.currentTime + 0.1);
});
```

例えば以下で400Hzの音が鳴ることになります。

``` ruby
beeper.next(400);
```
   
## まとめ

他にも細かいものがありますが、大まかには上記のストリーム等をつなげていくことでこのテトリスは構築されています。

テトリスが`RxJS`の学習の題材としてよかったかというと、疑問符がつきますが、それでも学ぶところも多く楽しい題材でした。ただ、やはり`RxJS`が一番生きる題材と言えばもう少し時間的な制御が必要なものだったかもしれません。

最後に勉強するに当って参考になって記事やページを挙げておきます。

## 参考記事

社内フロント勉強会の`Rx`回では以下の`Learn RxJS`の★付きOperatorをみんなで眺めました。なかなかよかったですが、サンプルがひどいケースもあるので他の資料やマーブルダイアグラムも合わせて眺めるのが効果的でした。

その中でも`Rx逆引き`とその中でリンクが貼られている、`RxJavaのjavadoc`を見ると結構よかったです。

- [Learn RxJS](https://www.learnrxjs.io/operators/)

- [Rx逆引き](http://wilfrem.github.io/learn_rx/operators.html) 

- [RxJava](http://reactivex.io/RxJava/javadoc/rx/Observable.html)

- [Tackling State](https://vsavkin.com/managing-state-in-angular-2-applications-caf78d123d02#.yzvb60f7r)
 
- [Functional Reactive Game Programming – RxJS Breakout](https://manu.ninja/functional-reactive-game-programming-rxjs-breakout)