p5.jsとDjangoでインタラクティブシミュレーション(4) p5.jsとDjangoの間のajaxでのデータのやり取り

# はじめに

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

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

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

# p5.jsとDjangoの間のajaxでのデータのやり取り

前回まででインタラクティブシミュレーションのプログラムをp5.jsで作成しwebに公開することができるようになった．実は，それだけが目的ならあえてDjangoを後ろに置く必要はない．

では，なぜp5.jsをDjangoと組み合わせるのかというと，シミュレーションのログをサーバ側に引き取ってデータベースに格納したり，サーバ側で適切に設定したパラメータをシミュレーションに送り込んだり，といったことを実現するためである．

インタラクティブシミュレーションを研究の手段として用いるためには，これらの機能は欠かすことができない．今回は，そうした機能の基礎として，p5.jsのシミュレーションとDjangoの間のajaxでのデータのやり取りの基本を押さえよう．

# Django側の準備

最初にDjangoにログデータを格納するためのデータ構造のクラスを追加しておこう．具体的には，sim/models.pyに下記の内容を書き込む．

```python
from django.db import models

class Game(models.Model):
    score = models.IntegerField(default=0)
    def __str__(self):
        return u"game_{}".format(self.pk)

class State(models.Model):
    game = models.ForeignKey(Game, on_delete=models.CASCADE)
    time = models.FloatField()
    vol = models.IntegerField()
    ordered = models.IntegerField()
    outs = models.IntegerField()
    holding_cost = models.FloatField()
    ordering_cost = models.IntegerField()
    revenue = models.IntegerField()
    def __str__(self):
        return u"state_{}_{}".format(self.game.pk, self.pk)
```

Gameクラスは，インタラクティブシミュレーションの1試行に対応しており，その試行のスコアの値を保持するようになっている．Stateクラスは，シミュレーションモデル（`my_model`）の`stateLog`の各要素に対応するデータを保持するためのデータ構造（ただし，簡単のため，`ordered`はリスト全体ではなくその先頭の値のみを整数で保持するようにしている）であり，多対1の関係でGameクラスと関係づけられている．

これらのクラスはadminサイトにも登録しておくと便利である．そのためには，sim/admin.pyに次の内容を書き込んでおけばよい．

```python
from django.contrib import admin
from . import models

class StateInline(admin.TabularInline):
    model = models.State
    extra = 0

class GameAdmin(admin.ModelAdmin):
    inlines = [StateInline]

admin.site.register(models.Game, GameAdmin)
```

続いて，ajaxでのデータ通信を処理するためのview関数を用意する．具体的には，sim/views.pyの中に次の2つの関数を定義する．

```python
import json
from django.shortcuts import render
from django.http import JsonResponse, HttpResponseServerError
from . import models

def get_games(request):
    games = models.Game.objects.all().order_by("-score").values()
    gamelist = list(games)
    return JsonResponse(gamelist, safe=False)

def post_logs(request):
    if request.method == 'POST' and request.body:
        json_dict = json.loads(request.body)
        score = int(json_dict['score'])
        game = models.Game.objects.create(score=score);
        for log in json_dict['logs'].values():
            models.State.objects.create(
                game=game,
                time=float(log['time']),
                vol=int(log['vol']),
                ordered=int(log['ordered']),
                outs=int(log['outs']),
                holding_cost=float(log['hc']),
                ordering_cost=int(log['oc']),
                revenue=int(log['rv']),
            );
        return JsonResponse(json_dict)
    else:
        return HttpResponseServerError()
```

`get_games()`関数は，データベースから取得した（Gameオブジェクトの）クエリセットをリストにして，さらにそれを`JsonResponse`に変換して返していることがみてとれる．これがGETでのajaxリクエストに応える典型的なパターンである．

一方，`post_logs()`関数の方は，クライアント側からPOSTで受け取ったログデータをデータベースに格納する処理を担当している．

送られてくるのはJSON形式のデータ（`request.body`）であり，それをまず`json.loads()`で`python`の辞書に変換する．この段階では，まだ辞書の値は文字列になっているので，続いて，必要に応じて，`int()`や`float()`で型変換を施しているのがわかる．

また，GameとStateのどちらにも`create()`メソッドを用いているが，このメソッドは，新しいオブジェクトの生成とセーブを同時に行う（ため，後で`save()`メソッドを呼ぶ必要はない）．これは，POSTでのajaxリクエストに応える典型的なパターンである．

これらのview関数をテンプレート側から利用できるようにするためにurlルーティングも追加しておく．sim/urls.pyを次のように変更しておこう．

```python
from django.urls import path
from django.views.generic import TemplateView
from . import views

app_name = 'sim'

urlpatterns = [
    path('', TemplateView.as_view(template_name='sim/index.html'), name='index'),
    path('get/', views.get_games, name='get_games'),
    path('post/', views.post_logs, name='post_logs'),
]
```

これでDjango側の準備は整った（ただし，makemigrationsとmigrateを忘れないこと）．続いて，p5.jsのコードの方にDjangoとのやり取りに関する機能を追加していく．

## p5.js側の拡張

まず，シミュレーション終了時に`show_results()`で表示する画面に，今回のスコアの詳細だけでなく，これまでのベストスコアや，今回の順位についての情報を含めるようにする．

そのために，`update()`メソッドの

```javascript
if(e.type == "over") {
  my_model.show_results();
  noLoop();
```

の箇所を次のように書き換える．

```javascript
if(e.type == "over") {
  loadJSON("/sim/get/", my_model.close_game.bind(this));
  noLoop();
```

