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:
2
TODO.md
2
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
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="lobby-view text-center">
|
||||
@@ -72,6 +85,22 @@
|
||||
{/if}
|
||||
</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">
|
||||
{#if state.player.is_admin}
|
||||
{#if state.players.length >= 3 || state.game.dev_mode}
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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 ###
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user