| name | convex-game-management |
| description | Manage game lifecycle, player turns, round rotation, scoring, and game state transitions. Use when implementing game creation, turn management, score updates, and game completion flows in PictionAI. |
| compatibility | Requires Convex backend with mutations and queries, React hooks (useQuery, useMutation) |
| metadata | [object Object] |
Convex Game Management
Overview
This skill handles the complete game lifecycle for PictionAI's multiplayer Pictionary game, including game creation, player management, turn-based rotation, atomic turn completion with scoring, and game state transitions.
Core Concepts
Game States
- waiting - Game created, players joining in lobby
- started - Game in progress, turns rotating
- finished - All rounds complete, winner determined
Turn States
- drawing - Active turn, timer running (60-120 seconds)
- completed - Turn finished with correct guess
- time_up - Timer expired, no points awarded
Atomic Turn System
Turns are completed atomically to ensure data consistency:
- Start Turn: Drawer calls
startNewTurnmutation - Draw & Guess Phase: Real-time canvas sync, players submit guesses
- Turn Completion: Three atomic scenarios:
- ✅ Correct guess → Both guesser and drawer earn points
- 🏆 Manual winner selection → Drawer selects winner from guessers
- ⏱️ Time up → No points, proceed to next turn
- Score Updates: Dual scoring system (guesser time bonus + drawer base score)
- Next Turn: Auto round-robin rotation to next player
Scoring System
Guess Scoring (Correct Answer)
- Guesser: Base 50 points + time bonus (50 - elapsed_seconds, min 5 points)
- Drawer: 25% of guesser's score, minimum 10 points
Drawer Scoring (Manual Winner)
- Selected Guesser: 30 points
- Drawer: 25 points
Key Mutations
startNewTurn
Start a new drawing turn, assign card, initialize timer.
mutation startNewTurn {
args: {
game_id: Id<"games">
}
// Returns: {
// turn_id: Id<"turns">,
// card: { word: string, category: string },
// drawer_id: Id<"users">,
// time_limit: number
// }
}
submitGuessAndCompleteTurn
Submit a guess and complete the turn with atomic scoring.
mutation submitGuessAndCompleteTurn {
args: {
game_id: Id<"games">,
turn_id: Id<"turns">,
guesser_id: Id<"users">,
guess: string,
elapsed_time: number,
is_correct?: boolean
}
// Handles three scenarios atomically:
// 1. Correct guess → Award points to both
// 2. Manual selection → Drawer chooses winner
// 3. Time up → Skip to next turn
}
selectWinner (Manual Selection)
Drawer selects a guesser as the winner when time expires.
mutation selectWinner {
args: {
turn_id: Id<"turns">,
selected_guesser_id: Id<"users">
}
// Awards 30 points to guesser, 25 to drawer
}
Key Queries
getGame
Fetch complete game state with players, current turn, scores.
query getGame {
args: { game_id: Id<"games"> }
// Returns full game with nested players, turns, scores
}
getGameTurns
Get all turns in a game with guesses and results.
query getGameTurns {
args: { game_id: Id<"games"> }
// Returns array of turns with scoring details
}
React Integration
// Start a new turn
const startTurn = useMutation(api.mutations.game.startNewTurn);
await startTurn({ game_id: gameId });
// Submit guess and complete turn
const submitGuess = useMutation(api.mutations.game.submitGuessAndCompleteTurn);
await submitGuess({
game_id: gameId,
turn_id: turnId,
guesser_id: userId,
guess: "elephant",
elapsed_time: 45,
});
// Fetch game state
const game = useQuery(api.queries.games.getGame, { game_id: gameId });
Database Schema References
// Games table
games: defineTable({
code: v.string(), // Unique game code
creator_id: v.id("users"),
players: v.array(
v.object({
user_id: v.id("users"),
score: v.number(),
is_drawer: v.boolean(),
})
),
state: v.union(
v.literal("waiting"),
v.literal("started"),
v.literal("finished")
),
current_turn_index: v.number(),
created_at: v.optional(v.number()),
round_count: v.number(),
});
// Turns table
turns: defineTable({
game_id: v.id("games"),
drawer_id: v.id("users"),
card_id: v.id("cards"),
state: v.union(
v.literal("drawing"),
v.literal("completed"),
v.literal("time_up")
),
timer_started_at: v.number(),
time_limit: v.number(),
correct_guesser_id: v.optional(v.id("users")),
guesses: v.array(
v.object({
guesser_id: v.id("users"),
guess: v.string(),
timestamp: v.number(),
is_correct: v.optional(v.boolean()),
})
),
});
Common Patterns
Monitor active turn
const game = useQuery(api.queries.games.getGame, { game_id });
const currentTurn = game?.turns[game.current_turn_index];
const isMyTurn = currentTurn?.drawer_id === userId;
Handle turn completion
// When drawer completes turn with guess result
const completeWithCorrect = useMutation(
api.mutations.game.submitGuessAndCompleteTurn
);
await completeWithCorrect({
game_id: gameId,
turn_id: turnId,
guesser_id: guesserId,
guess: "elephant",
elapsed_time: 30,
is_correct: true,
});
Automatic next turn
The mutation automatically:
- Rotates drawer (round-robin through players)
- Assigns new card
- Starts timer
- Updates game state
Edge Cases
- Multiple guesses per player: Allowed, only latest is scored
- Guess submitted after timer: System checks elapsed time on server
- Late guess arrival: Real-time subscription updates guess feed
- Manual selection ambiguity: Drawer must explicitly select one winner
- Game with 2 players: Drawer rotation still works with round-robin
See Also
- TURN_MANAGEMENT_ANALYSIS.md - Detailed atomic turn analysis
convex/mutations/game.ts- Complete mutation implementationscomponents/game/game-board.tsx- UI integration