a geicko-2 based round robin ranking system designed to test c++ battleship submissions battleship.dunkirk.sh
1package main 2 3import ( 4 "database/sql" 5 "time" 6 7 _ "github.com/mattn/go-sqlite3" 8) 9 10var globalDB *sql.DB 11 12type LeaderboardEntry struct { 13 Username string 14 Wins int 15 Losses int 16 AvgMoves float64 17 Stage string 18 LastPlayed time.Time 19} 20 21type Submission struct { 22 ID int 23 Username string 24 Filename string 25 UploadTime time.Time 26 Status string // pending, testing, completed, failed 27} 28 29type Tournament struct { 30 ID int 31 CreatedAt time.Time 32 Status string // active, completed 33 CurrentRound int 34 WinnerID int // ID of winning submission 35} 36 37type BracketMatch struct { 38 ID int 39 TournamentID int 40 Round int 41 Position int 42 Player1ID int 43 Player2ID int 44 WinnerID int 45 Player1Wins int 46 Player2Wins int 47 Player1Moves int 48 Player2Moves int 49 Status string // pending, in_progress, completed 50 Player1Name string // For display 51 Player2Name string 52} 53 54func initDB(path string) (*sql.DB, error) { 55 db, err := sql.Open("sqlite3", path+"?parseTime=true") 56 if err != nil { 57 return nil, err 58 } 59 60 schema := ` 61 CREATE TABLE IF NOT EXISTS submissions ( 62 id INTEGER PRIMARY KEY AUTOINCREMENT, 63 username TEXT NOT NULL, 64 filename TEXT NOT NULL, 65 upload_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 66 status TEXT DEFAULT 'pending', 67 is_active BOOLEAN DEFAULT 1 68 ); 69 70 CREATE TABLE IF NOT EXISTS tournaments ( 71 id INTEGER PRIMARY KEY AUTOINCREMENT, 72 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 73 status TEXT DEFAULT 'active', 74 current_round INTEGER DEFAULT 1, 75 winner_id INTEGER, 76 FOREIGN KEY (winner_id) REFERENCES submissions(id) 77 ); 78 79 CREATE TABLE IF NOT EXISTS bracket_matches ( 80 id INTEGER PRIMARY KEY AUTOINCREMENT, 81 tournament_id INTEGER, 82 round INTEGER, 83 position INTEGER, 84 player1_id INTEGER, 85 player2_id INTEGER, 86 winner_id INTEGER, 87 player1_wins INTEGER DEFAULT 0, 88 player2_wins INTEGER DEFAULT 0, 89 player1_moves INTEGER, 90 player2_moves INTEGER, 91 status TEXT DEFAULT 'pending', 92 timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 93 FOREIGN KEY (tournament_id) REFERENCES tournaments(id), 94 FOREIGN KEY (player1_id) REFERENCES submissions(id), 95 FOREIGN KEY (player2_id) REFERENCES submissions(id), 96 FOREIGN KEY (winner_id) REFERENCES submissions(id) 97 ); 98 99 CREATE TABLE IF NOT EXISTS matches ( 100 id INTEGER PRIMARY KEY AUTOINCREMENT, 101 player1_id INTEGER, 102 player2_id INTEGER, 103 winner_id INTEGER, 104 player1_wins INTEGER DEFAULT 0, 105 player2_wins INTEGER DEFAULT 0, 106 player1_moves INTEGER, 107 player2_moves INTEGER, 108 timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 109 FOREIGN KEY (player1_id) REFERENCES submissions(id), 110 FOREIGN KEY (player2_id) REFERENCES submissions(id), 111 FOREIGN KEY (winner_id) REFERENCES submissions(id) 112 ); 113 114 CREATE INDEX IF NOT EXISTS idx_bracket_matches_tournament ON bracket_matches(tournament_id); 115 CREATE INDEX IF NOT EXISTS idx_bracket_matches_status ON bracket_matches(status); 116 CREATE INDEX IF NOT EXISTS idx_tournaments_status ON tournaments(status); 117 CREATE INDEX IF NOT EXISTS idx_matches_player1 ON matches(player1_id); 118 CREATE INDEX IF NOT EXISTS idx_matches_player2 ON matches(player2_id); 119 CREATE INDEX IF NOT EXISTS idx_submissions_username ON submissions(username); 120 CREATE INDEX IF NOT EXISTS idx_submissions_status ON submissions(status); 121 CREATE INDEX IF NOT EXISTS idx_submissions_active ON submissions(is_active); 122 ` 123 124 _, err = db.Exec(schema) 125 return db, err 126} 127 128func getLeaderboard(limit int) ([]LeaderboardEntry, error) { 129 query := ` 130 SELECT 131 s.username, 132 SUM(CASE WHEN m.player1_id = s.id THEN m.player1_wins WHEN m.player2_id = s.id THEN m.player2_wins ELSE 0 END) as total_wins, 133 SUM(CASE WHEN m.player1_id = s.id THEN m.player2_wins WHEN m.player2_id = s.id THEN m.player1_wins ELSE 0 END) as total_losses, 134 AVG(CASE WHEN m.player1_id = s.id THEN m.player1_moves ELSE m.player2_moves END) as avg_moves, 135 MAX(m.timestamp) as last_played 136 FROM submissions s 137 LEFT JOIN matches m ON (m.player1_id = s.id OR m.player2_id = s.id) 138 WHERE s.is_active = 1 139 GROUP BY s.username 140 HAVING COUNT(m.id) > 0 141 ORDER BY total_wins DESC, total_losses ASC, avg_moves ASC 142 LIMIT ? 143 ` 144 145 rows, err := globalDB.Query(query, limit) 146 if err != nil { 147 return nil, err 148 } 149 defer rows.Close() 150 151 var entries []LeaderboardEntry 152 for rows.Next() { 153 var e LeaderboardEntry 154 var lastPlayed string 155 err := rows.Scan(&e.Username, &e.Wins, &e.Losses, &e.AvgMoves, &lastPlayed) 156 if err != nil { 157 return nil, err 158 } 159 160 // Parse the timestamp string 161 e.LastPlayed, _ = time.Parse("2006-01-02 15:04:05", lastPlayed) 162 163 entries = append(entries, e) 164 } 165 166 return entries, rows.Err() 167} 168 169func addSubmission(username, filename string) (int64, error) { 170 // Mark old submission as inactive 171 _, err := globalDB.Exec( 172 "UPDATE submissions SET is_active = 0 WHERE username = ?", 173 username, 174 ) 175 if err != nil { 176 return 0, err 177 } 178 179 // Insert new submission 180 result, err := globalDB.Exec( 181 "INSERT INTO submissions (username, filename, is_active) VALUES (?, ?, 1)", 182 username, filename, 183 ) 184 if err != nil { 185 return 0, err 186 } 187 return result.LastInsertId() 188} 189 190func addMatch(player1ID, player2ID, winnerID, player1Wins, player2Wins, player1Moves, player2Moves int) error { 191 _, err := globalDB.Exec( 192 "INSERT INTO matches (player1_id, player2_id, winner_id, player1_wins, player2_wins, player1_moves, player2_moves) VALUES (?, ?, ?, ?, ?, ?, ?)", 193 player1ID, player2ID, winnerID, player1Wins, player2Wins, player1Moves, player2Moves, 194 ) 195 return err 196} 197 198func updateSubmissionStatus(id int, status string) error { 199 _, err := globalDB.Exec("UPDATE submissions SET status = ? WHERE id = ?", status, id) 200 return err 201} 202 203func getPendingSubmissions() ([]Submission, error) { 204 rows, err := globalDB.Query( 205 "SELECT id, username, filename, upload_time, status FROM submissions WHERE status = 'pending' AND is_active = 1 ORDER BY upload_time", 206 ) 207 if err != nil { 208 return nil, err 209 } 210 defer rows.Close() 211 212 var submissions []Submission 213 for rows.Next() { 214 var s Submission 215 err := rows.Scan(&s.ID, &s.Username, &s.Filename, &s.UploadTime, &s.Status) 216 if err != nil { 217 return nil, err 218 } 219 submissions = append(submissions, s) 220 } 221 222 return submissions, rows.Err() 223} 224 225func getActiveSubmissions() ([]Submission, error) { 226 rows, err := globalDB.Query( 227 "SELECT id, username, filename, upload_time, status FROM submissions WHERE is_active = 1 AND status = 'completed' ORDER BY username", 228 ) 229 if err != nil { 230 return nil, err 231 } 232 defer rows.Close() 233 234 var submissions []Submission 235 for rows.Next() { 236 var s Submission 237 err := rows.Scan(&s.ID, &s.Username, &s.Filename, &s.UploadTime, &s.Status) 238 if err != nil { 239 return nil, err 240 } 241 submissions = append(submissions, s) 242 } 243 244 return submissions, rows.Err() 245} 246 247func getUserSubmissions(username string) ([]Submission, error) { 248 rows, err := globalDB.Query( 249 "SELECT id, username, filename, upload_time, status FROM submissions WHERE username = ? ORDER BY upload_time DESC LIMIT 10", 250 username, 251 ) 252 if err != nil { 253 return nil, err 254 } 255 defer rows.Close() 256 257 var submissions []Submission 258 for rows.Next() { 259 var s Submission 260 err := rows.Scan(&s.ID, &s.Username, &s.Filename, &s.UploadTime, &s.Status) 261 if err != nil { 262 return nil, err 263 } 264 submissions = append(submissions, s) 265 } 266 267 return submissions, rows.Err() 268} 269 270 271type MatchResult struct { 272 Player1Username string 273 Player2Username string 274 WinnerUsername string 275 AvgMoves int 276} 277 278func getAllMatches() ([]MatchResult, error) { 279 query := ` 280 SELECT 281 s1.username as player1, 282 s2.username as player2, 283 sw.username as winner, 284 m.player1_moves as avg_moves 285 FROM matches m 286 JOIN submissions s1 ON m.player1_id = s1.id 287 JOIN submissions s2 ON m.player2_id = s2.id 288 JOIN submissions sw ON m.winner_id = sw.id 289 WHERE s1.is_active = 1 AND s2.is_active = 1 290 ORDER BY m.timestamp DESC 291 ` 292 293 rows, err := globalDB.Query(query) 294 if err != nil { 295 return nil, err 296 } 297 defer rows.Close() 298 299 var matches []MatchResult 300 for rows.Next() { 301 var m MatchResult 302 err := rows.Scan(&m.Player1Username, &m.Player2Username, &m.WinnerUsername, &m.AvgMoves) 303 if err != nil { 304 return nil, err 305 } 306 matches = append(matches, m) 307 } 308 309 return matches, rows.Err() 310} 311 312// Tournament functions 313 314func getActiveTournament() (*Tournament, error) { 315 var t Tournament 316 var winnerID sql.NullInt64 317 err := globalDB.QueryRow( 318 "SELECT id, created_at, status, current_round, winner_id FROM tournaments WHERE status = 'active' ORDER BY id DESC LIMIT 1", 319 ).Scan(&t.ID, &t.CreatedAt, &t.Status, &t.CurrentRound, &winnerID) 320 321 if err == sql.ErrNoRows { 322 return nil, nil 323 } 324 if winnerID.Valid { 325 t.WinnerID = int(winnerID.Int64) 326 } 327 return &t, err 328} 329 330func getLatestTournament() (*Tournament, error) { 331 var t Tournament 332 var winnerID sql.NullInt64 333 err := globalDB.QueryRow( 334 "SELECT id, created_at, status, current_round, winner_id FROM tournaments ORDER BY id DESC LIMIT 1", 335 ).Scan(&t.ID, &t.CreatedAt, &t.Status, &t.CurrentRound, &winnerID) 336 337 if err == sql.ErrNoRows { 338 return nil, nil 339 } 340 if winnerID.Valid { 341 t.WinnerID = int(winnerID.Int64) 342 } 343 return &t, err 344} 345 346func createTournament() (*Tournament, error) { 347 result, err := globalDB.Exec("INSERT INTO tournaments (status, current_round) VALUES ('active', 1)") 348 if err != nil { 349 return nil, err 350 } 351 352 id, _ := result.LastInsertId() 353 return &Tournament{ 354 ID: int(id), 355 Status: "active", 356 CurrentRound: 1, 357 }, nil 358} 359 360func updateTournamentRound(tournamentID, round int) error { 361 _, err := globalDB.Exec("UPDATE tournaments SET current_round = ? WHERE id = ?", round, tournamentID) 362 return err 363} 364 365func completeTournament(tournamentID, winnerID int) error { 366 _, err := globalDB.Exec("UPDATE tournaments SET status = 'completed', winner_id = ? WHERE id = ?", winnerID, tournamentID) 367 return err 368} 369 370func addBracketMatch(tournamentID, round, position, player1ID, player2ID int) error { 371 _, err := globalDB.Exec( 372 "INSERT INTO bracket_matches (tournament_id, round, position, player1_id, player2_id, status) VALUES (?, ?, ?, ?, ?, 'pending')", 373 tournamentID, round, position, player1ID, player2ID, 374 ) 375 return err 376} 377 378func getPendingBracketMatches(tournamentID int) ([]BracketMatch, error) { 379 query := ` 380 SELECT 381 bm.id, bm.tournament_id, bm.round, bm.position, 382 bm.player1_id, bm.player2_id, bm.winner_id, 383 bm.player1_wins, bm.player2_wins, 384 bm.player1_moves, bm.player2_moves, bm.status, 385 s1.username as player1_name, s2.username as player2_name 386 FROM bracket_matches bm 387 JOIN submissions s1 ON bm.player1_id = s1.id 388 JOIN submissions s2 ON bm.player2_id = s2.id 389 WHERE bm.tournament_id = ? AND bm.status = 'pending' 390 ORDER BY bm.round, bm.position 391 ` 392 393 rows, err := globalDB.Query(query, tournamentID) 394 if err != nil { 395 return nil, err 396 } 397 defer rows.Close() 398 399 var matches []BracketMatch 400 for rows.Next() { 401 var m BracketMatch 402 var winnerID sql.NullInt64 403 var player1Moves, player2Moves sql.NullInt64 404 err := rows.Scan( 405 &m.ID, &m.TournamentID, &m.Round, &m.Position, 406 &m.Player1ID, &m.Player2ID, &winnerID, 407 &m.Player1Wins, &m.Player2Wins, 408 &player1Moves, &player2Moves, &m.Status, 409 &m.Player1Name, &m.Player2Name, 410 ) 411 if err != nil { 412 return nil, err 413 } 414 if winnerID.Valid { 415 m.WinnerID = int(winnerID.Int64) 416 } 417 if player1Moves.Valid { 418 m.Player1Moves = int(player1Moves.Int64) 419 } 420 if player2Moves.Valid { 421 m.Player2Moves = int(player2Moves.Int64) 422 } 423 matches = append(matches, m) 424 } 425 426 return matches, rows.Err() 427} 428 429func getAllBracketMatches(tournamentID int) ([]BracketMatch, error) { 430 query := ` 431 SELECT 432 bm.id, bm.tournament_id, bm.round, bm.position, 433 bm.player1_id, bm.player2_id, bm.winner_id, 434 bm.player1_wins, bm.player2_wins, 435 bm.player1_moves, bm.player2_moves, bm.status, 436 s1.username as player1_name, s2.username as player2_name 437 FROM bracket_matches bm 438 LEFT JOIN submissions s1 ON bm.player1_id = s1.id 439 LEFT JOIN submissions s2 ON bm.player2_id = s2.id 440 WHERE bm.tournament_id = ? 441 ORDER BY bm.round, bm.position 442 ` 443 444 rows, err := globalDB.Query(query, tournamentID) 445 if err != nil { 446 return nil, err 447 } 448 defer rows.Close() 449 450 var matches []BracketMatch 451 for rows.Next() { 452 var m BracketMatch 453 var player1Name, player2Name sql.NullString 454 var winnerID, player1Moves, player2Moves sql.NullInt64 455 err := rows.Scan( 456 &m.ID, &m.TournamentID, &m.Round, &m.Position, 457 &m.Player1ID, &m.Player2ID, &winnerID, 458 &m.Player1Wins, &m.Player2Wins, 459 &player1Moves, &player2Moves, &m.Status, 460 &player1Name, &player2Name, 461 ) 462 if err != nil { 463 return nil, err 464 } 465 if winnerID.Valid { 466 m.WinnerID = int(winnerID.Int64) 467 } 468 if player1Moves.Valid { 469 m.Player1Moves = int(player1Moves.Int64) 470 } 471 if player2Moves.Valid { 472 m.Player2Moves = int(player2Moves.Int64) 473 } 474 if player1Name.Valid { 475 m.Player1Name = player1Name.String 476 } 477 if player2Name.Valid { 478 m.Player2Name = player2Name.String 479 } 480 matches = append(matches, m) 481 } 482 483 return matches, rows.Err() 484} 485 486func updateBracketMatchResult(matchID, winnerID, player1Wins, player2Wins, player1Moves, player2Moves int) error { 487 _, err := globalDB.Exec( 488 `UPDATE bracket_matches 489 SET winner_id = ?, player1_wins = ?, player2_wins = ?, 490 player1_moves = ?, player2_moves = ?, status = 'completed' 491 WHERE id = ?`, 492 winnerID, player1Wins, player2Wins, player1Moves, player2Moves, matchID, 493 ) 494 return err 495} 496 497func isRoundComplete(tournamentID, round int) (bool, error) { 498 var pendingCount int 499 err := globalDB.QueryRow( 500 "SELECT COUNT(*) FROM bracket_matches WHERE tournament_id = ? AND round = ? AND status != 'completed'", 501 tournamentID, round, 502 ).Scan(&pendingCount) 503 504 return pendingCount == 0, err 505} 506