e.stopPropagation()}
>
{/* Title */}
diff --git a/e2e/fixtures/helpers.ts b/e2e/fixtures/helpers.ts
index 6c34c51..a5f14a3 100644
--- a/e2e/fixtures/helpers.ts
+++ b/e2e/fixtures/helpers.ts
@@ -236,8 +236,8 @@ export const SEARCH_TERMS = [
* Waits for the game to fully load (image and input visible).
*/
export async function waitForGameLoad(page: Page): Promise {
- // Wait for the game image
- await page.locator('img[alt*="Puzzle"]').waitFor({ state: 'visible', timeout: 15000 });
+ // Wait for the game image (use .first() since annotated image overlay may also match)
+ await page.locator('img[alt*="Puzzle"]').first().waitFor({ state: 'visible', timeout: 15000 });
// Wait for a visible input field (uses :visible to handle dual layout on any viewport)
await page.locator('input[placeholder="Diagnosis..."]:visible').first().waitFor({ state: 'visible', timeout: 5000 });
}
diff --git a/e2e/tests/first-time-user.spec.ts b/e2e/tests/first-time-user.spec.ts
index 04760b3..16850a5 100644
--- a/e2e/tests/first-time-user.spec.ts
+++ b/e2e/tests/first-time-user.spec.ts
@@ -110,7 +110,7 @@ test.describe('First-Time User Journey', () => {
// The results modal (inline in GameClient) should show stats
// Check for statistics section in the modal
- const modal = page.locator('.fixed.inset-0.bg-black');
+ const modal = page.locator('[data-testid="results-modal"]');
await expect(modal.getByText('Statistics')).toBeVisible();
// Verify stats: 1 game played
diff --git a/e2e/tests/guess-time-tracking.spec.ts b/e2e/tests/guess-time-tracking.spec.ts
index 378febc..2a703d2 100644
--- a/e2e/tests/guess-time-tracking.spec.ts
+++ b/e2e/tests/guess-time-tracking.spec.ts
@@ -78,7 +78,7 @@ test.describe('Guess Time Tracking', () => {
await expect(page.getByText('Congratulations!').first()).toBeVisible({ timeout: 3000 });
// The results modal should show average guess time
- const modal = page.locator('.fixed.inset-0.bg-black');
+ const modal = page.locator('[data-testid="results-modal"]');
// Look for the "Avg Time" label — use .first() since desktop+mobile layouts both render it
const avgTimeLabel = modal.getByText('Avg Time').first();
diff --git a/e2e/tests/losing-game.spec.ts b/e2e/tests/losing-game.spec.ts
index 2492ae2..ecba1ce 100644
--- a/e2e/tests/losing-game.spec.ts
+++ b/e2e/tests/losing-game.spec.ts
@@ -108,7 +108,7 @@ test.describe('Losing Game Flow', () => {
// Modal should be visible with stats
await expect(page.getByText('Game Over').first()).toBeVisible({ timeout: 3000 });
- const modal = page.locator('.fixed.inset-0.bg-black');
+ const modal = page.locator('[data-testid="results-modal"]');
// Verify stats: 1 game played, 0% win rate
const playedStat = modal.locator('text=Played').locator('..');
diff --git a/e2e/tests/mobile-responsive.spec.ts b/e2e/tests/mobile-responsive.spec.ts
index eb2f824..47e61e4 100644
--- a/e2e/tests/mobile-responsive.spec.ts
+++ b/e2e/tests/mobile-responsive.spec.ts
@@ -50,7 +50,7 @@ test.describe('Mobile Responsiveness', () => {
await waitForGameLoad(page);
// Image should be visible and fit within viewport
- const image = page.locator('img[alt*="Puzzle"]');
+ const image = page.locator('img[alt*="Puzzle"]').first();
const box = await image.boundingBox();
expect(box).not.toBeNull();
if (box) {
diff --git a/e2e/tests/network-failure.spec.ts b/e2e/tests/network-failure.spec.ts
index 45a8e51..9b602a6 100644
--- a/e2e/tests/network-failure.spec.ts
+++ b/e2e/tests/network-failure.spec.ts
@@ -45,7 +45,7 @@ test.describe('Network Failure Handling', () => {
// Wait for game to load
try {
- await page.locator('img[alt*="Puzzle"]').waitFor({ state: 'visible', timeout: 15000 });
+ await page.locator('img[alt*="Puzzle"]').first().waitFor({ state: 'visible', timeout: 15000 });
} catch {
// If the page fails to load, skip this test
test.skip();
@@ -95,7 +95,7 @@ test.describe('Network Failure Handling', () => {
await acceptCookieConsent(page);
try {
- await page.locator('img[alt*="Puzzle"]').waitFor({ state: 'visible', timeout: 15000 });
+ await page.locator('img[alt*="Puzzle"]').first().waitFor({ state: 'visible', timeout: 15000 });
} catch {
test.skip();
return;
@@ -113,7 +113,7 @@ test.describe('Network Failure Handling', () => {
await page.reload();
try {
- await page.locator('img[alt*="Puzzle"]').waitFor({ state: 'visible', timeout: 15000 });
+ await page.locator('img[alt*="Puzzle"]').first().waitFor({ state: 'visible', timeout: 15000 });
// Game loaded successfully after network restored
await expect(page.getByText("What's the Diagnosis?").first()).toBeVisible();
} catch {
diff --git a/lib/supabase.ts b/lib/supabase.ts
index 95f2599..e98d71e 100644
--- a/lib/supabase.ts
+++ b/lib/supabase.ts
@@ -469,28 +469,38 @@ export function calculatePercentileBeat(
}
/**
- * Fetches the guess distribution for a specific puzzle from the database.
+ * Fetches the guess distribution for a specific puzzle from the database,
+ * along with the total number of attempts (including losses).
*/
export async function getPuzzleGuessDistribution(
puzzleNumber: number
-): Promise<{ [key: number]: number } | null> {
+): Promise<{ distribution: { [key: number]: number }; totalAttempts: number } | null> {
try {
- const { data, error } = await supabase
- .from('game_stats_guess_distribution_by_puzzle')
- .select('guess_count, wins')
- .eq('puzzle_number', puzzleNumber);
-
- if (error) {
- console.error('Error fetching puzzle guess distribution:', error);
+ const [distResult, statsResult] = await Promise.all([
+ supabase
+ .from('game_stats_guess_distribution_by_puzzle')
+ .select('guess_count, wins')
+ .eq('puzzle_number', puzzleNumber),
+ supabase
+ .from('game_stats_by_puzzle')
+ .select('times_played')
+ .eq('puzzle_number', puzzleNumber)
+ .single(),
+ ]);
+
+ if (distResult.error) {
+ console.error('Error fetching puzzle guess distribution:', distResult.error);
return null;
}
const distribution: { [key: number]: number } = {};
- data?.forEach((row: { guess_count: number; wins: number }) => {
+ distResult.data?.forEach((row: { guess_count: number; wins: number }) => {
distribution[row.guess_count] = row.wins;
});
- return distribution;
+ const totalAttempts = Number(statsResult.data?.times_played) || 0;
+
+ return { distribution, totalAttempts };
} catch (error) {
console.error('Error fetching puzzle guess distribution:', error);
return null;
@@ -500,10 +510,12 @@ export async function getPuzzleGuessDistribution(
/**
* Calculates what percentage of players you beat on a specific puzzle,
* based on your actual guess count vs the puzzle's global distribution.
+ * Includes players who lost (failed to solve) as "worse than user".
*/
export function calculatePuzzlePercentile(
userGuessCount: number,
- puzzleGuessDistribution: { [key: number]: number }
+ puzzleGuessDistribution: { [key: number]: number },
+ totalAttempts?: number
): number | null {
let totalWins = 0;
for (const count of Object.values(puzzleGuessDistribution)) {
@@ -512,13 +524,16 @@ export function calculatePuzzlePercentile(
if (totalWins === 0) return null;
- // Count players who took MORE guesses than the user on this puzzle
- let worseThanUser = 0;
+ const totalPlayers = totalAttempts && totalAttempts > totalWins ? totalAttempts : totalWins;
+ const losers = totalPlayers - totalWins;
+
+ // Count winners who took MORE guesses + all players who lost
+ let worseThanUser = losers;
for (const [guess, count] of Object.entries(puzzleGuessDistribution)) {
if (Number(guess) > userGuessCount) {
worseThanUser += count;
}
}
- return Math.round((worseThanUser / totalWins) * 100);
+ return Math.round((worseThanUser / totalPlayers) * 100);
}