diff --git a/config.template b/config.template index 3009c4d..33fc450 100644 --- a/config.template +++ b/config.template @@ -448,6 +448,7 @@ hangman = True hamtest = True tictactoe = True wordOfTheDay = True +battleShip = True # enable or disable the quiz game module questions are in data/quiz.json quiz = False diff --git a/mesh_bot.py b/mesh_bot.py index 1f8e98c..3407e0b 100755 --- a/mesh_bot.py +++ b/mesh_bot.py @@ -16,7 +16,7 @@ import modules.settings as my_settings from modules.system import * # list of commands to remove from the default list for DM only -restrictedCommands = ["blackjack", "videopoker", "dopewars", "lemonstand", "golfsim", "mastermind", "hangman", "hamtest", "tictactoe", "tic-tac-toe", "quiz", "q:", "survey", "s:"] +restrictedCommands = ["blackjack", "videopoker", "dopewars", "lemonstand", "golfsim", "mastermind", "hangman", "hamtest", "tictactoe", "tic-tac-toe", "quiz", "q:", "survey", "s:", "battleship"] restrictedResponse = "🤖only available in a Direct Message📵" # "" for none def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_number, deviceID, isDM): @@ -31,6 +31,7 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n "ask:": lambda: handle_llm(message_from_id, channel_number, deviceID, message, publicChannel), "askai": lambda: handle_llm(message_from_id, channel_number, deviceID, message, publicChannel), "bannode": lambda: handle_bbsban(message, message_from_id, isDM), + "battleship": lambda: handleBattleship(message, message_from_id, deviceID), "bbsack": lambda: bbs_sync_posts(message, message_from_id, deviceID), "bbsdelete": lambda: handle_bbsdelete(message, message_from_id), "bbshelp": bbs_help, @@ -1094,6 +1095,118 @@ def handleTicTacToe(message, nodeID, deviceID): msg = tictactoe.play(nodeID, message) return msg + +def handleBattleship(message, nodeID, deviceID): + global battleshipTracker + from modules.games import battleship + + # Helper to get short_name from tracker + def get_short_name(nid): + entry = next((e for e in battleshipTracker if e['nodeID'] == nid), None) + return entry['short_name'] if entry and 'short_name' in entry else get_name_from_number(nid, 'short', deviceID) + + msg_lower = message.lower().strip() + tracker_entry = next((entry for entry in battleshipTracker if entry['nodeID'] == nodeID), None) + + # End/exit command + if msg_lower.startswith('end') or msg_lower.startswith('exit'): + if tracker_entry: + if 'session_id' in tracker_entry: + battleship.Battleship.end_game(tracker_entry['session_id']) + battleshipTracker.remove(tracker_entry) + return "Thanks for playing Battleship! 🚢" + + # Create new P2P game with short code + if msg_lower.startswith("battleship new"): + short_name = get_name_from_number(nodeID, 'short', deviceID) + msg, code = battleship.Battleship.new_game(nodeID, vs_ai=False) + battleshipTracker.append({ + "nodeID": nodeID, + "short_name": short_name, + "last_played": time.time(), + "session_id": battleship.Battleship.short_codes.get(code, code) + }) + return f"{msg}" + + # Show open P2P games waiting for a player + if msg_lower.startswith("battleship lobby"): + open_codes = [] + for code, session_id in battleship.Battleship.short_codes.items(): + session = battleship.Battleship.sessions.get(session_id) + if session and session.player2_id is None: + open_codes.append(code) + if not open_codes: + return "No open Battleship games waiting for players." + return "Open Battleship games (join with 'battleship join '):\n" + ", ".join(open_codes) + + # Join existing P2P game using short code + if msg_lower.startswith("battleship join"): + try: + code = msg_lower.split("join", 1)[1].strip() + except IndexError: + return "Usage: battleship join " + session = battleship.Battleship.get_session(code) + if not session: + return "Session not found." + if session.player2_id is not None: + return "Session already has two players." + session.player2_id = nodeID + session.next_turn = nodeID # Make joining player go first! + short_name = get_name_from_number(nodeID, 'short', deviceID) + battleshipTracker.append({ + "nodeID": nodeID, + "short_name": short_name, + "last_played": time.time(), + "session_id": session.session_id + }) + p1_short_name = get_short_name(session.player1_id) + send_message( + f"{p1_short_name}, your opponent {short_name} has joined the game! It's their turn first.", + 0, # channel 0 for DM + session.player1_id, # recipient nodeID + deviceID + ) + time.sleep(splitDelay) # slight delay to avoid message overlap + return "You joined the game! It's your turn. Enter your move (e.g., 'B4')." + + # If not found, create new tracker entry and new game vs AI (default) + if not tracker_entry: + short_name = get_name_from_number(nodeID, 'short', deviceID) + msg, session_id = battleship.Battleship.new_game(nodeID) + battleshipTracker.append({ + "nodeID": nodeID, + "short_name": short_name, + "last_played": time.time(), + "session_id": session_id + }) + return msg + + # Update last played + tracker_entry["last_played"] = time.time() + session_id = tracker_entry.get("session_id") + + # Play the game and check if we need to alert the next player + response = battleship.playBattleship(message, nodeID, deviceID, session_id=session_id) + + # --- Notify the next player when it's their turn in P2P --- + session = battleship.Battleship.get_session(session_id) + if session and not session.vs_ai and session.player1_id and session.player2_id: + # Only notify if the game is not over (optional: add a game-over check) + if getattr(session, "last_move", None): + next_player_id = session.next_turn + # Only notify if it's not the player who just moved + if next_player_id != nodeID: + next_player_short_name = get_short_name(next_player_id) + send_message( + f"{next_player_short_name}, it's your turn in Battleship! Enter your move (e.g., 'B4').", + 0, # channel 0 for DM + next_player_id, + deviceID + ) + time.sleep(splitDelay) # slight delay to avoid message overlap + + return response + def quizHandler(message, nodeID, deviceID): global quizGamePlayer user_name = get_name_from_number(nodeID) @@ -2128,6 +2241,7 @@ gameTrackers = [ (hamtestTracker, "HamTest", handleHamtest), (tictactoeTracker, "TicTacToe", handleTicTacToe), (surveyTracker, "Survey", surveyHandler), + (battleshipTracker, "Battleship", handleBattleship), # quiz does not use a tracker (quizGamePlayer) always active ] diff --git a/modules/games/README.md b/modules/games/README.md index 45f1eeb..ec4ee73 100644 --- a/modules/games/README.md +++ b/modules/games/README.md @@ -8,12 +8,13 @@ - [Lemonade Stand](#lemonade-stand-game-module) - [Tic-Tac-Toe (2D/3D)](#tic-tac-toe-game-module) - [MasterMind](#mastermind-game-module) +- [Battleship](#battleship-game-module) - [Video Poker](#video-poker-game-module) - [Hangman](#hangman-game-module) - [Quiz](#quiz-game-module) - [Survey](#survey--module-game) - [Word of the Day Game](#word-of-the-day-game--rules--features) -- [Game Server Configuration (`game.ini`)](#game-server-configuration-gameini) +- [Game Server](#game-server-configuration-gameini) - [PyGame Help](#pygame-help) --- @@ -520,6 +521,77 @@ Place your Bet, or (L)eave Table. - Adapted for Meshtastic mesh-bot by K7MHI Kelly Keeton 2024 +# Battleship Game Module + +A classic Battleship game for the Meshtastic mesh-bot. Play solo against the AI or challenge another user in peer-to-peer (P2P) mode! + +## How to Play + +- **Start a New Game (vs AI):** + Send `battleship` via DM to the bot to start a new game against the AI. + +- **Start a New P2P Game:** + Send `battleship new` to create a game and receive a join code. + Share the code with another user. + +- **Join a P2P Game:** + Send `battleship join ` (replace `` with the provided number) to join a waiting game. + +- **View Open Games:** + Send `battleship lobby` to see a list of open P2P games waiting for players. + +- **Gameplay:** + - Enter your move using coordinates: + - Format: `B4` or `B,4` (row letter, column number) + - Example: `C7` + - The bot will show your radar, ship status, and results after each move. + - In P2P, you and your opponent take turns. The bot will notify you when it’s your turn. + +- **End Game:** + Send `end` or `exit` to leave your current game. + +## Rules & Features + +- 10x10 grid, classic ship sizes (Carrier, Battleship, Cruiser, Submarine, Destroyer). +- Ships are placed randomly. +- In P2P, the joining player goes first. +- Radar view shows a 4x4 grid centered on your last move. +- Game tracks whose turn it is and notifies the next player in P2P mode. +- Game ends when all ships of one player are sunk. + +## Example Session + +``` +New 🚢Battleship🤖 game started! +Enter your move using coordinates: row-letter, column-number. +Example: B5 or C,7 +Type 'exit' or 'end' to quit the game. + +> B4 + +Your move: 💥Hit! +AI ships: 5/5 afloat +Radar: +🗺️3 4 5 6 +B ~ ~ * ~ +C ~ ~ ~ ~ +D ~ ~ ~ ~ +E ~ ~ ~ ~ +AI move: D7 (missed) +Your ships: 5/5 afloat +``` + +## Notes + +- Only one Battleship session per player at a time. +- Play via DM for best experience. +- In P2P, share the join code with your opponent. +- Coordinates are not case-sensitive. + +## Credits + +- Written for Meshtastic mesh-bot by K7MHI Kelly Keeton 2025 + # Word of the Day Game — Rules & Features - **Word of the Day:** @@ -736,8 +808,6 @@ This module implements a survey system for the Meshtastic mesh-bot. **Written for Meshtastic mesh-bot by K7MHI Kelly Keeton 2025** - - ___ # Game Server Configuration (`game.ini`) @@ -786,5 +856,4 @@ mUDP library provides UDP-based broadcasting of Meshtastic-compatible packets. M **Installation** ```sh pip install mudp -``` ---- \ No newline at end of file +``` \ No newline at end of file diff --git a/modules/games/battleship.py b/modules/games/battleship.py new file mode 100644 index 0000000..ac2cace --- /dev/null +++ b/modules/games/battleship.py @@ -0,0 +1,407 @@ +#!/usr/bin/env python3 +import random +import copy +import uuid + +OCEAN = "~" +FIRE = "x" +HIT = "*" +SIZE = 10 +SHIPS = [5, 4, 3, 3, 2] +SHIP_NAMES = ["✈️Carrier", "Battleship", "Cruiser", "Submarine", "Destroyer"] + +class Session: + def __init__(self, player1_id, player2_id=None, vs_ai=True): + self.session_id = str(uuid.uuid4()) + self.vs_ai = vs_ai + self.player1_id = player1_id + self.player2_id = player2_id + self.game = Battleship(vs_ai=vs_ai) + self.next_turn = player1_id + self.last_move = None + +class Battleship: + sessions = {} + short_codes = {} + + @classmethod + def _generate_short_code(cls): + while True: + code = str(random.randint(1000, 9999)) + if code not in cls.short_codes: + return code + + @classmethod + def new_game(cls, player_id, vs_ai=True, p2p_id=None): + session = Session(player1_id=player_id, player2_id=p2p_id, vs_ai=vs_ai) + cls.sessions[session.session_id] = session + if not vs_ai: + code = cls._generate_short_code() + cls.short_codes[code] = session.session_id + msg = ( + "New 🚢Battleship🚢 game started!\n" + "Joining player goes first, waiting for them to join...\n" + f"Share\n'battleship join {code}'" + ) + return msg, code + else: + msg = ( + "New 🚢Battleship🤖 game started!\n" + "Enter your move using coordinates: row-letter, column-number.\n" + "Example: B5 or C,7\n" + "Type 'exit' or 'end' to quit the game." + ) + return msg, session.session_id + + @classmethod + def end_game(cls, session_id): + if session_id in cls.sessions: + del cls.sessions[session_id] + return "Thanks for playing 🚢Battleship🚢" + + @classmethod + def get_session(cls, code_or_session_id): + session_id = cls.short_codes.get(code_or_session_id, code_or_session_id) + return cls.sessions.get(session_id) + + def __init__(self, vs_ai=True): + if vs_ai: + self.player_board = self._blank_board() + self.ai_board = self._blank_board() + self.player_radar = self._blank_board() + self.ai_radar = self._blank_board() + self.number_board = self._blank_board() + self.player_alive = sum(SHIPS) + self.ai_alive = sum(SHIPS) + self._place_ships(self.player_board, self.number_board) + self._place_ships(self.ai_board) + self.ai_targets = [] + self.ai_last_hit = None + self.ai_orientation = None + else: + # P2P: Each player has their own board and radar + self.player1_board = self._blank_board() + self.player2_board = self._blank_board() + self.player1_radar = self._blank_board() + self.player2_radar = self._blank_board() + self.player1_alive = sum(SHIPS) + self.player2_alive = sum(SHIPS) + self._place_ships(self.player1_board) + self._place_ships(self.player2_board) + + def _blank_board(self): + return [[OCEAN for _ in range(SIZE)] for _ in range(SIZE)] + + def _place_ships(self, board, number_board=None): + for idx, ship_len in enumerate(SHIPS): + placed = False + while not placed: + vertical = random.choice([True, False]) + if vertical: + row = random.randint(0, SIZE - ship_len) + col = random.randint(0, SIZE - 1) + if all(board[row + i][col] == OCEAN for i in range(ship_len)): + for i in range(ship_len): + board[row + i][col] = str(idx) + if number_board is not None: + number_board[row + i][col] = idx + placed = True + else: + row = random.randint(0, SIZE - 1) + col = random.randint(0, SIZE - ship_len) + if all(board[row][col + i] == OCEAN for i in range(ship_len)): + for i in range(ship_len): + board[row][col + i] = str(idx) + if number_board is not None: + number_board[row][col + i] = idx + placed = True + + def player_move(self, row, col): + """Player fires at AI's board. Returns 'hit', 'miss', or 'sunk:'.""" + if self.player_radar[row][col] != OCEAN: + return "repeat" + if self.ai_board[row][col] not in (OCEAN, FIRE, HIT): + self.player_radar[row][col] = HIT + ship_idx = int(self.ai_board[row][col]) + self.ai_board[row][col] = HIT + if self._is_ship_sunk(self.ai_board, ship_idx): + self.ai_alive -= SHIPS[ship_idx] + return f"sunk:{ship_idx}" + return "hit" + else: + self.player_radar[row][col] = FIRE + self.ai_board[row][col] = FIRE + return "miss" + + def ai_move(self): + """AI fires at player's board. Returns (row, col, result or 'sunk:').""" + while True: + row = random.randint(0, SIZE - 1) + col = random.randint(0, SIZE - 1) + if self.ai_radar[row][col] == OCEAN: + break + if self.player_board[row][col] not in (OCEAN, FIRE, HIT): + self.ai_radar[row][col] = HIT + ship_idx = int(self.player_board[row][col]) + self.player_board[row][col] = HIT + if self._is_ship_sunk(self.player_board, ship_idx): + self.player_alive -= SHIPS[ship_idx] + return row, col, f"sunk:{ship_idx}" + return row, col, "hit" + else: + self.ai_radar[row][col] = FIRE + self.player_board[row][col] = FIRE + return row, col, "miss" + + def p2p_player_move(self, row, col, attacker, defender, radar, defender_alive_attr): + """P2P: attacker fires at defender's board, updates radar and defender's board.""" + if radar[row][col] != OCEAN: + return "repeat" + if defender[row][col] not in (OCEAN, FIRE, HIT): + radar[row][col] = HIT + ship_idx = int(defender[row][col]) + defender[row][col] = HIT + if self._is_ship_sunk(defender, ship_idx): + setattr(self, defender_alive_attr, getattr(self, defender_alive_attr) - SHIPS[ship_idx]) + return f"sunk:{ship_idx}" + return "hit" + else: + radar[row][col] = FIRE + defender[row][col] = FIRE + return "miss" + + def _is_ship_sunk(self, board, ship_idx): + for row in board: + for cell in row: + if cell == str(ship_idx): + return False + return True + + def is_game_over(self, vs_ai=True): + if vs_ai: + return self.player_alive == 0 or self.ai_alive == 0 + else: + return self.player1_alive == 0 or self.player2_alive == 0 + + def get_player_board(self): + return copy.deepcopy(self.player_board) + + def get_player_radar(self): + return copy.deepcopy(self.player_radar) + + def get_ai_board(self): + return copy.deepcopy(self.ai_board) + + def get_ai_radar(self): + return copy.deepcopy(self.ai_radar) + + def get_ship_status(self, board): + status = {} + for idx in range(len(SHIPS)): + afloat = any(str(idx) in row for row in board) + status[idx] = "Afloat" if afloat else "Sunk" + return status + + def display_draw_board(self, board, label="Board"): + print(f"{label}") + print(" " + " ".join(str(i+1).rjust(2) for i in range(SIZE))) + for idx, row in enumerate(board): + print(chr(ord('A') + idx) + " " + " ".join(cell.rjust(2) for cell in row)) + +def get_short_name(node_id): + from mesh_bot import battleshipTracker + entry = next((e for e in battleshipTracker if e['nodeID'] == node_id), None) + return entry['short_name'] if entry and 'short_name' in entry else str(node_id) + +def playBattleship(message, nodeID, deviceID, session_id=None): + if not session_id or session_id not in Battleship.sessions: + return Battleship.new_game(nodeID, vs_ai=True) + + session = Battleship.get_session(session_id) + game = session.game + + if not session.vs_ai and session.player2_id is None: + code = next((k for k, v in Battleship.short_codes.items() if v == session.session_id), None) + return ( + f"Waiting for another player to join.\n" + f"Share this code: {code}\n" + "Type 'end' to cancel this P2P game." + ) + + if nodeID != session.next_turn: + return "It's not your turn!" + + msg = message.strip().lower() + if msg.startswith("battleship"): + msg = msg[len("battleship"):].strip() + if msg.startswith("b:"): + msg = msg[2:].strip() + msg = msg.replace(" ", "") + + x = y = None + if "," in msg: + parts = msg.split(",") + if len(parts) == 2 and len(parts[0]) == 1 and parts[0].isalpha() and parts[1].isdigit(): + y = ord(parts[0]) - ord('a') + x = int(parts[1]) - 1 + else: + return "Invalid coordinates. Use format A2 or A,2 (row letter, column number)." + elif len(msg) >= 2 and msg[0].isalpha() and msg[1:].isdigit(): + y = ord(msg[0]) - ord('a') + x = int(msg[1:]) - 1 + else: + return "Invalid command. Use format A2 or A,2 (row letter, column number)." + + if x is None or y is None or not (0 <= x < SIZE and 0 <= y < SIZE): + return "Coordinates out of range." + + ai_row = ai_col = ai_result = None + over = False + + if session.vs_ai: + result = game.player_move(y, x) + ai_row, ai_col, ai_result = game.ai_move() + over = game.is_game_over(vs_ai=True) + else: + # P2P: determine which player is moving and fire at the other player's board + if nodeID == session.player1_id: + attacker = "player1" + defender = "player2" + result = game.p2p_player_move( + y, x, + game.player1_board, game.player2_board, + game.player1_radar, "player2_alive" + ) + else: + attacker = "player2" + defender = "player1" + result = game.p2p_player_move( + y, x, + game.player2_board, game.player1_board, + game.player2_radar, "player1_alive" + ) + over = game.is_game_over(vs_ai=False) + coord_str = f"{chr(y+65)}{x+1}" + session.last_move = (nodeID, coord_str, result) + + # --- DEBUG DISPLAY --- + DEBUG = False + if DEBUG: + if session.vs_ai: + game.display_draw_board(game.player_board, label=f"Player Board ({session.player1_id})") + game.display_draw_board(game.player_radar, label="Player Radar") + game.display_draw_board(game.ai_board, label="AI Board") + game.display_draw_board(game.ai_radar, label="AI Radar") + else: + p1_id = session.player1_id + p2_id = session.player2_id if session.player2_id else "Waiting" + game.display_draw_board(game.player1_board, label=f"Player 1 Board ({p1_id})") + game.display_draw_board(game.player1_radar, label="Player 1 Radar") + game.display_draw_board(game.player2_board, label=f"Player 2 Board ({p2_id})") + game.display_draw_board(game.player2_radar, label="Player 2 Radar") + + # Format radar as a 4x4 grid centered on the player's move + if session.vs_ai: + radar = game.get_player_radar() + else: + radar = game.player1_radar if nodeID == session.player1_id else game.player2_radar + + window_size = 4 + half_window = window_size // 2 + min_row = max(0, min(y - half_window, SIZE - window_size)) + max_row = min(SIZE, min_row + window_size) + min_col = max(0, min(x - half_window, SIZE - window_size)) + max_col = min(SIZE, min_col + window_size) + + radar_str = "🗺️" + " ".join(str(i+1) for i in range(min_col, max_col)) + "\n" + for idx in range(min_row, max_row): + radar_str += chr(ord('A') + idx) + " " + " ".join(radar[idx][j] for j in range(min_col, max_col)) + "\n" + + def format_ship_status(status_dict): + afloat = 0 + for idx, state in status_dict.items(): + if state == "Afloat": + afloat += 1 + return f"{afloat}/{len(SHIPS)} afloat" + + if session.vs_ai: + ai_status_str = format_ship_status(game.get_ship_status(game.ai_board)) + player_status_str = format_ship_status(game.get_ship_status(game.player_board)) + else: + ai_status_str = format_ship_status(game.get_ship_status(game.player2_board)) + player_status_str = format_ship_status(game.get_ship_status(game.player1_board)) + + def move_result_text(res, is_player=True): + if res.startswith("sunk:"): + idx = int(res.split(":")[1]) + name = SHIP_NAMES[idx] + return f"Sunk🎯 {name}!" + elif res == "hit": + return "💥Hit!" + elif res == "miss": + return "missed" + elif res == "repeat": + return "📋already targeted" + else: + return res + + # After a valid move, switch turns + if session.vs_ai: + session.next_turn = nodeID + else: + session.next_turn = session.player2_id if nodeID == session.player1_id else session.player1_id + + # Output message + if session.vs_ai: + msg_out = ( + f"Your move: {move_result_text(result)}\n" + f"AI ships: {ai_status_str}\n" + f"Radar:\n{radar_str}" + f"AI move: {chr(ai_row+65)}{ai_col+1} ({move_result_text(ai_result, False)})\n" + f"Your ships: {player_status_str}" + ) + else: + my_name = get_short_name(nodeID) + opponent_id = session.player2_id if nodeID == session.player1_id else session.player1_id + opponent_short_name = get_short_name(opponent_id) if opponent_id else "Waiting" + opponent_label = f"{opponent_short_name}:" + my_move_result_str = f"Your move: {move_result_text(result)}\n" + last_move_str = "" + if session.last_move and session.last_move[0] != nodeID: + last_player_short_name = get_short_name(session.last_move[0]) + last_coord = session.last_move[1] + last_result = move_result_text(session.last_move[2]) + last_move_str = f"Last move by {last_player_short_name}: {last_coord} ({last_result})\n" + if session.next_turn == nodeID: + turn_prompt = f"Your turn, {my_name}! Enter your move:" + else: + turn_prompt = f"Waiting for {opponent_short_name}..." + msg_out = ( + f"{my_move_result_str}" + f"{last_move_str}" + f"{opponent_label} {ai_status_str}\n" + f"Radar:\n{radar_str}" + f"Your ships: {player_status_str}\n" + f"{turn_prompt}" + ) + + if over: + if session.vs_ai: + if game.player_alive == 0: + winner = "AI 🤖" + msg_out += f"\nGame over! {winner} wins! Better luck next time.\n" + else: + winner = get_short_name(nodeID) + msg_out += f"\nGame over! {winner} wins! You sank all the AI's ships! 🎉\n" + else: + # P2P: Announce winner by short name + if game.player1_alive == 0: + winner = get_short_name(session.player2_id) + elif game.player2_alive == 0: + winner = get_short_name(session.player1_id) + else: + winner = "Nobody" + msg_out += f"\nGame over! {winner} wins! 🚢🏆\n" + msg_out += "Type 'battleship' to start a new game." + + return msg_out \ No newline at end of file diff --git a/modules/games/battleship_vid.py b/modules/games/battleship_vid.py new file mode 100644 index 0000000..165cfef --- /dev/null +++ b/modules/games/battleship_vid.py @@ -0,0 +1,94 @@ +import pygame +import sys +import time + +from modules.games.battleship import Battleship, SHIP_NAMES, SIZE, OCEAN, FIRE, HIT + +CELL_SIZE = 40 +BOARD_MARGIN = 50 +STATUS_WIDTH = 320 + +def draw_board(screen, board, top_left, cell_size, show_ships=False): + font = pygame.font.Font(None, 28) + x0, y0 = top_left + for y in range(SIZE): + for x in range(SIZE): + rect = pygame.Rect(x0 + x*cell_size, y0 + y*cell_size, cell_size, cell_size) + pygame.draw.rect(screen, (100, 100, 200), rect, 1) + val = board[y][x] + # Show ships if requested, otherwise hide ship numbers + if not show_ships and val.isdigit(): + val = OCEAN + color = (200, 200, 255) if val == OCEAN else (255, 0, 0) if val == FIRE else (0, 255, 0) if val == HIT else (255,255,255) + if val != OCEAN: + pygame.draw.rect(screen, color, rect) + text = font.render(val, True, (0,0,0)) + screen.blit(text, rect.move(10, 5)) + # Draw row/col labels + for i in range(SIZE): + # Col numbers + num_surface = font.render(str(i+1), True, (255, 255, 0)) + screen.blit(num_surface, (x0 + i*cell_size + cell_size//2 - 8, y0 - 24)) + # Row letters + letter_surface = font.render(chr(ord('A') + i), True, (255, 255, 0)) + screen.blit(letter_surface, (x0 - 28, y0 + i*cell_size + cell_size//2 - 10)) + +def draw_status_panel(screen, game, top_left, width, height, is_player=True): + font = pygame.font.Font(None, 32) + x0, y0 = top_left + pygame.draw.rect(screen, (30, 30, 60), (x0, y0, width, height), border_radius=10) + # Title + title = font.render("Game Status", True, (255, 255, 0)) + screen.blit(title, (x0 + 10, y0 + 10)) + # Ships status + ships_title = font.render("Ships Remaining:", True, (200, 200, 255)) + screen.blit(ships_title, (x0 + 10, y0 + 60)) + # Get ship status + if is_player: + status_dict = game.get_ship_status(game.player_board) + else: + status_dict = game.get_ship_status(game.ai_board) + for i, ship in enumerate(SHIP_NAMES): + status = status_dict.get(i, "Afloat") + name_color = (200, 200, 255) + if status.lower() == "sunk": + status_color = (255, 0, 0) + status_text = "Sunk" + else: + status_color = (0, 255, 0) + status_text = "Afloat" + ship_name_surface = font.render(f"{ship}:", True, name_color) + screen.blit(ship_name_surface, (x0 + 20, y0 + 100 + i * 35)) + status_surface = font.render(f"{status_text}", True, status_color) + screen.blit(status_surface, (x0 + 180, y0 + 100 + i * 35)) + +def battleship_visual_main(game): + pygame.init() + screen = pygame.display.set_mode((2*SIZE*CELL_SIZE + STATUS_WIDTH + 3*BOARD_MARGIN, SIZE*CELL_SIZE + 2*BOARD_MARGIN)) + pygame.display.set_caption("Battleship Visualizer") + clock = pygame.time.Clock() + running = True + while running: + for event in pygame.event.get(): + if event.type == pygame.QUIT or (event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE): + running = False + screen.fill((20, 20, 30)) + # Draw radar (left) + draw_board(screen, game.get_player_radar(), (BOARD_MARGIN, BOARD_MARGIN+30), CELL_SIZE, show_ships=False) + radar_label = pygame.font.Font(None, 36).render("Your Radar", True, (0,255,255)) + screen.blit(radar_label, (BOARD_MARGIN, BOARD_MARGIN)) + # Draw player board (right) + draw_board(screen, game.get_player_board(), (SIZE*CELL_SIZE + 2*BOARD_MARGIN, BOARD_MARGIN+30), CELL_SIZE, show_ships=True) + board_label = pygame.font.Font(None, 36).render("Your Board", True, (0,255,255)) + screen.blit(board_label, (SIZE*CELL_SIZE + 2*BOARD_MARGIN, BOARD_MARGIN)) + # Draw status panel (far right) + draw_status_panel(screen, game, (2*SIZE*CELL_SIZE + 2*BOARD_MARGIN, BOARD_MARGIN), STATUS_WIDTH, SIZE*CELL_SIZE) + pygame.display.flip() + clock.tick(30) + pygame.quit() + sys.exit() + +# if __name__ == "__main__": +# # Example: create a new game and show the boards +# game = Battleship(vs_ai=True) +# battleship_visual_main(game) \ No newline at end of file diff --git a/modules/settings.py b/modules/settings.py index 754cf0a..9fd2079 100644 --- a/modules/settings.py +++ b/modules/settings.py @@ -50,6 +50,7 @@ lemonadeTracker = [] # Lemonade Stand game tracker dwPlayerTracker = [] # DopeWars player tracker jackTracker = [] # Jack game tracker mindTracker = [] # Mastermind (mmind) game tracker +battleshipTracker = [] # Battleship game tracker # Memory Management Constants MAX_MSG_HISTORY = 250 @@ -484,6 +485,7 @@ try: surveyRecordID = config['games'].getboolean('surveyRecordID', True) surveyRecordLocation = config['games'].getboolean('surveyRecordLocation', True) wordOfTheDay = config['games'].getboolean('wordOfTheDay', True) + battleship_enabled = config['games'].getboolean('battleShip', True) # messaging settings responseDelay = config['messagingSettings'].getfloat('responseDelay', 0.7) # default 0.7 diff --git a/modules/system.py b/modules/system.py index 69b0f43..fd57f3b 100644 --- a/modules/system.py +++ b/modules/system.py @@ -235,6 +235,11 @@ if wordOfTheDay: theWordOfTheDay = WordOfTheDayGame() # this runs in background and wont enable other games +if battleship_enabled: + from modules.games.battleship import playBattleship # from the spudgunman/meshing-around repo + trap_list = trap_list + ("battleship",) + games_enabled = True + # Games Configuration if games_enabled is True: help_message = help_message + ", games" @@ -262,6 +267,8 @@ if games_enabled is True: gamesCmdList += "hamTest, " if tictactoe_enabled: gamesCmdList += "ticTacToe, " + if battleship_enabled: + gamesCmdList += "battleship, " gamesCmdList = gamesCmdList[:-2] # remove the last comma else: gamesCmdList = "" @@ -483,7 +490,7 @@ def cleanup_game_trackers(current_time): tracker_names = [ 'dwPlayerTracker', 'lemonadeTracker', 'jackTracker', 'vpTracker', 'mindTracker', 'golfTracker', - 'hangmanTracker', 'hamtestTracker', 'tictactoeTracker', 'surveyTracker' + 'hangmanTracker', 'hamtestTracker', 'tictactoeTracker', 'surveyTracker', 'battleshipTracker' ] for tracker_name in tracker_names: