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.
|
- [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
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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."
|
||||||
|
|||||||
@@ -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_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)
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user