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}