要件
- 残り時間のあるゲームを作る。
- 成功すると +1秒, 失敗すると -1.5秒 残り時間に加算される。
- 5 秒経過でゲームオーバー
- 残り時間は5秒を越えない、また負の値にはならない
- ポーズ機能欲しいな〜(最初無かったんだけどあとから友人に指摘されました)
仕様
残り時間(msec)を使って初期化
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
しない、など)
ステートマシン図
実装
準備
javascript-state-machine
という npm パッケージを使用しています。
npm install --save-dev javascript-state-machine
ソースコード
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 などを使用していないので収まりが良くテストもしやすい(書いてないけど)。