Files
ChatApp/supabase/migrations/20260516000009_mini_games.sql

265 lines
8.0 KiB
PL/PgSQL
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- Phase 5B: per-conversation mini-games (Tic-Tac-Toe + Vier-Gewinnt /
-- Connect Four).
--
-- conversation_games: one row per game. The bubble in the chat is a
-- regular `messages` row whose plaintext payload is
-- `{v:1, type:'game', game_id:<id>, game_type:<'ttt'|'c4'>}`.
--
-- state JSON shape:
-- ttt: { kind: 'ttt', board: [null|0|1 × 9] } (row-major 3×3)
-- c4: { kind: 'c4', board: [null|0|1 × 42] } (row-major 6 rows × 7 cols)
--
-- players JSON: [user_id_a, user_id_b] — indices 0 and 1 map to board cells.
-- current_turn_user_id alternates; null once finished.
-- winner_user_id null on draw or unfinished.
create table if not exists public.conversation_games (
id uuid primary key default gen_random_uuid(),
conversation_id uuid not null references public.conversations(id) on delete cascade,
game_type text not null check (game_type in ('ttt', 'c4')),
state jsonb not null,
players jsonb not null,
current_turn_user_id uuid null references auth.users(id) on delete set null,
winner_user_id uuid null references auth.users(id) on delete set null,
created_at timestamptz not null default now(),
finished_at timestamptz null
);
create index if not exists conversation_games_conv_idx
on public.conversation_games(conversation_id, created_at desc);
alter table public.conversation_games enable row level security;
drop policy if exists conversation_games_select on public.conversation_games;
drop policy if exists conversation_games_insert on public.conversation_games;
create policy conversation_games_select
on public.conversation_games
for select
using (public.is_conversation_member(conversation_id));
create policy conversation_games_insert
on public.conversation_games
for insert
with check (
public.is_conversation_member(conversation_id)
and current_turn_user_id = auth.uid()
and (players->>0)::uuid = auth.uid()
);
alter table public.conversation_games replica identity full;
do $$
begin
if not exists (
select 1
from pg_publication_tables
where pubname = 'supabase_realtime'
and schemaname = 'public'
and tablename = 'conversation_games'
) then
execute 'alter publication supabase_realtime add table public.conversation_games';
end if;
end
$$;
-- ----------------------------------------------------------------------
-- Pure winner-check + board-full helpers.
-- ----------------------------------------------------------------------
create or replace function public.ttt_check_winner(board jsonb)
returns int
language plpgsql
immutable
as $$
declare
lines int[][] := array[
array[0,1,2], array[3,4,5], array[6,7,8],
array[0,3,6], array[1,4,7], array[2,5,8],
array[0,4,8], array[2,4,6]
];
ln int[];
a jsonb;
b jsonb;
c jsonb;
begin
foreach ln slice 1 in array lines loop
a := board->ln[1];
b := board->ln[2];
c := board->ln[3];
if jsonb_typeof(a) = 'number' and a = b and b = c then
return (a)::int;
end if;
end loop;
return null;
end;
$$;
create or replace function public.c4_check_winner(board jsonb)
returns int
language plpgsql
immutable
as $$
declare
rows constant int := 6;
cols constant int := 7;
r int;
c int;
i int;
d_r int;
d_c int;
directions int[][] := array[
array[0, 1],
array[1, 0],
array[1, 1],
array[1, -1]
];
dir int[];
base jsonb;
cell jsonb;
ok boolean;
begin
for r in 0..rows-1 loop
for c in 0..cols-1 loop
base := board->(r * cols + c);
if jsonb_typeof(base) <> 'number' then continue; end if;
foreach dir slice 1 in array directions loop
d_r := dir[1];
d_c := dir[2];
if r + d_r * 3 < 0 or r + d_r * 3 >= rows then continue; end if;
if c + d_c * 3 < 0 or c + d_c * 3 >= cols then continue; end if;
ok := true;
for i in 1..3 loop
cell := board->((r + d_r * i) * cols + (c + d_c * i));
if cell is null or cell <> base then ok := false; exit; end if;
end loop;
if ok then return (base)::int; end if;
end loop;
end loop;
end loop;
return null;
end;
$$;
create or replace function public.board_is_full(board jsonb)
returns boolean
language sql
immutable
as $$
select not exists (
select 1 from jsonb_array_elements(board) elt where jsonb_typeof(elt) is distinct from 'number'
);
$$;
-- ----------------------------------------------------------------------
-- The state-machine RPC.
-- ----------------------------------------------------------------------
create or replace function public.game_make_move(p_game_id uuid, p_move jsonb)
returns jsonb
language plpgsql
security definer
set search_path = public
as $$
declare
v_game record;
v_state jsonb;
v_board jsonb;
v_kind text;
v_player_idx int;
v_other_user uuid;
v_winner_idx int;
v_finished boolean;
v_cell int;
v_col int;
v_row int;
v_cols constant int := 7;
v_rows constant int := 6;
v_target_row int;
begin
if auth.uid() is null then
raise exception 'not_authenticated' using errcode = '28000';
end if;
select * into v_game from public.conversation_games where id = p_game_id for update;
if not found then
raise exception 'game_not_found' using errcode = 'P0002';
end if;
if v_game.finished_at is not null then
raise exception 'game_finished' using errcode = '42501';
end if;
if v_game.current_turn_user_id is null
or v_game.current_turn_user_id <> auth.uid() then
raise exception 'not_your_turn' using errcode = '42501';
end if;
if (v_game.players->>0)::uuid = auth.uid() then
v_player_idx := 0;
v_other_user := (v_game.players->>1)::uuid;
elsif (v_game.players->>1)::uuid = auth.uid() then
v_player_idx := 1;
v_other_user := (v_game.players->>0)::uuid;
else
raise exception 'not_a_player' using errcode = '42501';
end if;
v_state := v_game.state;
v_kind := v_state->>'kind';
v_board := v_state->'board';
if v_kind = 'ttt' then
if (p_move->'cell') is null or jsonb_typeof(p_move->'cell') <> 'number' then
raise exception 'bad_move' using errcode = '22023';
end if;
v_cell := (p_move->>'cell')::int;
if v_cell < 0 or v_cell > 8 then
raise exception 'bad_move' using errcode = '22023';
end if;
if jsonb_typeof(v_board->v_cell) = 'number' then
raise exception 'cell_taken' using errcode = '42501';
end if;
v_board := jsonb_set(v_board, array[v_cell::text], to_jsonb(v_player_idx));
v_winner_idx := public.ttt_check_winner(v_board);
elsif v_kind = 'c4' then
if (p_move->'column') is null or jsonb_typeof(p_move->'column') <> 'number' then
raise exception 'bad_move' using errcode = '22023';
end if;
v_col := (p_move->>'column')::int;
if v_col < 0 or v_col >= v_cols then
raise exception 'bad_move' using errcode = '22023';
end if;
v_target_row := -1;
for v_row in reverse v_rows - 1 .. 0 loop
if jsonb_typeof(v_board->(v_row * v_cols + v_col)) is distinct from 'number' then
v_target_row := v_row;
exit;
end if;
end loop;
if v_target_row < 0 then
raise exception 'column_full' using errcode = '42501';
end if;
v_board := jsonb_set(v_board, array[(v_target_row * v_cols + v_col)::text], to_jsonb(v_player_idx));
v_winner_idx := public.c4_check_winner(v_board);
else
raise exception 'unknown_game_kind' using errcode = '42501';
end if;
v_state := jsonb_set(v_state, '{board}', v_board);
v_finished := v_winner_idx is not null or public.board_is_full(v_board);
update public.conversation_games
set state = v_state,
current_turn_user_id = case when v_finished then null else v_other_user end,
winner_user_id = case when v_winner_idx is not null then (v_game.players->>v_winner_idx)::uuid else null end,
finished_at = case when v_finished then now() else null end
where id = p_game_id;
return v_state;
end;
$$;
revoke all on function public.game_make_move(uuid, jsonb) from public;
grant execute on function public.game_make_move(uuid, jsonb) to authenticated;