ライブページ¶
ライブページは、サーバーと継続的に通信し、リアルタイムに更新されるため、連続した時間のゲームが可能です。ライブページは、ユーザー同士のやりとりが多いゲームに向いています。
こちら で多くの例を確認できます。
サーバーへのデータ送信¶
テンプレートの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_
で始まるプレイヤー関数を定義する必要があります。(なお、 WaitPage
の live_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. については、 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
を実行する通常のボタンをクリックしてもらい、liveRecv
で document.getElementById("form").submit();
を実行するようにします。