a geicko-2 based round robin ranking system designed to test c++ battleship submissions battleship.dunkirk.sh
1package main 2 3import ( 4 "fmt" 5 "log" 6 "os" 7 "os/exec" 8 "path/filepath" 9 "regexp" 10 "strconv" 11 "strings" 12) 13 14const enginePath = "./battleship-engine" 15 16func processSubmissions() error { 17 submissions, err := getPendingSubmissions() 18 if err != nil { 19 return err 20 } 21 22 for _, sub := range submissions { 23 log.Printf("Starting compilation for submission %d: %s by %s", sub.ID, sub.Filename, sub.Username) 24 25 if err := compileSubmission(sub); err != nil { 26 log.Printf("Submission %d failed compilation: %v", sub.ID, err) 27 updateSubmissionStatus(sub.ID, "failed") 28 continue 29 } 30 31 log.Printf("Submission %d compiled successfully: %s by %s", sub.ID, sub.Filename, sub.Username) 32 updateSubmissionStatus(sub.ID, "completed") 33 34 // Run round-robin matches 35 log.Printf("Starting round-robin matches for submission %d", sub.ID) 36 runRoundRobinMatches(sub) 37 } 38 39 return nil 40} 41 42func generateHeader(filename, prefix string) string { 43 guard := strings.ToUpper(strings.Replace(filename, ".", "_", -1)) 44 45 // Capitalize first letter of prefix for function names 46 functionSuffix := strings.ToUpper(prefix[0:1]) + prefix[1:] 47 48 return fmt.Sprintf(`#ifndef %s 49#define %s 50 51#include "memory.h" 52#include <string> 53 54void initMemory%s(ComputerMemory &memory); 55std::string smartMove%s(const ComputerMemory &memory); 56void updateMemory%s(int row, int col, int result, ComputerMemory &memory); 57 58#endif 59`, guard, guard, functionSuffix, functionSuffix, functionSuffix) 60} 61 62func parseFunctionNames(cppContent string) (string, error) { 63 // Look for the initMemory function to extract the suffix 64 re := regexp.MustCompile(`void\s+initMemory(\w+)\s*\(`) 65 matches := re.FindStringSubmatch(cppContent) 66 if len(matches) < 2 { 67 return "", fmt.Errorf("could not find initMemory function") 68 } 69 return matches[1], nil 70} 71 72func compileSubmission(sub Submission) error { 73 updateSubmissionStatus(sub.ID, "testing") 74 75 // Extract prefix from filename (memory_functions_XXXXX.cpp -> XXXXX) 76 re := regexp.MustCompile(`memory_functions_(\w+)\.cpp`) 77 matches := re.FindStringSubmatch(sub.Filename) 78 if len(matches) < 2 { 79 return fmt.Errorf("invalid filename format") 80 } 81 prefix := matches[1] 82 83 // Create temporary build directory 84 buildDir := filepath.Join(enginePath, "build") 85 os.MkdirAll(buildDir, 0755) 86 87 // Copy submission to engine 88 srcPath := filepath.Join(uploadDir, sub.Username, sub.Filename) 89 dstPath := filepath.Join(enginePath, "src", sub.Filename) 90 91 log.Printf("Copying %s to %s", srcPath, dstPath) 92 input, err := os.ReadFile(srcPath) 93 if err != nil { 94 return err 95 } 96 if err := os.WriteFile(dstPath, input, 0644); err != nil { 97 return err 98 } 99 100 // Parse function names from the cpp file 101 functionSuffix, err := parseFunctionNames(string(input)) 102 if err != nil { 103 return fmt.Errorf("failed to parse function names: %v", err) 104 } 105 106 log.Printf("Detected function suffix: %s", functionSuffix) 107 108 // Generate header file with parsed function names 109 headerFilename := fmt.Sprintf("memory_functions_%s.h", prefix) 110 headerPath := filepath.Join(enginePath, "src", headerFilename) 111 headerContent := generateHeader(headerFilename, functionSuffix) 112 if err := os.WriteFile(headerPath, []byte(headerContent), 0644); err != nil { 113 return err 114 } 115 116 log.Printf("Compiling submission %d for %s", sub.ID, prefix) 117 118 // Compile check only (no linking) to validate syntax 119 cmd := exec.Command("g++", "-std=c++11", "-c", "-O3", 120 "-I", filepath.Join(enginePath, "src"), 121 "-o", filepath.Join(buildDir, "ai_"+prefix+".o"), 122 filepath.Join(enginePath, "src", sub.Filename), 123 ) 124 output, err := cmd.CombinedOutput() 125 if err != nil { 126 return fmt.Errorf("compilation failed: %s", output) 127 } 128 129 return nil 130} 131 132 133 134func getSubmissionByID(id int) (Submission, error) { 135 var sub Submission 136 err := globalDB.QueryRow( 137 "SELECT id, username, filename, upload_time, status FROM submissions WHERE id = ?", 138 id, 139 ).Scan(&sub.ID, &sub.Username, &sub.Filename, &sub.UploadTime, &sub.Status) 140 return sub, err 141} 142 143func runRoundRobinMatches(newSub Submission) { 144 // Get all active submissions 145 activeSubmissions, err := getActiveSubmissions() 146 if err != nil { 147 log.Printf("Failed to get active submissions: %v", err) 148 return 149 } 150 151 totalMatches := len(activeSubmissions) - 1 // Exclude self 152 if totalMatches <= 0 { 153 log.Printf("No opponents for %s, skipping matches", newSub.Username) 154 return 155 } 156 157 log.Printf("Starting round-robin for %s against %d opponents", newSub.Username, totalMatches) 158 matchNum := 0 159 160 // Run matches against all other submissions 161 for _, opponent := range activeSubmissions { 162 if opponent.ID == newSub.ID { 163 continue 164 } 165 166 matchNum++ 167 log.Printf("[%d/%d] Running match: %s vs %s (1000 games)", matchNum, totalMatches, newSub.Username, opponent.Username) 168 169 // Run match (1000 games total) 170 player1Wins, player2Wins, totalMoves := runHeadToHead(newSub, opponent, 1000) 171 172 // Determine winner 173 var winnerID int 174 avgMoves := totalMoves / 1000 175 176 if player1Wins > player2Wins { 177 winnerID = newSub.ID 178 log.Printf("[%d/%d] Match result: %s wins (%d-%d, avg %d moves)", matchNum, totalMatches, newSub.Username, player1Wins, player2Wins, avgMoves) 179 } else if player2Wins > player1Wins { 180 winnerID = opponent.ID 181 log.Printf("[%d/%d] Match result: %s wins (%d-%d, avg %d moves)", matchNum, totalMatches, opponent.Username, player2Wins, player1Wins, avgMoves) 182 } else { 183 // Tie - coin flip 184 if totalMoves%2 == 0 { 185 winnerID = newSub.ID 186 } else { 187 winnerID = opponent.ID 188 } 189 log.Printf("[%d/%d] Match result: Tie %d-%d, winner by coin flip: %d", matchNum, totalMatches, player1Wins, player2Wins, winnerID) 190 } 191 192 // Store match result 193 if err := addMatch(newSub.ID, opponent.ID, winnerID, player1Wins, player2Wins, avgMoves, avgMoves); err != nil { 194 log.Printf("Failed to store match result: %v", err) 195 } else { 196 // Notify SSE clients of update after each match 197 log.Printf("Broadcasting leaderboard update after match %d/%d", matchNum, totalMatches) 198 NotifyLeaderboardUpdate() 199 } 200 } 201 202 log.Printf("Round-robin complete for %s (%d matches)", newSub.Username, totalMatches) 203} 204 205func runHeadToHead(player1, player2 Submission, numGames int) (int, int, int) { 206 re := regexp.MustCompile(`memory_functions_(\w+)\.cpp`) 207 matches1 := re.FindStringSubmatch(player1.Filename) 208 matches2 := re.FindStringSubmatch(player2.Filename) 209 210 if len(matches1) < 2 || len(matches2) < 2 { 211 return 0, 0, 0 212 } 213 214 prefix1 := matches1[1] 215 prefix2 := matches2[1] 216 217 // Read both cpp files to extract function suffixes 218 cpp1Path := filepath.Join(enginePath, "src", player1.Filename) 219 cpp2Path := filepath.Join(enginePath, "src", player2.Filename) 220 221 cpp1Content, err := os.ReadFile(cpp1Path) 222 if err != nil { 223 log.Printf("Failed to read %s: %v", cpp1Path, err) 224 return 0, 0, 0 225 } 226 227 cpp2Content, err := os.ReadFile(cpp2Path) 228 if err != nil { 229 log.Printf("Failed to read %s: %v", cpp2Path, err) 230 return 0, 0, 0 231 } 232 233 suffix1, err := parseFunctionNames(string(cpp1Content)) 234 if err != nil { 235 log.Printf("Failed to parse function names for %s: %v", player1.Filename, err) 236 return 0, 0, 0 237 } 238 239 suffix2, err := parseFunctionNames(string(cpp2Content)) 240 if err != nil { 241 log.Printf("Failed to parse function names for %s: %v", player2.Filename, err) 242 return 0, 0, 0 243 } 244 245 buildDir := filepath.Join(enginePath, "build") 246 247 // Create a combined binary with both AIs 248 combinedBinary := filepath.Join(buildDir, fmt.Sprintf("match_%s_vs_%s", prefix1, prefix2)) 249 250 // Generate main file that uses both AIs with correct function suffixes 251 mainContent := generateMatchMain(prefix1, prefix2, suffix1, suffix2) 252 mainPath := filepath.Join(enginePath, "src", fmt.Sprintf("match_%s_vs_%s.cpp", prefix1, prefix2)) 253 if err := os.WriteFile(mainPath, []byte(mainContent), 0644); err != nil { 254 log.Printf("Failed to write match main: %v", err) 255 return 0, 0, 0 256 } 257 258 // Compile combined binary 259 compileArgs := []string{"-std=c++11", "-O3", 260 "-o", combinedBinary, 261 mainPath, 262 filepath.Join(enginePath, "src", "battleship_light.cpp"), 263 } 264 265 // Add player files (avoid duplicates if same AI) 266 if prefix1 == prefix2 { 267 // Same AI - only compile once 268 compileArgs = append(compileArgs, filepath.Join(enginePath, "src", fmt.Sprintf("memory_functions_%s.cpp", prefix1))) 269 } else { 270 // Different AIs 271 compileArgs = append(compileArgs, 272 filepath.Join(enginePath, "src", fmt.Sprintf("memory_functions_%s.cpp", prefix1)), 273 filepath.Join(enginePath, "src", fmt.Sprintf("memory_functions_%s.cpp", prefix2)), 274 ) 275 } 276 277 cmd := exec.Command("g++", compileArgs...) 278 output, err := cmd.CombinedOutput() 279 if err != nil { 280 log.Printf("Failed to compile match binary: %s", output) 281 return 0, 0, 0 282 } 283 284 // Run the match 285 cmd = exec.Command(combinedBinary, strconv.Itoa(numGames)) 286 output, err = cmd.CombinedOutput() 287 if err != nil { 288 log.Printf("Match execution failed: %v", err) 289 return 0, 0, 0 290 } 291 292 // Parse results 293 return parseMatchOutput(string(output)) 294} 295 296func generateMatchMain(prefix1, prefix2, suffix1, suffix2 string) string { 297 return fmt.Sprintf(`#include "battleship_light.h" 298#include "memory.h" 299#include "memory_functions_%s.h" 300#include "memory_functions_%s.h" 301#include <iostream> 302#include <cstdlib> 303#include <ctime> 304 305using namespace std; 306 307struct MatchResult { 308 int player1Wins = 0; 309 int player2Wins = 0; 310 int ties = 0; 311 int totalMoves = 0; 312}; 313 314MatchResult runMatch(int numGames) { 315 MatchResult result; 316 srand(time(NULL)); 317 318 for (int game = 0; game < numGames; game++) { 319 Board board1, board2; 320 ComputerMemory memory1, memory2; 321 322 initializeBoard(board1); 323 initializeBoard(board2); 324 initMemory%s(memory1); 325 initMemory%s(memory2); 326 327 int shipsSunk1 = 0; 328 int shipsSunk2 = 0; 329 int moveCount = 0; 330 331 while (true) { 332 moveCount++; 333 334 // Player 1 move 335 string move1 = smartMove%s(memory1); 336 int row1, col1; 337 int check1 = checkMove(move1, board2, row1, col1); 338 while (check1 != VALID_MOVE) { 339 move1 = randomMove(); 340 check1 = checkMove(move1, board2, row1, col1); 341 } 342 343 // Player 2 move 344 string move2 = smartMove%s(memory2); 345 int row2, col2; 346 int check2 = checkMove(move2, board1, row2, col2); 347 while (check2 != VALID_MOVE) { 348 move2 = randomMove(); 349 check2 = checkMove(move2, board1, row2, col2); 350 } 351 352 // Execute moves 353 int result1 = playMove(row1, col1, board2); 354 int result2 = playMove(row2, col2, board1); 355 356 updateMemory%s(row1, col1, result1, memory1); 357 updateMemory%s(row2, col2, result2, memory2); 358 359 if (isASunk(result1)) shipsSunk1++; 360 if (isASunk(result2)) shipsSunk2++; 361 362 if (shipsSunk1 == 5 || shipsSunk2 == 5) { 363 break; 364 } 365 } 366 367 result.totalMoves += moveCount; 368 369 if (shipsSunk1 == 5 && shipsSunk2 == 5) { 370 result.ties++; 371 } else if (shipsSunk1 == 5) { 372 result.player1Wins++; 373 } else { 374 result.player2Wins++; 375 } 376 } 377 378 return result; 379} 380 381int main(int argc, char* argv[]) { 382 if (argc < 2) { 383 cerr << "Usage: " << argv[0] << " <num_games>" << endl; 384 return 1; 385 } 386 387 int numGames = atoi(argv[1]); 388 if (numGames <= 0) numGames = 10; 389 390 setDebugMode(false); 391 392 MatchResult result = runMatch(numGames); 393 394 // Output in parseable format 395 cout << "PLAYER1_WINS=" << result.player1Wins << endl; 396 cout << "PLAYER2_WINS=" << result.player2Wins << endl; 397 cout << "TIES=" << result.ties << endl; 398 cout << "TOTAL_MOVES=" << result.totalMoves << endl; 399 cout << "AVG_MOVES=" << (result.totalMoves / numGames) << endl; 400 401 return 0; 402} 403`, prefix1, prefix2, suffix1, suffix2, suffix1, suffix2, suffix1, suffix2) 404} 405 406func parseMatchOutput(output string) (int, int, int) { 407 player1Wins := 0 408 player2Wins := 0 409 totalMoves := 0 410 411 lines := strings.Split(output, "\n") 412 for _, line := range lines { 413 if strings.HasPrefix(line, "PLAYER1_WINS=") { 414 fmt.Sscanf(line, "PLAYER1_WINS=%d", &player1Wins) 415 } else if strings.HasPrefix(line, "PLAYER2_WINS=") { 416 fmt.Sscanf(line, "PLAYER2_WINS=%d", &player2Wins) 417 } else if strings.HasPrefix(line, "TOTAL_MOVES=") { 418 fmt.Sscanf(line, "TOTAL_MOVES=%d", &totalMoves) 419 } 420 } 421 422 return player1Wins, player2Wins, totalMoves 423}