Implement Specials feature, Admin UI enhancements, and Database Rebuild tool

This commit is contained in:
Hördle Bot
2025-11-22 16:09:45 +01:00
parent c270f2098f
commit 903d626699
16 changed files with 816 additions and 37 deletions

View File

@@ -111,3 +111,92 @@ export async function getOrCreateDailyPuzzle(genreName: string | null = null) {
return null;
}
}
export async function getOrCreateSpecialPuzzle(specialName: string) {
try {
const today = getTodayISOString();
const special = await prisma.special.findUnique({
where: { name: specialName }
});
if (!special) return null;
let dailyPuzzle = await prisma.dailyPuzzle.findFirst({
where: {
date: today,
specialId: special.id
},
include: { song: true },
});
if (!dailyPuzzle) {
// Get songs available for this special
const allSongs = await prisma.song.findMany({
where: { specials: { some: { id: special.id } } },
include: {
puzzles: {
where: { specialId: special.id }
},
},
});
if (allSongs.length === 0) return null;
// Calculate weights
const weightedSongs = allSongs.map(song => ({
song,
weight: 1.0 / (song.puzzles.length + 1),
}));
const totalWeight = weightedSongs.reduce((sum, item) => sum + item.weight, 0);
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;
}
}
try {
dailyPuzzle = await prisma.dailyPuzzle.create({
data: {
date: today,
songId: selectedSong.id,
specialId: special.id
},
include: { song: true },
});
} catch (e) {
dailyPuzzle = await prisma.dailyPuzzle.findFirst({
where: {
date: today,
specialId: special.id
},
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,
special: specialName,
maxAttempts: special.maxAttempts,
unlockSteps: JSON.parse(special.unlockSteps)
};
} catch (error) {
console.error('Error in getOrCreateSpecialPuzzle:', error);
return null;
}
}

View File

@@ -25,7 +25,7 @@ export interface Statistics {
const STORAGE_KEY = 'hoerdle_game_state';
const STATS_KEY = 'hoerdle_statistics';
export function useGameState(genre: string | null = null) {
export function useGameState(genre: string | null = null, maxAttempts: number = 7) {
const [gameState, setGameState] = useState<GameState | null>(null);
const [statistics, setStatistics] = useState<Statistics | null>(null);
@@ -115,6 +115,10 @@ export function useGameState(genre: string | null = null) {
case 5: newStats.solvedIn5++; break;
case 6: newStats.solvedIn6++; break;
case 7: newStats.solvedIn7++; break;
default:
// For custom attempts > 7, we currently don't have specific stats buckets
// We could add a 'solvedInOther' or just ignore for now
break;
}
} else {
newStats.failed++;
@@ -129,7 +133,7 @@ export function useGameState(genre: string | null = null) {
const newGuesses = [...gameState.guesses, guess];
const isSolved = correct;
const isFailed = !correct && newGuesses.length >= 7;
const isFailed = !correct && newGuesses.length >= maxAttempts;
const newState = {
...gameState,