Páginas en vivo
Las páginas en vivo se comunican con el servidor de manera continua y se actualizan en tiempo real, lo que permite juegos de tiempo continuo. Las páginas en vivo son ideales para juegos con mucha interacción entre los usuarios.
Hay un montón de ejemplos aquí.
Envío de datos al servidor
En el código JavaScript de tu plantilla, llama a la función liveSend() siempre que quieras enviar datos al servidor. Por ejemplo, para presentar una oferta de 99 en nombre del usuario, llama:
liveSend(99);
Define una función que recibirá este mensaje. Su argumento es cualquier dato que se haya enviado.
class MyPage(Page):
@staticmethod
def live_method(player, data):
print('received a bid from', player.id_in_group, ':', data)
If you are using oTree Studio, you must define a player function whose name
starts with live_.
Nota
If you want to use live methods on a WaitPage, you must install oTree 6.0
Enviando datos a la página
Para enviar datos de vuelta, devuelve un diccionario cuyos claves son los IDs de los jugadores que recibirán un mensaje. Por ejemplo, aquí hay un método que simplemente envía «thanks» a quien envíe un mensaje.
def live_method(player, data):
return {player.id_in_group: 'thanks'}
Para enviar a múltiples jugadores, utiliza su id_in_group. Por ejemplo, este método reenvía todos los mensajes a los jugadores 2 y 3:
def live_method(player, data):
return {2: data, 3: data}
Para transmitirlo a todo el grupo, utiliza 0 (caso especial ya que no es un id_in_group real).
def live_method(player, data):
return {0: data}
En tu JavaScript, define una función liveRecv. Esta se llamará automáticamente cada vez que se reciba un mensaje del servidor.
function liveRecv(data) {
console.log('received a message!', data);
// your code goes here
}
Ejemplo: subasta
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>
(Nota, en JavaScript data.id_in_group == data['id_in_group'].)
Datos
Los datos que envías y recibes pueden ser de cualquier tipo de dato (siempre que sean serializables en JSON). Por ejemplo, estos son todos válidos:
liveSend(99);
liveSend('hello world');
liveSend([4, 5, 6]);
liveSend({'type': 'bid', 'value': 10.5});
El tipo de dato más versátil es un dict, ya que te permite incluir múltiples piezas de metadatos, en particular qué tipo de mensaje es:
liveSend({'type': 'offer', 'value': 99.9, 'to': 3})
liveSend({'type': 'response', 'accepted': true, 'to': 3})
Luego puedes usar declaraciones if para procesar diferentes tipos de mensajes:
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
...
Historia
Por defecto, los participantes no verán los mensajes que se enviaron antes de que llegaran a la página. (Y los datos no se volverán a enviar si actualizan la página.) Si deseas guardar el historial, debes almacenarlo en la base de datos. Cuando un jugador carga la página, tu JavaScript puede llamar algo como liveSend({}), y puedes configurar tu método live_method para recuperar el historial del juego de la base de datos.
ExtraModel
Las páginas en vivo a menudo se utilizan junto con un ExtraModel, que permite almacenar cada mensaje o acción individual en la base de datos.
Manteniendo a los usuarios en la página
Supongamos que necesitas enviar 10 mensajes antes de que los usuarios puedan proceder a la siguiente página.
Primero, debes omitir el {{ next_button }}. (O usa JS para ocultarlo hasta que la tarea esté completa.)
Cuando la tarea esté completa, envías un mensaje:
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}
Entonces, en la plantilla, envía automáticamente la página a través de 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..
}
Por cierto, utilizando una técnica similar, podrías implementar una página de espera personalizada, por ejemplo, una que te permita continuar después de un cierto tiempo de espera, incluso si no todos los jugadores han llegado.
Asynchronous live methods (AI, web APIs, etc)
Nota
To use this, you must install oTree 6.0
If you want to call external APIs like OpenAI/ChatGPT during experiments, you should use an asynchronous live method as described below. (If you use a regular live method together with web requests, it can cause the server to freeze if multiple participants are trying to load pages at the same time.)
Define your live_method as async and use yield instead of return.
This means you must use async/await throughout the function.
# IMPORTANT: make sure whatever API you are using has an async version,
# and use that.
# If they don't, consider making raw requests with httpx.
OPENAI_CLIENT = AsyncOpenAI(api_key=OPENAI_KEY)
class MyPage(Page):
@staticmethod
async def live_method(player: Player, data):
completion = await OPENAI_CLIENT.chat.completions.create(
model="chatgpt-4o-latest",
messages=[{"role": "user", "content": data}],
stream=False,
)
yield {player.id_in_group: completion.choices[0].message.content}
You can also stream content, rather than waiting for the full reply
(useful for chat interfaces etc).
Use the API provider’s streaming option and multiple yield statements.
class MyPageWithStreaming(Page):
@staticmethod
async def live_method(player: Player, data):
completion = await OPENAI_CLIENT.chat.completions.create(
model="chatgpt-4o-latest",
messages=[{"role": "user", "content": data}],
stream=True,
)
async for chunk in completion:
content = chunk.choices[0].delta.content
yield {player.id_in_group: content}
Async live method is safe to use if you are only modifying the current player, but you can get irregular behavior if multiple players are modifying the same object (e.g. the group). That’s because this function executes in parallel, meaning there is a risk of race conditions.
Consejos generales sobre páginas en vivo
Aquí hay algunos consejos generales (no aplican a todas las situaciones). Recomendamos implementar la mayor parte de tu lógica en Python, y solo usar JavaScript para actualizar el HTML de la página, porque:
El lenguaje JavaScript puede ser bastante complicado de usar correctamente.
Tu código de Python se ejecuta en el servidor, que es centralizado y confiable. JavaScript se ejecuta en los clientes, que pueden desincronizarse entre sí, y los datos pueden perderse cuando se cierra o recarga la página.
Debido a que el código de Python se ejecuta en el servidor, es más seguro y no puede ser visto ni modificado por los participantes.
Ejemplo: tres en raya
Supongamos que estás implementando un juego de tres en raya. Hay 2 tipos de mensajes que tu live_method puede recibir:
Un usuario marca un cuadrado, por lo que necesitas notificar al otro jugador.
Un usuario carga (o recarga) la página, por lo que necesitas enviarle el diseño actual del tablero.
Para la situación 1, deberías usar un controlador de eventos de JavaScript como onclick, por ejemplo, para que cuando el usuario haga clic en el cuadrado 3, ese movimiento se envíe al servidor:
liveSend({square: 3});
Para la situación 2, es bueno poner un código como este en tu plantilla, que envía un mensaje vacío al servidor cuando se carga la página:
document.addEventListener("DOMContentLoaded", (event) => {
liveSend({});
});
El servidor maneja estas 2 situaciones con una declaración «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}
En la situación 2 (el jugador carga la página), el cliente recibe un mensaje como:
{'board': 'X OOXX O', 'whose_turn': 2}
En la situación 1, el jugador recibe la actualización sobre el movimiento que se acaba de realizar, Y el estado actual.
{'board': 'X OOXX O', 'whose_turn': 2, 'square': square, 'id_in_group': player.id_in_group}
El código de JavaScript puede ser «tonto». No necesita hacer un seguimiento de de quién es el turno; simplemente confía en la información que recibe del servidor. Incluso puede redibujar el tablero cada vez que recibe un mensaje.
Tu código también necesitará validar la entrada del usuario. Por ejemplo, si el jugador 1 intenta moverse cuando en realidad es el turno del jugador 2, necesitas bloquear eso. Por las razones mencionadas en la sección anterior, es mejor hacerlo en tu live_method que en el código de JavaScript.
Resumen
Como se ilustra arriba, el patrón típico para un live_method es así:
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
Tenga en cuenta que obtenemos el estado del juego dos veces. Eso es porque el estado cambia cuando actualizamos nuestros modelos, por lo que necesitamos refrescarlo.
Solución de problemas
Si llamas a liveSend antes de que la página haya terminado de cargarse, obtendrás un error como liveSend is not defined. Así que, espera a DOMContentLoaded (o jQuery document.ready, etc.):
window.addEventListener('DOMContentLoaded', (event) => {
// your code goes here...
});
No dispares liveSend cuando el usuario haga clic en el botón «siguiente», ya que abandonar la página podría interrumpir el liveSend. En su lugar, haz que el usuario haga clic en un botón normal que dispare un liveSend, y luego ejecuta document.getElementById("form").submit(); en tu liveRecv.