Update Version (Beta): One word each new UTC hour
This commit is contained in:
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user