实时页面

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.

There are a bunch of examples here.

向服务器发送数据

在你模板的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
        ...

历史

By default, participants will not see messages that were sent before they arrived at the page. (And data will not be re-sent if they refresh the page.) If you want to save history, you should store it in the database. When a player loads the page, your JavaScript can call something like liveSend({}), and you can configure your live_method to retrieve the history of the game from the database.

ExtraModel

Live pages are often used together with an ExtraModel, which allows you to store each individual message or action in the database.

让用户停留在页面上

假定玩家在进行下一个页面之前你需要先发送10条消息。

First, you should omit the {{ next_button }}. (Or use JS to hide it until the task is complete.)

当信息发送完成后,发送一条消息:

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..
}

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

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)
        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}

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.
    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

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.