diff --git a/TODO.md b/TODO.md
index cf1f774..c04750c 100644
--- a/TODO.md
+++ b/TODO.md
@@ -6,7 +6,7 @@
- [x] **Cancel Revise**. In character creation, if you click "Revise" and then don't want to make any changes after all, there should be a cancel button that throws away any edits you've made since you clicked revise.
-- [ ] **Teaching mode**. The creator/admin should have an option to seed themselves as the player who is forced to be the Deep for the first scene
+- [x] **Teaching mode**. The creator/admin should have an option to seed themselves as the player who is forced to be the Deep for the first scene
- [ ] Visual feedback confirming hand refresh in Deep Upkeep
diff --git a/frontend/src/assets/css/lobby.css b/frontend/src/assets/css/lobby.css
index 8ec4a30..86dc8f2 100644
--- a/frontend/src/assets/css/lobby.css
+++ b/frontend/src/assets/css/lobby.css
@@ -13,6 +13,22 @@
margin-bottom: 2rem;
}
+.teaching-box {
+ text-align: left;
+ margin-bottom: 2rem;
+ padding: 1rem 1.25rem;
+}
+
+.teaching-box .checkbox-label.disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
+.teaching-box .info-text {
+ margin: 0.5rem 0 0;
+ font-size: 0.85rem;
+}
+
.link-item {
margin-bottom: 1.5rem;
}
diff --git a/frontend/src/components/LobbyPhase.svelte b/frontend/src/components/LobbyPhase.svelte
index 2e11ca0..b5daaf7 100644
--- a/frontend/src/components/LobbyPhase.svelte
+++ b/frontend/src/components/LobbyPhase.svelte
@@ -24,6 +24,19 @@
starting = false;
}
}
+
+ $: teaching = state.game.teaching_deep_player_id === state.player.id;
+ $: teachingTakenByOther = state.game.teaching_deep_player_id && !teaching;
+
+ async function toggleTeaching() {
+ try {
+ await apiRequest(`/game/${state.game.id}/player/${state.player.id}/teaching-mode`, 'POST', {
+ enabled: !teaching
+ });
+ } catch (err) {
+ console.error("Failed to toggle teaching mode:", err);
+ }
+ }
@@ -72,6 +85,22 @@
{/if}
+ {#if state.player.is_admin}
+
+
+
+ {#if teachingTakenByOther}
+ Another admin has already volunteered to teach as the Deep.
+ {:else}
+ Seeds you as the Rank-3 player who must play the Deep in scene 1, so you can run the table for new crews. Otherwise it's assigned at random.
+ {/if}
+
+
+ {/if}
+
{#if state.player.is_admin}
{#if state.players.length >= 3 || state.game.dev_mode}
diff --git a/src/pirats/crud_character.py b/src/pirats/crud_character.py
index 8a5ca96..d455ce4 100644
--- a/src/pirats/crud_character.py
+++ b/src/pirats/crud_character.py
@@ -435,6 +435,14 @@ def maybe_finish_assign_techniques(db: Session, game: Game):
# 1. Randomly assign starting ranks
random.shuffle(players_list)
+ # Teaching mode: an admin opted to be the forced Rank-3 Deep for the first
+ # scene, so seed them at the front instead of leaving it to chance.
+ if game.teaching_deep_player_id:
+ teacher = next((p for p in players_list if p.id == game.teaching_deep_player_id), None)
+ if teacher:
+ players_list.remove(teacher)
+ players_list.insert(0, teacher)
+
# "Randomly choose one Player to start the game at Rank 3. This Player must play the Deep during the first scene."
# "Randomly choose one Player to start the game at Rank 1. This Player must play their Pi-Rat during the first scene."
# "All remaining Players start at Rank 2. These Players may choose to play either during the first scene."
diff --git a/src/pirats/migrations/versions/19ee9ae5ca0e_add_teaching_deep_player_id_to_game.py b/src/pirats/migrations/versions/19ee9ae5ca0e_add_teaching_deep_player_id_to_game.py
new file mode 100644
index 0000000..437c8b5
--- /dev/null
+++ b/src/pirats/migrations/versions/19ee9ae5ca0e_add_teaching_deep_player_id_to_game.py
@@ -0,0 +1,32 @@
+"""add teaching_deep_player_id to game
+
+Revision ID: 19ee9ae5ca0e
+Revises: e6294f6f8a81
+Create Date: 2026-06-14 09:11:17.810765
+
+"""
+from alembic import op
+import sqlalchemy as sa
+import sqlmodel
+
+
+revision = '19ee9ae5ca0e'
+down_revision = 'e6294f6f8a81'
+branch_labels = None
+depends_on = None
+
+
+def upgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ with op.batch_alter_table('game', schema=None) as batch_op:
+ batch_op.add_column(sa.Column('teaching_deep_player_id', sqlmodel.sql.sqltypes.AutoString(), nullable=True))
+
+ # ### end Alembic commands ###
+
+
+def downgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ with op.batch_alter_table('game', schema=None) as batch_op:
+ batch_op.drop_column('teaching_deep_player_id')
+
+ # ### end Alembic commands ###
diff --git a/src/pirats/models.py b/src/pirats/models.py
index a53dc6d..71a05fb 100644
--- a/src/pirats/models.py
+++ b/src/pirats/models.py
@@ -18,6 +18,7 @@ class Game(SQLModel, table=True):
completed_crew_2: bool = Field(default=False) # Choose a Captain
completed_crew_3: bool = Field(default=False) # Commit Piracy
captain_player_id: Optional[str] = Field(default=None) # Persistent captaincy; set once the crew controls a ship
+ teaching_deep_player_id: Optional[str] = Field(default=None) # Teaching mode: an admin who seeds themselves as the forced Rank-3 Deep for the first scene (set in the lobby, honored when starting ranks are rolled)
players: List["Player"] = Relationship(back_populates="game", cascade_delete=True)
obstacles: List["Obstacle"] = Relationship(back_populates="game", cascade_delete=True)
diff --git a/src/pirats/routes_admin.py b/src/pirats/routes_admin.py
index 656bd07..680ff81 100644
--- a/src/pirats/routes_admin.py
+++ b/src/pirats/routes_admin.py
@@ -33,6 +33,36 @@ def set_dev_mode(
crud.add_game_event(db, game.id, f"🛠Dev Mode turned {'ON' if enabled else 'OFF'} by {player.name}.", kind="info")
return {"status": "ok", "dev_mode": game.dev_mode}
+# Teaching mode: an admin volunteers to be the forced Rank-3 Deep for scene 1.
+# Only meaningful before starting ranks are rolled (i.e. before scene setup).
+_PRE_RANK_PHASES = {"lobby", "character_creation", "swap_techniques", "assign_techniques"}
+
+@router.post("/game/{game_id}/player/{player_id}/teaching-mode")
+def set_teaching_mode(
+ game_id: str,
+ player_id: str,
+ enabled: bool = Form(...),
+ db: Session = Depends(get_session)
+):
+ game, player = _get_admin(db, game_id, player_id)
+ if game.phase not in _PRE_RANK_PHASES:
+ raise HTTPException(status_code=400, detail="Starting ranks have already been rolled.")
+ if enabled:
+ game.teaching_deep_player_id = player.id
+ elif game.teaching_deep_player_id == player.id:
+ game.teaching_deep_player_id = None
+ else:
+ # Disabling someone else's teaching mode shouldn't silently clear it.
+ return {"status": "ok", "teaching_deep_player_id": game.teaching_deep_player_id}
+ db.add(game)
+ db.commit()
+ crud.add_game_event(
+ db, game.id,
+ f"📚 {player.name} {'will teach as the Deep' if enabled else 'cancelled teaching mode'} for the first scene.",
+ kind="info",
+ )
+ return {"status": "ok", "teaching_deep_player_id": game.teaching_deep_player_id}
+
# Dev Mode shortcut: auto-complete character creation for everyone and move on
@router.post("/game/{game_id}/player/{player_id}/skip-character-creation")
def skip_character_creation_route(