Address gemini review on pod draft fix: connected_seats fallback + dedupe broadcasts - #1246
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a robust mechanism for tracking per-seat connection states and pick statuses during draft sessions. It replaces the simple picks_this_round counter with a seats_picked_this_round bitmap to prevent a single seat from repeatedly picking and forcing a pack-pass. It also introduces a connected_seats bitmap to authoritatively track and display player connection states in the UI. The review comments correctly identify a performance issue with redundant P2P broadcasts during auto-pick sweeps, as well as a critical bug where uninitialized connected_seats on loaded old saves default to false (incorrectly showing all players as disconnected in the UI).
| let anyPicked = false; | ||
| for (let seat = 0; seat < this.activePodSize; seat++) { | ||
| if (this.picksThisRound.has(seat)) continue; | ||
| try { | ||
| const view = await this.adapter.getViewForSeat(seat); | ||
| if (view.current_pack && view.current_pack.length > 0) { | ||
| const randomIndex = Math.floor(Math.random() * view.current_pack.length); | ||
| const card = view.current_pack[randomIndex]; | ||
| await this.handlePick(seat, card.instance_id); | ||
| anyPicked = true; | ||
| } | ||
| } catch (err) { | ||
| console.error(`[P2PDraftHost] auto-pick failed for seat ${seat}:`, err); | ||
| } | ||
| } | ||
| // If the round did not exhaust via handlePick's internal allPicksSubmitted | ||
| // check, broadcast the swept state so guests don't sit on a stale view | ||
| // (and so the timer-restart logic in handlePick doesn't strand the UI). | ||
| if (anyPicked) { | ||
| await this.broadcastViews(); | ||
| } | ||
| } |
There was a problem hiding this comment.
[HIGH] Redundant P2P broadcasts and bot resolutions during auto-pick sweep. Evidence: client/src/adapter/p2p-draft-host.ts:805.
Why it matters: Calling handlePick with the default resolveBots = true inside the loop triggers redundant bot pick evaluations and expensive P2P view broadcasts for every single timed-out seat.
Suggested fix: Pass false to handlePick inside the loop, then resolve bots and broadcast once at the end.
let anyPicked = false;
for (let seat = 0; seat < this.activePodSize; seat++) {
if (this.picksThisRound.has(seat)) continue;
try {
const view = await this.adapter.getViewForSeat(seat);
if (view.current_pack && view.current_pack.length > 0) {
const randomIndex = Math.floor(Math.random() * view.current_pack.length);
const card = view.current_pack[randomIndex];
await this.handlePick(seat, card.instance_id, false);
anyPicked = true;
}
} catch (err) {
console.error(`[P2PDraftHost] auto-pick failed for seat ${seat}:`, err);
}
}
if (anyPicked) {
await this.resolveBotPicks({ emit: true, persist: true });
const allPicked = await this.adapter.allPicksSubmitted();
if (!allPicked) {
await this.broadcastViews();
}
}
}| pub fn get(&self, seat: u8) -> bool { | ||
| self.0.get(seat as usize).copied().unwrap_or(false) | ||
| } |
There was a problem hiding this comment.
[HIGH] Uninitialized connected_seats defaults to false for loaded old saves. Evidence: crates/draft-core/src/types.rs:281.
Why it matters: When resuming an old draft session, connected_seats is empty, causing get to return false and incorrectly showing all players as disconnected in the UI until the first pick is made.
Suggested fix: Add a get_or helper to SeatFlags that accepts a default value.
pub fn get(&self, seat: u8) -> bool {
self.0.get(seat as usize).copied().unwrap_or(false)
}
pub fn get_or(&self, seat: u8, default: bool) -> bool {
self.0.get(seat as usize).copied().unwrap_or(default)
}| // Source of truth: the runtime `connected_seats` bitmap, | ||
| // populated via `DraftAction::SetSeatConnected` by the host | ||
| // adapter on (dis)connect. Bots are always considered | ||
| // connected by construction. | ||
| DraftSeat::Human { .. } => session.connected_seats.get(i as u8), |
There was a problem hiding this comment.
[HIGH] Uninitialized connected_seats defaults to false for loaded old saves. Evidence: crates/draft-core/src/view.rs:160.
Why it matters: When resuming an old draft session, connected_seats is empty, causing get to return false and incorrectly showing all players as disconnected in the spectator view.
Suggested fix: Use get_or with a default of true to correctly reflect the connection state.
| // Source of truth: the runtime `connected_seats` bitmap, | |
| // populated via `DraftAction::SetSeatConnected` by the host | |
| // adapter on (dis)connect. Bots are always considered | |
| // connected by construction. | |
| DraftSeat::Human { .. } => session.connected_seats.get(i as u8), | |
| // Source of truth: the runtime `connected_seats` bitmap, | |
| // populated via `DraftAction::SetSeatConnected` by the host | |
| // adapter on (dis)connect. Bots are always considered | |
| // connected by construction. | |
| DraftSeat::Human { .. } => session.connected_seats.get_or(i as u8, true), |
| // Source of truth: the runtime `connected_seats` bitmap, | ||
| // populated via `DraftAction::SetSeatConnected` by the host | ||
| // adapter on (dis)connect. Bots are always considered | ||
| // connected by construction. | ||
| DraftSeat::Human { .. } => session.connected_seats.get(i as u8), |
There was a problem hiding this comment.
[HIGH] Uninitialized connected_seats defaults to false for loaded old saves. Evidence: crates/draft-core/src/view.rs:266.
Why it matters: When resuming an old draft session, connected_seats is empty, causing get to return false and incorrectly showing all players as disconnected in the player view.
Suggested fix: Use get_or with a default of true to correctly reflect the connection state.
| // Source of truth: the runtime `connected_seats` bitmap, | |
| // populated via `DraftAction::SetSeatConnected` by the host | |
| // adapter on (dis)connect. Bots are always considered | |
| // connected by construction. | |
| DraftSeat::Human { .. } => session.connected_seats.get(i as u8), | |
| // Source of truth: the runtime `connected_seats` bitmap, | |
| // populated via `DraftAction::SetSeatConnected` by the host | |
| // adapter on (dis)connect. Bots are always considered | |
| // connected by construction. | |
| DraftSeat::Human { .. } => session.connected_seats.get_or(i as u8, true), |
…adcasts Two HIGH findings from the gemini-code-assist review: 1. Uninitialized `connected_seats` defaults to false for old saves. `#[serde(default)]` produces `SeatFlags(vec![])` for in-flight host saves that predate this field. The view layer then read `.get(i) -> false` for every human seat, rendering the entire pod as disconnected until the first pick lazily-initialised the bitmap via `ensure_len`. Fix: add `SeatFlags::get_or(seat, default)`. The view-layer reads (`filter_for_player` + `filter_for_spectator`) pass `true`, so missing bitmap entries fall back to "connected". Other call sites still use `get` where the false-on-absent semantic is correct (`seats_picked_this_round` reads). 2. Redundant broadcasts during the auto-pick sweep. `handlePick` defaults to `resolveBots: true`, which internally runs `resolveBotPicks` and a full `broadcastViews` after every pick. In an N-seat sweep that's N redundant broadcasts plus the final one I'd added. Fix: pass `resolveBots: false` inside the loop so the per-pick broadcast is suppressed. After the loop, run `resolveBotPicks` once and broadcast once — but only if the round didn't complete via `allPicksSubmitted` inside the last `handlePick` (which broadcasts on its own). Verified: - cargo test -p draft-core (114 pass) - cargo clippy -p draft-core -p draft-wasm -p server-core --all-targets - pnpm run type-check && pnpm lint - pnpm test --run (1018 pass)
|
@gemini-code-assist review — addressed both HIGH findings in commit e79a89c: |
There was a problem hiding this comment.
Code Review
This pull request refactors the draft session's connection and pick-tracking logic to prevent a single seat from forcing a pack-pass (Bug #1) and to properly track runtime connection states. It replaces the simple picks_this_round counter with a per-seat seats_picked_this_round bitmap, introduces a SetSeatConnected action to authoritative engine-side connection tracking, and updates the frontend and adapters to handle these changes. One review comment was provided regarding adherence to the style guide rule R2 (No bool fields), which points out that using a raw bool for connection state in the SetSeatConnected action payload violates the repository's architectural guidelines and suggests using a typed enum instead.
| SetSeatConnected { | ||
| seat: u8, | ||
| connected: bool, | ||
| }, |
There was a problem hiding this comment.
[MEDIUM] Adherence to R2 (No bool fields — parameterize with existing typed enums). Evidence: crates/draft-core/src/types.rs:361-364. Why it matters: Using a raw bool for connection state in DraftAction variant payloads bypasses the typed design space and violates the repository's R2 architectural rule. Suggested fix: Introduce a typed ConnectionStatus enum (e.g., Connected / Disconnected) and use it instead of bool.
References
- R2. No bool fields — parameterize with existing typed enums. A bool field never expresses the design space; the project uses typed enums instead. (link)
…adcasts Two HIGH findings from the gemini-code-assist review: 1. Uninitialized `connected_seats` defaults to false for old saves. `#[serde(default)]` produces `SeatFlags(vec![])` for in-flight host saves that predate this field. The view layer then read `.get(i) -> false` for every human seat, rendering the entire pod as disconnected until the first pick lazily-initialised the bitmap via `ensure_len`. Fix: add `SeatFlags::get_or(seat, default)`. The view-layer reads (`filter_for_player` + `filter_for_spectator`) pass `true`, so missing bitmap entries fall back to "connected". Other call sites still use `get` where the false-on-absent semantic is correct (`seats_picked_this_round` reads). 2. Redundant broadcasts during the auto-pick sweep. `handlePick` defaults to `resolveBots: true`, which internally runs `resolveBotPicks` and a full `broadcastViews` after every pick. In an N-seat sweep that's N redundant broadcasts plus the final one I'd added. Fix: pass `resolveBots: false` inside the loop so the per-pick broadcast is suppressed. After the loop, run `resolveBotPicks` once and broadcast once — but only if the round didn't complete via `allPicksSubmitted` inside the last `handlePick` (which broadcasts on its own). Verified: - cargo test -p draft-core (114 pass) - cargo clippy -p draft-core -p draft-wasm -p server-core --all-targets - pnpm run type-check && pnpm lint - pnpm test --run (1018 pass)
e79a89c to
47b8633
Compare
…n-frozen-timer-inv
Follow-up to
a08d4ce8c(which landed directly on main) addressing two HIGH findings from gemini-code-assist's review on PR #1246.1.
connected_seatsdefaults to "all disconnected" for in-flight saves#[serde(default)]producesSeatFlags(vec![])for host save snapshotsthat predate this field. The view layer then read
.get(i) -> falseforevery human seat, rendering the entire pod as disconnected until the
first pick lazily-initialised the bitmap via
ensure_len.Fix: add
SeatFlags::get_or(seat, default). The view-layer reads(
filter_for_player+filter_for_spectator) passtrue, so missingbitmap entries fall back to "connected". Other call sites still use
getwhere false-on-absent is correct (seats_picked_this_roundreads).2. Redundant broadcasts during auto-pick sweep
handlePickdefaults toresolveBots: true, which internally runsresolveBotPicksand a fullbroadcastViewsafter every pick. In anN-seat sweep that's N redundant broadcasts plus the final one added in
the original fix.
Fix: pass
resolveBots: falseinside the loop so the per-pickbroadcast is suppressed. After the loop, run
resolveBotPicksonce andbroadcast once — but only if the round didn't complete via
allPicksSubmittedinside the lasthandlePick(which broadcasts onits own).
Verified
cargo test -p draft-core— 114 passcargo clippy -p draft-core -p draft-wasm -p server-core --all-targetspnpm run type-check && pnpm lintpnpm test --run— 1018 pass