实时页面

Live pages communicate with the server continuously and update in real time, enabling continuous time games. Live pages are a great fit for games with lots of back-and-forth interaction between users, and for single-player games with quick iteration.

向服务器发送数据

在你模板的JavaScript代码中,调用 liveSend() 函数来向服务器发送数据。例如,代表用户提交一个99的点数:

liveSend(99);

Player 中,定义一个可以接收此消息的方法。它的参数就是所发送的数据。

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

(注意,在 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, data):
    group = player.group
    my_id = player.id_in_group
    if bid > group.highest_bid:
        group.highest_bid = data
        group.highest_bidder = my_id
        response = dict(id_in_group=my_id, bid=data)
        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({'type': 'connect'}) 的东西,并且你可以配置你自己的实时方法来从数据库中提取游戏的历史。

让用户停留在页面上

假定玩家在进行下一个页面之前你需要先发送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..
}

出于安全考虑,你应当使用 error_message:

class MyPage(Page):
    def live_method(player, data):
        ...

    @staticmethod
    def error_message(player, values):
        group = player.group
        if not group.game_finished:
            return 'you need to stay until 10 messages are sent'

顺带一提,使用相同的技巧,你可以实现一个自定义的等待页面,例如在某个时间之后才会进行下一页面,即使所有玩家均已到达的等待页面。

表单验证

注解

如果你的表单中有多个字段,那么使用常规页面的 form_modelform_fields 验证比较简单。因为你可以享受 {% formfields %}error_message 等带来的便利。

假定你的实时页面要求玩家提交出价,并且最高出价为99。在非实时页面中你可能会使用 简单的表单验证 来检查这一点。但在实时页面中,你必须在 live_method 中确认这一点:

def live_method(player, bid):
    if bid > 99:
        # just an example.
        # it's up to you to handle this message in your JavaScript code.
        response = dict(type='error', message='Bid is too high')
        return {player.id_in_group: response}
    ...

除此之外,你可以给 <input> 元素添加属性如 max="99".(但请注意HTML代码不是安全的并且可被了解技术的参与者修改。)如果你这样做了,你应当还要添加 form="liveform".这会将 <input> 表单排除在页面的主表单之外,使得用户点击 {% next_button %} 时,此验证不会被触发。

所以代码看起来如下:

<input id="whatever" type="number" max="99" required form="liveform">

为了在用户提交报价时触发验证,使用下面的代码(例如在你的 onclick 事件处理器中):

let liveform = document.getElementById('liveform');
let isValid = liveform.reportValidity();

reportValidity() 是一个内置的JavaScript函数用来展示用户在表单中的错误。它还会返回一个布尔值表明表单是合法的。你可以使用此方法跳过 liveSend.

General advice about live pages

Here is some general advice (does not apply to all situations). We recommend implementing most of your logic in Python, and just using JavaScript to update the page’s HTML, because:

  • The JavaScript language can be quite tricky to use properly
  • Your Python code runs on the server, which is centralized and reliable. JavaScript runs on the clients, which can get out of sync with each other, and data can get lost when the page is closed or reloaded.
  • Because Python code runs on the server, it is more secure and cannot be viewed or modified by participants.

Example: tic-tac-toe

Let’s say you are implementing a game of tic-tac-toe. There are 2 types of messages your live_method can receive:

  1. A user marks a square, so you need to notify the other player
  2. A user loads (or reloads) the page, so you need to send them the current board layout.

For situation 1, you should use a JavaScript event handler like onclick, e.g. so when the user clicks on square 3, that move gets sent to the server:

liveSend({square: 3});

For situation 2, it’s good to put some code like this in your template, which sends an empty message to the server when the page loads:

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

The server handles these 2 situations with an “if” statement:

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)
        feedback = {'square': square, 'id_in_group': player.id_in_group}
    else:
        # SITUATION 2
        feedback = {}
    # 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(feedback)
    return {0: payload}

In situation 2 (the player loads the page), the client gets a message like:

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

In situation 1, the player gets the update about the move that was just made, AND the current state.

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

The JavaScript code can be “dumb”. It doesn’t need to keep track of whose move it is; it just trusts the info it receives from the server. It can even redraw the board each time it receives a message.

Your code will also need to validate user input. For example, if player 1 tries to move when it is actually player 2’s turn, you need to block that. For reasons listed in the previous section, it’s better to do this in your live_method than in JavaScript code.

Summary

As illustrated above, the typical pattern for a live_method is like this:

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.
    feedback = (produce the feedback to send back to the user, or onward to other users)
else:
    feedback = (nothing)
state = (get the current state of the game)
payload = (state combined with feedback)
return payload

Note that we get the game’s state twice. That’s because the state changes when we update our models, so we need to refresh it.

故障排除

如果你在页面载入完成之前调用了 liveSend ,你会得到一个错误如 liveSend is not defined.所以先等待 DOMContentLoaded (或jQuery document.ready 等等):

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

Don’t trigger liveSend when the user clicks the “next” button, since leaving the page might interrupt the liveSend. Instead, have the user click a regular button that triggers a liveSend, and then doing document.getElementById("form").submit(); in your liveRecv.