ライブページ

ライブページは、サーバーと継続的に通信し、リアルタイムに更新されるため、連続した時間のゲームが可能です。ライブページは、ユーザー同士のやりとりが多いゲームに向いています。

こちら で多くの例を確認できます。

サーバーへのデータ送信

テンプレートのJavaScriptコードでは、サーバーにデータを送信したいときに liveSend() 関数を呼び出します。例えば、ユーザーに代わって 99 の入札を行う場合は、次のように呼び出します:

liveSend(99);

このメッセージを受信する関数を定義します。この関数の引数は、送信されたデータです。

class MyPage(Page):
    @staticmethod
    def live_method(player, data):
        print('received a bid from', player.id_in_group, ':', data)

oTree Studio を使用している場合は、名前が live_ で始まるプレイヤー関数を定義する必要があります。(なお、 WaitPagelive_method はまだサポートされていません。)

ページへのデータ送信

データを送り返すには、メッセージを受け取るプレイヤーの ID をキーにした辞書型を返します。例えば、メッセージを送ってきた人に "thanks" と送るメソッドは以下の通りです:

def live_method(player, data):
    return {player.id_in_group: 'thanks'}

複数のプレイヤーに送信するには、そのプレイヤーの id_in_group を使用します。例えば、このメソッドはすべてのメッセージをプレイヤー2とプレイヤー3に転送します:

def live_method(player, data):
    return {2: data, 3: data}

グループ全体に送信する場合は、 0 を使用します(実際の id_in_group ではないため、特殊なケースです)。

def live_method(player, data):
    return {0: data}

JavaScript で、 liveRecv 関数を定義します。この関数は、サーバーからメッセージを受信するたびに自動的に呼び出されます。

function liveRecv(data) {
    console.log('received a message!', data);
    // your code goes here
}

例: オークション

class Group(BaseGroup):
    highest_bidder = models.IntegerField()
    highest_bid = models.CurrencyField(initial=0)

class Player(BasePlayer):
    pass
def live_method(player, bid):
    group = player.group
    my_id = player.id_in_group
    if bid > group.highest_bid:
        group.highest_bid = bid
        group.highest_bidder = my_id
        response = dict(id_in_group=my_id, bid=bid)
        return {0: response}
<table id="history" class="table">
<tr>
  <th>Player</th>
  <th>Bid</th>
</tr>
</table>
<input id="inputbox" type="number">
<button type="button" onclick="sendValue()">Send</button>

<script>

  let history = document.getElementById('history');
  let inputbox = document.getElementById('inputbox');

  function liveRecv(data) {
      history.innerHTML += '<tr><td>' + data.id_in_group + '</td><td>' + data.bid + '</td></tr>';
  }

  function sendValue() {
    liveSend(parseInt(inputbox.value));
  }

</script>

(注: JavaScriptでは data.id_in_group == data['id_in_group'] となります。)

データ

送受信するデータは、どのようなデータタイプでも問題ありません。(JSONにシリアライズできるものであれば) 例えば、これらはすべて有効です:

liveSend(99);
liveSend('hello world');
liveSend([4, 5, 6]);
liveSend({'type': 'bid', 'value': 10.5});

最も汎用性の高いデータタイプは辞書型です。複数のメタデータ、特にメッセージの種類を含めることができるからです:

liveSend({'type': 'offer', 'value': 99.9, 'to': 3})
liveSend({'type': 'response', 'accepted': true, 'to': 3})

if 文を使って異なるタイプのメッセージを処理することができます:

def live_method(player, data):
    t = data['type']
    if t == 'offer':
        other_player = data['to']
        response = {
            'type': 'offer',
            'from': player.id_in_group,
            'value': data['value']
        }
        return {other_player: response}
    if t == 'response':
        # etc
        ...

履歴

デフォルトでは、参加者がページに到着する前に送信されたメッセージは表示されません。(また、ページを更新してもデータは再送信されません。)履歴を保存したい場合は、データベースに保存する必要があります。プレイヤーがページを読み込んだときに、JavaScript で liveSend({}) のように呼び出し、データベースからゲームの履歴を取得するように live_method を設計することができます。

エクストラモデル

ライブページは、個々のメッセージやアクションをデータベースに保存することができる エクストラモデル と一緒に使用されることが多いです。

ユーザをページに留めておく方法

ユーザーが次のページに進む前に、10通のメッセージを送信する必要があるとします。

まず、 {{ next_button }} を削除します。(または JS を使ってタスクが完了するまで非表示にします。)

タスクが完了したら、メッセージを送信します:

class Group(BaseGroup):
    num_messages = models.IntegerField()
    game_finished = models.BooleanField()


