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

feat: move to bracket style

dunkirk.sh 8c03432e 462a2381

verified
+8
.gitignore
···
battleship-arena
*.log
build/
+
+
# Generated files in engine
+
battleship-engine/src/memory_functions_*.cpp
+
battleship-engine/src/memory_functions_*.h
+
battleship-engine/src/match_*.cpp
+
battleship-engine/build/
+
+
scripts/test-submissions/memory_functions_klukas.cpp
+68
TOURNAMENT.md
···
+
# Battleship Arena - Tournament System
+
+
## Overview
+
Tournament-style battleship AI competition with automatic header generation and local compilation.
+
+
## Architecture
+
+
### Battleship Engine
+
- Located in `./battleship-engine/`
+
- Contains the lightweight battleship game engine
+
- No external dependencies on the school repo
+
- Auto-generates header files for submissions
+
+
### Submission Flow
+
1. User uploads `memory_functions_<name>.cpp` via SCP/SFTP
+
2. System auto-generates `memory_functions_<name>.h` header
+
3. Compiles submission with the battleship engine
+
4. If successful, runs tournament matches against all active submissions
+
5. Updates leaderboard with results
+
+
### Tournament Matching
+
- Each match compiles both AIs into a single binary
+
- Runs 10 games per match
+
- Winner determined by total wins
+
- All results stored in database
+
+
## Test Submissions
+
+
Three AI implementations for testing:
+
+
1. **random** - Pure random shooter (baseline)
+
2. **hunter** - Checkerboard hunt + adjacent targeting
+
3. **klukas** - Advanced probability-based AI
+
+
## Testing
+
+
```bash
+
# Upload test submissions
+
./scripts/test-upload.sh
+
+
# This uploads:
+
# - alice with random AI
+
# - bob with hunter AI
+
# - charlie with klukas AI
+
```
+
+
## Requirements
+
+
Submissions must:
+
- Be named `memory_functions_<name>.cpp`
+
- Implement three functions:
+
- `void initMemory<Name>(ComputerMemory &memory)`
+
- `std::string smartMove<Name>(const ComputerMemory &memory)`
+
- `void updateMemory<Name>(int row, int col, int result, ComputerMemory &memory)`
+
- Use only standard includes and provided headers (battleship.h, kasbs.h, memory.h)
+
+
Headers are auto-generated - users only need to upload the `.cpp` file!
+
+
## Usage
+
+
Start server:
+
```bash
+
./battleship-arena
+
```
+
+
View results:
+
- SSH TUI: `ssh -p 2222 username@0.0.0.0`
+
- Web: http://0.0.0.0:8080
+452
battleship-engine/src/battle_light.cpp
···
+
// Lightweight cross-platform battleship implementation
+
// Author: Kieran Klukas
+
// Date: November 2025
+
// Purpose: Test smart battleship AI with benchmarking on non-Linux systems
+
+
#include "battleship_light.h"
+
#include "memory.h"
+
#include "memory_functions_klukas.h"
+
#include <iostream>
+
#include <chrono>
+
#include <thread>
+
#include <vector>
+
#include <mutex>
+
#include <atomic>
+
+
using namespace std;
+
+
struct BenchmarkStats {
+
atomic<int> wins{0};
+
atomic<int> losses{0};
+
atomic<int> ties{0};
+
atomic<long long> totalMoves{0};
+
atomic<long long> totalTimeNs{0};
+
atomic<int> minMovesWin{999999};
+
atomic<int> maxMovesWin{0};
+
atomic<int> minMovesLoss{999999};
+
atomic<int> maxMovesLoss{0};
+
+
mutex updateMutex; // For min/max updates
+
};
+
+
void printStats(const BenchmarkStats &stats, int gamesPlayed) {
+
const int MAX_MOVES = 200; // Theoretical max (both players shoot all 100 squares)
+
double avgMoves = (double)stats.totalMoves.load() / gamesPlayed;
+
double movesPercent = (avgMoves / MAX_MOVES) * 100.0;
+
+
cout << "\n========== BENCHMARK RESULTS ==========" << endl;
+
cout << "Games played: " << gamesPlayed << endl;
+
cout << "Smart AI wins: " << stats.wins.load() << " ("
+
<< (100.0 * stats.wins.load() / gamesPlayed) << "%)" << endl;
+
cout << "Dumb AI wins: " << stats.losses.load() << " ("
+
<< (100.0 * stats.losses.load() / gamesPlayed) << "%)" << endl;
+
cout << "Ties: " << stats.ties.load() << endl;
+
cout << "Avg moves per game: " << (int)avgMoves
+
<< " (" << movesPercent << "% of max)" << endl;
+
+
if (stats.wins.load() > 0) {
+
cout << "Win move range: " << stats.minMovesWin.load() << "-" << stats.maxMovesWin.load() << endl;
+
}
+
if (stats.losses.load() > 0) {
+
cout << "Loss move range: " << stats.minMovesLoss.load() << "-" << stats.maxMovesLoss.load() << endl;
+
}
+
+
double avgTimeMs = (double)stats.totalTimeNs.load() / gamesPlayed / 1000000.0;
+
cout << "Avg time per game: " << avgTimeMs << "ms" << endl;
+
cout << "========================================\n" << endl;
+
}
+
+
// Thread-safe game runner function
+
void runGames(int startGame, int endGame, BenchmarkStats &stats, bool logLosses,
+
atomic<int> &gamesCompleted, int totalGames, unsigned int threadSeed) {
+
// Each thread gets its own random seed
+
srand(threadSeed);
+
+
for (int game = startGame; game < endGame; game++) {
+
auto startTime = chrono::high_resolution_clock::now();
+
+
Board dumbComputerBoard, smartComputerBoard;
+
ComputerMemory smartComputerMemory;
+
+
string dumbComputerMove, smartComputerMove;
+
int numDumbComputerShipsSunk = 0;
+
int numSmartComputerShipsSunk = 0;
+
int dumbComputerRow, dumbComputerColumn;
+
int smartComputerRow, smartComputerColumn;
+
int checkValue, dumbComputerResult, smartComputerResult;
+
int moveCount = 0;
+
+
initializeBoard(dumbComputerBoard);
+
initializeBoard(smartComputerBoard);
+
initMemoryKlukas(smartComputerMemory);
+
+
while (true) {
+
moveCount++;
+
+
// Dumb computer move
+
dumbComputerMove = randomMove();
+
checkValue = checkMove(dumbComputerMove, smartComputerBoard,
+
dumbComputerRow, dumbComputerColumn);
+
+
while (checkValue != VALID_MOVE) {
+
dumbComputerMove = randomMove();
+
checkValue = checkMove(dumbComputerMove, smartComputerBoard,
+
dumbComputerRow, dumbComputerColumn);
+
}
+
+
// Smart computer move
+
smartComputerMove = smartMoveKlukas(smartComputerMemory);
+
int checkResult = checkMove(smartComputerMove, dumbComputerBoard,
+
smartComputerRow, smartComputerColumn);
+
+
while (checkResult != VALID_MOVE) {
+
smartComputerMove = randomMove();
+
checkResult = checkMove(smartComputerMove, dumbComputerBoard,
+
smartComputerRow, smartComputerColumn);
+
}
+
+
// Execute moves
+
dumbComputerResult = playMove(dumbComputerRow, dumbComputerColumn,
+
smartComputerBoard);
+
smartComputerResult = playMove(smartComputerRow, smartComputerColumn,
+
dumbComputerBoard);
+
updateMemoryKlukas(smartComputerRow, smartComputerColumn,
+
smartComputerResult, smartComputerMemory);
+
+
if (isASunk(dumbComputerResult)) {
+
numDumbComputerShipsSunk++;
+
}
+
if (isASunk(smartComputerResult)) {
+
numSmartComputerShipsSunk++;
+
}
+
+
if (numDumbComputerShipsSunk == 5 || numSmartComputerShipsSunk == 5) {
+
break;
+
}
+
}
+
+
auto endTime = chrono::high_resolution_clock::now();
+
auto duration = chrono::duration_cast<chrono::nanoseconds>(endTime - startTime);
+
+
// Update stats atomically
+
stats.totalMoves += moveCount;
+
stats.totalTimeNs += duration.count();
+
+
if (numDumbComputerShipsSunk == 5 && numSmartComputerShipsSunk == 5) {
+
stats.ties++;
+
} else if (numSmartComputerShipsSunk == 5) {
+
stats.wins++;
+
+
// Update min/max with mutex protection
+
lock_guard<mutex> lock(stats.updateMutex);
+
int currentMin = stats.minMovesWin.load();
+
if (moveCount < currentMin) stats.minMovesWin = moveCount;
+
int currentMax = stats.maxMovesWin.load();
+
if (moveCount > currentMax) stats.maxMovesWin = moveCount;
+
} else {
+
stats.losses++;
+
+
lock_guard<mutex> lock(stats.updateMutex);
+
int currentMin = stats.minMovesLoss.load();
+
if (moveCount < currentMin) stats.minMovesLoss = moveCount;
+
int currentMax = stats.maxMovesLoss.load();
+
if (moveCount > currentMax) stats.maxMovesLoss = moveCount;
+
}
+
+
// Update progress counter
+
gamesCompleted++;
+
}
+
}
+
+
int main(int argc, char* argv[]) {
+
bool benchmark = false;
+
bool verbose = false;
+
bool logLosses = false;
+
bool catchGuards = false;
+
int numGames = 1;
+
+
// Parse command line args
+
for (int i = 1; i < argc; i++) {
+
string arg = argv[i];
+
if (arg == "--benchmark" || arg == "-b") {
+
benchmark = true;
+
if (i + 1 < argc) {
+
numGames = atoi(argv[++i]);
+
if (numGames <= 0) numGames = 100;
+
} else {
+
numGames = 100;
+
}
+
} else if (arg == "--verbose" || arg == "-v") {
+
verbose = true;
+
} else if (arg == "--log-losses" || arg == "-l") {
+
logLosses = true;
+
} else if (arg == "--catch-guards" || arg == "-g") {
+
catchGuards = true;
+
}
+
}
+
+
BenchmarkStats stats;
+
srand(time(NULL));
+
+
// Catch-guards mode: run games until we hit a guard
+
if (catchGuards) {
+
cout << "Running games until guard is tripped..." << endl;
+
int gamesRun = 0;
+
+
while (true) {
+
gamesRun++;
+
resetGuardTripped();
+
+
Board dumbComputerBoard, smartComputerBoard;
+
ComputerMemory smartComputerMemory;
+
string dumbComputerMove, smartComputerMove;
+
int numDumbComputerShipsSunk = 0;
+
int numSmartComputerShipsSunk = 0;
+
int dumbComputerRow, dumbComputerColumn;
+
int smartComputerRow, smartComputerColumn;
+
int checkValue, dumbComputerResult, smartComputerResult;
+
int moveCount = 0;
+
+
initializeBoard(dumbComputerBoard);
+
initializeBoard(smartComputerBoard);
+
initMemoryKlukas(smartComputerMemory);
+
+
bool guardTripped = false;
+
while (true) {
+
moveCount++;
+
+
// Dumb computer move
+
dumbComputerMove = randomMove();
+
checkValue = checkMove(dumbComputerMove, smartComputerBoard,
+
dumbComputerRow, dumbComputerColumn);
+
while (checkValue != VALID_MOVE) {
+
dumbComputerMove = randomMove();
+
checkValue = checkMove(dumbComputerMove, smartComputerBoard,
+
dumbComputerRow, dumbComputerColumn);
+
}
+
+
// Smart computer move
+
smartComputerMove = smartMoveKlukas(smartComputerMemory);
+
+
// Check if guard was tripped
+
if (getGuardTripped()) {
+
guardTripped = true;
+
break;
+
}
+
+
int checkResult = checkMove(smartComputerMove, dumbComputerBoard,
+
smartComputerRow, smartComputerColumn);
+
while (checkResult != VALID_MOVE) {
+
smartComputerMove = randomMove();
+
checkResult = checkMove(smartComputerMove, dumbComputerBoard,
+
smartComputerRow, smartComputerColumn);
+
}
+
+
// Execute moves
+
dumbComputerResult = playMove(dumbComputerRow, dumbComputerColumn,
+
smartComputerBoard);
+
smartComputerResult = playMove(smartComputerRow, smartComputerColumn,
+
dumbComputerBoard);
+
updateMemoryKlukas(smartComputerRow, smartComputerColumn,
+
smartComputerResult, smartComputerMemory);
+
+
if (isASunk(dumbComputerResult)) {
+
numDumbComputerShipsSunk++;
+
}
+
if (isASunk(smartComputerResult)) {
+
numSmartComputerShipsSunk++;
+
}
+
+
if (numDumbComputerShipsSunk == 5 || numSmartComputerShipsSunk == 5) {
+
break;
+
}
+
}
+
+
if (guardTripped) {
+
cout << "\n==================================" << endl;
+
cout << "GUARD TRIPPED after " << gamesRun << " games!" << endl;
+
cout << "==================================" << endl;
+
cout << "\nDebug log (last 50 entries):" << endl;
+
cout << "----------------------------------" << endl;
+
+
vector<string> log = getDebugLog();
+
int start = max(0, (int)log.size() - 50);
+
for (int i = start; i < (int)log.size(); i++) {
+
cout << log[i] << endl;
+
}
+
+
return 0;
+
}
+
+
if (gamesRun % 100 == 0) {
+
cout << "Completed " << gamesRun << " games..." << endl;
+
}
+
}
+
}
+
+
if (!benchmark) {
+
setDebugMode(true);
+
welcome(true);
+
verbose = true; // Always show moves in interactive mode
+
} else {
+
setDebugMode(false);
+
+
// Determine number of threads (use hardware concurrency)
+
unsigned int numThreads = thread::hardware_concurrency();
+
if (numThreads == 0) numThreads = 4; // Fallback if detection fails
+
+
cout << "Running " << numGames << " games on " << numThreads << " threads..." << endl;
+
+
// Progress tracking
+
atomic<int> gamesCompleted{0};
+
+
// Launch threads
+
vector<thread> threads;
+
int gamesPerThread = numGames / numThreads;
+
int remainder = numGames % numThreads;
+
+
int startGame = 0;
+
for (unsigned int t = 0; t < numThreads; t++) {
+
int endGame = startGame + gamesPerThread + (t < (unsigned)remainder ? 1 : 0);
+
unsigned int threadSeed = time(NULL) + t; // Unique seed per thread
+
+
threads.emplace_back(runGames, startGame, endGame, ref(stats),
+
logLosses, ref(gamesCompleted), numGames, threadSeed);
+
startGame = endGame;
+
}
+
+
// Progress monitoring thread
+
thread progressThread([&]() {
+
int lastReported = 0;
+
int interval;
+
if (numGames >= 10000) interval = 1000;
+
else if (numGames >= 1000) interval = 100;
+
else if (numGames >= 100) interval = 10;
+
else interval = numGames / 5;
+
+
while (gamesCompleted.load() < numGames) {
+
this_thread::sleep_for(chrono::milliseconds(100));
+
int completed = gamesCompleted.load();
+
if (interval > 0 && completed >= lastReported + interval) {
+
cout << "Completed " << completed << " games..." << endl;
+
lastReported = (completed / interval) * interval;
+
}
+
}
+
});
+
+
// Wait for all game threads to complete
+
for (auto &t : threads) {
+
t.join();
+
}
+
+
// Stop progress thread
+
progressThread.join();
+
}
+
+
if (!benchmark) {
+
// Single game mode (existing code)
+
Board dumbComputerBoard, smartComputerBoard;
+
ComputerMemory smartComputerMemory;
+
+
string dumbComputerMove, smartComputerMove;
+
int numDumbComputerShipsSunk = 0;
+
int numSmartComputerShipsSunk = 0;
+
int dumbComputerRow, dumbComputerColumn;
+
int smartComputerRow, smartComputerColumn;
+
int checkValue, dumbComputerResult, smartComputerResult;
+
+
initializeBoard(dumbComputerBoard);
+
initializeBoard(smartComputerBoard);
+
initMemoryKlukas(smartComputerMemory);
+
+
while (true) {
+
if (verbose) {
+
clearTheScreen();
+
cout << "Dumb Computer Board:" << endl;
+
displayBoard(1, 5, HUMAN, dumbComputerBoard);
+
cout << "Smart Computer Board:" << endl;
+
displayBoard(1, 40, HUMAN, smartComputerBoard);
+
}
+
+
// Dumb computer move
+
dumbComputerMove = randomMove();
+
checkValue = checkMove(dumbComputerMove, smartComputerBoard,
+
dumbComputerRow, dumbComputerColumn);
+
+
while (checkValue != VALID_MOVE) {
+
dumbComputerMove = randomMove();
+
checkValue = checkMove(dumbComputerMove, smartComputerBoard,
+
dumbComputerRow, dumbComputerColumn);
+
}
+
+
// Smart computer move
+
smartComputerMove = smartMoveKlukas(smartComputerMemory);
+
int checkResult = checkMove(smartComputerMove, dumbComputerBoard,
+
smartComputerRow, smartComputerColumn);
+
+
while (checkResult != VALID_MOVE) {
+
if (verbose) {
+
debug("INVALID! Using random instead", 0, 0);
+
}
+
smartComputerMove = randomMove();
+
checkResult = checkMove(smartComputerMove, dumbComputerBoard,
+
smartComputerRow, smartComputerColumn);
+
}
+
+
// Execute moves
+
dumbComputerResult = playMove(dumbComputerRow, dumbComputerColumn,
+
smartComputerBoard);
+
smartComputerResult = playMove(smartComputerRow, smartComputerColumn,
+
dumbComputerBoard);
+
updateMemoryKlukas(smartComputerRow, smartComputerColumn,
+
smartComputerResult, smartComputerMemory);
+
+
if (verbose) {
+
clearTheScreen();
+
cout << "Dumb Computer Board:" << endl;
+
displayBoard(1, 5, HUMAN, dumbComputerBoard);
+
cout << "Smart Computer Board:" << endl;
+
displayBoard(1, 40, HUMAN, smartComputerBoard);
+
+
writeMessage(15, 0, "The dumb computer chooses: " + dumbComputerMove);
+
writeMessage(16, 0, "The smart computer chooses: " + smartComputerMove);
+
+
writeResult(18, 0, dumbComputerResult, COMPUTER);
+
writeResult(19, 0, smartComputerResult, HUMAN);
+
+
// Delay so the game is watchable
+
this_thread::sleep_for(chrono::milliseconds(50));
+
}
+
+
if (isASunk(dumbComputerResult)) {
+
numDumbComputerShipsSunk++;
+
}
+
if (isASunk(smartComputerResult)) {
+
numSmartComputerShipsSunk++;
+
}
+
+
if (numDumbComputerShipsSunk == 5 || numSmartComputerShipsSunk == 5) {
+
break;
+
}
+
}
+
+
cout << "\nFinal Dumb Computer Board:" << endl;
+
displayBoard(1, 5, HUMAN, dumbComputerBoard);
+
cout << "Final Smart Computer Board:" << endl;
+
displayBoard(1, 40, HUMAN, smartComputerBoard);
+
+
if (numDumbComputerShipsSunk == 5 && numSmartComputerShipsSunk == 5) {
+
writeMessage(21, 1, "The game is a tie.");
+
} else if (numDumbComputerShipsSunk == 5) {
+
writeMessage(21, 1, "Amazing, the dumb computer won.");
+
} else {
+
writeMessage(21, 1, "Smart AI won! As it should.");
+
}
+
}
+
+
if (benchmark) {
+
printStats(stats, numGames);
+
}
+
+
return 0;
+
}
+97
battleship-engine/src/battleship.h
···
+
#ifndef BATTLESHIP_H
+
#define BATTLESHIP_H
+
+
// Author: Keith Shomper
+
// Date: 24 Oct 03
+
// Purpose: Header file for implementing a text-based battleship game
+
// Updated: 14 Nov 08 to align class types with Cedarville standard
+
// Updated: 3 Apr 14 to use struct rather than class for the defined types
+
// Updated: 5 Nov 15 to introduce new functions to replace the ISAxxx macros
+
// and also the SHIPNUM and DEBUGOUT macros
+
+
// include files for implementing battleship
+
#include <iostream>
+
#include <cstdlib>
+
#include <time.h>
+
#include <curses.h>
+
#include "kasbs.h"
+
+
using namespace std;
+
+
// use these constants to indicate if the player is a human or a computer
+
// battleship board size is 10x10 grid (defined in kasbs.h)
+
+
// data structure for position
+
struct Position {
+
int startRow; // ship's initial row
+
int startCol; // ship's initial column
+
int orient; // indicates whether the ship is running across
+
// or up and down
+
};
+
+
// data structure for ship
+
struct Ship {
+
Position pos; // where the ship is on the board
+
int size; // number of hits required to sink the ship
+
int hitsToSink; // number of hits remaining before the ship is sunk
+
char marker; // the ASCII marker used to denote the ship on the
+
// board
+
};
+
+
// a game board is made up of a 10x10 playing grid and the ships
+
struct Board {
+
char grid[BOARDSIZE][BOARDSIZE];
+
Ship s[6]; // NOTE: the first (zeroth) position is left empty
+
};
+
+
// use these constants for designating to which player we are referring
+
const int HUMAN = 0;
+
const int COMPUTER = 1;
+
+
// use these constants for deciding whether or not the user gave a proper move
+
const int VALID_MOVE = 0;
+
const int ILLEGAL_FORMAT = 1;
+
const int REUSED_MOVE = 2;
+
+
// functions for screen control and I/O
+
void welcome(bool debug = false, bool pf = false);
+
void clearTheLine(int x);
+
void clearTheScreen(void);
+
void pauseForEnter(void);
+
string getResponse(int x, int y, string prompt);
+
void writeMessage(int x, int y, string message);
+
void writeResult(int x, int y, int result, int playerType);
+
void displayBoard(int x, int y, int playerType, const Board &gameBoard);
+
+
// functions to control the board situation
+
void initializeBoard(Board &gameBoard, bool file = false);
+
int playMove(int row, int col, Board &gameBoard);
+
+
// function to tell what happened in the last play_move() command
+
bool isAMiss(int playMoveResult);
+
bool isAHit (int playMoveResult);
+
bool isASunk(int playMoveResult); // formerly named isItSunk()
+
int isShip (int playMoveResult);
+
+
// misc game functions
+
string randomMove(void);
+
int checkMove(string move, const Board &gameBoard, int &row, int &col);
+
void debug(string s, int x = 22, int y = 1);
+
string numToString(int x);
+
+
#ifdef BATTLESHIP_BACKWARD_COMPATIBILITY
+
+
// former function signatures
+
void debug(int x, int y, string s);
+
bool isItSunk(int playMoveResult);
+
+
// a debug macro
+
#ifdef DEBUG
+
#define DEBUGOUT(str) debug (22, 1, (str));
+
#else
+
#define DEBUGOUT(str)
+
#endif // DEBUG
+
+
#endif // BATTLESHIP_BACKWARD_COMPATABILITY
+
+
#endif // BATTLESHIP_H
+317
battleship-engine/src/battleship_light.cpp
···
+
#include "battleship_light.h"
+
#include <sstream>
+
#include <cctype>
+
#include <vector>
+
#include <mutex>
+
+
// Global flag for debug output
+
static bool g_debugEnabled = false;
+
static bool g_guardTripped = false;
+
static vector<string> g_debugLog;
+
static mutex g_debugLogMutex;
+
static const size_t MAX_DEBUG_LOG_SIZE = 1000;
+
+
void setDebugMode(bool enabled) {
+
g_debugEnabled = enabled;
+
}
+
+
bool getGuardTripped() {
+
return g_guardTripped;
+
}
+
+
void resetGuardTripped() {
+
g_guardTripped = false;
+
lock_guard<mutex> lock(g_debugLogMutex);
+
g_debugLog.clear();
+
}
+
+
vector<string> getDebugLog() {
+
lock_guard<mutex> lock(g_debugLogMutex);
+
return g_debugLog;
+
}
+
+
void welcome(bool debug) {
+
clearTheScreen();
+
cout << "========================================" << endl;
+
cout << " BATTLESHIP - Lightweight" << endl;
+
cout << "========================================" << endl;
+
if (debug) {
+
cout << "Debug mode enabled" << endl;
+
}
+
cout << endl;
+
}
+
+
void clearTheScreen() {
+
// Simple cross-platform clear
+
cout << "\033[2J\033[1;1H";
+
}
+
+
void pauseForEnter() {
+
cout << "Press Enter to continue...";
+
cin.ignore();
+
cin.get();
+
}
+
+
void writeMessage(int x, int y, string message) {
+
cout << message << endl;
+
}
+
+
void writeResult(int x, int y, int result, int playerType) {
+
string player = (playerType == HUMAN) ? "Player" : "Computer";
+
+
if (isASunk(result)) {
+
int shipNum = isShip(result);
+
char shipName;
+
switch(shipNum) {
+
case AC: shipName = 'A'; break;
+
case BS: shipName = 'B'; break;
+
case CR: shipName = 'C'; break;
+
case SB: shipName = 'S'; break;
+
case DS: shipName = 'D'; break;
+
default: shipName = '?'; break;
+
}
+
cout << player << " SUNK a ship (" << shipName << ")!" << endl;
+
} else if (isAHit(result)) {
+
cout << player << " HIT!" << endl;
+
} else {
+
cout << player << " MISS" << endl;
+
}
+
}
+
+
void displayBoard(int x, int y, int playerType, const Board &gameBoard) {
+
cout << " ";
+
for (int col = 0; col < BOARDSIZE; col++) {
+
cout << (col + 1) << " ";
+
}
+
cout << endl;
+
+
for (int row = 0; row < BOARDSIZE; row++) {
+
cout << char('A' + row) << " ";
+
for (int col = 0; col < BOARDSIZE; col++) {
+
char cell = gameBoard.grid[row][col];
+
+
// Hide ships if showing to computer player
+
if (playerType == COMPUTER) {
+
if (cell != HIT_MARKER && cell != MISS_MARKER && cell != SUNK_MARKER) {
+
cell = EMPTY_MARKER;
+
}
+
}
+
+
cout << cell << " ";
+
}
+
cout << endl;
+
}
+
cout << endl;
+
}
+
+
bool placeShip(Board &gameBoard, int shipNum, int row, int col, int orient) {
+
Ship &ship = gameBoard.s[shipNum];
+
+
// Check bounds
+
if (orient == HORZ) {
+
if (col + ship.size > BOARDSIZE) return false;
+
} else {
+
if (row + ship.size > BOARDSIZE) return false;
+
}
+
+
// Check for collisions
+
for (int i = 0; i < ship.size; i++) {
+
int r = (orient == VERT) ? row + i : row;
+
int c = (orient == HORZ) ? col + i : col;
+
if (gameBoard.grid[r][c] != EMPTY_MARKER) {
+
return false;
+
}
+
}
+
+
// Place the ship
+
ship.pos.startRow = row;
+
ship.pos.startCol = col;
+
ship.pos.orient = orient;
+
+
for (int i = 0; i < ship.size; i++) {
+
int r = (orient == VERT) ? row + i : row;
+
int c = (orient == HORZ) ? col + i : col;
+
gameBoard.grid[r][c] = ship.marker;
+
}
+
+
return true;
+
}
+
+
void initializeBoard(Board &gameBoard, bool file) {
+
// Initialize grid
+
for (int i = 0; i < BOARDSIZE; i++) {
+
for (int j = 0; j < BOARDSIZE; j++) {
+
gameBoard.grid[i][j] = EMPTY_MARKER;
+
}
+
}
+
+
// Initialize ships
+
gameBoard.s[AC].size = AC_SIZE;
+
gameBoard.s[AC].hitsToSink = AC_SIZE;
+
gameBoard.s[AC].marker = AC_MARKER;
+
+
gameBoard.s[BS].size = BS_SIZE;
+
gameBoard.s[BS].hitsToSink = BS_SIZE;
+
gameBoard.s[BS].marker = BS_MARKER;
+
+
gameBoard.s[CR].size = CR_SIZE;
+
gameBoard.s[CR].hitsToSink = CR_SIZE;
+
gameBoard.s[CR].marker = CR_MARKER;
+
+
gameBoard.s[SB].size = SB_SIZE;
+
gameBoard.s[SB].hitsToSink = SB_SIZE;
+
gameBoard.s[SB].marker = SB_MARKER;
+
+
gameBoard.s[DS].size = DS_SIZE;
+
gameBoard.s[DS].hitsToSink = DS_SIZE;
+
gameBoard.s[DS].marker = DS_MARKER;
+
+
// Place ships randomly
+
for (int shipNum = AC; shipNum <= DS; shipNum++) {
+
bool placed = false;
+
while (!placed) {
+
int row = rand() % BOARDSIZE;
+
int col = rand() % BOARDSIZE;
+
int orient = rand() % 2;
+
placed = placeShip(gameBoard, shipNum, row, col, orient);
+
}
+
}
+
}
+
+
int playMove(int row, int col, Board &gameBoard) {
+
char cell = gameBoard.grid[row][col];
+
+
// Already hit
+
if (cell == HIT_MARKER || cell == MISS_MARKER || cell == SUNK_MARKER) {
+
return MISS;
+
}
+
+
// Miss
+
if (cell == EMPTY_MARKER) {
+
gameBoard.grid[row][col] = MISS_MARKER;
+
return MISS;
+
}
+
+
// Hit a ship
+
int shipNum = 0;
+
if (cell == AC_MARKER) shipNum = AC;
+
else if (cell == BS_MARKER) shipNum = BS;
+
else if (cell == CR_MARKER) shipNum = CR;
+
else if (cell == SB_MARKER) shipNum = SB;
+
else if (cell == DS_MARKER) shipNum = DS;
+
+
if (shipNum == 0) {
+
gameBoard.grid[row][col] = MISS_MARKER;
+
return MISS;
+
}
+
+
Ship &ship = gameBoard.s[shipNum];
+
ship.hitsToSink--;
+
+
gameBoard.grid[row][col] = HIT_MARKER;
+
+
if (ship.hitsToSink == 0) {
+
// Mark all parts as sunk
+
for (int i = 0; i < ship.size; i++) {
+
int r = (ship.pos.orient == VERT) ? ship.pos.startRow + i : ship.pos.startRow;
+
int c = (ship.pos.orient == HORZ) ? ship.pos.startCol + i : ship.pos.startCol;
+
gameBoard.grid[r][c] = SUNK_MARKER;
+
}
+
return SUNK | shipNum;
+
}
+
+
return HIT | shipNum;
+
}
+
+
bool isAMiss(int playMoveResult) {
+
return !(playMoveResult & HIT);
+
}
+
+
bool isAHit(int playMoveResult) {
+
return (playMoveResult & HIT) != 0;
+
}
+
+
bool isASunk(int playMoveResult) {
+
return (playMoveResult & SUNK) != 0;
+
}
+
+
int isShip(int playMoveResult) {
+
return playMoveResult & SHIP;
+
}
+
+
string randomMove() {
+
int row = rand() % BOARDSIZE;
+
int col = rand() % BOARDSIZE;
+
+
char letter = 'A' + row;
+
return string(1, letter) + " " + to_string(col + 1);
+
}
+
+
int checkMove(string move, const Board &gameBoard, int &row, int &col) {
+
// Trim whitespace
+
move.erase(0, move.find_first_not_of(" \t\n\r"));
+
move.erase(move.find_last_not_of(" \t\n\r") + 1);
+
+
if (move.empty()) {
+
return ILLEGAL_FORMAT;
+
}
+
+
// Parse format: "A 5" or "A5"
+
char letter = toupper(move[0]);
+
if (letter < 'A' || letter > 'J') {
+
return ILLEGAL_FORMAT;
+
}
+
+
row = letter - 'A';
+
+
// Extract number
+
string numStr = move.substr(1);
+
numStr.erase(0, numStr.find_first_not_of(" \t"));
+
+
if (numStr.empty()) {
+
return ILLEGAL_FORMAT;
+
}
+
+
try {
+
col = stoi(numStr) - 1;
+
} catch (...) {
+
return ILLEGAL_FORMAT;
+
}
+
+
if (col < 0 || col >= BOARDSIZE) {
+
return ILLEGAL_FORMAT;
+
}
+
+
// Check if already used
+
char cell = gameBoard.grid[row][col];
+
if (cell == HIT_MARKER || cell == MISS_MARKER || cell == SUNK_MARKER) {
+
return REUSED_MOVE;
+
}
+
+
return VALID_MOVE;
+
}
+
+
void debug(string s, int x, int y) {
+
// Only accumulate logs if debug mode is enabled or we need to track guards
+
// This prevents memory bloat during benchmarks
+
if (g_debugEnabled || s.find("*** GUARD TRIPPED ***") != string::npos) {
+
lock_guard<mutex> lock(g_debugLogMutex);
+
g_debugLog.push_back(s);
+
+
// Limit log size to prevent unbounded growth
+
if (g_debugLog.size() > MAX_DEBUG_LOG_SIZE) {
+
g_debugLog.erase(g_debugLog.begin(),
+
g_debugLog.begin() + (g_debugLog.size() - MAX_DEBUG_LOG_SIZE));
+
}
+
}
+
+
if (g_debugEnabled) {
+
cout << "[DEBUG] " << s << endl;
+
}
+
}
+
+
string numToString(int x) {
+
stringstream ss;
+
ss << x;
+
return ss.str();
+
}
+65
battleship-engine/src/battleship_light.h
···
+
#ifndef BATTLESHIP_LIGHT_H
+
#define BATTLESHIP_LIGHT_H
+
+
#include <iostream>
+
#include <cstdlib>
+
#include <ctime>
+
#include <string>
+
#include <vector>
+
+
// Use the same constants and types as the normal version
+
#include "kasbs.h"
+
+
using namespace std;
+
+
// Player types (not in kasbs.h)
+
const int HUMAN = 0;
+
const int COMPUTER = 1;
+
+
// Move validation (not in kasbs.h)
+
const int VALID_MOVE = 0;
+
const int ILLEGAL_FORMAT = 1;
+
const int REUSED_MOVE = 2;
+
+
// Position, Ship, and Board are compatible with normal version
+
struct Position {
+
int startRow;
+
int startCol;
+
int orient;
+
};
+
+
struct Ship {
+
Position pos;
+
int size;
+
int hitsToSink;
+
char marker;
+
};
+
+
struct Board {
+
char grid[BOARDSIZE][BOARDSIZE];
+
Ship s[6]; // index 0 unused
+
};
+
+
// Core functions - lightweight implementations
+
void setDebugMode(bool enabled);
+
bool getGuardTripped();
+
void resetGuardTripped();
+
vector<string> getDebugLog();
+
void welcome(bool debug = false);
+
void clearTheScreen();
+
void pauseForEnter();
+
void writeMessage(int x, int y, string message);
+
void writeResult(int x, int y, int result, int playerType);
+
void displayBoard(int x, int y, int playerType, const Board &gameBoard);
+
void initializeBoard(Board &gameBoard, bool file = false);
+
int playMove(int row, int col, Board &gameBoard);
+
bool isAMiss(int playMoveResult);
+
bool isAHit(int playMoveResult);
+
bool isASunk(int playMoveResult);
+
int isShip(int playMoveResult);
+
string randomMove();
+
int checkMove(string move, const Board &gameBoard, int &row, int &col);
+
void debug(string s, int x = 22, int y = 1);
+
string numToString(int x);
+
+
#endif // BATTLESHIP_LIGHT_H
+78
battleship-engine/src/kasbs.h
···
+
// Author: Keith Shomper
+
// Date: 24 Oct 03
+
// Purpose: Type definitions for implementing a text-based battleship game
+
// Modified:15 Nov 2005 - Moved the class definitions into the file battleship.h
+
// to make the data structure more visible.
+
// Modified: 5 Nov 2013 - Moved the ISA macros higher in the file to make them
+
// more visible. Added ISAMISS macro
+
// Modified: 5 Nov 2015 - Moved the ISA macros back to a lower section and made
+
// them conditionally compilable. The purpose of this
+
// change was to promote their replacement by like-named
+
// functions, while maintaining backward compatibility.
+
+
// use these constants to indicate whether a ship goes across the grid or up
+
// and down
+
+
#ifndef BATTLESHIP_TYPE_DEFINITIONS_H
+
#define BATTLESHIP_TYPE_DEFINITIONS_H
+
+
// Board size
+
const int BOARDSIZE = 10;
+
+
// use these constants to indicate whether a ship goes across the grid or up
+
// and down
+
const int HORZ = 0;
+
const int VERT = 1;
+
+
// these constants allow use to refer to the ships by numerical values
+
const int AC = 1;
+
const int BS = 2;
+
const int CR = 3;
+
const int SB = 4;
+
const int DS = 5;
+
+
// constants for the ship size
+
const int AC_SIZE = 5;
+
const int BS_SIZE = 4;
+
const int CR_SIZE = 3;
+
const int SB_SIZE = 3;
+
const int DS_SIZE = 2;
+
+
// markers for keeping track of game play
+
const char HIT_MARKER = 'H';
+
const char MISS_MARKER = '*';
+
const char SUNK_MARKER = 'X';
+
const char EMPTY_MARKER = ' ';
+
const char AC_MARKER = 'A';
+
const char BS_MARKER = 'B';
+
const char CR_MARKER = 'C';
+
const char SB_MARKER = 'S';
+
const char DS_MARKER = 'D';
+
+
// TO STUDENTS: It should not be necessary in your assignment for you to use
+
// the information appearing below this comment
+
+
// color constants for diferent game situations
+
const int BATTLE_WHITE = 1;
+
const int BATTLE_GREEN = 2;
+
const int BATTLE_YELLOW = 3;
+
const int BATTLE_RED = 4;
+
+
// use these constants to set the return value in playMove(), they allow us
+
// to send back mutiple pieces of information about the result of the move in
+
// a single integer variable
+
const int MISS = 0;
+
const int SHIP = 7;
+
const int HIT = 8;
+
const int SUNK = 16;
+
+
#ifdef BATTLESHIP_BACKWARD_COMPATIBILITY
+
// these macros use the MISS, HIT, etc. constants below to determine the
+
// result of the move
+
#define ISAMISS(parm) (!((parm) & HIT))
+
#define ISAHIT(parm) ((parm) & HIT)
+
#define ISASUNK(parm) ((parm) & SUNK)
+
#define SHIPNUM(parm) ((parm) & SHIP)
+
#endif // BATTLESHIP_BACKWARD_COMPATIBILITY
+
+
#endif // BATTLESHIP_TYPE_DEFINITIONS_H
+38
battleship-engine/src/memory.h
···
+
#ifndef MEMORY_H
+
#define MEMORY_H
+
+
#include "kasbs.h"
+
+
using namespace std;
+
+
#define RANDOM 1
+
#define SEARCH 2
+
#define DESTROY 3
+
+
#define NONE 0
+
#define NORTH 1
+
#define SOUTH 2
+
#define EAST 3
+
#define WEST 4
+
+
struct ComputerMemory {
+
int hitRow, hitCol;
+
int hitShip;
+
int fireDir;
+
int fireDist;
+
int lastResult;
+
int mode;
+
char grid[BOARDSIZE][BOARDSIZE];
+
+
// optional attributes for students wanting to keep track of hits on
+
// multiple ships
+
int depth;
+
int hitRows[5], hitCols[5];
+
int hitShips[5];
+
int fireDirs[5];
+
int fireDists[5];
+
int lastResults[5];
+
int modes[5];
+
};
+
+
#endif // MEMORY_H
+116
battleship-engine/src/tournament_battle.cpp
···
+
// Tournament battle runner - runs matches between two AI implementations
+
// Outputs results in parseable format
+
+
#include "battleship_light.h"
+
#include "memory.h"
+
#include <iostream>
+
#include <cstdlib>
+
#include <ctime>
+
+
using namespace std;
+
+
// Function pointers for the two AIs
+
void (*initMemory1)(ComputerMemory&) = nullptr;
+
string (*smartMove1)(const ComputerMemory&) = nullptr;
+
void (*updateMemory1)(int, int, int, ComputerMemory&) = nullptr;
+
+
void (*initMemory2)(ComputerMemory&) = nullptr;
+
string (*smartMove2)(const ComputerMemory&) = nullptr;
+
void (*updateMemory2)(int, int, int, ComputerMemory&) = nullptr;
+
+
struct MatchResult {
+
int player1Wins = 0;
+
int player2Wins = 0;
+
int ties = 0;
+
int totalMoves = 0;
+
};
+
+
MatchResult runMatch(int numGames) {
+
MatchResult result;
+
srand(time(NULL));
+
+
for (int game = 0; game < numGames; game++) {
+
Board board1, board2;
+
ComputerMemory memory1, memory2;
+
+
initializeBoard(board1);
+
initializeBoard(board2);
+
initMemory1(memory1);
+
initMemory2(memory2);
+
+
int shipsSunk1 = 0;
+
int shipsSunk2 = 0;
+
int moveCount = 0;
+
+
while (true) {
+
moveCount++;
+
+
// Player 1 move
+
string move1 = smartMove1(memory1);
+
int row1, col1;
+
int check1 = checkMove(move1, board2, row1, col1);
+
while (check1 != VALID_MOVE) {
+
move1 = randomMove();
+
check1 = checkMove(move1, board2, row1, col1);
+
}
+
+
// Player 2 move
+
string move2 = smartMove2(memory2);
+
int row2, col2;
+
int check2 = checkMove(move2, board1, row2, col2);
+
while (check2 != VALID_MOVE) {
+
move2 = randomMove();
+
check2 = checkMove(move2, board1, row2, col2);
+
}
+
+
// Execute moves
+
int result1 = playMove(row1, col1, board2);
+
int result2 = playMove(row2, col2, board1);
+
+
updateMemory1(row1, col1, result1, memory1);
+
updateMemory2(row2, col2, result2, memory2);
+
+
if (isASunk(result1)) shipsSunk1++;
+
if (isASunk(result2)) shipsSunk2++;
+
+
if (shipsSunk1 == 5 || shipsSunk2 == 5) {
+
break;
+
}
+
}
+
+
result.totalMoves += moveCount;
+
+
if (shipsSunk1 == 5 && shipsSunk2 == 5) {
+
result.ties++;
+
} else if (shipsSunk1 == 5) {
+
result.player1Wins++;
+
} else {
+
result.player2Wins++;
+
}
+
}
+
+
return result;
+
}
+
+
int main(int argc, char* argv[]) {
+
if (argc < 2) {
+
cerr << "Usage: " << argv[0] << " <num_games>" << endl;
+
return 1;
+
}
+
+
int numGames = atoi(argv[1]);
+
if (numGames <= 0) numGames = 10;
+
+
setDebugMode(false);
+
+
MatchResult result = runMatch(numGames);
+
+
// Output in parseable format
+
cout << "PLAYER1_WINS=" << result.player1Wins << endl;
+
cout << "PLAYER2_WINS=" << result.player2Wins << endl;
+
cout << "TIES=" << result.ties << endl;
+
cout << "TOTAL_MOVES=" << result.totalMoves << endl;
+
cout << "AVG_MOVES=" << (result.totalMoves / numGames) << endl;
+
+
return 0;
+
}
+100 -18
database.go
···
username TEXT NOT NULL,
filename TEXT NOT NULL,
upload_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-
status TEXT DEFAULT 'pending'
+
status TEXT DEFAULT 'pending',
+
is_active BOOLEAN DEFAULT 1
);
-
CREATE TABLE IF NOT EXISTS results (
+
CREATE TABLE IF NOT EXISTS matches (
id INTEGER PRIMARY KEY AUTOINCREMENT,
-
submission_id INTEGER,
-
opponent TEXT,
-
result TEXT, -- win, loss, tie
-
moves INTEGER,
+
player1_id INTEGER,
+
player2_id INTEGER,
+
winner_id INTEGER,
+
player1_moves INTEGER,
+
player2_moves INTEGER,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-
FOREIGN KEY (submission_id) REFERENCES submissions(id)
+
FOREIGN KEY (player1_id) REFERENCES submissions(id),
+
FOREIGN KEY (player2_id) REFERENCES submissions(id),
+
FOREIGN KEY (winner_id) REFERENCES submissions(id)
);
-
CREATE INDEX IF NOT EXISTS idx_results_submission ON results(submission_id);
+
CREATE INDEX IF NOT EXISTS idx_matches_player1 ON matches(player1_id);
+
CREATE INDEX IF NOT EXISTS idx_matches_player2 ON matches(player2_id);
CREATE INDEX IF NOT EXISTS idx_submissions_username ON submissions(username);
CREATE INDEX IF NOT EXISTS idx_submissions_status ON submissions(status);
+
CREATE INDEX IF NOT EXISTS idx_submissions_active ON submissions(is_active);
`
_, err = db.Exec(schema)
···
query := `
SELECT
s.username,
-
SUM(CASE WHEN r.result = 'win' THEN 1 ELSE 0 END) as wins,
-
SUM(CASE WHEN r.result = 'loss' THEN 1 ELSE 0 END) as losses,
-
AVG(r.moves) as avg_moves,
-
MAX(r.timestamp) as last_played
+
COUNT(CASE WHEN m.winner_id = s.id THEN 1 END) as wins,
+
COUNT(CASE WHEN (m.player1_id = s.id OR m.player2_id = s.id) AND m.winner_id != s.id THEN 1 END) as losses,
+
AVG(CASE WHEN m.player1_id = s.id THEN m.player1_moves ELSE m.player2_moves END) as avg_moves,
+
MAX(m.timestamp) as last_played
FROM submissions s
-
JOIN results r ON s.id = r.submission_id
+
LEFT JOIN matches m ON (m.player1_id = s.id OR m.player2_id = s.id)
+
WHERE s.is_active = 1
GROUP BY s.username
+
HAVING COUNT(m.id) > 0
ORDER BY wins DESC, losses ASC, avg_moves ASC
LIMIT ?
`
···
}
func addSubmission(username, filename string) (int64, error) {
+
// Mark old submission as inactive
+
_, err := globalDB.Exec(
+
"UPDATE submissions SET is_active = 0 WHERE username = ?",
+
username,
+
)
+
if err != nil {
+
return 0, err
+
}
+
+
// Insert new submission
result, err := globalDB.Exec(
-
"INSERT INTO submissions (username, filename) VALUES (?, ?)",
+
"INSERT INTO submissions (username, filename, is_active) VALUES (?, ?, 1)",
username, filename,
)
if err != nil {
···
return result.LastInsertId()
}
-
func addResult(submissionID int, opponent, result string, moves int) error {
+
func addMatch(player1ID, player2ID, winnerID, player1Moves, player2Moves int) error {
_, err := globalDB.Exec(
-
"INSERT INTO results (submission_id, opponent, result, moves) VALUES (?, ?, ?, ?)",
-
submissionID, opponent, result, moves,
+
"INSERT INTO matches (player1_id, player2_id, winner_id, player1_moves, player2_moves) VALUES (?, ?, ?, ?, ?)",
+
player1ID, player2ID, winnerID, player1Moves, player2Moves,
)
return err
}
···
func getPendingSubmissions() ([]Submission, error) {
rows, err := globalDB.Query(
-
"SELECT id, username, filename, upload_time, status FROM submissions WHERE status = 'pending' ORDER BY upload_time",
+
"SELECT id, username, filename, upload_time, status FROM submissions WHERE status = 'pending' AND is_active = 1 ORDER BY upload_time",
+
)
+
if err != nil {
+
return nil, err
+
}
+
defer rows.Close()
+
+
var submissions []Submission
+
for rows.Next() {
+
var s Submission
+
err := rows.Scan(&s.ID, &s.Username, &s.Filename, &s.UploadTime, &s.Status)
+
if err != nil {
+
return nil, err
+
}
+
submissions = append(submissions, s)
+
}
+
+
return submissions, rows.Err()
+
}
+
+
func getActiveSubmissions() ([]Submission, error) {
+
rows, err := globalDB.Query(
+
"SELECT id, username, filename, upload_time, status FROM submissions WHERE is_active = 1 AND status = 'completed' ORDER BY username",
)
if err != nil {
return nil, err
···
return submissions, rows.Err()
}
+
+
+
type MatchResult struct {
+
Player1Username string
+
Player2Username string
+
WinnerUsername string
+
AvgMoves int
+
}
+
+
func getAllMatches() ([]MatchResult, error) {
+
query := `
+
SELECT
+
s1.username as player1,
+
s2.username as player2,
+
sw.username as winner,
+
m.player1_moves as avg_moves
+
FROM matches m
+
JOIN submissions s1 ON m.player1_id = s1.id
+
JOIN submissions s2 ON m.player2_id = s2.id
+
JOIN submissions sw ON m.winner_id = sw.id
+
WHERE s1.is_active = 1 AND s2.is_active = 1
+
ORDER BY m.timestamp DESC
+
`
+
+
rows, err := globalDB.Query(query)
+
if err != nil {
+
return nil, err
+
}
+
defer rows.Close()
+
+
var matches []MatchResult
+
for rows.Next() {
+
var m MatchResult
+
err := rows.Scan(&m.Player1Username, &m.Player2Username, &m.WinnerUsername, &m.AvgMoves)
+
if err != nil {
+
return nil, err
+
}
+
matches = append(matches, m)
+
}
+
+
return matches, rows.Err()
+
}
+67 -2
model.go
···
height int
submissions []Submission
leaderboard []LeaderboardEntry
+
matches []MatchResult
}
func initialModel(username string, width, height int) model {
···
}
func (m model) Init() tea.Cmd {
-
return tea.Batch(loadLeaderboard, loadSubmissions(m.username), tickCmd())
+
return tea.Batch(loadLeaderboard, loadSubmissions(m.username), loadMatches, tickCmd())
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
···
m.leaderboard = msg.entries
case submissionsMsg:
m.submissions = msg.submissions
+
case matchesMsg:
+
m.matches = msg.matches
case tickMsg:
-
return m, tea.Batch(loadLeaderboard, loadSubmissions(m.username), tickCmd())
+
return m, tea.Batch(loadLeaderboard, loadSubmissions(m.username), loadMatches, tickCmd())
}
return m, nil
}
···
b.WriteString("\n")
}
+
// Show bracket-style matches
+
if len(m.matches) > 0 {
+
b.WriteString(renderBracket(m.matches))
+
b.WriteString("\n")
+
}
+
// Show leaderboard if loaded
if len(m.leaderboard) > 0 {
b.WriteString(renderLeaderboard(m.leaderboard))
···
}
}
+
type matchesMsg struct {
+
matches []MatchResult
+
}
+
+
func loadMatches() tea.Msg {
+
matches, err := getAllMatches()
+
if err != nil {
+
return matchesMsg{matches: nil}
+
}
+
return matchesMsg{matches: matches}
+
}
+
type tickMsg time.Time
func tickCmd() tea.Cmd {
···
return b.String()
}
+
+
func renderBracket(matches []MatchResult) string {
+
var b strings.Builder
+
b.WriteString(lipgloss.NewStyle().Bold(true).Render("⚔️ Recent Matches") + "\n\n")
+
+
if len(matches) == 0 {
+
return b.String()
+
}
+
+
// Show most recent matches (up to 10)
+
displayCount := len(matches)
+
if displayCount > 10 {
+
displayCount = 10
+
}
+
+
for i := 0; i < displayCount; i++ {
+
match := matches[i]
+
+
// Determine styling based on winner
+
player1Style := lipgloss.NewStyle()
+
player2Style := lipgloss.NewStyle()
+
+
if match.WinnerUsername == match.Player1Username {
+
player1Style = player1Style.Foreground(lipgloss.Color("green")).Bold(true)
+
player2Style = player2Style.Foreground(lipgloss.Color("240"))
+
} else {
+
player2Style = player2Style.Foreground(lipgloss.Color("green")).Bold(true)
+
player1Style = player1Style.Foreground(lipgloss.Color("240"))
+
}
+
+
// Format: [Player1] ──vs── [Player2] → Winner (avg moves)
+
player1Str := player1Style.Render(fmt.Sprintf("%-15s", match.Player1Username))
+
vsStr := lipgloss.NewStyle().Foreground(lipgloss.Color("240")).Render(" ──vs── ")
+
player2Str := player2Style.Render(fmt.Sprintf("%-15s", match.Player2Username))
+
+
winnerMark := "→"
+
winnerStr := lipgloss.NewStyle().Foreground(lipgloss.Color("green")).Render(
+
fmt.Sprintf("%s %s wins (avg %d moves)", winnerMark, match.WinnerUsername, match.AvgMoves))
+
+
b.WriteString(fmt.Sprintf("%s%s%s %s\n", player1Str, vsStr, player2Str, winnerStr))
+
}
+
+
return b.String()
+
}
+315 -53
runner.go
···
"os/exec"
"path/filepath"
"regexp"
+
"strconv"
"strings"
)
-
const battleshipRepoPath = "/Users/kierank/code/school/cs1210-battleship"
+
const enginePath = "./battleship-engine"
func processSubmissions() error {
submissions, err := getPendingSubmissions()
···
}
for _, sub := range submissions {
-
log.Printf("Starting test for submission %d: %s by %s", sub.ID, sub.Filename, sub.Username)
+
log.Printf("Starting compilation for submission %d: %s by %s", sub.ID, sub.Filename, sub.Username)
-
if err := testSubmission(sub); err != nil {
-
log.Printf("Submission %d failed: %v", sub.ID, err)
+
if err := compileSubmission(sub); err != nil {
+
log.Printf("Submission %d failed compilation: %v", sub.ID, err)
updateSubmissionStatus(sub.ID, "failed")
continue
}
-
log.Printf("Submission %d completed successfully: %s by %s", sub.ID, sub.Filename, sub.Username)
+
log.Printf("Submission %d compiled successfully: %s by %s", sub.ID, sub.Filename, sub.Username)
updateSubmissionStatus(sub.ID, "completed")
+
+
// Run tournament matches against all active submissions
+
go runTournamentMatches(sub)
}
return nil
}
-
func testSubmission(sub Submission) error {
-
log.Printf("Setting submission %d to testing status", sub.ID)
+
func generateHeader(filename, prefix string) string {
+
guard := strings.ToUpper(strings.Replace(filename, ".", "_", -1))
+
+
// Capitalize first letter of prefix for function names
+
functionSuffix := strings.ToUpper(prefix[0:1]) + prefix[1:]
+
+
return fmt.Sprintf(`#ifndef %s
+
#define %s
+
+
#include "memory.h"
+
#include <string>
+
+
void initMemory%s(ComputerMemory &memory);
+
std::string smartMove%s(const ComputerMemory &memory);
+
void updateMemory%s(int row, int col, int result, ComputerMemory &memory);
+
+
#endif
+
`, guard, guard, functionSuffix, functionSuffix, functionSuffix)
+
}
+
+
func parseFunctionNames(cppContent string) (string, error) {
+
// Look for the initMemory function to extract the suffix
+
re := regexp.MustCompile(`void\s+initMemory(\w+)\s*\(`)
+
matches := re.FindStringSubmatch(cppContent)
+
if len(matches) < 2 {
+
return "", fmt.Errorf("could not find initMemory function")
+
}
+
return matches[1], nil
+
}
+
+
func compileSubmission(sub Submission) error {
updateSubmissionStatus(sub.ID, "testing")
-
// Copy submission to battleship repo
+
// Extract prefix from filename (memory_functions_XXXXX.cpp -> XXXXX)
+
re := regexp.MustCompile(`memory_functions_(\w+)\.cpp`)
+
matches := re.FindStringSubmatch(sub.Filename)
+
if len(matches) < 2 {
+
return fmt.Errorf("invalid filename format")
+
}
+
prefix := matches[1]
+
+
// Create temporary build directory
+
buildDir := filepath.Join(enginePath, "build")
+
os.MkdirAll(buildDir, 0755)
+
+
// Copy submission to engine
srcPath := filepath.Join(uploadDir, sub.Username, sub.Filename)
-
dstPath := filepath.Join(battleshipRepoPath, "src", sub.Filename)
-
+
dstPath := filepath.Join(enginePath, "src", sub.Filename)
+
log.Printf("Copying %s to %s", srcPath, dstPath)
input, err := os.ReadFile(srcPath)
if err != nil {
···
return err
}
-
// Extract student ID from filename (memory_functions_NNNN.cpp)
-
re := regexp.MustCompile(`memory_functions_(\w+)\.cpp`)
-
matches := re.FindStringSubmatch(sub.Filename)
-
if len(matches) < 2 {
-
return fmt.Errorf("invalid filename format")
+
// Parse function names from the cpp file
+
functionSuffix, err := parseFunctionNames(string(input))
+
if err != nil {
+
return fmt.Errorf("failed to parse function names: %v", err)
}
-
studentID := matches[1]
+
+
log.Printf("Detected function suffix: %s", functionSuffix)
-
// Build the battleship program
-
buildDir := filepath.Join(battleshipRepoPath, "build")
-
os.MkdirAll(buildDir, 0755)
+
// Generate header file with parsed function names
+
headerFilename := fmt.Sprintf("memory_functions_%s.h", prefix)
+
headerPath := filepath.Join(enginePath, "src", headerFilename)
+
headerContent := generateHeader(headerFilename, functionSuffix)
+
if err := os.WriteFile(headerPath, []byte(headerContent), 0644); err != nil {
+
return err
+
}
-
log.Printf("Compiling submission %d for student %s", sub.ID, studentID)
-
// Compile using the light version for testing
-
cmd := exec.Command("g++", "-std=c++11", "-O3",
-
"-o", filepath.Join(buildDir, "battle_"+studentID),
-
filepath.Join(battleshipRepoPath, "src", "battle_light.cpp"),
-
filepath.Join(battleshipRepoPath, "src", "battleship_light.cpp"),
-
filepath.Join(battleshipRepoPath, "src", sub.Filename),
+
log.Printf("Compiling submission %d for %s", sub.ID, prefix)
+
+
// Compile check only (no linking) to validate syntax
+
cmd := exec.Command("g++", "-std=c++11", "-c", "-O3",
+
"-I", filepath.Join(enginePath, "src"),
+
"-o", filepath.Join(buildDir, "ai_"+prefix+".o"),
+
filepath.Join(enginePath, "src", sub.Filename),
)
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("compilation failed: %s", output)
}
-
log.Printf("Running benchmark for submission %d (100 games)", sub.ID)
-
// Run benchmark tests (100 games)
-
cmd = exec.Command(filepath.Join(buildDir, "battle_"+studentID), "--benchmark", "100")
-
output, err = cmd.CombinedOutput()
+
return nil
+
}
+
+
func runTournamentMatches(newSub Submission) {
+
// Get all active submissions
+
activeSubmissions, err := getActiveSubmissions()
if err != nil {
-
return fmt.Errorf("benchmark failed: %s", output)
+
log.Printf("Failed to get active submissions: %v", err)
+
return
}
-
// Parse results and store in database
-
log.Printf("Parsing results for submission %d", sub.ID)
-
results := parseResults(string(output))
-
for opponent, result := range results {
-
addResult(sub.ID, opponent, result.Result, result.Moves)
+
// Run matches against all other submissions
+
for _, opponent := range activeSubmissions {
+
if opponent.ID == newSub.ID {
+
continue
+
}
+
+
log.Printf("Running match: %s vs %s (1000 games)", newSub.Username, opponent.Username)
+
+
// Run match (1000 games total)
+
player1Wins, player2Wins, totalMoves := runHeadToHead(newSub, opponent, 1000)
+
+
// Determine winner
+
var winnerID int
+
avgMoves := totalMoves / 1000
+
+
if player1Wins > player2Wins {
+
winnerID = newSub.ID
+
log.Printf("Match result: %s wins (%d-%d, avg %d moves)", newSub.Username, player1Wins, player2Wins, avgMoves)
+
} else if player2Wins > player1Wins {
+
winnerID = opponent.ID
+
log.Printf("Match result: %s wins (%d-%d, avg %d moves)", opponent.Username, player2Wins, player1Wins, avgMoves)
+
} else {
+
// Tie - coin flip
+
if totalMoves%2 == 0 {
+
winnerID = newSub.ID
+
} else {
+
winnerID = opponent.ID
+
}
+
log.Printf("Match result: Tie %d-%d, winner by coin flip: %d", player1Wins, player2Wins, winnerID)
+
}
+
+
// Store match result
+
if err := addMatch(newSub.ID, opponent.ID, winnerID, avgMoves, avgMoves); err != nil {
+
log.Printf("Failed to store match result: %v", err)
+
}
}
+
}
-
return nil
+
func runHeadToHead(player1, player2 Submission, numGames int) (int, int, int) {
+
re := regexp.MustCompile(`memory_functions_(\w+)\.cpp`)
+
matches1 := re.FindStringSubmatch(player1.Filename)
+
matches2 := re.FindStringSubmatch(player2.Filename)
+
+
if len(matches1) < 2 || len(matches2) < 2 {
+
return 0, 0, 0
+
}
+
+
prefix1 := matches1[1]
+
prefix2 := matches2[1]
+
+
// Read both cpp files to extract function suffixes
+
cpp1Path := filepath.Join(enginePath, "src", player1.Filename)
+
cpp2Path := filepath.Join(enginePath, "src", player2.Filename)
+
+
cpp1Content, err := os.ReadFile(cpp1Path)
+
if err != nil {
+
log.Printf("Failed to read %s: %v", cpp1Path, err)
+
return 0, 0, 0
+
}
+
+
cpp2Content, err := os.ReadFile(cpp2Path)
+
if err != nil {
+
log.Printf("Failed to read %s: %v", cpp2Path, err)
+
return 0, 0, 0
+
}
+
+
suffix1, err := parseFunctionNames(string(cpp1Content))
+
if err != nil {
+
log.Printf("Failed to parse function names for %s: %v", player1.Filename, err)
+
return 0, 0, 0
+
}
+
+
suffix2, err := parseFunctionNames(string(cpp2Content))
+
if err != nil {
+
log.Printf("Failed to parse function names for %s: %v", player2.Filename, err)
+
return 0, 0, 0
+
}
+
+
buildDir := filepath.Join(enginePath, "build")
+
+
// Create a combined binary with both AIs
+
combinedBinary := filepath.Join(buildDir, fmt.Sprintf("match_%s_vs_%s", prefix1, prefix2))
+
+
// Generate main file that uses both AIs with correct function suffixes
+
mainContent := generateMatchMain(prefix1, prefix2, suffix1, suffix2)
+
mainPath := filepath.Join(enginePath, "src", fmt.Sprintf("match_%s_vs_%s.cpp", prefix1, prefix2))
+
if err := os.WriteFile(mainPath, []byte(mainContent), 0644); err != nil {
+
log.Printf("Failed to write match main: %v", err)
+
return 0, 0, 0
+
}
+
+
// Compile combined binary
+
cmd := exec.Command("g++", "-std=c++11", "-O3",
+
"-o", combinedBinary,
+
mainPath,
+
filepath.Join(enginePath, "src", "battleship_light.cpp"),
+
filepath.Join(enginePath, "src", fmt.Sprintf("memory_functions_%s.cpp", prefix1)),
+
filepath.Join(enginePath, "src", fmt.Sprintf("memory_functions_%s.cpp", prefix2)),
+
)
+
output, err := cmd.CombinedOutput()
+
if err != nil {
+
log.Printf("Failed to compile match binary: %s", output)
+
return 0, 0, 0
+
}
+
+
// Run the match
+
cmd = exec.Command(combinedBinary, strconv.Itoa(numGames))
+
output, err = cmd.CombinedOutput()
+
if err != nil {
+
log.Printf("Match execution failed: %v", err)
+
return 0, 0, 0
+
}
+
+
// Parse results
+
return parseMatchOutput(string(output))
}
-
type GameResult struct {
-
Result string
-
Moves int
+
func generateMatchMain(prefix1, prefix2, suffix1, suffix2 string) string {
+
return fmt.Sprintf(`#include "battleship_light.h"
+
#include "memory.h"
+
#include "memory_functions_%s.h"
+
#include "memory_functions_%s.h"
+
#include <iostream>
+
#include <cstdlib>
+
#include <ctime>
+
+
using namespace std;
+
+
struct MatchResult {
+
int player1Wins = 0;
+
int player2Wins = 0;
+
int ties = 0;
+
int totalMoves = 0;
+
};
+
+
MatchResult runMatch(int numGames) {
+
MatchResult result;
+
srand(time(NULL));
+
+
for (int game = 0; game < numGames; game++) {
+
Board board1, board2;
+
ComputerMemory memory1, memory2;
+
+
initializeBoard(board1);
+
initializeBoard(board2);
+
initMemory%s(memory1);
+
initMemory%s(memory2);
+
+
int shipsSunk1 = 0;
+
int shipsSunk2 = 0;
+
int moveCount = 0;
+
+
while (true) {
+
moveCount++;
+
+
// Player 1 move
+
string move1 = smartMove%s(memory1);
+
int row1, col1;
+
int check1 = checkMove(move1, board2, row1, col1);
+
while (check1 != VALID_MOVE) {
+
move1 = randomMove();
+
check1 = checkMove(move1, board2, row1, col1);
+
}
+
+
// Player 2 move
+
string move2 = smartMove%s(memory2);
+
int row2, col2;
+
int check2 = checkMove(move2, board1, row2, col2);
+
while (check2 != VALID_MOVE) {
+
move2 = randomMove();
+
check2 = checkMove(move2, board1, row2, col2);
+
}
+
+
// Execute moves
+
int result1 = playMove(row1, col1, board2);
+
int result2 = playMove(row2, col2, board1);
+
+
updateMemory%s(row1, col1, result1, memory1);
+
updateMemory%s(row2, col2, result2, memory2);
+
+
if (isASunk(result1)) shipsSunk1++;
+
if (isASunk(result2)) shipsSunk2++;
+
+
if (shipsSunk1 == 5 || shipsSunk2 == 5) {
+
break;
+
}
+
}
+
+
result.totalMoves += moveCount;
+
+
if (shipsSunk1 == 5 && shipsSunk2 == 5) {
+
result.ties++;
+
} else if (shipsSunk1 == 5) {
+
result.player1Wins++;
+
} else {
+
result.player2Wins++;
+
}
+
}
+
+
return result;
}
-
func parseResults(output string) map[string]GameResult {
-
results := make(map[string]GameResult)
+
int main(int argc, char* argv[]) {
+
if (argc < 2) {
+
cerr << "Usage: " << argv[0] << " <num_games>" << endl;
+
return 1;
+
}
+
+
int numGames = atoi(argv[1]);
+
if (numGames <= 0) numGames = 10;
+
+
setDebugMode(false);
+
+
MatchResult result = runMatch(numGames);
+
+
// Output in parseable format
+
cout << "PLAYER1_WINS=" << result.player1Wins << endl;
+
cout << "PLAYER2_WINS=" << result.player2Wins << endl;
+
cout << "TIES=" << result.ties << endl;
+
cout << "TOTAL_MOVES=" << result.totalMoves << endl;
+
cout << "AVG_MOVES=" << (result.totalMoves / numGames) << endl;
+
+
return 0;
+
}
+
`, prefix1, prefix2, suffix1, suffix2, suffix1, suffix2, suffix1, suffix2)
+
}
+
+
func parseMatchOutput(output string) (int, int, int) {
+
player1Wins := 0
+
player2Wins := 0
+
totalMoves := 0
-
// Parse win/loss stats from benchmark output
-
// Example: "Smart AI wins: 95 (95.0%)"
lines := strings.Split(output, "\n")
for _, line := range lines {
-
if strings.Contains(line, "Smart AI wins:") {
-
// Extract win count
-
re := regexp.MustCompile(`Smart AI wins: (\d+)`)
-
matches := re.FindStringSubmatch(line)
-
if len(matches) >= 2 {
-
// For now, just record as wins against "random"
-
results["random"] = GameResult{Result: "win", Moves: 50}
-
}
+
if strings.HasPrefix(line, "PLAYER1_WINS=") {
+
fmt.Sscanf(line, "PLAYER1_WINS=%d", &player1Wins)
+
} else if strings.HasPrefix(line, "PLAYER2_WINS=") {
+
fmt.Sscanf(line, "PLAYER2_WINS=%d", &player2Wins)
+
} else if strings.HasPrefix(line, "TOTAL_MOVES=") {
+
fmt.Sscanf(line, "TOTAL_MOVES=%d", &totalMoves)
}
}
-
return results
+
return player1Wins, player2Wins, totalMoves
}
+7
scp.go
···
return 0, err
}
+
// Remove old file if it exists to ensure clean overwrite
+
targetPath := filepath.Join(userDir, filename)
+
if _, err := os.Stat(targetPath); err == nil {
+
log.Printf("Removing old file: %s", targetPath)
+
os.Remove(targetPath)
+
}
+
// Modify the entry to write to user's subdirectory
userEntry := &scp.FileEntry{
Name: filepath.Join(s.User(), filename),
-30
scripts/test-submission.sh
···
-
#!/bin/bash
-
# Example test script for submitting and testing an AI
-
-
set -e
-
-
USER="testuser"
-
HOST="localhost"
-
PORT="2222"
-
FILE="memory_functions_test.cpp"
-
-
echo "🚢 Battleship Arena Test Script"
-
echo "================================"
-
-
# Check if submission file exists
-
if [ ! -f "$1" ]; then
-
echo "Usage: $0 <memory_functions_*.cpp>"
-
exit 1
-
fi
-
-
FILE=$(basename "$1")
-
-
echo "📤 Uploading $FILE..."
-
scp -P $PORT "$1" ${USER}@${HOST}:~/
-
-
echo "✅ Upload complete!"
-
echo ""
-
echo "Next steps:"
-
echo "1. SSH into the server: ssh -p $PORT ${USER}@${HOST}"
-
echo "2. Navigate to 'Test Submission' in the menu"
-
echo "3. View results on the leaderboard: http://localhost:8080"
+128
scripts/test-submissions/memory_functions_hunter.cpp
···
+
#include "battleship.h"
+
#include "kasbs.h"
+
#include "memory.h"
+
#include <string>
+
+
using namespace std;
+
+
// Hunter AI - uses hunt/target mode (simpler than klukas)
+
inline bool onBoard(int row, int col) {
+
return row >= 0 && row < BOARDSIZE && col >= 0 && col < BOARDSIZE;
+
}
+
+
void initMemoryHunter(ComputerMemory &memory) {
+
memory.mode = RANDOM;
+
memory.hitRow = -1;
+
memory.hitCol = -1;
+
memory.hitShip = NONE;
+
memory.fireDir = NONE;
+
memory.fireDist = 1;
+
memory.lastResult = NONE;
+
+
for (int i = 0; i < BOARDSIZE; i++) {
+
for (int j = 0; j < BOARDSIZE; j++) {
+
memory.grid[i][j] = EMPTY_MARKER;
+
}
+
}
+
}
+
+
string smartMoveHunter(const ComputerMemory &memory) {
+
if (memory.mode == RANDOM) {
+
// Use checkerboard pattern for hunting
+
for (int i = 0; i < BOARDSIZE; i++) {
+
for (int j = 0; j < BOARDSIZE; j++) {
+
if ((i + j) % 2 == 0 && memory.grid[i][j] == EMPTY_MARKER) {
+
char letter = static_cast<char>('A' + i);
+
return string(1, letter) + to_string(j + 1);
+
}
+
}
+
}
+
+
// If no checkerboard cells left, use any empty cell
+
for (int i = 0; i < BOARDSIZE; i++) {
+
for (int j = 0; j < BOARDSIZE; j++) {
+
if (memory.grid[i][j] == EMPTY_MARKER) {
+
char letter = static_cast<char>('A' + i);
+
return string(1, letter) + to_string(j + 1);
+
}
+
}
+
}
+
}
+
+
// Target mode - try adjacent cells
+
int directions[4][2] = {{-1, 0}, {0, 1}, {1, 0}, {0, -1}}; // N, E, S, W
+
int dirIdx = memory.fireDir;
+
+
if (dirIdx == NONE) dirIdx = 0;
+
+
// Try current direction
+
for (int tries = 0; tries < 4; tries++) {
+
int idx = (dirIdx + tries) % 4;
+
int dr = directions[idx][0];
+
int dc = directions[idx][1];
+
int newRow = memory.hitRow + dr;
+
int newCol = memory.hitCol + dc;
+
+
if (onBoard(newRow, newCol) && memory.grid[newRow][newCol] == EMPTY_MARKER) {
+
char letter = static_cast<char>('A' + newRow);
+
return string(1, letter) + to_string(newCol + 1);
+
}
+
}
+
+
// Fallback to random
+
for (int i = 0; i < BOARDSIZE; i++) {
+
for (int j = 0; j < BOARDSIZE; j++) {
+
if (memory.grid[i][j] == EMPTY_MARKER) {
+
char letter = static_cast<char>('A' + i);
+
return string(1, letter) + to_string(j + 1);
+
}
+
}
+
}
+
+
return "A1";
+
}
+
+
void updateMemoryHunter(int row, int col, int result, ComputerMemory &memory) {
+
memory.lastResult = result;
+
char marker;
+
if (isAMiss(result)) {
+
marker = MISS_MARKER;
+
} else {
+
marker = HIT_MARKER;
+
}
+
memory.grid[row][col] = marker;
+
+
if (memory.mode == RANDOM) {
+
if (!isAMiss(result)) {
+
// Got a hit, switch to target mode
+
memory.mode = SEARCH;
+
memory.hitRow = row;
+
memory.hitCol = col;
+
memory.fireDir = NORTH; // Start trying north
+
}
+
} else {
+
// In target mode
+
if (isASunk(result)) {
+
// Sunk the ship, back to hunt mode
+
memory.mode = RANDOM;
+
memory.hitRow = -1;
+
memory.hitCol = -1;
+
memory.fireDir = NONE;
+
} else if (!isAMiss(result)) {
+
// Another hit, keep current direction
+
// (fireDir stays the same)
+
} else {
+
// Miss in target mode, try next direction
+
if (memory.fireDir == NORTH) memory.fireDir = EAST;
+
else if (memory.fireDir == EAST) memory.fireDir = SOUTH;
+
else if (memory.fireDir == SOUTH) memory.fireDir = WEST;
+
else {
+
// Tried all directions, back to hunt
+
memory.mode = RANDOM;
+
memory.hitRow = -1;
+
memory.hitCol = -1;
+
memory.fireDir = NONE;
+
}
+
}
+
}
+
}
+59
scripts/test-submissions/memory_functions_random.cpp
···
+
#include "battleship.h"
+
#include "kasbs.h"
+
#include "memory.h"
+
#include <string>
+
+
using namespace std;
+
+
// Random AI - just picks random valid moves
+
void initMemoryRandom(ComputerMemory &memory) {
+
memory.mode = RANDOM;
+
memory.hitRow = -1;
+
memory.hitCol = -1;
+
memory.hitShip = NONE;
+
memory.fireDir = NONE;
+
memory.fireDist = 1;
+
memory.lastResult = NONE;
+
+
for (int i = 0; i < BOARDSIZE; i++) {
+
for (int j = 0; j < BOARDSIZE; j++) {
+
memory.grid[i][j] = EMPTY_MARKER;
+
}
+
}
+
}
+
+
string smartMoveRandom(const ComputerMemory &memory) {
+
// Find all empty cells
+
for (int attempts = 0; attempts < 100; attempts++) {
+
int row = rand() % BOARDSIZE;
+
int col = rand() % BOARDSIZE;
+
+
if (memory.grid[row][col] == EMPTY_MARKER) {
+
char letter = static_cast<char>('A' + row);
+
return string(1, letter) + to_string(col + 1);
+
}
+
}
+
+
// Fallback: find first empty cell
+
for (int i = 0; i < BOARDSIZE; i++) {
+
for (int j = 0; j < BOARDSIZE; j++) {
+
if (memory.grid[i][j] == EMPTY_MARKER) {
+
char letter = static_cast<char>('A' + i);
+
return string(1, letter) + to_string(j + 1);
+
}
+
}
+
}
+
+
return "A1"; // Should never reach here
+
}
+
+
void updateMemoryRandom(int row, int col, int result, ComputerMemory &memory) {
+
memory.lastResult = result;
+
char marker;
+
if (isAMiss(result)) {
+
marker = MISS_MARKER;
+
} else {
+
marker = HIT_MARKER;
+
}
+
memory.grid[row][col] = marker;
+
}
+70
scripts/test-upload.sh
···
+
#!/bin/bash
+
+
# Test script to upload three different AI submissions
+
+
HOST="0.0.0.0"
+
PORT="2222"
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+
+
echo "🚢 Battleship Arena - Test Submission Script"
+
echo "=============================================="
+
echo ""
+
+
# Copy klukas submission to test-submissions if it doesn't exist
+
if [ ! -f "$SCRIPT_DIR/test-submissions/memory_functions_klukas.cpp" ]; then
+
echo "Copying klukas submission..."
+
mkdir -p "$SCRIPT_DIR/test-submissions"
+
cp /Users/kierank/code/school/cs1210-battleship/src/memory_functions_klukas.cpp "$SCRIPT_DIR/test-submissions/"
+
fi
+
+
# Upload for alice (random AI)
+
echo "📤 Uploading for user: alice"
+
echo " File: test-submissions/memory_functions_random.cpp"
+
if [ -f "$SCRIPT_DIR/test-submissions/memory_functions_random.cpp" ]; then
+
scp -P $PORT "$SCRIPT_DIR/test-submissions/memory_functions_random.cpp" "alice@$HOST:~/memory_functions_random.cpp"
+
if [ $? -eq 0 ]; then
+
echo "✅ Upload successful for alice"
+
else
+
echo "❌ Upload failed for alice"
+
fi
+
else
+
echo "❌ Error: File not found"
+
fi
+
echo ""
+
+
# Upload for bob (hunter AI)
+
echo "📤 Uploading for user: bob"
+
echo " File: test-submissions/memory_functions_hunter.cpp"
+
if [ -f "$SCRIPT_DIR/test-submissions/memory_functions_hunter.cpp" ]; then
+
scp -P $PORT "$SCRIPT_DIR/test-submissions/memory_functions_hunter.cpp" "bob@$HOST:~/memory_functions_hunter.cpp"
+
if [ $? -eq 0 ]; then
+
echo "✅ Upload successful for bob"
+
else
+
echo "❌ Upload failed for bob"
+
fi
+
else
+
echo "❌ Error: File not found"
+
fi
+
echo ""
+
+
# Upload for charlie (klukas AI)
+
echo "📤 Uploading for user: charlie"
+
echo " File: test-submissions/memory_functions_klukas.cpp"
+
if [ -f "$SCRIPT_DIR/test-submissions/memory_functions_klukas.cpp" ]; then
+
scp -P $PORT "$SCRIPT_DIR/test-submissions/memory_functions_klukas.cpp" "charlie@$HOST:~/memory_functions_klukas.cpp"
+
if [ $? -eq 0 ]; then
+
echo "✅ Upload successful for charlie"
+
else
+
echo "❌ Upload failed for charlie"
+
fi
+
else
+
echo "❌ Error: File not found"
+
fi
+
echo ""
+
+
echo "=============================================="
+
echo "✨ All submissions uploaded!"
+
echo ""
+
echo "You can now:"
+
echo " - SSH to view the TUI: ssh -p $PORT alice@$HOST"
+
echo " - Check the web leaderboard: http://$HOST:8080"
+6
sftp.go
···
dstPath := filepath.Join(h.baseDir, filename)
log.Printf("SFTP: Creating file %s for user %s", dstPath, h.username)
+
// Remove old file if it exists to ensure clean overwrite
+
if _, err := os.Stat(dstPath); err == nil {
+
log.Printf("SFTP: Removing old file: %s", dstPath)
+
os.Remove(dstPath)
+
}
+
flags := r.Pflags()
var osFlags int
if flags.Creat {