From 777804ffe47b43f9a95831c6d66e1c8da40a672c Mon Sep 17 00:00:00 2001 From: Markus Aurala Date: Mon, 8 Jun 2026 21:38:24 +0300 Subject: [PATCH] fix(cluster): dedupe win/explode positions to prevent bonus freeze MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A board cell can belong to more than one win — a wild substituting into multiple adjacent clusters lists its position in each. boardWithAnimateSymbols and tumbleBoardExplode store each position's resolver on the symbol via `reelSymbol.oncomplete = resolve`; a duplicate overwrites the prior resolver, so the symbol's single `complete` resolves only the last await — the earlier one never settles, Promise.all hangs, and the round freezes. Dedupe positions by (reel,row) before awaiting in both handlers. Addresses #14. Co-Authored-By: Claude Opus 4.8 --- apps/cluster/src/components/Board.svelte | 16 +++++++++++++++- apps/cluster/src/components/TumbleBoard.svelte | 13 ++++++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/apps/cluster/src/components/Board.svelte b/apps/cluster/src/components/Board.svelte index 342aab676..76647ed7d 100644 --- a/apps/cluster/src/components/Board.svelte +++ b/apps/cluster/src/components/Board.svelte @@ -30,8 +30,22 @@ boardShow: () => (show = true), boardHide: () => (show = false), boardWithAnimateSymbols: async ({ symbolPositions }) => { + // Dedupe positions before awaiting. A board cell can appear in more + // than one win — a wild substitutes into multiple adjacent clusters, + // so each lists its position. Without dedupe, the duplicate iteration + // overwrites the prior `reelSymbol.oncomplete`, so the symbol's single + // `complete` resolves only the last await; the earlier one never + // settles and `Promise.all` hangs forever — the bonus/free-spin freeze + // (#14). + const seen = new Set(); + const uniquePositions = symbolPositions.filter((position) => { + const key = `${position.reel},${position.row}`; + if (seen.has(key)) return false; + seen.add(key); + return true; + }); const getPromises = () => - symbolPositions.map(async (position) => { + uniquePositions.map(async (position) => { const reelSymbol = context.stateGame.board[position.reel].reelState.symbols[position.row]; reelSymbol.symbolState = 'win'; await waitForResolve((resolve) => (reelSymbol.oncomplete = resolve)); diff --git a/apps/cluster/src/components/TumbleBoard.svelte b/apps/cluster/src/components/TumbleBoard.svelte index 2020e475e..d86accded 100644 --- a/apps/cluster/src/components/TumbleBoard.svelte +++ b/apps/cluster/src/components/TumbleBoard.svelte @@ -82,8 +82,19 @@ context.stateGame.tumbleBoardBase = []; }, tumbleBoardExplode: async ({ explodingPositions }) => { + // Dedupe positions (same reason as boardWithAnimateSymbols in + // Board.svelte): a shared wild can appear in explodingPositions more + // than once; the duplicate overwrites `tumbleSymbol.oncomplete`, so the + // earlier await never settles and `Promise.all` hangs forever (#14). + const seen = new Set(); + const uniquePositions = explodingPositions.filter((position) => { + const key = `${position.reel},${position.row}`; + if (seen.has(key)) return false; + seen.add(key); + return true; + }); const getPromises = () => - explodingPositions.map(async (position) => { + uniquePositions.map(async (position) => { const tumbleSymbol = context.stateGame.tumbleBoardBase[position.reel][position.row]; tumbleSymbol.symbolState = 'explosion'; await waitForResolve((resolve) => (tumbleSymbol.oncomplete = resolve));