Battleship (#2720)

* Made the Battleship 2P 2PP game - FSK is wip
* Using POCSAG
This commit is contained in:
RocketGod
2025-06-28 11:02:29 -07:00
committed by GitHub
parent 0eb03373b1
commit 4e276cdc71
5 changed files with 1153 additions and 1 deletions

View File

@@ -0,0 +1,71 @@
/*
* ------------------------------------------------------------
* | Made by RocketGod |
* | Find me at https://betaskynet.com |
* | Argh matey! |
* ------------------------------------------------------------
*/
#include "ui.hpp"
#include "ui_battleship.hpp"
#include "ui_navigation.hpp"
#include "external_app.hpp"
namespace ui::external_app::battleship {
void initialize_app(ui::NavigationView& nav) {
nav.push<BattleshipView>();
}
} // namespace ui::external_app::battleship
extern "C" {
__attribute__((section(".external_app.app_battleship.application_information"), used)) application_information_t _application_information_battleship = {
/*.memory_location = */ (uint8_t*)0x00000000,
/*.externalAppEntry = */ ui::external_app::battleship::initialize_app,
/*.header_version = */ CURRENT_HEADER_VERSION,
/*.app_version = */ VERSION_MD5,
/*.app_name = */ "Battleship",
/*.bitmap_data = */ {
// Ship icon
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x1C,
0x38,
0x3E,
0x7C,
0x7F,
0xFE,
0xFF,
0xFF,
0xFF,
0xFF,
0x7F,
0xFE,
0x3F,
0xFC,
0x1F,
0xF8,
0x0F,
0xF0,
0x07,
0xE0,
0x03,
0xC0,
0x01,
0x80,
},
/*.icon_color = */ ui::Color::blue().v,
/*.menu_location = */ app_location_t::GAMES,
/*.desired_menu_position = */ -1,
/*.m4_app_tag = */ {'P', 'P', 'O', '2'}, // Use POCSAG2 baseband (larger than FSKTX)
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
};
}

View File

@@ -0,0 +1,822 @@
/*
* ------------------------------------------------------------
* | Made by RocketGod |
* | Find me at https://betaskynet.com |
* | Argh matey! |
* ------------------------------------------------------------
*/
#include "ui_battleship.hpp"
#include "portapack_shared_memory.hpp"
#include "utility.hpp"
#include "modems.hpp"
#include "bch_code.hpp"
using namespace portapack;
using namespace modems;
namespace ui::external_app::battleship {
// POCSAG address for battleship game messages
constexpr uint32_t BATTLESHIP_BASE_ADDRESS = 1000000;
constexpr uint32_t RED_TEAM_ADDRESS = BATTLESHIP_BASE_ADDRESS + 1;
constexpr uint32_t BLUE_TEAM_ADDRESS = BATTLESHIP_BASE_ADDRESS + 2;
BattleshipView::BattleshipView(NavigationView& nav)
: nav_{nav} {
baseband::run_image(portapack::spi_flash::image_tag_pocsag2);
add_children({&rssi,
&field_frequency,
&text_status,
&text_score,
&button_red_team,
&button_blue_team,
&button_rotate,
&button_place,
&button_fire,
&button_menu});
text_score.hidden(true);
button_rotate.hidden(true);
button_place.hidden(true);
button_fire.hidden(true);
button_menu.hidden(true);
field_frequency.set_value(DEFAULT_FREQUENCY);
field_frequency.on_change = [this](rf::Frequency freq) {
tx_frequency = freq;
rx_frequency = freq;
if (!is_transmitting) {
receiver_model.set_target_frequency(rx_frequency);
}
};
button_red_team.on_select = [this](Button&) {
start_team(true);
};
button_blue_team.on_select = [this](Button&) {
start_team(false);
};
button_rotate.on_select = [this](Button&) {
placing_horizontal = !placing_horizontal;
set_dirty();
};
button_place.on_select = [this](Button&) {
place_ship();
};
button_fire.on_select = [this](Button&) {
fire_at_position();
};
button_menu.on_select = [this](Button&) {
reset_game();
};
set_focusable(true);
init_game();
}
BattleshipView::~BattleshipView() {
transmitter_model.disable();
receiver_model.disable();
audio::output::stop();
baseband::shutdown();
}
void BattleshipView::focus() {
if (game_state == GameState::MENU) {
button_red_team.focus();
} else {
View::focus();
}
}
void BattleshipView::init_game() {
for (uint8_t y = 0; y < GRID_SIZE; y++) {
for (uint8_t x = 0; x < GRID_SIZE; x++) {
my_grid[y][x] = CellState::EMPTY;
enemy_grid[y][x] = CellState::EMPTY;
}
}
setup_ships();
update_score();
}
void BattleshipView::reset_game() {
transmitter_model.disable();
receiver_model.disable();
audio::output::stop();
game_state = GameState::MENU;
is_red_team = false;
opponent_ready = false;
current_ship_index = 0;
placing_horizontal = true;
ships_remaining = 5;
enemy_ships_remaining = 5;
cursor_x = 0;
cursor_y = 0;
target_x = 0;
target_y = 0;
is_transmitting = false;
last_address = 0;
init_game();
current_status = "Choose your team!";
update_score();
text_status.hidden(false);
text_score.hidden(true);
field_frequency.hidden(false);
button_red_team.hidden(false);
button_blue_team.hidden(false);
button_rotate.hidden(true);
button_place.hidden(true);
button_fire.hidden(true);
button_menu.hidden(true);
button_red_team.set_focusable(true);
button_blue_team.set_focusable(true);
button_red_team.focus();
set_dirty();
}
void BattleshipView::setup_ships() {
static const ShipType types[] = {ShipType::CARRIER, ShipType::BATTLESHIP,
ShipType::CRUISER, ShipType::SUBMARINE, ShipType::DESTROYER};
for (uint8_t i = 0; i < 5; i++) {
my_ships[i] = {types[i], 0, 0, true, 0, false};
}
}
void BattleshipView::start_team(bool red) {
is_red_team = red;
game_state = GameState::PLACING_SHIPS;
field_frequency.hidden(true);
button_red_team.hidden(true);
button_blue_team.hidden(true);
button_rotate.hidden(false);
button_place.hidden(false);
button_menu.hidden(false);
text_status.hidden(true);
text_score.hidden(true);
current_status = "Place carrier (5)";
button_rotate.set_focusable(false);
button_place.set_focusable(false);
button_menu.set_focusable(false);
focus();
is_transmitting = true;
configure_radio_rx();
set_dirty();
}
void BattleshipView::configure_radio_tx() {
if (is_transmitting) return;
audio::output::stop();
receiver_model.disable();
baseband::shutdown();
chThdSleepMilliseconds(100);
baseband::run_image(portapack::spi_flash::image_tag_fsktx);
chThdSleepMilliseconds(100);
transmitter_model.set_target_frequency(tx_frequency);
transmitter_model.set_sampling_rate(2280000);
transmitter_model.set_baseband_bandwidth(1750000);
transmitter_model.set_rf_amp(false);
transmitter_model.set_tx_gain(35);
is_transmitting = true;
}
void BattleshipView::configure_radio_rx() {
if (is_transmitting) {
transmitter_model.disable();
baseband::shutdown();
chThdSleepMilliseconds(100);
}
baseband::run_image(portapack::spi_flash::image_tag_pocsag2);
chThdSleepMilliseconds(100);
receiver_model.set_target_frequency(rx_frequency);
receiver_model.set_sampling_rate(3072000);
receiver_model.set_baseband_bandwidth(1750000);
receiver_model.set_rf_amp(false);
receiver_model.set_lna(24);
receiver_model.set_vga(24);
baseband::set_pocsag();
receiver_model.enable();
audio::set_rate(audio::Rate::Hz_24000);
audio::output::start();
is_transmitting = false;
current_status = "RX Ready";
set_dirty();
}
void BattleshipView::paint(Painter& painter) {
painter.fill_rectangle({0, 0, 240, 320}, Color::black());
if (game_state == GameState::MENU) {
auto style_title = *ui::Theme::getInstance()->fg_light;
painter.draw_string({60, 20}, style_title, "BATTLESHIP");
painter.draw_string({40, 80}, style_title, "Choose your team:");
painter.draw_string({10, 180}, *ui::Theme::getInstance()->fg_medium, "Set same freq on both!");
return;
}
Color team_color = is_red_team ? Color::red() : Color::blue();
painter.fill_rectangle({0, 5, 240, 16}, team_color);
auto style_white = Style{
.font = ui::font::fixed_8x16,
.background = team_color,
.foreground = Color::white()};
painter.draw_string({85, 5}, style_white, is_red_team ? "RED TEAM" : "BLUE TEAM");
auto style_status = *ui::Theme::getInstance()->fg_light;
painter.fill_rectangle({0, 21, 240, 16}, Color::black());
painter.draw_string({10, 21}, style_status, current_status);
if (game_state != GameState::MENU) {
painter.draw_string({170, 21}, style_status, current_score);
}
if (game_state == GameState::PLACING_SHIPS) {
draw_grid(painter, GRID_OFFSET_X, GRID_OFFSET_Y + 5, my_grid, true);
if (current_ship_index < 5) {
draw_ship_preview(painter);
}
} else if (game_state == GameState::MY_TURN) {
draw_grid(painter, GRID_OFFSET_X, GRID_OFFSET_Y + 5, enemy_grid, false, true);
painter.draw_string({10, GRID_OFFSET_Y + GRID_SIZE * CELL_SIZE + 10}, style_status,
"Enemy ships: " + to_string_dec_uint(enemy_ships_remaining));
} else if (game_state == GameState::OPPONENT_TURN || game_state == GameState::WAITING_FOR_OPPONENT) {
draw_grid(painter, GRID_OFFSET_X, GRID_OFFSET_Y + 5, my_grid, true);
painter.draw_string({10, GRID_OFFSET_Y + GRID_SIZE * CELL_SIZE + 10}, style_status,
"Your ships: " + to_string_dec_uint(ships_remaining));
} else if (game_state == GameState::GAME_OVER) {
painter.draw_string({50, 150}, style_status, "Game Over!");
painter.draw_string({30, 170}, style_status, current_status);
}
}
void BattleshipView::draw_grid(Painter& painter, uint8_t grid_x, uint8_t grid_y, const std::array<std::array<CellState, GRID_SIZE>, GRID_SIZE>& grid, bool show_ships, bool is_offense_grid) {
painter.fill_rectangle({grid_x, grid_y, GRID_SIZE * CELL_SIZE, GRID_SIZE * CELL_SIZE},
Color::dark_blue());
for (uint8_t i = 0; i <= GRID_SIZE; i++) {
painter.draw_vline({grid_x + i * CELL_SIZE, grid_y},
GRID_SIZE * CELL_SIZE, Color::grey());
painter.draw_hline({grid_x, grid_y + i * CELL_SIZE},
GRID_SIZE * CELL_SIZE, Color::grey());
}
for (uint8_t y = 0; y < GRID_SIZE; y++) {
for (uint8_t x = 0; x < GRID_SIZE; x++) {
draw_cell(painter, grid_x + x * CELL_SIZE + 1, grid_y + y * CELL_SIZE + 1,
grid[y][x], show_ships, is_offense_grid,
is_cursor_at(x, y, is_offense_grid));
}
}
}
void BattleshipView::draw_cell(Painter& painter, uint8_t cell_x, uint8_t cell_y, CellState state, bool show_ships, bool is_offense_grid, bool is_cursor) {
Color cell_color = Color::dark_blue();
bool should_fill = false;
if (game_state == GameState::PLACING_SHIPS && !is_offense_grid && current_ship_index < 5) {
uint8_t ship_size = my_ships[current_ship_index].size();
for (uint8_t i = 0; i < ship_size; i++) {
uint8_t preview_x = placing_horizontal ? cursor_x + i : cursor_x;
uint8_t preview_y = placing_horizontal ? cursor_y : cursor_y + i;
uint8_t grid_x = (cell_x - 1) / CELL_SIZE;
uint8_t grid_y = (cell_y - GRID_OFFSET_Y - 6) / CELL_SIZE;
if (grid_x == preview_x && grid_y == preview_y && preview_x < GRID_SIZE && preview_y < GRID_SIZE) {
return;
}
}
}
switch (state) {
case CellState::SHIP:
if (show_ships) {
cell_color = Color::grey();
should_fill = true;
}
break;
case CellState::HIT:
cell_color = Color::red();
should_fill = true;
break;
case CellState::MISS:
cell_color = Color::light_grey();
should_fill = true;
break;
case CellState::SUNK:
cell_color = Color::dark_red();
should_fill = true;
break;
default:
if (is_offense_grid && state == CellState::EMPTY) {
cell_color = Color::dark_grey();
should_fill = true;
}
break;
}
if (should_fill) {
painter.fill_rectangle({cell_x, cell_y, CELL_SIZE - 2, CELL_SIZE - 2}, cell_color);
}
if (state == CellState::HIT || state == CellState::SUNK) {
painter.draw_hline({cell_x + 4, cell_y + 4}, CELL_SIZE - 10, Color::white());
painter.draw_hline({cell_x + 4, cell_y + CELL_SIZE - 6}, CELL_SIZE - 10, Color::white());
painter.draw_vline({cell_x + 4, cell_y + 4}, CELL_SIZE - 10, Color::white());
painter.draw_vline({cell_x + CELL_SIZE - 6, cell_y + 4}, CELL_SIZE - 10, Color::white());
} else if (state == CellState::MISS) {
painter.draw_hline({cell_x + 8, cell_y + 4}, 8, Color::white());
painter.draw_hline({cell_x + 8, cell_y + CELL_SIZE - 6}, 8, Color::white());
painter.draw_vline({cell_x + 4, cell_y + 8}, 8, Color::white());
painter.draw_vline({cell_x + CELL_SIZE - 6, cell_y + 8}, 8, Color::white());
}
if (is_cursor) {
painter.draw_rectangle({cell_x - 1, cell_y - 1, CELL_SIZE, CELL_SIZE},
is_offense_grid && game_state == GameState::MY_TURN ? Color::yellow() : Color::cyan());
}
}
bool BattleshipView::is_cursor_at(uint8_t x, uint8_t y, bool is_offense_grid) {
if (game_state == GameState::PLACING_SHIPS && !is_offense_grid) {
return x == cursor_x && y == cursor_y;
} else if (is_offense_grid && game_state == GameState::MY_TURN) {
return x == target_x && y == target_y;
}
return false;
}
void BattleshipView::draw_ship_preview(Painter& painter) {
if (current_ship_index >= 5) return;
const Ship& ship = my_ships[current_ship_index];
uint8_t size = ship.size();
bool can_place = can_place_ship(cursor_x, cursor_y, size, placing_horizontal);
for (uint8_t i = 0; i < size; i++) {
uint8_t x = placing_horizontal ? cursor_x + i : cursor_x;
uint8_t y = placing_horizontal ? cursor_y : cursor_y + i;
if (x < GRID_SIZE && y < GRID_SIZE) {
uint8_t cell_x = GRID_OFFSET_X + x * CELL_SIZE + 1;
uint8_t cell_y = GRID_OFFSET_Y + 5 + y * CELL_SIZE + 1;
Color preview_color = can_place ? Color::green() : Color::red();
painter.fill_rectangle({cell_x, cell_y, CELL_SIZE - 2, CELL_SIZE - 2}, preview_color);
painter.draw_rectangle({cell_x, cell_y, CELL_SIZE - 2, CELL_SIZE - 2}, Color::white());
}
}
}
bool BattleshipView::can_place_ship(uint8_t x, uint8_t y, uint8_t size, bool horizontal) {
if ((horizontal && x + size > GRID_SIZE) || (!horizontal && y + size > GRID_SIZE)) {
return false;
}
for (uint8_t i = 0; i < size; i++) {
uint8_t check_x = horizontal ? x + i : x;
uint8_t check_y = horizontal ? y : y + i;
if (my_grid[check_y][check_x] != CellState::EMPTY) {
return false;
}
for (int dy = -1; dy <= 1; dy++) {
for (int dx = -1; dx <= 1; dx++) {
int adj_x = check_x + dx;
int adj_y = check_y + dy;
if (adj_x >= 0 && adj_x < GRID_SIZE &&
adj_y >= 0 && adj_y < GRID_SIZE) {
if (my_grid[adj_y][adj_x] == CellState::SHIP) {
return false;
}
}
}
}
}
return true;
}
void BattleshipView::place_ship() {
if (current_ship_index >= 5) return;
Ship& ship = my_ships[current_ship_index];
uint8_t size = ship.size();
if (!can_place_ship(cursor_x, cursor_y, size, placing_horizontal)) {
current_status = "Invalid placement!";
set_dirty();
return;
}
ship.x = cursor_x;
ship.y = cursor_y;
ship.horizontal = placing_horizontal;
ship.placed = true;
for (uint8_t i = 0; i < size; i++) {
uint8_t x = placing_horizontal ? cursor_x + i : cursor_x;
uint8_t y = placing_horizontal ? cursor_y : cursor_y + i;
my_grid[y][x] = CellState::SHIP;
}
current_ship_index++;
if (current_ship_index >= 5) {
button_rotate.hidden(true);
button_place.hidden(true);
send_message({MessageType::READY, 0, 0});
if (is_red_team) {
game_state = GameState::MY_TURN;
current_status = "Your turn! Fire!";
button_fire.hidden(false);
button_fire.set_focusable(false);
touch_enabled = true;
} else {
game_state = GameState::WAITING_FOR_OPPONENT;
current_status = "Waiting for Red...";
touch_enabled = false;
}
focus();
} else {
static const char* ship_names[] = {"carrier (5)", "battleship (4)", "cruiser (3)",
"submarine (3)", "destroyer (2)"};
current_status = "Place ";
current_status += ship_names[current_ship_index];
}
set_dirty();
}
void BattleshipView::send_message(const GameMessage& msg) {
static const char* msg_strings[] = {"READY", "SHOT:", "HIT:", "MISS:", "SUNK:", "WIN"};
std::string message = msg_strings[static_cast<int>(msg.type)];
if (msg.type != MessageType::READY && msg.type != MessageType::WIN) {
message += to_string_dec_uint(msg.x) + "," + to_string_dec_uint(msg.y);
}
configure_radio_tx();
// Use POCSAG encoding
uint32_t target_address = is_red_team ? BLUE_TEAM_ADDRESS : RED_TEAM_ADDRESS;
std::vector<uint32_t> codewords;
BCHCode BCH_code{{1, 0, 1, 0, 0, 1}, 5, 31, 21, 2};
// Use the pocsag namespace to access ALPHANUMERIC
pocsag::pocsag_encode(pocsag::MessageType::ALPHANUMERIC, BCH_code, 0, message, target_address, codewords);
// Copy codewords to shared memory
uint8_t* data_ptr = shared_memory.bb_data.data;
size_t bi = 0;
for (size_t i = 0; i < codewords.size(); i++) {
uint32_t codeword = codewords[i];
data_ptr[bi++] = (codeword >> 24) & 0xFF;
data_ptr[bi++] = (codeword >> 16) & 0xFF;
data_ptr[bi++] = (codeword >> 8) & 0xFF;
data_ptr[bi++] = codeword & 0xFF;
}
// Set baseband FSK data
baseband::set_fsk_data(
codewords.size() * 32, // Total bits
2280000 / 1200, // Bit duration (1200 baud)
4500, // Deviation
64); // Packet repeat
transmitter_model.set_baseband_bandwidth(1750000);
transmitter_model.enable();
current_status = "TX: " + message;
set_dirty();
}
void BattleshipView::on_pocsag_packet(const POCSAGPacketMessage* message) {
if (message->packet.flag() != pocsag::NORMAL) {
return;
}
// Decode POCSAG message
pocsag_state.codeword_index = 0;
pocsag_state.errors = 0;
while (pocsag::pocsag_decode_batch(message->packet, pocsag_state)) {
if (pocsag_state.out_type == pocsag::MESSAGE) {
// Check if message is for our team
uint32_t expected_address = is_red_team ? RED_TEAM_ADDRESS : BLUE_TEAM_ADDRESS;
if (pocsag_state.address == expected_address) {
process_message(pocsag_state.output);
}
}
}
}
void BattleshipView::on_tx_progress(const uint32_t progress, const bool done) {
(void)progress;
if (done) {
transmitter_model.disable();
chThdSleepMilliseconds(200);
configure_radio_rx();
if (game_state == GameState::MY_TURN) {
current_status = "Waiting for response";
set_dirty();
}
}
}
bool BattleshipView::parse_coords(const std::string& coords, uint8_t& x, uint8_t& y) {
size_t comma_pos = coords.find(',');
if (comma_pos == std::string::npos) return false;
x = 0;
y = 0;
for (size_t i = 0; i < comma_pos; i++) {
char c = coords[i];
if (c >= '0' && c <= '9') {
x = x * 10 + (c - '0');
}
}
for (size_t i = comma_pos + 1; i < coords.length(); i++) {
char c = coords[i];
if (c >= '0' && c <= '9') {
y = y * 10 + (c - '0');
}
}
return x < GRID_SIZE && y < GRID_SIZE;
}
void BattleshipView::mark_ship_sunk(uint8_t x, uint8_t y, std::array<std::array<CellState, GRID_SIZE>, GRID_SIZE>& grid) {
for (int dy = -1; dy <= 1; dy++) {
for (int dx = -1; dx <= 1; dx++) {
int check_x = x + dx;
int check_y = y + dy;
if (check_x >= 0 && check_x < GRID_SIZE &&
check_y >= 0 && check_y < GRID_SIZE) {
if (grid[check_y][check_x] == CellState::HIT) {
grid[check_y][check_x] = CellState::SUNK;
}
}
}
}
}
void BattleshipView::process_message(const std::string& message) {
if (message.empty()) return;
size_t colon_pos = message.find(':');
std::string msg_type = (colon_pos != std::string::npos)
? message.substr(0, colon_pos)
: message;
if (msg_type == "READY") {
opponent_ready = true;
if (!is_red_team && game_state == GameState::WAITING_FOR_OPPONENT) {
current_status = "Red ready! Waiting...";
set_dirty();
}
} else if (msg_type == "SHOT" && colon_pos != std::string::npos) {
if (game_state == GameState::OPPONENT_TURN ||
(game_state == GameState::WAITING_FOR_OPPONENT && !is_red_team)) {
uint8_t x, y;
if (parse_coords(message.substr(colon_pos + 1), x, y)) {
process_shot(x, y);
}
}
} else if ((msg_type == "HIT" || msg_type == "MISS" || msg_type == "SUNK") && colon_pos != std::string::npos) {
uint8_t x, y;
if (parse_coords(message.substr(colon_pos + 1), x, y)) {
if (msg_type == "HIT") {
enemy_grid[y][x] = CellState::HIT;
current_status = "Hit! Fire again!";
} else if (msg_type == "MISS") {
enemy_grid[y][x] = CellState::MISS;
current_status = "Miss! Enemy turn";
game_state = GameState::OPPONENT_TURN;
button_fire.hidden(true);
touch_enabled = false;
} else if (msg_type == "SUNK") {
enemy_grid[y][x] = CellState::SUNK;
enemy_ships_remaining--;
current_status = "Ship sunk! Fire!";
mark_ship_sunk(x, y, enemy_grid);
}
if (game_state == GameState::MY_TURN) {
touch_enabled = true;
}
}
} else if (msg_type == "WIN") {
game_state = GameState::GAME_OVER;
current_status = (is_red_team ? "BLUE" : "RED") + std::string(" TEAM WINS!");
button_fire.hidden(true);
touch_enabled = false;
losses++;
update_score();
}
set_dirty();
}
void BattleshipView::fire_at_position() {
if (game_state != GameState::MY_TURN) return;
if (enemy_grid[target_y][target_x] != CellState::EMPTY) {
current_status = "Already fired!";
set_dirty();
return;
}
send_message({MessageType::SHOT, target_x, target_y});
current_status = "Firing...";
touch_enabled = false;
set_dirty();
}
void BattleshipView::process_shot(uint8_t x, uint8_t y) {
if (my_grid[y][x] == CellState::SHIP) {
my_grid[y][x] = CellState::HIT;
bool sunk = false;
for (auto& ship : my_ships) {
if (!ship.placed) continue;
bool hit_this_ship = false;
for (uint8_t i = 0; i < ship.size(); i++) {
uint8_t check_x = ship.horizontal ? ship.x + i : ship.x;
uint8_t check_y = ship.horizontal ? ship.y : ship.y + i;
if (check_x == x && check_y == y) {
hit_this_ship = true;
ship.hits++;
break;
}
}
if (hit_this_ship && ship.is_sunk()) {
sunk = true;
ships_remaining--;
for (uint8_t i = 0; i < ship.size(); i++) {
uint8_t sunk_x = ship.horizontal ? ship.x + i : ship.x;
uint8_t sunk_y = ship.horizontal ? ship.y : ship.y + i;
my_grid[sunk_y][sunk_x] = CellState::SUNK;
}
break;
}
}
if (sunk) {
send_message({MessageType::SUNK, x, y});
if (ships_remaining == 0) {
send_message({MessageType::WIN, 0, 0});
game_state = GameState::GAME_OVER;
current_status = (is_red_team ? "RED" : "BLUE") + std::string(" TEAM WINS!");
wins++;
update_score();
}
} else {
send_message({MessageType::HIT, x, y});
}
} else {
my_grid[y][x] = CellState::MISS;
send_message({MessageType::MISS, x, y});
game_state = GameState::MY_TURN;
button_fire.hidden(false);
touch_enabled = true;
current_status = "Your turn! Fire!";
}
set_dirty();
}
void BattleshipView::update_score() {
current_score = "W:" + to_string_dec_uint(wins) + " L:" + to_string_dec_uint(losses);
}
bool BattleshipView::on_touch(const TouchEvent event) {
if (event.type != TouchEvent::Type::Start || !touch_enabled) {
return false;
}
uint16_t x = event.point.x();
uint16_t y = event.point.y();
if (x >= GRID_OFFSET_X && x < GRID_OFFSET_X + GRID_SIZE * CELL_SIZE &&
y >= GRID_OFFSET_Y + 5 && y < GRID_OFFSET_Y + 5 + GRID_SIZE * CELL_SIZE) {
uint8_t grid_x = (x - GRID_OFFSET_X) / CELL_SIZE;
uint8_t grid_y = (y - GRID_OFFSET_Y - 5) / CELL_SIZE;
if (game_state == GameState::PLACING_SHIPS) {
cursor_x = grid_x;
cursor_y = grid_y;
} else if (game_state == GameState::MY_TURN) {
target_x = grid_x;
target_y = grid_y;
}
set_dirty();
return true;
}
return false;
}
bool BattleshipView::on_encoder(const EncoderEvent delta) {
if (delta == 0) return false;
if (game_state == GameState::PLACING_SHIPS) {
placing_horizontal = !placing_horizontal;
} else if (game_state == GameState::MY_TURN) {
target_x = (delta > 0) ? (target_x + 1) % GRID_SIZE : (target_x == 0) ? GRID_SIZE - 1
: target_x - 1;
}
set_dirty();
return true;
}
bool BattleshipView::on_key(const KeyEvent key) {
if (key == KeyEvent::Select) {
if (game_state == GameState::PLACING_SHIPS) {
place_ship();
return true;
} else if (game_state == GameState::MY_TURN) {
fire_at_position();
return true;
}
} else if (key == KeyEvent::Back) {
if (game_state != GameState::MENU) {
reset_game();
return true;
}
} else if (key == KeyEvent::Up || key == KeyEvent::Down) {
uint8_t* coord_y = (game_state == GameState::PLACING_SHIPS) ? &cursor_y : &target_y;
if (key == KeyEvent::Up) {
*coord_y = (*coord_y == 0) ? GRID_SIZE - 1 : *coord_y - 1;
} else {
*coord_y = (*coord_y + 1) % GRID_SIZE;
}
set_dirty();
return true;
} else if (key == KeyEvent::Left || key == KeyEvent::Right) {
uint8_t* coord_x = (game_state == GameState::PLACING_SHIPS) ? &cursor_x : &target_x;
if (key == KeyEvent::Left) {
*coord_x = (*coord_x == 0) ? GRID_SIZE - 1 : *coord_x - 1;
} else {
*coord_x = (*coord_x + 1) % GRID_SIZE;
}
set_dirty();
return true;
}
return false;
}
} // namespace ui::external_app::battleship

View File

@@ -0,0 +1,247 @@
/*
* ------------------------------------------------------------
* | Made by RocketGod |
* | Find me at https://betaskynet.com |
* | Argh matey! |
* ------------------------------------------------------------
*/
#ifndef __UI_BATTLESHIP_H__
#define __UI_BATTLESHIP_H__
#include "ui.hpp"
#include "ui_navigation.hpp"
#include "ui_receiver.hpp"
#include "ui_transmitter.hpp"
#include "app_settings.hpp"
#include "radio_state.hpp"
#include "baseband_api.hpp"
#include "string_format.hpp"
#include "audio.hpp"
#include "portapack.hpp"
#include "message.hpp"
#include "pocsag.hpp"
#include "portapack_shared_memory.hpp"
#include <string>
#include <array>
#include <cstdint>
namespace ui::external_app::battleship {
using namespace portapack;
constexpr uint8_t GRID_SIZE = 10;
constexpr uint8_t CELL_SIZE = 24;
constexpr uint8_t GRID_OFFSET_X = 0;
constexpr uint8_t GRID_OFFSET_Y = 32;
constexpr uint16_t BUTTON_Y = 280;
constexpr uint32_t DEFAULT_FREQUENCY = 433920000;
enum class ShipType : uint8_t {
CARRIER = 5,
BATTLESHIP = 4,
CRUISER = 3,
SUBMARINE = 3,
DESTROYER = 2
};
enum class GameState : uint8_t {
MENU,
PLACING_SHIPS,
WAITING_FOR_OPPONENT,
MY_TURN,
OPPONENT_TURN,
GAME_OVER
};
enum class CellState : uint8_t {
EMPTY,
SHIP,
HIT,
MISS,
SUNK
};
enum class MessageType : uint8_t {
READY,
SHOT,
HIT,
MISS,
SUNK,
WIN
};
struct Ship {
ShipType type;
uint8_t x;
uint8_t y;
bool horizontal;
uint8_t hits;
bool placed;
uint8_t size() const {
return static_cast<uint8_t>(type);
}
bool is_sunk() const {
return hits >= size();
}
};
struct GameMessage {
MessageType type;
uint8_t x;
uint8_t y;
};
class BattleshipView : public View {
public:
BattleshipView(NavigationView& nav);
~BattleshipView();
void focus() override;
void paint(Painter& painter) override;
bool on_touch(const TouchEvent event) override;
bool on_encoder(const EncoderEvent delta) override;
bool on_key(const KeyEvent key) override;
std::string title() const override { return "Battleship"; }
private:
NavigationView& nav_;
RxRadioState rx_radio_state_{
DEFAULT_FREQUENCY /* frequency */,
1750000 /* bandwidth */,
2280000 /* sampling rate */
};
TxRadioState tx_radio_state_{
DEFAULT_FREQUENCY /* frequency */,
1750000 /* bandwidth */,
2280000 /* sampling rate */
};
app_settings::SettingsManager settings_{
"battleship",
app_settings::Mode::RX_TX,
{{"rx_freq"sv, &rx_frequency},
{"tx_freq"sv, &tx_frequency},
{"wins"sv, &wins},
{"losses"sv, &losses}}};
GameState game_state{GameState::MENU};
bool is_red_team{false};
bool opponent_ready{false};
uint32_t wins{0};
uint32_t losses{0};
std::array<std::array<CellState, GRID_SIZE>, GRID_SIZE> my_grid{};
std::array<std::array<CellState, GRID_SIZE>, GRID_SIZE> enemy_grid{};
std::string current_status{"Choose your team!"};
std::string current_score{"W:0 L:0"};
std::array<Ship, 5> my_ships{};
uint8_t current_ship_index{0};
bool placing_horizontal{true};
uint8_t ships_remaining{5};
uint8_t enemy_ships_remaining{5};
uint8_t cursor_x{0};
uint8_t cursor_y{0};
uint8_t target_x{0};
uint8_t target_y{0};
bool touch_enabled{true};
uint32_t tx_frequency{DEFAULT_FREQUENCY};
uint32_t rx_frequency{DEFAULT_FREQUENCY};
bool is_transmitting{false};
// POCSAG decoding state
pocsag::EccContainer ecc{};
pocsag::POCSAGState pocsag_state{&ecc};
uint32_t last_address{0};
RSSI rssi{
{21 * 8, 0, 6 * 8, 4}};
FrequencyField field_frequency{
{10, 50}};
Text text_status{
{10, 16, 220, 16},
"Choose your team!"};
Text text_score{
{170, 16, 60, 16},
"W:0 L:0"};
Button button_red_team{
{20, 100, 90, 40},
"RED TEAM"};
Button button_blue_team{
{130, 100, 90, 40},
"BLUE TEAM"};
Button button_rotate{
{10, BUTTON_Y, 60, 32},
"Rotate"};
Button button_place{
{80, BUTTON_Y, 60, 32},
"Place"};
Button button_fire{
{80, BUTTON_Y, 60, 32},
"Fire!"};
Button button_menu{
{150, BUTTON_Y, 60, 32},
"Menu"};
void init_game();
void reset_game();
void start_team(bool red);
void setup_ships();
void draw_grid(Painter& painter, uint8_t grid_x, uint8_t grid_y, const std::array<std::array<CellState, GRID_SIZE>, GRID_SIZE>& grid, bool show_ships, bool is_offense_grid = false);
void draw_cell(Painter& painter, uint8_t cell_x, uint8_t cell_y, CellState state, bool show_ships, bool is_offense_grid, bool is_cursor);
void draw_ship_preview(Painter& painter);
void place_ship();
bool can_place_ship(uint8_t x, uint8_t y, uint8_t size, bool horizontal);
void fire_at_position();
void process_shot(uint8_t x, uint8_t y);
void update_score();
bool is_cursor_at(uint8_t x, uint8_t y, bool is_offense_grid);
void send_message(const GameMessage& msg);
void process_message(const std::string& message);
bool parse_coords(const std::string& coords, uint8_t& x, uint8_t& y);
void mark_ship_sunk(uint8_t x, uint8_t y, std::array<std::array<CellState, GRID_SIZE>, GRID_SIZE>& grid);
void configure_radio_tx();
void configure_radio_rx();
MessageHandlerRegistration message_handler_packet{
Message::ID::POCSAGPacket,
[this](Message* const p) {
const auto message = static_cast<const POCSAGPacketMessage*>(p);
on_pocsag_packet(message);
}};
MessageHandlerRegistration message_handler_tx_progress{
Message::ID::TXProgress,
[this](const Message* const p) {
const auto message = static_cast<const TXProgressMessage*>(p);
on_tx_progress(message->progress, message->done);
}};
void on_pocsag_packet(const POCSAGPacketMessage* message);
void on_tx_progress(const uint32_t progress, const bool done);
};
} // namespace ui::external_app::battleship
#endif

View File

@@ -227,7 +227,11 @@ set(EXTCPPSRC
#blackjack #blackjack
external/blackjack/main.cpp external/blackjack/main.cpp
external/blackjack/ui_blackjack.cpp external/blackjack/ui_blackjack.cpp
#battleship
external/battleship/main.cpp
external/battleship/ui_battleship.cpp
) )
set(EXTAPPLIST set(EXTAPPLIST
@@ -286,4 +290,5 @@ set(EXTAPPLIST
detector_rx detector_rx
spaceinv spaceinv
blackjack blackjack
battleship
) )

View File

@@ -78,6 +78,7 @@ MEMORY
ram_external_app_dinogame (rwx) : org = 0xADE50000, len = 32k ram_external_app_dinogame (rwx) : org = 0xADE50000, len = 32k
ram_external_app_spaceinv (rwx) : org = 0xADE60000, len = 32k ram_external_app_spaceinv (rwx) : org = 0xADE60000, len = 32k
ram_external_app_blackjack (rwx) : org = 0xADE70000, len = 32k ram_external_app_blackjack (rwx) : org = 0xADE70000, len = 32k
ram_external_app_battleship (rwx) : org = 0xADE80000, len = 32k
} }
SECTIONS SECTIONS
@@ -412,5 +413,11 @@ SECTIONS
*(*ui*external_app*blackjack*); *(*ui*external_app*blackjack*);
} > ram_external_app_blackjack } > ram_external_app_blackjack
.external_app_battleship : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_battleship.application_information));
*(*ui*external_app*battleship*);
} > ram_external_app_battleship
} }