Skip to content

Commit d4040df

Browse files
committed
Add Excluded Images Logic
1 parent f92468b commit d4040df

7 files changed

Lines changed: 174 additions & 24 deletions

File tree

server.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -212,10 +212,13 @@ app.prepare().then(() => {
212212

213213
switch (message.type) {
214214
case "room:create": {
215-
const { hostId } = message.payload as { hostId: string };
215+
const { hostId, excludeImageIds } = message.payload as {
216+
hostId: string;
217+
excludeImageIds?: string[];
218+
};
216219
console.log(`[WebSocket] room:create - hostId: ${hostId}`);
217220

218-
const room = createRoom(hostId);
221+
const room = createRoom(hostId, excludeImageIds || []);
219222
console.log(`[WebSocket] Room created - roomId: ${room.roomId}`);
220223

221224
ws.roomId = room.roomId;
@@ -228,7 +231,11 @@ app.prepare().then(() => {
228231

229232
sendTo(ws, {
230233
type: "room:created",
231-
payload: { roomId: room.roomId, hostId: room.hostId },
234+
payload: {
235+
roomId: room.roomId,
236+
hostId: room.hostId,
237+
imageIds: room.images.map((img) => img.id),
238+
},
232239
});
233240
break;
234241
}

src/app/[locale]/host/page.tsx

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { Leaderboard } from "@/components/Leaderboard";
1111
import { LanguageSwitcher } from "@/components/LanguageSwitcher";
1212
import { Footer } from "@/components/Footer";
1313
import Link from "next/link";
14+
import { getSeenImageIds, markImagesAsSeen } from "@/lib/seenImages";
1415
import type {
1516
WSMessage,
1617
Player,
@@ -36,6 +37,7 @@ interface HostState {
3637
timeLeft: number;
3738
voteCount: number;
3839
correctAnswer: ImageType | null;
40+
imageIds: string[];
3941
}
4042

4143
export default function HostPage() {
@@ -53,17 +55,23 @@ export default function HostPage() {
5355
timeLeft: 30,
5456
voteCount: 0,
5557
correctAnswer: null,
58+
imageIds: [],
5659
});
5760

5861
const handleMessage = useCallback((message: WSMessage) => {
5962
switch (message.type) {
6063
case "room:created": {
61-
const payload = message.payload as { roomId: string; hostId: string };
64+
const payload = message.payload as {
65+
roomId: string;
66+
hostId: string;
67+
imageIds: string[];
68+
};
6269
setState((prev) => ({
6370
...prev,
6471
status: "lobby",
6572
roomId: payload.roomId,
6673
hostId: payload.hostId,
74+
imageIds: payload.imageIds || [],
6775
}));
6876
break;
6977
}
@@ -128,11 +136,18 @@ export default function HostPage() {
128136

129137
case "game:end": {
130138
const payload = message.payload as { players: Player[] };
131-
setState((prev) => ({
132-
...prev,
133-
status: "finished",
134-
players: payload.players,
135-
}));
139+
setState((prev) => {
140+
// Mark all game images as seen
141+
if (prev.imageIds.length > 0) {
142+
markImagesAsSeen(prev.imageIds);
143+
}
144+
145+
return {
146+
...prev,
147+
status: "finished",
148+
players: payload.players,
149+
};
150+
});
136151
break;
137152
}
138153
}
@@ -147,11 +162,13 @@ export default function HostPage() {
147162
useEffect(() => {
148163
if (isConnected && !state.roomId) {
149164
const hostId = crypto.randomUUID();
165+
const excludeImageIds = getSeenImageIds();
150166
console.log("[Host] Creating room via WebSocket, hostId:", hostId);
167+
console.log("[Host] Excluding seen images:", excludeImageIds.length);
151168

152169
send({
153170
type: "room:create",
154-
payload: { hostId },
171+
payload: { hostId, excludeImageIds },
155172
});
156173
}
157174
}, [isConnected, state.roomId, send]);

src/app/api/images/route.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,14 @@ export async function GET(request: NextRequest) {
55
try {
66
const { searchParams } = new URL(request.url);
77
const includeTypes = searchParams.get("includeTypes") === "true";
8+
const excludeIdsParam = searchParams.get("excludeIds");
89

9-
const images = selectGameImages(12);
10+
// Parse excluded image IDs from query parameter
11+
const excludeIds: string[] = excludeIdsParam
12+
? JSON.parse(decodeURIComponent(excludeIdsParam))
13+
: [];
14+
15+
const images = selectGameImages(12, excludeIds);
1016

1117
if (includeTypes) {
1218
// For solo mode - include types (secure since it's all client-side)

src/hooks/useSoloGame.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { useState, useCallback, useEffect } from "react";
22
import { useTimer } from "./useTimer";
33
import type { GameImage, ImageType } from "@/lib/types";
4+
import {
5+
getSeenImageIds,
6+
markImagesAsSeen,
7+
resetSeenImages,
8+
} from "@/lib/seenImages";
49

510
interface SoloGameState {
611
status: "loading" | "ready" | "playing" | "showing-result" | "finished";
@@ -50,12 +55,19 @@ export function useSoloGame() {
5055

5156
const loadImages = async () => {
5257
try {
53-
const response = await fetch("/api/images");
54-
const data = await response.json();
58+
// Get list of already seen image IDs
59+
const seenIds = getSeenImageIds();
5560

56-
// We need to fetch the full images with types from server
57-
// For solo mode, we'll fetch them with a special flag
58-
const fullResponse = await fetch("/api/images?includeTypes=true");
61+
// Build URL with exclude parameter
62+
const params = new URLSearchParams({
63+
includeTypes: "true",
64+
});
65+
66+
if (seenIds.length > 0) {
67+
params.set("excludeIds", encodeURIComponent(JSON.stringify(seenIds)));
68+
}
69+
70+
const fullResponse = await fetch(`/api/images?${params.toString()}`);
5971
const fullData = await fullResponse.json();
6072

6173
setImages(fullData);
@@ -115,6 +127,10 @@ export function useSoloGame() {
115127
const nextRoundNum = gameState.currentRound + 1;
116128

117129
if (nextRoundNum > TOTAL_ROUNDS) {
130+
// Mark all played images as seen
131+
const playedImageIds = images.map((img) => img.id);
132+
markImagesAsSeen(playedImageIds);
133+
118134
setGameState((prev) => ({
119135
...prev,
120136
status: "finished",

src/lib/game-store.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,12 @@ const rooms = new Map<string, RoomState>();
88
const TOTAL_ROUNDS = 12;
99
const TIME_PER_ROUND = 30;
1010

11-
export function createRoom(hostId: string): RoomState {
11+
export function createRoom(
12+
hostId: string,
13+
excludeImageIds: string[] = [],
14+
): RoomState {
1215
const roomId = generateRoomCode();
13-
const images = selectGameImages(TOTAL_ROUNDS);
16+
const images = selectGameImages(TOTAL_ROUNDS, excludeImageIds);
1417

1518
const room: RoomState = {
1619
roomId,

src/lib/images.ts

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,16 +53,42 @@ export function shuffleArray<T>(array: T[]): T[] {
5353
}
5454

5555
// Select images for a game (50/50 split, 12 total by default)
56-
export function selectGameImages(totalRounds: number = 12): GameImage[] {
56+
// excludeIds: optional array of image IDs to exclude (for no-repeat functionality)
57+
export function selectGameImages(
58+
totalRounds: number = 12,
59+
excludeIds: string[] = [],
60+
): GameImage[] {
5761
const allImages = getAvailableImages();
58-
const realImages = shuffleArray(
59-
allImages.filter((img) => img.type === "real"),
62+
63+
// Filter out excluded images
64+
const availableImages = allImages.filter(
65+
(img) => !excludeIds.includes(img.id),
6066
);
61-
const aiImages = shuffleArray(allImages.filter((img) => img.type === "ai"));
67+
68+
// Separate by type
69+
let realImages = availableImages.filter((img) => img.type === "real");
70+
let aiImages = availableImages.filter((img) => img.type === "ai");
6271

6372
const halfRounds = Math.floor(totalRounds / 2);
64-
const selectedReal = realImages.slice(0, halfRounds);
65-
const selectedAI = aiImages.slice(0, totalRounds - halfRounds);
73+
74+
// If we don't have enough unseen images, reset and use all images
75+
if (
76+
realImages.length < halfRounds ||
77+
aiImages.length < totalRounds - halfRounds
78+
) {
79+
console.log(
80+
"Not enough unseen images, resetting to use all available images",
81+
);
82+
realImages = allImages.filter((img) => img.type === "real");
83+
aiImages = allImages.filter((img) => img.type === "ai");
84+
}
85+
86+
// Shuffle and select
87+
const shuffledReal = shuffleArray(realImages);
88+
const shuffledAI = shuffleArray(aiImages);
89+
90+
const selectedReal = shuffledReal.slice(0, halfRounds);
91+
const selectedAI = shuffledAI.slice(0, totalRounds - halfRounds);
6692

6793
// Combine and shuffle for random order
6894
return shuffleArray([...selectedReal, ...selectedAI]);

src/lib/seenImages.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/**
2+
* Client-side utility for tracking which images a user has seen
3+
* Uses localStorage to persist across sessions
4+
*/
5+
6+
const SEEN_IMAGES_KEY = "realoria_seen_images";
7+
8+
export interface SeenImagesData {
9+
seenIds: string[];
10+
lastUpdated: number;
11+
}
12+
13+
/**
14+
* Get the list of image IDs that the user has already seen
15+
*/
16+
export function getSeenImageIds(): string[] {
17+
if (typeof window === "undefined") return [];
18+
19+
try {
20+
const data = localStorage.getItem(SEEN_IMAGES_KEY);
21+
if (!data) return [];
22+
23+
const parsed: SeenImagesData = JSON.parse(data);
24+
return parsed.seenIds || [];
25+
} catch (error) {
26+
console.error("Error reading seen images:", error);
27+
return [];
28+
}
29+
}
30+
31+
/**
32+
* Mark a list of image IDs as seen
33+
*/
34+
export function markImagesAsSeen(imageIds: string[]): void {
35+
if (typeof window === "undefined") return;
36+
37+
try {
38+
const currentSeen = getSeenImageIds();
39+
const updatedSeen = [...new Set([...currentSeen, ...imageIds])];
40+
41+
const data: SeenImagesData = {
42+
seenIds: updatedSeen,
43+
lastUpdated: Date.now(),
44+
};
45+
46+
localStorage.setItem(SEEN_IMAGES_KEY, JSON.stringify(data));
47+
} catch (error) {
48+
console.error("Error saving seen images:", error);
49+
}
50+
}
51+
52+
/**
53+
* Reset the list of seen images (start fresh)
54+
*/
55+
export function resetSeenImages(): void {
56+
if (typeof window === "undefined") return;
57+
58+
try {
59+
localStorage.removeItem(SEEN_IMAGES_KEY);
60+
} catch (error) {
61+
console.error("Error resetting seen images:", error);
62+
}
63+
}
64+
65+
/**
66+
* Check if we need to reset (when user has seen all or most images)
67+
* Returns true if we should reset the seen list
68+
*/
69+
export function shouldResetSeenImages(
70+
totalAvailableImages: number,
71+
seenCount: number,
72+
): boolean {
73+
// If user has seen 90% or more of available images, reset
74+
return seenCount >= totalAvailableImages * 0.9;
75+
}

0 commit comments

Comments
 (0)