a geicko-2 based round robin ranking system designed to test c++ battleship submissions battleship.dunkirk.sh
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: optimize matches and use win percentage to rank

+71 -42
+40 -9
database.go
··· 10 10 var globalDB *sql.DB 11 11 12 12 type LeaderboardEntry struct { 13 - Username string 14 - Wins int 15 - Losses int 16 - AvgMoves float64 17 - Stage string 13 + Username string 14 + Wins int 15 + Losses int 16 + WinPct float64 17 + AvgMoves float64 18 + Stage string 18 19 LastPlayed time.Time 19 20 } 20 21 ··· 105 106 player2_wins INTEGER DEFAULT 0, 106 107 player1_moves INTEGER, 107 108 player2_moves INTEGER, 109 + is_valid BOOLEAN DEFAULT 1, 108 110 timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 109 111 FOREIGN KEY (player1_id) REFERENCES submissions(id), 110 112 FOREIGN KEY (player2_id) REFERENCES submissions(id), ··· 116 118 CREATE INDEX IF NOT EXISTS idx_tournaments_status ON tournaments(status); 117 119 CREATE INDEX IF NOT EXISTS idx_matches_player1 ON matches(player1_id); 118 120 CREATE INDEX IF NOT EXISTS idx_matches_player2 ON matches(player2_id); 121 + CREATE INDEX IF NOT EXISTS idx_matches_valid ON matches(is_valid); 119 122 CREATE INDEX IF NOT EXISTS idx_submissions_username ON submissions(username); 120 123 CREATE INDEX IF NOT EXISTS idx_submissions_status ON submissions(status); 121 124 CREATE INDEX IF NOT EXISTS idx_submissions_active ON submissions(is_active); 125 + CREATE UNIQUE INDEX IF NOT EXISTS idx_matches_unique_pair ON matches(player1_id, player2_id, is_valid) WHERE is_valid = 1; 122 126 ` 123 127 124 128 _, err = db.Exec(schema) ··· 134 138 AVG(CASE WHEN m.player1_id = s.id THEN m.player1_moves ELSE m.player2_moves END) as avg_moves, 135 139 MAX(m.timestamp) as last_played 136 140 FROM submissions s 137 - LEFT JOIN matches m ON (m.player1_id = s.id OR m.player2_id = s.id) 141 + LEFT JOIN matches m ON (m.player1_id = s.id OR m.player2_id = s.id) AND m.is_valid = 1 138 142 WHERE s.is_active = 1 139 143 GROUP BY s.username 140 144 HAVING COUNT(m.id) > 0 141 - ORDER BY total_wins DESC, total_losses ASC, avg_moves ASC 145 + ORDER BY (CAST(total_wins AS REAL) / (total_wins + total_losses)) DESC, avg_moves ASC 142 146 LIMIT ? 143 147 ` 144 148 ··· 157 161 return nil, err 158 162 } 159 163 164 + // Calculate win percentage 165 + totalGames := e.Wins + e.Losses 166 + if totalGames > 0 { 167 + e.WinPct = float64(e.Wins) / float64(totalGames) * 100.0 168 + } 169 + 160 170 // Parse the timestamp string 161 171 e.LastPlayed, _ = time.Parse("2006-01-02 15:04:05", lastPlayed) 162 172 ··· 167 177 } 168 178 169 179 func addSubmission(username, filename string) (int64, error) { 170 - // Mark old submission as inactive 180 + // Invalidate all matches involving this user's submissions 171 181 _, err := globalDB.Exec( 182 + `UPDATE matches SET is_valid = 0 183 + WHERE player1_id IN (SELECT id FROM submissions WHERE username = ?) 184 + OR player2_id IN (SELECT id FROM submissions WHERE username = ?)`, 185 + username, username, 186 + ) 187 + if err != nil { 188 + return 0, err 189 + } 190 + 191 + // Mark old submission as inactive 192 + _, err = globalDB.Exec( 172 193 "UPDATE submissions SET is_active = 0 WHERE username = ?", 173 194 username, 174 195 ) ··· 267 288 return submissions, rows.Err() 268 289 } 269 290 291 + func hasMatchBetween(player1ID, player2ID int) (bool, error) { 292 + var count int 293 + err := globalDB.QueryRow( 294 + `SELECT COUNT(*) FROM matches 295 + WHERE is_valid = 1 296 + AND ((player1_id = ? AND player2_id = ?) OR (player1_id = ? AND player2_id = ?))`, 297 + player1ID, player2ID, player2ID, player1ID, 298 + ).Scan(&count) 299 + return count > 0, err 300 + } 270 301 271 302 type MatchResult struct { 272 303 Player1Username string ··· 286 317 JOIN submissions s1 ON m.player1_id = s1.id 287 318 JOIN submissions s2 ON m.player2_id = s2.id 288 319 JOIN submissions sw ON m.winner_id = sw.id 289 - WHERE s1.is_active = 1 AND s2.is_active = 1 320 + WHERE s1.is_active = 1 AND s2.is_active = 1 AND m.is_valid = 1 290 321 ORDER BY m.timestamp DESC 291 322 ` 292 323
+1 -7
model.go
··· 193 193 "Rank", "User", "Wins", "Losses", "Win Rate", "Avg Moves")) 194 194 195 195 for i, entry := range entries { 196 - winRate := 0.0 197 - total := entry.Wins + entry.Losses 198 - if total > 0 { 199 - winRate = float64(entry.Wins) / float64(total) * 100 200 - } 201 - 202 196 rank := fmt.Sprintf("#%d", i+1) 203 197 204 198 // Apply color only to the rank ··· 215 209 216 210 // Format line with proper spacing 217 211 b.WriteString(fmt.Sprintf("%-4s %-20s %8d %8d %9.2f%% %9.1f\n", 218 - coloredRank, entry.Username, entry.Wins, entry.Losses, winRate, entry.AvgMoves)) 212 + coloredRank, entry.Username, entry.Wins, entry.Losses, entry.WinPct, entry.AvgMoves)) 219 213 } 220 214 221 215 return b.String()
+25 -10
runner.go
··· 148 148 return 149 149 } 150 150 151 - totalMatches := len(activeSubmissions) - 1 // Exclude self 151 + // Filter to only opponents we haven't played yet 152 + var unplayedOpponents []Submission 153 + for _, opponent := range activeSubmissions { 154 + if opponent.ID == newSub.ID { 155 + continue 156 + } 157 + 158 + // Check if match already exists 159 + hasMatch, err := hasMatchBetween(newSub.ID, opponent.ID) 160 + if err != nil { 161 + log.Printf("Error checking match history: %v", err) 162 + continue 163 + } 164 + 165 + if !hasMatch { 166 + unplayedOpponents = append(unplayedOpponents, opponent) 167 + } 168 + } 169 + 170 + totalMatches := len(unplayedOpponents) 152 171 if totalMatches <= 0 { 153 - log.Printf("No opponents for %s, skipping matches", newSub.Username) 172 + log.Printf("No new opponents for %s, all matches already played", newSub.Username) 154 173 return 155 174 } 156 175 157 - log.Printf("Starting round-robin for %s against %d opponents", newSub.Username, totalMatches) 176 + log.Printf("Starting round-robin for %s against %d new opponents", newSub.Username, totalMatches) 158 177 matchNum := 0 159 178 160 - // Run matches against all other submissions 161 - for _, opponent := range activeSubmissions { 162 - if opponent.ID == newSub.ID { 163 - continue 164 - } 165 - 179 + // Run matches against unplayed opponents only 180 + for _, opponent := range unplayedOpponents { 166 181 matchNum++ 167 182 log.Printf("[%d/%d] Running match: %s vs %s (1000 games)", matchNum, totalMatches, newSub.Username, opponent.Username) 168 183 ··· 199 214 } 200 215 } 201 216 202 - log.Printf("Round-robin complete for %s (%d matches)", newSub.Username, totalMatches) 217 + log.Printf("Round-robin complete for %s (%d new matches)", newSub.Username, totalMatches) 203 218 } 204 219 205 220 func runHeadToHead(player1, player2 Submission, numGames int) (int, int, int) {
+5 -16
web.go
··· 298 298 299 299 tbody.innerHTML = entries.map((e, i) => { 300 300 const rank = i + 1; 301 - const total = e.Wins + e.Losses; 302 - const winRate = total === 0 ? 0 : ((e.Wins / total) * 100).toFixed(1); 303 - const winRateClass = winRate >= 60 ? 'win-rate-high' : winRate >= 40 ? 'win-rate-med' : 'win-rate-low'; 301 + const winRate = e.WinPct.toFixed(1); 302 + const winRateClass = e.WinPct >= 60 ? 'win-rate-high' : e.WinPct >= 40 ? 'win-rate-med' : 'win-rate-low'; 304 303 const medals = ['🥇', '🥈', '🥉']; 305 304 const medal = medals[i] || rank; 306 305 const lastPlayed = new Date(e.LastPlayed).toLocaleString('en-US', { ··· 422 421 return "" 423 422 }, 424 423 "winRate": func(e LeaderboardEntry) string { 425 - total := e.Wins + e.Losses 426 - if total == 0 { 427 - return "0.0" 428 - } 429 - rate := float64(e.Wins) / float64(total) * 100 430 - return formatFloat(rate, 1) 424 + return formatFloat(e.WinPct, 1) 431 425 }, 432 426 "winRateClass": func(e LeaderboardEntry) string { 433 - total := e.Wins + e.Losses 434 - if total == 0 { 435 - return "win-rate-low" 436 - } 437 - rate := float64(e.Wins) / float64(total) * 100 438 - if rate >= 60 { 427 + if e.WinPct >= 60 { 439 428 return "win-rate-high" 440 - } else if rate >= 40 { 429 + } else if e.WinPct >= 40 { 441 430 return "win-rate-med" 442 431 } 443 432 return "win-rate-low"