Python Battleship

Python Battleship


Codul complet

import pandas as pd
import random

class BattleshipBoard:
    column_list = ["a", "b", "c", "d", "e"]
    row_list = [1, 2, 3, 4, 5]

    def __init__(self):
        self._board = pd.DataFrame([[ '*' for _ in range(1, 6)] for _ in range(5) ], columns = self.column_list, index = self.row_list)
        self._mask_board = pd.DataFrame([[ '*' for _ in range(1, 6)] for _ in range(5) ], columns = self.column_list, index = self.row_list)
        self.generate_ship(1)
        self.generate_ship(2)
        self.generate_ship(3)

    @property
    def board(self):
        return self._board

    @property
    def mask_board(self):
        return self._mask_board

    def generate_ship(self, size):
        while True:
            column = random.choice(self.column_list)
            start_pos = random.randint(1, len(self.column_list))

            if(start_pos+size-1) > len(self.column_list):
                continue

            ship_position = set(self._board.loc[start_pos:start_pos+size-1, column].values.flatten())

            if (not ship_position.intersection([1])):
                self._board.loc[start_pos:start_pos+size-1, column] = 1
                break

    def shoot(self, row, column):
        if(self._board.loc[int(row), column]) == 1:
            print(f"{column}{row} ------> Boom!!\n\n")
            self._board.loc[int(row), column] = "X"
            self._mask_board.loc[int(row), column] = "X"
        else:
            print(f"{column}{row} ------> Pe langa!!\n\n")
            self._mask_board.loc[int(row), column] = "-"
            self._board.loc[int(row), column] = "-"

    @property 
    def get_remaining_ships(self):
        return self._board.eq(1).sum().sum()

class Player:
    def __init__(self, name):
        self._player = name
        self._board = BattleshipBoard()

    @property
    def player(self):
        return self._player

    @property
    def board(self):
        return self._board.board

    @property
    def mask_board(self):
        return self._board.mask_board

class BattleshipGame:
    bot_shot_history = []
    player_shot_history = []

    def __init__(self, player1):
        self.player1 = player1
        self.player2 = Player("Programatic Bot")

    def is_game_ongoing(self, player1, player2):
        if player1._board.get_remaining_ships == 0:
            return False, player1.player

        if player2._board.get_remaining_ships == 0:
            return False, player2.player            

        return (True,)

    def start_game(self):
        i = 1
        while True:
            print(f"Tabla ta {self.player1.player}\n{self.player1.board}")
            self.handle_player_shot()
            self.handle_bot_shot()
            game_state = self.is_game_ongoing(self.player1, self.player2)
            if game_state[0] == False:
                winner = self.player2.player if game_state[1] == self.player1.player else self.player1.player
                print(f"Jocul s-a terminat... \n A castigat {winner}")
                break
            i += 1
            print(f"Runda {i}")

    def handle_player_shot(self):
        print(f"Tabla lui {self.player2.player}")
        print(self.player2.mask_board)
        while True:
            index = input("\n\nCoordonatele(ex: a1, e5) -> ")
            if f"{index[1]}{index[0]}" not in self.player_shot_history:
                self.player_shot_history.append(f"{index[1]}{index[0]}")
                break
            else:
                print(f"Ai folosit deja coordonatele: {index[1]}{index[0]} Incearca altele")
        self.player2._board.shoot(index[1], index[0])

    def handle_bot_shot(self):
        while True:
            column = random.choice(self.player1._board.column_list)
            row = random.choice(self.player1._board.row_list)
            if f"{column}{row}" not in self.bot_shot_history:
                self.bot_shot_history.append(f"{column}{row}")
                break
        print(f"Programatic Bot a tras: {column}{row}")
        self.player1._board.shoot(row, column)        

player_name = input("Creeaza numele jucatorului tau: ")
player = Player(player_name)

BattleshipGame(player).start_game()


Explicarea codului

Importarea bibliotecilor

import pandas as pd
import random


pandas
  • Pandas este o bibliotecă populară în Python, utilizată pentru manipularea și analiza datelor. În contextul jocului nostru, folosim Pandas pentru a crea și manipula tabele 2D denumite DataFrames, care vor reprezenta tabla de joc.
  • DataFrame funcționează similar cu o matrice sau tabel, având rânduri și coloane. Este ideal pentru gestionarea coordonatelor de pe tabla de joc.
random
  • Biblioteca random este folosită pentru a genera valori aleatorii, esențiale în acest joc, deoarece navele sunt plasate aleatoriu pe tablă și botul trage tot aleatoriu.

