Spaces:
Sleeping
Sleeping
File size: 9,045 Bytes
a095c00 04fc815 a095c00 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 | -- Migration 009: 1v1 Mode
-- Adds 4 minimal tables for the 1-vs-1 competitive mode.
-- All tables reference the existing `users` table (auth.users) and reuse
-- the per-type challenges tables (encryption_challenges,
-- code_fixing_challenges, log_analysis_challenges).
--
-- Design notes:
-- * `onevone_rooms.code` is a 6-char base32 join code (unique index).
-- * `onevone_matches.state` is a small enum: waiting | countdown | playing |
-- overtime | finished. Timer ticks are derived from start_time + duration
-- (client + server compute the same value), but server is the authority.
-- * `onevone_submissions` is append-only. The first row with `is_correct=true`
-- is the winner; an index on (match_id, is_correct) makes that O(1).
-- * `winner_user_id` is set the moment a winning submission is accepted,
-- atomically with `state = 'finished'` via a single UPDATE (see backend).
-- * RLS is permissive (read/write for anon role) like the other pool tables;
-- the backend is the gatekeeper via the anon key + room token check.
BEGIN;
CREATE EXTENSION IF NOT EXISTS pgcrypto;
-- ----------------------------------------------------------------------------
-- 1v1 rooms — owner creates, picks side (red/blue), gets a code
-- ----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS public.onevone_rooms (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
code text NOT NULL UNIQUE, -- 6-char base32 (e.g. "K7P3QA")
owner_user_id uuid NOT NULL, -- FK -> auth.users.id
team_role text NOT NULL CHECK (team_role IN ('red','blue')),
status text NOT NULL DEFAULT 'open'
CHECK (status IN ('open','closed','abandoned')),
challenge_source text NOT NULL DEFAULT 'random' -- 'random' | 'manual:<id>'
CHECK (challenge_source LIKE 'random%' OR challenge_source LIKE 'manual:%'),
challenge_id text, -- populated once the match starts
created_at timestamptz NOT NULL DEFAULT now(),
closed_at timestamptz
);
CREATE INDEX IF NOT EXISTS idx_onevone_rooms_code ON public.onevone_rooms (code);
CREATE INDEX IF NOT EXISTS idx_onevone_rooms_owner ON public.onevone_rooms (owner_user_id);
CREATE INDEX IF NOT EXISTS idx_onevone_rooms_status ON public.onevone_rooms (status);
DROP POLICY IF EXISTS "Allow all access to onevone_rooms" ON public.onevone_rooms;
CREATE POLICY "Allow all access to onevone_rooms"
ON public.onevone_rooms FOR ALL USING (true) WITH CHECK (true);
ALTER TABLE public.onevone_rooms ENABLE ROW LEVEL SECURITY;
-- ----------------------------------------------------------------------------
-- 1v1 players — exactly 2 rows per match (owner + joiner)
-- ----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS public.onevone_players (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
room_id uuid NOT NULL REFERENCES public.onevone_rooms(id) ON DELETE CASCADE,
user_id uuid NOT NULL, -- FK -> auth.users.id
slot smallint NOT NULL CHECK (slot IN (1,2)), -- 1 = owner, 2 = joiner
display_name text NOT NULL,
is_ready boolean NOT NULL DEFAULT false,
joined_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (room_id, slot),
UNIQUE (room_id, user_id)
);
CREATE INDEX IF NOT EXISTS idx_onevone_players_room ON public.onevone_players (room_id);
CREATE INDEX IF NOT EXISTS idx_onevone_players_user ON public.onevone_players (user_id);
DROP POLICY IF EXISTS "Allow all access to onevone_players" ON public.onevone_players;
CREATE POLICY "Allow all access to onevone_players"
ON public.onevone_players FOR ALL USING (true) WITH CHECK (true);
ALTER TABLE public.onevone_players ENABLE ROW LEVEL SECURITY;
-- ----------------------------------------------------------------------------
-- 1v1 matches — exactly 1 per room; state machine
-- ----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS public.onevone_matches (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
room_id uuid NOT NULL UNIQUE REFERENCES public.onevone_rooms(id) ON DELETE CASCADE,
challenge_id text NOT NULL, -- reuses existing per-type challenge id
challenge_type text NOT NULL -- 'crypto' | 'web' | 'code-fixing' | 'log-analysis'
CHECK (challenge_type IN ('crypto','web','code-fixing','log-analysis')),
state text NOT NULL DEFAULT 'waiting'
CHECK (state IN ('waiting','countdown','playing','overtime','finished')),
-- server-authoritative timer:
started_at timestamptz,
ends_at timestamptz, -- main timer end
overtime_ends_at timestamptz, -- overtime end (NULL until entered)
main_duration_s integer NOT NULL DEFAULT 600, -- 10 min main round
overtime_duration_s integer NOT NULL DEFAULT 120, -- 2 min overtime
winner_user_id uuid, -- NULL while playing
win_reason text -- 'flag' | 'fix' | 'timeout' | 'overtime_draw'
CHECK (win_reason IS NULL OR win_reason IN ('flag','fix','timeout','overtime_draw','abandoned')),
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_onevone_matches_room ON public.onevone_matches (room_id);
CREATE INDEX IF NOT EXISTS idx_onevone_matches_state ON public.onevone_matches (state);
CREATE INDEX IF NOT EXISTS idx_onevone_matches_winner ON public.onevone_matches (winner_user_id);
DROP POLICY IF EXISTS "Allow all access to onevone_matches" ON public.onevone_matches;
CREATE POLICY "Allow all access to onevone_matches"
ON public.onevone_matches FOR ALL USING (true) WITH CHECK (true);
ALTER TABLE public.onevone_matches ENABLE ROW LEVEL SECURITY;
-- ----------------------------------------------------------------------------
-- 1v1 submissions — append-only audit log; first correct one wins
-- ----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS public.onevone_submissions (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
match_id uuid NOT NULL REFERENCES public.onevone_matches(id) ON DELETE CASCADE,
user_id uuid NOT NULL, -- FK -> auth.users.id
submission text NOT NULL, -- raw user input (flag text OR fixed code / ip / etc.)
is_correct boolean NOT NULL DEFAULT false,
is_final boolean NOT NULL DEFAULT false, -- true => accepted, match should end
received_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_onevone_sub_match ON public.onevone_submissions (match_id);
CREATE INDEX IF NOT EXISTS idx_onevone_sub_correct ON public.onevone_submissions (match_id, is_correct);
DROP POLICY IF EXISTS "Allow all access to onevone_submissions" ON public.onevone_submissions;
CREATE POLICY "Allow all access to onevone_submissions"
ON public.onevone_submissions FOR ALL USING (true) WITH CHECK (true);
ALTER TABLE public.onevone_submissions ENABLE ROW LEVEL SECURITY;
COMMIT;
-- ---------------------------------------------------------------------------
-- Atomic "claim win" RPC.
-- Only ONE concurrent call can succeed. The UPDATE in PostgreSQL acquires
-- a row-level lock the instant the WHERE clause matches, so two parallel
-- requests for the same match_id are serialized: the loser sees ROW_COUNT=0
-- and learns who actually won.
-- ---------------------------------------------------------------------------
CREATE OR REPLACE FUNCTION public.onevone_claim_win(
p_match_id uuid,
p_user_id uuid,
p_win_reason text
) RETURNS TABLE(won boolean, winner_id uuid, final_state text)
LANGUAGE plpgsql
AS $$
DECLARE
v_winner uuid;
v_state text;
BEGIN
UPDATE public.onevone_matches m
SET state = 'finished',
winner_user_id = p_user_id,
win_reason = p_win_reason
WHERE m.id = p_match_id
AND m.state IN ('playing', 'overtime')
RETURNING m.winner_user_id, m.state
INTO v_winner, v_state;
IF FOUND THEN
RETURN QUERY SELECT true, v_winner, v_state;
ELSE
SELECT m.winner_user_id, m.state
INTO v_winner, v_state
FROM public.onevone_matches m
WHERE m.id = p_match_id;
RETURN QUERY SELECT false, COALESCE(v_winner, NULL::uuid), COALESCE(v_state, 'unknown');
END IF;
END;
$$;
-- Grant execute to the anon role (same RLS posture as the rest of the API).
GRANT EXECUTE ON FUNCTION public.onevone_claim_win(uuid, uuid, text) TO anon, authenticated;
-- Verification:
-- SELECT table_name FROM information_schema.tables
-- WHERE table_schema='public' AND table_name LIKE 'onevone_%'
-- ORDER BY table_name;
-- Expected: onevone_matches, onevone_players, onevone_rooms, onevone_submissions
|