Stager¶
The Stager manages participants' progression through a sequence of scenes. It tracks where each participant is in the experiment, handles scene transitions, and ensures each participant experiences scenes in the correct order.
What is a Stager?¶
A Stager is a container for scenes that:
- Defines the sequence of scenes in your experiment
- Manages per-participant state (which scene they're on)
- Handles scene activation and deactivation
- Coordinates data collection across scenes
One Stager instance per participant:
When a participant joins, the server creates a Stager instance for them. Each participant's Stager is independent, allowing multiple participants to be at different stages of the experiment simultaneously.
Creating a Stager¶
Basic Usage¶
from mug.scenes import stager, static_scene, gym_scene
# Define your scenes
start_scene = static_scene.StartScene().display(...)
game_scene = gym_scene.GymScene().gameplay(...)
end_scene = static_scene.EndScene().display(...)
# Create the stager
experiment_stager = stager.Stager(
scenes=[start_scene, game_scene, end_scene]
)
Required structure:
- First scene must be a
StartScene - Last scene must be an
EndScene - Any number of scenes can be between them
With Scene Wrappers¶
Scenes can be wrapped with RandomizeOrder or RepeatScene to control ordering and repetition:
from mug.scenes import scene
experiment_stager = stager.Stager(
scenes=[
start_scene,
scene.RandomizeOrder([game_a, game_b], keep_n=1),
survey_scene,
end_scene,
],
)
How Staging Works¶
Scene Progression¶
Participants move through scenes in order:
- StartScene: Participant sees welcome/instructions, clicks "Continue"
- Next scene activates: Could be GymScene, StaticScene, etc.
- Scene completes: GymScene ends after episodes, StaticScene on "Continue"
- Process repeats: Until EndScene is reached
- EndScene: Experiment ends, participant sees thank you/redirect
Automatic Progression:
- StartScene/StaticScene: Clicking "Continue" advances to next scene
- GymScene: Automatically advances after all episodes complete
- EndScene: No progression (experiment ends)
Per-Participant State¶
Each participant's Stager tracks:
stager_instance = {
"current_scene_index": 2, # Which scene they're on
"subject_id": "participant_123", # Their unique ID
"scenes": [scene1, scene2, scene3], # Scene sequence
"metadata": { # Custom data
"condition": "A",
"start_time": "2024-01-01T10:00:00",
}
}
This state is maintained throughout the experiment, surviving scene transitions.
Multi-Participant Management¶
Multiple participants can be in different scenes simultaneously:
Participant 1: Welcome Scene (scene 0)
Participant 2: Game Scene (scene 1)
Participant 3: Survey Scene (scene 2)
Participant 4: Game Scene (scene 1)
Participant 5: Thank You Scene (scene 3)
Each has their own independent Stager instance managing their progress.
Stager Lifecycle¶
For each participant:
- Connection: Participant connects to server
- Stager Creation: New Stager instance created with scene sequence
- Scene 0 Activation: First scene (StartScene) becomes active
-
Progression Loop:
-
Participant completes current scene
- Stager deactivates current scene
- Stager activates next scene
-
Repeat until EndScene
-
Completion: Participant finishes EndScene, connection closes
- Cleanup: Stager instance and associated resources released
Scene Activation/Deactivation¶
The Stager handles scene lifecycle:
Activation:
def activate_scene(self, scene_index):
# Deactivate current scene (if any)
if self.current_scene:
self.current_scene.deactivate()
# Activate new scene
self.current_scene = self.scenes[scene_index]
self.current_scene.activate(socketio=self.socketio, room=self.room)
self.current_scene_index = scene_index
Deactivation:
def deactivate_current_scene(self):
if self.current_scene:
self.current_scene.deactivate()
# Cleanup: save data, release resources, etc.
This ensures proper initialization and cleanup for each scene.
Advanced Usage¶
Custom Stager Subclass¶
Extend the Stager for custom behavior:
from mug.scenes.stager import Stager
class ConditionalStager(Stager):
def get_next_scene_index(self):
"""Override to implement conditional branching"""
current = self.current_scene_index
# Example: Skip scene 2 if condition met
if current == 1 and self.check_condition():
return 3 # Skip to scene 3
else:
return current + 1 # Normal progression
def check_condition(self):
# Custom logic
return self.metadata.get("skip_tutorial", False)
Scene Branching¶
Implement conditional scene sequences:
class BranchingStager(Stager):
def __init__(self, scenes, condition_fn):
super().__init__(scenes)
self.condition_fn = condition_fn
def get_next_scene_index(self):
current = self.current_scene_index
# Branch based on performance
if current == 1: # After practice game
score = self.get_participant_score()
if self.condition_fn(score):
return 2 # Go to hard version
else:
return 3 # Go to easy version
return current + 1
Usage:
def high_performer(score):
return score > 50
branching_stager = BranchingStager(
scenes=[start, practice, hard_game, easy_game, survey, end],
condition_fn=high_performer
)
Metadata Tracking¶
Add custom metadata to track throughout the experiment:
stager_instance = stager.Stager(scenes=[...])
# Add metadata programmatically
stager_instance.metadata["condition"] = random.choice(["A", "B"])
stager_instance.metadata["start_time"] = datetime.now().isoformat()
# Access in scene callbacks
def on_game_complete(game, stager_instance):
condition = stager_instance.metadata["condition"]
# Log or adjust based on condition
Stager and GameManager¶
For GymScenes, the Stager interacts with the GameManager:
Stager (per participant)
├── Activates GymScene
│ └── GymScene creates/joins Game via GameManager
│ ├── GameManager assigns to Game
│ ├── Game runs environment
│ └── Game collects data
│
└── Waits for Game completion
└── Deactivates GymScene
└── Advances to next scene
The Stager delegates game mechanics to the GameManager but maintains overall experiment flow.
Data Organization¶
The Stager doesn't directly handle data collection, but it organizes where data is saved:
data/
├── {scene_0_id}/ # StartScene data
│ └── {subject_id}_metadata.json
├── {scene_1_id}/ # First GymScene
│ ├── {subject_id}.csv
│ └── {subject_id}_metadata.json
├── {scene_2_id}/ # StaticScene (survey)
│ └── {subject_id}.csv
└── {scene_3_id}/ # EndScene
└── {subject_id}_metadata.json
Each scene's ID determines its data directory.
Common Patterns¶
Simple Linear Experiment¶
Practice + Main Game¶
stager = stager.Stager(scenes=[
start_scene,
tutorial_scene,
practice_game_scene, # Low stakes
instructions_scene,
main_game_scene, # Real data collection
survey_scene,
end_scene,
])
Multiple Conditions¶
Use RandomizeOrder with keep_n=1 to randomly assign each participant to one condition. The wrapper selects one scene at random when building the sequence for each participant:
from mug.scenes import scene
stager = stager.Stager(scenes=[
start_scene,
scene.RandomizeOrder(
[control_game_scene, treatment_game_scene],
keep_n=1, # Each participant sees exactly one
),
survey_scene,
end_scene,
])
Repeated Measures¶
Use RandomizeOrder to counterbalance the order of conditions across participants:
from mug.scenes import scene
# Each participant plays all versions in a random order
stager = stager.Stager(scenes=[
start_scene,
scene.RandomizeOrder([
game_version_a,
game_version_b,
game_version_c,
]),
survey_scene,
end_scene,
])
Between-Subjects Design¶
Use RandomizeOrder with keep_n=1 to implement between-subjects designs. Each participant is randomly assigned to one condition:
from mug.scenes import scene
# Each participant sees exactly one game version
stager = stager.Stager(scenes=[
start,
scene.RandomizeOrder(
[game_a, game_b, game_c],
keep_n=1,
),
survey,
end,
])
Debugging and Testing¶
Test Scene Progression¶
Run through your experiment to verify scenes flow correctly:
# Start server
python my_experiment.py
# Open browser, complete each scene
# Check logs for:
# - Scene activation messages
# - Data saving confirmations
# - Any errors during transitions
Check Stager State¶
Add logging to see what's happening:
class DebugStager(Stager):
def activate_scene(self, scene_index):
print(f"Activating scene {scene_index}: {self.scenes[scene_index].scene_id}")
super().activate_scene(scene_index)
def deactivate_current_scene(self):
print(f"Deactivating scene {self.current_scene_index}")
super().deactivate_current_scene()
Best Practices¶
- Use descriptive scene IDs: Makes data organization clearer
- Test the full flow: Complete the entire experiment yourself
- Handle disconnections: Consider what happens if a participant refreshes
- Log state transitions: Useful for debugging progression issues
- Validate scene order: Ensure StartScene is first, EndScene is last
- Keep metadata light: Don't store large objects in stager metadata
Common Issues¶
Scene not advancing
- Check that GymScene has correct
num_episodesset - Verify "Continue" button is enabled in StaticScenes
- Look for JavaScript errors in browser console
Data not saving
- Confirm
scene_idis set for each scene - Check file permissions in data directory
- Verify
should_export_metadata=Trueif expecting metadata files
Participants see wrong scene
- Check scene order in Stager initialization
- Verify no custom
get_next_scene_index()logic causing issues - Look for race conditions in custom Stager subclass
Multiple participants interfering
- Each participant should have their own Stager instance (handled automatically)
- Check that you're not sharing game state across participants
- Verify thread-safety in custom callbacks