Clasa BattleshipBoard

Clasa BattleshipBoard definește tabla de joc și logica de amplasare a navelor. Acesta este punctul central al jocului, întrucât în această clasă se gestionează ambele table de joc (jucător și bot).

class BattleshipBoard:
    column_list = ["a", "b", "c", "d", "e"]
    row_list = [1, 2, 3, 4, 5]


column_list și row_list
  • column_list și row_list definesc dimensiunile și coordonatele tablei de joc.
    • column_list: are 5 coloane etichetate cu litere ('a', 'b', 'c', 'd', 'e').
    • row_list: are 5 rânduri etichetate cu numere (1, 2, 3, 4, 5).
  • Aceste liste sunt utilizate pentru a crea capetele de tabel și pentru a genera coordonatele necesare atât la plasarea navelor, cât și la tragerea loviturilor.

Constructorul clasei
def __init__(self):
    self._board = pd.DataFrame([[ '*' for _ in range(1, 6)] for _ in range(5)], columns=self.column_list, index=self.row_list)
    self._mask_board = pd.DataFrame([[ '*' for _ in range(1, 6)] for _ in range(5)], columns=self.column_list, index=self.row_list)
    self.generate_ship(1)
    self.generate_ship(2)
    self.generate_ship(3)


  • self._board: Inițializează tabla de joc. Aceasta este o DataFrame 5x5, cu * în toate celulele. Asteriscul (*) reprezintă apa.

    • DataFrame este creat cu 5 rânduri și 5 coloane. Fiecare element din matrice este marcat cu *.
    • columns=self.column_list: etichetează coloanele (a-e).
    • index=self.row_list: etichetează rândurile (1-5).
  • self._mask_board: Reprezintă o a doua tablă de joc, utilizată pentru a ascunde locațiile reale ale navelor. Aceasta este folosită în interacțiunile cu jucătorul pentru a afișa doar loviturile reușite (X) și ratările (-), fără a expune locația navelor.

  • generate_ship(): Plasează 3 nave pe tabla de joc. Navele au dimensiuni diferite: 1, 2 și 3. Fiecare navă este amplasată pe verticală pe o coloană aleatorie și la o poziție de început aleatorie.


Metoda generate_ship
def generate_ship(self, size):
    while True:
        column = random.choice(self.column_list)
        start_pos = random.randint(1, len(self.column_list))

        if (start_pos + size - 1) > len(self.column_list):
            continue

        ship_position = set(self._board.loc[start_pos:start_pos + size - 1, column].values.flatten())

        if (not ship_position.intersection([1])):
            self._board.loc[start_pos:start_pos + size - 1, column] = 1
            break


Această metodă plasează o navă de dimensiune size pe tabla de joc. Să vedem cum funcționează detaliat:

  1. Alegerea coloanei și poziției de start:

    • column = random.choice(self.column_list): Selectează aleatoriu o coloană din lista column_list.
    • start_pos = random.randint(1, len(self.column\_list)): Selectează aleatoriu o poziție de start pe coloană. Poziția de start este un număr între 1 și 5 (coordonatele rândurilor).
  2. Verificarea dacă nava se încadrează pe tablă:

    • if (start_pos + size - 1) > len(self.column_list): Dacă nava nu se încadrează pe tablă (de exemplu, dacă o navă de mărime 3 începe la rândul 4, ea ar ieși în afara grilei), se trece la o altă iterație a buclei.
  3. Verificarea suprapunerii cu alte nave:

    • ship_position = set(self._board.loc[start_pos:start_pos + size - 1, column].values.flatten()): Aceasta linie ia toate valorile de pe coloana selectată, în intervalul de rânduri unde nava urmează să fie plasată, și le adaugă într-un set. Dacă pe acele poziții există deja o navă (valori 1), plasarea este anulată și bucla continuă până când găsește o poziție liberă.
  4. Plasarea navei:

    • self._board.loc[start_pos:start_pos + size - 1, column] = 1: Dacă pozițiile selectate sunt libere, nava este plasată pe tablă. 1 reprezintă o parte a navei.


Proprietățile board și mask_board

@property
def board(self):
    return self._board

@property
def mask_board(self):
    return self._mask_board


Aceste două proprietăți sunt metode de acces pentru a obține tabla completă a jocului (board) și tabla mascată (mask_board), care va fi afișată jucătorului.


Metoda shoot

Aceasta metodă procesează loviturile jucătorului sau ale botului pe tabla de joc.

