a geicko-2 based round robin ranking system designed to test c++ battleship submissions
battleship.dunkirk.sh
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}