A Torpedó (angol nyelvterületen Battleship) a legismertebb táblás játékok egyike, amely számítógépes formában is játszható gépi ellenféllel. Az alábbiakban egy tkinter alapú grafikus felületű megvalósítás kerül bemutatásra.
A működő játék elkészítésén túl a cél egy olyan, objektumorientált alkalmazás létrehozása volt, amely szemléltető példaként szolgálhat a Python számos nyelvi lehetőségének használatára, valamint az objektumorientált tervezés bemutatására, ahol világosan elkülönül a játéklogika és a felhasználói felület.
A teljes forráskód a cikk végén megtalálható, illetve GitHubon is elérhető itt: https://github.com/pythontudasepites/battleship
Elsőként a játék használatát ismertetjük, majd bemutatjuk az alkalmazás szerkezetét, működési logikáját és a főbb tervezési megfontolásokat. Ez utóbbi rész elsősorban azoknak szól, akik a program felépítése és a forráskód működése iránt is érdeklődnek.
A játék használata
A program a klasszikus Torpedójáték számítógépes változata, amelyben a játékos a számítógéppel mint gépi ellenféllel mérkőzik meg. A játék a hagyományos szabályokat követi: mindkét fél egy 10×10 mezős táblán helyezi el a flottáját, majd felváltva próbálják elsüllyeszteni egymás hajóit.
A játék indítása
A program elindítása után két játéktábla jelenik meg. A bal oldali tábla a játékos saját flottáját, a jobb oldali pedig az ellenfél rejtett hajóit reprezentálja. A játék kezdetén a program kiírja, hogy milyen méretű hajót kell éppen elhelyezni.
Hajók elhelyezése
A hajók elhelyezése egérkattintásokkal történik a saját táblán. A program mindig jelzi, hogy a következő hajónak hány mezőből kell állnia. A hajó részeit a kívánt mezőkre kattintva lehet kijelölni. A program minden kattintás után ellenőrzi, hogy az aktuális elrendezésből még kialakítható-e szabályos hajó, ezért érvénytelen mező nem választható ki.
A program csak a szabályos elhelyezést engedi meg:
- a hajók kizárólag vízszintesen vagy függőlegesen helyezhetők el;
- a hajótest nem tartalmazhat megszakítást;
- a hajók egymással még a sarkukon sem érintkezhetnek.
Amikor egy hajó elkészült, hangjelzés hallható, majd a program automatikusan a következő hajó elhelyezésére kér.
Miután az összes hajó felkerült a táblára, a játék automatikusan átvált csata üzemmódba.
Csata
A játék során a jobb oldali táblán kell bal egérgombbal rákattintani arra a mezőre, amelyre lövést szeretnénk leadni.
A lövés eredménye azonnal megjelenik:
- kis fekete pont: a lövés nem talált;
- piros X szürke háttéren: találat érte a hajót, de az még nem süllyedt el;
- fekete mező: a hajó teljes egészében elsüllyedt.
Az emberi játékos minden lövését automatikusan követi a számítógép válaszlépése, amely a saját táblán jelenik meg ugyanilyen jelölésekkel.
A játék vége
A játék addig tart, amíg valamelyik fél összes hajója el nem süllyed. A győztesről a program üzenetet jelenít meg.
Az ellenfél sértetlen hajóinak megjelenítése
A csata során a jobb egérgombbal a gép játékos még sértetlen hajóinak helye is megjeleníthető. Ez a funkció lehetővé teszi, hogy egy elvesztett játék után megtekintsük a még találatot nem kapott ellenséges hajók helyzetét, így könnyen visszanézhető, mely mezőket kellett volna még megtalálni a győzelemhez.
Képernyőképek
Az alábbi képek a játék néhány fontosabb állapotát mutatják be: a hajók elhelyezését, a csata közbeni megjelenítést és a játék végét.





