Skip to content

Synchronizing progress

In multiplayer experiments, you often need to wait for all group members to reach a certain point before continuing. The SynchronizingWait page type handles this automatically and provides a callback to process all players' data together.

Basic synchronization

Create a wait page that synchronizes players by subclassing SynchronizingWait:

class WaitForAll(SynchronizingWait):
    pass

When a participant reaches this page, they wait until all other group members arrive. Once everyone is present, they all advance together.

page_order = [
    GroupPlease,      # Form groups first
    Decision,         # Each player makes a decision
    WaitForAll,       # Wait for all decisions
    Results,          # Show results
]

Processing data with all_here

The all_here callback runs exactly once when all group members have arrived. This is where you calculate outcomes based on everyone's choices:

class Sync(SynchronizingWait):
    @classmethod
    def all_here(page, group):
        total = sum(p.contribution for p in players(group))

        for player in players(group):
            player.payoff = ENDOWMENT - player.contribution + MPCR * total

The callback receives the group object and has access to all players' data.

See the public_goods_game example · beauty_contest example · minimum_effort_game example

Calculating payoffs

A common pattern is to calculate payoffs for all players in all_here. Here's a complete example using the prisoner's dilemma payoff matrix:

def set_payoff(player):
    other = other_in_group(player)

    match player.cooperate, other.cooperate:
        case True, True:
            player.payoff = 10
        case True, False:
            player.payoff = 0
        case False, True:
            player.payoff = 15
        case False, False:
            player.payoff = 3


class Sync(SynchronizingWait):
    @classmethod
    def all_here(page, group):
        for player in players(group):
            set_payoff(player)

See the prisoners_dilemma example

Using apply for bulk operations

You can also use apply() to run a function on all players:

class Sync(SynchronizingWait):
    @classmethod
    def set_payoff(page, player):
        other = other_in_group(player)

        if player.claim + other.claim <= 100:
            player.payoff = player.claim

    @classmethod
    def all_here(page, group):
        players(group).apply(page.set_payoff)

See the focal_point example

Finding specific players

Use find_one() to locate players with specific attributes:

class Sync(SynchronizingWait):
    @classmethod
    def all_here(page, group):
        dictator = players(group).find_one(dictator=True)
        recipient = players(group).find_one(dictator=False)

        dictator.payoff = cu(10) - dictator.give
        recipient.payoff = dictator.give

See the dictator_game example · trust_game example · ultimatum_game example

Session-level synchronization

By default, SynchronizingWait synchronizes within the group. To synchronize across the entire session instead:

class WaitForSession(SynchronizingWait):
    synchronize = "session"

    @classmethod
    def all_here(page, session):
        # Runs when ALL participants in the session have arrived
        session.total_participants = len(players(session))

Note that when synchronize = "session", the callback receives the session object instead of group.

Custom synchronization with wait_for

Override the wait_for method for custom synchronization logic:

class CustomWait(SynchronizingWait):
    @classmethod
    def wait_for(page, player):
        # Return list of PlayerIdentifiers to wait for
        # Default implementation returns players(player.group)
        return players(player.group)

Synchronization in repeated games

When using Rounds, each round typically needs its own synchronization point:

page_order = [
    GroupPlease,
    Rounds(
        Decision,
        Sync,           # Sync after each round
        RoundResults,
        n=3,
    ),
    FinalResults,
]

The all_here callback runs once per round, allowing you to calculate round-specific outcomes:

class Sync(SynchronizingWait):
    @classmethod
    def all_here(page, group):
        for player in players(group):
            set_payoff(player)  # Payoff for this round

In templates, you can access the current round number:

<h2>Round {{ player.round }} results</h2>

See the prisoners_dilemma_repeated example

Accessing historical data in repeated games

Use player.within() to access data from specific rounds:

def digest(session):
    for gname in session.groups:
        with session.group(gname) as group:
            player1 = players(group).find_one(member_id=0)

            for round in range(1, player1.round + 1):
                # Get this player's choice in a specific round
                choice = player1.within(round=round).get("cooperate")

See the prisoners_dilemma_repeated example

Complete example: public goods game

Here's a complete example showing synchronization with a three-player group:

ENDOWMENT = cu("10")
MPCR = cu("0.5")  # Marginal Per Capita Return


class GroupPlease(GroupCreatingWait):
    group_size = 3


class Contribute(Page):
    fields = dict(
        contribution=DecimalField(
            label="How much do you contribute to the group account?",
            min=0,
            max=ENDOWMENT,
        ),
    )

    @classmethod
    def context(page, player):
        group_size = GroupPlease.group_size
        multiplier = MPCR * group_size

        return dict(
            endowment=ENDOWMENT,
            group_size=group_size,
            multiplier=multiplier,
        )


class Sync(SynchronizingWait):
    @classmethod
    def all_here(page, group):
        total = sum(p.contribution for p in players(group))

        for player in players(group):
            player.payoff = ENDOWMENT - player.contribution + MPCR * total


class Results(Page):
    @classmethod
    def context(page, player):
        total = sum(p.contribution for p in players(player.group))
        return dict(total=total)


page_order = [
    GroupPlease,
    Contribute,
    Sync,
    Results,
]

See the full public_goods_game example

Summary

Feature Purpose
SynchronizingWait Wait page that syncs group members
all_here(page, group) Callback when all members arrive
synchronize = "group" Sync within group (default)
synchronize = "session" Sync entire session
wait_for(page, player) Custom sync logic
players(group).find_one() Find player by attribute
players(group).apply() Run function on all players
player.within(round=n) Access data from specific round