Initial commit - battleship

This commit is contained in:
Dan VANDACHEVICI 2026-04-09 08:35:28 +02:00
commit bb1b8f7abd
20 changed files with 3169 additions and 0 deletions

17
battleship/CMakeLists.txt Normal file
View file

@ -0,0 +1,17 @@
cmake_minimum_required(VERSION 3.16)
# Workaround: use CommandLineTools SDK if Xcode license is not accepted
if(APPLE AND NOT CMAKE_OSX_SYSROOT)
set(CMAKE_OSX_SYSROOT "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk" CACHE STRING "macOS SDK")
endif()
project(battleship LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
add_subdirectory(common)
add_subdirectory(server)
add_subdirectory(client)

57
battleship/build.sh Executable file
View file

@ -0,0 +1,57 @@
#!/bin/bash
# build.sh — Build script for Battleship (Phase 1)
# Works without CMake / make when Xcode license is not accepted.
set -e
CXX="${CXX:-/Library/Developer/CommandLineTools/usr/bin/c++}"
SYSROOT="${SYSROOT:-/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk}"
CXXFLAGS="-std=c++17 -isysroot $SYSROOT -Wall -Wextra -O2"
OUTDIR="build"
mkdir -p "$OUTDIR"
echo "=== Compiling common ==="
$CXX $CXXFLAGS -c -I common/include \
common/src/serialization.cpp -o "$OUTDIR/serialization.o"
echo "=== Compiling server ==="
$CXX $CXXFLAGS -c -I common/include -I server/include \
server/src/game_thread.cpp -o "$OUTDIR/game_thread.o"
$CXX $CXXFLAGS -c -I common/include -I server/include \
server/src/server.cpp -o "$OUTDIR/server.o"
$CXX $CXXFLAGS -c -I common/include -I server/include \
server/src/main.cpp -o "$OUTDIR/server_main.o"
echo "=== Compiling client ==="
$CXX $CXXFLAGS -c -I common/include -I client/include \
client/src/display.cpp -o "$OUTDIR/display.o"
$CXX $CXXFLAGS -c -I common/include -I client/include \
client/src/client.cpp -o "$OUTDIR/client.o"
$CXX $CXXFLAGS -c -I common/include -I client/include \
client/src/main.cpp -o "$OUTDIR/client_main.o"
echo "=== Linking server ==="
$CXX -isysroot "$SYSROOT" \
"$OUTDIR/serialization.o" \
"$OUTDIR/game_thread.o" \
"$OUTDIR/server.o" \
"$OUTDIR/server_main.o" \
-lpthread -o "$OUTDIR/battleship_server"
echo "=== Linking client ==="
$CXX -isysroot "$SYSROOT" \
"$OUTDIR/serialization.o" \
"$OUTDIR/display.o" \
"$OUTDIR/client.o" \
"$OUTDIR/client_main.o" \
-lpthread -o "$OUTDIR/battleship_client"
echo ""
echo "Build complete!"
echo " Server: $OUTDIR/battleship_server"
echo " Client: $OUTDIR/battleship_client"
echo ""
echo "Usage:"
echo " $OUTDIR/battleship_server [port] # default port: 8080"
echo " $OUTDIR/battleship_client [host] [port] # default: 127.0.0.1:8080"

View file

@ -0,0 +1,16 @@
add_executable(battleship_client
src/main.cpp
src/client.cpp
src/display.cpp
)
target_include_directories(battleship_client
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include
)
target_link_libraries(battleship_client
PRIVATE
common
pthread
)

View file

@ -0,0 +1,75 @@
#pragma once
#include <client/display.h>
#include <common/types.h>
#include <common/protocol.h>
#include <common/serialization.h>
#include <cstdint>
#include <string>
#include <vector>
namespace battleship {
/// Phase-1 CLI client. Fully single-threaded: the turn-based protocol
/// guarantees a deterministic message order, so no listener thread is needed.
class Client {
public:
Client(const std::string& host, uint16_t port);
~Client();
/// Open a TCP connection to the server.
bool connect();
/// Main entry-point: menu → create/join → placement → battle loop.
void run();
private:
// -- Network ------------------------------------------------------------
bool sendMsg(const Buffer& msg);
bool recvMsg(MessageHeader& header, Buffer& payload);
/// Read messages in a loop until one with the expected type arrives.
/// Other message types are dispatched to handleUnexpected().
/// Returns false on disconnect.
bool waitForMessage(MessageType expected, Buffer& out_payload);
/// Handle a message that arrived when we were waiting for something else.
void handleUnexpected(MessageType type, Buffer& payload);
// -- Game flow ----------------------------------------------------------
void showMainMenu();
void createGame();
void listAndJoinGame();
void waitForSetup(); ///< block until GameStateUpdate = SETUP
void placeShipsPhase();
void waitForGameStart(); ///< block until GameStateUpdate = IN_PROGRESS
void gameLoop();
// -- Helpers ------------------------------------------------------------
Ship createShipInteractively(uint8_t ship_index, uint8_t length);
// -- Connection ---------------------------------------------------------
std::string host_;
uint16_t port_;
int sock_fd_;
// -- Game state ---------------------------------------------------------
uint32_t game_id_ = 0;
uint32_t player_id_ = 0;
GameConfig game_config_{};
// Own board (for display)
std::vector<CellState> own_grid_;
std::vector<Ship> own_ships_;
// Tracking board (what we know about the enemy)
std::vector<CellState> tracking_grid_;
Display display_;
bool my_turn_ = false;
bool game_over_ = false;
};
} // namespace battleship

View file

@ -0,0 +1,44 @@
#pragma once
#include <common/types.h>
#include <cstdint>
#include <string>
#include <vector>
namespace battleship {
class Display {
public:
Display(uint16_t width, uint16_t height);
/// Render both boards (own + tracking) side by side.
void render(const std::vector<CellState>& own_grid,
const std::vector<Ship>& own_ships,
const std::vector<CellState>& tracking_grid,
uint16_t width, uint16_t height);
/// Show a message below the boards.
void showMessage(const std::string& msg);
/// Render just the own board during ship placement.
void renderPlacement(const std::vector<CellState>& grid,
const std::vector<Ship>& ships,
uint16_t width, uint16_t height);
/// Clear the terminal screen.
void clearScreen();
private:
/// Return the display character for a cell on the own board.
/// Checks if a ship occupies that cell and renders the correct part.
char ownCellChar(uint16_t x, uint16_t y,
const std::vector<CellState>& grid,
const std::vector<Ship>& ships,
uint16_t board_width) const;
/// Return the display character for a cell on the tracking board.
char trackingCellChar(CellState state) const;
};
} // namespace battleship

View file

@ -0,0 +1,677 @@
#include <client/client.h>
#include <common/protocol.h>
#include <common/serialization.h>
#include <common/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
#include <iostream>
#include <limits>
#include <string>
namespace battleship {
// ---------------------------------------------------------------------------
// Construction / Destruction
// ---------------------------------------------------------------------------
Client::Client(const std::string& host, uint16_t port)
: host_(host)
, port_(port)
, sock_fd_(-1)
, display_(10, 10)
{}
Client::~Client() {
if (sock_fd_ >= 0) {
::close(sock_fd_);
}
}
// ---------------------------------------------------------------------------
// Networking
// ---------------------------------------------------------------------------
bool Client::connect() {
sock_fd_ = ::socket(AF_INET, SOCK_STREAM, 0);
if (sock_fd_ < 0) {
std::cerr << "Error: failed to create socket\n";
return false;
}
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_port = htons(port_);
if (::inet_pton(AF_INET, host_.c_str(), &addr.sin_addr) <= 0) {
std::cerr << "Error: invalid address " << host_ << "\n";
::close(sock_fd_);
sock_fd_ = -1;
return false;
}
if (::connect(sock_fd_, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) < 0) {
std::cerr << "Error: connection refused (" << host_ << ":" << port_ << ")\n";
::close(sock_fd_);
sock_fd_ = -1;
return false;
}
return true;
}
bool Client::sendMsg(const Buffer& msg) {
return sendMessage(sock_fd_, msg);
}
bool Client::recvMsg(MessageHeader& header, Buffer& payload) {
return receiveMessage(sock_fd_, header, payload);
}
// ---------------------------------------------------------------------------
// Message waiting helper
// ---------------------------------------------------------------------------
bool Client::waitForMessage(MessageType expected, Buffer& out_payload) {
while (true) {
MessageHeader hdr{};
Buffer payload;
if (!recvMsg(hdr, payload)) {
std::cerr << "Disconnected from server.\n";
return false;
}
auto type = static_cast<MessageType>(hdr.message_type);
if (type == expected) {
out_payload = std::move(payload);
return true;
}
// Not the message we wanted — handle it and keep waiting.
handleUnexpected(type, payload);
}
}
void Client::handleUnexpected(MessageType type, Buffer& payload) {
switch (type) {
case MessageType::OPPONENT_BOMB_RESULT: {
auto result = deserializeOpponentBombResult(payload);
std::size_t idx = static_cast<std::size_t>(result.y) * game_config_.width
+ result.x;
own_grid_[idx] = result.hit ? CellState::HIT : CellState::MISS;
if (result.hit) {
Coordinate coord{result.x, result.y};
for (auto& ship : own_ships_) {
for (uint8_t i = 0; i < ship.length(); ++i) {
if (ship.cellAt(i) == coord) {
ship.hit_flags[i] = true;
break;
}
}
}
}
break;
}
case MessageType::GAME_STATE_UPDATE: {
auto update = deserializeGameStateUpdate(payload);
my_turn_ = (update.whose_turn == player_id_);
if (static_cast<GameState>(update.state) == GameState::FINISHED) {
game_over_ = true;
}
break;
}
case MessageType::ERROR_MSG: {
auto err = deserializeError(payload);
std::cerr << "Server error: " << err.message << "\n";
break;
}
default:
std::cerr << "Unexpected message type: 0x" << std::hex
<< static_cast<int>(type) << std::dec << "\n";
break;
}
}
// ---------------------------------------------------------------------------
// Main entry-point
// ---------------------------------------------------------------------------
void Client::run() {
showMainMenu();
}
// ---------------------------------------------------------------------------
// Main menu
// ---------------------------------------------------------------------------
void Client::showMainMenu() {
while (true) {
std::cout << "\n=== BATTLESHIP ===\n"
<< "1) Create game\n"
<< "2) Join game\n"
<< "3) Quit\n"
<< "Choice: ";
int choice = 0;
if (!(std::cin >> choice)) {
std::cin.clear();
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
continue;
}
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
switch (choice) {
case 1: createGame(); break;
case 2: listAndJoinGame(); break;
case 3: return;
default:
std::cout << "Invalid choice.\n";
continue;
}
if (game_id_ != 0) {
waitForSetup();
placeShipsPhase();
waitForGameStart();
gameLoop();
return;
}
}
}
// ---------------------------------------------------------------------------
// Create game
// ---------------------------------------------------------------------------
void Client::createGame() {
uint16_t w = 0, h = 0;
std::cout << "Board width: "; std::cin >> w;
std::cout << "Board height: "; std::cin >> h;
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
if (w == 0 || h == 0) {
std::cout << "Invalid board size.\n";
return;
}
uint16_t num_ships = 0;
std::cout << "Number of ship types: "; std::cin >> num_ships;
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
std::vector<ShipConfig> configs;
for (uint16_t i = 0; i < num_ships; ++i) {
uint16_t len = 0;
std::cout << " Ship " << (i + 1) << " length (2-6): "; std::cin >> len;
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
configs.push_back(ShipConfig{static_cast<uint8_t>(len)});
}
// Send CreateGameMessage
CreateGameMessage msg;
msg.width = w;
msg.height = h;
msg.ship_configs = configs;
if (!sendMsg(serialize(msg))) {
std::cerr << "Failed to send CreateGame.\n";
return;
}
// Receive CreateGameResponse
MessageHeader hdr{};
Buffer payload;
if (!recvMsg(hdr, payload)) {
std::cerr << "Failed to receive CreateGameResponse.\n";
return;
}
if (static_cast<MessageType>(hdr.message_type) == MessageType::ERROR_MSG) {
auto err = deserializeError(payload);
std::cerr << "Server error: " << err.message << "\n";
return;
}
auto resp = deserializeCreateGameResponse(payload);
if (!resp.success) {
std::cerr << "Server rejected game creation.\n";
return;
}
game_id_ = resp.game_id;
std::cout << "Game created (ID=" << game_id_ << "). Joining...\n";
// Auto-join the game we just created
std::string name;
std::cout << "Your name: ";
std::getline(std::cin, name);
if (name.empty()) name = "Player1";
JoinGameMessage join;
join.game_id = game_id_;
join.player_name = name;
if (!sendMsg(serialize(join))) {
std::cerr << "Failed to send JoinGame.\n";
game_id_ = 0;
return;
}
if (!recvMsg(hdr, payload)) {
std::cerr << "Failed to receive JoinGameResponse.\n";
game_id_ = 0;
return;
}
if (static_cast<MessageType>(hdr.message_type) == MessageType::ERROR_MSG) {
auto err = deserializeError(payload);
std::cerr << "Server error: " << err.message << "\n";
game_id_ = 0;
return;
}
auto jresp = deserializeJoinGameResponse(payload);
if (!jresp.success) {
std::cerr << "Failed to join game.\n";
game_id_ = 0;
return;
}
player_id_ = jresp.player_id;
game_config_ = jresp.game_config;
std::size_t sz = static_cast<std::size_t>(game_config_.width) * game_config_.height;
own_grid_.assign(sz, CellState::EMPTY);
tracking_grid_.assign(sz, CellState::EMPTY);
display_ = Display(game_config_.width, game_config_.height);
std::cout << "Joined game " << game_id_ << " as player " << player_id_ << ".\n";
}
// ---------------------------------------------------------------------------
// List & join
// ---------------------------------------------------------------------------
void Client::listAndJoinGame() {
ListGamesMessage list_msg;
if (!sendMsg(serialize(list_msg))) {
std::cerr << "Failed to send ListGames.\n";
return;
}
MessageHeader hdr{};
Buffer payload;
if (!recvMsg(hdr, payload)) {
std::cerr << "Failed to receive ListGamesResponse.\n";
return;
}
if (static_cast<MessageType>(hdr.message_type) == MessageType::ERROR_MSG) {
auto err = deserializeError(payload);
std::cerr << "Server error: " << err.message << "\n";
return;
}
auto resp = deserializeListGamesResponse(payload);
if (resp.games.empty()) {
std::cout << "No games available.\n";
return;
}
std::cout << "\nAvailable games:\n";
for (std::size_t i = 0; i < resp.games.size(); ++i) {
const auto& g = resp.games[i];
std::string state_str;
switch (static_cast<GameState>(g.state)) {
case GameState::WAITING_FOR_PLAYERS: state_str = "waiting"; break;
case GameState::SETUP: state_str = "setup"; break;
case GameState::IN_PROGRESS: state_str = "playing"; break;
case GameState::FINISHED: state_str = "done"; break;
}
std::cout << " " << (i + 1)
<< ") Game ID=" << g.game_id
<< " players=" << static_cast<int>(g.player_count)
<< " state=" << state_str << "\n";
}
std::cout << "Select game (number): ";
int sel = 0;
std::cin >> sel;
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
if (sel < 1 || sel > static_cast<int>(resp.games.size())) {
std::cout << "Invalid selection.\n";
return;
}
uint32_t chosen_id = resp.games[sel - 1].game_id;
std::string name;
std::cout << "Your name: ";
std::getline(std::cin, name);
if (name.empty()) name = "Player2";
JoinGameMessage join;
join.game_id = chosen_id;
join.player_name = name;
if (!sendMsg(serialize(join))) {
std::cerr << "Failed to send JoinGame.\n";
return;
}
if (!recvMsg(hdr, payload)) {
std::cerr << "Failed to receive JoinGameResponse.\n";
return;
}
if (static_cast<MessageType>(hdr.message_type) == MessageType::ERROR_MSG) {
auto err = deserializeError(payload);
std::cerr << "Server error: " << err.message << "\n";
return;
}
auto jresp = deserializeJoinGameResponse(payload);
if (!jresp.success) {
std::cerr << "Failed to join game.\n";
return;
}
game_id_ = chosen_id;
player_id_ = jresp.player_id;
game_config_ = jresp.game_config;
std::size_t sz = static_cast<std::size_t>(game_config_.width) * game_config_.height;
own_grid_.assign(sz, CellState::EMPTY);
tracking_grid_.assign(sz, CellState::EMPTY);
display_ = Display(game_config_.width, game_config_.height);
std::cout << "Joined game " << game_id_ << " as player " << player_id_ << ".\n";
}
// ---------------------------------------------------------------------------
// Wait for game to enter SETUP (both players joined)
// ---------------------------------------------------------------------------
void Client::waitForSetup() {
std::cout << "Waiting for opponent to join...\n";
Buffer payload;
if (!waitForMessage(MessageType::GAME_STATE_UPDATE, payload)) {
return;
}
auto update = deserializeGameStateUpdate(payload);
auto state = static_cast<GameState>(update.state);
if (state == GameState::SETUP || state == GameState::IN_PROGRESS) {
std::cout << "Opponent joined! Time to place your ships.\n";
}
}
// ---------------------------------------------------------------------------
// Ship placement
// ---------------------------------------------------------------------------
Ship Client::createShipInteractively(uint8_t ship_index, uint8_t length) {
while (true) {
display_.renderPlacement(own_grid_, own_ships_,
game_config_.width, game_config_.height);
std::cout << "Place ship " << static_cast<int>(ship_index + 1)
<< " (length " << static_cast<int>(length) << ")\n";
uint16_t x = 0, y = 0;
char ori_ch = 'h';
std::cout << " x: "; std::cin >> x;
std::cout << " y: "; std::cin >> y;
std::cout << " orientation (h/v): "; std::cin >> ori_ch;
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
Orientation ori = (ori_ch == 'v' || ori_ch == 'V')
? Orientation::VERTICAL
: Orientation::HORIZONTAL;
Ship ship(ship_index, length, Coordinate{x, y}, ori);
// Local validation
Board temp_board(game_config_.width, game_config_.height);
for (const auto& s : own_ships_) {
temp_board.placeShip(s);
}
if (!temp_board.isValidPlacement(ship)) {
std::cout << "Invalid placement! Ships must be within bounds and not overlap.\n";
continue;
}
return ship;
}
}
void Client::placeShipsPhase() {
own_ships_.clear();
for (std::size_t i = 0; i < game_config_.ship_configs.size(); ++i) {
uint8_t length = game_config_.ship_configs[i].length;
Ship ship = createShipInteractively(static_cast<uint8_t>(i), length);
for (uint8_t s = 0; s < ship.length(); ++s) {
Coordinate c = ship.cellAt(s);
std::size_t idx = static_cast<std::size_t>(c.y) * game_config_.width + c.x;
own_grid_[idx] = CellState::SHIP;
}
own_ships_.push_back(ship);
}
display_.renderPlacement(own_grid_, own_ships_,
game_config_.width, game_config_.height);
std::cout << "All ships placed. Sending to server...\n";
// Build PlaceShipsMessage
PlaceShipsMessage msg;
msg.game_id = game_id_;
msg.player_id = player_id_;
for (std::size_t i = 0; i < own_ships_.size(); ++i) {
const auto& s = own_ships_[i];
ShipPlacement sp;
sp.ship_type_index = static_cast<uint8_t>(i);
sp.x = s.position.x;
sp.y = s.position.y;
sp.orientation = static_cast<uint8_t>(s.orientation);
msg.placements.push_back(sp);
}
if (!sendMsg(serialize(msg))) {
std::cerr << "Failed to send PlaceShips.\n";
return;
}
// Wait for PlaceShipsResponse
Buffer payload;
if (!waitForMessage(MessageType::PLACE_SHIPS_RESPONSE, payload)) {
return;
}
auto resp = deserializePlaceShipsResponse(payload);
if (!resp.success) {
std::cerr << "Server rejected placement: " << resp.error_message << "\n";
// TODO: allow retry
return;
}
std::cout << "Ships accepted!\n";
}
// ---------------------------------------------------------------------------
// Wait for game to enter IN_PROGRESS (both players placed ships)
// ---------------------------------------------------------------------------
void Client::waitForGameStart() {
std::cout << "Waiting for game to start...\n";
Buffer payload;
if (!waitForMessage(MessageType::GAME_STATE_UPDATE, payload)) {
return;
}
auto update = deserializeGameStateUpdate(payload);
my_turn_ = (update.whose_turn == player_id_);
std::cout << "Game started! "
<< (my_turn_ ? "Your turn first." : "Opponent goes first.") << "\n";
}
// ---------------------------------------------------------------------------
// Game loop (single-threaded, turn-based)
// ---------------------------------------------------------------------------
void Client::gameLoop() {
game_over_ = false;
while (!game_over_) {
// Render current state
display_.render(own_grid_, own_ships_, tracking_grid_,
game_config_.width, game_config_.height);
if (my_turn_) {
// --- Our turn: prompt, bomb, read response, read state update ---
uint16_t bx = 0, by = 0;
std::cout << "Your turn! Enter bomb coordinates:\n";
std::cout << " x: "; std::cin >> bx;
std::cout << " y: "; std::cin >> by;
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
BombMessage bomb;
bomb.game_id = game_id_;
bomb.player_id = player_id_;
bomb.x = bx;
bomb.y = by;
if (!sendMsg(serialize(bomb))) {
std::cerr << "Failed to send Bomb.\n";
game_over_ = true;
break;
}
// Read BombResponse
Buffer payload;
if (!waitForMessage(MessageType::BOMB_RESPONSE, payload)) {
game_over_ = true;
break;
}
auto resp = deserializeBombResponse(payload);
// Update tracking grid
std::size_t idx = static_cast<std::size_t>(by) * game_config_.width + bx;
tracking_grid_[idx] = resp.hit ? CellState::HIT : CellState::MISS;
// Show result
display_.render(own_grid_, own_ships_, tracking_grid_,
game_config_.width, game_config_.height);
if (resp.hit) {
std::string result_msg = "HIT!";
if (resp.sunk) {
result_msg += " You sank a ship (length "
+ std::to_string(resp.ship_length) + ")!";
}
display_.showMessage(result_msg);
} else {
display_.showMessage("Miss.");
}
if (resp.game_over) {
game_over_ = true;
display_.showMessage(resp.winner_id == player_id_
? "*** YOU WIN! ***"
: "*** YOU LOSE! ***");
break;
}
// Read GameStateUpdate (turn change)
if (!waitForMessage(MessageType::GAME_STATE_UPDATE, payload)) {
game_over_ = true;
break;
}
auto update = deserializeGameStateUpdate(payload);
my_turn_ = (update.whose_turn == player_id_);
if (static_cast<GameState>(update.state) == GameState::FINISHED) {
game_over_ = true;
}
} else {
// --- Opponent's turn: wait for their bomb result, then state update ---
display_.showMessage("Waiting for opponent's move...");
Buffer payload;
if (!waitForMessage(MessageType::OPPONENT_BOMB_RESULT, payload)) {
game_over_ = true;
break;
}
auto result = deserializeOpponentBombResult(payload);
// Update own grid
std::size_t idx = static_cast<std::size_t>(result.y) * game_config_.width
+ result.x;
own_grid_[idx] = result.hit ? CellState::HIT : CellState::MISS;
// Update ship hit_flags
if (result.hit) {
Coordinate coord{result.x, result.y};
for (auto& ship : own_ships_) {
for (uint8_t i = 0; i < ship.length(); ++i) {
if (ship.cellAt(i) == coord) {
ship.hit_flags[i] = true;
break;
}
}
}
}
// Show what happened
display_.render(own_grid_, own_ships_, tracking_grid_,
game_config_.width, game_config_.height);
if (result.hit) {
std::string msg = "Opponent HIT your ship at (" +
std::to_string(result.x) + "," + std::to_string(result.y) + ")!";
if (result.sunk) {
msg += " Your ship (length " +
std::to_string(result.your_ship_sunk_length) + ") was sunk!";
}
display_.showMessage(msg);
} else {
display_.showMessage("Opponent missed at (" +
std::to_string(result.x) + "," + std::to_string(result.y) + ").");
}
// Read GameStateUpdate (turn change or game over)
if (!waitForMessage(MessageType::GAME_STATE_UPDATE, payload)) {
game_over_ = true;
break;
}
auto update = deserializeGameStateUpdate(payload);
my_turn_ = (update.whose_turn == player_id_);
if (static_cast<GameState>(update.state) == GameState::FINISHED) {
game_over_ = true;
display_.showMessage("*** GAME OVER ***");
}
}
}
std::cout << "\nPress Enter to exit...";
std::cin.get();
}
} // namespace battleship

View file

@ -0,0 +1,234 @@
#include <client/display.h>
#include <iostream>
#include <iomanip>
#include <sstream>
#include <string>
namespace battleship {
// ---------------------------------------------------------------------------
// Construction
// ---------------------------------------------------------------------------
Display::Display(uint16_t /*width*/, uint16_t /*height*/)
{}
// ---------------------------------------------------------------------------
// Screen helpers
// ---------------------------------------------------------------------------
void Display::clearScreen() {
// ANSI escape: clear screen + move cursor to top-left
std::cout << "\033[2J\033[H" << std::flush;
}
// ---------------------------------------------------------------------------
// Cell character helpers
// ---------------------------------------------------------------------------
char Display::ownCellChar(uint16_t x, uint16_t y,
const std::vector<CellState>& grid,
const std::vector<Ship>& ships,
uint16_t board_width) const
{
std::size_t idx = static_cast<std::size_t>(y) * board_width + x;
CellState state = grid[idx];
if (state == CellState::MISS) {
return 'o';
}
// Check if a ship occupies this cell
for (const auto& ship : ships) {
for (uint8_t i = 0; i < ship.length(); ++i) {
Coordinate c = ship.cellAt(i);
if (c.x == x && c.y == y) {
// This cell belongs to this ship at segment index `i`
if (ship.hit_flags[i]) {
return 'x'; // hit segment
}
// Determine visual based on orientation + position in hull
bool is_bow = (i == 0);
bool is_stern = (i == ship.length() - 1);
if (ship.orientation == Orientation::HORIZONTAL) {
if (is_bow) return '<';
if (is_stern) return '>';
return '=';
} else {
// Vertical
if (is_bow) return '^';
if (is_stern) return 'v';
return '#';
}
}
}
}
if (state == CellState::HIT) {
return 'x'; // fallback (shouldn't normally happen)
}
return '~'; // EMPTY or SHIP with no matching ship object
}
char Display::trackingCellChar(CellState state) const {
switch (state) {
case CellState::HIT: return 'X';
case CellState::MISS: return 'o';
default: return '~';
}
}
// ---------------------------------------------------------------------------
// render show both boards side by side
// ---------------------------------------------------------------------------
void Display::render(const std::vector<CellState>& own_grid,
const std::vector<Ship>& own_ships,
const std::vector<CellState>& tracking_grid,
uint16_t width, uint16_t height)
{
clearScreen();
// Build row strings for each board
std::vector<std::string> own_rows(height);
std::vector<std::string> tracking_rows(height);
for (uint16_t r = 0; r < height; ++r) {
std::string own_line;
std::string trk_line;
for (uint16_t c = 0; c < width; ++c) {
own_line += ownCellChar(c, r, own_grid, own_ships, width);
std::size_t idx = static_cast<std::size_t>(r) * width + c;
trk_line += trackingCellChar(tracking_grid[idx]);
}
own_rows[r] = own_line;
tracking_rows[r] = trk_line;
}
// Print side by side by building combined lines
// Title line
int board_display_width = 4 + width * 3 + 1; // "XX | " prefix + cells + "|"
std::string own_title = "YOUR BOARD";
std::string enemy_title = "ENEMY BOARD";
// Column headers for both boards
std::ostringstream oss;
// --- YOUR BOARD title ---
oss << " " << own_title;
int pad = board_display_width - 2 - static_cast<int>(own_title.size());
if (pad > 0) oss << std::string(pad, ' ');
oss << " ";
oss << " " << enemy_title << "\n";
// Column numbers — own board
oss << " ";
for (uint16_t c = 0; c < width; ++c) {
oss << std::setw(2) << c << " ";
}
oss << " ";
oss << " ";
for (uint16_t c = 0; c < width; ++c) {
oss << std::setw(2) << c << " ";
}
oss << "\n";
// Top border — both
oss << " +";
for (uint16_t c = 0; c < width; ++c) oss << "---";
oss << "+";
oss << " ";
oss << " +";
for (uint16_t c = 0; c < width; ++c) oss << "---";
oss << "+\n";
// Data rows
for (uint16_t r = 0; r < height; ++r) {
// Own board row
oss << std::setw(2) << r << " | ";
for (char ch : own_rows[r]) {
oss << ch << " ";
}
oss << "|";
oss << " ";
// Tracking board row
oss << std::setw(2) << r << " | ";
for (char ch : tracking_rows[r]) {
oss << ch << " ";
}
oss << "|\n";
}
// Bottom border — both
oss << " +";
for (uint16_t c = 0; c < width; ++c) oss << "---";
oss << "+";
oss << " ";
oss << " +";
for (uint16_t c = 0; c < width; ++c) oss << "---";
oss << "+\n";
std::cout << oss.str() << std::flush;
}
// ---------------------------------------------------------------------------
// showMessage
// ---------------------------------------------------------------------------
void Display::showMessage(const std::string& msg) {
std::cout << "\n" << msg << "\n" << std::flush;
}
// ---------------------------------------------------------------------------
// renderPlacement single board for ship placement phase
// ---------------------------------------------------------------------------
void Display::renderPlacement(const std::vector<CellState>& grid,
const std::vector<Ship>& ships,
uint16_t width, uint16_t height)
{
clearScreen();
std::vector<std::string> rows(height);
for (uint16_t r = 0; r < height; ++r) {
std::string line;
for (uint16_t c = 0; c < width; ++c) {
line += ownCellChar(c, r, grid, ships, width);
}
rows[r] = line;
}
// Print single board
std::ostringstream oss;
oss << " SHIP PLACEMENT\n";
oss << " ";
for (uint16_t c = 0; c < width; ++c) {
oss << std::setw(2) << c << " ";
}
oss << "\n";
oss << " +";
for (uint16_t c = 0; c < width; ++c) oss << "---";
oss << "+\n";
for (uint16_t r = 0; r < height; ++r) {
oss << std::setw(2) << r << " | ";
for (char ch : rows[r]) {
oss << ch << " ";
}
oss << "|\n";
}
oss << " +";
for (uint16_t c = 0; c < width; ++c) oss << "---";
oss << "+\n";
std::cout << oss.str() << std::flush;
}
} // namespace battleship

View file

@ -0,0 +1,31 @@
#include <client/client.h>
#include <cstdlib>
#include <iostream>
#include <string>
int main(int argc, char* argv[]) {
std::string host = "127.0.0.1";
uint16_t port = 8080;
if (argc >= 2) {
host = argv[1];
}
if (argc >= 3) {
port = static_cast<uint16_t>(std::atoi(argv[2]));
}
std::cout << "Battleship Client\n";
std::cout << "Connecting to " << host << ":" << port << "...\n";
battleship::Client client(host, port);
if (!client.connect()) {
std::cerr << "Failed to connect to server.\n";
return 1;
}
std::cout << "Connected!\n";
client.run();
return 0;
}

View file

@ -0,0 +1,10 @@
add_library(common
src/serialization.cpp
)
target_include_directories(common
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
)
target_compile_features(common PUBLIC cxx_std_17)

View file

@ -0,0 +1,218 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
// Note: Orientation and GameState enums are defined in types.h
namespace battleship {
// ---------------------------------------------------------------------------
// MessageType enum
// ---------------------------------------------------------------------------
enum class MessageType : uint8_t {
CREATE_GAME = 0x01,
CREATE_GAME_RESPONSE = 0x02,
LIST_GAMES = 0x03,
LIST_GAMES_RESPONSE = 0x04,
JOIN_GAME = 0x05,
JOIN_GAME_RESPONSE = 0x06,
PLACE_SHIPS = 0x07,
PLACE_SHIPS_RESPONSE = 0x08,
BOMB = 0x09,
BOMB_RESPONSE = 0x0A,
GAME_STATE_UPDATE = 0x0B,
OPPONENT_BOMB_RESULT = 0x0C,
ERROR_MSG = 0xFF
};
// ---------------------------------------------------------------------------
// Message header — precedes every message on the wire
// ---------------------------------------------------------------------------
struct MessageHeader {
uint8_t message_type; // MessageType cast to uint8_t
uint32_t payload_size; // size of the payload that follows (bytes)
};
// ---------------------------------------------------------------------------
// Shared config structs
// ---------------------------------------------------------------------------
struct ShipConfig {
uint8_t length;
};
struct GameConfig {
uint16_t width;
uint16_t height;
std::vector<ShipConfig> ship_configs;
};
// ---------------------------------------------------------------------------
// 1. CREATE_GAME (client → server)
// Payload: width(u16), height(u16), ship_count(u16),
// ship_count × { length(u8) }
// ---------------------------------------------------------------------------
struct CreateGameMessage {
static constexpr MessageType TYPE = MessageType::CREATE_GAME;
uint16_t width;
uint16_t height;
std::vector<ShipConfig> ship_configs; // count inferred from size()
};
// ---------------------------------------------------------------------------
// 2. CREATE_GAME_RESPONSE (server → client)
// Payload: game_id(u32), success(bool/u8)
// ---------------------------------------------------------------------------
struct CreateGameResponseMessage {
static constexpr MessageType TYPE = MessageType::CREATE_GAME_RESPONSE;
uint32_t game_id;
bool success;
};
// ---------------------------------------------------------------------------
// 3. LIST_GAMES (client → server)
// Payload: (none)
// ---------------------------------------------------------------------------
struct ListGamesMessage {
static constexpr MessageType TYPE = MessageType::LIST_GAMES;
};
// ---------------------------------------------------------------------------
// 4. LIST_GAMES_RESPONSE (server → client)
// Payload: count(u16), count × { game_id(u32), player_count(u8), state(u8) }
// ---------------------------------------------------------------------------
struct GameListEntry {
uint32_t game_id;
uint8_t player_count;
uint8_t state; // GameState cast to uint8_t
};
struct ListGamesResponseMessage {
static constexpr MessageType TYPE = MessageType::LIST_GAMES_RESPONSE;
std::vector<GameListEntry> games; // count inferred from size()
};
// ---------------------------------------------------------------------------
// 5. JOIN_GAME (client → server)
// Payload: game_id(u32), name_len(u16), name_bytes[name_len]
// ---------------------------------------------------------------------------
struct JoinGameMessage {
static constexpr MessageType TYPE = MessageType::JOIN_GAME;
uint32_t game_id;
std::string player_name; // length-prefixed on the wire
};
// ---------------------------------------------------------------------------
// 6. JOIN_GAME_RESPONSE (server → client)
// Payload: success(u8), player_id(u32),
// width(u16), height(u16), ship_count(u16),
// ship_count × { length(u8) }
// ---------------------------------------------------------------------------
struct JoinGameResponseMessage {
static constexpr MessageType TYPE = MessageType::JOIN_GAME_RESPONSE;
bool success;
uint32_t player_id;
GameConfig game_config;
};
// ---------------------------------------------------------------------------
// 7. PLACE_SHIPS (client → server)
// Payload: game_id(u32), player_id(u32), ship_count(u8),
// ship_count × { ship_type_index(u8), x(u16), y(u16), orientation(u8) }
// ---------------------------------------------------------------------------
struct ShipPlacement {
uint8_t ship_type_index;
uint16_t x;
uint16_t y;
uint8_t orientation; // Orientation cast to uint8_t
};
struct PlaceShipsMessage {
static constexpr MessageType TYPE = MessageType::PLACE_SHIPS;
uint32_t game_id;
uint32_t player_id;
std::vector<ShipPlacement> placements; // count inferred from size()
};
// ---------------------------------------------------------------------------
// 8. PLACE_SHIPS_RESPONSE (server → client)
// Payload: success(u8), error_len(u16), error_bytes[error_len]
// ---------------------------------------------------------------------------
struct PlaceShipsResponseMessage {
static constexpr MessageType TYPE = MessageType::PLACE_SHIPS_RESPONSE;
bool success;
std::string error_message; // empty when success == true
};
// ---------------------------------------------------------------------------
// 9. BOMB (client → server)
// Payload: game_id(u32), player_id(u32), x(u16), y(u16)
// ---------------------------------------------------------------------------
struct BombMessage {
static constexpr MessageType TYPE = MessageType::BOMB;
uint32_t game_id;
uint32_t player_id;
uint16_t x;
uint16_t y;
};
// ---------------------------------------------------------------------------
// 10. BOMB_RESPONSE (server → client)
// Payload: hit(u8), sunk(u8), ship_length(u8), game_over(u8), winner_id(u32)
// ---------------------------------------------------------------------------
struct BombResponseMessage {
static constexpr MessageType TYPE = MessageType::BOMB_RESPONSE;
bool hit;
bool sunk;
uint8_t ship_length; // length of the sunk ship, 0 if !sunk
bool game_over;
uint32_t winner_id; // meaningful only when game_over == true
};
// ---------------------------------------------------------------------------
// 11. GAME_STATE_UPDATE (server → client)
// Payload: whose_turn(u32), state(u8)
// ---------------------------------------------------------------------------
struct GameStateUpdateMessage {
static constexpr MessageType TYPE = MessageType::GAME_STATE_UPDATE;
uint32_t whose_turn;
uint8_t state; // GameState cast to uint8_t
};
// ---------------------------------------------------------------------------
// 12. OPPONENT_BOMB_RESULT (server → client)
// Payload: x(u16), y(u16), hit(u8), sunk(u8), your_ship_sunk_length(u8)
// ---------------------------------------------------------------------------
struct OpponentBombResultMessage {
static constexpr MessageType TYPE = MessageType::OPPONENT_BOMB_RESULT;
uint16_t x;
uint16_t y;
bool hit;
bool sunk;
uint8_t your_ship_sunk_length; // 0 if !sunk
};
// ---------------------------------------------------------------------------
// 13. ERROR (server → client)
// Payload: error_code(u8), msg_len(u16), msg_bytes[msg_len]
// ---------------------------------------------------------------------------
struct ErrorMessage {
static constexpr MessageType TYPE = MessageType::ERROR_MSG;
uint8_t error_code;
std::string message; // length-prefixed on the wire
};
} // namespace battleship

View file

@ -0,0 +1,144 @@
#pragma once
#include <common/protocol.h>
#include <cstdint>
#include <cstring>
#include <stdexcept>
#include <string>
#include <vector>
namespace battleship {
// ---------------------------------------------------------------------------
// Buffer — a simple byte buffer for binary serialization/deserialization
// ---------------------------------------------------------------------------
class Buffer {
public:
Buffer() = default;
explicit Buffer(const std::vector<uint8_t>& data) : data_(data), read_pos_(0) {}
explicit Buffer(std::vector<uint8_t>&& data) : data_(std::move(data)), read_pos_(0) {}
// -- Writing ------------------------------------------------------------
void writeU8(uint8_t val) { data_.push_back(val); }
void writeU16(uint16_t val) {
data_.push_back(static_cast<uint8_t>(val & 0xFF));
data_.push_back(static_cast<uint8_t>((val >> 8) & 0xFF));
}
void writeU32(uint32_t val) {
data_.push_back(static_cast<uint8_t>(val & 0xFF));
data_.push_back(static_cast<uint8_t>((val >> 8) & 0xFF));
data_.push_back(static_cast<uint8_t>((val >> 16) & 0xFF));
data_.push_back(static_cast<uint8_t>((val >> 24) & 0xFF));
}
void writeBool(bool val) { writeU8(val ? 1 : 0); }
void writeString(const std::string& s) {
writeU16(static_cast<uint16_t>(s.size()));
data_.insert(data_.end(), s.begin(), s.end());
}
// -- Reading ------------------------------------------------------------
uint8_t readU8() {
checkReadable(1);
return data_[read_pos_++];
}
uint16_t readU16() {
checkReadable(2);
uint16_t val = static_cast<uint16_t>(data_[read_pos_])
| (static_cast<uint16_t>(data_[read_pos_ + 1]) << 8);
read_pos_ += 2;
return val;
}
uint32_t readU32() {
checkReadable(4);
uint32_t val = static_cast<uint32_t>(data_[read_pos_])
| (static_cast<uint32_t>(data_[read_pos_ + 1]) << 8)
| (static_cast<uint32_t>(data_[read_pos_ + 2]) << 16)
| (static_cast<uint32_t>(data_[read_pos_ + 3]) << 24);
read_pos_ += 4;
return val;
}
bool readBool() { return readU8() != 0; }
std::string readString() {
uint16_t len = readU16();
checkReadable(len);
std::string s(data_.begin() + read_pos_,
data_.begin() + read_pos_ + len);
read_pos_ += len;
return s;
}
// -- Access -------------------------------------------------------------
const std::vector<uint8_t>& data() const { return data_; }
std::size_t size() const { return data_.size(); }
std::size_t readPos() const { return read_pos_; }
std::size_t remaining() const { return data_.size() - read_pos_; }
private:
void checkReadable(std::size_t bytes) const {
if (read_pos_ + bytes > data_.size()) {
throw std::runtime_error("Buffer underflow");
}
}
std::vector<uint8_t> data_;
std::size_t read_pos_ = 0;
};
// ---------------------------------------------------------------------------
// Serialization functions
// ---------------------------------------------------------------------------
// Serialize a message into a buffer (header + payload)
Buffer serialize(const MessageHeader& header, const Buffer& payload);
// Serialize individual message types into full wire-format buffers
Buffer serialize(const CreateGameMessage& msg);
Buffer serialize(const CreateGameResponseMessage& msg);
Buffer serialize(const ListGamesMessage& msg);
Buffer serialize(const ListGamesResponseMessage& msg);
Buffer serialize(const JoinGameMessage& msg);
Buffer serialize(const JoinGameResponseMessage& msg);
Buffer serialize(const PlaceShipsMessage& msg);
Buffer serialize(const PlaceShipsResponseMessage& msg);
Buffer serialize(const BombMessage& msg);
Buffer serialize(const BombResponseMessage& msg);
Buffer serialize(const GameStateUpdateMessage& msg);
Buffer serialize(const OpponentBombResultMessage& msg);
Buffer serialize(const ErrorMessage& msg);
// Deserialization functions — given a payload buffer
CreateGameMessage deserializeCreateGame(Buffer& buf);
CreateGameResponseMessage deserializeCreateGameResponse(Buffer& buf);
ListGamesResponseMessage deserializeListGamesResponse(Buffer& buf);
JoinGameMessage deserializeJoinGame(Buffer& buf);
JoinGameResponseMessage deserializeJoinGameResponse(Buffer& buf);
PlaceShipsMessage deserializePlaceShips(Buffer& buf);
PlaceShipsResponseMessage deserializePlaceShipsResponse(Buffer& buf);
BombMessage deserializeBomb(Buffer& buf);
BombResponseMessage deserializeBombResponse(Buffer& buf);
GameStateUpdateMessage deserializeGameStateUpdate(Buffer& buf);
OpponentBombResultMessage deserializeOpponentBombResult(Buffer& buf);
ErrorMessage deserializeError(Buffer& buf);
// Helper: read a MessageHeader from raw bytes
MessageHeader deserializeHeader(const uint8_t* data);
// Helper: write a full message (header + payload) to a socket fd
bool sendMessage(int fd, const Buffer& message);
// Helper: read a full message (header + payload) from a socket fd
// Returns false on disconnect or error
bool receiveMessage(int fd, MessageHeader& header, Buffer& payload);
} // namespace battleship

View file

@ -0,0 +1,256 @@
#pragma once
#include <algorithm>
#include <cstdint>
#include <string>
#include <utility>
#include <vector>
namespace battleship {
// ---------------------------------------------------------------------------
// Coordinate
// ---------------------------------------------------------------------------
struct Coordinate {
uint16_t x = 0;
uint16_t y = 0;
bool operator==(const Coordinate& other) const {
return x == other.x && y == other.y;
}
bool operator!=(const Coordinate& other) const {
return !(*this == other);
}
};
// ---------------------------------------------------------------------------
// Orientation
// ---------------------------------------------------------------------------
enum class Orientation : uint8_t {
HORIZONTAL,
VERTICAL
};
// ---------------------------------------------------------------------------
// CellState
// ---------------------------------------------------------------------------
enum class CellState : uint8_t {
EMPTY,
SHIP,
HIT,
MISS
};
// ---------------------------------------------------------------------------
// ShipType
// ---------------------------------------------------------------------------
struct ShipType {
uint8_t id = 0;
uint8_t length = 0; // valid range: 26
std::string name;
};
// ---------------------------------------------------------------------------
// Ship
// ---------------------------------------------------------------------------
struct Ship {
uint8_t ship_type_id = 0;
Coordinate position; // top-left cell of the ship
Orientation orientation = Orientation::HORIZONTAL;
std::vector<bool> hit_flags; // one flag per cell, true = hit
/// Construct a ship with `length` cells, all initially not hit.
explicit Ship(uint8_t type_id = 0,
uint8_t length = 0,
Coordinate pos = {},
Orientation ori = Orientation::HORIZONTAL)
: ship_type_id(type_id)
, position(pos)
, orientation(ori)
, hit_flags(length, false)
{}
/// Number of cells this ship occupies.
uint8_t length() const { return static_cast<uint8_t>(hit_flags.size()); }
/// True when every cell has been hit.
bool isSunk() const {
for (std::size_t i = 0; i < hit_flags.size(); ++i) {
if (!hit_flags[i]) return false;
}
return !hit_flags.empty();
}
/// Return the coordinate of the i-th cell (0-based).
Coordinate cellAt(uint8_t index) const {
Coordinate c = position;
if (orientation == Orientation::HORIZONTAL) {
c.x = static_cast<uint16_t>(c.x + index);
} else {
c.y = static_cast<uint16_t>(c.y + index);
}
return c;
}
/// Compute the rotation pivot index.
/// Even length → length/2, Odd length → length/2 + 1.
uint8_t pivotIndex() const {
uint8_t len = length();
return (len % 2 == 0) ? static_cast<uint8_t>(len / 2)
: static_cast<uint8_t>(len / 2 + 1);
}
};
// ---------------------------------------------------------------------------
// Board
// ---------------------------------------------------------------------------
class Board {
public:
Board(uint16_t width, uint16_t height)
: width_(width)
, height_(height)
, grid_(static_cast<std::size_t>(width) * height, CellState::EMPTY)
{}
// -- Accessors ----------------------------------------------------------
uint16_t width() const { return width_; }
uint16_t height() const { return height_; }
const std::vector<CellState>& getGrid() const { return grid_; }
const std::vector<Ship>& getShips() const { return ships_; }
CellState cellAt(uint16_t x, uint16_t y) const {
return grid_[index(x, y)];
}
// -- Ship placement -----------------------------------------------------
/// Validate that `ship` can be placed on this board (in bounds, no
/// overlap with existing ship cells).
bool isValidPlacement(const Ship& ship) const {
uint8_t len = ship.length();
if (len < 2 || len > 6) return false;
for (uint8_t i = 0; i < len; ++i) {
Coordinate c = ship.cellAt(i);
if (c.x >= width_ || c.y >= height_) return false;
if (grid_[index(c.x, c.y)] != CellState::EMPTY) return false;
}
return true;
}
/// Place a ship on the board. Returns false if placement is invalid.
bool placeShip(const Ship& ship) {
if (!isValidPlacement(ship)) return false;
ships_.push_back(ship);
Ship& placed = ships_.back();
for (uint8_t i = 0; i < placed.length(); ++i) {
Coordinate c = placed.cellAt(i);
grid_[index(c.x, c.y)] = CellState::SHIP;
}
return true;
}
// -- Bombing ------------------------------------------------------------
/// Bomb the given coordinate.
/// Returns {hit, sunk}.
/// hit true if a ship cell was struck
/// sunk true if that hit caused the ship to sink
/// Bombing the same cell twice returns {false, false}.
std::pair<bool, bool> receiveBombing(const Coordinate& coord) {
if (coord.x >= width_ || coord.y >= height_) {
return {false, false};
}
std::size_t idx = index(coord.x, coord.y);
CellState& cell = grid_[idx];
// Already bombed no effect.
if (cell == CellState::HIT || cell == CellState::MISS) {
return {false, false};
}
if (cell == CellState::SHIP) {
cell = CellState::HIT;
// Find the ship that owns this cell and mark its hit flag.
for (auto& ship : ships_) {
for (uint8_t i = 0; i < ship.length(); ++i) {
if (ship.cellAt(i) == coord) {
ship.hit_flags[i] = true;
return {true, ship.isSunk()};
}
}
}
// Should not reach here if state is consistent.
return {true, false};
}
// EMPTY cell miss.
cell = CellState::MISS;
return {false, false};
}
// -- Query --------------------------------------------------------------
/// True when every ship on the board has been sunk.
bool allShipsSunk() const {
if (ships_.empty()) return false;
return std::all_of(ships_.cbegin(), ships_.cend(),
[](const Ship& s) { return s.isSunk(); });
}
private:
std::size_t index(uint16_t x, uint16_t y) const {
return static_cast<std::size_t>(y) * width_ + x;
}
uint16_t width_;
uint16_t height_;
std::vector<CellState> grid_;
std::vector<Ship> ships_;
};
// ---------------------------------------------------------------------------
// GameState
// ---------------------------------------------------------------------------
enum class GameState : uint8_t {
WAITING_FOR_PLAYERS = 0,
SETUP = 1,
IN_PROGRESS = 2,
FINISHED = 3
};
// ---------------------------------------------------------------------------
// PlayerInfo
// ---------------------------------------------------------------------------
struct PlayerInfo {
uint32_t player_id = 0;
std::string name;
Board board;
bool ready = false;
explicit PlayerInfo(uint32_t id = 0,
std::string player_name = {},
uint16_t board_width = 10,
uint16_t board_height = 10)
: player_id(id)
, name(std::move(player_name))
, board(board_width, board_height)
, ready(false)
{}
};
} // namespace battleship

View file

@ -0,0 +1,363 @@
#include <common/serialization.h>
#include <cstring>
#include <sys/socket.h>
#include <unistd.h>
namespace battleship {
// ---------------------------------------------------------------------------
// Helper: wrap a payload with a MessageHeader to form a complete wire message
// ---------------------------------------------------------------------------
Buffer serialize(const MessageHeader& header, const Buffer& payload) {
Buffer result;
result.writeU8(header.message_type);
result.writeU32(header.payload_size);
const auto& pd = payload.data();
for (auto byte : pd) {
result.writeU8(byte);
}
return result;
}
// ---------------------------------------------------------------------------
// Serialize overloads
// ---------------------------------------------------------------------------
Buffer serialize(const CreateGameMessage& msg) {
Buffer payload;
payload.writeU16(msg.width);
payload.writeU16(msg.height);
payload.writeU16(static_cast<uint16_t>(msg.ship_configs.size()));
for (const auto& sc : msg.ship_configs) {
payload.writeU8(sc.length);
}
MessageHeader header{static_cast<uint8_t>(CreateGameMessage::TYPE),
static_cast<uint32_t>(payload.size())};
return serialize(header, payload);
}
Buffer serialize(const CreateGameResponseMessage& msg) {
Buffer payload;
payload.writeU32(msg.game_id);
payload.writeBool(msg.success);
MessageHeader header{static_cast<uint8_t>(CreateGameResponseMessage::TYPE),
static_cast<uint32_t>(payload.size())};
return serialize(header, payload);
}
Buffer serialize(const ListGamesMessage& /*msg*/) {
Buffer payload; // empty payload
MessageHeader header{static_cast<uint8_t>(ListGamesMessage::TYPE),
static_cast<uint32_t>(payload.size())};
return serialize(header, payload);
}
Buffer serialize(const ListGamesResponseMessage& msg) {
Buffer payload;
payload.writeU16(static_cast<uint16_t>(msg.games.size()));
for (const auto& entry : msg.games) {
payload.writeU32(entry.game_id);
payload.writeU8(entry.player_count);
payload.writeU8(entry.state);
}
MessageHeader header{static_cast<uint8_t>(ListGamesResponseMessage::TYPE),
static_cast<uint32_t>(payload.size())};
return serialize(header, payload);
}
Buffer serialize(const JoinGameMessage& msg) {
Buffer payload;
payload.writeU32(msg.game_id);
payload.writeString(msg.player_name);
MessageHeader header{static_cast<uint8_t>(JoinGameMessage::TYPE),
static_cast<uint32_t>(payload.size())};
return serialize(header, payload);
}
Buffer serialize(const JoinGameResponseMessage& msg) {
Buffer payload;
payload.writeBool(msg.success);
payload.writeU32(msg.player_id);
payload.writeU16(msg.game_config.width);
payload.writeU16(msg.game_config.height);
payload.writeU16(static_cast<uint16_t>(msg.game_config.ship_configs.size()));
for (const auto& sc : msg.game_config.ship_configs) {
payload.writeU8(sc.length);
}
MessageHeader header{static_cast<uint8_t>(JoinGameResponseMessage::TYPE),
static_cast<uint32_t>(payload.size())};
return serialize(header, payload);
}
Buffer serialize(const PlaceShipsMessage& msg) {
Buffer payload;
payload.writeU32(msg.game_id);
payload.writeU32(msg.player_id);
payload.writeU8(static_cast<uint8_t>(msg.placements.size()));
for (const auto& p : msg.placements) {
payload.writeU8(p.ship_type_index);
payload.writeU16(p.x);
payload.writeU16(p.y);
payload.writeU8(p.orientation);
}
MessageHeader header{static_cast<uint8_t>(PlaceShipsMessage::TYPE),
static_cast<uint32_t>(payload.size())};
return serialize(header, payload);
}
Buffer serialize(const PlaceShipsResponseMessage& msg) {
Buffer payload;
payload.writeBool(msg.success);
payload.writeString(msg.error_message);
MessageHeader header{static_cast<uint8_t>(PlaceShipsResponseMessage::TYPE),
static_cast<uint32_t>(payload.size())};
return serialize(header, payload);
}
Buffer serialize(const BombMessage& msg) {
Buffer payload;
payload.writeU32(msg.game_id);
payload.writeU32(msg.player_id);
payload.writeU16(msg.x);
payload.writeU16(msg.y);
MessageHeader header{static_cast<uint8_t>(BombMessage::TYPE),
static_cast<uint32_t>(payload.size())};
return serialize(header, payload);
}
Buffer serialize(const BombResponseMessage& msg) {
Buffer payload;
payload.writeBool(msg.hit);
payload.writeBool(msg.sunk);
payload.writeU8(msg.ship_length);
payload.writeBool(msg.game_over);
payload.writeU32(msg.winner_id);
MessageHeader header{static_cast<uint8_t>(BombResponseMessage::TYPE),
static_cast<uint32_t>(payload.size())};
return serialize(header, payload);
}
Buffer serialize(const GameStateUpdateMessage& msg) {
Buffer payload;
payload.writeU32(msg.whose_turn);
payload.writeU8(msg.state);
MessageHeader header{static_cast<uint8_t>(GameStateUpdateMessage::TYPE),
static_cast<uint32_t>(payload.size())};
return serialize(header, payload);
}
Buffer serialize(const OpponentBombResultMessage& msg) {
Buffer payload;
payload.writeU16(msg.x);
payload.writeU16(msg.y);
payload.writeBool(msg.hit);
payload.writeBool(msg.sunk);
payload.writeU8(msg.your_ship_sunk_length);
MessageHeader header{static_cast<uint8_t>(OpponentBombResultMessage::TYPE),
static_cast<uint32_t>(payload.size())};
return serialize(header, payload);
}
Buffer serialize(const ErrorMessage& msg) {
Buffer payload;
payload.writeU8(msg.error_code);
payload.writeString(msg.message);
MessageHeader header{static_cast<uint8_t>(ErrorMessage::TYPE),
static_cast<uint32_t>(payload.size())};
return serialize(header, payload);
}
// ---------------------------------------------------------------------------
// Deserialize functions
// ---------------------------------------------------------------------------
CreateGameMessage deserializeCreateGame(Buffer& buf) {
CreateGameMessage msg;
msg.width = buf.readU16();
msg.height = buf.readU16();
uint16_t count = buf.readU16();
msg.ship_configs.resize(count);
for (uint16_t i = 0; i < count; ++i) {
msg.ship_configs[i].length = buf.readU8();
}
return msg;
}
CreateGameResponseMessage deserializeCreateGameResponse(Buffer& buf) {
CreateGameResponseMessage msg;
msg.game_id = buf.readU32();
msg.success = buf.readBool();
return msg;
}
ListGamesResponseMessage deserializeListGamesResponse(Buffer& buf) {
ListGamesResponseMessage msg;
uint16_t count = buf.readU16();
msg.games.resize(count);
for (uint16_t i = 0; i < count; ++i) {
msg.games[i].game_id = buf.readU32();
msg.games[i].player_count = buf.readU8();
msg.games[i].state = buf.readU8();
}
return msg;
}
JoinGameMessage deserializeJoinGame(Buffer& buf) {
JoinGameMessage msg;
msg.game_id = buf.readU32();
msg.player_name = buf.readString();
return msg;
}
JoinGameResponseMessage deserializeJoinGameResponse(Buffer& buf) {
JoinGameResponseMessage msg;
msg.success = buf.readBool();
msg.player_id = buf.readU32();
msg.game_config.width = buf.readU16();
msg.game_config.height = buf.readU16();
uint16_t count = buf.readU16();
msg.game_config.ship_configs.resize(count);
for (uint16_t i = 0; i < count; ++i) {
msg.game_config.ship_configs[i].length = buf.readU8();
}
return msg;
}
PlaceShipsMessage deserializePlaceShips(Buffer& buf) {
PlaceShipsMessage msg;
msg.game_id = buf.readU32();
msg.player_id = buf.readU32();
uint8_t count = buf.readU8();
msg.placements.resize(count);
for (uint8_t i = 0; i < count; ++i) {
msg.placements[i].ship_type_index = buf.readU8();
msg.placements[i].x = buf.readU16();
msg.placements[i].y = buf.readU16();
msg.placements[i].orientation = buf.readU8();
}
return msg;
}
PlaceShipsResponseMessage deserializePlaceShipsResponse(Buffer& buf) {
PlaceShipsResponseMessage msg;
msg.success = buf.readBool();
msg.error_message = buf.readString();
return msg;
}
BombMessage deserializeBomb(Buffer& buf) {
BombMessage msg;
msg.game_id = buf.readU32();
msg.player_id = buf.readU32();
msg.x = buf.readU16();
msg.y = buf.readU16();
return msg;
}
BombResponseMessage deserializeBombResponse(Buffer& buf) {
BombResponseMessage msg;
msg.hit = buf.readBool();
msg.sunk = buf.readBool();
msg.ship_length = buf.readU8();
msg.game_over = buf.readBool();
msg.winner_id = buf.readU32();
return msg;
}
GameStateUpdateMessage deserializeGameStateUpdate(Buffer& buf) {
GameStateUpdateMessage msg;
msg.whose_turn = buf.readU32();
msg.state = buf.readU8();
return msg;
}
OpponentBombResultMessage deserializeOpponentBombResult(Buffer& buf) {
OpponentBombResultMessage msg;
msg.x = buf.readU16();
msg.y = buf.readU16();
msg.hit = buf.readBool();
msg.sunk = buf.readBool();
msg.your_ship_sunk_length = buf.readU8();
return msg;
}
ErrorMessage deserializeError(Buffer& buf) {
ErrorMessage msg;
msg.error_code = buf.readU8();
msg.message = buf.readString();
return msg;
}
// ---------------------------------------------------------------------------
// deserializeHeader
// ---------------------------------------------------------------------------
MessageHeader deserializeHeader(const uint8_t* data) {
MessageHeader header;
header.message_type = data[0];
header.payload_size = static_cast<uint32_t>(data[1])
| (static_cast<uint32_t>(data[2]) << 8)
| (static_cast<uint32_t>(data[3]) << 16)
| (static_cast<uint32_t>(data[4]) << 24);
return header;
}
// ---------------------------------------------------------------------------
// Socket I/O helpers
// ---------------------------------------------------------------------------
bool sendMessage(int fd, const Buffer& message) {
const auto& bytes = message.data();
std::size_t total = bytes.size();
std::size_t sent = 0;
while (sent < total) {
ssize_t n = ::write(fd, bytes.data() + sent, total - sent);
if (n <= 0) {
return false;
}
sent += static_cast<std::size_t>(n);
}
return true;
}
bool receiveMessage(int fd, MessageHeader& header, Buffer& payload) {
// Read the 5-byte header
uint8_t headerBuf[5];
std::size_t headerRead = 0;
while (headerRead < 5) {
ssize_t n = ::read(fd, headerBuf + headerRead, 5 - headerRead);
if (n <= 0) {
return false; // disconnect or error
}
headerRead += static_cast<std::size_t>(n);
}
header = deserializeHeader(headerBuf);
// Read the payload
if (header.payload_size == 0) {
payload = Buffer();
return true;
}
std::vector<uint8_t> payloadBuf(header.payload_size);
std::size_t payloadRead = 0;
while (payloadRead < header.payload_size) {
ssize_t n = ::read(fd, payloadBuf.data() + payloadRead,
header.payload_size - payloadRead);
if (n <= 0) {
return false; // disconnect or error
}
payloadRead += static_cast<std::size_t>(n);
}
payload = Buffer(std::move(payloadBuf));
return true;
}
} // namespace battleship

149
battleship/readme.md Normal file
View file

@ -0,0 +1,149 @@
# Battleship game
1. This is a game suite. It includes a server and a CLI client. Both are built in C++.
2. One server instance can serve multiple games at the same time.
3. The goal of the game is to destroy all of your opponent(s) ships, while still keeping at least one of your own ship alive
4. The start doesn't have to be balanced - ships might differ between players from the start.
5. At the start of each game, one must define the size of the map and the ships available to the players.
6. The game will be developed in phases.
7. One player sets the game up. Other players can join
8. Turn-based game
# Phase 1 - Proof of Concept (POC)
- server + client cli
- simplified version, with exactly 2 players, and static ships. All ships have a width of 1. 1 < length < 7
## Gameplay
- setting up the game: one player creates a new game. Sets up the size of the map, and which ships are available to each player
- once the map is set up, the first player can join the game.
- other players can also join.
- when joining for the first time, each player must set up their ships - place them on the map, and rotate them per wish
- ships always rotate on their middle point.
- Rotation: If their length is even, rotate on length/2. if it's odd, rotate on length/2+1.
- each turn, the player can only specify the coordinates. Like (x=12, y=64).
- the player will learn if they hit or miss.
- once the player has no more
## Server
- new game instances are created through the client CLI
- when game is created, return a game id for the first player, and the cli sends the join command
- when a new cli is started, for a new player to join, they get to choose between existing games, or create a new one
- spawn a thread for each game instance
- when a new client joins, they join that specific thread. A thread ID must be shared with all the customer CLIs.
- keep the map and ships in memory
- the main process handles all connections. Each message it receives contains the thread ID, so that it knows which game it is for
- receiving a bombing location, the server must communicate this with the correct thread. The thread returns hit or miss.
- the main process uses some form of efficient interprocess communication to communicate with the child thread
## The Game thread
- each game is handled by its own thread.
- each thread keeps the game's map in memory, each game owning a different map
- when the bombing command is received, the thread will mark the bombing, and will return whether it's a hit or a miss
- the main server must ensure sending this information back to the customer
## Serialization
- all commands must be binary serialized. This is a very efficient game, and communication with the server must be minimal
- sample bombing object: game id, player id, coordinates (x, y)
- sample return object: game id, player id, coordinates (x, y), hit/miss
## CLI
- the cli constantly shows the map of the game
- asks the user for coordinates
- if they get hit on the other player's turn, the CLI must show this visually
- initially the map only shows own units, and empty water all around
- Characters for drawing: use '~' for empty waters. '||','=', '^', '>', '<' and 'v' for ship drawing.
Example:
<====>
^
#
v
- use 'x' for hit regions. example <==x> for a hit on the second position in a 5-length ship
# Phase 2
## Movement
Each ship can move. Each ship has a speed. Smaller ships are faster. Larger ships are slower. Speed is measured in squares per turn. Highest speed is 1.
Movement happens at the end of the turn. On their turn, each player decides if they keep their ships' current movement trajectory, or change it. Example: if a ship is moving towards the west, and the user likes it that way, the user doesn't have to do anything. If they want to stop the ship, or change course, they need to mention it in the movement instructions. These are all passed to the server.
Example movement object
game id, player id, ship id, type: set course (w/e/n/s,stop)
return - confirmation
## Ammunition
Each ship has infinite ammo of one single type on deck.
each ammo type has various damage
when a ship sinks, all its ammo is no longer usable.
On a turn, each user can use only 1 ship to fire at the opponent.
# Phase 3
## New unit types
- air
- submarine
- radar
## Bases
- a ship must return to base for various needs like refueling, resupplying or ammo
- Bases also fix ships
- the fix of a ship is to 100% and just takes one turn
- the base must be visible on the map
## Multi-ammo ships
- each ship can carry various types of ammo. There are constraints, certain ammo can only be carried by certain ships
- ammo is limited
- once the ammo is out, the ship must return to its base to get more ammo.
## Resources
- special ships can be set up to mine for resources like oil
## Research
- research can be performed to invent new ammo types
- radars can also be researched
## Radars
- each time a new radar is used, it shows a new portion of the map
- the radar has a physical location, so must be placed on the map. It will always display the area around it, 5 squares in each direction
- once a radar is placed, it will keep showing that area until it is destroyed, or the game ends
- a radar can be destroyed if a ship hits it, just like a regular ship
- cannot see submarines
- can see air ships
## Submarines
- require air surveillance to be discovered
- special ammo can take them down - torpedoes
- can hit any ship or submarine
- can see any ship or submarine
- can't hit or see air ships
## New ammo
- anti-air - can take down any air ship (plane / helicopter) in one single hit
- torpedo - can take down any submarine in one single hit
# Phase 4
## Time to build other clients
- web
- local/native
## Carrier
- new type of unit
- can carry drones, planes, helicopters
- is large
- has own radar
## Air unit - reconnaissance plane
- used just for discovery
- can find ships or submarines
- move very fast
- can only see 4 squares around them
- once they pass, they can no longer see the ships in the places it left
- large range
## Air unit - reconnaissance helicopter
- similar to radar, but has smaller radius
- can see any ship or submarine
- does not make damage
- small range
## Air unit - attack plane
- very fast
- can attack any kind of ship - air, water, underwater
- short range

View file

@ -0,0 +1,16 @@
add_executable(battleship_server
src/main.cpp
src/server.cpp
src/game_thread.cpp
)
target_include_directories(battleship_server
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include
)
target_link_libraries(battleship_server
PRIVATE
common
pthread
)

View file

@ -0,0 +1,91 @@
#pragma once
#include <common/protocol.h>
#include <common/types.h>
#include <cstdint>
#include <mutex>
#include <string>
#include <utility>
#include <vector>
namespace battleship {
// ---------------------------------------------------------------------------
// GameThread — manages a single game instance (Phase 1: synchronous calls)
//
// In this POC phase the main server thread calls methods directly; mutex
// protection keeps everything thread-safe. A real event-loop will be added
// in a later phase.
// ---------------------------------------------------------------------------
class GameThread {
public:
GameThread(uint32_t game_id, const GameConfig& config);
~GameThread();
// -- Observers (lock internally) ----------------------------------------
uint32_t gameId() const;
GameState state() const;
uint8_t playerCount() const;
uint32_t currentTurn() const;
bool isGameOver() const;
const GameConfig& config() const;
/// Return the opponent's socket fd so the caller can push notifications.
/// Returns -1 if the player is not found or has no opponent yet.
int getOpponentFd(uint32_t player_id) const;
// -- Mutators (lock internally) -----------------------------------------
/// Add a player. Returns the assigned player_id, or 0 on failure
/// (game full or not in WAITING_FOR_PLAYERS state).
uint32_t addPlayer(const std::string& name, int client_fd);
/// Place ships for a player.
/// Returns {success, error_message}. On success error_message is empty.
std::pair<bool, std::string> placeShips(
uint32_t player_id,
const std::vector<ShipPlacement>& placements);
/// Process a bomb from `player_id` at (x, y).
/// Returns a fully-populated BombResponseMessage.
BombResponseMessage processBomb(uint32_t player_id, uint16_t x, uint16_t y);
private:
// -- Internal helpers (caller must hold mutex_) -------------------------
/// Find a player by id. Returns nullptr when not found.
struct PlayerData; // forward-declared, defined below
PlayerData* findPlayer(uint32_t player_id);
const PlayerData* findPlayer(uint32_t player_id) const;
/// Index of the opponent in players_ (assumes exactly 2 players).
std::size_t opponentIndex(std::size_t my_index) const;
// -- Data ---------------------------------------------------------------
uint32_t game_id_;
GameConfig config_;
GameState state_;
struct PlayerData {
PlayerInfo info;
int client_fd;
PlayerData(uint32_t id, std::string name,
uint16_t board_w, uint16_t board_h, int fd)
: info(id, std::move(name), board_w, board_h)
, client_fd(fd)
{}
};
std::vector<PlayerData> players_;
uint32_t current_turn_index_ = 0; // index into players_
uint32_t next_player_id_ = 1; // monotonically increasing per game
mutable std::mutex mutex_;
};
} // namespace battleship

View file

@ -0,0 +1,65 @@
#pragma once
#include <common/protocol.h>
#include <common/serialization.h>
#include <server/game_thread.h>
#include <atomic>
#include <cstdint>
#include <map>
#include <memory>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
namespace battleship {
class Server {
public:
explicit Server(uint16_t port = 8080);
~Server();
/// Start the server. Blocks in the accept loop until stop() is called.
void start();
/// Signal the server to stop accepting new connections and shut down.
void stop();
private:
void acceptLoop();
void handleClient(int client_fd);
// -----------------------------------------------------------------------
// Message handlers
// -----------------------------------------------------------------------
void handleCreateGame(int client_fd, Buffer& payload);
void handleListGames(int client_fd);
void handleJoinGame(int client_fd, Buffer& payload);
void handlePlaceShips(int client_fd, Buffer& payload);
void handleBomb(int client_fd, Buffer& payload);
// -----------------------------------------------------------------------
// Helpers
// -----------------------------------------------------------------------
GameThread* findGame(uint32_t game_id);
void sendError(int client_fd, uint8_t error_code, const std::string& message);
void sendGameStateUpdate(GameThread* game);
// -----------------------------------------------------------------------
// Data members
// -----------------------------------------------------------------------
uint16_t port_;
int server_fd_;
std::atomic<bool> running_;
std::mutex games_mutex_;
std::map<uint32_t, std::unique_ptr<GameThread>> games_;
uint32_t next_game_id_;
std::mutex threads_mutex_;
std::vector<std::thread> client_threads_;
};
} // namespace battleship

View file

@ -0,0 +1,264 @@
#include "server/game_thread.h"
#include <algorithm>
#include <sstream>
namespace battleship {
// ---------------------------------------------------------------------------
// Construction / destruction
// ---------------------------------------------------------------------------
GameThread::GameThread(uint32_t game_id, const GameConfig& config)
: game_id_(game_id)
, config_(config)
, state_(GameState::WAITING_FOR_PLAYERS)
{}
GameThread::~GameThread() = default;
// ---------------------------------------------------------------------------
// Observers
// ---------------------------------------------------------------------------
uint32_t GameThread::gameId() const {
std::lock_guard<std::mutex> lock(mutex_);
return game_id_;
}
GameState GameThread::state() const {
std::lock_guard<std::mutex> lock(mutex_);
return state_;
}
uint8_t GameThread::playerCount() const {
std::lock_guard<std::mutex> lock(mutex_);
return static_cast<uint8_t>(players_.size());
}
uint32_t GameThread::currentTurn() const {
std::lock_guard<std::mutex> lock(mutex_);
if (state_ != GameState::IN_PROGRESS || players_.empty()) {
return 0;
}
return players_[current_turn_index_].info.player_id;
}
bool GameThread::isGameOver() const {
std::lock_guard<std::mutex> lock(mutex_);
return state_ == GameState::FINISHED;
}
const GameConfig& GameThread::config() const {
std::lock_guard<std::mutex> lock(mutex_);
return config_;
}
int GameThread::getOpponentFd(uint32_t player_id) const {
std::lock_guard<std::mutex> lock(mutex_);
for (std::size_t i = 0; i < players_.size(); ++i) {
if (players_[i].info.player_id == player_id) {
if (players_.size() < 2) return -1;
return players_[opponentIndex(i)].client_fd;
}
}
return -1;
}
// ---------------------------------------------------------------------------
// addPlayer
// ---------------------------------------------------------------------------
uint32_t GameThread::addPlayer(const std::string& name, int client_fd) {
std::lock_guard<std::mutex> lock(mutex_);
if (state_ != GameState::WAITING_FOR_PLAYERS) return 0;
if (players_.size() >= 2) return 0;
uint32_t id = next_player_id_++;
players_.emplace_back(id, name, config_.width, config_.height, client_fd);
// When the second player joins, move to the SETUP (placing ships) phase.
if (players_.size() == 2) {
state_ = GameState::SETUP;
}
return id;
}
// ---------------------------------------------------------------------------
// placeShips
// ---------------------------------------------------------------------------
std::pair<bool, std::string> GameThread::placeShips(
uint32_t player_id,
const std::vector<ShipPlacement>& placements)
{
std::lock_guard<std::mutex> lock(mutex_);
// --- State / player validation -----------------------------------------
if (state_ != GameState::SETUP) {
return {false, "Game is not in the ship-placement phase"};
}
PlayerData* pd = findPlayer(player_id);
if (!pd) {
return {false, "Unknown player_id"};
}
if (pd->info.ready) {
return {false, "Ships already placed"};
}
// --- Validate placement count matches config ---------------------------
if (placements.size() != config_.ship_configs.size()) {
std::ostringstream oss;
oss << "Expected " << config_.ship_configs.size()
<< " ship placements, got " << placements.size();
return {false, oss.str()};
}
// --- Build Ship objects and place them ---------------------------------
// Work on a temporary board so a partial failure leaves the real board
// untouched.
Board temp_board(config_.width, config_.height);
for (std::size_t i = 0; i < placements.size(); ++i) {
const ShipPlacement& sp = placements[i];
// Validate ship_type_index.
if (sp.ship_type_index >= config_.ship_configs.size()) {
std::ostringstream oss;
oss << "Invalid ship_type_index " << static_cast<int>(sp.ship_type_index);
return {false, oss.str()};
}
uint8_t length = config_.ship_configs[sp.ship_type_index].length;
Orientation ori = (sp.orientation == static_cast<uint8_t>(Orientation::VERTICAL))
? Orientation::VERTICAL
: Orientation::HORIZONTAL;
Ship ship(sp.ship_type_index, length,
Coordinate{sp.x, sp.y}, ori);
if (!temp_board.placeShip(ship)) {
std::ostringstream oss;
oss << "Invalid placement for ship index " << i
<< " (type " << static_cast<int>(sp.ship_type_index)
<< ", length " << static_cast<int>(length) << ")";
return {false, oss.str()};
}
}
// All placements valid — commit to the real board.
pd->info.board = std::move(temp_board);
pd->info.ready = true;
// If both players are now ready, start the game.
bool all_ready = std::all_of(
players_.begin(), players_.end(),
[](const PlayerData& p) { return p.info.ready; });
if (all_ready) {
state_ = GameState::IN_PROGRESS;
current_turn_index_ = 0; // first player to join goes first
}
return {true, {}};
}
// ---------------------------------------------------------------------------
// processBomb
// ---------------------------------------------------------------------------
BombResponseMessage GameThread::processBomb(uint32_t player_id,
uint16_t x, uint16_t y)
{
std::lock_guard<std::mutex> lock(mutex_);
BombResponseMessage resp{};
resp.hit = false;
resp.sunk = false;
resp.ship_length = 0;
resp.game_over = false;
resp.winner_id = 0;
// --- Validate state ----------------------------------------------------
if (state_ != GameState::IN_PROGRESS) {
return resp; // silently ignore out-of-phase bombs
}
// --- Validate it is this player's turn ---------------------------------
if (players_[current_turn_index_].info.player_id != player_id) {
return resp; // not your turn
}
// --- Find opponent and bomb their board --------------------------------
std::size_t opp_idx = opponentIndex(current_turn_index_);
Board& opp_board = players_[opp_idx].info.board;
auto [hit, sunk] = opp_board.receiveBombing(Coordinate{x, y});
resp.hit = hit;
resp.sunk = sunk;
if (sunk) {
// Find the ship that was just sunk to report its length.
for (const auto& ship : opp_board.getShips()) {
for (uint8_t ci = 0; ci < ship.length(); ++ci) {
Coordinate cell = ship.cellAt(ci);
if (cell.x == x && cell.y == y && ship.isSunk()) {
resp.ship_length = ship.length();
break;
}
}
if (resp.ship_length != 0) break;
}
}
// --- Check for game over -----------------------------------------------
if (opp_board.allShipsSunk()) {
resp.game_over = true;
resp.winner_id = player_id;
state_ = GameState::FINISHED;
}
// --- Toggle turn -------------------------------------------------------
if (!resp.game_over) {
current_turn_index_ = static_cast<uint32_t>(opp_idx);
}
return resp;
}
// ---------------------------------------------------------------------------
// Private helpers
// ---------------------------------------------------------------------------
GameThread::PlayerData* GameThread::findPlayer(uint32_t player_id) {
for (auto& pd : players_) {
if (pd.info.player_id == player_id) return &pd;
}
return nullptr;
}
const GameThread::PlayerData* GameThread::findPlayer(uint32_t player_id) const {
for (const auto& pd : players_) {
if (pd.info.player_id == player_id) return &pd;
}
return nullptr;
}
std::size_t GameThread::opponentIndex(std::size_t my_index) const {
return (my_index == 0) ? 1 : 0;
}
} // namespace battleship

View file

@ -0,0 +1,45 @@
#include <server/server.h>
#include <csignal>
#include <cstdlib>
#include <iostream>
#include <string>
static battleship::Server* g_server = nullptr;
static void signalHandler(int /*signum*/) {
std::cerr << "\n[main] Caught SIGINT, shutting down...\n";
if (g_server) {
g_server->stop();
}
}
int main(int argc, char* argv[]) {
uint16_t port = 8080;
if (argc >= 2) {
int p = std::atoi(argv[1]);
if (p <= 0 || p > 65535) {
std::cerr << "Usage: " << argv[0] << " [port]\n";
std::cerr << " port must be in range 1-65535 (default: 8080)\n";
return 1;
}
port = static_cast<uint16_t>(p);
}
// Install SIGINT handler for graceful shutdown.
struct sigaction sa{};
sa.sa_handler = signalHandler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGINT, &sa, nullptr);
battleship::Server server(port);
g_server = &server;
std::cerr << "[main] Starting Battleship server on port " << port << "\n";
server.start();
std::cerr << "[main] Server stopped.\n";
return 0;
}

