Update Version (Beta): One word each new UTC hour

This commit is contained in:
Zakaria
2026-05-11 20:55:14 -04:00
parent 70f19904f2
commit 3175a4dbf1
30 changed files with 4196 additions and 17 deletions
+245 -5
View File
@@ -9,12 +9,17 @@ let lastResult = null
let hasInitialized = false
let wordListMode = "loading"
let isAnimating = false
let hourlyRound = null
let lockCountdownTimer = null
let supabaseClient = null
const WORD_LENGTH = 5
const MAX_GUESSES = 6
const FLIP_ANIMATION_DURATION = 500
const DANCE_ANIMATION_DURATION = 500
const STATS_KEY = "fancy-wordle-stats-v2"
const LOCAL_ROUND_KEY = "fancy-wordle-hourly-round-v1"
const PLAY_INTERVAL_MS = 60 * 60 * 1000
const KEY_ROWS = ["QWERTYUIOP", "ASDFGHJKL", "ZXCVBNM"]
const FALLBACK_TARGET_WORDS = [
"about",
@@ -92,10 +97,7 @@ async function initializeGame() {
showAlert("Using a compact offline word set", 3000)
}
targetWord = targetWords[Math.floor(Math.random() * targetWords.length)]
dictionarySet.add(targetWord)
updateTriesStatus()
startInteraction()
await startHourlyRound()
}
async function loadWordLists() {
@@ -165,6 +167,145 @@ function normalizeWords(words) {
.filter(word => word.length === WORD_LENGTH)
}
async function startHourlyRound() {
const selectedWord = getCurrentHourlyWord()
const remoteRound = await startRemoteHourlyRound()
const round = remoteRound || startLocalHourlyRound(selectedWord)
applyHourlyRound(round)
}
async function startRemoteHourlyRound() {
const client = getSupabaseClient()
if (!client) return null
try {
await ensureSupabaseSession(client)
const { data, error } = await client.rpc("start_hourly_round")
if (error) throw error
const row = Array.isArray(data) ? data[0] : data
if (!row) throw new Error("Supabase did not return a round")
return normalizeRemoteRound(row)
} catch (error) {
console.warn("Supabase hourly lock unavailable; using local lock.", error)
showAlert("Using local hourly lock", 3000)
return null
}
}
function getSupabaseClient() {
const config = window.FANCY_WORDLE_SUPABASE || {}
if (!config.url || !config.anonKey || !window.supabase) return null
if (!supabaseClient) {
supabaseClient = window.supabase.createClient(config.url, config.anonKey)
}
return supabaseClient
}
async function ensureSupabaseSession(client) {
const { data: { session } } = await client.auth.getSession()
if (session) return
const { error } = await client.auth.signInAnonymously()
if (error) throw error
}
function normalizeRemoteRound(row) {
return {
id: row.round_id,
backend: "supabase",
word: row.word,
hourStart: Date.parse(row.hour_start),
startedAt: Date.parse(row.started_at),
nextPlayableAt: Date.parse(row.next_playable_at),
completedAt: row.completed_at ? Date.parse(row.completed_at) : null,
won: row.won,
guessCount: row.guess_count,
isExisting: row.is_existing
}
}
function startLocalHourlyRound(selectedWord) {
const savedRound = getLocalHourlyRound()
if (savedRound && savedRound.nextPlayableAt > Date.now()) return savedRound
const now = Date.now()
const hourStart = getCurrentHourStart(now)
const round = {
id: `local-${now}`,
backend: "local",
word: selectedWord,
hourStart,
startedAt: now,
nextPlayableAt: hourStart + PLAY_INTERVAL_MS,
completedAt: null,
won: null,
guessCount: null,
isExisting: false
}
saveLocalHourlyRound(round)
return round
}
function getLocalHourlyRound() {
try {
const round = JSON.parse(localStorage.getItem(LOCAL_ROUND_KEY))
if (!round?.word || !round?.nextPlayableAt) return null
return {
...round,
backend: "local",
hourStart: Number(round.hourStart) || getCurrentHourStart(Number(round.startedAt) || Date.now()),
startedAt: Number(round.startedAt),
nextPlayableAt: Number(round.nextPlayableAt),
completedAt: round.completedAt ? Number(round.completedAt) : null,
isExisting: true
}
} catch {
return null
}
}
function saveLocalHourlyRound(round) {
localStorage.setItem(LOCAL_ROUND_KEY, JSON.stringify(round))
}
function getCurrentHourlyWord() {
const hourStart = getCurrentHourStart(Date.now())
const hourIndex = Math.floor(hourStart / PLAY_INTERVAL_MS)
return targetWords[hourIndex % targetWords.length]
}
function getCurrentHourStart(timestamp) {
return Math.floor(timestamp / PLAY_INTERVAL_MS) * PLAY_INTERVAL_MS
}
function applyHourlyRound(round) {
hourlyRound = round
targetWord = round.word
dictionarySet.add(targetWord)
currentGuessIndex = round.completedAt ? round.guessCount || 0 : 0
updateTriesStatus()
if (round.completedAt) {
lastResult = {
won: Boolean(round.won),
guesses: round.guessCount || MAX_GUESSES,
word: round.word
}
lockUntilNextWord()
return
}
roundStatus.textContent = round.isExisting ? "Hourly word resumed" : "Hourly word ready"
startInteraction()
}
function createBoard() {
guessGrid.innerHTML = ""
@@ -453,6 +594,7 @@ function checkWinLose(guess, tiles) {
celebrateWithConfetti()
roundStatus.textContent = `Solved in ${currentGuessIndex}`
setTimeout(() => showWordDefinition(targetWord, true), 1800)
startLockCountdown(2400)
stopInteraction()
return
}
@@ -465,6 +607,7 @@ function checkWinLose(guess, tiles) {
showAlert(targetWord.toUpperCase(), null)
roundStatus.textContent = "Round complete"
showWordDefinition(targetWord, false)
startLockCountdown(2400)
stopInteraction()
return
}
@@ -477,6 +620,62 @@ function updateTriesStatus() {
triesStatus.textContent = `${currentGuessIndex} / ${MAX_GUESSES}`
}
function lockUntilNextWord() {
gameFinished = true
stopInteraction()
startLockCountdown()
}
function startLockCountdown(delay = 0) {
clearLockCountdown()
const beginCountdown = () => {
updateLockCountdown()
lockCountdownTimer = setInterval(updateLockCountdown, 1000)
}
if (delay > 0) {
lockCountdownTimer = setTimeout(beginCountdown, delay)
return
}
beginCountdown()
}
function clearLockCountdown() {
if (!lockCountdownTimer) return
clearTimeout(lockCountdownTimer)
clearInterval(lockCountdownTimer)
lockCountdownTimer = null
}
function updateLockCountdown() {
if (!hourlyRound?.nextPlayableAt) return
const remaining = hourlyRound.nextPlayableAt - Date.now()
if (remaining <= 0) {
clearLockCountdown()
if (hourlyRound.backend === "local") localStorage.removeItem(LOCAL_ROUND_KEY)
roundStatus.textContent = "New word available"
triesStatus.textContent = "Ready"
showAlert("New word available. Refresh to play.", null)
return
}
roundStatus.textContent = `Next word in ${formatRemainingTime(remaining)}`
triesStatus.textContent = "Locked"
}
function formatRemainingTime(milliseconds) {
const totalSeconds = Math.max(1, Math.ceil(milliseconds / 1000))
const minutes = Math.floor(totalSeconds / 60)
const seconds = totalSeconds % 60
if (minutes > 0) return `${minutes}m ${String(seconds).padStart(2, "0")}s`
return `${seconds}s`
}
function playCelebrationSound() {
const audio = new Audio("Celebration.mp3")
audio.volume = 0.45
@@ -613,6 +812,40 @@ function saveGameResult(result) {
}
saveStats(stats)
completeHourlyRound(result)
}
function completeHourlyRound(result) {
if (!hourlyRound || hourlyRound.completedAt) return
hourlyRound = {
...hourlyRound,
completedAt: Date.now(),
won: result.won,
guessCount: result.guesses
}
if (hourlyRound.backend === "local") {
saveLocalHourlyRound(hourlyRound)
return
}
completeRemoteHourlyRound(result).catch(error => {
console.warn("Failed to complete Supabase hourly round:", error)
})
}
async function completeRemoteHourlyRound(result) {
const client = getSupabaseClient()
if (!client || !hourlyRound?.id) return
const { error } = await client.rpc("complete_hourly_round", {
round_id: hourlyRound.id,
did_win: result.won,
guess_total: result.guesses
})
if (error) throw error
}
function renderStats() {
@@ -670,8 +903,15 @@ function buildShareText() {
const status = lastResult
? `${lastResult.won ? lastResult.guesses : "X"}/${MAX_GUESSES}`
: `${currentGuessIndex}/${MAX_GUESSES}`
const roundLabel = getShareRoundLabel()
return `Fancy Wordle ${status}\n${getResultGrid()}`
return `Fancy Wordle ${roundLabel} ${status}\n${getResultGrid()}`
}
function getShareRoundLabel() {
if (!Number.isFinite(hourlyRound?.hourStart)) return "Hourly"
return new Date(hourlyRound.hourStart).toISOString().slice(0, 13).replace("T", " ") + ":00 UTC"
}
function getResultGrid() {