現在時刻に依存しないタイマーモデル

# 要件
- 残り時間のあるゲームを作る。
- 成功すると +1秒, 失敗すると -1.5秒 残り時間に加算される。
- 5 秒経過でゲームオーバー
- 残り時間は5秒を越えない、また負の値にはならない
- ポーズ機能欲しいな〜（最初無かったんだけどあとから友人に指摘されました）

# 仕様
## 残り時間(msec)を使って初期化
```js
var timer = new Timer(5000)  // 5 sec
```

## start(currentTime) で開始
```
timer.start(Date.now());
```

## add(currentTime, msec) で秒数加算
```
timer.add(Date.now(),  1000); // 成功時
timer.add(Date.now(), -1500); // 失敗時
```

## pause(currentTime) で一時停止, resume(currentTime) で再開
```
timer.pause(Date.now());
timer.resume(Date.now());
```

## timeup(currentTime) で終了判定＆（終了していたら）状態更新
```
timer.timeup(Date.now());
```

## reset() でスタート前に戻る
```
timer.reset();
```

以上は内部状態によっては何も起こらないこともある（例えば `Pausing` 状態では `timeup` しない、など）

## ステートマシン図
![Timer2.png](https://qiita-image-store.s3.amazonaws.com/0/38864/e5ebc0d3-57b0-3d0c-3b30-10d13407ba11.png)


# 実装
## 準備
`javascript-state-machine` という npm パッケージを使用しています。

```
npm install --save-dev javascript-state-machine
```

## ソースコード

```js:models/Timer.js
"use strict";

import StateMachine from "javascript-state-machine"

/*
 *  waiting    | running                      | finished
 * ------------|------------*-----------------|------------
 *             `startTime   `currentTime      `endTime
 */

export default class Timer {
  constructor(max) {
    this.max = max;
    var startTime, endTime, pauseTime;

    var setSpan = (n) => {
      startTime = n;
      endTime = n + max;
    }

    var fsm = StateMachine.create({
      initial: "Waiting",
      events: [
        {name: "start",  from: "Waiting",  to: "Running"},
        {name: "add",    from: "Running",  to: "Running"},
        {name: "pause",  from: "Running",  to: "Pausing"},
        {name: "resume", from: "Pausing",  to: "Running"},
        {name: "timeup", from: "Running",  to: "Finished"},
        {name: "reset",  from: "Finished", to: "Waiting"}
      ],
      callbacks: {
        onWaiting: () => { setSpan(Infinity); },
        onstart: (ev, f, t, now) => {
          setSpan(now);
        },
        onadd: (ev, f, t, now, msec) => {
          setSpan(Math.min(startTime + msec, now));
        },
        onbeforepause: (ev, f, t, now) => startTime <= now,
        onenterPausing: (ev, f, t, now) => {
          pauseTime = now;
        },
        onleavePausing: (ev, f, t, now) => {
          setSpan(startTime + now - pauseTime);
          pauseTime = null;
        },
        onbeforetimeup: (ev, f, t, now) => endTime <= now
      }
    });

    var elapsedTime = {
      Waiting: () => 0,
      Running: (now) => Math.min(max, Math.max(0, now - startTime)),
      Pausing: (now) => elapsedTime.Running(Math.min(now, pauseTime)),
      Finished: () => max
    }

    this.elapsedTime = (now) => elapsedTime[fsm.current](now);
    this.remain = (now) => max - this.elapsedTime(now);
    this.percent = (now) => Math.floor(100 * this.elapsedTime(now) / max);

    var run = (ev) => function() {
      return fsm.can(ev) && fsm[ev].apply(fsm, arguments);
    }

    this.start = run("start");
    this.add = (now, msec) => {
      this.timeup(now);
      run("add")(now, msec);
      this.timeup(now);
    }
    this.pause = (now) => {
      this.timeup(now);
      return run("pause")(now);
    }
    this.resume = run("resume");
    this.timeup = run("timeup");
    this.reset = run("reset");
    this.currentState = () => fsm.current;
    this.is = (s) => fsm.is(s);
    this.can = (e) => fsm.can(e);
  }
}
```

単に全てのメソッドで必ず現在時刻を渡しているだけ。 setTimeout などを使用していないので収まりが良くテストもしやすい（書いてないけど）。