Add teaching mode: admin can seed themselves as scene-1 Deep

An admin can now opt into "teaching mode" from the lobby, seeding
themselves as the Rank-3 player who must play the Deep in the first
scene (instead of leaving it to the random starting-rank roll), so they
can run the table for new crews.

- Game.teaching_deep_player_id (+ Alembic migration)
- maybe_finish_assign_techniques honors it before rolling ranks, so both
  the normal flow and the dev-mode skip path respect it
- Admin-gated POST .../teaching-mode toggle (pre-rank phases only)
- Lobby checkbox UI, greyed out if another admin already volunteered

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-14 09:14:58 -07:00
parent 5a9d712c95
commit bfe982251e
7 changed files with 117 additions and 1 deletions

View File

@@ -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. - [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 - [ ] Visual feedback confirming hand refresh in Deep Upkeep

View File

@@ -13,6 +13,22 @@
margin-bottom: 2rem; 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 { .link-item {
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
} }

View File

@@ -24,6 +24,19 @@
starting = false; 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);
}
}
</script> </script>
<div class="lobby-view text-center"> <div class="lobby-view text-center">
@@ -72,6 +85,22 @@
{/if} {/if}
</div> </div>
{#if state.player.is_admin}
<div class="teaching-box glass-panel">
<label class="checkbox-label" class:disabled={teachingTakenByOther}>
<input type="checkbox" checked={teaching} disabled={teachingTakenByOther} on:change={toggleTeaching}>
<span>📚 <strong>Teaching mode:</strong> I'll be the Deep for the first scene</span>
</label>
<p class="info-text">
{#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}
</p>
</div>
{/if}
<div class="lobby-action"> <div class="lobby-action">
{#if state.player.is_admin} {#if state.player.is_admin}
{#if state.players.length >= 3 || state.game.dev_mode} {#if state.players.length >= 3 || state.game.dev_mode}

View File

@@ -435,6 +435,14 @@ def maybe_finish_assign_techniques(db: Session, game: Game):
# 1. Randomly assign starting ranks # 1. Randomly assign starting ranks
random.shuffle(players_list) 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 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." # "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." # "All remaining Players start at Rank 2. These Players may choose to play either during the first scene."

View File

@@ -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 ###

View File

@@ -18,6 +18,7 @@ class Game(SQLModel, table=True):
completed_crew_2: bool = Field(default=False) # Choose a Captain completed_crew_2: bool = Field(default=False) # Choose a Captain
completed_crew_3: bool = Field(default=False) # Commit Piracy 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 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) players: List["Player"] = Relationship(back_populates="game", cascade_delete=True)
obstacles: List["Obstacle"] = Relationship(back_populates="game", cascade_delete=True) obstacles: List["Obstacle"] = Relationship(back_populates="game", cascade_delete=True)

View File

@@ -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") 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} 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 # Dev Mode shortcut: auto-complete character creation for everyone and move on
@router.post("/game/{game_id}/player/{player_id}/skip-character-creation") @router.post("/game/{game_id}/player/{player_id}/skip-character-creation")
def skip_character_creation_route( def skip_character_creation_route(