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