`loadJSON()`は，p5.jsに用意されているJSONデータをajaxで取得してくるための関数である．第1引数がurlを指定しており，上で指定したurlルーティングによって，これが`get_games()`のview関数に紐付けられることがわかる．

第2引数はいわゆるコールバック関数で，JSONデータを取得し終わったらそのデータを引数にしてこの関数が呼ばれることになる．ここでは，コールバック関数として，`my_model`の`close_game()`メソッドに`.bind(this)`をつけて利用している．

`close_game()`メソッドは次のように定義した．

```javascript
Model.prototype.close_game = function(games) {
  var scores = [];
  for(var game of games) {
    scores.push(parseInt(game.score));
  }
  var my_score = this.calc_score();
  var best_score = my_score;
  var my_rank = 1;
  if(scores.length > 0) {
    best_score = max(best_score, scores[0]);
    for(var score of scores) {
      if(score > my_score) {
        my_rank ++;
      } else {
        break;
      }
    }
  }
  this.show_results(my_score, best_score, my_rank, scores.length +1);
  this.save_log();
}
```

view関数`get_games()`から返されてくるJSONデータから過去のスコアのリスト（`scores`）を作成し，それと今回のスコア（`my_score`）を比較しながら，今回の順位（`my_rank`）やこれまでのベストスコア（`best_score`）を算出していることがわかる．

最後にそれらのデータを引数として，`show_results()`メソッドと`save_log()`メソッドを呼んでいる．ここでは，`show_results()`の詳細は省略するが，今回のスコアに加えて，順位やベストスコアなども表示するように少し拡張を加えた．

また，ゲームスコアの計算は`calc_score()`というメソッドにまとめている．

```javascript
Model.prototype.calc_score = function() {
  return floor(
    this.state.rv -this.state.hc -this.state.oc -this.state.outs *this.par.SOP
  );
}
```

`save_log()`メソッドについて紹介する前に，`.bind(this)`についてみておこう．これはjavaScriptの`this`の挙動と関係がある．

仮に，`close_game()`を`my_model`のメソッドとして単純に，

```javascript
my_model.close_game();
```

のように呼び出した場合は，直感どおり，`this`は`my_model`オブジェクトを指す．

ところが，`my_model.close_game`がコールバック関数として別の関数などに渡された場合は，`close_game()`の関数としての定義だけが引数として送り込まれ，それが後で利用される．その際，引数として送り込まれた関数と`my_model`オブジェクトとの対応関係は見えなくなり，`this`は`my_model`を指さなくなってしまう（グローバルオブジェクトを指すようになる）．

そこで，コールバック関数（だけではないが）の呼び出し時に，強制的に`this`を指定できるようにするのが`.bind()`の機能である．上では，`update()`メソッドが動いている際の`this`（すなわち`my_model`）を`.bind()`の引数に与えて，`my_model.close_game`が呼び出されたときの`this`が`my_model`を指すように仕組んでいるわけである．

続いて，`save_log()`メソッドについてみていく．これは，ログデータをajaxでDjangoに送り込むためのメソッドである．

このメソッドでは，結果としてデータベースに変更を加えることになるため，POSTを利用する．p5.jsには`httpPost()`というPOSTのための関数も用意されているが，Djangoのcsrf対策のために必要なヘッダ情報を付与することができないので，Djangoとの通信に利用するのは難しい．

そこで，[JavaScriptクッキーライブラリ](https://github.com/js-cookie/js-cookie/)と[axios](https://github.com/axios/axios)を用いることにする．まず，これらを利用できるようにするために，index.htmlの中に次の2行を加えておく．

```javascript
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/js-cookie@2/src/js.cookie.min.js"></script>
```

そして，`save_log()`メソッドを次のように定義する．

```javascript
Model.prototype.save_log = function() {
  var csrftoken = Cookies.get('csrftoken');
  var headers = {'X-CSRFToken': csrftoken};
  var data = {
    "score": this.calc_score(),
    "logs": {}
  };
  for(var i = 0; i < this.stateLog.length; i++) {
    var log = Object.assign({}, this.stateLog[i]);
    log.ordered = log.ordered.length ? log.ordered[0] : 0;
    data.logs[String(i)] = log;
  }
  console.log(data);
  axios.post("/sim/post/", data, {headers: headers})
    .then(function (response) {
      console.log(response);
    })
    .catch(function (error) {
      console.log(error);
    });
}
```

2行目と3行目でcsrf対策のためのヘッダ情報（`headers`）を作成し，それを`axios.post()`を呼ぶ際にデータと一緒に渡していることがわかる．データの中身はスコアと`stateLog`であり，上で作成したview関数`post_logs()`に対応した形式でJSONデータを送り込んでいる．

ここまでで，シミュレーションが終了すると，それに対応したログデータがデータベースに格納されるようになった．

## シミュレーションの再実行ボタン

もう一度シミュレーションを最初から開始したくなった場合は，このままでもリロードすればよいが，最後に，そのためのボタンを追加してみよう．`setup()`の中に，ボタンを1つ付け加える．

```javascript
var reset_btn = createButton("Play Again");
reset_btn.parent("buttons");
reset_btn.mousePressed(reset_sim);
```

そして，このボタンに対応するコールバック関数`reset_sim()`を次のように定義する．

```javascript
function reset_sim() {
  if(frameCount >= 900) {
    my_model = new Model();
    frameCount = 0;
    loop();
  }
}
```

これで，このボタンを押せば再度シミュレーションが最初から開始されるようになった．

# おわりに

以上で最終回も終了．

ここまで読んでくださった方に感謝． ｍ（＿＿）ｍ