Spaces:
Running
Running
| -- ============================================================================= | |
| -- Devil's Dozen - Supabase Database Schema | |
| -- ============================================================================= | |
| -- Run this SQL in your Supabase SQL Editor to set up the database. | |
| -- Supabase Dashboard → SQL Editor → New Query → Paste & Run | |
| -- ============================================================================= | |
| -- Enable UUID extension (usually already enabled in Supabase) | |
| CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; | |
| -- ============================================================================= | |
| -- TABLES | |
| -- ============================================================================= | |
| -- Lobbies: Game rooms that contain players | |
| CREATE TABLE IF NOT EXISTS lobbies ( | |
| id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), | |
| -- Shareable 6-character join code (e.g., "ABC123") | |
| code VARCHAR(6) UNIQUE NOT NULL, | |
| -- Game mode: 'peasants_gamble' or 'alchemists_ascent' | |
| game_mode VARCHAR(20) NOT NULL CHECK (game_mode IN ('peasants_gamble', 'alchemists_ascent')), | |
| -- Target score to win the game | |
| win_condition INT NOT NULL CHECK (win_condition > 0), | |
| -- Index of current player's turn (0-based, references players by turn_order) | |
| current_turn_index INT DEFAULT 0 CHECK (current_turn_index >= 0), | |
| -- Lobby status: waiting (for players), active (in game), finished (game over) | |
| status VARCHAR(20) DEFAULT 'waiting' CHECK (status IN ('waiting', 'active', 'finished')), | |
| -- Winner player ID (set when game finishes) | |
| winner_id UUID, | |
| -- Timestamps | |
| created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL, | |
| updated_at TIMESTAMPTZ DEFAULT NOW() NOT NULL | |
| ); | |
| -- Players: Participants in a game lobby | |
| CREATE TABLE IF NOT EXISTS players ( | |
| id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), | |
| -- Reference to the lobby this player is in | |
| lobby_id UUID NOT NULL REFERENCES lobbies(id) ON DELETE CASCADE, | |
| -- Player display name (unique within lobby) | |
| username VARCHAR(30) NOT NULL, | |
| -- Total score accumulated across all banked turns | |
| total_score INT DEFAULT 0 CHECK (total_score >= 0), | |
| -- Turn order (0, 1, 2, or 3 for up to 4 players) | |
| turn_order INT NOT NULL CHECK (turn_order >= 0 AND turn_order <= 3), | |
| -- Connection status for handling disconnects | |
| is_connected BOOLEAN DEFAULT true, | |
| -- Timestamp | |
| created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL, | |
| -- Constraints | |
| UNIQUE(lobby_id, username), -- No duplicate names in same lobby | |
| UNIQUE(lobby_id, turn_order) -- No duplicate turn orders in same lobby | |
| ); | |
| -- Game State: Current turn state (transient, one per lobby) | |
| CREATE TABLE IF NOT EXISTS game_state ( | |
| -- One game state per lobby | |
| lobby_id UUID PRIMARY KEY REFERENCES lobbies(id) ON DELETE CASCADE, | |
| -- Current dice values as JSON array (e.g., [1, 4, 5, 2, 3, 6]) | |
| active_dice JSONB DEFAULT '[]'::jsonb, | |
| -- Indices of held dice as JSON array (e.g., [0, 2]) | |
| held_indices JSONB DEFAULT '[]'::jsonb, | |
| -- Points accumulated this turn (not yet banked) | |
| turn_score INT DEFAULT 0 CHECK (turn_score >= 0), | |
| -- Whether the current roll resulted in a bust | |
| is_bust BOOLEAN DEFAULT false, | |
| -- Number of rolls taken this turn | |
| roll_count INT DEFAULT 0 CHECK (roll_count >= 0), | |
| -- Current tier for Alchemist's Ascent (1=Red, 2=Green, 3=Blue) | |
| tier INT DEFAULT 1 CHECK (tier >= 1 AND tier <= 3), | |
| -- Previous dice values for Tier 2 reroll comparison | |
| previous_dice JSONB DEFAULT '[]'::jsonb, | |
| -- Timestamp for last update | |
| updated_at TIMESTAMPTZ DEFAULT NOW() NOT NULL | |
| ); | |
| -- ============================================================================= | |
| -- INDEXES | |
| -- ============================================================================= | |
| -- Fast lookup of players by lobby | |
| CREATE INDEX IF NOT EXISTS idx_players_lobby ON players(lobby_id); | |
| -- Fast lookup of lobbies by join code | |
| CREATE INDEX IF NOT EXISTS idx_lobbies_code ON lobbies(code); | |
| -- Filter lobbies by status (for listing active/waiting games) | |
| CREATE INDEX IF NOT EXISTS idx_lobbies_status ON lobbies(status); | |
| -- ============================================================================= | |
| -- TRIGGERS | |
| -- ============================================================================= | |
| -- Function to update the updated_at timestamp | |
| CREATE OR REPLACE FUNCTION update_updated_at_column() | |
| RETURNS TRIGGER AS $$ | |
| BEGIN | |
| NEW.updated_at = NOW(); | |
| RETURN NEW; | |
| END; | |
| $$ LANGUAGE plpgsql; | |
| -- Auto-update lobbies.updated_at | |
| CREATE TRIGGER update_lobbies_updated_at | |
| BEFORE UPDATE ON lobbies | |
| FOR EACH ROW | |
| EXECUTE FUNCTION update_updated_at_column(); | |
| -- Auto-update game_state.updated_at | |
| CREATE TRIGGER update_game_state_updated_at | |
| BEFORE UPDATE ON game_state | |
| FOR EACH ROW | |
| EXECUTE FUNCTION update_updated_at_column(); | |
| -- ============================================================================= | |
| -- ROW LEVEL SECURITY (RLS) | |
| -- ============================================================================= | |
| -- Enable RLS on all tables | |
| ALTER TABLE lobbies ENABLE ROW LEVEL SECURITY; | |
| ALTER TABLE players ENABLE ROW LEVEL SECURITY; | |
| ALTER TABLE game_state ENABLE ROW LEVEL SECURITY; | |
| -- Lobbies: Anyone can read, insert, and update | |
| -- (In production, you might want more restrictive policies) | |
| CREATE POLICY "Lobbies are publicly readable" | |
| ON lobbies FOR SELECT | |
| USING (true); | |
| CREATE POLICY "Anyone can create lobbies" | |
| ON lobbies FOR INSERT | |
| WITH CHECK (true); | |
| CREATE POLICY "Anyone can update lobbies" | |
| ON lobbies FOR UPDATE | |
| USING (true); | |
| CREATE POLICY "Anyone can delete lobbies" | |
| ON lobbies FOR DELETE | |
| USING (true); | |
| -- Players: Same permissive policies for MVP | |
| CREATE POLICY "Players are publicly readable" | |
| ON players FOR SELECT | |
| USING (true); | |
| CREATE POLICY "Anyone can create players" | |
| ON players FOR INSERT | |
| WITH CHECK (true); | |
| CREATE POLICY "Anyone can update players" | |
| ON players FOR UPDATE | |
| USING (true); | |
| CREATE POLICY "Anyone can delete players" | |
| ON players FOR DELETE | |
| USING (true); | |
| -- Game State: Same permissive policies | |
| CREATE POLICY "Game state is publicly readable" | |
| ON game_state FOR SELECT | |
| USING (true); | |
| CREATE POLICY "Anyone can create game state" | |
| ON game_state FOR INSERT | |
| WITH CHECK (true); | |
| CREATE POLICY "Anyone can update game state" | |
| ON game_state FOR UPDATE | |
| USING (true); | |
| CREATE POLICY "Anyone can delete game state" | |
| ON game_state FOR DELETE | |
| USING (true); | |
| -- ============================================================================= | |
| -- REALTIME CONFIGURATION | |
| -- ============================================================================= | |
| -- Enable realtime for all tables | |
| -- Go to Supabase Dashboard → Database → Replication and enable these tables | |
| -- Alternatively, use this command (requires superuser): | |
| -- ALTER PUBLICATION supabase_realtime ADD TABLE lobbies; | |
| -- ALTER PUBLICATION supabase_realtime ADD TABLE players; | |
| -- ALTER PUBLICATION supabase_realtime ADD TABLE game_state; | |
| -- ============================================================================= | |
| -- HELPER FUNCTIONS | |
| -- ============================================================================= | |
| -- Generate a unique lobby code | |
| CREATE OR REPLACE FUNCTION generate_lobby_code() | |
| RETURNS VARCHAR(6) AS $$ | |
| DECLARE | |
| code VARCHAR(6); | |
| chars VARCHAR := 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; -- Excluding O, I, 0, 1 | |
| i INT; | |
| BEGIN | |
| LOOP | |
| code := ''; | |
| FOR i IN 1..6 LOOP | |
| code := code || substr(chars, floor(random() * length(chars) + 1)::int, 1); | |
| END LOOP; | |
| -- Check if code already exists | |
| IF NOT EXISTS (SELECT 1 FROM lobbies WHERE lobbies.code = code) THEN | |
| RETURN code; | |
| END IF; | |
| END LOOP; | |
| END; | |
| $$ LANGUAGE plpgsql; | |
| -- Create a new lobby with auto-generated code | |
| CREATE OR REPLACE FUNCTION create_lobby( | |
| p_game_mode VARCHAR(20), | |
| p_win_condition INT | |
| ) | |
| RETURNS UUID AS $$ | |
| DECLARE | |
| new_lobby_id UUID; | |
| new_code VARCHAR(6); | |
| BEGIN | |
| new_code := generate_lobby_code(); | |
| INSERT INTO lobbies (code, game_mode, win_condition) | |
| VALUES (new_code, p_game_mode, p_win_condition) | |
| RETURNING id INTO new_lobby_id; | |
| -- Create initial game state for this lobby | |
| INSERT INTO game_state (lobby_id) | |
| VALUES (new_lobby_id); | |
| RETURN new_lobby_id; | |
| END; | |
| $$ LANGUAGE plpgsql; | |
| -- Add a player to a lobby | |
| CREATE OR REPLACE FUNCTION join_lobby( | |
| p_lobby_code VARCHAR(6), | |
| p_username VARCHAR(30) | |
| ) | |
| RETURNS UUID AS $$ | |
| DECLARE | |
| v_lobby_id UUID; | |
| v_player_count INT; | |
| v_new_player_id UUID; | |
| BEGIN | |
| -- Find the lobby | |
| SELECT id INTO v_lobby_id | |
| FROM lobbies | |
| WHERE code = p_lobby_code AND status = 'waiting'; | |
| IF v_lobby_id IS NULL THEN | |
| RAISE EXCEPTION 'Lobby not found or game already started'; | |
| END IF; | |
| -- Count existing players | |
| SELECT COUNT(*) INTO v_player_count | |
| FROM players | |
| WHERE lobby_id = v_lobby_id; | |
| IF v_player_count >= 4 THEN | |
| RAISE EXCEPTION 'Lobby is full'; | |
| END IF; | |
| -- Add the player | |
| INSERT INTO players (lobby_id, username, turn_order) | |
| VALUES (v_lobby_id, p_username, v_player_count) | |
| RETURNING id INTO v_new_player_id; | |
| RETURN v_new_player_id; | |
| END; | |
| $$ LANGUAGE plpgsql; | |
| -- ============================================================================= | |
| -- CLEANUP (run manually to reset database) | |
| -- ============================================================================= | |
| -- To reset the database, uncomment and run: | |
| -- DROP TABLE IF EXISTS game_state CASCADE; | |
| -- DROP TABLE IF EXISTS players CASCADE; | |
| -- DROP TABLE IF EXISTS lobbies CASCADE; | |
| -- DROP FUNCTION IF EXISTS update_updated_at_column CASCADE; | |
| -- DROP FUNCTION IF EXISTS generate_lobby_code CASCADE; | |
| -- DROP FUNCTION IF EXISTS create_lobby CASCADE; | |
| -- DROP FUNCTION IF EXISTS join_lobby CASCADE; | |