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 "math" 6 "time" 7 8 _ "github.com/mattn/go-sqlite3" 9) 10 11var globalDB *sql.DB 12 13type LeaderboardEntry struct { 14 Username string 15 Wins int 16 Losses int 17 WinPct float64 18 Rating int // Glicko-2 rating 19 RD int // Rating Deviation (uncertainty) 20 AvgMoves float64 21 Stage string 22 LastPlayed time.Time 23} 24 25type Submission struct { 26 ID int 27 Username string 28 Filename string 29 UploadTime time.Time 30 Status string // pending, testing, completed, failed 31} 32 33type Tournament struct { 34 ID int 35 CreatedAt time.Time 36 Status string // active, completed 37 CurrentRound int 38 WinnerID int // ID of winning submission 39} 40 41type BracketMatch struct { 42 ID int 43 TournamentID int 44 Round int 45 Position int 46 Player1ID int 47 Player2ID int 48 WinnerID int 49 Player1Wins int 50 Player2Wins int 51 Player1Moves int 52 Player2Moves int 53 Status string // pending, in_progress, completed 54 Player1Name string // For display 55 Player2Name string 56} 57 58func initDB(path string) (*sql.DB, error) { 59 db, err := sql.Open("sqlite3", path+"?parseTime=true") 60 if err != nil { 61 return nil, err 62 } 63 64 schema := ` 65 CREATE TABLE IF NOT EXISTS submissions ( 66 id INTEGER PRIMARY KEY AUTOINCREMENT, 67 username TEXT NOT NULL, 68 filename TEXT NOT NULL, 69 upload_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 70 status TEXT DEFAULT 'pending', 71 is_active BOOLEAN DEFAULT 1, 72 glicko_rating REAL DEFAULT 1500.0, 73 glicko_rd REAL DEFAULT 350.0, 74 glicko_volatility REAL DEFAULT 0.06 75 ); 76 77 CREATE TABLE IF NOT EXISTS tournaments ( 78 id INTEGER PRIMARY KEY AUTOINCREMENT, 79 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 80 status TEXT DEFAULT 'active', 81 current_round INTEGER DEFAULT 1, 82 winner_id INTEGER, 83 FOREIGN KEY (winner_id) REFERENCES submissions(id) 84 ); 85 86 CREATE TABLE IF NOT EXISTS bracket_matches ( 87 id INTEGER PRIMARY KEY AUTOINCREMENT, 88 tournament_id INTEGER, 89 round INTEGER, 90 position INTEGER, 91 player1_id INTEGER, 92 player2_id INTEGER, 93 winner_id INTEGER, 94 player1_wins INTEGER DEFAULT 0, 95 player2_wins INTEGER DEFAULT 0, 96 player1_moves INTEGER, 97 player2_moves INTEGER, 98 status TEXT DEFAULT 'pending', 99 timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 100 FOREIGN KEY (tournament_id) REFERENCES tournaments(id), 101 FOREIGN KEY (player1_id) REFERENCES submissions(id), 102 FOREIGN KEY (player2_id) REFERENCES submissions(id), 103 FOREIGN KEY (winner_id) REFERENCES submissions(id) 104 ); 105 106 CREATE TABLE IF NOT EXISTS matches ( 107 id INTEGER PRIMARY KEY AUTOINCREMENT, 108 player1_id INTEGER, 109 player2_id INTEGER, 110 winner_id INTEGER, 111 player1_wins INTEGER DEFAULT 0, 112 player2_wins INTEGER DEFAULT 0, 113 player1_moves INTEGER, 114 player2_moves INTEGER, 115 is_valid BOOLEAN DEFAULT 1, 116 timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 117 FOREIGN KEY (player1_id) REFERENCES submissions(id), 118 FOREIGN KEY (player2_id) REFERENCES submissions(id), 119 FOREIGN KEY (winner_id) REFERENCES submissions(id) 120 ); 121 122 CREATE INDEX IF NOT EXISTS idx_bracket_matches_tournament ON bracket_matches(tournament_id); 123 CREATE INDEX IF NOT EXISTS idx_bracket_matches_status ON bracket_matches(status); 124 CREATE INDEX IF NOT EXISTS idx_tournaments_status ON tournaments(status); 125 CREATE INDEX IF NOT EXISTS idx_matches_player1 ON matches(player1_id); 126 CREATE INDEX IF NOT EXISTS idx_matches_player2 ON matches(player2_id); 127 CREATE INDEX IF NOT EXISTS idx_matches_valid ON matches(is_valid); 128 CREATE INDEX IF NOT EXISTS idx_submissions_username ON submissions(username); 129 CREATE INDEX IF NOT EXISTS idx_submissions_status ON submissions(status); 130 CREATE INDEX IF NOT EXISTS idx_submissions_active ON submissions(is_active); 131 CREATE UNIQUE INDEX IF NOT EXISTS idx_matches_unique_pair ON matches(player1_id, player2_id, is_valid) WHERE is_valid = 1; 132 ` 133 134 _, err = db.Exec(schema) 135 return db, err 136} 137 138func getLeaderboard(limit int) ([]LeaderboardEntry, error) { 139 query := ` 140 SELECT 141 s.username, 142 s.glicko_rating, 143 s.glicko_rd, 144 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, 145 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, 146 AVG(CASE WHEN m.player1_id = s.id THEN m.player1_moves ELSE m.player2_moves END) as avg_moves, 147 MAX(m.timestamp) as last_played 148 FROM submissions s 149 LEFT JOIN matches m ON (m.player1_id = s.id OR m.player2_id = s.id) AND m.is_valid = 1 150 WHERE s.is_active = 1 151 GROUP BY s.username, s.glicko_rating, s.glicko_rd 152 HAVING COUNT(m.id) > 0 153 ORDER BY s.glicko_rating DESC, total_wins DESC 154 LIMIT ? 155 ` 156 157 rows, err := globalDB.Query(query, limit) 158 if err != nil { 159 return nil, err 160 } 161 defer rows.Close() 162 163 var entries []LeaderboardEntry 164 for rows.Next() { 165 var e LeaderboardEntry 166 var lastPlayed string 167 var rating, rd float64 168 err := rows.Scan(&e.Username, &rating, &rd, &e.Wins, &e.Losses, &e.AvgMoves, &lastPlayed) 169 if err != nil { 170 return nil, err 171 } 172 173 e.Rating = int(rating) 174 e.RD = int(rd) 175 176 // Calculate win percentage 177 totalGames := e.Wins + e.Losses 178 if totalGames > 0 { 179 e.WinPct = float64(e.Wins) / float64(totalGames) * 100.0 180 } 181 182 // Parse the timestamp string 183 e.LastPlayed, _ = time.Parse("2006-01-02 15:04:05", lastPlayed) 184 185 entries = append(entries, e) 186 } 187 188 return entries, rows.Err() 189} 190 191func addSubmission(username, filename string) (int64, error) { 192 // Invalidate all matches involving this user's submissions 193 _, err := globalDB.Exec( 194 `UPDATE matches SET is_valid = 0 195 WHERE player1_id IN (SELECT id FROM submissions WHERE username = ?) 196 OR player2_id IN (SELECT id FROM submissions WHERE username = ?)`, 197 username, username, 198 ) 199 if err != nil { 200 return 0, err 201 } 202 203 // Mark old submission as inactive 204 _, err = globalDB.Exec( 205 "UPDATE submissions SET is_active = 0 WHERE username = ?", 206 username, 207 ) 208 if err != nil { 209 return 0, err 210 } 211 212 // Insert new submission 213 result, err := globalDB.Exec( 214 "INSERT INTO submissions (username, filename, is_active) VALUES (?, ?, 1)", 215 username, filename, 216 ) 217 if err != nil { 218 return 0, err 219 } 220 return result.LastInsertId() 221} 222 223func addMatch(player1ID, player2ID, winnerID, player1Wins, player2Wins, player1Moves, player2Moves int) error { 224 _, err := globalDB.Exec( 225 "INSERT INTO matches (player1_id, player2_id, winner_id, player1_wins, player2_wins, player1_moves, player2_moves) VALUES (?, ?, ?, ?, ?, ?, ?)", 226 player1ID, player2ID, winnerID, player1Wins, player2Wins, player1Moves, player2Moves, 227 ) 228 return err 229} 230 231func updateSubmissionStatus(id int, status string) error { 232 _, err := globalDB.Exec("UPDATE submissions SET status = ? WHERE id = ?", status, id) 233 return err 234} 235 236func getPendingSubmissions() ([]Submission, error) { 237 rows, err := globalDB.Query( 238 "SELECT id, username, filename, upload_time, status FROM submissions WHERE status = 'pending' AND is_active = 1 ORDER BY upload_time", 239 ) 240 if err != nil { 241 return nil, err 242 } 243 defer rows.Close() 244 245 var submissions []Submission 246 for rows.Next() { 247 var s Submission 248 err := rows.Scan(&s.ID, &s.Username, &s.Filename, &s.UploadTime, &s.Status) 249 if err != nil { 250 return nil, err 251 } 252 submissions = append(submissions, s) 253 } 254 255 return submissions, rows.Err() 256} 257 258func getActiveSubmissions() ([]Submission, error) { 259 rows, err := globalDB.Query( 260 "SELECT id, username, filename, upload_time, status FROM submissions WHERE is_active = 1 AND status = 'completed' ORDER BY username", 261 ) 262 if err != nil { 263 return nil, err 264 } 265 defer rows.Close() 266 267 var submissions []Submission 268 for rows.Next() { 269 var s Submission 270 err := rows.Scan(&s.ID, &s.Username, &s.Filename, &s.UploadTime, &s.Status) 271 if err != nil { 272 return nil, err 273 } 274 submissions = append(submissions, s) 275 } 276 277 return submissions, rows.Err() 278} 279 280func getUserSubmissions(username string) ([]Submission, error) { 281 rows, err := globalDB.Query( 282 "SELECT id, username, filename, upload_time, status FROM submissions WHERE username = ? ORDER BY upload_time DESC LIMIT 10", 283 username, 284 ) 285 if err != nil { 286 return nil, err 287 } 288 defer rows.Close() 289 290 var submissions []Submission 291 for rows.Next() { 292 var s Submission 293 err := rows.Scan(&s.ID, &s.Username, &s.Filename, &s.UploadTime, &s.Status) 294 if err != nil { 295 return nil, err 296 } 297 submissions = append(submissions, s) 298 } 299 300 return submissions, rows.Err() 301} 302 303// Glicko-2 rating system implementation 304// Based on Mark Glickman's paper: http://www.glicko.net/glicko/glicko2.pdf 305 306const ( 307 glickoTau = 0.5 // System constant (volatility change constraint) 308 glickoEpsilon = 0.000001 // Convergence tolerance 309 glicko2Scale = 173.7178 // Conversion factor: rating / 173.7178 310) 311 312type Glicko2Player struct { 313 Rating float64 // μ in Glicko-2 scale 314 RD float64 // φ in Glicko-2 scale 315 Volatility float64 // σ 316} 317 318type Glicko2Result struct { 319 OpponentRating float64 320 OpponentRD float64 321 Score float64 // 0.0 to 1.0 322} 323 324// Convert rating from standard scale to Glicko-2 scale 325func toGlicko2Scale(rating, rd float64) (float64, float64) { 326 return (rating - 1500.0) / glicko2Scale, rd / glicko2Scale 327} 328 329// Convert rating from Glicko-2 scale to standard scale 330func fromGlicko2Scale(mu, phi float64) (float64, float64) { 331 return mu*glicko2Scale + 1500.0, phi * glicko2Scale 332} 333 334func g(phi float64) float64 { 335 return 1.0 / math.Sqrt(1.0+3.0*phi*phi/(math.Pi*math.Pi)) 336} 337 338func eFunc(mu, muJ, phiJ float64) float64 { 339 return 1.0 / (1.0 + math.Exp(-g(phiJ)*(mu-muJ))) 340} 341 342func updateGlicko2(player Glicko2Player, results []Glicko2Result) Glicko2Player { 343 // Step 2: Convert to Glicko-2 scale 344 mu, phi := toGlicko2Scale(player.Rating, player.RD) 345 sigma := player.Volatility 346 347 if len(results) == 0 { 348 // No games played - increase RD due to inactivity 349 phiStar := math.Sqrt(phi*phi + sigma*sigma) 350 rating, rd := fromGlicko2Scale(mu, phiStar) 351 return Glicko2Player{Rating: rating, RD: rd, Volatility: sigma} 352 } 353 354 // Step 3: Compute v (variance) 355 var vInv float64 356 for _, result := range results { 357 muJ, phiJ := toGlicko2Scale(result.OpponentRating, result.OpponentRD) 358 gPhiJ := g(phiJ) 359 eVal := eFunc(mu, muJ, phiJ) 360 vInv += gPhiJ * gPhiJ * eVal * (1.0 - eVal) 361 } 362 v := 1.0 / vInv 363 364 // Step 4: Compute delta (improvement) 365 var delta float64 366 for _, result := range results { 367 muJ, phiJ := toGlicko2Scale(result.OpponentRating, result.OpponentRD) 368 gPhiJ := g(phiJ) 369 eVal := eFunc(mu, muJ, phiJ) 370 delta += gPhiJ * (result.Score - eVal) 371 } 372 delta *= v 373 374 // Step 5: Determine new volatility using Illinois algorithm 375 a := math.Log(sigma * sigma) 376 377 deltaSquared := delta * delta 378 phiSquared := phi * phi 379 380 fFunc := func(x float64) float64 { 381 eX := math.Exp(x) 382 num := eX * (deltaSquared - phiSquared - v - eX) 383 denom := 2.0 * (phiSquared + v + eX) * (phiSquared + v + eX) 384 return num/denom - (x-a)/(glickoTau*glickoTau) 385 } 386 387 // Find bounds 388 A := a 389 var B float64 390 if deltaSquared > phiSquared+v { 391 B = math.Log(deltaSquared - phiSquared - v) 392 } else { 393 k := 1.0 394 for fFunc(a-k*glickoTau) < 0 { 395 k++ 396 } 397 B = a - k*glickoTau 398 } 399 400 // Illinois algorithm iteration 401 fA := fFunc(A) 402 fB := fFunc(B) 403 404 for math.Abs(B-A) > glickoEpsilon { 405 C := A + (A-B)*fA/(fB-fA) 406 fC := fFunc(C) 407 408 if fC*fB < 0 { 409 A = B 410 fA = fB 411 } else { 412 fA = fA / 2.0 413 } 414 415 B = C 416 fB = fC 417 } 418 419 sigmaNew := math.Exp(A / 2.0) 420 421 // Step 6: Update rating deviation 422 phiStar := math.Sqrt(phiSquared + sigmaNew*sigmaNew) 423 424 // Step 7: Update rating and RD 425 phiNew := 1.0 / math.Sqrt(1.0/(phiStar*phiStar)+1.0/v) 426 427 var muNew float64 428 for _, result := range results { 429 muJ, phiJ := toGlicko2Scale(result.OpponentRating, result.OpponentRD) 430 muNew += g(phiJ) * (result.Score - eFunc(mu, muJ, phiJ)) 431 } 432 muNew = mu + phiNew*phiNew*muNew 433 434 // Step 8: Convert back to standard scale 435 rating, rd := fromGlicko2Scale(muNew, phiNew) 436 437 return Glicko2Player{Rating: rating, RD: rd, Volatility: sigmaNew} 438} 439 440func updateGlicko2Ratings(player1ID, player2ID, player1Wins, player2Wins int) error { 441 // Get current Glicko-2 values for both players 442 var p1Rating, p1RD, p1Vol, p2Rating, p2RD, p2Vol float64 443 444 err := globalDB.QueryRow( 445 "SELECT glicko_rating, glicko_rd, glicko_volatility FROM submissions WHERE id = ?", 446 player1ID, 447 ).Scan(&p1Rating, &p1RD, &p1Vol) 448 if err != nil { 449 return err 450 } 451 452 err = globalDB.QueryRow( 453 "SELECT glicko_rating, glicko_rd, glicko_volatility FROM submissions WHERE id = ?", 454 player2ID, 455 ).Scan(&p2Rating, &p2RD, &p2Vol) 456 if err != nil { 457 return err 458 } 459 460 // Calculate scores 461 totalGames := player1Wins + player2Wins 462 player1Score := float64(player1Wins) / float64(totalGames) 463 player2Score := float64(player2Wins) / float64(totalGames) 464 465 // Update player 1 466 p1 := Glicko2Player{Rating: p1Rating, RD: p1RD, Volatility: p1Vol} 467 p1Results := []Glicko2Result{{OpponentRating: p2Rating, OpponentRD: p2RD, Score: player1Score}} 468 p1New := updateGlicko2(p1, p1Results) 469 470 // Update player 2 471 p2 := Glicko2Player{Rating: p2Rating, RD: p2RD, Volatility: p2Vol} 472 p2Results := []Glicko2Result{{OpponentRating: p1Rating, OpponentRD: p1RD, Score: player2Score}} 473 p2New := updateGlicko2(p2, p2Results) 474 475 // Save updated ratings 476 _, err = globalDB.Exec( 477 "UPDATE submissions SET glicko_rating = ?, glicko_rd = ?, glicko_volatility = ? WHERE id = ?", 478 p1New.Rating, p1New.RD, p1New.Volatility, player1ID, 479 ) 480 if err != nil { 481 return err 482 } 483 484 _, err = globalDB.Exec( 485 "UPDATE submissions SET glicko_rating = ?, glicko_rd = ?, glicko_volatility = ? WHERE id = ?", 486 p2New.Rating, p2New.RD, p2New.Volatility, player2ID, 487 ) 488 return err 489} 490 491func hasMatchBetween(player1ID, player2ID int) (bool, error) { 492 var count int 493 err := globalDB.QueryRow( 494 `SELECT COUNT(*) FROM matches 495 WHERE is_valid = 1 496 AND ((player1_id = ? AND player2_id = ?) OR (player1_id = ? AND player2_id = ?))`, 497 player1ID, player2ID, player2ID, player1ID, 498 ).Scan(&count) 499 return count > 0, err 500} 501 502type MatchResult struct { 503 Player1Username string 504 Player2Username string 505 WinnerUsername string 506 AvgMoves int 507} 508 509func getAllMatches() ([]MatchResult, error) { 510 query := ` 511 SELECT 512 s1.username as player1, 513 s2.username as player2, 514 sw.username as winner, 515 m.player1_moves as avg_moves 516 FROM matches m 517 JOIN submissions s1 ON m.player1_id = s1.id 518 JOIN submissions s2 ON m.player2_id = s2.id 519 JOIN submissions sw ON m.winner_id = sw.id 520 WHERE s1.is_active = 1 AND s2.is_active = 1 AND m.is_valid = 1 521 ORDER BY m.timestamp DESC 522 ` 523 524 rows, err := globalDB.Query(query) 525 if err != nil { 526 return nil, err 527 } 528 defer rows.Close() 529 530 var matches []MatchResult 531 for rows.Next() { 532 var m MatchResult 533 err := rows.Scan(&m.Player1Username, &m.Player2Username, &m.WinnerUsername, &m.AvgMoves) 534 if err != nil { 535 return nil, err 536 } 537 matches = append(matches, m) 538 } 539 540 return matches, rows.Err() 541} 542 543// Tournament functions 544 545func getActiveTournament() (*Tournament, error) { 546 var t Tournament 547 var winnerID sql.NullInt64 548 err := globalDB.QueryRow( 549 "SELECT id, created_at, status, current_round, winner_id FROM tournaments WHERE status = 'active' ORDER BY id DESC LIMIT 1", 550 ).Scan(&t.ID, &t.CreatedAt, &t.Status, &t.CurrentRound, &winnerID) 551 552 if err == sql.ErrNoRows { 553 return nil, nil 554 } 555 if winnerID.Valid { 556 t.WinnerID = int(winnerID.Int64) 557 } 558 return &t, err 559} 560 561func getLatestTournament() (*Tournament, error) { 562 var t Tournament 563 var winnerID sql.NullInt64 564 err := globalDB.QueryRow( 565 "SELECT id, created_at, status, current_round, winner_id FROM tournaments ORDER BY id DESC LIMIT 1", 566 ).Scan(&t.ID, &t.CreatedAt, &t.Status, &t.CurrentRound, &winnerID) 567 568 if err == sql.ErrNoRows { 569 return nil, nil 570 } 571 if winnerID.Valid { 572 t.WinnerID = int(winnerID.Int64) 573 } 574 return &t, err 575} 576 577func createTournament() (*Tournament, error) { 578 result, err := globalDB.Exec("INSERT INTO tournaments (status, current_round) VALUES ('active', 1)") 579 if err != nil { 580 return nil, err 581 } 582 583 id, _ := result.LastInsertId() 584 return &Tournament{ 585 ID: int(id), 586 Status: "active", 587 CurrentRound: 1, 588 }, nil 589} 590 591func updateTournamentRound(tournamentID, round int) error { 592 _, err := globalDB.Exec("UPDATE tournaments SET current_round = ? WHERE id = ?", round, tournamentID) 593 return err 594} 595 596func completeTournament(tournamentID, winnerID int) error { 597 _, err := globalDB.Exec("UPDATE tournaments SET status = 'completed', winner_id = ? WHERE id = ?", winnerID, tournamentID) 598 return err 599} 600 601func addBracketMatch(tournamentID, round, position, player1ID, player2ID int) error { 602 _, err := globalDB.Exec( 603 "INSERT INTO bracket_matches (tournament_id, round, position, player1_id, player2_id, status) VALUES (?, ?, ?, ?, ?, 'pending')", 604 tournamentID, round, position, player1ID, player2ID, 605 ) 606 return err 607} 608 609func getPendingBracketMatches(tournamentID int) ([]BracketMatch, error) { 610 query := ` 611 SELECT 612 bm.id, bm.tournament_id, bm.round, bm.position, 613 bm.player1_id, bm.player2_id, bm.winner_id, 614 bm.player1_wins, bm.player2_wins, 615 bm.player1_moves, bm.player2_moves, bm.status, 616 s1.username as player1_name, s2.username as player2_name 617 FROM bracket_matches bm 618 JOIN submissions s1 ON bm.player1_id = s1.id 619 JOIN submissions s2 ON bm.player2_id = s2.id 620 WHERE bm.tournament_id = ? AND bm.status = 'pending' 621 ORDER BY bm.round, bm.position 622 ` 623 624 rows, err := globalDB.Query(query, tournamentID) 625 if err != nil { 626 return nil, err 627 } 628 defer rows.Close() 629 630 var matches []BracketMatch 631 for rows.Next() { 632 var m BracketMatch 633 var winnerID sql.NullInt64 634 var player1Moves, player2Moves sql.NullInt64 635 err := rows.Scan( 636 &m.ID, &m.TournamentID, &m.Round, &m.Position, 637 &m.Player1ID, &m.Player2ID, &winnerID, 638 &m.Player1Wins, &m.Player2Wins, 639 &player1Moves, &player2Moves, &m.Status, 640 &m.Player1Name, &m.Player2Name, 641 ) 642 if err != nil { 643 return nil, err 644 } 645 if winnerID.Valid { 646 m.WinnerID = int(winnerID.Int64) 647 } 648 if player1Moves.Valid { 649 m.Player1Moves = int(player1Moves.Int64) 650 } 651 if player2Moves.Valid { 652 m.Player2Moves = int(player2Moves.Int64) 653 } 654 matches = append(matches, m) 655 } 656 657 return matches, rows.Err() 658} 659 660func getAllBracketMatches(tournamentID int) ([]BracketMatch, error) { 661 query := ` 662 SELECT 663 bm.id, bm.tournament_id, bm.round, bm.position, 664 bm.player1_id, bm.player2_id, bm.winner_id, 665 bm.player1_wins, bm.player2_wins, 666 bm.player1_moves, bm.player2_moves, bm.status, 667 s1.username as player1_name, s2.username as player2_name 668 FROM bracket_matches bm 669 LEFT JOIN submissions s1 ON bm.player1_id = s1.id 670 LEFT JOIN submissions s2 ON bm.player2_id = s2.id 671 WHERE bm.tournament_id = ? 672 ORDER BY bm.round, bm.position 673 ` 674 675 rows, err := globalDB.Query(query, tournamentID) 676 if err != nil { 677 return nil, err 678 } 679 defer rows.Close() 680 681 var matches []BracketMatch 682 for rows.Next() { 683 var m BracketMatch 684 var player1Name, player2Name sql.NullString 685 var winnerID, player1Moves, player2Moves sql.NullInt64 686 err := rows.Scan( 687 &m.ID, &m.TournamentID, &m.Round, &m.Position, 688 &m.Player1ID, &m.Player2ID, &winnerID, 689 &m.Player1Wins, &m.Player2Wins, 690 &player1Moves, &player2Moves, &m.Status, 691 &player1Name, &player2Name, 692 ) 693 if err != nil { 694 return nil, err 695 } 696 if winnerID.Valid { 697 m.WinnerID = int(winnerID.Int64) 698 } 699 if player1Moves.Valid { 700 m.Player1Moves = int(player1Moves.Int64) 701 } 702 if player2Moves.Valid { 703 m.Player2Moves = int(player2Moves.Int64) 704 } 705 if player1Name.Valid { 706 m.Player1Name = player1Name.String 707 } 708 if player2Name.Valid { 709 m.Player2Name = player2Name.String 710 } 711 matches = append(matches, m) 712 } 713 714 return matches, rows.Err() 715} 716 717func updateBracketMatchResult(matchID, winnerID, player1Wins, player2Wins, player1Moves, player2Moves int) error { 718 _, err := globalDB.Exec( 719 `UPDATE bracket_matches 720 SET winner_id = ?, player1_wins = ?, player2_wins = ?, 721 player1_moves = ?, player2_moves = ?, status = 'completed' 722 WHERE id = ?`, 723 winnerID, player1Wins, player2Wins, player1Moves, player2Moves, matchID, 724 ) 725 return err 726} 727 728func isRoundComplete(tournamentID, round int) (bool, error) { 729 var pendingCount int 730 err := globalDB.QueryRow( 731 "SELECT COUNT(*) FROM bracket_matches WHERE tournament_id = ? AND round = ? AND status != 'completed'", 732 tournamentID, round, 733 ).Scan(&pendingCount) 734 735 return pendingCount == 0, err 736} 737