def shoot(self, row, column):
    if(self._board.loc[int(row), column]) == 1:
        print(f"{column}{row} ------> Boom!!\n\n")
        self._board.loc[int(row), column] = "X"
        self._mask_board.loc[int(row), column] = "X"
    else:
        print(f"{column}{row} ------> Pe langa!!\n\n")
        self._mask_board.loc[int(row), column] = "-"
        self._board.loc[int(row), column] = "-"


  1. Primirea coordonatelor: Primește două argumente: rândul (row) și coloana (column) unde jucătorul sau botul trage.

  2. Determinarea loviturii:

    • if(self._board.loc[int(row), column]) == 1: Verifică dacă pe poziția selectată există o parte a navei (1). Dacă există, lovitura este reușită.
  3. Actualizarea tablei:

    • Lovitură reușită: Dacă lovitura este reușită, valoarea de pe self._board și self._mask_board se schimbă în X pentru a marca lovitura.
    • Lovitură ratată: Dacă lovitura este ratată (adică pe poziția selectată era apă), tabla este actualizată cu -.

Proprietatea get_remaining_ships

@property 
def get_remaining_ships(self):
    return self._board.eq(1).sum().sum()


  • get_remaining_ships returnează numărul de părți ale navelor rămase pe tabla jucătorului. Folosește funcția eq(1) pentru a număra toate pozițiile care conțin valoarea 1 (navele), iar sum() însumează valorile pentru a returna numărul total de nave rămase.

Clasa Player

Clasa Player definește un jucător al jocului.

class Player:
    def __init__(self, name):
        self._player = name
        self._board = BattleshipBoard()


  • name: Numele jucătorului.
  • _board: O instanță a clasei BattleshipBoard, reprezentând tabla de joc pentru acest jucător.

Clasa BattleshipGame

Clasa BattleshipGame controlează întreaga logică a jocului, inclusiv gestionarea rundelor, tragerea loviturilor și determinarea câștigătorului.

Metoda is_game_ongoing

Această metodă verifică dacă jocul mai este activ.

def is_game_ongoing(self, player1, player2):
    if player1._board.get_remaining_ships == 0:
        return False, player1.player

    if player2._board.get_remaining_ships == 0:
        return False, player2.player            

    return (True,)


  • Verifică dacă unul dintre jucători nu mai are nave. Dacă toate navele unui jucător au fost distruse, jocul se termină.

Concluzie

Acest tutorial a demonstrat pas cu pas cum să implementăm un joc de Battleship în Python, folosind Pandas pentru manipularea tabelelor și generarea unei grile de joc 5x5. Am început cu construcția clasei BattleshipBoard, care gestionează plasarea navelor și actualizarea tablei de joc. Am explorat detaliat cum navele sunt plasate aleatoriu pe grilă și cum sunt verificate suprapunerile pentru a asigura un joc corect. De asemenea, am discutat despre utilizarea a două tabele: una care reprezintă pozițiile reale ale navelor și una care maschează aceste poziții pentru a oferi o experiență de joc autentică.

Apoi, am trecut la definirea jucătorilor și a interacțiunii dintre aceștia și tabla de joc. Am explicat cum fiecare jucător, inclusiv botul, poate trage în poziții aleatorii, iar loviturile sunt gestionate prin actualizarea tablei și prin feedback-ul vizual oferit jucătorului (prin simbolurile "X" pentru lovituri reușite și "-" pentru cele ratate). Logica jocului a fost gestionată de clasa BattleshipGame, care coordonează alternarea rundelor și verificarea stării jocului pentru a determina când un jucător a câștigat.

Acest proiect nu doar că ilustrează concepte fundamentale de programare în Python, cum ar fi manipularea datelor în structuri bidimensionale și utilizarea funcțiilor aleatorii, dar pune și accent pe organizarea codului și modularizarea logicii unui joc complex. Extensibilitatea codului permite adăugarea de noi funcționalități, cum ar fi nave cu forme diferite, grile mai mari, sau chiar mai mulți jucători.

Cu această implementare de bază, puteți construi un joc mai complex, îmbunătățindu-vă abilitățile de programare și înțelegerea conceptelor de dezvoltare software. Am reușit să creăm un proiect captivant și interactiv, cu potențial de extindere, care demonstrează aplicabilitatea bibliotecilor Python precum Pandas în soluții mai creative, nu doar în analiza datelor.


Trebuie să fii autentificat pentru a accesa editorul de cod și pentru a experimenta codul prezentat în acest tutorial.

Intră în cont