Feat: Genre system with per-genre daily puzzles
This commit is contained in:
113
lib/dailyPuzzle.ts
Normal file
113
lib/dailyPuzzle.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { getTodayISOString } from './dateUtils';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export async function getOrCreateDailyPuzzle(genreName: string | null = null) {
|
||||
try {
|
||||
const today = getTodayISOString();
|
||||
let genreId: number | null = null;
|
||||
|
||||
if (genreName) {
|
||||
const genre = await prisma.genre.findUnique({
|
||||
where: { name: genreName }
|
||||
});
|
||||
if (genre) {
|
||||
genreId = genre.id;
|
||||
} else {
|
||||
return null; // Genre not found
|
||||
}
|
||||
}
|
||||
|
||||
let dailyPuzzle = await prisma.dailyPuzzle.findFirst({
|
||||
where: {
|
||||
date: today,
|
||||
genreId: genreId
|
||||
},
|
||||
include: { song: true },
|
||||
});
|
||||
|
||||
console.log(`[Daily Puzzle] Date: ${today}, Genre: ${genreName || 'Global'}, Found existing: ${!!dailyPuzzle}`);
|
||||
|
||||
if (!dailyPuzzle) {
|
||||
// Get songs available for this genre
|
||||
const whereClause = genreId
|
||||
? { genres: { some: { id: genreId } } }
|
||||
: {}; // Global puzzle picks from ALL songs
|
||||
|
||||
const allSongs = await prisma.song.findMany({
|
||||
where: whereClause,
|
||||
include: {
|
||||
puzzles: {
|
||||
where: { genreId: genreId }
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (allSongs.length === 0) {
|
||||
console.log(`[Daily Puzzle] No songs available for genre: ${genreName || 'Global'}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Calculate weights
|
||||
const weightedSongs = allSongs.map(song => ({
|
||||
song,
|
||||
weight: 1.0 / (song.puzzles.length + 1),
|
||||
}));
|
||||
|
||||
// Calculate total weight
|
||||
const totalWeight = weightedSongs.reduce((sum, item) => sum + item.weight, 0);
|
||||
|
||||
// Pick a random song based on weights
|
||||
let random = Math.random() * totalWeight;
|
||||
let selectedSong = weightedSongs[0].song;
|
||||
|
||||
for (const item of weightedSongs) {
|
||||
random -= item.weight;
|
||||
if (random <= 0) {
|
||||
selectedSong = item.song;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Create the daily puzzle
|
||||
try {
|
||||
dailyPuzzle = await prisma.dailyPuzzle.create({
|
||||
data: {
|
||||
date: today,
|
||||
songId: selectedSong.id,
|
||||
genreId: genreId
|
||||
},
|
||||
include: { song: true },
|
||||
});
|
||||
console.log(`[Daily Puzzle] Created new puzzle for ${today} (Genre: ${genreName || 'Global'}) with song: ${selectedSong.title}`);
|
||||
} catch (e) {
|
||||
// Handle race condition
|
||||
console.log('[Daily Puzzle] Creation failed, trying to fetch again (likely race condition)');
|
||||
dailyPuzzle = await prisma.dailyPuzzle.findFirst({
|
||||
where: {
|
||||
date: today,
|
||||
genreId: genreId
|
||||
},
|
||||
include: { song: true },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!dailyPuzzle) return null;
|
||||
|
||||
return {
|
||||
id: dailyPuzzle.id,
|
||||
audioUrl: `/uploads/${dailyPuzzle.song.filename}`,
|
||||
songId: dailyPuzzle.songId,
|
||||
title: dailyPuzzle.song.title,
|
||||
artist: dailyPuzzle.song.artist,
|
||||
coverImage: dailyPuzzle.song.coverImage ? `/uploads/covers/${dailyPuzzle.song.coverImage}` : null,
|
||||
genre: genreName
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error in getOrCreateDailyPuzzle:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -25,13 +25,20 @@ export interface Statistics {
|
||||
const STORAGE_KEY = 'hoerdle_game_state';
|
||||
const STATS_KEY = 'hoerdle_statistics';
|
||||
|
||||
export function useGameState() {
|
||||
export function useGameState(genre: string | null = null) {
|
||||
const [gameState, setGameState] = useState<GameState | null>(null);
|
||||
const [statistics, setStatistics] = useState<Statistics | null>(null);
|
||||
|
||||
const STORAGE_KEY_PREFIX = 'hoerdle_game_state';
|
||||
const STATS_KEY_PREFIX = 'hoerdle_statistics';
|
||||
|
||||
const getStorageKey = () => genre ? `${STORAGE_KEY_PREFIX}_${genre}` : STORAGE_KEY_PREFIX;
|
||||
const getStatsKey = () => genre ? `${STATS_KEY_PREFIX}_${genre}` : STATS_KEY_PREFIX;
|
||||
|
||||
useEffect(() => {
|
||||
// Load game state
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
const storageKey = getStorageKey();
|
||||
const stored = localStorage.getItem(storageKey);
|
||||
const today = getTodayISOString();
|
||||
|
||||
if (stored) {
|
||||
@@ -48,7 +55,7 @@ export function useGameState() {
|
||||
lastPlayed: Date.now(),
|
||||
};
|
||||
setGameState(newState);
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(newState));
|
||||
localStorage.setItem(storageKey, JSON.stringify(newState));
|
||||
}
|
||||
} else {
|
||||
// No state
|
||||
@@ -60,11 +67,12 @@ export function useGameState() {
|
||||
lastPlayed: Date.now(),
|
||||
};
|
||||
setGameState(newState);
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(newState));
|
||||
localStorage.setItem(storageKey, JSON.stringify(newState));
|
||||
}
|
||||
|
||||
// Load statistics
|
||||
const storedStats = localStorage.getItem(STATS_KEY);
|
||||
const statsKey = getStatsKey();
|
||||
const storedStats = localStorage.getItem(statsKey);
|
||||
if (storedStats) {
|
||||
const parsedStats = JSON.parse(storedStats);
|
||||
// Migration for existing stats without solvedIn7
|
||||
@@ -84,13 +92,13 @@ export function useGameState() {
|
||||
failed: 0,
|
||||
};
|
||||
setStatistics(newStats);
|
||||
localStorage.setItem(STATS_KEY, JSON.stringify(newStats));
|
||||
localStorage.setItem(statsKey, JSON.stringify(newStats));
|
||||
}
|
||||
}, []);
|
||||
}, [genre]); // Re-run when genre changes
|
||||
|
||||
const saveState = (newState: GameState) => {
|
||||
setGameState(newState);
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(newState));
|
||||
localStorage.setItem(getStorageKey(), JSON.stringify(newState));
|
||||
};
|
||||
|
||||
const updateStatistics = (attempts: number, solved: boolean) => {
|
||||
@@ -113,7 +121,7 @@ export function useGameState() {
|
||||
}
|
||||
|
||||
setStatistics(newStats);
|
||||
localStorage.setItem(STATS_KEY, JSON.stringify(newStats));
|
||||
localStorage.setItem(getStatsKey(), JSON.stringify(newStats));
|
||||
};
|
||||
|
||||
const addGuess = (guess: string, correct: boolean) => {
|
||||
|
||||
Reference in New Issue
Block a user