diff --git a/src/components/GameTable.tsx b/src/components/GameTable.tsx
index b80b451..6974203 100644
--- a/src/components/GameTable.tsx
+++ b/src/components/GameTable.tsx
@@ -55,7 +55,26 @@ export function GameTable({
const game = useGameStore((s) => s.game);
const reset = useGameStore((s) => s.reset);
const mode = useGameStore((s) => s.mode);
+ const seatPlayers = useGameStore((s) => s.seatPlayers);
const { t } = useI18n();
+
+ // While waiting for the first server state broadcast, show a spinner instead
+ // of an empty green felt — prevents the "3 colored dots / stuck table" bug.
+ const connecting = mode === "online" && seatPlayers.length === 0;
+ if (connecting) {
+ return (
+
+
+
+
{t("mm.searching")}
+
+
+ );
+ }
const [askFf, setAskFf] = useState(false);
const sfx = useSoundStore((s) => s.sfx);
diff --git a/src/lib/game-store.ts b/src/lib/game-store.ts
index 5daee73..5c2828d 100644
--- a/src/lib/game-store.ts
+++ b/src/lib/game-store.ts
@@ -448,6 +448,16 @@ export const useGameStore = create((set, get) => {
reconnectDeadline: null,
seatPlayers: [],
});
+ // Green-felt guard: if the initial state hasn't arrived after 3 s, ask the
+ // server to resend it. Repeat every 3 s until it lands (stops automatically
+ // once seatPlayers is populated, meaning applyServerState ran).
+ const resyncInterval = setInterval(() => {
+ if (get().seatPlayers.length > 0 || !get().live) {
+ clearInterval(resyncInterval);
+ return;
+ }
+ service.resync();
+ }, 3000);
},
applyServerState: (s) => {
diff --git a/src/lib/online/mock-service.ts b/src/lib/online/mock-service.ts
index 26497dd..4274630 100644
--- a/src/lib/online/mock-service.ts
+++ b/src/lib/online/mock-service.ts
@@ -984,6 +984,8 @@ export class MockOnlineService implements OnlineService {
this.matchmaking.phase = "idle";
}
+ resync() { /* mock: game runs client-side, no server resync needed */ }
+
async playNow() {
if (this.matchmaking.phase !== "searching" && this.matchmaking.phase !== "queued") return;
const me = this.profile!;
diff --git a/src/lib/online/service.ts b/src/lib/online/service.ts
index e3886d4..298c4d5 100644
--- a/src/lib/online/service.ts
+++ b/src/lib/online/service.ts
@@ -134,6 +134,8 @@ export interface OnlineService {
cancelMatchmaking(): Promise;
/** Stop waiting for online players — start now, filling empty seats with bots. */
playNow(): Promise;
+ /** Ask the server to re-broadcast the current game state (green-felt guard). */
+ resync(): void;
onMatchmaking(cb: (state: MatchmakingState) => void): Unsubscribe;
/* ----- match players (for the online game driver) ----- */
diff --git a/src/lib/online/signalr-service.ts b/src/lib/online/signalr-service.ts
index 1478a58..678350d 100644
--- a/src/lib/online/signalr-service.ts
+++ b/src/lib/online/signalr-service.ts
@@ -338,6 +338,10 @@ export class SignalrService implements OnlineService {
await this.conn?.invoke("PlayNow");
}
+ resync(): void {
+ void this.conn?.invoke("Resync").catch(() => {});
+ }
+
onMatchmaking(cb: (s: MatchmakingState) => void): Unsubscribe {
this.mmCbs.add(cb);
return () => this.mmCbs.delete(cb);