├── BattagliaNavale.py ├── LICENSE └── README.md /BattagliaNavale.py: -------------------------------------------------------------------------------- 1 | ''' 2 | File: BattagliaNavale.py 3 | Author: Njco - @nkoexe 4 | Date: 25-05-2022 5 | Version: 1.0 6 | Copyright: 2022 Njco 7 | License: MIT 8 | 9 | 10 | Programma per giocare a Battaglia Navale contro il computer. 11 | 12 | ~ Fase 1: Piazzamento delle navi. 13 | Piazzamento delle navi in base a quelle disponibili, descritte secondo (lunghezza_nave, quantita_disponibile) nella lista sottostante. 14 | + Comandi: 15 | ← ↑ → ↓ : Per muovere la selezione della nave. 16 | r : Per ruotare la nave. 17 | : Per piazzare la nave. 18 | 19 | 20 | ~ Fase 2: Gioco. 21 | A turno si colpisce un punto del campo avversario, vince chi affonda tutte le navi avversarie. 22 | + Comandi: 23 | ← ↑ → ↓ : Per muovere il mirino. 24 | : Per colpire il punto selezionato. 25 | 26 | Requisiti: 27 | keyboard: pip install keyboard (richiede sudo su linux, anche per l'esecuzione) 28 | 29 | Programma creato per Windows, su Unix potrebbero esserci problemi nella gestione dell'input. 30 | ''' 31 | 32 | from os import system, name 33 | from random import randint 34 | from keyboard import add_hotkey, remove_hotkey, wait 35 | 36 | # ----------- 37 | # Game variables 38 | BOARD_WIDTH = 10 39 | BOARD_HEIGHT = 10 40 | AVAILABLE_BOATS = [(4, 1), 41 | (3, 2), 42 | (2, 3)] 43 | 44 | # ----------- 45 | # Graphic elements 46 | EMPTY = ' ' 47 | MISS = '·' 48 | BOAT = '○' 49 | HIT = '●' 50 | SUNK = '◆' 51 | 52 | SELECTORS = ('⎡', '⎦') 53 | 54 | # ----------- 55 | # Don't touch 56 | HORIZONTAL = 0 57 | VERTICAL = 1 58 | 59 | # Testing 60 | SHOW_OPPONENT_BOARD = False 61 | 62 | 63 | class Boat: 64 | def __init__(self, size: int, pos: tuple, direction: int): 65 | self.size = size 66 | self.direction = direction 67 | self.hits = [] 68 | self.sunk = False 69 | 70 | # Initialize the coordinates along the orientation 71 | self.coords = [] 72 | if direction == HORIZONTAL: 73 | for i in range(size): 74 | self.coords.append((pos[0], pos[1] + i)) 75 | elif direction == VERTICAL: 76 | for i in range(size): 77 | self.coords.append((pos[0] + i, pos[1])) 78 | 79 | def hit(self, pos: tuple): 80 | # Check if a hit is valid, and if the boat is sunk 81 | if pos in self.coords: 82 | if pos not in self.hits: 83 | self.hits.append(pos) 84 | if len(self.hits) == self.size: 85 | self.sunk = True 86 | return 4 87 | return 3 88 | return 1 89 | 90 | 91 | class Screen: 92 | def __init__(self): 93 | self.pointerx = self.pointery = 0 94 | 95 | if name == 'nt': 96 | self.clear = lambda: system('cls') 97 | else: 98 | self.clear = lambda: print('\033c') 99 | 100 | def render_board(self, board: list, selected: list = None, hidden: bool = False): 101 | # Turn the board list into a string 102 | res = '┌' + '───┬' * (BOARD_WIDTH-1) + '───┐\n' 103 | for i in range(BOARD_HEIGHT): 104 | res += '│' 105 | for j in range(BOARD_WIDTH): 106 | if selected is not None and (i, j) in selected: 107 | selectors = SELECTORS 108 | else: 109 | selectors = (' ', ' ') 110 | 111 | if board[i][j] == 0: 112 | res += selectors[0] + EMPTY + selectors[1] 113 | elif board[i][j] == 1: 114 | res += selectors[0] + MISS + selectors[1] 115 | elif board[i][j] == 2: 116 | if hidden: 117 | res += selectors[0] + EMPTY + selectors[1] 118 | else: 119 | res += selectors[0] + BOAT + selectors[1] 120 | elif board[i][j] == 3: 121 | res += selectors[0] + HIT + selectors[1] 122 | elif board[i][j] == 4: 123 | res += selectors[0] + SUNK + selectors[1] 124 | 125 | res += '│' 126 | 127 | # Line separator 128 | if i != BOARD_HEIGHT-1: 129 | res += '\n├' + '───┼' * (BOARD_WIDTH-1) + '───┤\n' 130 | 131 | res += '\n└' + '───┴' * (BOARD_WIDTH-1) + '───┘' 132 | 133 | return res 134 | 135 | def render(self, first_board: list, second_board: list = None, selected: list = None): 136 | if second_board is None: 137 | # If there is no opponent board (when creating a boat) show only the first one 138 | first_board = self.render_board(first_board, selected) 139 | second_board = '' 140 | 141 | else: 142 | # General case with both boards (when playing) 143 | first_board = self.render_board(first_board, selected, not SHOW_OPPONENT_BOARD) + '\n ~' + '^~' * (BOARD_WIDTH*2-2) # Separator between boards 144 | second_board = self.render_board(second_board) 145 | 146 | # Clear the screen, TODO: find the best way to do this 147 | self.clear() 148 | 149 | # Print the boards 150 | print(first_board) 151 | print(second_board) 152 | 153 | def is_valid(self, board: list, boat: Boat): 154 | # Check if the boat is out of bounds 155 | if boat.direction == HORIZONTAL: 156 | if (boat.coords[0][1] + boat.size) > BOARD_WIDTH: 157 | return False 158 | elif boat.direction == VERTICAL: 159 | if (boat.coords[0][0] + boat.size) > BOARD_HEIGHT: 160 | return False 161 | 162 | # Check if the boat overlaps with another one 163 | for y, x in boat.coords: 164 | if board[y][x] != 0: 165 | return False 166 | 167 | return True 168 | 169 | # Generate one boat 170 | def init_boat(self, board: list, length: int, random: bool = False): 171 | # Generate a boat with random position and direction 172 | if random: 173 | y = randint(0, BOARD_HEIGHT-1) 174 | x = randint(0, BOARD_WIDTH-1) 175 | 176 | boat = Boat(length, (y, x), randint(0, 1)) 177 | 178 | # If the boat is valid return it, otherwise try again 179 | if self.is_valid(board, boat): 180 | return boat 181 | 182 | return self.init_boat(board, length, True) 183 | 184 | else: 185 | # Starting orientation 186 | d = HORIZONTAL 187 | 188 | # Function called on keypress 189 | def move(_y: int = 0, _x: int = 0, _d: bool = False): 190 | nonlocal d 191 | 192 | # Move the boat 193 | self.pointery += _y 194 | self.pointerx += _x 195 | 196 | # Reset if out of bounds, TODO: check full length of boat 197 | if self.pointery < 0 or self.pointery >= BOARD_HEIGHT: 198 | self.pointery -= _y 199 | if self.pointerx < 0 or self.pointerx >= BOARD_WIDTH: 200 | self.pointerx -= _x 201 | 202 | # Change direction 203 | if _d: 204 | d = not d 205 | 206 | # Generate selected coordinates 207 | if d == HORIZONTAL: 208 | select = [(self.pointery, self.pointerx + i) for i in range(length)] 209 | elif d == VERTICAL: 210 | select = [(self.pointery + i, self.pointerx) for i in range(length)] 211 | 212 | # Refresh the screen 213 | self.render(board, selected=select) 214 | 215 | # Add keypress hooks to move the boat 216 | add_hotkey('left', move, args=(0, -1), suppress=True) 217 | add_hotkey('up', move, args=(-1, 0), suppress=True) 218 | add_hotkey('right', move, args=(0, 1), suppress=True) 219 | add_hotkey('down', move, args=(1, 0), suppress=True) 220 | add_hotkey('r', move, args=(0, 0, True), suppress=True) 221 | 222 | # Render the board 223 | move() 224 | 225 | # Wait for the user to select a position and press enter 226 | wait('enter', suppress=True) 227 | 228 | # Remove keypress hooks 229 | remove_hotkey('left') 230 | remove_hotkey('up') 231 | remove_hotkey('right') 232 | remove_hotkey('down') 233 | remove_hotkey('r') 234 | 235 | # Generate boat based on selected coordinates and direction 236 | boat = Boat(length, (self.pointery, self.pointerx), d) 237 | 238 | # Check if the boat is valid and return it, otherwise try again 239 | if self.is_valid(board, boat): 240 | return boat 241 | 242 | return self.init_boat(board, length) 243 | 244 | def select_for_hit(self, first_board: list, second_board: list): 245 | 246 | # Function called on keypress 247 | def move(_y: int = 0, _x: int = 0): 248 | # Move the selection 249 | self.pointery += _y 250 | self.pointerx += _x 251 | 252 | # Reset if out of bounds 253 | if self.pointery < 0 or self.pointery >= BOARD_HEIGHT: 254 | self.pointery -= _y 255 | if self.pointerx < 0 or self.pointerx >= BOARD_WIDTH: 256 | self.pointerx -= _x 257 | 258 | self.render(first_board, second_board, [(self.pointery, self.pointerx)]) 259 | 260 | # Add keypress hooks to move the boat 261 | add_hotkey('left', move, args=(0, -1), suppress=True) 262 | add_hotkey('up', move, args=(-1, 0), suppress=True) 263 | add_hotkey('right', move, args=(0, 1), suppress=True) 264 | add_hotkey('down', move, args=(1, 0), suppress=True) 265 | 266 | # Render the board 267 | move() 268 | 269 | # Wait for the user to select a position and press enter 270 | wait('enter', suppress=True) 271 | 272 | # Remove keypress hooks 273 | remove_hotkey('left') 274 | remove_hotkey('up') 275 | remove_hotkey('right') 276 | remove_hotkey('down') 277 | 278 | return (self.pointery, self.pointerx) 279 | 280 | 281 | class Game: 282 | def __init__(self, screen: Screen): 283 | self.screen = screen 284 | self.player_boats = [] 285 | self.opponent_boats = [] 286 | self.player_board = [[0 for i in range(BOARD_WIDTH)] for j in range(BOARD_HEIGHT)] 287 | self.opponent_board = [[0 for i in range(BOARD_WIDTH)] for j in range(BOARD_HEIGHT)] 288 | 289 | def player_turn(self): 290 | hit_coords = self.screen.select_for_hit(self.opponent_board, self.player_board) 291 | 292 | for boat in self.opponent_boats: 293 | hit_res = boat.hit(hit_coords) 294 | 295 | self.opponent_board[hit_coords[0]][hit_coords[1]] = hit_res 296 | 297 | if hit_res != 1: 298 | # Sunk boat 299 | if hit_res == 4: 300 | # Update all coordinates to be sunk 301 | for i in boat.coords: 302 | self.opponent_board[i[0]][i[1]] = 4 303 | 304 | # Check if all boats are sunk 305 | if sum([boat.sunk for boat in self.opponent_boats]) == len(self.opponent_boats): 306 | self.end() 307 | 308 | # Boat hit already found, no need to continue 309 | break 310 | 311 | def opponent_turn(self): 312 | # TODO: AI 313 | # Hit a random coordinate 314 | hit_coords = (randint(0, BOARD_HEIGHT-1), randint(0, BOARD_WIDTH-1)) 315 | 316 | for boat in self.player_boats: 317 | hit_res = boat.hit(hit_coords) 318 | 319 | self.player_board[hit_coords[0]][hit_coords[1]] = hit_res 320 | 321 | if hit_res != 1: 322 | if hit_res == 4: 323 | for i in boat.coords: 324 | self.player_board[i[0]][i[1]] = 4 325 | 326 | if sum([boat.sunk for boat in self.player_boats]) == len(self.player_boats): 327 | self.end(False) 328 | 329 | break 330 | 331 | def init_boats(self): 332 | for l, q in AVAILABLE_BOATS: 333 | for j in range(q): 334 | # RANDOM OPPONENT BOATS 335 | boat = self.screen.init_boat(self.opponent_board, l, random=True) 336 | self.opponent_boats.append(boat) 337 | for y, x in boat.coords: 338 | self.opponent_board[y][x] = 2 339 | 340 | # PLAYER BOATS 341 | boat = self.screen.init_boat(self.player_board, l) 342 | self.player_boats.append(boat) 343 | for y, x in boat.coords: 344 | self.player_board[y][x] = 2 345 | 346 | def start(self): 347 | self.in_game = True 348 | 349 | while True: 350 | self.player_turn() 351 | if not self.in_game: 352 | break 353 | 354 | self.opponent_turn() 355 | if not self.in_game: 356 | break 357 | 358 | def end(self, win: bool = True): 359 | self.in_game = False 360 | 361 | self.screen.render(self.opponent_board, self.player_board) 362 | 363 | print() 364 | if win: 365 | print(' ' * (BOARD_WIDTH*2-11) + '▀▄▀▄▀▄ YOU WON ! ▄▀▄▀▄▀') 366 | else: 367 | print(' ' * (BOARD_WIDTH*2-11) + '▀▄▀▄▀▄ YOU LOST ! ▄▀▄▀▄▀') 368 | 369 | if input('\nPress enter to exit, or r+enter to restart.\n') == 'r': 370 | self.player_boats = [] 371 | self.opponent_boats = [] 372 | self.player_board = [[0 for i in range(BOARD_WIDTH)] for j in range(BOARD_HEIGHT)] 373 | self.opponent_board = [[0 for i in range(BOARD_WIDTH)] for j in range(BOARD_HEIGHT)] 374 | 375 | self.init_boats() 376 | self.start() 377 | 378 | 379 | def main(): 380 | game = Game(Screen()) 381 | game.init_boats() 382 | game.start() 383 | 384 | 385 | if __name__ == '__main__': 386 | main() 387 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Njco - @nkoexe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Personal Python Projects 2 | 3 | Collection of small personal projects I've made. 4 | --------------------------------------------------------------------------------