File size: 9,908 Bytes
2c6b921
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
-- =============================================================================
-- 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;