Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- /**
- * Battleship_v3.ino
- * Two-Arduino Battleship over AltSoftSerial (ATmega328 / Arduino Nano).
- *
- * Wiring (Nano <-> Nano):
- * - Cross TX/RX: D9 (TX) -> D8 (RX) on the other Nano
- * D8 (RX) -> D9 (TX) on the other Nano
- * - Common GND
- *
- * For testing with Python host via USB, set TEST_WITH_PYTHON to 1 below,
- * connect Python to Nano USB at 57600, no adapter needed, debug prints disabled, AI mode hardcoded.
- *
- * USB Serial (debug): 115200
- * AltSoftSerial link: 57600
- *
- * Human mode: manual ship placement (e.g., "A1 E") and moves ("B4", "A10"),
- * AI mode: automatic placement and hunt/target shooting.
- *
- * Author: Trent M. Wyatt ([email protected])
- * Date: September, 2025
- * License: MIT
- *
- */
- #define TEST_WITH_PYTHON 0 // Set to 1 for testing with Python via USB Serial as link
- #if TEST_WITH_PYTHON
- #define DEBUG_PRINT 0
- #else
- #define DEBUG_PRINT 1
- #endif
- #if !TEST_WITH_PYTHON
- #include <AltSoftSerial.h>
- #endif
- #include <Arduino.h>
- // ------------------------------ Constants ------------------------------------
- // Grid size and cell count.
- enum : uint8_t { GRID_W = 10, GRID_H = 10, CELLS = GRID_W * GRID_H };
- // Ship types and lengths.
- enum ship_type_t : uint8_t {
- CARRIER = 0,
- BATTLESHIP = 1,
- CRUISER = 2,
- SUBMARINE = 3,
- DESTROYER = 4,
- NUM_SHIP_TYPES = 5
- };
- static const uint8_t SHIP_LEN[NUM_SHIP_TYPES] = { 5, 4, 3, 3, 2 };
- static const char * const SHIP_NAME[NUM_SHIP_TYPES] = {
- "Carrier", "Battleship", "Cruiser", "Submarine", "Destroyer"
- };
- // Directions and shot results.
- enum dir_t : uint8_t { NORTH = 0, SOUTH = 1, WEST = 2, EAST = 3 };
- enum result_code_t : uint8_t {
- RES_MISS = 0,
- RES_HIT = 1,
- RES_SUNK = 2,
- RES_REPEAT = 3,
- RES_ERROR = 255
- };
- // Link protocol packet types.
- enum pkt_type_t : uint8_t {
- PKT_HELLO = 0x01, // payload: 4b token
- PKT_START = 0x02, // no payload (host->guest)
- PKT_MOVE = 0x10, // payload: 1b idx (0..99)
- PKT_RESULT = 0x11, // payload: 1b result_code
- PKT_GAMEOVER = 0x12, // payload: 1b winner_id
- PKT_PLACE_CHECK = 0x31, // (unused here; tolerated if received)
- PKT_PLACE_REPLY = 0x32, // (unused here; tolerated if received)
- PKT_PLACE_DONE = 0x33 // no payload (peer finished placement)
- };
- static const uint8_t PKT_SYNC = 0x42;
- // Serial speeds and debug options.
- #define DEBUG_SERIAL_BAUD 115200
- #define LINK_BAUD 57600
- #define PRINT_BOARDS_EVERY_TURN 1
- // Timeout for waiting in placement and handshake (ms)
- #define WAIT_TIMEOUT 30000
- // ------------------------------ Utilities ------------------------------------
- // Convert (x,y) to linear index.
- static inline uint8_t xy_to_idx(uint8_t x, uint8_t y) {
- return (uint8_t)(y * GRID_W + x);
- }
- // Extract x from linear index.
- static inline uint8_t idx_x(uint8_t idx) {
- return (uint8_t)(idx % GRID_W);
- }
- // Extract y from linear index.
- static inline uint8_t idx_y(uint8_t idx) {
- return (uint8_t)(idx / GRID_W);
- }
- // Coordinate pair.
- struct xy_t {
- uint8_t x;
- uint8_t y;
- // Return true if inside the board.
- inline bool in_bounds() const {
- return x < GRID_W && y < GRID_H;
- }
- };
- // Ship record.
- struct ship_t {
- ship_type_t type;
- uint8_t len;
- xy_t start;
- dir_t dir;
- uint8_t hits;
- // Initialize to a particular ship type.
- void reset(ship_type_t t) {
- type = t;
- len = SHIP_LEN[t];
- start = { 0, 0 };
- dir = SOUTH;
- hits = 0;
- }
- };
- #if !TEST_WITH_PYTHON
- // One AltSoftSerial link shared by the sketch.
- AltSoftSerial link;
- #else
- #define link Serial
- #endif
- // ------------------------------ Board ----------------------------------------
- // Per-player state and helpers.
- struct board_t {
- // 0xFF = water, else ship index [0..NUM_SHIP_TYPES-1].
- uint8_t my_grid[CELLS];
- // Incoming shot marks (0/1).
- uint8_t my_shots[CELLS];
- // Our view of opponent cells (0=unknown,1=miss,2=hit).
- uint8_t opp_view[CELLS];
- // Our tried moves (0/1).
- uint8_t my_moves[CELLS];
- // Our ships.
- ship_t my_ships[NUM_SHIP_TYPES];
- // Remaining unhit ship cells (17 at start).
- uint8_t ships_remaining_cells;
- // Reset board to empty with full fleet unhit.
- void clear() {
- for (uint16_t i = 0; i < CELLS; ++i) {
- my_grid[i] = 0xFF;
- }
- for (uint16_t i = 0; i < CELLS; ++i) {
- my_shots[i] = 0;
- }
- for (uint16_t i = 0; i < CELLS; ++i) {
- opp_view[i] = 0;
- }
- for (uint16_t i = 0; i < CELLS; ++i) {
- my_moves[i] = 0;
- }
- for (uint8_t i = 0; i < NUM_SHIP_TYPES; ++i) {
- my_ships[i].reset((ship_type_t)i);
- }
- ships_remaining_cells = 5 + 4 + 3 + 3 + 2;
- }
- // Return true if a ship of len fits at s in direction d, without
- // overlapping our own ships or leaving bounds.
- bool can_place(uint8_t len, xy_t s, dir_t d) const {
- if (!s.in_bounds()) {
- return false;
- }
- int dx = 0;
- int dy = 0;
- switch (d) {
- case NORTH: { dy = -1; break; }
- case SOUTH: { dy = 1; break; }
- case WEST: { dx = -1; break; }
- case EAST: { dx = 1; break; }
- }
- int x = s.x;
- int y = s.y;
- for (uint8_t i = 0; i < len; ++i) {
- if (x < 0 || y < 0 || x >= GRID_W || y >= GRID_H) {
- return false;
- }
- uint8_t idx = xy_to_idx((uint8_t)x, (uint8_t)y);
- if (my_grid[idx] != 0xFF) {
- return false;
- }
- x += dx;
- y += dy;
- }
- return true;
- }
- // Write ship cells into our grid.
- void place(ship_t &sh, xy_t s, dir_t d, uint8_t ship_index) {
- sh.start = s;
- sh.dir = d;
- int dx = 0;
- int dy = 0;
- switch (d) {
- case NORTH: { dy = -1; break; }
- case SOUTH: { dy = 1; break; }
- case WEST: { dx = -1; break; }
- case EAST: { dx = 1; break; }
- }
- int x = s.x;
- int y = s.y;
- for (uint8_t i = 0; i < sh.len; ++i) {
- uint8_t idx = xy_to_idx((uint8_t)x, (uint8_t)y);
- my_grid[idx] = ship_index;
- x += dx;
- y += dy;
- }
- }
- // Produce the linear cells covered by ship if placed at s,d.
- static void cells_for(const ship_t &sh, xy_t s, dir_t d,
- uint8_t *out_cells) {
- int dx = 0;
- int dy = 0;
- switch (d) {
- case NORTH: { dy = -1; break; }
- case SOUTH: { dy = 1; break; }
- case WEST: { dx = -1; break; }
- case EAST: { dx = 1; break; }
- }
- int x = s.x;
- int y = s.y;
- for (uint8_t i = 0; i < sh.len; ++i) {
- out_cells[i] = xy_to_idx((uint8_t)x, (uint8_t)y);
- x += dx;
- y += dy;
- }
- }
- // Apply an incoming shot to our board and return the result.
- result_code_t receive_shot(uint8_t idx, uint8_t &sunk_ship_type) {
- if (idx >= CELLS) {
- return RES_ERROR;
- }
- if (my_shots[idx]) {
- return RES_REPEAT;
- }
- my_shots[idx] = 1;
- uint8_t cell = my_grid[idx];
- if (cell == 0xFF) {
- return RES_MISS;
- }
- ship_t &sh = my_ships[cell];
- sh.hits++;
- ships_remaining_cells--;
- if (sh.hits >= sh.len) {
- sunk_ship_type = (uint8_t)sh.type;
- return RES_SUNK;
- }
- return RES_HIT;
- }
- // Mark our opponent view for a given result at idx.
- void mark_opp(uint8_t idx, result_code_t r) {
- if (idx >= CELLS) {
- return;
- }
- switch (r) {
- case RES_MISS: { opp_view[idx] = 1; break; }
- case RES_HIT: { opp_view[idx] = 2; break; }
- case RES_SUNK: { opp_view[idx] = 2; break; }
- default : { break; }
- }
- }
- // Return true if we already fired at idx.
- bool move_was_made(uint8_t idx) const {
- return idx < CELLS && my_moves[idx];
- }
- // Remember that we fired at idx.
- void remember_move(uint8_t idx) {
- if (idx < CELLS) {
- my_moves[idx] = 1;
- }
- }
- // Fast xorshift32 PRNG step.
- static uint32_t rand32(uint32_t &state) {
- uint32_t x = state;
- x ^= x << 13;
- x ^= x >> 17;
- x ^= x << 5;
- state = x;
- return x;
- }
- // Collect some ADC noise and micros to seed PRNG.
- static uint32_t seed_from_adc() {
- uint32_t s = micros();
- for (int i = 0; i < 8; ++i) {
- s ^= (uint32_t)analogRead(A0) << (i * 2);
- delay(1);
- }
- return s ^ micros();
- }
- // Pick a valid placement for a ship (AI).
- static bool ai_pick_ship_placement(const board_t &b, const ship_t &sh,
- uint32_t &rng_state, xy_t &s_out, dir_t &d_out) {
- for (uint16_t tries = 0; tries < 600; ++tries) {
- dir_t d = (dir_t)(rand32(rng_state) % 4);
- xy_t s;
- switch (d) {
- case EAST: {
- s = { (uint8_t)(rand32(rng_state) %
- (GRID_W - sh.len + 1)),
- (uint8_t)(rand32(rng_state) % GRID_H) };
- break;
- }
- case WEST: {
- s = { (uint8_t)((sh.len - 1) +
- (rand32(rng_state) %
- (GRID_W - sh.len + 1))),
- (uint8_t)(rand32(rng_state) % GRID_H) };
- break;
- }
- case SOUTH: {
- s = { (uint8_t)(rand32(rng_state) % GRID_W),
- (uint8_t)(rand32(rng_state) %
- (GRID_H - sh.len + 1)) };
- break;
- }
- case NORTH: {
- s = { (uint8_t)(rand32(rng_state) % GRID_W),
- (uint8_t)((sh.len - 1) +
- (rand32(rng_state) %
- (GRID_H - sh.len + 1))) };
- break;
- }
- }
- if (b.can_place(sh.len, s, d)) {
- s_out = s;
- d_out = d;
- return true;
- }
- }
- return false;
- }
- // Choose next shot with improved targeting.
- uint8_t choose_move(uint32_t &rng_state) {
- // Phase 1: extend any horizontal or vertical line of hits.
- for (uint8_t y = 0; y < GRID_H; ++y) {
- for (uint8_t x = 0; x < GRID_W; ++x) {
- uint8_t idx = xy_to_idx(x, y);
- if (opp_view[idx] != 2) {
- continue;
- }
- // Horizontal run detection and extension.
- bool left_hit = (x > 0) &&
- (opp_view[xy_to_idx(x - 1, y)] == 2);
- bool right_hit = (x + 1 < GRID_W) &&
- (opp_view[xy_to_idx(x + 1, y)] == 2);
- if (left_hit || right_hit) {
- int lx = x;
- int rx = x;
- while ((lx > 0) &&
- (opp_view[xy_to_idx(lx - 1, y)] == 2)) {
- --lx;
- }
- while ((rx + 1 < GRID_W) &&
- (opp_view[xy_to_idx(rx + 1, y)] == 2)) {
- ++rx;
- }
- uint8_t cand[2];
- uint8_t n_c = 0;
- if (lx > 0) {
- cand[n_c++] = xy_to_idx((uint8_t)(lx - 1), y);
- }
- if (rx + 1 < GRID_W) {
- cand[n_c++] = xy_to_idx((uint8_t)(rx + 1), y);
- }
- if (n_c == 2 && (rand32(rng_state) & 1)) {
- uint8_t t = cand[0];
- cand[0] = cand[1];
- cand[1] = t;
- }
- for (uint8_t i = 0; i < n_c; ++i) {
- uint8_t c = cand[i];
- if (!move_was_made(c) && opp_view[c] == 0) {
- return c;
- }
- }
- }
- // Vertical run detection and extension.
- bool up_hit = (y > 0) &&
- (opp_view[xy_to_idx(x, y - 1)] == 2);
- bool down_hit = (y + 1 < GRID_H) &&
- (opp_view[xy_to_idx(x, y + 1)] == 2);
- if (up_hit || down_hit) {
- int ty = y;
- int by = y;
- while ((ty > 0) &&
- (opp_view[xy_to_idx(x, ty - 1)] == 2)) {
- --ty;
- }
- while ((by + 1 < GRID_H) &&
- (opp_view[xy_to_idx(x, by + 1)] == 2)) {
- ++by;
- }
- uint8_t cand[2];
- uint8_t n_c = 0;
- if (ty > 0) {
- cand[n_c++] = xy_to_idx(x, (uint8_t)(ty - 1));
- }
- if (by + 1 < GRID_H) {
- cand[n_c++] = xy_to_idx(x, (uint8_t)(by + 1));
- }
- if (n_c == 2 && (rand32(rng_state) & 1)) {
- uint8_t t = cand[0];
- cand[0] = cand[1];
- cand[1] = t;
- }
- for (uint8_t i = 0; i < n_c; ++i) {
- uint8_t c = cand[i];
- if (!move_was_made(c) && opp_view[c] == 0) {
- return c;
- }
- }
- }
- }
- }
- // Phase 2: probe around any single hit in random neighbor order.
- for (uint8_t y = 0; y < GRID_H; ++y) {
- for (uint8_t x = 0; x < GRID_W; ++x) {
- uint8_t idx = xy_to_idx(x, y);
- if (opp_view[idx] != 2) {
- continue;
- }
- uint8_t nbr[4];
- uint8_t n_n = 0;
- if (x > 0) {
- nbr[n_n++] = xy_to_idx(x - 1, y);
- }
- if (x + 1 < GRID_W) {
- nbr[n_n++] = xy_to_idx(x + 1, y);
- }
- if (y > 0) {
- nbr[n_n++] = xy_to_idx(x, y - 1);
- }
- if (y + 1 < GRID_H) {
- nbr[n_n++] = xy_to_idx(x, y + 1);
- }
- for (uint8_t i = 0; i + 1 < n_n; ++i) {
- uint8_t j = (uint8_t)(i +
- (rand32(rng_state) % (uint8_t)(n_n - i)));
- uint8_t t = nbr[i];
- nbr[i] = nbr[j];
- nbr[j] = t;
- }
- for (uint8_t i = 0; i < n_n; ++i) {
- uint8_t c = nbr[i];
- if (!move_was_made(c) && opp_view[c] == 0) {
- return c;
- }
- }
- }
- }
- // Phase 3: parity hunt on unknown cells.
- for (uint16_t tries = 0; tries < 400; ++tries) {
- uint8_t x = rand32(rng_state) % GRID_W;
- uint8_t y = rand32(rng_state) % GRID_H;
- if (((x ^ y) & 1) != 0) {
- uint8_t idx = xy_to_idx(x, y);
- if (!move_was_made(idx) && opp_view[idx] == 0) {
- return idx;
- }
- }
- }
- // Phase 4: first unknown.
- for (uint8_t i = 0; i < CELLS; ++i) {
- if (!move_was_made(i) && opp_view[i] == 0) {
- return i;
- }
- }
- return 0;
- }
- // Print our own board to Stream.
- void print_my_board(Stream &s) const {
- #if DEBUG_PRINT
- s.println(F("My Board: ('#'=ship, 'x'=hit, 'o'=miss)"));
- for (uint8_t y = 0; y < GRID_H; ++y) {
- for (uint8_t x = 0; x < GRID_W; ++x) {
- uint8_t idx = xy_to_idx(x, y);
- char c = '.';
- if (my_shots[idx]) {
- c = (my_grid[idx] == 0xFF) ? 'o' : 'x';
- }
- else {
- if (my_grid[idx] != 0xFF) {
- c = '#';
- }
- }
- s.print(c);
- }
- s.println();
- }
- #else
- (void)s;
- #endif
- }
- // Print our view of opponent to Stream.
- void print_opp_view(Stream &s) const {
- #if DEBUG_PRINT
- s.println(F("Opponent View: ('?'=unknown, 'x'=hit, 'o'=miss)"));
- for (uint8_t y = 0; y < GRID_H; ++y) {
- for (uint8_t x = 0; x < GRID_W; ++x) {
- uint8_t idx = xy_to_idx(x, y);
- char c = '?';
- switch (opp_view[idx]) {
- case 1: { c = 'o'; break; }
- case 2: { c = 'x'; break; }
- default: { break; }
- }
- s.print(c);
- }
- s.println();
- }
- #else
- (void)s;
- #endif
- }
- };
- // ------------------------------ Globals --------------------------------------
- static board_t board;
- static bool am_host = false;
- static bool my_turn = false;
- static uint8_t my_id = 0;
- static uint8_t opp_id = 1;
- static bool game_over = false;
- static uint32_t rng_state = 0;
- static bool is_human = false;
- static bool manual_ship_placement = true;
- // ------------------------------ Link I/O -------------------------------------
- // Write a framed packet to AltSoftSerial.
- static void link_write_packet(uint8_t type, const uint8_t *payload,
- uint8_t len) {
- uint8_t crc = type ^ len;
- link.write(PKT_SYNC);
- link.write(type);
- link.write(len);
- for (uint8_t i = 0; i < len; ++i) {
- link.write(payload[i]);
- crc ^= payload[i];
- }
- link.write(crc);
- #if !TEST_WITH_PYTHON
- link.flushOutput();
- #else
- link.flush();
- #endif
- }
- // Read a framed packet with timeout. Returns payload length or -1.
- static int link_read_packet(uint8_t &type, uint8_t *buf, uint8_t maxlen,
- uint16_t timeout_ms) {
- const uint32_t deadline = millis() + timeout_ms;
- enum { ST_SYNC, ST_TYPE, ST_LEN, ST_PAYLOAD, ST_CRC } st = ST_SYNC;
- uint8_t len = 0;
- uint8_t crc = 0;
- uint8_t got = 0;
- while ((int32_t)(deadline - millis()) > 0) {
- if (!link.available()) {
- continue;
- }
- uint8_t b = (uint8_t)link.read();
- switch (st) {
- case ST_SYNC: {
- if (b == PKT_SYNC) {
- st = ST_TYPE;
- }
- break;
- }
- case ST_TYPE: {
- type = b;
- crc = b;
- st = ST_LEN;
- break;
- }
- case ST_LEN: {
- len = b;
- crc ^= b;
- if (len > maxlen) {
- for (uint8_t i = 0; i < len + 1; ++i) {
- while (!link.available()) {
- // spin
- }
- (void)link.read();
- }
- st = ST_SYNC;
- }
- else {
- got = 0;
- st = (len == 0) ? ST_CRC : ST_PAYLOAD;
- }
- break;
- }
- case ST_PAYLOAD: {
- buf[got++] = b;
- crc ^= b;
- if (got >= len) {
- st = ST_CRC;
- }
- break;
- }
- case ST_CRC: {
- if (crc == b) {
- return len;
- }
- st = ST_SYNC;
- break;
- }
- }
- }
- return -1;
- }
- // ------------------------------ Helpers --------------------------------------
- // Discard any bytes available on a stream.
- static void read_flush_input(Stream &s) {
- while (s.available()) {
- (void)s.read();
- }
- }
- // Read a line with timeout into buf, NUL-terminated.
- static bool read_line(Stream &s, char *buf, size_t maxlen,
- uint32_t timeout_ms) {
- size_t n = 0;
- const uint32_t deadline = millis() + timeout_ms;
- while ((int32_t)(deadline - millis()) > 0) {
- while (s.available()) {
- char c = (char)s.read();
- if (c == '\r') {
- continue;
- }
- if (c == '\n') {
- buf[n] = 0;
- return true;
- }
- if (n + 1 < maxlen) {
- buf[n++] = c;
- }
- }
- // Poll link for unexpected packets during wait
- if (link.available()) {
- uint8_t type;
- uint8_t pbuf[16];
- int pn = link_read_packet(type, pbuf, sizeof(pbuf), 5);
- if (pn >= 0) {
- if (type == PKT_HELLO && pn == 4) {
- #if DEBUG_PRINT
- Serial.println(F("Detected peer restart. Resetting game..."));
- #endif
- game_over = false;
- board.clear();
- do_handshake();
- placement_phase();
- }
- if (type == PKT_PLACE_CHECK && pn >= 1) {
- uint8_t rc = 0;
- link_write_packet(PKT_PLACE_REPLY, &rc, 1);
- }
- }
- }
- }
- buf[n] = 0;
- return n > 0;
- }
- // Uppercase ASCII letter if needed.
- static inline uint8_t to_upper(uint8_t c) {
- return (c >= 'a' && c <= 'z') ? (uint8_t)(c - 'a' + 'A') : c;
- }
- // Parse "A1".."J10" into linear index.
- static bool parse_cell_str(const char *p, uint8_t &idx_out) {
- while (*p == ' ' || *p == '\t') {
- ++p;
- }
- if (!*p) {
- return false;
- }
- char col = to_upper(*p++);
- if (col < 'A' || col > 'J') {
- return false;
- }
- while (*p == ' ' || *p == '\t') {
- ++p;
- }
- if (!*p) {
- return false;
- }
- int row = 0;
- if (*p < '0' || *p > '9') {
- return false;
- }
- while (*p >= '0' && *p <= '9') {
- row = row * 10 + (*p - '0');
- ++p;
- }
- while (*p == ' ' || *p == '\t') {
- ++p;
- }
- if (row < 1 || row > 10) {
- return false;
- }
- uint8_t x = (uint8_t)(col - 'A');
- uint8_t y = (uint8_t)(row - 1);
- if (x >= GRID_W || y >= GRID_H) {
- return false;
- }
- idx_out = xy_to_idx(x, y);
- return true;
- }
- // Parse direction token after cell: N/S/E/W (U/D/L/R also accepted).
- static bool parse_dir_str(const char *p, dir_t &d_out) {
- while (*p && *p != ' ' && *p != '\t') {
- ++p;
- }
- while (*p == ' ' || *p == '\t') {
- ++p;
- }
- if (!*p) {
- return false;
- }
- char c = to_upper(*p);
- if (c == 'N' || c == 'U') {
- d_out = NORTH;
- return true;
- }
- if (c == 'S' || c == 'D') {
- d_out = SOUTH;
- return true;
- }
- if (c == 'W' || c == 'L') {
- d_out = WEST;
- return true;
- }
- if (c == 'E' || c == 'R') {
- d_out = EAST;
- return true;
- }
- return false;
- }
- // Print cell in "A1" style.
- static void print_cell(uint8_t idx) {
- #if DEBUG_PRINT
- Serial.print((char)('A' + idx_x(idx)));
- Serial.print((int)(idx_y(idx) + 1));
- #endif
- }
- // Print a banner for current turn.
- static void print_turn_banner() {
- #if DEBUG_PRINT
- Serial.print(F("\n--- Turn: Player "));
- Serial.print((int)(my_turn ? my_id : opp_id));
- Serial.println(F(" ---"));
- #endif
- }
- // Handle peer restart by resetting game state and restarting handshake/placement.
- static void handle_peer_restart() {
- #if DEBUG_PRINT
- Serial.println(F("Detected peer restart. Resetting game..."));
- #endif
- game_over = false;
- board.clear();
- do_handshake();
- placement_phase();
- }
- // ---------------------------------- Placement -------------------------------
- // Service any unexpected PKT_PLACE_CHECK by replying OK.
- static void service_place_check_once_nonblocking() {
- if (!link.available()) {
- return;
- }
- uint8_t type;
- uint8_t buf[16];
- int n = link_read_packet(type, buf, sizeof(buf), 5);
- if (n >= 0) {
- if (type == PKT_HELLO && n == 4) {
- handle_peer_restart();
- }
- if (type == PKT_PLACE_CHECK && n >= 1) {
- uint8_t rc = 0;
- link_write_packet(PKT_PLACE_REPLY, &rc, 1);
- }
- }
- }
- // Wait for the peer to signal placement completion.
- static void wait_for_peer_placement_done() {
- #if DEBUG_PRINT
- Serial.println(F("[Placement] Waiting for peer to finish..."));
- #endif
- uint32_t start = millis();
- uint32_t last_log = start;
- while (true) {
- uint8_t type;
- uint8_t buf[4];
- int n = link_read_packet(type, buf, sizeof(buf), 200);
- if (n >= 0) {
- if (type == PKT_PLACE_DONE) {
- #if DEBUG_PRINT
- Serial.println(F("[Placement] Peer finished."));
- #endif
- break;
- }
- if (type == PKT_HELLO && n == 4) {
- handle_peer_restart();
- return; // Exit after restart to avoid stuck loop
- }
- if (type == PKT_PLACE_CHECK && n >= 1) {
- uint8_t rc = 0;
- link_write_packet(PKT_PLACE_REPLY, &rc, 1);
- }
- }
- uint32_t current = millis();
- if (current - start > WAIT_TIMEOUT) {
- #if DEBUG_PRINT
- Serial.println(F("[Placement] Timeout waiting for peer. Forcing restart..."));
- #endif
- uint8_t dummy_token[4] = {0,0,0,0};
- link_write_packet(PKT_HELLO, dummy_token, 4);
- handle_peer_restart();
- return;
- }
- if (current - last_log > 10000) {
- #if DEBUG_PRINT
- Serial.println(F("[Placement] Still waiting for peer..."));
- #endif
- last_log = current;
- }
- }
- }
- // Prompt a human to place all ships (no cross-player validation).
- static void collect_human_fleet_placements(bool /*unused*/) {
- #if DEBUG_PRINT
- Serial.println(F("\n=== Human Fleet Placement ==="));
- Serial.println(F("Format: A1 E or b4 south"));
- #endif
- for (uint8_t i = 0; i < NUM_SHIP_TYPES; ++i) {
- ship_t &sh = board.my_ships[i];
- while (true) {
- #if DEBUG_PRINT
- Serial.print(F("\nPlace "));
- Serial.print(SHIP_NAME[sh.type]);
- Serial.print(F(" (length "));
- Serial.print(sh.len);
- Serial.println(F(")"));
- Serial.print(F("> "));
- #endif
- char line[32];
- read_flush_input(Serial);
- if (!read_line(Serial, line, sizeof(line), 600000)) {
- service_place_check_once_nonblocking();
- continue;
- }
- uint8_t idx0;
- if (!parse_cell_str(line, idx0)) {
- #if DEBUG_PRINT
- Serial.println(F("Bad cell. Use A1..J10."));
- #endif
- continue;
- }
- dir_t d;
- if (!parse_dir_str(line, d)) {
- #if DEBUG_PRINT
- Serial.println(F("Bad direction. Use N/S/E/W."));
- #endif
- continue;
- }
- xy_t s = { idx_x(idx0), idx_y(idx0) };
- if (!board.can_place(sh.len, s, d)) {
- #if DEBUG_PRINT
- Serial.println(F("Does not fit / overlaps your ships."));
- #endif
- continue;
- }
- uint8_t cells[5];
- board_t::cells_for(sh, s, d, cells);
- board.place(sh, s, d, i);
- #if DEBUG_PRINT
- Serial.print(F("Placed "));
- Serial.print(SHIP_NAME[sh.type]);
- Serial.print(F(" at "));
- print_cell(cells[0]);
- Serial.println();
- board.print_my_board(Serial);
- #endif
- break;
- }
- }
- link_write_packet(PKT_PLACE_DONE, nullptr, 0);
- #if DEBUG_PRINT
- Serial.println(F("[Placement] All ships placed."));
- #endif
- }
- // AI placement with no cross-player validation.
- static void ai_fleet_placement_no_crosscheck() {
- #if DEBUG_PRINT
- Serial.println(F("\n=== Computer Fleet Placement ==="));
- #endif
- for (uint8_t i = 0; i < NUM_SHIP_TYPES; ++i) {
- ship_t &sh = board.my_ships[i];
- while (true) {
- xy_t s;
- dir_t d;
- if (!board_t::ai_pick_ship_placement(board, sh, rng_state,
- s, d)) {
- continue;
- }
- uint8_t cells[5];
- board_t::cells_for(sh, s, d, cells);
- board.place(sh, s, d, i);
- #if DEBUG_PRINT
- Serial.print(F("Placed "));
- Serial.print(SHIP_NAME[sh.type]);
- Serial.print(F(" at "));
- print_cell(cells[0]);
- Serial.println();
- #endif
- break;
- }
- }
- link_write_packet(PKT_PLACE_DONE, nullptr, 0);
- #if DEBUG_PRINT
- Serial.println(F("[Placement] Computer placed all ships."));
- #endif
- }
- // ------------------------------ Gameplay -------------------------------------
- // If a GAMEOVER arrives, consume and mark state.
- static void maybe_receive_gameover_nonblocking() {
- if (!link.available()) {
- return;
- }
- uint8_t type;
- uint8_t buf[4];
- int n = link_read_packet(type, buf, sizeof(buf), 5);
- if (n >= 0) {
- if (type == PKT_HELLO && n == 4) {
- handle_peer_restart();
- }
- if (type == PKT_GAMEOVER && n >= 1) {
- uint8_t winner = buf[0];
- if (winner == my_id) {
- #if DEBUG_PRINT
- Serial.println(F("\n*** GAME OVER: We win. ***"));
- #endif
- }
- else {
- #if DEBUG_PRINT
- Serial.println(F("\n*** GAME OVER: Opponent wins. ***"));
- #endif
- }
- game_over = true;
- }
- if (type == PKT_PLACE_CHECK && n >= 1) {
- uint8_t rc = 0;
- link_write_packet(PKT_PLACE_REPLY, &rc, 1);
- }
- }
- }
- // Send MOVE, await RESULT (with retry). Returns true on success.
- static bool send_move_and_get_result(uint8_t idx, result_code_t &res_out) {
- uint8_t c = idx;
- link_write_packet(PKT_MOVE, &c, 1);
- uint32_t last_log = millis();
- for (uint8_t tries = 0; tries < 10; ++tries) {
- uint8_t type;
- uint8_t buf[4];
- int n = link_read_packet(type, buf, sizeof(buf), 1500);
- if (n >= 0) {
- if (type == PKT_RESULT && n >= 1) {
- res_out = (result_code_t)buf[0];
- return true;
- }
- if (type == PKT_HELLO && n == 4) {
- handle_peer_restart();
- }
- if (type == PKT_PLACE_CHECK && n >= 1) {
- uint8_t rc = 0;
- link_write_packet(PKT_PLACE_REPLY, &rc, 1);
- }
- }
- link_write_packet(PKT_MOVE, &c, 1);
- uint32_t current = millis();
- if (current - last_log > 10000) {
- #if DEBUG_PRINT
- Serial.println(F("Still waiting for RESULT..."));
- #endif
- last_log = current;
- }
- }
- return false;
- }
- // Handle an incoming MOVE and send RESULT.
- static void respond_to_incoming_move(uint8_t idx) {
- uint8_t sunk_type = 0xFF;
- result_code_t r = board.receive_shot(idx, sunk_type);
- uint8_t rc = (uint8_t)r;
- link_write_packet(PKT_RESULT, &rc, 1);
- #if DEBUG_PRINT
- Serial.print(F("Incoming shot at "));
- print_cell(idx);
- Serial.print(F(" -> "));
- switch (r) {
- case RES_MISS: {
- Serial.println(F("MISS"));
- break;
- }
- case RES_HIT: {
- Serial.println(F("HIT"));
- break;
- }
- case RES_SUNK: {
- Serial.print(F("SUNK "));
- Serial.println((int)sunk_type);
- break;
- }
- case RES_REPEAT: {
- Serial.println(F("REPEAT"));
- break;
- }
- default: {
- Serial.println(F("ERROR"));
- break;
- }
- }
- #endif
- if (board.ships_remaining_cells == 0) {
- uint8_t winner = opp_id;
- link_write_packet(PKT_GAMEOVER, &winner, 1);
- #if DEBUG_PRINT
- Serial.println(F("\n*** GAME OVER: Opponent wins. ***"));
- #endif
- game_over = true;
- }
- }
- // Prompt human for a legal shot coordinate.
- static uint8_t prompt_human_move_idx() {
- char line[16];
- while (true) {
- #if DEBUG_PRINT
- Serial.print(F("Enter move (e.g., A1): "));
- #endif
- read_flush_input(Serial);
- if (!read_line(Serial, line, sizeof(line), 600000)) {
- continue;
- }
- uint8_t idx;
- if (!parse_cell_str(line, idx)) {
- #if DEBUG_PRINT
- Serial.println(F("Bad coordinate. Try again."));
- #endif
- continue;
- }
- if (board.move_was_made(idx)) {
- #if DEBUG_PRINT
- Serial.println(F("Already shot there. Try again."));
- #endif
- continue;
- }
- return idx;
- }
- }
- // Show our boards if enabled.
- static void show_boards_if_configured() {
- #if PRINT_BOARDS_EVERY_TURN
- board.print_my_board(Serial);
- board.print_opp_view(Serial);
- #endif
- }
- // Service link for unexpected packets non-blocking (HELLO, PLACE_CHECK, GAMEOVER).
- static void service_link_nonblocking() {
- if (!link.available()) {
- return;
- }
- uint8_t type;
- uint8_t buf[4];
- int n = link_read_packet(type, buf, sizeof(buf), 5);
- if (n >= 0) {
- if (type == PKT_HELLO && n == 4) {
- handle_peer_restart();
- }
- if (type == PKT_PLACE_CHECK && n >= 1) {
- uint8_t rc = 0;
- link_write_packet(PKT_PLACE_REPLY, &rc, 1);
- }
- if (type == PKT_GAMEOVER && n >= 1) {
- uint8_t winner = buf[0];
- if (winner == my_id) {
- #if DEBUG_PRINT
- Serial.println(F("\n*** GAME OVER: We win. ***"));
- #endif
- }
- else {
- #if DEBUG_PRINT
- Serial.println(F("\n*** GAME OVER: Opponent wins. ***"));
- #endif
- }
- game_over = true;
- }
- }
- }
- // --------------------------- Handshake / Setup -------------------------------
- // Exchange HELLO, decide host, then START if host.
- static void do_handshake() {
- uint32_t my_token = (board_t::seed_from_adc() ^ 0xA5A5A5A5UL);
- rng_state = my_token ^ 0x55AA3311UL;
- uint8_t tx[4] = {
- (uint8_t)my_token,
- (uint8_t)(my_token >> 8),
- (uint8_t)(my_token >> 16),
- (uint8_t)(my_token >> 24)
- };
- uint32_t start = millis();
- uint32_t last_log = start;
- uint32_t peer_token = 0;
- while (true) {
- link_write_packet(PKT_HELLO, tx, 4);
- delay(50); // Small delay to allow peer to receive
- uint8_t type;
- uint8_t buf[4];
- int n = link_read_packet(type, buf, sizeof(buf), 500);
- if (n == 4 && type == PKT_HELLO) {
- peer_token = (uint32_t)buf[0] |
- ((uint32_t)buf[1] << 8) |
- ((uint32_t)buf[2] << 16) |
- ((uint32_t)buf[3] << 24);
- break;
- }
- uint32_t current = millis();
- if (current - start > WAIT_TIMEOUT) {
- #if DEBUG_PRINT
- Serial.println(F("[Handshake] Timeout waiting for peer HELLO. Forcing restart..."));
- #endif
- handle_peer_restart();
- return;
- }
- if (current - last_log > 10000) {
- #if DEBUG_PRINT
- Serial.println(F("[Handshake] Still waiting for peer HELLO..."));
- #endif
- last_log = current;
- }
- }
- if (my_token == peer_token) {
- delay(10);
- do_handshake();
- return;
- }
- am_host = (my_token > peer_token);
- my_id = am_host ? 0 : 1;
- opp_id = am_host ? 1 : 0;
- if (am_host) {
- link_write_packet(PKT_START, nullptr, 0);
- my_turn = true;
- #if DEBUG_PRINT
- Serial.println(F("[Handshake] HOST. I start."));
- #endif
- }
- else {
- uint8_t type;
- uint8_t buf[1];
- uint32_t start = millis();
- uint32_t last_log = start;
- while (true) {
- int n = link_read_packet(type, buf, sizeof(buf), 3000);
- if (n == 0 && type == PKT_START) {
- break;
- }
- uint32_t current = millis();
- if (current - start > WAIT_TIMEOUT) {
- #if DEBUG_PRINT
- Serial.println(F("[Handshake] Timeout waiting for host START. Forcing restart..."));
- #endif
- handle_peer_restart();
- return;
- }
- if (current - last_log > 10000) {
- #if DEBUG_PRINT
- Serial.println(F("[Handshake] Still waiting for host START..."));
- #endif
- last_log = current;
- }
- }
- my_turn = false;
- #if DEBUG_PRINT
- Serial.println(F("[Handshake] GUEST. Opponent starts."));
- #endif
- }
- }
- #if !TEST_WITH_PYTHON
- // Ask user for player mode; default is Computer.
- static void prompt_player_mode() {
- Serial.println(F("\n=== Player Setup ==="));
- Serial.println(F("Type 'H' for Human, 'C' for Computer."));
- Serial.println(F("Default: C after 5s."));
- Serial.print(F("> "));
- read_flush_input(Serial);
- char line[8];
- if (read_line(Serial, line, sizeof(line), 5000)) {
- is_human = (to_upper(line[0]) == 'H');
- }
- else {
- is_human = false;
- }
- if (is_human) {
- Serial.println(F("Manual ship placement? (Y/n)"));
- Serial.println(F("Default: Y after 5s."));
- Serial.print(F("> "));
- if (read_line(Serial, line, sizeof(line), 5000)) {
- manual_ship_placement = (to_upper(line[0]) != 'N');
- }
- else {
- manual_ship_placement = true;
- }
- }
- else {
- manual_ship_placement = false;
- Serial.println(F("Computer player selected."));
- }
- }
- #endif
- // Perform placement phase.
- static void placement_phase() {
- if (am_host) {
- #if DEBUG_PRINT
- Serial.println(F("[Placement] HOST placing fleet..."));
- #endif
- if (is_human && manual_ship_placement) {
- collect_human_fleet_placements(false);
- }
- else {
- ai_fleet_placement_no_crosscheck();
- }
- #if DEBUG_PRINT
- Serial.println(F("[Placement] Waiting for peer..."));
- #endif
- wait_for_peer_placement_done();
- }
- else {
- #if DEBUG_PRINT
- Serial.println(F("[Placement] Waiting for host..."));
- #endif
- wait_for_peer_placement_done();
- #if DEBUG_PRINT
- Serial.println(F("[Placement] GUEST placing fleet..."));
- #endif
- if (is_human && manual_ship_placement) {
- collect_human_fleet_placements(false);
- }
- else {
- ai_fleet_placement_no_crosscheck();
- }
- }
- }
- // ------------------------------ Arduino API ----------------------------------
- // Arduino setup entrypoint.
- void setup() {
- #if !TEST_WITH_PYTHON
- Serial.begin(DEBUG_SERIAL_BAUD);
- while (!Serial) {
- // Nano returns immediately
- }
- delay(100);
- analogReference(DEFAULT);
- pinMode(A0, INPUT);
- #if DEBUG_PRINT
- Serial.println(F("\nBattleship starting..."));
- Serial.println(F("AltSoftSerial on D9 (TX), D8 (RX)."));
- #endif
- prompt_player_mode();
- link.begin(LINK_BAUD);
- #else
- is_human = false;
- manual_ship_placement = false;
- Serial.begin(LINK_BAUD);
- analogReference(DEFAULT);
- pinMode(A0, INPUT);
- #endif
- delay(50);
- board.clear();
- do_handshake();
- placement_phase();
- #if DEBUG_PRINT
- Serial.println(F("Ships placed."));
- board.print_my_board(Serial);
- #endif
- }
- // Arduino loop entrypoint.
- void loop() {
- if (game_over) {
- service_link_nonblocking();
- delay(100);
- return;
- }
- print_turn_banner();
- if (my_turn) {
- uint8_t idx = 0;
- if (is_human) {
- idx = prompt_human_move_idx();
- }
- else {
- idx = board.choose_move(rng_state);
- }
- if (board.move_was_made(idx)) {
- for (uint8_t i = 0; i < CELLS; ++i) {
- if (!board.move_was_made(i) && board.opp_view[i] == 0) {
- idx = i;
- break;
- }
- }
- }
- board.remember_move(idx);
- #if DEBUG_PRINT
- Serial.print(F("My shot at "));
- print_cell(idx);
- Serial.println();
- #endif
- result_code_t r;
- if (!send_move_and_get_result(idx, r)) {
- #if DEBUG_PRINT
- Serial.println(F("Link error waiting for RESULT."));
- #endif
- maybe_receive_gameover_nonblocking();
- delay(50);
- return;
- }
- board.mark_opp(idx, r);
- #if DEBUG_PRINT
- Serial.print(F("Result: "));
- switch (r) {
- case RES_MISS: {
- Serial.println(F("MISS"));
- break;
- }
- case RES_HIT: {
- Serial.println(F("HIT"));
- break;
- }
- case RES_SUNK: {
- Serial.println(F("SUNK!"));
- break;
- }
- case RES_REPEAT: {
- Serial.println(F("REPEAT? (duplicate)"));
- break;
- }
- default: {
- Serial.println(F("ERROR"));
- break;
- }
- }
- #endif
- maybe_receive_gameover_nonblocking();
- my_turn = false;
- }
- else {
- uint8_t type;
- uint8_t buf[2];
- int n = link_read_packet(type, buf, sizeof(buf), 5000);
- if (n >= 0) {
- if (type == PKT_MOVE && n >= 1) {
- uint8_t idx = buf[0];
- respond_to_incoming_move(idx);
- maybe_receive_gameover_nonblocking();
- my_turn = !game_over;
- }
- else {
- if (type == PKT_HELLO && n == 4) {
- handle_peer_restart();
- }
- if (type == PKT_PLACE_CHECK && n >= 1) {
- uint8_t rc = 0;
- link_write_packet(PKT_PLACE_REPLY, &rc, 1);
- }
- if (type == PKT_GAMEOVER && n >= 1) {
- uint8_t winner = buf[0];
- if (winner == my_id) {
- #if DEBUG_PRINT
- Serial.println(F("\n*** GAME OVER: We win. ***"));
- #endif
- }
- else {
- #if DEBUG_PRINT
- Serial.println(F("\n*** GAME OVER: Opponent wins. ***"));
- #endif
- }
- game_over = true;
- }
- delay(10);
- return;
- }
- }
- else {
- service_place_check_once_nonblocking();
- maybe_receive_gameover_nonblocking();
- delay(10);
- return;
- }
- }
- show_boards_if_configured();
- delay(30);
- }
Advertisement
Add Comment
Please, Sign In to add comment