View file

@ -0,0 +1,397 @@
#include <server/server.h>
#include <common/protocol.h>
#include <common/serialization.h>
#include <server/game_thread.h>
#include <arpa/inet.h>
#include <cerrno>
#include <cstring>
#include <iostream>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
namespace battleship {
// ---------------------------------------------------------------------------
// Construction / destruction
// ---------------------------------------------------------------------------
Server::Server(uint16_t port)
: port_(port)
, server_fd_(-1)
, running_(false)
, next_game_id_(1)
{}
Server::~Server() {
stop();
}
// ---------------------------------------------------------------------------
// start / stop
// ---------------------------------------------------------------------------
void Server::start() {
server_fd_ = ::socket(AF_INET, SOCK_STREAM, 0);
if (server_fd_ < 0) {
std::cerr << "[server] socket() failed: " << std::strerror(errno) << "\n";
return;
}
int opt = 1;
if (::setsockopt(server_fd_, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) {
std::cerr << "[server] setsockopt(SO_REUSEADDR) failed: " << std::strerror(errno) << "\n";
::close(server_fd_);
server_fd_ = -1;
return;
}
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(port_);
if (::bind(server_fd_, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) < 0) {
std::cerr << "[server] bind() failed: " << std::strerror(errno) << "\n";
::close(server_fd_);
server_fd_ = -1;
return;
}
if (::listen(server_fd_, SOMAXCONN) < 0) {
std::cerr << "[server] listen() failed: " << std::strerror(errno) << "\n";
::close(server_fd_);
server_fd_ = -1;
return;
}
running_ = true;
std::cerr << "[server] Listening on port " << port_ << "\n";
acceptLoop();
}
void Server::stop() {
running_ = false;
if (server_fd_ >= 0) {
::close(server_fd_);
server_fd_ = -1;
}
// Join all client threads that are still joinable.
std::lock_guard<std::mutex> lock(threads_mutex_);
for (auto& t : client_threads_) {
if (t.joinable()) {
t.detach();
}
}
client_threads_.clear();
}
// ---------------------------------------------------------------------------
// Accept loop
// ---------------------------------------------------------------------------
void Server::acceptLoop() {
while (running_) {
sockaddr_in client_addr{};
socklen_t client_len = sizeof(client_addr);
int client_fd = ::accept(server_fd_,
reinterpret_cast<sockaddr*>(&client_addr),
&client_len);
if (client_fd < 0) {
if (running_) {
std::cerr << "[server] accept() failed: " << std::strerror(errno) << "\n";
}
continue;
}
char ip_str[INET_ADDRSTRLEN];
::inet_ntop(AF_INET, &client_addr.sin_addr, ip_str, sizeof(ip_str));
std::cerr << "[server] New connection from " << ip_str
<< ":" << ntohs(client_addr.sin_port)
<< " (fd=" << client_fd << ")\n";
std::lock_guard<std::mutex> lock(threads_mutex_);
client_threads_.emplace_back(&Server::handleClient, this, client_fd);
}
}
// ---------------------------------------------------------------------------
// Per-client handler (runs in its own thread)
// ---------------------------------------------------------------------------
void Server::handleClient(int client_fd) {
std::cerr << "[server] Client handler started (fd=" << client_fd << ")\n";
while (running_) {
MessageHeader header{};
Buffer payload;
if (!receiveMessage(client_fd, header, payload)) {
std::cerr << "[server] Client disconnected (fd=" << client_fd << ")\n";
break;
}
auto msg_type = static_cast<MessageType>(header.message_type);
try {
switch (msg_type) {
case MessageType::CREATE_GAME:
handleCreateGame(client_fd, payload);
break;
case MessageType::LIST_GAMES:
handleListGames(client_fd);
break;
case MessageType::JOIN_GAME:
handleJoinGame(client_fd, payload);
break;
case MessageType::PLACE_SHIPS:
handlePlaceShips(client_fd, payload);
break;
case MessageType::BOMB:
handleBomb(client_fd, payload);
break;
default:
std::cerr << "[server] Unknown message type 0x"
<< std::hex << static_cast<int>(header.message_type)
<< std::dec << " from fd=" << client_fd << "\n";
sendError(client_fd, 1, "Unknown message type");
break;
}
} catch (const std::exception& e) {
std::cerr << "[server] Exception handling message from fd=" << client_fd
<< ": " << e.what() << "\n";
sendError(client_fd, 2, std::string("Internal error: ") + e.what());
}
}
::close(client_fd);
std::cerr << "[server] Client handler exiting (fd=" << client_fd << ")\n";
}
// ---------------------------------------------------------------------------
// Message handlers
// ---------------------------------------------------------------------------
void Server::handleCreateGame(int client_fd, Buffer& payload) {
auto msg = deserializeCreateGame(payload);
GameConfig config;
config.width = msg.width;
config.height = msg.height;
config.ship_configs = msg.ship_configs;
uint32_t game_id;
{
std::lock_guard<std::mutex> lock(games_mutex_);
game_id = next_game_id_++;
games_[game_id] = std::make_unique<GameThread>(game_id, config);
}
std::cerr << "[server] Game " << game_id << " created (fd=" << client_fd << ")\n";
CreateGameResponseMessage resp;
resp.game_id = game_id;
resp.success = true;
Buffer out = serialize(resp);
sendMessage(client_fd, out);
}
void Server::handleListGames(int client_fd) {
ListGamesResponseMessage resp;
{
std::lock_guard<std::mutex> lock(games_mutex_);
for (auto& [id, game] : games_) {
GameListEntry entry;
entry.game_id = game->gameId();
entry.player_count = game->playerCount();
entry.state = static_cast<uint8_t>(game->state());
resp.games.push_back(entry);
}
}
std::cerr << "[server] Listing " << resp.games.size() << " games (fd=" << client_fd << ")\n";
Buffer out = serialize(resp);
sendMessage(client_fd, out);
}
void Server::handleJoinGame(int client_fd, Buffer& payload) {
auto msg = deserializeJoinGame(payload);
GameThread* game = findGame(msg.game_id);
if (!game) {
JoinGameResponseMessage resp;
resp.success = false;
resp.player_id = 0;
resp.game_config = {};
Buffer out = serialize(resp);
sendMessage(client_fd, out);
return;
}
uint32_t player_id = game->addPlayer(msg.player_name, client_fd);
if (player_id == 0) {
JoinGameResponseMessage resp;
resp.success = false;
resp.player_id = 0;
resp.game_config = {};
Buffer out = serialize(resp);
sendMessage(client_fd, out);
return;
}
std::cerr << "[server] Player '" << msg.player_name << "' (id=" << player_id
<< ") joined game " << msg.game_id << " (fd=" << client_fd << ")\n";
// Build the game config from the GameThread.
// We reconstruct it here — the game caches the config internally.
JoinGameResponseMessage resp;
resp.success = true;
resp.player_id = player_id;
// Retrieve config: width, height, ship_configs.
// GameThread exposes these through its config() accessor or we
// passed them in at construction. For simplicity, we re-derive from
// the CreateGameMessage config that was used to create the game.
// The GameThread stores it; we assume a config() getter.
// (The game_thread.h contract provides the needed accessors.)
resp.game_config.width = game->config().width;
resp.game_config.height = game->config().height;
resp.game_config.ship_configs = game->config().ship_configs;
Buffer out = serialize(resp);
sendMessage(client_fd, out);
// If both players have joined, transition to SETUP and notify both.
if (game->playerCount() == 2 && game->state() == GameState::SETUP) {
sendGameStateUpdate(game);
}
}
void Server::handlePlaceShips(int client_fd, Buffer& payload) {
auto msg = deserializePlaceShips(payload);
GameThread* game = findGame(msg.game_id);
if (!game) {
sendError(client_fd, 3, "Game not found");
return;
}
auto [success, error] = game->placeShips(msg.player_id, msg.placements);
PlaceShipsResponseMessage resp;
resp.success = success;
resp.error_message = error;
Buffer out = serialize(resp);
sendMessage(client_fd, out);
if (!success) {
return;
}
std::cerr << "[server] Player " << msg.player_id << " placed ships in game "
<< msg.game_id << " (fd=" << client_fd << ")\n";
// If both players are ready, transition to IN_PROGRESS and notify both.
if (game->state() == GameState::IN_PROGRESS) {
sendGameStateUpdate(game);
}
}
void Server::handleBomb(int client_fd, Buffer& payload) {
auto msg = deserializeBomb(payload);
GameThread* game = findGame(msg.game_id);
if (!game) {
sendError(client_fd, 3, "Game not found");
return;
}
// Check it's this player's turn.
if (game->currentTurn() != msg.player_id) {
sendError(client_fd, 4, "Not your turn");
return;
}
BombResponseMessage bomb_resp = game->processBomb(msg.player_id, msg.x, msg.y);
std::cerr << "[server] Player " << msg.player_id << " bombed ("
<< msg.x << "," << msg.y << ") in game " << msg.game_id
<< " — hit=" << bomb_resp.hit << " sunk=" << bomb_resp.sunk
<< " game_over=" << bomb_resp.game_over << "\n";
// Send BombResponse to the attacking player.
Buffer out = serialize(bomb_resp);
sendMessage(client_fd, out);
// Send OpponentBombResult to the opponent.
int opponent_fd = game->getOpponentFd(msg.player_id);
if (opponent_fd >= 0) {
OpponentBombResultMessage opp_msg;
opp_msg.x = msg.x;
opp_msg.y = msg.y;
opp_msg.hit = bomb_resp.hit;
opp_msg.sunk = bomb_resp.sunk;
opp_msg.your_ship_sunk_length = bomb_resp.ship_length;
Buffer opp_out = serialize(opp_msg);
sendMessage(opponent_fd, opp_out);
}
// Send state update to both players (turn change or game over).
sendGameStateUpdate(game);
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
GameThread* Server::findGame(uint32_t game_id) {
std::lock_guard<std::mutex> lock(games_mutex_);
auto it = games_.find(game_id);
if (it == games_.end()) {
return nullptr;
}
return it->second.get();
}
void Server::sendError(int client_fd, uint8_t error_code, const std::string& message) {
ErrorMessage err;
err.error_code = error_code;
err.message = message;
Buffer out = serialize(err);
sendMessage(client_fd, out);
}
void Server::sendGameStateUpdate(GameThread* game) {
GameStateUpdateMessage update;
update.whose_turn = game->currentTurn();
update.state = static_cast<uint8_t>(game->state());
Buffer out = serialize(update);
// Send to both players via their stored fds.
// GameThread provides player fds through getOpponentFd with each player id.
// We iterate known player ids (1-based: player 1 and player 2).
// A cleaner approach: GameThread exposes a method to get all fds.
// For now, we use the fact that player ids are sequential from 1.
for (uint32_t pid = 1; pid <= 2; ++pid) {
int fd = game->getOpponentFd(pid);
// getOpponentFd returns the *opponent's* fd, so we also need the
// player's own fd. We'll send to the opponent fd for pid=1 (which is
// player 2's fd) and for pid=2 (which is player 1's fd), covering both.
if (fd >= 0) {
sendMessage(fd, out);
}
}
}
} // namespace battleship