Add map-specific tournament creation link
Some checks failed
Deploy MineGeo to Dokploy / deploy (push) Failing after 3m31s

This commit is contained in:
2026-10-18 13:32:56 +05:00
parent a7085e4e80
commit da1a50a6ad
10 changed files with 74 additions and 9 deletions

View 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; $$;