Update Version (Beta): login + leaderboard
This commit is contained in:
+642
-164
@@ -5,138 +5,32 @@ create table if not exists public.wordle_words (
|
||||
word text not null unique check (word ~ '^[a-z]{5}$')
|
||||
);
|
||||
|
||||
insert into public.wordle_words (position, word) values
|
||||
(1, 'cigar'),
|
||||
(2, 'rebut'),
|
||||
(3, 'sissy'),
|
||||
(4, 'humph'),
|
||||
(5, 'awake'),
|
||||
(6, 'blush'),
|
||||
(7, 'focal'),
|
||||
(8, 'evade'),
|
||||
(9, 'naval'),
|
||||
(10, 'serve'),
|
||||
(11, 'heath'),
|
||||
(12, 'dwarf'),
|
||||
(13, 'model'),
|
||||
(14, 'karma'),
|
||||
(15, 'stink'),
|
||||
(16, 'grade'),
|
||||
(17, 'quiet'),
|
||||
(18, 'bench'),
|
||||
(19, 'abate'),
|
||||
(20, 'feign'),
|
||||
(21, 'major'),
|
||||
(22, 'death'),
|
||||
(23, 'fresh'),
|
||||
(24, 'crust'),
|
||||
(25, 'stool'),
|
||||
(26, 'colon'),
|
||||
(27, 'abase'),
|
||||
(28, 'marry'),
|
||||
(29, 'react'),
|
||||
(30, 'batty'),
|
||||
(31, 'pride'),
|
||||
(32, 'floss'),
|
||||
(33, 'helix'),
|
||||
(34, 'croak'),
|
||||
(35, 'staff'),
|
||||
(36, 'paper'),
|
||||
(37, 'unfed'),
|
||||
(38, 'whelp'),
|
||||
(39, 'trawl'),
|
||||
(40, 'outdo'),
|
||||
(41, 'adobe'),
|
||||
(42, 'crazy'),
|
||||
(43, 'sower'),
|
||||
(44, 'repay'),
|
||||
(45, 'digit'),
|
||||
(46, 'crate'),
|
||||
(47, 'cluck'),
|
||||
(48, 'spike'),
|
||||
(49, 'mimic'),
|
||||
(50, 'pound'),
|
||||
(51, 'maxim'),
|
||||
(52, 'linen'),
|
||||
(53, 'unmet'),
|
||||
(54, 'flesh'),
|
||||
(55, 'booby'),
|
||||
(56, 'forth'),
|
||||
(57, 'first'),
|
||||
(58, 'stand'),
|
||||
(59, 'belly'),
|
||||
(60, 'ivory'),
|
||||
(61, 'seedy'),
|
||||
(62, 'print'),
|
||||
(63, 'yearn'),
|
||||
(64, 'drain'),
|
||||
(65, 'bribe'),
|
||||
(66, 'stout'),
|
||||
(67, 'panel'),
|
||||
(68, 'crass'),
|
||||
(69, 'flume'),
|
||||
(70, 'offal'),
|
||||
(71, 'agree'),
|
||||
(72, 'error'),
|
||||
(73, 'swirl'),
|
||||
(74, 'argue'),
|
||||
(75, 'bleed'),
|
||||
(76, 'delta'),
|
||||
(77, 'flick'),
|
||||
(78, 'totem'),
|
||||
(79, 'wooer'),
|
||||
(80, 'front'),
|
||||
(81, 'shrub'),
|
||||
(82, 'parry'),
|
||||
(83, 'biome'),
|
||||
(84, 'lapel'),
|
||||
(85, 'start'),
|
||||
(86, 'greet'),
|
||||
(87, 'goner'),
|
||||
(88, 'golem'),
|
||||
(89, 'lusty'),
|
||||
(90, 'loopy'),
|
||||
(91, 'round'),
|
||||
(92, 'audit'),
|
||||
(93, 'lying'),
|
||||
(94, 'gamma'),
|
||||
(95, 'labor'),
|
||||
(96, 'islet'),
|
||||
(97, 'civic'),
|
||||
(98, 'forge'),
|
||||
(99, 'corny'),
|
||||
(100, 'moult'),
|
||||
(101, 'basic'),
|
||||
(102, 'salad'),
|
||||
(103, 'agate'),
|
||||
(104, 'spicy'),
|
||||
(105, 'spray'),
|
||||
(106, 'essay'),
|
||||
(107, 'fjord'),
|
||||
(108, 'spend'),
|
||||
(109, 'kebab'),
|
||||
(110, 'guild'),
|
||||
(111, 'aback'),
|
||||
(112, 'motor'),
|
||||
(113, 'alone'),
|
||||
(114, 'hatch'),
|
||||
(115, 'hyper'),
|
||||
(116, 'thumb'),
|
||||
(117, 'dowry'),
|
||||
(118, 'ought'),
|
||||
(119, 'belch'),
|
||||
(120, 'dutch')
|
||||
on conflict (position) do nothing;
|
||||
create table if not exists public.wordle_dictionary (
|
||||
word text primary key check (word ~ '^[a-z]{5}$')
|
||||
);
|
||||
|
||||
alter table public.wordle_words enable row level security;
|
||||
create table if not exists public.profiles (
|
||||
id uuid primary key references auth.users(id) on delete cascade,
|
||||
username text,
|
||||
display_name text,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
alter table public.profiles
|
||||
add column if not exists username text;
|
||||
|
||||
create unique index if not exists profiles_username_key
|
||||
on public.profiles (lower(username))
|
||||
where username is not null;
|
||||
|
||||
create table if not exists public.wordle_rounds (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
user_id uuid not null references auth.users(id) on delete cascade,
|
||||
word text not null check (word ~ '^[a-z]{5}$'),
|
||||
hour_start timestamptz,
|
||||
hour_start timestamptz not null,
|
||||
started_at timestamptz not null default now(),
|
||||
next_playable_at timestamptz not null default (now() + interval '1 hour'),
|
||||
next_playable_at timestamptz not null,
|
||||
completed_at timestamptz,
|
||||
won boolean,
|
||||
guess_count integer check (guess_count between 1 and 6),
|
||||
@@ -144,14 +38,31 @@ create table if not exists public.wordle_rounds (
|
||||
);
|
||||
|
||||
alter table public.wordle_rounds
|
||||
add column if not exists hour_start timestamptz;
|
||||
add column if not exists hour_start timestamptz,
|
||||
add column if not exists next_playable_at timestamptz;
|
||||
|
||||
update public.wordle_rounds
|
||||
set hour_start = date_trunc('hour', started_at)
|
||||
where hour_start is null;
|
||||
|
||||
update public.wordle_rounds
|
||||
set next_playable_at = hour_start + interval '1 hour'
|
||||
where next_playable_at is null;
|
||||
|
||||
alter table public.wordle_rounds
|
||||
alter column hour_start set not null;
|
||||
alter column hour_start set not null,
|
||||
alter column next_playable_at set not null;
|
||||
|
||||
create table if not exists public.wordle_guesses (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
round_id uuid not null references public.wordle_rounds(id) on delete cascade,
|
||||
user_id uuid not null references auth.users(id) on delete cascade,
|
||||
guess text not null check (guess ~ '^[a-z]{5}$'),
|
||||
row_index integer not null check (row_index between 1 and 6),
|
||||
states jsonb not null,
|
||||
created_at timestamptz not null default now(),
|
||||
unique (round_id, row_index)
|
||||
);
|
||||
|
||||
create index if not exists wordle_rounds_user_started_idx
|
||||
on public.wordle_rounds (user_id, started_at desc);
|
||||
@@ -159,7 +70,27 @@ create index if not exists wordle_rounds_user_started_idx
|
||||
create unique index if not exists wordle_rounds_user_hour_idx
|
||||
on public.wordle_rounds (user_id, hour_start);
|
||||
|
||||
create index if not exists wordle_guesses_round_row_idx
|
||||
on public.wordle_guesses (round_id, row_index);
|
||||
|
||||
alter table public.wordle_words enable row level security;
|
||||
alter table public.wordle_dictionary enable row level security;
|
||||
alter table public.profiles enable row level security;
|
||||
alter table public.wordle_rounds enable row level security;
|
||||
alter table public.wordle_guesses enable row level security;
|
||||
|
||||
drop policy if exists "Users can read their own profile" on public.profiles;
|
||||
create policy "Users can read their own profile"
|
||||
on public.profiles
|
||||
for select
|
||||
using (auth.uid() = id);
|
||||
|
||||
drop policy if exists "Users can update their own profile" on public.profiles;
|
||||
create policy "Users can update their own profile"
|
||||
on public.profiles
|
||||
for update
|
||||
using (auth.uid() = id)
|
||||
with check (auth.uid() = id);
|
||||
|
||||
drop policy if exists "Users can read their own rounds" on public.wordle_rounds;
|
||||
create policy "Users can read their own rounds"
|
||||
@@ -167,12 +98,113 @@ create policy "Users can read their own rounds"
|
||||
for select
|
||||
using (auth.uid() = user_id);
|
||||
|
||||
drop policy if exists "Users can read their own guesses" on public.wordle_guesses;
|
||||
create policy "Users can read their own guesses"
|
||||
on public.wordle_guesses
|
||||
for select
|
||||
using (auth.uid() = user_id);
|
||||
|
||||
create or replace function public.handle_new_user()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path = public
|
||||
as $$
|
||||
begin
|
||||
insert into public.profiles (id, username, display_name)
|
||||
values (
|
||||
new.id,
|
||||
nullif(lower(regexp_replace(coalesce(new.raw_user_meta_data->>'username', split_part(new.email, '@', 1)), '[^a-z0-9_]', '', 'g')), ''),
|
||||
coalesce(new.raw_user_meta_data->>'display_name', new.raw_user_meta_data->>'username', split_part(new.email, '@', 1))
|
||||
)
|
||||
on conflict (id) do nothing;
|
||||
|
||||
return new;
|
||||
end;
|
||||
$$;
|
||||
|
||||
drop trigger if exists on_auth_user_created on auth.users;
|
||||
create trigger on_auth_user_created
|
||||
after insert on auth.users
|
||||
for each row execute function public.handle_new_user();
|
||||
|
||||
create or replace function public.score_guess(guess text, answer text)
|
||||
returns jsonb
|
||||
language plpgsql
|
||||
immutable
|
||||
as $$
|
||||
declare
|
||||
states text[] := array['wrong', 'wrong', 'wrong', 'wrong', 'wrong'];
|
||||
remaining_letters text := '';
|
||||
guess_letter text;
|
||||
answer_letter text;
|
||||
match_index integer;
|
||||
index integer;
|
||||
begin
|
||||
for index in 1..5 loop
|
||||
guess_letter := substr(guess, index, 1);
|
||||
answer_letter := substr(answer, index, 1);
|
||||
|
||||
if guess_letter = answer_letter then
|
||||
states[index] := 'correct';
|
||||
else
|
||||
remaining_letters := remaining_letters || answer_letter;
|
||||
end if;
|
||||
end loop;
|
||||
|
||||
for index in 1..5 loop
|
||||
if states[index] = 'correct' then
|
||||
continue;
|
||||
end if;
|
||||
|
||||
guess_letter := substr(guess, index, 1);
|
||||
match_index := strpos(remaining_letters, guess_letter);
|
||||
|
||||
if match_index > 0 then
|
||||
states[index] := 'wrong-position';
|
||||
remaining_letters := overlay(remaining_letters placing '' from match_index for 1);
|
||||
end if;
|
||||
end loop;
|
||||
|
||||
return to_jsonb(states);
|
||||
end;
|
||||
$$;
|
||||
|
||||
create or replace function public.get_hourly_word(hour_start timestamptz)
|
||||
returns text
|
||||
language plpgsql
|
||||
stable
|
||||
security definer
|
||||
set search_path = public
|
||||
as $$
|
||||
declare
|
||||
word_count integer;
|
||||
word_offset integer;
|
||||
hourly_word text;
|
||||
begin
|
||||
select count(*) into word_count from public.wordle_words;
|
||||
|
||||
if word_count = 0 then
|
||||
raise exception 'No hourly words are configured';
|
||||
end if;
|
||||
|
||||
word_offset := (floor(extract(epoch from hour_start) / 3600)::bigint % word_count)::integer;
|
||||
|
||||
select public.wordle_words.word
|
||||
into hourly_word
|
||||
from public.wordle_words
|
||||
order by position
|
||||
limit 1 offset word_offset;
|
||||
|
||||
return hourly_word;
|
||||
end;
|
||||
$$;
|
||||
|
||||
drop function if exists public.start_hourly_round(text);
|
||||
|
||||
create or replace function public.start_hourly_round()
|
||||
returns table (
|
||||
round_id uuid,
|
||||
word text,
|
||||
hour_start timestamptz,
|
||||
started_at timestamptz,
|
||||
next_playable_at timestamptz,
|
||||
@@ -180,7 +212,9 @@ returns table (
|
||||
won boolean,
|
||||
guess_count integer,
|
||||
server_now timestamptz,
|
||||
is_existing boolean
|
||||
is_existing boolean,
|
||||
revealed_word text,
|
||||
guesses jsonb
|
||||
)
|
||||
language plpgsql
|
||||
security definer
|
||||
@@ -190,27 +224,12 @@ declare
|
||||
existing_round public.wordle_rounds%rowtype;
|
||||
new_round public.wordle_rounds%rowtype;
|
||||
current_hour timestamptz := date_trunc('hour', now());
|
||||
word_count integer;
|
||||
word_offset integer;
|
||||
hourly_word text;
|
||||
hourly_word text := public.get_hourly_word(date_trunc('hour', now()));
|
||||
begin
|
||||
if auth.uid() is null then
|
||||
raise exception 'Authentication required';
|
||||
end if;
|
||||
|
||||
select count(*) into word_count from public.wordle_words;
|
||||
if word_count = 0 then
|
||||
raise exception 'No hourly words are configured';
|
||||
end if;
|
||||
|
||||
word_offset := (floor(extract(epoch from current_hour) / 3600)::bigint % word_count)::integer;
|
||||
|
||||
select public.wordle_words.word
|
||||
into hourly_word
|
||||
from public.wordle_words
|
||||
order by position
|
||||
limit 1 offset word_offset;
|
||||
|
||||
select *
|
||||
into existing_round
|
||||
from public.wordle_rounds
|
||||
@@ -222,7 +241,6 @@ begin
|
||||
if found then
|
||||
return query select
|
||||
existing_round.id,
|
||||
existing_round.word,
|
||||
existing_round.hour_start,
|
||||
existing_round.started_at,
|
||||
existing_round.next_playable_at,
|
||||
@@ -230,7 +248,17 @@ begin
|
||||
existing_round.won,
|
||||
existing_round.guess_count,
|
||||
now(),
|
||||
true;
|
||||
true,
|
||||
case when existing_round.completed_at is null then null else existing_round.word end,
|
||||
coalesce((
|
||||
select jsonb_agg(jsonb_build_object(
|
||||
'guess', wordle_guesses.guess,
|
||||
'rowIndex', wordle_guesses.row_index,
|
||||
'states', wordle_guesses.states
|
||||
) order by wordle_guesses.row_index)
|
||||
from public.wordle_guesses
|
||||
where wordle_guesses.round_id = existing_round.id
|
||||
), '[]'::jsonb);
|
||||
return;
|
||||
end if;
|
||||
|
||||
@@ -240,7 +268,6 @@ begin
|
||||
|
||||
return query select
|
||||
new_round.id,
|
||||
new_round.word,
|
||||
new_round.hour_start,
|
||||
new_round.started_at,
|
||||
new_round.next_playable_at,
|
||||
@@ -248,10 +275,461 @@ begin
|
||||
new_round.won,
|
||||
new_round.guess_count,
|
||||
now(),
|
||||
false;
|
||||
false,
|
||||
null::text,
|
||||
'[]'::jsonb;
|
||||
end;
|
||||
$$;
|
||||
|
||||
drop function if exists public.submit_guess(uuid, text);
|
||||
|
||||
create or replace function public.submit_guess(target_round_id uuid, submitted_guess text)
|
||||
returns table (
|
||||
guess text,
|
||||
row_index integer,
|
||||
states jsonb,
|
||||
completed boolean,
|
||||
won boolean,
|
||||
guess_count integer,
|
||||
revealed_word text,
|
||||
next_playable_at timestamptz,
|
||||
server_now timestamptz
|
||||
)
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path = public
|
||||
as $$
|
||||
declare
|
||||
active_round public.wordle_rounds%rowtype;
|
||||
normalized_guess text := lower(trim(submitted_guess));
|
||||
next_row integer;
|
||||
scored_states jsonb;
|
||||
did_win boolean;
|
||||
did_complete boolean;
|
||||
begin
|
||||
if auth.uid() is null then
|
||||
raise exception 'Authentication required';
|
||||
end if;
|
||||
|
||||
if normalized_guess !~ '^[a-z]{5}$' then
|
||||
raise exception 'Guess must be five letters';
|
||||
end if;
|
||||
|
||||
if not exists (select 1 from public.wordle_dictionary where word = normalized_guess) then
|
||||
raise exception 'Not in word list';
|
||||
end if;
|
||||
|
||||
select *
|
||||
into active_round
|
||||
from public.wordle_rounds rounds
|
||||
where rounds.id = target_round_id
|
||||
and rounds.user_id = auth.uid()
|
||||
limit 1;
|
||||
|
||||
if not found then
|
||||
raise exception 'Round not found';
|
||||
end if;
|
||||
|
||||
if active_round.completed_at is not null then
|
||||
raise exception 'Round already complete';
|
||||
end if;
|
||||
|
||||
select (count(*) + 1)::integer
|
||||
into next_row
|
||||
from public.wordle_guesses
|
||||
where wordle_guesses.round_id = active_round.id;
|
||||
|
||||
if next_row > 6 then
|
||||
raise exception 'No guesses remaining';
|
||||
end if;
|
||||
|
||||
scored_states := public.score_guess(normalized_guess, active_round.word);
|
||||
did_win := normalized_guess = active_round.word;
|
||||
did_complete := did_win or next_row = 6;
|
||||
|
||||
insert into public.wordle_guesses (round_id, user_id, guess, row_index, states)
|
||||
values (active_round.id, auth.uid(), normalized_guess, next_row, scored_states);
|
||||
|
||||
if did_complete then
|
||||
update public.wordle_rounds
|
||||
set completed_at = now(),
|
||||
won = did_win,
|
||||
guess_count = next_row
|
||||
where id = active_round.id;
|
||||
end if;
|
||||
|
||||
return query select
|
||||
normalized_guess,
|
||||
next_row,
|
||||
scored_states,
|
||||
did_complete,
|
||||
did_win,
|
||||
case when did_complete then next_row else null end,
|
||||
case when did_complete then active_round.word else null end,
|
||||
active_round.next_playable_at,
|
||||
now();
|
||||
end;
|
||||
$$;
|
||||
|
||||
create or replace function public.get_user_stats()
|
||||
returns table (
|
||||
played integer,
|
||||
wins integer,
|
||||
current_streak integer,
|
||||
max_streak integer,
|
||||
distribution integer[]
|
||||
)
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path = public
|
||||
as $$
|
||||
declare
|
||||
round_record record;
|
||||
running_streak integer := 0;
|
||||
previous_hour timestamptz;
|
||||
begin
|
||||
if auth.uid() is null then
|
||||
raise exception 'Authentication required';
|
||||
end if;
|
||||
|
||||
played := 0;
|
||||
wins := 0;
|
||||
current_streak := 0;
|
||||
max_streak := 0;
|
||||
distribution := array[0, 0, 0, 0, 0, 0];
|
||||
|
||||
for round_record in
|
||||
select won, guess_count, hour_start
|
||||
from public.wordle_rounds
|
||||
where user_id = auth.uid()
|
||||
and completed_at is not null
|
||||
order by hour_start asc
|
||||
loop
|
||||
played := played + 1;
|
||||
|
||||
if round_record.won then
|
||||
wins := wins + 1;
|
||||
running_streak := case
|
||||
when running_streak > 0 and previous_hour is not null and round_record.hour_start = previous_hour + interval '1 hour'
|
||||
then running_streak + 1
|
||||
else 1
|
||||
end;
|
||||
max_streak := greatest(max_streak, running_streak);
|
||||
|
||||
if round_record.guess_count between 1 and 6 then
|
||||
distribution[round_record.guess_count] := distribution[round_record.guess_count] + 1;
|
||||
end if;
|
||||
else
|
||||
running_streak := 0;
|
||||
end if;
|
||||
|
||||
previous_hour := round_record.hour_start;
|
||||
end loop;
|
||||
|
||||
previous_hour := null;
|
||||
|
||||
for round_record in
|
||||
select won, hour_start
|
||||
from public.wordle_rounds
|
||||
where user_id = auth.uid()
|
||||
and completed_at is not null
|
||||
order by hour_start desc
|
||||
loop
|
||||
if not round_record.won then
|
||||
exit;
|
||||
end if;
|
||||
|
||||
if previous_hour is not null and round_record.hour_start <> previous_hour - interval '1 hour' then
|
||||
exit;
|
||||
end if;
|
||||
|
||||
current_streak := current_streak + 1;
|
||||
previous_hour := round_record.hour_start;
|
||||
end loop;
|
||||
|
||||
return next;
|
||||
end;
|
||||
$$;
|
||||
|
||||
create or replace function public.is_username_available(candidate_username text)
|
||||
returns boolean
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path = public
|
||||
as $$
|
||||
declare
|
||||
normalized_username text := nullif(lower(regexp_replace(trim(candidate_username), '[^a-z0-9_]', '', 'g')), '');
|
||||
begin
|
||||
if normalized_username is null then
|
||||
return false;
|
||||
end if;
|
||||
|
||||
return not exists (
|
||||
select 1
|
||||
from public.profiles
|
||||
where lower(username) = normalized_username
|
||||
);
|
||||
end;
|
||||
$$;
|
||||
|
||||
create or replace function public.get_player_current_streak(target_user_id uuid)
|
||||
returns integer
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path = public
|
||||
as $$
|
||||
declare
|
||||
round_record record;
|
||||
streak integer := 0;
|
||||
previous_hour timestamptz;
|
||||
begin
|
||||
for round_record in
|
||||
select won, hour_start
|
||||
from public.wordle_rounds
|
||||
where user_id = target_user_id
|
||||
and completed_at is not null
|
||||
order by hour_start desc
|
||||
loop
|
||||
if not round_record.won then
|
||||
exit;
|
||||
end if;
|
||||
|
||||
if previous_hour is not null and round_record.hour_start <> previous_hour - interval '1 hour' then
|
||||
exit;
|
||||
end if;
|
||||
|
||||
streak := streak + 1;
|
||||
previous_hour := round_record.hour_start;
|
||||
end loop;
|
||||
|
||||
return streak;
|
||||
end;
|
||||
$$;
|
||||
|
||||
create or replace function public.get_hourly_leaderboard()
|
||||
returns table (
|
||||
username text,
|
||||
won boolean,
|
||||
guess_count integer,
|
||||
completed_at timestamptz,
|
||||
rank integer
|
||||
)
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path = public
|
||||
as $$
|
||||
begin
|
||||
return query
|
||||
select
|
||||
coalesce(profiles.username, profiles.display_name, 'player') as username,
|
||||
rounds.won,
|
||||
rounds.guess_count,
|
||||
rounds.completed_at,
|
||||
row_number() over (
|
||||
order by
|
||||
rounds.won desc,
|
||||
rounds.guess_count asc nulls last,
|
||||
rounds.completed_at asc
|
||||
)::integer as rank
|
||||
from public.wordle_rounds rounds
|
||||
left join public.profiles profiles on profiles.id = rounds.user_id
|
||||
where rounds.hour_start = date_trunc('hour', now())
|
||||
and rounds.completed_at is not null
|
||||
order by rank
|
||||
limit 25;
|
||||
end;
|
||||
$$;
|
||||
|
||||
create or replace function public.get_leaderboard(board_scope text default 'hour')
|
||||
returns table (
|
||||
username text,
|
||||
won boolean,
|
||||
guess_count integer,
|
||||
completed_at timestamptz,
|
||||
rank integer,
|
||||
current_streak integer,
|
||||
is_current_user boolean,
|
||||
wins integer,
|
||||
played integer,
|
||||
average_guesses numeric
|
||||
)
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path = public
|
||||
as $$
|
||||
declare
|
||||
normalized_scope text := lower(coalesce(board_scope, 'hour'));
|
||||
begin
|
||||
if normalized_scope = 'today' then
|
||||
return query
|
||||
with aggregate_rows as (
|
||||
select
|
||||
rounds.user_id,
|
||||
coalesce(profiles.username, profiles.display_name, 'player') as username,
|
||||
count(*)::integer as played,
|
||||
count(*) filter (where rounds.won)::integer as wins,
|
||||
round(avg(rounds.guess_count) filter (where rounds.won), 2) as average_guesses,
|
||||
min(rounds.completed_at) filter (where rounds.won) as first_win_at,
|
||||
max(rounds.completed_at) as latest_completed_at
|
||||
from public.wordle_rounds rounds
|
||||
left join public.profiles profiles on profiles.id = rounds.user_id
|
||||
where rounds.completed_at is not null
|
||||
and rounds.completed_at >= date_trunc('day', now())
|
||||
group by rounds.user_id, profiles.username, profiles.display_name
|
||||
), ranked as (
|
||||
select
|
||||
aggregate_rows.*,
|
||||
row_number() over (
|
||||
order by aggregate_rows.wins desc, aggregate_rows.average_guesses asc nulls last, aggregate_rows.first_win_at asc nulls last
|
||||
)::integer as row_rank
|
||||
from aggregate_rows
|
||||
)
|
||||
select
|
||||
ranked.username,
|
||||
ranked.wins > 0,
|
||||
null::integer,
|
||||
ranked.latest_completed_at,
|
||||
ranked.row_rank,
|
||||
public.get_player_current_streak(ranked.user_id),
|
||||
auth.uid() = ranked.user_id,
|
||||
ranked.wins,
|
||||
ranked.played,
|
||||
ranked.average_guesses
|
||||
from ranked
|
||||
where ranked.row_rank <= 25 or auth.uid() = ranked.user_id
|
||||
order by ranked.row_rank;
|
||||
return;
|
||||
end if;
|
||||
|
||||
if normalized_scope = 'all' then
|
||||
return query
|
||||
with aggregate_rows as (
|
||||
select
|
||||
rounds.user_id,
|
||||
coalesce(profiles.username, profiles.display_name, 'player') as username,
|
||||
count(*)::integer as played,
|
||||
count(*) filter (where rounds.won)::integer as wins,
|
||||
round(avg(rounds.guess_count) filter (where rounds.won), 2) as average_guesses,
|
||||
min(rounds.completed_at) filter (where rounds.won) as first_win_at,
|
||||
max(rounds.completed_at) as latest_completed_at
|
||||
from public.wordle_rounds rounds
|
||||
left join public.profiles profiles on profiles.id = rounds.user_id
|
||||
where rounds.completed_at is not null
|
||||
group by rounds.user_id, profiles.username, profiles.display_name
|
||||
), ranked as (
|
||||
select
|
||||
aggregate_rows.*,
|
||||
row_number() over (
|
||||
order by aggregate_rows.wins desc, public.get_player_current_streak(aggregate_rows.user_id) desc, aggregate_rows.average_guesses asc nulls last, aggregate_rows.first_win_at asc nulls last
|
||||
)::integer as row_rank
|
||||
from aggregate_rows
|
||||
)
|
||||
select
|
||||
ranked.username,
|
||||
ranked.wins > 0,
|
||||
null::integer,
|
||||
ranked.latest_completed_at,
|
||||
ranked.row_rank,
|
||||
public.get_player_current_streak(ranked.user_id),
|
||||
auth.uid() = ranked.user_id,
|
||||
ranked.wins,
|
||||
ranked.played,
|
||||
ranked.average_guesses
|
||||
from ranked
|
||||
where ranked.row_rank <= 25 or auth.uid() = ranked.user_id
|
||||
order by ranked.row_rank;
|
||||
return;
|
||||
end if;
|
||||
|
||||
return query
|
||||
with ranked as (
|
||||
select
|
||||
rounds.user_id,
|
||||
coalesce(profiles.username, profiles.display_name, 'player') as username,
|
||||
rounds.won,
|
||||
rounds.guess_count,
|
||||
rounds.completed_at,
|
||||
row_number() over (
|
||||
order by rounds.won desc, rounds.guess_count asc nulls last, rounds.completed_at asc
|
||||
)::integer as row_rank
|
||||
from public.wordle_rounds rounds
|
||||
left join public.profiles profiles on profiles.id = rounds.user_id
|
||||
where rounds.hour_start = date_trunc('hour', now())
|
||||
and rounds.completed_at is not null
|
||||
)
|
||||
select
|
||||
ranked.username,
|
||||
ranked.won,
|
||||
ranked.guess_count,
|
||||
ranked.completed_at,
|
||||
ranked.row_rank,
|
||||
public.get_player_current_streak(ranked.user_id),
|
||||
auth.uid() = ranked.user_id,
|
||||
case when ranked.won then 1 else 0 end,
|
||||
1,
|
||||
ranked.guess_count::numeric
|
||||
from ranked
|
||||
where ranked.row_rank <= 25 or auth.uid() = ranked.user_id
|
||||
order by ranked.row_rank;
|
||||
end;
|
||||
$$;
|
||||
|
||||
create or replace function public.get_player_history(history_limit integer default 10)
|
||||
returns table (
|
||||
hour_start timestamptz,
|
||||
word text,
|
||||
won boolean,
|
||||
guess_count integer,
|
||||
completed_at timestamptz
|
||||
)
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path = public
|
||||
as $$
|
||||
begin
|
||||
if auth.uid() is null then
|
||||
raise exception 'Authentication required';
|
||||
end if;
|
||||
|
||||
return query
|
||||
select
|
||||
rounds.hour_start,
|
||||
rounds.word,
|
||||
rounds.won,
|
||||
rounds.guess_count,
|
||||
rounds.completed_at
|
||||
from public.wordle_rounds rounds
|
||||
where rounds.user_id = auth.uid()
|
||||
and rounds.completed_at is not null
|
||||
order by rounds.hour_start desc
|
||||
limit greatest(1, least(coalesce(history_limit, 10), 25));
|
||||
end;
|
||||
$$;
|
||||
|
||||
create or replace function public.get_hourly_summary()
|
||||
returns table (
|
||||
completed_count integer,
|
||||
win_rate integer,
|
||||
average_guesses numeric
|
||||
)
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path = public
|
||||
as $$
|
||||
begin
|
||||
return query
|
||||
select
|
||||
count(*)::integer,
|
||||
coalesce(round((count(*) filter (where won)::numeric / nullif(count(*), 0)) * 100), 0)::integer,
|
||||
round(avg(guess_count) filter (where won), 2)
|
||||
from public.wordle_rounds
|
||||
where hour_start = date_trunc('hour', now())
|
||||
and completed_at is not null;
|
||||
end;
|
||||
$$;
|
||||
|
||||
drop function if exists public.get_email_for_username(text);
|
||||
|
||||
create or replace function public.complete_hourly_round(
|
||||
round_id uuid,
|
||||
did_win boolean,
|
||||
@@ -263,25 +741,25 @@ security definer
|
||||
set search_path = public
|
||||
as $$
|
||||
begin
|
||||
if auth.uid() is null then
|
||||
raise exception 'Authentication required';
|
||||
end if;
|
||||
|
||||
if guess_total < 1 or guess_total > 6 then
|
||||
raise exception 'Guess total must be between 1 and 6';
|
||||
end if;
|
||||
|
||||
update public.wordle_rounds
|
||||
set completed_at = coalesce(completed_at, now()),
|
||||
won = did_win,
|
||||
guess_count = guess_total
|
||||
where id = round_id
|
||||
and user_id = auth.uid();
|
||||
raise exception 'complete_hourly_round is deprecated; use submit_guess';
|
||||
end;
|
||||
$$;
|
||||
|
||||
revoke all on public.wordle_rounds from anon, authenticated;
|
||||
revoke all on public.wordle_words from anon, authenticated;
|
||||
revoke all on public.wordle_dictionary from anon, authenticated;
|
||||
revoke all on public.profiles from anon, authenticated;
|
||||
revoke all on public.wordle_rounds from anon, authenticated;
|
||||
revoke all on public.wordle_guesses from anon, authenticated;
|
||||
|
||||
grant select, update on public.profiles to authenticated;
|
||||
grant select on public.wordle_rounds to authenticated;
|
||||
grant select on public.wordle_guesses to authenticated;
|
||||
grant execute on function public.is_username_available(text) to anon, authenticated;
|
||||
grant execute on function public.start_hourly_round() to authenticated;
|
||||
grant execute on function public.submit_guess(uuid, text) to authenticated;
|
||||
grant execute on function public.get_user_stats() to authenticated;
|
||||
grant execute on function public.get_hourly_leaderboard() to anon, authenticated;
|
||||
grant execute on function public.get_leaderboard(text) to anon, authenticated;
|
||||
grant execute on function public.get_player_history(integer) to authenticated;
|
||||
grant execute on function public.get_hourly_summary() to anon, authenticated;
|
||||
grant execute on function public.complete_hourly_round(uuid, boolean, integer) to authenticated;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user