class MyPage(Page):
    def live_method(player, data):
        group = player.group
        group.num_messages += 1
        if group.num_messages >= 10:
            group.game_finished = True
            response = dict(type='game_finished')
            return {0: response}

そして、テンプレートでは、JavaScript でページを自動送信します:

function liveRecv(data) {
    console.log('received', data);
    let type = data.type;
    if (type === 'game_finished') {
        document.getElementById("form").submit();
    }
    // handle other types of messages here..
}

また、同様のテクニックを使って、独自の待ち受けページを実装することができます。例えば、一定のタイムアウト後に、すべてのプレイヤーが到着していなくても、続行できるようなページです。

ライブページに関する一般的なアドバイス

ここでは、一般的なアドバイスを紹介します (すべての状況に当てはまるわけではありません)。ロジックのほとんどを Python で実装し、JavaScript はページの HTML を更新するためにを使用することをお勧めします。理由は以下の通りです:

  • JavaScript は適切に使用するのが難しい言語である。
  • Python のコードはサーバー上で実行され、集中管理されていて信頼性が高い。JavaScript はクライアント上で実行されるが、クライアントは互いに同期が取れず、ページを閉じたりリロードしたりするとデータが失われる可能性がある。
  • Python のコードはサーバー上で実行されるため、より安全で、参加者が見たり変更したりすることができない。

例: 三目並べ

三目並べのゲームを実装するとしましょう。live_method が受け取るメッセージには2種類あります:

  1. ユーザーがマスをマークしたときに、他のプレイヤーに通知する必要がある
  2. ユーザーがページをロード (またはリロード) したときに、現在のボードレイアウトを送信する必要がある

1. については、 onclick などの JavaScript のイベントハンドラを使用して、ユーザが 3 のマスをクリックすると、その動きがサーバに送られるようにします:

liveSend({square: 3});

2. については、テンプレートに次のようなコードを入れて、ページが読み込まれたときに、サーバーに空のメッセージを送るのが良いでしょう:

document.addEventListener("DOMContentLoaded", (event) => {
    liveSend({});
});

サーバーはこの2つの状況を if 文で処理します:

def live_method(player, data):
    group = player.group

    if 'square' in data:
        # SITUATION 1
        square = data['square']

        # save_move should save the move into a group field.
        # for example, if player 1 modifies square 3,
        # that changes group.board from 'X O XX  O' to 'X OOXX  O'
        save_move(group, square, player.id_in_group)
        # so that we can highlight the square (and maybe say who made the move)
        news = {'square': square, 'id_in_group': player.id_in_group}
    else:
        # SITUATION 2
        news = {}
    # get_state should contain the current state of the game, for example:
    # {'board': 'X O XX  O', 'whose_turn': 2}
    payload = get_state(group)
    # .update just combines 2 dicts
    payload.update(news)
    return {0: payload}

2. (プレイヤーがページを読み込む状況) では、クライアントは次のようなメッセージを受け取ります:

{'board': 'X OOXX  O', 'whose_turn': 2}

1. では、プレイヤーは先ほどの動きに関する更新情報と、現在の状態を取得します。

{'board': 'X OOXX  O', 'whose_turn': 2, 'square': square, 'id_in_group': player.id_in_group}

JavaScript のコードは、誰の一手なのかを把握する必要はなく、サーバーから受け取った情報を信頼するだけでいいのです。メッセージを受け取るたびに盤面を描き直すこともできます。

あなたのコードは、ユーザーの入力を検証する必要もあります。例えば、実際にはプレイヤー2の番なのにプレイヤー1が動こうとした場合、それをブロックする必要があります。前のセクションで述べた理由から、 JavaScript のコードではなく、 live_method で実装するのが良いでしょう。

要約

これまで説明してきたように、 live_method の典型的なパターンは次のようなものです:

if the user made an action:
    state = (get the current state of the game)
    if (action is illegal/invalid):
        return
    update the models based on the move.
    news = (produce the feedback to send back to the user, or onward to other users)
else:
    news = (nothing)
state = (get the current state of the game)
payload = (state combined with news)
return payload

ゲームの状態を2回取得していることに注意してください。モデルを更新するとゲームの状態が変化するため、再度ゲームの状態を取得しています。

トラブルシューティング

ページの読み込みが終わる前に liveSend を呼び出すと、 liveSend is not defined などのエラーが発生します。そのため、 DOMContentLoaded (または jQuery の document.ready など) を待つようにします:

window.addEventListener('DOMContentLoaded', (event) => {
    // your code goes here...
});

ユーザーが 「次へ」 ボタンをクリックしたときには、 liveSend を実行しないでください。ページを離れると、 liveSend が中断される可能性があります。代わりに、ユーザーに liveSend を実行する通常のボタンをクリックしてもらい、liveRecvdocument.getElementById("form").submit(); を実行するようにします。