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: add reasons why stuff failed

+56 -44
+18 -17
internal/runner/runner.go
··· 135 135 return nil 136 136 } 137 137 138 - func RunHeadToHead(player1, player2 storage.Submission, numGames int) (int, int, int) { 138 + func RunHeadToHead(player1, player2 storage.Submission, numGames int) (int, int, int, string) { 139 139 re := regexp.MustCompile(`memory_functions_(\w+)\.cpp`) 140 140 matches1 := re.FindStringSubmatch(player1.Filename) 141 141 matches2 := re.FindStringSubmatch(player2.Filename) 142 142 143 143 if len(matches1) < 2 || len(matches2) < 2 { 144 - return 0, 0, 0 144 + return 0, 0, 0, "Invalid filename format" 145 145 } 146 146 147 147 prefix1 := matches1[1] ··· 153 153 // Ensure both files exist in engine/src (copy from uploads if missing) 154 154 if _, err := os.Stat(cpp1Path); os.IsNotExist(err) { 155 155 log.Printf("Player1 file missing in engine/src, skipping: %s", cpp1Path) 156 - return 0, 0, 0 156 + return 0, 0, 0, fmt.Sprintf("File missing: %s", cpp1Path) 157 157 } 158 158 159 159 if _, err := os.Stat(cpp2Path); os.IsNotExist(err) { 160 160 log.Printf("Player2 file missing in engine/src, skipping: %s", cpp2Path) 161 - return 0, 0, 0 161 + return 0, 0, 0, fmt.Sprintf("Opponent file missing: %s", cpp2Path) 162 162 } 163 163 164 164 cpp1Content, err := os.ReadFile(cpp1Path) 165 165 if err != nil { 166 166 log.Printf("Failed to read %s: %v", cpp1Path, err) 167 - return 0, 0, 0 167 + return 0, 0, 0, fmt.Sprintf("Failed to read file: %v", err) 168 168 } 169 169 170 170 cpp2Content, err := os.ReadFile(cpp2Path) 171 171 if err != nil { 172 172 log.Printf("Failed to read %s: %v", cpp2Path, err) 173 - return 0, 0, 0 173 + return 0, 0, 0, fmt.Sprintf("Failed to read opponent file: %v", err) 174 174 } 175 175 176 176 suffix1, err := parseFunctionNames(string(cpp1Content)) 177 177 if err != nil { 178 178 log.Printf("Failed to parse function names for %s: %v", player1.Filename, err) 179 - return 0, 0, 0 179 + return 0, 0, 0, fmt.Sprintf("Could not find required function signatures (initMemory, smartMove, updateMemory)") 180 180 } 181 181 182 182 suffix2, err := parseFunctionNames(string(cpp2Content)) 183 183 if err != nil { 184 184 log.Printf("Failed to parse function names for %s: %v", player2.Filename, err) 185 - return 0, 0, 0 185 + return 0, 0, 0, fmt.Sprintf("Opponent file parse error: %v", err) 186 186 } 187 187 188 188 buildDir := filepath.Join(enginePath, "build") ··· 192 192 mainPath := filepath.Join(enginePath, "src", fmt.Sprintf("match_%s_vs_%s.cpp", prefix1, prefix2)) 193 193 if err := os.WriteFile(mainPath, []byte(mainContent), 0644); err != nil { 194 194 log.Printf("Failed to write match main: %v", err) 195 - return 0, 0, 0 195 + return 0, 0, 0, fmt.Sprintf("Failed to write match file: %v", err) 196 196 } 197 197 198 198 // Compile match binary in sandbox with 120 second timeout ··· 215 215 output, err := runSandboxed(context.Background(), "compile-match", compileArgs, 120) 216 216 if err != nil { 217 217 log.Printf("Failed to compile match binary (err=%v): %s", err, output) 218 - return 0, 0, 0 218 + return 0, 0, 0, fmt.Sprintf("Compilation error: %s", string(output)) 219 219 } 220 220 221 221 log.Printf("Match compilation output: %s", output) ··· 223 223 // Check if binary was actually created 224 224 if _, err := os.Stat(combinedBinary); os.IsNotExist(err) { 225 225 log.Printf("Match binary was not created at %s, compilation succeeded but no binary found", combinedBinary) 226 - return 0, 0, 0 226 + return 0, 0, 0, "Match binary not created after compilation" 227 227 } 228 228 229 229 // Run match in sandbox with 300 second timeout (1000 games should be ~60s, give headroom) ··· 231 231 output, err = runSandboxed(context.Background(), "run-match", runArgs, 300) 232 232 if err != nil { 233 233 log.Printf("Match execution failed: %v\n%s", err, output) 234 - return 0, 0, 0 234 + return 0, 0, 0, fmt.Sprintf("Runtime error: %s (possible crash, timeout, or infinite loop)", strings.TrimSpace(string(output))) 235 235 } 236 236 237 - return parseMatchOutput(string(output)) 237 + p1, p2, moves := parseMatchOutput(string(output)) 238 + return p1, p2, moves, "" 238 239 } 239 240 240 241 func RunRoundRobinMatches(newSub storage.Submission, uploadDir string, broadcastFunc func(string, int, int, time.Time, []string)) { ··· 308 309 queuedPlayers := storage.GetQueuedPlayerNames() 309 310 broadcastFunc(newSub.Username, matchNum, totalMatches, startTime, queuedPlayers) 310 311 311 - player1Wins, player2Wins, totalMoves := RunHeadToHead(newSub, opponent, 1000) 312 + player1Wins, player2Wins, totalMoves, errMsg := RunHeadToHead(newSub, opponent, 1000) 312 313 313 - // If match failed (returned 0-0-0), mark submission as compilation_failed 314 + // If match failed (returned 0-0-0), mark submission as match_failed with error message 314 315 if player1Wins == 0 && player2Wins == 0 && totalMoves == 0 { 315 - log.Printf("❌ Match failed for %s vs %s - marking as compilation_failed", newSub.Username, opponent.Username) 316 - storage.UpdateSubmissionStatus(newSub.ID, "compilation_failed") 316 + log.Printf("❌ Match execution failed for %s vs %s - marking as match_failed", newSub.Username, opponent.Username) 317 + storage.UpdateSubmissionStatusWithMessage(newSub.ID, "match_failed", errMsg) 317 318 return 318 319 } 319 320
+1 -1
internal/runner/worker.go
··· 61 61 62 62 if err := CompileSubmission(sub, uploadDir); err != nil { 63 63 log.Printf("❌ Compilation failed for %s: %v", sub.Username, err) 64 - storage.UpdateSubmissionStatus(sub.ID, "compilation_failed") 64 + storage.UpdateSubmissionStatusWithMessage(sub.ID, "compilation_failed", err.Error()) 65 65 notifyFunc() 66 66 continue 67 67 }
+2 -2
internal/server/web.go
··· 750 750 {{range $i, $e := .Entries}} 751 751 <tr{{if $e.IsPending}} class="pending"{{else if $e.IsBroken}} class="broken"{{end}}> 752 752 <td class="rank rank-{{add $i 1}}">{{if $e.IsBroken}}💥{{else if $e.IsPending}}⏳{{else if lt $i 3}}{{medal $i}}{{else}}{{add $i 1}}{{end}}</td> 753 - <td class="player-name"><a href="/user/{{$e.Username}}" style="color: inherit; text-decoration: none;">{{$e.Username}}{{if $e.IsPending}} <span style="font-size: 0.8em;">(pending)</span>{{else if $e.IsBroken}} <span style="font-size: 0.8em; color: #ef4444;">(compilation failed)</span>{{end}}</a></td> 753 + <td class="player-name"><a href="/user/{{$e.Username}}" style="color: inherit; text-decoration: none;">{{$e.Username}}{{if $e.IsPending}} <span style="font-size: 0.8em;">(pending)</span>{{else if $e.IsBroken}} <span style="font-size: 0.8em; color: #ef4444;" title="{{$e.FailureMessage}}">(failed)</span>{{end}}</a></td> 754 754 <td>{{if or $e.IsPending $e.IsBroken}}-{{else}}<strong>{{$e.Rating}}</strong> <span style="color: #94a3b8; font-size: 0.85em;">±{{$e.RD}}</span>{{end}}</td> 755 755 <td>{{if or $e.IsPending $e.IsBroken}}-{{else}}{{$e.Wins}}{{end}}</td> 756 756 <td>{{if or $e.IsPending $e.IsBroken}}-{{else}}{{$e.Losses}}{{end}}</td> 757 757 <td>{{if or $e.IsPending $e.IsBroken}}-{{else}}<span class="win-rate {{winRateClass $e}}">{{winRate $e}}%</span>{{end}}</td> 758 758 <td>{{if or $e.IsPending $e.IsBroken}}-{{else}}{{printf "%.1f" $e.AvgMoves}}{{end}}</td> 759 - <td style="color: #64748b;">{{if $e.IsPending}}Waiting...{{else if $e.IsBroken}}Failed{{else}}{{$e.LastPlayed.Format "Jan 2, 3:04 PM"}}{{end}}</td> 759 + <td style="color: #64748b;">{{if $e.IsPending}}Waiting...{{else if $e.IsBroken}}<span title="{{$e.FailureMessage}}">Failed</span>{{else}}{{$e.LastPlayed.Format "Jan 2, 3:04 PM"}}{{end}}</td> 760 760 </tr> 761 761 {{end}} 762 762 {{else}}
+35 -24
internal/storage/database.go
··· 11 11 var DB *sql.DB 12 12 13 13 type LeaderboardEntry struct { 14 - Username string 15 - Wins int 16 - Losses int 17 - WinPct float64 18 - Rating int 19 - RD int 20 - AvgMoves float64 21 - Stage string 22 - LastPlayed time.Time 23 - IsPending bool 24 - IsBroken bool 14 + Username string 15 + Wins int 16 + Losses int 17 + WinPct float64 18 + Rating int 19 + RD int 20 + AvgMoves float64 21 + Stage string 22 + LastPlayed time.Time 23 + IsPending bool 24 + IsBroken bool 25 + FailureMessage string 25 26 } 26 27 27 28 type Submission struct { 28 - ID int 29 - Username string 30 - Filename string 31 - UploadTime time.Time 32 - Status string 33 - IsActive bool 29 + ID int 30 + Username string 31 + Filename string 32 + UploadTime time.Time 33 + Status string 34 + IsActive bool 35 + FailureMessage string 34 36 } 35 37 36 38 type SubmissionWithStats struct { ··· 112 114 is_active BOOLEAN DEFAULT 1, 113 115 glicko_rating REAL DEFAULT 1500.0, 114 116 glicko_rd REAL DEFAULT 350.0, 115 - glicko_volatility REAL DEFAULT 0.06 117 + glicko_volatility REAL DEFAULT 0.06, 118 + failure_message TEXT 116 119 ); 117 120 118 121 CREATE TABLE IF NOT EXISTS tournaments ( ··· 204 207 AVG(CASE WHEN m.player1_id = s.id THEN m.player1_moves ELSE m.player2_moves END) as avg_moves, 205 208 MAX(m.timestamp) as last_played, 206 209 0 as is_pending, 207 - 0 as is_broken 210 + 0 as is_broken, 211 + '' as failure_message 208 212 FROM submissions s 209 213 LEFT JOIN matches m ON (m.player1_id = s.id OR m.player2_id = s.id) AND m.is_valid = 1 210 - WHERE s.is_active = 1 AND s.status NOT IN ('compilation_failed') 214 + WHERE s.is_active = 1 AND s.status NOT IN ('compilation_failed', 'match_failed') 211 215 GROUP BY s.username, s.glicko_rating, s.glicko_rd 212 216 HAVING COUNT(m.id) > 0 213 217 ··· 222 226 999.0 as avg_moves, 223 227 s.upload_time as last_played, 224 228 1 as is_pending, 225 - 0 as is_broken 229 + 0 as is_broken, 230 + '' as failure_message 226 231 FROM submissions s 227 232 LEFT JOIN matches m ON (m.player1_id = s.id OR m.player2_id = s.id) AND m.is_valid = 1 228 233 WHERE s.is_active = 1 AND s.status IN ('pending', 'testing', 'completed') ··· 240 245 999.0 as avg_moves, 241 246 s.upload_time as last_played, 242 247 0 as is_pending, 243 - 1 as is_broken 248 + 1 as is_broken, 249 + COALESCE(s.failure_message, '') as failure_message 244 250 FROM submissions s 245 - WHERE s.is_active = 1 AND s.status = 'compilation_failed' 251 + WHERE s.is_active = 1 AND s.status IN ('compilation_failed', 'match_failed') 246 252 247 253 ORDER BY is_broken ASC, is_pending ASC, rating DESC, total_wins DESC, avg_moves ASC 248 254 LIMIT ? ··· 260 266 var lastPlayed string 261 267 var rating, rd float64 262 268 var isPending, isBroken int 263 - err := rows.Scan(&e.Username, &rating, &rd, &e.Wins, &e.Losses, &e.AvgMoves, &lastPlayed, &isPending, &isBroken) 269 + err := rows.Scan(&e.Username, &rating, &rd, &e.Wins, &e.Losses, &e.AvgMoves, &lastPlayed, &isPending, &isBroken, &e.FailureMessage) 264 270 if err != nil { 265 271 return nil, err 266 272 } ··· 324 330 325 331 func UpdateSubmissionStatus(id int, status string) error { 326 332 _, err := DB.Exec("UPDATE submissions SET status = ? WHERE id = ?", status, id) 333 + return err 334 + } 335 + 336 + func UpdateSubmissionStatusWithMessage(id int, status string, message string) error { 337 + _, err := DB.Exec("UPDATE submissions SET status = ?, failure_message = ? WHERE id = ?", status, message, id) 327 338 return err 328 339 } 329 340