Improve MineGeo UI and application flows
Some checks failed
Deploy MineGeo to Dokploy / deploy (push) Failing after 2m37s
Some checks failed
Deploy MineGeo to Dokploy / deploy (push) Failing after 2m37s
This commit is contained in:
145
supabase/migrations/20261018060642_extended_difficulty_rules.sql
Normal file
145
supabase/migrations/20261018060642_extended_difficulty_rules.sql
Normal file
@@ -0,0 +1,145 @@
|
||||
-- Preserve the original defaults and snapshot the expanded rules per game.
|
||||
alter table public.difficulty_presets
|
||||
add column pan boolean not null default true,
|
||||
add column compass boolean not null default true,
|
||||
add column coordinates boolean not null default true,
|
||||
add column map_zoom boolean not null default true,
|
||||
add column scoring text not null default 'normal' check (scoring in ('normal','strict','gentle'));
|
||||
|
||||
update public.game_sessions set difficulty_settings =
|
||||
'{"pan":true,"compass":true,"coordinates":true,"mapZoom":true,"scoring":"normal"}'::jsonb || difficulty_settings;
|
||||
update public.multiplayer_rooms set difficulty_settings =
|
||||
'{"pan":true,"compass":true,"coordinates":true,"mapZoom":true,"scoring":"normal"}'::jsonb || difficulty_settings;
|
||||
|
||||
create or replace function public.resolve_difficulty_preset(p_id text)
|
||||
returns jsonb language plpgsql stable security invoker set search_path = '' as $$
|
||||
declare preset jsonb;
|
||||
begin
|
||||
if p_id in ('easy','normal','hard') then
|
||||
return jsonb_build_object('id',p_id,'name',initcap(p_id),
|
||||
'seconds',case p_id when 'easy' then 180 when 'hard' then 30 else 90 end,
|
||||
'zoom',p_id <> 'hard','pan',true,'compass',true,'coordinates',true,'mapZoom',true,'scoring','normal');
|
||||
end if;
|
||||
select jsonb_build_object('id',p.id::text,'name',p.name,'seconds',p.seconds,'zoom',p.zoom,
|
||||
'pan',p.pan,'compass',p.compass,'coordinates',p.coordinates,'mapZoom',p.map_zoom,'scoring',p.scoring)
|
||||
into preset from public.difficulty_presets p where p.id::text=lower(p_id);
|
||||
if not found then raise exception 'Choose a saved difficulty'; end if;
|
||||
return preset;
|
||||
end; $$;
|
||||
|
||||
-- Shared server-side curve used by solo, multiplayer, and tournament scoring.
|
||||
create function public.score_difficulty_guess(p_distance double precision,p_diagonal double precision,p_scoring text default 'normal')
|
||||
returns integer language plpgsql immutable security invoker set search_path = '' as $$
|
||||
begin
|
||||
if p_scoring is null or p_scoring not in ('normal','strict','gentle') then raise exception 'Invalid scoring rules'; end if;
|
||||
if p_distance is null or p_diagonal is null or p_distance < 0 or p_diagonal <= 0
|
||||
or p_distance in ('NaN'::float8,'Infinity'::float8,'-Infinity'::float8)
|
||||
or p_diagonal in ('NaN'::float8,'Infinity'::float8,'-Infinity'::float8)
|
||||
then raise exception 'Invalid scoring distance'; end if;
|
||||
if p_distance <= 1 then return 5000; end if;
|
||||
return round(5000 * exp(-(case p_scoring when 'strict' then 8 when 'gentle' then 3 else 5 end) * p_distance / p_diagonal))::integer;
|
||||
end; $$;
|
||||
revoke all on function public.score_difficulty_guess(double precision,double precision,text) from public,anon,authenticated;
|
||||
grant execute on function public.score_difficulty_guess(double precision,double precision,text) to service_role;
|
||||
|
||||
create or replace function public.submit_guess(p_session uuid,p_round integer,p_access_hash text,p_x double precision default null,p_z double precision default null) returns jsonb language plpgsql security invoker set search_path='' as $$
|
||||
declare s public.game_sessions; r public.game_rounds; manifest jsonb; b jsonb; d double precision; diagonal double precision; points integer:=0; timed_out boolean; answer jsonb;
|
||||
begin
|
||||
select * into s from public.game_sessions where id=p_session and access_hash=p_access_hash for update;
|
||||
if not found then raise exception 'Session not found'; end if;
|
||||
select * into r from public.game_rounds where session_id=p_session and round_number=p_round for update;
|
||||
if r.result is not null then return r.result; end if;
|
||||
if s.current_round<>p_round or r.deadline is null then raise exception 'Round is not active'; end if;
|
||||
select revisions.manifest into manifest from public.revisions where id=s.revision_id; b:=manifest->'bounds';
|
||||
timed_out:=clock_timestamp()>=r.deadline;
|
||||
if not timed_out and (p_x is null or p_z is null) then raise exception 'Place a guess first'; end if;
|
||||
if not timed_out then
|
||||
if p_x='NaN'::float8 or p_z='NaN'::float8 or p_x<(b->>'minX')::float8 or p_x>(b->>'maxX')::float8 or p_z<(b->>'minZ')::float8 or p_z>(b->>'maxZ')::float8 then raise exception 'Guess outside map'; end if;
|
||||
d:=sqrt(power(p_x-(r.viewpoint->>'x')::float8,2)+power(p_z-(r.viewpoint->>'z')::float8,2));
|
||||
diagonal:=sqrt(power((b->>'maxX')::float8-(b->>'minX')::float8,2)+power((b->>'maxZ')::float8-(b->>'minZ')::float8,2));
|
||||
points:=public.score_difficulty_guess(d,diagonal,coalesce(s.difficulty_settings->>'scoring','normal'));
|
||||
end if;
|
||||
answer:=jsonb_build_object('score',points,'distance',d,'timedOut',timed_out,'actual',jsonb_build_object('x',r.viewpoint->'x','z',r.viewpoint->'z'),'guess',case when timed_out then null else jsonb_build_object('x',p_x,'z',p_z) end);
|
||||
update public.game_rounds set result=answer where session_id=p_session and round_number=p_round;
|
||||
update public.game_sessions set current_round=current_round+1 where id=p_session;
|
||||
return answer;
|
||||
end; $$;
|
||||
|
||||
create or replace function public.multiplayer_action(p_room uuid,p_access_hash text,p_action text,p_payload jsonb default '{}')
|
||||
returns jsonb language plpgsql security invoker set search_path='' as $$
|
||||
declare room public.multiplayer_rooms; player public.multiplayer_players; turn public.multiplayer_rounds;
|
||||
target uuid; n integer; px double precision; pz double precision; distance double precision;
|
||||
diagonal double precision; points integer; bounds jsonb; answer jsonb; active integer;
|
||||
begin
|
||||
select * into room from public.multiplayer_rooms where id=p_room for update;
|
||||
if not found then raise exception 'Room not found'; end if;
|
||||
select * into player from public.multiplayer_players where room_id=p_room and access_hash=p_access_hash;
|
||||
if p_action='join' then
|
||||
if room.status='closed' then raise exception 'This room is closed'; end if;
|
||||
if player.id is not null and not player.has_left then return public.multiplayer_snapshot(p_room,p_access_hash); end if;
|
||||
if room.status<>'lobby' then raise exception 'This match has already started'; end if;
|
||||
select count(*) into active from public.multiplayer_players where room_id=p_room and not has_left;
|
||||
if active>=room.player_limit then raise exception 'This room is full'; end if;
|
||||
if exists(select 1 from public.multiplayer_players where room_id=p_room and lower(display_name)=lower(btrim(p_payload->>'name'))) then
|
||||
raise exception 'This name is already taken in this room';
|
||||
end if;
|
||||
insert into public.multiplayer_players(room_id,access_hash,display_name) values(p_room,p_access_hash,btrim(p_payload->>'name'));
|
||||
return public.multiplayer_snapshot(p_room,p_access_hash);
|
||||
end if;
|
||||
if player.id is null or player.has_left then
|
||||
if p_action='view' then return public.multiplayer_snapshot(p_room,p_access_hash); end if;
|
||||
raise exception 'Room membership permission required';
|
||||
end if;
|
||||
perform public.multiplayer_tick(p_room);
|
||||
select * into room from public.multiplayer_rooms where id=p_room;
|
||||
if p_action in ('start','next','kick','close') and room.host_player_id<>player.id then raise exception 'Room owner permission required'; end if;
|
||||
if p_action='start' then
|
||||
if room.status<>'lobby' then return public.multiplayer_snapshot(p_room,p_access_hash); end if;
|
||||
if (select count(*) from public.multiplayer_players where room_id=p_room and not has_left)<2 then raise exception 'Invite at least one friend before starting'; end if;
|
||||
update public.multiplayer_rooms set status='loading',transition_at=clock_timestamp()+interval '30 seconds' where id=p_room;
|
||||
elsif p_action='ready' then
|
||||
n := (p_payload->>'round')::integer;
|
||||
if room.status='loading' and n=room.current_round then update public.multiplayer_players set ready_round=n where id=player.id; end if;
|
||||
elsif p_action='guess' then
|
||||
n := (p_payload->>'round')::integer;
|
||||
if exists(select 1 from public.multiplayer_guesses where room_id=p_room and player_id=player.id and round_number=n) then
|
||||
return public.multiplayer_snapshot(p_room,p_access_hash);
|
||||
end if;
|
||||
if room.status<>'playing' or n is distinct from room.current_round then raise exception 'Round is not active'; end if;
|
||||
select * into turn from public.multiplayer_rounds where room_id=p_room and round_number=n;
|
||||
px := (p_payload->>'x')::double precision; pz := (p_payload->>'z')::double precision;
|
||||
if px is null or pz is null then raise exception 'Place a guess first'; end if;
|
||||
select manifest->'bounds' into bounds from public.revisions where id=room.revision_id;
|
||||
if px='NaN'::float8 or pz='NaN'::float8 or px<(bounds->>'minX')::float8 or px>(bounds->>'maxX')::float8 or pz<(bounds->>'minZ')::float8 or pz>(bounds->>'maxZ')::float8 then raise exception 'Guess outside map'; end if;
|
||||
-- Check again after validation: a request arriving at the deadline scores zero.
|
||||
if turn.deadline<=clock_timestamp() then
|
||||
perform public.multiplayer_tick(p_room); return public.multiplayer_snapshot(p_room,p_access_hash);
|
||||
end if;
|
||||
distance := sqrt(power(px-(turn.viewpoint->>'x')::float8,2)+power(pz-(turn.viewpoint->>'z')::float8,2));
|
||||
diagonal := sqrt(power((bounds->>'maxX')::float8-(bounds->>'minX')::float8,2)+power((bounds->>'maxZ')::float8-(bounds->>'minZ')::float8,2));
|
||||
points := public.score_difficulty_guess(distance,diagonal,coalesce(room.difficulty_settings->>'scoring','normal'));
|
||||
answer := jsonb_build_object('score',points,'distance',distance,'timedOut',false,'guess',jsonb_build_object('x',px,'z',pz),'actual',jsonb_build_object('x',turn.viewpoint->'x','z',turn.viewpoint->'z'));
|
||||
insert into public.multiplayer_guesses(room_id,player_id,round_number,result) values(p_room,player.id,n,answer);
|
||||
elsif p_action='next' then
|
||||
n := (p_payload->>'round')::integer;
|
||||
if n<room.current_round then return public.multiplayer_snapshot(p_room,p_access_hash); end if;
|
||||
if room.status<>'results' or n is distinct from room.current_round then raise exception 'Wait for the round to finish'; end if;
|
||||
update public.multiplayer_rooms set current_round=current_round+1,status='loading',transition_at=clock_timestamp()+interval '30 seconds' where id=p_room;
|
||||
elsif p_action='kick' then
|
||||
if room.status<>'lobby' then raise exception 'Players can only be removed before the match starts'; end if;
|
||||
target := (p_payload->>'playerId')::uuid;
|
||||
if target=room.host_player_id then raise exception 'The owner cannot remove themselves'; end if;
|
||||
delete from public.multiplayer_players where room_id=p_room and id=target;
|
||||
elsif p_action='leave' then
|
||||
if room.status='lobby' then delete from public.multiplayer_players where id=player.id;
|
||||
else update public.multiplayer_players set has_left=true where id=player.id; end if;
|
||||
select id into target from public.multiplayer_players where room_id=p_room and not has_left order by joined_at,id limit 1;
|
||||
if target is null then update public.multiplayer_rooms set status='closed',transition_at=null where id=p_room;
|
||||
elsif room.host_player_id=player.id then update public.multiplayer_rooms set host_player_id=target where id=p_room; end if;
|
||||
elsif p_action='close' then
|
||||
update public.multiplayer_rooms set status='closed',transition_at=null where id=p_room;
|
||||
elsif p_action<>'view' then raise exception 'Invalid room action';
|
||||
end if;
|
||||
perform public.multiplayer_tick(p_room);
|
||||
return public.multiplayer_snapshot(p_room,p_access_hash);
|
||||
end; $$;
|
||||
Reference in New Issue
Block a user