extremely claude-assisted go game based on atproto! working on cleaning up and giving a more unique design, still has a bit of a slop vibe to it.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Phase 6: Drop moves/passes tables, simplify schema to minimal index

Games table now only stores discovery index data: rkey, AT URI, players,
board_size, status, action_count, last_action_type, timestamps.

Removed: moves table, passes table, MoveRecord/PassRecord DB interfaces,
winner/score columns. Migration recreates table to drop old columns.
Updated server firehose placeholder to match new schema.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+53 -184
+43 -89
src/lib/server/db.ts
··· 5 5 import path from 'path'; 6 6 7 7 export interface GameRecord { 8 - id: string; // AT URI 8 + id: string; // AT URI (game_at_uri) 9 9 rkey: string; // Record key (TID) 10 10 player_one: string; 11 11 player_two: string | null; 12 12 board_size: number; 13 13 status: 'waiting' | 'active' | 'completed'; 14 - winner: string | null; 15 - black_score: number | null; 16 - white_score: number | null; 17 - black_scorer: string | null; 18 - white_scorer: string | null; 19 14 action_count: number; 20 15 last_action_type: string | null; 21 16 created_at: string; 22 17 updated_at: string; 23 18 } 24 19 25 - export interface MoveRecord { 26 - id: string; // AT URI 27 - rkey: string; // Record key (TID) 28 - game_id: string; // AT URI reference 29 - player: string; 30 - move_number: number; 31 - x: number; 32 - y: number; 33 - color: 'black' | 'white'; 34 - capture_count: number; 35 - created_at: string; 36 - } 37 - 38 - export interface PassRecord { 39 - id: string; // AT URI 40 - rkey: string; // Record key (TID) 41 - game_id: string; // AT URI reference 42 - player: string; 43 - move_number: number; 44 - color: 'black' | 'white'; 45 - created_at: string; 46 - } 47 - 48 20 export interface Database { 49 21 games: GameRecord; 50 - moves: MoveRecord; 51 - passes: PassRecord; 52 22 } 53 23 54 24 let db: Kysely<Database> | null = null; 55 25 56 26 export function getDb(): Kysely<Database> { 57 27 if (!db) { 58 - // Ensure data directory exists 59 28 const dbDir = path.dirname(DATABASE_PATH); 60 29 if (!fs.existsSync(dbDir)) { 61 30 fs.mkdirSync(dbDir, { recursive: true }); 62 31 } 63 32 64 33 const sqlite = new Database(DATABASE_PATH); 65 - 66 - // Enable WAL mode for better concurrency 67 34 sqlite.pragma('journal_mode = WAL'); 68 35 69 36 const dialect = new SqliteDialect({ ··· 74 41 dialect, 75 42 }); 76 43 77 - // Initialize tables 78 44 initializeTables(sqlite); 79 - 80 - // Run migrations 81 45 runMigrations(sqlite); 82 46 } 83 47 ··· 85 49 } 86 50 87 51 function initializeTables(sqlite: Database.Database) { 88 - // Create games table 89 52 sqlite.exec(` 90 53 CREATE TABLE IF NOT EXISTS games ( 91 54 id TEXT PRIMARY KEY, ··· 94 57 player_two TEXT, 95 58 board_size INTEGER NOT NULL DEFAULT 19, 96 59 status TEXT NOT NULL CHECK(status IN ('waiting', 'active', 'completed')), 97 - winner TEXT, 98 - black_score INTEGER, 99 - white_score INTEGER, 100 - black_scorer TEXT, 101 - white_scorer TEXT, 60 + action_count INTEGER NOT NULL DEFAULT 0, 61 + last_action_type TEXT, 102 62 created_at TEXT NOT NULL, 103 63 updated_at TEXT NOT NULL 104 64 ) 105 65 `); 106 66 107 - // Create moves table 108 - sqlite.exec(` 109 - CREATE TABLE IF NOT EXISTS moves ( 110 - id TEXT PRIMARY KEY, 111 - rkey TEXT NOT NULL, 112 - game_id TEXT NOT NULL, 113 - player TEXT NOT NULL, 114 - move_number INTEGER NOT NULL, 115 - x INTEGER NOT NULL, 116 - y INTEGER NOT NULL, 117 - color TEXT NOT NULL CHECK(color IN ('black', 'white')), 118 - capture_count INTEGER NOT NULL DEFAULT 0, 119 - created_at TEXT NOT NULL, 120 - FOREIGN KEY (game_id) REFERENCES games(id) 121 - ) 122 - `); 123 - 124 - // Create passes table 125 - sqlite.exec(` 126 - CREATE TABLE IF NOT EXISTS passes ( 127 - id TEXT PRIMARY KEY, 128 - rkey TEXT NOT NULL, 129 - game_id TEXT NOT NULL, 130 - player TEXT NOT NULL, 131 - move_number INTEGER NOT NULL, 132 - color TEXT NOT NULL CHECK(color IN ('black', 'white')), 133 - created_at TEXT NOT NULL, 134 - FOREIGN KEY (game_id) REFERENCES games(id) 135 - ) 136 - `); 137 - 138 - // Create indexes 139 67 sqlite.exec(` 140 68 CREATE INDEX IF NOT EXISTS idx_games_status ON games(status); 141 69 CREATE INDEX IF NOT EXISTS idx_games_player_one ON games(player_one); 142 70 CREATE INDEX IF NOT EXISTS idx_games_player_two ON games(player_two); 143 - CREATE INDEX IF NOT EXISTS idx_moves_game_id ON moves(game_id); 144 - CREATE INDEX IF NOT EXISTS idx_moves_move_number ON moves(game_id, move_number); 145 - CREATE INDEX IF NOT EXISTS idx_passes_game_id ON passes(game_id); 71 + CREATE INDEX IF NOT EXISTS idx_games_rkey ON games(rkey); 146 72 `); 147 73 } 148 74 149 75 function runMigrations(sqlite: Database.Database) { 76 + // Migration: add action_count/last_action_type if upgrading from old schema 150 77 const tableInfo = sqlite.prepare("PRAGMA table_info(games)").all() as Array<{ name: string }>; 151 78 const columnNames = tableInfo.map(col => col.name); 152 79 153 - if (!columnNames.includes('black_score')) { 154 - sqlite.exec(` 155 - ALTER TABLE games ADD COLUMN black_score INTEGER; 156 - ALTER TABLE games ADD COLUMN white_score INTEGER; 157 - ALTER TABLE games ADD COLUMN black_scorer TEXT; 158 - ALTER TABLE games ADD COLUMN white_scorer TEXT; 159 - `); 160 - } 161 - 162 80 if (!columnNames.includes('action_count')) { 163 81 sqlite.exec(` 164 82 ALTER TABLE games ADD COLUMN action_count INTEGER NOT NULL DEFAULT 0; 165 83 ALTER TABLE games ADD COLUMN last_action_type TEXT; 166 84 `); 167 85 168 - // Backfill action_count from existing moves/passes tables 169 - // Check if the moves table exists before trying to backfill 86 + // Backfill from old tables if they exist 170 87 const tables = sqlite.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='moves'").all(); 171 88 if (tables.length > 0) { 172 89 sqlite.exec(` ··· 179 96 ); 180 97 `); 181 98 } 99 + } 100 + 101 + // Migration: drop old tables and columns that are no longer needed 102 + const oldTables = sqlite.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name IN ('moves', 'passes')").all(); 103 + if (oldTables.length > 0) { 104 + sqlite.exec(` 105 + DROP TABLE IF EXISTS moves; 106 + DROP TABLE IF EXISTS passes; 107 + `); 108 + } 109 + 110 + // Drop old score/winner columns by recreating the table if they exist 111 + if (columnNames.includes('winner')) { 112 + sqlite.exec(` 113 + CREATE TABLE IF NOT EXISTS games_new ( 114 + id TEXT PRIMARY KEY, 115 + rkey TEXT NOT NULL, 116 + player_one TEXT NOT NULL, 117 + player_two TEXT, 118 + board_size INTEGER NOT NULL DEFAULT 19, 119 + status TEXT NOT NULL CHECK(status IN ('waiting', 'active', 'completed')), 120 + action_count INTEGER NOT NULL DEFAULT 0, 121 + last_action_type TEXT, 122 + created_at TEXT NOT NULL, 123 + updated_at TEXT NOT NULL 124 + ); 125 + INSERT INTO games_new (id, rkey, player_one, player_two, board_size, status, action_count, last_action_type, created_at, updated_at) 126 + SELECT id, rkey, player_one, player_two, board_size, status, 127 + COALESCE(action_count, 0), last_action_type, created_at, updated_at 128 + FROM games; 129 + DROP TABLE games; 130 + ALTER TABLE games_new RENAME TO games; 131 + CREATE INDEX IF NOT EXISTS idx_games_status ON games(status); 132 + CREATE INDEX IF NOT EXISTS idx_games_player_one ON games(player_one); 133 + CREATE INDEX IF NOT EXISTS idx_games_player_two ON games(player_two); 134 + CREATE INDEX IF NOT EXISTS idx_games_rkey ON games(rkey); 135 + `); 182 136 } 183 137 }
+10 -90
src/lib/server/firehose.ts
··· 1 1 import { getDb } from './db'; 2 2 3 - // Firehose subscription for AT Protocol network updates 4 - // This would connect to the AT Protocol firehose and listen for our custom lexicon records 3 + // Server-side firehose subscription for syncing the discovery index. 4 + // In the PDS-first architecture, this only updates the games index — 5 + // moves and passes are fetched client-side from Constellation/PDS. 5 6 6 7 let isSubscribed = false; 7 8 ··· 10 11 11 12 console.log('Firehose subscription starting...'); 12 13 13 - // Note: Full firehose implementation would require: 14 - // 1. WebSocket connection to AT Protocol relay (wss://bsky.network) 15 - // 2. Subscription to com.atproto.sync.subscribeRepos 16 - // 3. Parsing CAR files from the firehose 17 - // 4. Filtering for our custom lexicons (boo.sky.go.*) 18 - // 5. Updating local database with received records 19 - 20 - // For a production implementation, you would use: 21 - // import { Firehose } from '@atproto/sync'; 22 - // const firehose = new Firehose({ 23 - // filterCollections: ['boo.sky.go.game', 'boo.sky.go.move', 'boo.sky.go.pass'] 24 - // }); 25 - 26 - // Simplified placeholder that would be replaced with actual firehose logic 14 + // Placeholder — a production implementation would connect to 15 + // com.atproto.sync.subscribeRepos and filter for boo.sky.go.game records. 27 16 setupFirehoseListener(); 28 17 29 18 isSubscribed = true; 30 19 } 31 20 32 21 async function setupFirehoseListener() { 33 - const db = getDb(); 34 - 35 - // This is where you would: 36 - // 1. Connect to the firehose WebSocket 37 - // 2. Listen for commit events 38 - // 3. Parse the records 39 - // 4. Update the database 40 - 41 - /* 42 - Example structure: 43 - 44 - firehose.on('commit', async (evt) => { 45 - if (evt.ops) { 46 - for (const op of evt.ops) { 47 - if (op.action === 'create' || op.action === 'update') { 48 - if (op.path.startsWith('boo.sky.go.game/')) { 49 - await handleGameRecord(op); 50 - } else if (op.path.startsWith('boo.sky.go.move/')) { 51 - await handleMoveRecord(op); 52 - } else if (op.path.startsWith('boo.sky.go.pass/')) { 53 - await handlePassRecord(op); 54 - } 55 - } 56 - } 57 - } 58 - }); 59 - */ 60 - 61 22 console.log('Firehose listener configured (placeholder)'); 62 23 } 63 24 64 25 async function handleGameRecord(record: any) { 65 26 const db = getDb(); 27 + const now = new Date().toISOString(); 66 28 67 - // Insert or update game in database 68 29 await db 69 30 .insertInto('games') 70 31 .values({ ··· 74 35 player_two: record.value.playerTwo || null, 75 36 board_size: record.value.boardSize, 76 37 status: record.value.status, 77 - winner: record.value.winner || null, 38 + action_count: 0, 39 + last_action_type: null, 78 40 created_at: record.value.createdAt, 79 - updated_at: new Date().toISOString(), 41 + updated_at: now, 80 42 }) 81 43 .onConflict((oc) => 82 44 oc.column('id').doUpdateSet({ 83 45 player_two: record.value.playerTwo || null, 84 46 status: record.value.status, 85 - winner: record.value.winner || null, 86 - updated_at: new Date().toISOString(), 47 + updated_at: now, 87 48 }) 88 49 ) 89 50 .execute(); 90 51 } 91 - 92 - async function handleMoveRecord(record: any) { 93 - const db = getDb(); 94 - 95 - // Insert move in database 96 - await db 97 - .insertInto('moves') 98 - .values({ 99 - id: record.uri, 100 - rkey: record.rkey, 101 - game_id: record.value.game, 102 - player: record.value.player, 103 - move_number: record.value.moveNumber, 104 - x: record.value.x, 105 - y: record.value.y, 106 - color: record.value.color, 107 - capture_count: record.value.captureCount, 108 - created_at: record.value.createdAt, 109 - }) 110 - .onConflict((oc) => oc.column('id').doNothing()) 111 - .execute(); 112 - } 113 - 114 - async function handlePassRecord(record: any) { 115 - const db = getDb(); 116 - 117 - // Insert pass in database 118 - await db 119 - .insertInto('passes') 120 - .values({ 121 - id: record.uri, 122 - rkey: record.rkey, 123 - game_id: record.value.game, 124 - player: record.value.player, 125 - move_number: record.value.moveNumber, 126 - color: record.value.color, 127 - created_at: record.value.createdAt, 128 - }) 129 - .onConflict((oc) => oc.column('id').doNothing()) 130 - .execute(); 131 - }
-5
src/routes/api/games/+server.ts
··· 68 68 player_two: null, 69 69 board_size: boardSize, 70 70 status: 'waiting', 71 - winner: null, 72 - black_score: null, 73 - white_score: null, 74 - black_scorer: null, 75 - white_scorer: null, 76 71 action_count: 0, 77 72 last_action_type: null, 78 73 created_at: now,