Az alkalmazás szerkezete és működési logikája
A program fejlesztése során az elsődleges cél nem csupán egy működő Torpedó (Battleship) játék elkészítése volt, hanem egy olyan alkalmazás megvalósítása is, amelynek felépítése figyelembe veszi az objektumorientált tervezés alapelveit. Ennek megfelelően a játéklogika, a felhasználói felület és a játékot vezérlő eseménykezelés egymástól elkülönülő komponensekben kapott helyet.
A program felépítése
Az alkalmazás két modulból áll.
A model modul tartalmazza a játék teljes logikai modelljét: a játéktáblákat, a hajókat, a lövések feldolgozását, a gépi játékos működését és a játék állapotának kezelését. Ez a modul teljesen független a grafikus felülettől, ezért más – grafikus vagy konzolos – felhasználói felülettel is alkalmas a játék működtetésére.
A main modul valósítja meg a tkinter alapú grafikus felhasználói felületet. Feladata kizárólag a táblák megjelenítése, az egérkattintások kezelése és a modell által szolgáltatott eredmények megjelenítése. A játékszabályok ismerete kizárólag a modell feladata.
A felépítés a Model–View–Controller (MVC) architektúra egyszerűsített változatának tekinthető. A modell és a nézet felelősségi köre jól elkülönül egymástól, míg a vezérlő szerepét külön Controller osztály helyett a BattleshipGame alkalmazásobjektum látja el. Ez az objektum kezeli a felhasználói eseményeket, továbbítja a kéréseket a modell felé, majd a modell eredményei alapján frissíti a megjelenítést.
A játék modellje
A modell központi eleme a Board osztály, amely egy játékos teljes játéktábláját reprezentálja. Nyilvántartja a rajta elhelyezett hajókat, a korábban leadott lövéseket, valamint biztosítja a hajók elhelyezéséhez és a lövések kiértékeléséhez szükséges műveleteket.
A hajók önálló objektumokként jelennek meg. Egy Ship példány kizárólag az általa elfoglalt cellákat tárolja, ebből azonban minden fontos tulajdonsága meghatározható: mérete, tájolása, a találatok állapota, valamint az is, hogy elsüllyedt-e. A hajó saját maga végzi a rá leadott lövések kiértékelését, valamint biztosítja a saját geometriai tulajdonságainak ellenőrzését. A táblán való elhelyezhetőség vizsgálatában a Board objektummal együttműködve vesz részt
A koordináták kezelésére egy külön Cell adatosztály szolgál. Ennek használata egységesíti a cellák reprezentációját, így a koordináták egyszerű számpárok helyett jól definiált objektumként jelennek meg. Mivel a Cell változtathatatlan (frozen=True), használható halmazok elemeként és szótárkulcsként is, ami a program több algoritmusát jelentősen leegyszerűsíti.
Hajók elhelyezése
A gépi játékos hajóinak elhelyezését a ComputerFleetFactory végzi. Az algoritmus véletlenszerűen generál vízszintes vagy függőleges hajókat, és addig próbálkozik új pozíciókkal, amíg azok szabályosan elhelyezhetők. A hajók sem egymást nem fedhetik át, sem egymás közvetlen szomszédságába nem kerülhetnek.
Az emberi játékos hajóépítése ettől eltérő megközelítést alkalmaz. Minden kattintás után a program előállítja az összes olyan lehetséges hajót, amely
- a megfelelő méretű,
- tartalmazza a már kijelölt cellákat,
- és szabályosan elhelyezhető a táblán.
Ha a kiválasztott cellákból legalább egy ilyen hajó létrehozható, akkor a kijelölés érvényes, ellenkező esetben a kattintást a program figyelmen kívül hagyja. Ez a megoldás egyszerű felhasználói kezelést biztosít, miközben folyamatosan biztosítja a szabályos hajóelhelyezést.
Lövések feldolgozása
A lövések kiértékelése két lépésben történik. A játéktábla objektum először meghatározza, hogy a célcellán található-e hajó. Találat esetén a hajó saját maga dönti el, hogy a lövés egyszerű találatot vagy teljes elsüllyedést eredményezett.
A feldolgozás eredménye a ShotResult felsorolástípus egy tagja lesz, amely egyértelműen megkülönbözteti a három lehetséges állapotot: mellélövés, találat vagy süllyedés.
A gépi játékos stratégiája
A gépi játékos lövési algoritmusa két egymást követő stratégiát alkalmaz. Amíg nem talál hajót, addig véletlenszerűen választ célpontot azok közül a mezők közül, amelyekre még nem lőtt, illetve amelyek biztosan nem tartoznak már elsüllyedt hajóhoz.
Találat után a stratégia megváltozik. Egyetlen sérült hajórész esetén a négy szomszédos mező valamelyikét választja célpontnak. Amint két egymás melletti találat meghatározza a hajó irányát, a keresés már kizárólag a hajó két végpontja felé folytatódik mindaddig, amíg a hajó el nem süllyed.
Tervezési megfontolások összegzése
A fejlesztés során a cél nem csupán a játék működésének megvalósítása volt, hanem egy jól áttekinthető és könnyen karbantartható programstruktúra kialakítása is. Ennek érdekében a különböző felelősségi körök önálló osztályokba kerültek, a koordináták kezelését egy külön adatosztály egységesíti, míg az algoritmusok több helyen a Python beépített adatszerkezeteire, különösen a halmazműveletekre épülnek. Ez a felépítés nemcsak a kód olvashatóságát javítja, hanem lehetővé teszi az egyes komponensek egymástól független fejlesztését és módosítását is.
A program megírásához szükséges ismeretek megtalálhatók a Python tudásépítés lépésről lépésre az alapoktól az első asztali alkalmazásig című e-könyvben.
A teljes forráskód
Az alábbiakban a program forráskódja olvasható. A teljes projekt fájljai a GitHubon is elérhetők, így a játék közvetlenül kipróbálható, illetve a forrásfájlok letölthetők.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 |
# main modul import tkinter as tk from itertools import product import winsound from typing import cast, Callable from model import Cell, GameModel, ShotResult, GamePhase, Winner class BoardView(tk.Frame): """A játéktáblákat megjelenítő osztály, amely az emberi és gépi játékosok tábláinak közös metódusait definiálja.""" def __init__(self, master, board_size: int) -> None: super().__init__(master) self._board_size = board_size if board_size > 10 else 10 # A klasszikus játék 10x10-es táblájánál nem lehet kisebb. self._cell_canvases: dict[Cell, tk.Canvas] = {} # Az egyes cellák és azokat megjelenítő vászonobjektumok tárolása. self._create_cell_grid() def _create_cell_grid(self) -> None: """A játéktáblán létrehozza és megjeleníti a cellákat.""" for row_index, column_index in product(range(self._board_size), repeat=2): grid_cell = Cell(row_index, column_index) cell_canvas = tk.Canvas(self, width='1.2c', height='1.2c', bg="#B3F0FF", bd=1, highlightthickness=0) cell_canvas.grid(row=row_index, column=column_index, padx=1, pady=1) self._cell_canvases[grid_cell] = cell_canvas def bind_cell_event(self, tk_event_descriptor: str, handler: Callable[[tk.Event], None]) -> None: """A cellákhoz rendeli a megadott eseményt és eseménykezelőt.""" for cell_widget in self._cell_canvases.values(): cell_widget.bind(tk_event_descriptor, handler) def update_on_shot(self, affected_cells: set[Cell], shot_result: ShotResult) -> None: """A játéktábla megadott celláinak megjelenését módosítja a lövés eredményének megfelelően. Ha nincs találat, akkor az első argumentum egyelemű, és e cellán egy kis fekete körlapot rajzol. Ha van találat, de még nem süllyed el a hajó, akkor az első argumentum egyelemű, és e cella színe szürkére változik, és egy piros X jel jelenik meg. Ha a lövés hatására a hajó elsüllyed, akkor az első argumentum legalább egyelemű, és e cellák fekete színűek lesznek. """ match shot_result: case ShotResult.MISS: self._update_miss(affected_cells.pop()) case ShotResult.HIT: self._update_hit(affected_cells.pop()) case ShotResult.SUNK: self._update_sunk(affected_cells) def _update_miss(self, cell: Cell) -> None: """A megadott cellán egy kis fekete körlapot rajzol.""" canvas = self._cell_canvases[cell] cnv_width, cnv_height = canvas.winfo_width(), canvas.winfo_height() canvas.create_oval(cnv_width * 0.4, cnv_height * 0.4, cnv_width * 0.6, cnv_height * 0.6, fill="black") def _update_hit(self, cell: Cell) -> None: """A megadott cella színe szürkére változik, és egy piros X jel jelenik meg benne.""" canvas = self._cell_canvases[cell] cnv_width, cnv_height = canvas.winfo_width(), canvas.winfo_height() canvas.config(bg='grey75') canvas.create_line(0, 0, cnv_width, cnv_height, fill="red", width=4) canvas.create_line(0, cnv_height, cnv_width, 0, fill="red", width=4) def _update_sunk(self, cells: set[Cell]) -> None: """A megadott cellák fekete színűek lesznek.""" for cell in cells: canvas = self._cell_canvases[cell] canvas.delete('all') canvas.config(bg='black') class HumanBoardView(BoardView): """Az emberi játékos táblájának megjelenítésére szolgáló panel.""" def mark_as_ship_cell(self, cell: Cell): """A megadott cellát mint a hajó egy részét színezéssel jeleníti meg.""" self._cell_canvases[cell].config(bg="SeaGreen3") @staticmethod def play_ship_placement_sound() -> None: """Új hajó elhelyezésére felhívó hangjelzés.""" winsound.PlaySound("SystemExclamation", winsound.SND_ASYNC) class ComputerBoardView(BoardView): """A gépi játékos táblájának megjelenítésére szolgáló panel.""" def mark_as_revealed_ship_cell(self, cell: Cell) -> None: """A még fel nem fedezett hajóhoz tartozó megadott cella színét módosítja.""" self._cell_canvases[cell].config(bg='yellow') class MessageView(tk.Frame): """A tájékoztató szövegeket megjelenítő panel.""" def __init__(self, master, control_variable: tk.StringVar) -> None: super().__init__(master) message_lbl = tk.Label(self, textvariable=control_variable, bg='white', fg='red', font=('Tahoma', 14, 'bold')) message_lbl.pack(fill='both', expand=True) class BattleshipGame(tk.Tk): """Az alkalmazás vezérlőobjektuma, amely egyben a GUI főablakát is reprezentálja. Feladata a modell és a nézetek létrehozása, az eseménykezelők regisztrálása, valamint a játékfolyamat koordinálása. """ def __init__(self, board_size: int = 10) -> None: super().__init__() self.title("Torpedó játék") self.resizable(False, False) # A játék modell objektumának létrehozása. self._game_model = GameModel(board_size) # A megjelenítő panelek létrehozása és lehelyezése. human_board_title = tk.Label(self, text='Emberi játékos táblája', font=('Tahoma', 12, 'bold')) computer_board_title = tk.Label(self, text='Gépi játékos táblája', font=('Tahoma', 12, 'bold')) human_board_title.grid(row=0, column=0, padx=10, pady=(10, 0), sticky='news') computer_board_title.grid(row=0, column=1, padx=10, pady=(10, 0), sticky='news') self.human_board = HumanBoardView(self, board_size) self.computer_board = ComputerBoardView(self, board_size) self.human_board.grid(row=1, column=0, padx=10, pady=10) self.computer_board.grid(row=1, column=1, padx=10, pady=10) self.message = tk.StringVar(self, '') MessageView(self, self.message).grid(row=2, column=0, columnspan=2, padx=10, pady=(0, 10), sticky='news') # Meghatározzuk, hogy milyen, az emberi játékos által keltett eseményekre reagáljanak a játéktáblák cellái, és # ezek bekövetkezésekor mely metódusokat kell meghívni. self.human_board.bind_cell_event("<Button-1>", self._place_human_ship) self.computer_board.bind_cell_event("<Button-1>", self._process_shot_on_computer_board) self.computer_board.bind_cell_event("<Button-3>", self._reveal_undamaged_computer_ship_cells) # Induláskori üzenet, amely az első hajó lehelyezésére szólít fel. self.message.set(f'Helyezz el egy {self._game_model.get_next_human_ship_size()} méretű hajót.') # ----------------------------------- ESEMÉNYKEZELŐK -------------------------------- def _place_human_ship(self, event: tk.Event) -> None: """A játék hajólehelyezési fázisában az emberi játékos tábláján az eseménnyel érintett celláról eldönti, hogy az hajó része-e. Ha igen, akkor ezt a cella kinézetének módosításával jelzi. Ha a cella a hajó utolsó cellája, vagyis a hajó teljesen elkészült, akkor egy hangjelzés jelzi, hogy egy következő hajót kell lehelyezni, aminek méretét szöveges kiírás mutatja. Ha nincs már több lehelyezenő hajó, akkor a játékot csata fázisba hozza. """ cell_canvas = cast(tk.Canvas, event.widget) clicked_cell: Cell = Cell(cell_canvas.grid_info()['row'], cell_canvas.grid_info()['column']) if self._game_model.game_phase == GamePhase.PLACEMENT: cell_is_ship_body, ship_completed = self._game_model.human_make_ship(clicked_cell) if cell_is_ship_body: # Ha a kiválasztott cella a hajó része, akkor azt valamilyen módon vizuálisan jelezzük. self.human_board.mark_as_ship_cell(clicked_cell) if self._game_model.get_next_human_ship_size(): if ship_completed: # Ha a hajó teljesen elkészült, akkor egy hangjelzést követő kiírással jelezzük, hogy a következő # hajónak mekkora mértetűnek kell lenni. self.human_board.play_ship_placement_sound() self.message.set(f'Helyezz el egy ' f'{self._game_model.get_next_human_ship_size()} méretű hajót.') else: # Ha nincs már több elhelyezésre váró hajó, akkor a játék a csata fázisba lép, amit # egy kiírt üzenettel is jelzünk. self._game_model.game_phase = GamePhase.BATTLE self.message.set('Lőjél cellákra az ellenfél tábláján') def _process_shot_on_computer_board(self, event: tk.Event) -> None: """Az emberi játékos ellenfél táblájára leadott lövésének kiértékelése, ha még nem ért véget a játék. A lövés eredménye megjelenik a gépi játékos tábláján. Ha a lövéssel minden hajó elsüllyed, akkor a játék véget ér. """ cell_canvas = cast(tk.Canvas, event.widget) clicked_cell: Cell = Cell(cell_canvas.grid_info()['row'], cell_canvas.grid_info()['column']) if self._game_model.game_phase == GamePhase.BATTLE: shot_result, affected_cells = self._game_model.human_shoot(clicked_cell) self.computer_board.update_on_shot(affected_cells, shot_result) self._notify_upon_wictory() # Miután az emberi játékos lőtt, következik a gépi játékos lövése. self._computer_shoot() def _reveal_undamaged_computer_ship_cells(self, event: tk.Event) -> None: """Megjeleníti a gépi játékos hajóinak azon celláit, amelyeket még nem ért találat. Hajólehelyezési fázisban nincs hatása. """ if self._game_model.game_phase != GamePhase.PLACEMENT: for cell in self._game_model.get_undamaged_computer_ship_cells(): self.computer_board.mark_as_revealed_ship_cell(cell) # ------------------------------------- SEGÉDMETÓDUSOK ----------------------------- def _computer_shoot(self) -> None: """A gépi játékos lő az emberi játékos táblájának egy mezőjére, ha még nem ért véget a játék. A lövés eredménye megjelenik az emberi játékos tábláján. Ha a lövéssel minden hajó elsüllyed, akkor a játék véget ér. """ if self._game_model.game_phase != GamePhase.VICTORY: shot_result, affected_cells = self._game_model.computer_shoot() self.human_board.update_on_shot(affected_cells, shot_result) self._notify_upon_wictory() def _notify_upon_wictory(self) -> None: """Ha valamelyik játékos nyer, akkor ezt szövegesen kijelzi, és a játékot befejező fázisba hozza.""" if (winner := self._game_model.check_winner()) is not None: text = 'Te nyertél' if winner == Winner.HUMAN else 'A gép nyert' self.message.set(text) self._game_model.game_phase = GamePhase.VICTORY def run(self) -> None: """Elindítja a játékot.""" self.mainloop() if __name__ == '__main__': battleship_game = BattleshipGame() battleship_game.run() |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 |
# model modul from dataclasses import dataclass from enum import StrEnum, auto from itertools import product from random import randint, choice from typing import Iterator @dataclass(frozen=True) class Cell: row: int column: int def __add__(self, t: tuple[int, int]) -> Cell: return Cell(self.row + t[0], self.column + t[1]) class ShotResult(StrEnum): HIT = auto() MISS = auto() SUNK = auto() class GamePhase(StrEnum): PLACEMENT = auto() BATTLE = auto() VICTORY = auto() class Winner(StrEnum): HUMAN = auto() COMPUTER = auto() class InvalidShipShape(ValueError): pass class ShipOverlapError(ValueError): pass class Board: """A játékosok tábláját modellezi.""" def __init__(self, size: int, ship_sizes: tuple[int, ...]) -> None: self.size = size # A tábla sorainak és oszlopainak száma. self.ship_sizes = ship_sizes # A táblán elhelyezendő hajók méretei. self._remaining_ship_sizes = list(self.ship_sizes) # A még fennmaradó elhelyezendő hajóméretek. self._ships: list[Ship] = [] # A táblán elhelyezett hajók. self.shots_received: set[Cell] = set() # A tábla valamely cellájára már leadott lövések coordinátáit tárolja. def __contains__(self, cell: Cell) -> bool: """True értékkel tér vissza, ha a megadott cellát a tábla tartalmazza.""" return 0 <= cell.row < self.size and 0 <= cell.column < self.size def __iter__(self) -> Iterator[Ship]: """Egymás után kiadja a táblán elhelyzetett hajókat.""" yield from self._ships def add_ship(self, ship: Ship): """Eltárolja a megadott hajót amennyiben a hajótest nincs átfedésben más, korábban elhelyezett hajóval. A hajó méretét kiveszi a még elhelyezendő hajóméretek közül.""" if not ship.can_be_placed_on(self): raise ShipOverlapError("The ship overlaps with an already placed ship.") if self._remaining_ship_sizes: self._ships.append(ship) self._remaining_ship_sizes.remove(ship.size) def get_cells(self) -> set[Cell]: return {Cell(row_index, column_index) for row_index, column_index in product(range(self.size), repeat=2)} def get_cells_available_to_target(self) -> set[Cell]: """Azon cellákat adja vissza, amelyek nem azonosak az elsüllyedt hajók effectíven elfoglalt celláival.""" return self.get_cells() - {cell for sunken_ship in self.get_sunken_ships() for cell in sunken_ship.get_occupied_area()} def get_ship(self, cell: Cell) -> Ship | None: """Az adott cellát tartalmazó hajót adja vissza. Ha nincs ilyen hajó, akkor None-t.""" return next((ship for ship in self if cell in ship), None) def get_sunken_ships(self) -> set[Ship]: return {ship for ship in self if ship.is_sunk()} def process_shot(self, target_cell: Cell) -> ShotResult: """Visszaadja a játéktábla adott cellájára irányuló lövés eredményét.""" self.shots_received.add(target_cell) ship = self.get_ship(target_cell) if ship is not None: return ship.evaluate_shot(target_cell) return ShotResult.MISS def all_ships_sunk(self) -> bool: """True értékkel tér vissza, ha a játéktáblán minden hajó elsüllyedt.""" return all(ship.is_sunk() for ship in self) def all_ships_placed(self) -> bool: """True értékkel tér vissza, ha minden hajó le lett helyezve a játéktáblára.""" return not self._remaining_ship_sizes class Ship: """A hajót modellező osztály.""" def __init__(self, cells: set[Cell]): if not self.valid_ship_shape(cells): raise InvalidShipShape('A megadott cellákból nem lehet hajót készíteni.') # A hajót alkotó cellák, amikhez rendelt True érték azt jelzi, hogy az a hajórész találatot kapott. self._cells_with_hit_states: dict[Cell, bool] = {cell: False for cell in cells} def __repr__(self) -> str: return f'{type(self).__name__}({set(self._cells_with_hit_states)})' def __str__(self) -> str: coords = {(c.row, c.column) for c in self._cells_with_hit_states.keys()} return f'{type(self).__name__}({coords})' def __contains__(self, cell: Cell) -> bool: """True értékkel tér vissza, ha a megadott cella a hajó által lefedett.""" return cell in self._cells_with_hit_states @property def size(self) -> int: return len(self._cells_with_hit_states) @property def cells(self) -> set[Cell]: return set(self._cells_with_hit_states.keys()) def get_occupied_area(self) -> set[Cell]: return self._cells_with_hit_states.keys() | self._get_buffer_cells() def has_cells(self, *cells: Cell) -> bool: """True értékkel tér vissza, ha a cellák a hajó által lefedettek.""" return set(cells) <= self._cells_with_hit_states.keys() @staticmethod def valid_ship_shape(cells: set[Cell]) -> bool: """Ellenőrzi, hogy a cellák vizszintes vagy függőleges elrendezésűek-e és hézagmentesen egymást követik.""" if not cells: return False rows = {cell.row for cell in cells} cols = {cell.column for cell in cells} is_horizontal = len(rows) == 1 # A viszintes elrendezés feltétele: minden cella sorindexe azonos is_vertical = len(cols) == 1 # A függőleges elrendezés feltétele: minden cella oszlopindexe azonos if is_horizontal: return max(cols) - min(cols) + 1 == len(cols) # Az oszlopindexek számtani sorozatot alkotnak. if is_vertical: return max(rows) - min(rows) + 1 == len(rows) # # A sorindexek számtani sorozatot alkotnak. return False def _get_buffer_cells(self) -> set[Cell]: """Visszaadja a hajó puffercelláit. Puffercellák azok a cellák a hajó körül, amelyeken nem szabad, hogy egy másik hajó része legyen. """ buffer_cells = set() for cell in self._cells_with_hit_states: for shift in [(-1, -1), (0, -1), (+1, -1), (+1, 0), (+1, +1), (0, +1), (-1, +1), (-1, 0)]: neighbor = cell + shift if neighbor not in self._cells_with_hit_states: buffer_cells.add(neighbor) return buffer_cells def can_be_placed_on(self, board: Board) -> bool: """True értékkel tér vissza, ha a hajó lehelyezhető a táblára, azaz nincs átfedésben más, korábban elhelyezett hajóval. """ return (all(cell in board for cell in self._cells_with_hit_states) and not any(self.is_overlapped(_ship) for _ship in board)) def evaluate_shot(self, target_cell: Cell) -> ShotResult: """Kiértékeli, hogy a hajóra leadott lövés talált-e vagy sem. Ha talált a hajó elsüllyedt-e.""" if target_cell not in self._cells_with_hit_states: return ShotResult.MISS self._cells_with_hit_states[target_cell] = True return ShotResult.SUNK if self.is_sunk() else ShotResult.HIT def cell_is_damaged(self, cell: Cell) -> bool: """True értékkel tér vissza, ha a megadott cellával azonosított hajórész találatot kapott.""" return False if cell not in self._cells_with_hit_states else self._cells_with_hit_states[cell] def is_sunk(self) -> bool: """True értékkel tér vissza, ha a hajó elsüllyedt.""" return all(self._cells_with_hit_states.values()) def is_overlapped(self, ship: Ship) -> bool: """Visszatérési értéke True, ha a self törzsét alkotó egy vagy több cellája megegyezik a ship hajótestet alkotó vagy pufferzóna celláival. Más szóval, ha a self részben vagy egészben érintkezik vagy átfedésben van a ship példánnyal. """ return bool(self._cells_with_hit_states.keys() & ship._cells_with_hit_states.keys() or self._cells_with_hit_states.keys() & ship._get_buffer_cells()) class ComputerFleetFactory: """A gépi játékos hajóinak előállításáért felelős osztály.""" def __init__(self, computer_board: Board): self.board = computer_board def _create_ship_cells(self, ship_size: int) -> set[Cell]: """Véletlenszerűen létrehozza egy vízszintes vagy függőleges helyzetű, adott méretű hajó celláit a játéktábla méretein belül. """ horizontal = choice([True, False]) if horizontal: row_index = randint(0, self.board.size - 1) col_index = randint(0, self.board.size - ship_size) # Balról az első cella oszlopindexe. return {Cell(row_index, col_index + i) for i in range(ship_size)} else: row_index = randint(0, self.board.size - ship_size) # Fentről az első cella sorindexe. col_index = randint(0, self.board.size - 1) return {Cell(row_index + i, col_index) for i in range(ship_size)} def produce_ships(self) -> None: """Létrehozza a teljes flotta hajóit elhelyezve a táblán.""" for ship_size in self.board.ship_sizes: placed = False while not placed: cells = self._create_ship_cells(ship_size) ship = Ship(cells) try: self.board.add_ship(ship) except ShipOverlapError: continue # Ütközés esetén újra próbálkozunk. else: placed = True # Sikeres elhelyezés. class ComputerPlayer: """Gépi játékos, ami a hajókat véletlenszerűen elhelyezi és leadja a lövéseket az ellenfél játéktáblájára.""" def __init__(self, computer_board: Board): self.computer_board = computer_board self._human_ship_damaged_cells: list[Cell] = [] self._cells_already_shot: set[Cell] = set() def place_ships(self): factory = ComputerFleetFactory(self.computer_board) factory.produce_ships() def shoot(self, human_board: Board) -> tuple[ShotResult, set[Cell]]: """Véletlenszerű célpont választás a humán játékos tábláján, ami nem ismétli ugyanazt a cellát. Ha sikerül egy hajót eltalálni, akkor a további lövések az eltalált hajó elsüllyesztéséig tartanak, és csak utána lesz egy új célpont megint véletlenszerű. """ # Ha nincs még eltalált cella, akkor véletlenszerűen választunk célpontot, egyébként pedig az eltalált hajóra # lövünk mindaddig, amíg el nem süllyed. if not self._human_ship_damaged_cells: # Célcella véletlenszerű kiválasztása, ami nem lehet egy már korábbi. target_cell = self._choose_random_target_cell(human_board) else: target_cell = self._choose_target_cell_to_sink(human_board) shot_result = human_board.process_shot(target_cell) if shot_result == ShotResult.HIT: self._human_ship_damaged_cells.append(target_cell) if shot_result == ShotResult.SUNK: self._human_ship_damaged_cells.clear() affected_cells = set() if shot_result == ShotResult.MISS or shot_result == ShotResult.HIT: affected_cells = {target_cell} elif shot_result == ShotResult.SUNK: ship = human_board.get_ship(target_cell) assert ship is not None # Ha a hajó elsüllyedt, akkor a target_cell biztosan egy hajó része. affected_cells = set(ship.cells) return shot_result, affected_cells @staticmethod def _choose_random_target_cell(human_board: Board) -> Cell: """Véletlenszerű célpont választása az emberi játékos tábláján, ami nem ismétli ugyanazt a cellát. A célpont olyan cella sem lesz, amely a már elsüllyedt hajók foglalt területére esik. """ target_cell = choice(list(human_board.get_cells_available_to_target() - human_board.shots_received)) return target_cell def _choose_target_cell_to_sink(self, human_board: Board) -> Cell: """Célpont választása az emberi játékos tábláján úgy, hogy ha eltalált egy hajót, akkor a következő célpontok úgy lesznek meghatározva, hogy a hajó elsüllyedjen. """ potential_target_cells: tuple[Cell, ...] = () if len(self._human_ship_damaged_cells) == 1: # Ha még csak egy céllát ért találat, akkor a következő célpont az oldalakkal szomszédos négy cella # valamelyike lehet. left_cell, right_cell = self._human_ship_damaged_cells[0] + (-1, 0), self._human_ship_damaged_cells[0] + (+1, 0) top_cell, bottom_cell = self._human_ship_damaged_cells[0] + (0, -1), self._human_ship_damaged_cells[0] + (0, +1) potential_target_cells = (left_cell, right_cell, top_cell, bottom_cell) elif len(self._human_ship_damaged_cells) > 1: # Ha egynél több cellát ért találat, akkor a következő célpont a vízszitesen vagy függőlegesen # egymás mellett levő cellák két végponti oldalával szomszédos két cella valamelyike lehet. if len({cell.row for cell in self._human_ship_damaged_cells}) == 1: # Vízszintes cellák esetén a bal és jobb végek melleti cellákra kell lőni. left_cell = min(self._human_ship_damaged_cells, key=lambda c: c.column) + (0, -1) right_cell = max(self._human_ship_damaged_cells, key=lambda c: c.column) + (0, +1) potential_target_cells = (left_cell, right_cell) elif len({cell.column for cell in self._human_ship_damaged_cells}) == 1: # Függőleges cellák esetén a felső és alsó végek melleti cellákra kell lőni. top_cell = min(self._human_ship_damaged_cells, key=lambda c: c.row) + (-1, 0) bottom_cell = max(self._human_ship_damaged_cells, key=lambda c: c.row) + (+1, 0) potential_target_cells = (top_cell, bottom_cell) # A célpontként szóbajöhető cellák a játéktáblán kell, hogy legyenek. selectable_cells = [cell for cell in potential_target_cells if cell in human_board] while True: target_cell = choice(selectable_cells) if target_cell not in human_board.shots_received: return target_cell class HumanFleetFactory: """Az emberi játékos hajóinak előállításáért felelős osztály.""" def __init__(self, human_board: Board): self.board = human_board self.ship_sizes_to_build = list(self.board.ship_sizes) self.current_ship_size_to_build: int = self.ship_sizes_to_build[0] self._selected_human_ship_cells: list[Cell] = [] def assemble_ship(self, cell: Cell) -> tuple[bool, bool]: if cell in self._selected_human_ship_cells: return False, False cell_is_ship_body, ship_completed = False, False if not self.board.all_ships_placed(): self._selected_human_ship_cells.append(cell) self.current_ship_size_to_build = self.ship_sizes_to_build[0] alowed_cells = self._get_possible_human_ship_cells(self.current_ship_size_to_build, *self._selected_human_ship_cells) if any(cell in _cells for _cells in alowed_cells): cell_is_ship_body = True if len(self._selected_human_ship_cells) == self.current_ship_size_to_build: self.board.add_ship(Ship(set(self._selected_human_ship_cells))) ship_completed = True self._selected_human_ship_cells.clear() self.ship_sizes_to_build.pop(0) else: self._selected_human_ship_cells.pop() return cell_is_ship_body, ship_completed def _get_possible_human_ship_cells(self, size: int, *cells: Cell) -> tuple[set[Cell], ...]: """Visszaadja az összes adott méretű lehelyezhető hajó celláit, amelyek tartalmazzák a megadott cellákat.""" r0, c0 = cells[0].row, cells[0].column # Összes lehetséges vízszintesen elhelyezhető hajó, amely tartalmazza a megadottak közül az első cellát. all_hor_possible_ships1 = [ship for i in range(-(size - 1), 1) if (ship := Ship({Cell(r0, c0 + s + i) for s in range(size)})).can_be_placed_on(self.board)] # Összes lehetséges vízszintesen elhelyezhető hajó, amely a többi megadott cellát is tartalmazza. all_hor_possible_ships2 = [ship for ship in all_hor_possible_ships1 if ship.has_cells(*cells)] # Összes lehetséges függőlegesen elhelyezhető hajó, amely tartalmazza a megadottak közül az első cellát. all_ver_possible_ships1 = [ship for i in range(-(size - 1), 1) if (ship := Ship({Cell(r0 + s + i, c0) for s in range(size)})).can_be_placed_on(self.board)] # Összes lehetséges függőlegesen elhelyezhető hajó, amely a többi megadott cellát is tartalmazza. all_ver_possible_ships2 = [ship for ship in all_ver_possible_ships1 if ship.has_cells(*cells)] return tuple(ship.cells for ship in all_hor_possible_ships2 + all_ver_possible_ships2) class GameModel: def __init__(self, board_size=10): self.board_size = board_size if board_size > 10 else 10 # A játéktáblák sorainak és oszlopainak száma. self.ship_sizes = (4, 3, 3, 2, 2, 2, 1, 1, 1, 1) # A lehelyezendő hajók cellaszámban mért méretei. self.current_shipsize_to_build = self.ship_sizes[0] # Az emberi és gépi játékosok tábláinak létrehozása. self.human_board = Board(self.board_size, self.ship_sizes) self.computer_board = Board(self.board_size, self.ship_sizes) # A gépi játékost modellező objektum létrehozása. self.computer_player = ComputerPlayer(self.computer_board) self.game_phase = GamePhase.PLACEMENT # A játék kezdő, azaz hajólehelyezési fázisba állítása. # A gépi játékos elhelyezi a hajóit. self.computer_player.place_ships() # Ahhoz, hogy az emberi játékos hajókat tudjon elhelyezni, létrehozzuk a hajók előállítására képes objektumot. self.human_fleet_factory = HumanFleetFactory(self.human_board) def human_make_ship(self, cell: Cell) -> tuple[bool, bool]: """Megállapítja, hogy az emberi játékos táblján a megadott cella lehet-e az aktuálisan létrehozandó hajó része. Ezt jelzi a visszatérési érték első eleme. A második elem pedig azt, hogy ezzel a hajó el is készül. Ha ez utóbbi igaz, akkor létre is jön a hajó. Ha az emberi játékos minden hajója elkészült, a játékot a csata fázisba lépteti. """ if self.human_board.all_ships_placed(): self.game_phase = GamePhase.BATTLE return False, False return self.human_fleet_factory.assemble_ship(cell) def human_shoot(self, target_cell: Cell) -> tuple[ShotResult, set[Cell]]: """Az emberi játékosnak az ellenfél táblájának egy cellájára leadott lövését értékeli ki. Visszaadja a lövés eredményét és a lövéssel érintett cellát, ha nem talált vagy talált, de nem süllyedt, vagy a hajó összes celláját, ha a lövés következtében a hajó elsüllyedt. """ shot_result = self.computer_board.process_shot(target_cell) affected_cells = set() if shot_result == ShotResult.MISS or shot_result == ShotResult.HIT: affected_cells = {target_cell} elif shot_result == ShotResult.SUNK: ship = self.computer_board.get_ship(target_cell) assert ship is not None # Ha a hajó elsüllyedt, akkor a target_cell biztosan egy hajó része. affected_cells = set(ship.cells) return shot_result, affected_cells def computer_shoot(self) -> tuple[ShotResult, set[Cell]]: """A gépi játékos ellenfél táblájának egy cellájára leadott lövését értékeli ki. Visszaadja a lövés eredményét és a megcélzott cellát. """ return self.computer_player.shoot(self.human_board) def check_winner(self) -> Winner | None: """Ha van nyertes, visszaadja, hogy ki az, egyébként None a vissztérési érték.""" if self.human_board.all_ships_sunk(): return Winner.COMPUTER elif self.computer_board.all_ships_sunk(): return Winner.HUMAN else: return None def get_undamaged_computer_ship_cells(self) -> set[Cell]: """Visszaadja a gépi játékos hajóinak azon celláit, amelyeket még nem ért találat.""" return set(cell for ship in self.computer_board for cell in ship.cells if not ship.cell_is_damaged(cell)) def get_next_human_ship_size(self) -> int | None: """Visszaadja az emberi játékos következő megépítendő és lehelyezendő hajójának méretét. Ha nincs már ilyen hajó, akkor a visszatérési érték None. """ return next(iter(self.human_fleet_factory.ship_sizes_to_build), None) |
GitHub link: https://github.com/pythontudasepites/battleship