p5.jsとDjangoでインタラクティブシミュレーション(3) シミュレーションをインタラクティブにしてみよう

# はじめに

筆者のラボでは，人を含むシステムの挙動を分析するために，またそうしたシステムの中に置かれた人の挙動を分析するために，ユーザが途中で介入できる形式のコンピュータシミュレーション（インタラクティブシミュレーション）をウェブ上のゲームとして開発し，よく利用している．

このドキュメントは，そうしたインタラクティブシミュレーションのアプリケーションをp5.jsとDjangoを用いて開発するための基礎を身につけてもらうことを狙いとしたもので，あまり一般的なニーズはないかもしれないが，もし多少でもどなたかの参考になれば幸いだ．

今回は全4回中の3回目で，全体のコードはまとめて[GitHub](https://github.com/j54854/myP5js)に置いた．

# シミュレーションをインタラクティブにしてみよう

今回は，在庫管理モデルのシミュレーションにインタラクティブな機能を組み込んで，少しゲームらしくしてみよう．具体的には，定量発注方式で在庫量が発注点まで減ってきたら自動的に所定の量を発注していた部分を削除し，発注のタイミングと量をユーザがインタラクティブに決定できるようにする．下のスケッチが到達点のイメージである（いつか動画に差替えたい）．

<img width="640" alt="スクリーンショット 2019-04-11 19.55.07.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394879/b51b9486-6f46-9e2e-c290-8f5cb1fdb2f0.png">

## UI要素の準備

p5.jsにはマウスやキーの操作に関連したイベントリスナーやマウスの座標を参照するための変数が標準装備されているので，スケッチ内にインタラクションのための機能を盛り込むこともできるが，ここでは，簡単のために，p5.domのライブラリに用意されているUI要素を利用することにする．

index.htmlを見直してみると，p5.dom.min.jsがすでに読み込まれていることが確認できる（ので，p5.domライブラリの機能はすでに利用可能になっている）．

UI要素を配置するためのdivを用意しておこう．index.htmlのbody要素を下記のように少し書き換える．

```html
<body>
  <h1>Interactive Simulation</h1>
  <div id="mysketch"></div>
  <div id="buttons"></div>
  <h1>Please Enjoy!</h1>
</body>
```

インタラクションに利用するUI要素は，このbuttonsというidのdivの中に配置していく．具体的には，`setup()`の中に下記のコードを書き加えよう（p5.domライブラリに用意されている機能の詳細は[ここ](https://p5js.org/reference/#/libraries/p5.dom)を参照してほしい）．

```javascript
order_input = createInput("How many?");
var order_btn = createButton("Order");
var stop_btn = createButton("Pause");
var resume_btn = createButton("Resume");

order_input.parent("buttons");
order_btn.parent("buttons");
stop_btn.parent("buttons");
resume_btn.parent("buttons");

order_btn.mousePressed(place_order);
stop_btn.mousePressed(noLoop);
resume_btn.mousePressed(loop);
```

ボタンや入力ボックスを作成し，それらを上で作成したbuttonsのdivの子要素に設定して，最後に各ボタンが押されたときに実行するコールバック関数を定義している．

`order_input`にだけ頭に`var`がついていないのには意味がある．後で定義する関数`place_order()`の中でこれを参照したいので，`setup()`内のローカル変数としてではなく，大域変数として定義しておく必要があるからである．`setup()`の外に，

```javascript
var order_input;
```

と書いておこう．

## 介入効果を実現するためのコールバック関数

`stop_btn`のコールバック関数は`noLoop()`，`resume_btn`のコールバック関数は`loop()`になっている．これらはp5.jsの関数で，それぞれ`draw()`のループを止める，再開するという機能に対応している．したがって，これらのボタンでシミュレーションを途中で停止させたり，再開させたりすることが可能になる．

発注の機能は，`order_input`の入力ボックス，`order_btn`とそのコールバック関数`place_order()`で実現していく．これについて見ていく前に，少しモデルを修正しておくことにする．

```javascript
function Model() {
  this.par = {
    MTB: 2,  //mean time between shipments
    LT: 10,  // lead time to replenishment
    HC: 1,  // stock holding cost
    OC: 500,  // ordering cost
    SOP: 250,  // stock out penalty
    RV: 100,  // sales revenue
  };
  this.state = {
    time: 0,  // what time is it now?
    vol: 20,  // stock volume at hand
    ordered: [],  // quantities ordered
    outs: 0,  // number of stockouts
    hc: 0,  // total stock holding cost
    oc: 0,  // total ordering cost
    rv: 0,  // total revenue
  };
  this.calendar = new Calendar([
    {time:exp_rand(1 /this.par.MTB), type:"ship_out"},
    {time:900, type:"over"}
  ]);
  this.stateLog = [this.state];
}
```

モデルのパラメータ（`par`）と状態変数（`state`）に少し変更を加えている．これは，売上とコストを導入してそれらの差額で最終的な収益（＝ゲームのスコア）を計算するようにしたいためである．

メソッド`reduce()`は下のように更新した．

```javascript
Model.prototype.reduce = function() {
  if(this.state.vol > 0) {
    this.state.vol --;
    this.state.rv += this.par.RV;
  } else {
    this.state.outs ++;
  }
  this.calendar.extend({
    time: this.state.time +exp_rand(1 /this.par.MTB),
    type: "ship_out"
  });
  this.par.MTB = max(0.1, this.par.MTB +random(-0.1, 0.1))
}
```

在庫量が発注点を下回ると自動的に発注をかけ，それに対応する補充イベントをカレンダに追加していた部分は削除した．

また，4行目で，1つ出荷されるごとに売上（`rv`）の値を一定値ずつ増加させるようにしている．最後の部分で`MTB`の値をランダムウォークで変化させているのは，需要を徐々に変化させるようにしてゲーム性を高めるためである．

`update()`にも少しコードを追加している．

```javascript
Model.prototype.update = function() {
  var e = this.calendar.fire();
  this.state = Object.assign({}, this.state);
  this.state.ordered = this.state.ordered.concat();
  this.state.hc += (e.time -this.state.time) *this.state.vol *this.par.HC;
  this.state.time = e.time;
  if(e.type == "over") {
    my_model.show_results();
    noLoop();
  } else if(e.type == "ship_out") {
    this.reduce();
  } else if(e.type == "refill") {
    this.raise();
  }
  this.stateLog.push(this.state);
}
```

4行目に追加しているのは，在庫保管コスト（`hc`）の更新式である．このコストは在庫量と保管期間の長さに比例する．また，7行目で，シミュレーション終了時に結果を表示するメソッド`show_results()`を呼ぶようにしている．

`show_results()`は新しく下記のように作成した．

```javascript
Model.prototype.show_results = function() {
  background(200);
  var my_ratio = width /1000;
  push();
  scale(my_ratio);
  translate(50, 100);
  var my_score = this.state.rv -this.state.hc -this.state.oc -this.state.outs *this.par.SOP;
  textSize(40);
  text("Your Score: " +floor(my_score), 0, 0);
  textSize(20);
  text("Sales Revenue: " +floor(this.state.rv), 100, 100);
  text("Holding Cost: " +floor(this.state.hc), 100, 140);
  text("Ordering Cost: " +floor(this.state.oc), 100, 180);
  text("Stockout Penalty: " +floor(this.state.outs *this.par.SOP), 100, 220);
  pop();
}
```

上で説明した，ゲームスコアとしての収益のほか，売上（`rv`），在庫保管コスト（`hc`），発注コスト（`oc`），欠品ペナルティの値を表示している．

最後に，発注のためのインタラクションを実現するメソッド`place_order()`を作っておこう．

```javascript
function place_order() {
  var oq = parseInt(order_input.value());
  if(Number.isNaN(oq)) oq = 0;
  my_model.calendar.extend({
    time: frameCount +my_model.par.LT,
    type: "refill"
  });
  my_model.state.ordered.push(oq);
  my_model.state.oc += my_model.par.OC;
}
```

入力ボックス`order_input`の値を整数に変換し，補充イベントを作成してカレンダに追加してることがわかる．あわせて発注コスト（`oc`）の値も更新している．なお，3行目は，入力ボックスに数値以外のデータが入っていたりしてうまく整数に変換できなかったときのエラー処理である．

これで，単純だけど，インタラクティブなシミュレーションが完成した．試しに動かしてみてほしい．

# おわりに

第3回はここで終了．[第4回](https://qiita.com/j54854/items/0627d4b38913bb463875)に続く．