a geicko-2 based round robin ranking system designed to test c++ battleship submissions battleship.dunkirk.sh
at main 12 kB view raw
1package storage 2 3import ( 4 "database/sql" 5 "fmt" 6 "log" 7 "math" 8 "sort" 9) 10 11func GetActiveTournament() (*Tournament, error) { 12 var t Tournament 13 var winnerID sql.NullInt64 14 err := DB.QueryRow( 15 "SELECT id, created_at, status, current_round, winner_id FROM tournaments WHERE status = 'active' ORDER BY id DESC LIMIT 1", 16 ).Scan(&t.ID, &t.CreatedAt, &t.Status, &t.CurrentRound, &winnerID) 17 18 if err == sql.ErrNoRows { 19 return nil, nil 20 } 21 if winnerID.Valid { 22 t.WinnerID = int(winnerID.Int64) 23 } 24 return &t, err 25} 26 27func GetLatestTournament() (*Tournament, error) { 28 var t Tournament 29 var winnerID sql.NullInt64 30 err := DB.QueryRow( 31 "SELECT id, created_at, status, current_round, winner_id FROM tournaments ORDER BY id DESC LIMIT 1", 32 ).Scan(&t.ID, &t.CreatedAt, &t.Status, &t.CurrentRound, &winnerID) 33 34 if err == sql.ErrNoRows { 35 return nil, nil 36 } 37 if winnerID.Valid { 38 t.WinnerID = int(winnerID.Int64) 39 } 40 return &t, err 41} 42 43func CreateTournament() (*Tournament, error) { 44 result, err := DB.Exec("INSERT INTO tournaments (status, current_round) VALUES ('active', 1)") 45 if err != nil { 46 return nil, err 47 } 48 49 id, _ := result.LastInsertId() 50 return &Tournament{ 51 ID: int(id), 52 Status: "active", 53 CurrentRound: 1, 54 }, nil 55} 56 57func UpdateTournamentRound(tournamentID, round int) error { 58 _, err := DB.Exec("UPDATE tournaments SET current_round = ? WHERE id = ?", round, tournamentID) 59 return err 60} 61 62func CompleteTournament(tournamentID, winnerID int) error { 63 _, err := DB.Exec("UPDATE tournaments SET status = 'completed', winner_id = ? WHERE id = ?", winnerID, tournamentID) 64 return err 65} 66 67func AddBracketMatch(tournamentID, round, position, player1ID, player2ID int) error { 68 _, err := DB.Exec( 69 "INSERT INTO bracket_matches (tournament_id, round, position, player1_id, player2_id, status) VALUES (?, ?, ?, ?, ?, 'pending')", 70 tournamentID, round, position, player1ID, player2ID, 71 ) 72 return err 73} 74 75func GetPendingBracketMatches(tournamentID int) ([]BracketMatch, error) { 76 query := ` 77 SELECT 78 bm.id, bm.tournament_id, bm.round, bm.position, 79 bm.player1_id, bm.player2_id, bm.winner_id, 80 bm.player1_wins, bm.player2_wins, 81 bm.player1_moves, bm.player2_moves, bm.status, 82 s1.username as player1_name, s2.username as player2_name 83 FROM bracket_matches bm 84 JOIN submissions s1 ON bm.player1_id = s1.id 85 JOIN submissions s2 ON bm.player2_id = s2.id 86 WHERE bm.tournament_id = ? AND bm.status = 'pending' 87 ORDER BY bm.round, bm.position 88 ` 89 90 rows, err := DB.Query(query, tournamentID) 91 if err != nil { 92 return nil, err 93 } 94 defer rows.Close() 95 96 var matches []BracketMatch 97 for rows.Next() { 98 var m BracketMatch 99 var winnerID sql.NullInt64 100 var player1Moves, player2Moves sql.NullInt64 101 err := rows.Scan( 102 &m.ID, &m.TournamentID, &m.Round, &m.Position, 103 &m.Player1ID, &m.Player2ID, &winnerID, 104 &m.Player1Wins, &m.Player2Wins, 105 &player1Moves, &player2Moves, &m.Status, 106 &m.Player1Name, &m.Player2Name, 107 ) 108 if err != nil { 109 return nil, err 110 } 111 if winnerID.Valid { 112 m.WinnerID = int(winnerID.Int64) 113 } 114 if player1Moves.Valid { 115 m.Player1Moves = int(player1Moves.Int64) 116 } 117 if player2Moves.Valid { 118 m.Player2Moves = int(player2Moves.Int64) 119 } 120 matches = append(matches, m) 121 } 122 123 return matches, rows.Err() 124} 125 126func GetAllBracketMatches(tournamentID int) ([]BracketMatch, error) { 127 query := ` 128 SELECT 129 bm.id, bm.tournament_id, bm.round, bm.position, 130 bm.player1_id, bm.player2_id, bm.winner_id, 131 bm.player1_wins, bm.player2_wins, 132 bm.player1_moves, bm.player2_moves, bm.status, 133 s1.username as player1_name, s2.username as player2_name 134 FROM bracket_matches bm 135 LEFT JOIN submissions s1 ON bm.player1_id = s1.id 136 LEFT JOIN submissions s2 ON bm.player2_id = s2.id 137 WHERE bm.tournament_id = ? 138 ORDER BY bm.round, bm.position 139 ` 140 141 rows, err := DB.Query(query, tournamentID) 142 if err != nil { 143 return nil, err 144 } 145 defer rows.Close() 146 147 var matches []BracketMatch 148 for rows.Next() { 149 var m BracketMatch 150 var player1Name, player2Name sql.NullString 151 var winnerID, player1Moves, player2Moves sql.NullInt64 152 err := rows.Scan( 153 &m.ID, &m.TournamentID, &m.Round, &m.Position, 154 &m.Player1ID, &m.Player2ID, &winnerID, 155 &m.Player1Wins, &m.Player2Wins, 156 &player1Moves, &player2Moves, &m.Status, 157 &player1Name, &player2Name, 158 ) 159 if err != nil { 160 return nil, err 161 } 162 if winnerID.Valid { 163 m.WinnerID = int(winnerID.Int64) 164 } 165 if player1Moves.Valid { 166 m.Player1Moves = int(player1Moves.Int64) 167 } 168 if player2Moves.Valid { 169 m.Player2Moves = int(player2Moves.Int64) 170 } 171 if player1Name.Valid { 172 m.Player1Name = player1Name.String 173 } 174 if player2Name.Valid { 175 m.Player2Name = player2Name.String 176 } 177 matches = append(matches, m) 178 } 179 180 return matches, rows.Err() 181} 182 183func UpdateBracketMatchResult(matchID, winnerID, player1Wins, player2Wins, player1Moves, player2Moves int) error { 184 _, err := DB.Exec( 185 `UPDATE bracket_matches 186 SET winner_id = ?, player1_wins = ?, player2_wins = ?, 187 player1_moves = ?, player2_moves = ?, status = 'completed' 188 WHERE id = ?`, 189 winnerID, player1Wins, player2Wins, player1Moves, player2Moves, matchID, 190 ) 191 return err 192} 193 194func IsRoundComplete(tournamentID, round int) (bool, error) { 195 var pendingCount int 196 err := DB.QueryRow( 197 "SELECT COUNT(*) FROM bracket_matches WHERE tournament_id = ? AND round = ? AND status != 'completed'", 198 tournamentID, round, 199 ).Scan(&pendingCount) 200 201 return pendingCount == 0, err 202} 203 204func SeedSubmissions(submissions []Submission) []Submission { 205 type seedEntry struct { 206 submission Submission 207 avgMoves float64 208 } 209 210 var entries []seedEntry 211 for _, sub := range submissions { 212 var avgMoves float64 213 err := DB.QueryRow(` 214 SELECT AVG(CASE 215 WHEN m.player1_id = ? THEN m.player1_moves 216 ELSE m.player2_moves 217 END) 218 FROM matches m 219 WHERE m.player1_id = ? OR m.player2_id = ? 220 `, sub.ID, sub.ID, sub.ID).Scan(&avgMoves) 221 222 if err != nil || avgMoves == 0 { 223 avgMoves = 100 224 } 225 226 entries = append(entries, seedEntry{sub, avgMoves}) 227 } 228 229 sort.Slice(entries, func(i, j int) bool { 230 return entries[i].avgMoves < entries[j].avgMoves 231 }) 232 233 var seeded []Submission 234 for _, entry := range entries { 235 seeded = append(seeded, entry.submission) 236 } 237 238 return seeded 239} 240 241func CreateBracket(tournament *Tournament) error { 242 submissions, err := GetActiveSubmissions() 243 if err != nil { 244 return err 245 } 246 247 if len(submissions) < 2 { 248 return fmt.Errorf("need at least 2 players for tournament") 249 } 250 251 seeded := SeedSubmissions(submissions) 252 253 log.Printf("Tournament %d: Seeded %d players", tournament.ID, len(seeded)) 254 for i, sub := range seeded { 255 log.Printf(" Seed %d: %s", i+1, sub.Username) 256 } 257 258 numPlayers := len(seeded) 259 bracketSize := int(math.Pow(2, math.Ceil(math.Log2(float64(numPlayers))))) 260 261 log.Printf("Tournament %d: Bracket size=%d, players=%d", 262 tournament.ID, bracketSize, numPlayers) 263 264 numFirstRoundMatches := bracketSize / 2 265 266 for matchPos := 0; matchPos < numFirstRoundMatches; matchPos++ { 267 topSeedIdx := matchPos 268 bottomSeedIdx := numPlayers - 1 - matchPos 269 270 if topSeedIdx >= bottomSeedIdx && topSeedIdx < numPlayers && bottomSeedIdx >= 0 { 271 if topSeedIdx == bottomSeedIdx { 272 player1ID := seeded[topSeedIdx].ID 273 player1Name := seeded[topSeedIdx].Username 274 275 err = AddBracketMatch(tournament.ID, 1, matchPos, player1ID, 0) 276 if err != nil { 277 return err 278 } 279 280 DB.Exec(` 281 UPDATE bracket_matches 282 SET winner_id = ?, status = 'completed', 283 player1_wins = 0, player2_wins = 0, 284 player1_moves = 0, player2_moves = 0 285 WHERE tournament_id = ? AND round = 1 AND position = ? 286 `, player1ID, tournament.ID, matchPos) 287 288 log.Printf(" Match %d: %s vs BYE (auto-advance)", matchPos, player1Name) 289 } 290 break 291 } 292 293 var player1ID, player2ID int 294 var player1Name, player2Name string 295 296 if topSeedIdx < numPlayers { 297 player1ID = seeded[topSeedIdx].ID 298 player1Name = seeded[topSeedIdx].Username 299 } else { 300 player1ID = 0 301 player1Name = "BYE" 302 } 303 304 if bottomSeedIdx >= 0 && bottomSeedIdx < numPlayers { 305 player2ID = seeded[bottomSeedIdx].ID 306 player2Name = seeded[bottomSeedIdx].Username 307 } else { 308 player2ID = 0 309 player2Name = "BYE" 310 } 311 312 if player1ID == 0 && player2ID == 0 { 313 continue 314 } 315 316 err = AddBracketMatch(tournament.ID, 1, matchPos, player1ID, player2ID) 317 if err != nil { 318 return err 319 } 320 321 if player1ID == 0 || player2ID == 0 { 322 winnerID := player1ID 323 if player1ID == 0 { 324 winnerID = player2ID 325 } 326 327 DB.Exec(` 328 UPDATE bracket_matches 329 SET winner_id = ?, status = 'completed', 330 player1_wins = 0, player2_wins = 0, 331 player1_moves = 0, player2_moves = 0 332 WHERE tournament_id = ? AND round = 1 AND position = ? 333 `, winnerID, tournament.ID, matchPos) 334 335 log.Printf(" Match %d: %s vs %s (BYE - winner: %s)", 336 matchPos, player1Name, player2Name, 337 map[bool]string{true: player1Name, false: player2Name}[player1ID == winnerID]) 338 } else { 339 log.Printf(" Match %d: %s (seed %d) vs %s (seed %d)", 340 matchPos, player1Name, topSeedIdx+1, player2Name, bottomSeedIdx+1) 341 } 342 } 343 344 return nil 345} 346 347func AdvanceWinners(tournamentID, currentRound int) error { 348 query := ` 349 SELECT id, position, winner_id 350 FROM bracket_matches 351 WHERE tournament_id = ? AND round = ? AND status = 'completed' 352 ORDER BY position 353 ` 354 355 rows, err := DB.Query(query, tournamentID, currentRound) 356 if err != nil { 357 return err 358 } 359 defer rows.Close() 360 361 var winners []struct { 362 matchID int 363 position int 364 winnerID int 365 } 366 367 for rows.Next() { 368 var w struct { 369 matchID int 370 position int 371 winnerID int 372 } 373 rows.Scan(&w.matchID, &w.position, &w.winnerID) 374 winners = append(winners, w) 375 } 376 377 if len(winners) == 1 { 378 log.Printf("Tournament %d complete! Winner: ID %d", tournamentID, winners[0].winnerID) 379 return CompleteTournament(tournamentID, winners[0].winnerID) 380 } 381 382 nextRound := currentRound + 1 383 log.Printf("Advancing to round %d with %d winners", nextRound, len(winners)) 384 385 for i := 0; i < len(winners); i += 2 { 386 if i+1 >= len(winners) { 387 log.Printf(" Round %d Match %d: BYE (winner: ID %d)", nextRound, i/2, winners[i].winnerID) 388 err = AddBracketMatch(tournamentID, nextRound, i/2, winners[i].winnerID, 0) 389 if err != nil { 390 return err 391 } 392 DB.Exec(` 393 UPDATE bracket_matches 394 SET winner_id = ?, status = 'completed', 395 player1_wins = 0, player2_wins = 0, 396 player1_moves = 0, player2_moves = 0 397 WHERE tournament_id = ? AND round = ? AND position = ? 398 `, winners[i].winnerID, tournamentID, nextRound, i/2) 399 } else { 400 log.Printf(" Round %d Match %d: ID %d vs ID %d", nextRound, i/2, winners[i].winnerID, winners[i+1].winnerID) 401 err = AddBracketMatch(tournamentID, nextRound, i/2, winners[i].winnerID, winners[i+1].winnerID) 402 if err != nil { 403 return err 404 } 405 } 406 } 407 408 return UpdateTournamentRound(tournamentID, nextRound) 409} 410 411func EnsureTournamentExists() (*Tournament, error) { 412 tournament, err := GetActiveTournament() 413 if err != nil { 414 return nil, err 415 } 416 417 if tournament != nil { 418 return tournament, nil 419 } 420 421 latestTournament, err := GetLatestTournament() 422 if err != nil { 423 return nil, err 424 } 425 426 submissions, err := GetActiveSubmissions() 427 if err != nil { 428 return nil, err 429 } 430 431 if len(submissions) < 2 { 432 log.Printf("Not enough players for tournament (%d/2), waiting...", len(submissions)) 433 return nil, fmt.Errorf("need at least 2 players") 434 } 435 436 if latestTournament != nil { 437 hasNewSubmission := false 438 for _, sub := range submissions { 439 if sub.UploadTime.After(latestTournament.CreatedAt) { 440 hasNewSubmission = true 441 break 442 } 443 } 444 445 if !hasNewSubmission { 446 log.Printf("No new submissions since last tournament, not creating new tournament") 447 return nil, fmt.Errorf("no new submissions") 448 } 449 } 450 451 log.Printf("Creating tournament with %d players...", len(submissions)) 452 tournament, err = CreateTournament() 453 if err != nil { 454 return nil, err 455 } 456 457 err = CreateBracket(tournament) 458 if err != nil { 459 return nil, err 460 } 461 log.Printf("Created tournament %d with bracket", tournament.ID) 462 463 return tournament, nil 464}