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