diff --git a/app/deck.py b/app/deck.py new file mode 100644 index 00000000..ab1b22d7 --- /dev/null +++ b/app/deck.py @@ -0,0 +1,11 @@ +from dataclasses import dataclass + + +@dataclass +class Deck: + row: int + column: int + is_alive: bool = True + + def __repr__(self) -> str: + return f"Deck ({self.row}, {self.column}) alive = {self.is_alive}" diff --git a/app/errors.py b/app/errors.py new file mode 100644 index 00000000..bed476d6 --- /dev/null +++ b/app/errors.py @@ -0,0 +1,20 @@ +class NumberOfShipsError(Exception): + """number of ships should be 10.""" + pass + + +class CountShipsDecks(Exception): + """number of ships with corresponding number of decks + should be 4 for 1 deck, 3 for 2 decks, 2 for 3 decks, + 1 for 4 decks """ + pass + + +class SpaceBetweenShips(Exception): + """there must be space between the ships.""" + pass + + +class FleetError(Exception): + """all errors in Battleship""" + pass diff --git a/app/main.py b/app/main.py index 626f41cf..822c3f5c 100644 --- a/app/main.py +++ b/app/main.py @@ -1,34 +1,154 @@ -class Deck: - def __init__(self, row, column, is_alive=True): - pass +from dataclasses import dataclass, field as fd +from app.ship import Ship +from app.errors import ( + NumberOfShipsError, + CountShipsDecks, + SpaceBetweenShips, + FleetError +) -class Ship: - def __init__(self, start, end, is_drowned=False): - # Create decks and save them to a list `self.decks` - pass - def get_deck(self, row, column): - # Find the corresponding deck in the list - pass +@dataclass +class Battleship: + ships: list + field: dict = fd(default_factory=dict) - def fire(self, row, column): - # Change the `is_alive` status of the deck - # And update the `is_drowned` value if it's needed - pass + def __post_init__(self) -> None: + self.field = {} + self.ships = [ + Ship(*first_last_decks) + for first_last_decks + in self.ships + ] + for ship in self.ships: + for deck in ship.decks: + self.field[deck.row, deck.column] = ship + def __repr__(self) -> str: + text = "Battleship\nShips:\n" + for ship in self.ships: + text += f"{ship}\n" + return text -class Battleship: - def __init__(self, ships): - # Create a dict `self.field`. - # Its keys are tuples - the coordinates of the non-empty cells, - # A value for each cell is a reference to the ship - # which is located in it - pass - - def fire(self, location: tuple): - # This function should check whether the location - # is a key in the `self.field` - # If it is, then it should check if this cell is the last alive - # in the ship or not. - pass + def fire(self, location: tuple) -> str: + stricken_ship = self.field.get(location) + if stricken_ship: + stricken_ship.fire(*location) + if stricken_ship.is_drowned: + return "Sunk!" + else: + return "Hit!" + else: + return "Miss!" + + def print_field(self) -> None: + empty = "~" + alive_deck = u"\u25A1" + drowned = "x" + destroyed_deck = "*" + line = "" + for row in range(0, 11): + for column in range(0, 11): + if (row, column) not in self.field: + line += empty + elif self.field[(row, column)].get_deck(row, column): + + line += alive_deck + else: + if not self.field[(row, column)].is_drowned: + line += destroyed_deck + else: + line += drowned + line += "\n" + print(line) + + def _validate_number_of_ships(self) -> list: + errors = [] + try: + if len(self.ships) != 10: + raise NumberOfShipsError( + f"the number of ships should be 10" + f" but received {len(self.ships)}") + except NumberOfShipsError as e: + errors.append(e) + return errors + + def _validate_matching_decks(self) -> list: + errors = [] + count_1_decks_ship = 0 + count_2_decks_ship = 0 + count_3_decks_ship = 0 + count_4_decks_ship = 0 + + for ship in Ship.fleet: + if ship.count_of_decks == 1: + count_1_decks_ship += 1 + elif ship.count_of_decks == 2: + count_2_decks_ship += 1 + elif ship.count_of_decks == 3: + count_3_decks_ship += 1 + elif ship.count_of_decks == 4: + count_4_decks_ship += 1 + + try: + if count_1_decks_ship != 4: + raise CountShipsDecks( + f"There should be single-deck ships 4," + f" received {count_1_decks_ship}") + except CountShipsDecks as e: + errors.append(e) + try: + if count_2_decks_ship != 3: + raise CountShipsDecks( + f"There should be two-deck ships 3," + f" received {count_2_decks_ship}") + except CountShipsDecks as e: + errors.append(e) + try: + if count_3_decks_ship != 2: + raise CountShipsDecks( + f"There should be three-deck ships 2," + f" received {count_3_decks_ship}") + except CountShipsDecks as e: + errors.append(e) + try: + if count_4_decks_ship != 1: + raise CountShipsDecks( + f"There should be four-deck ships 1," + f" received {count_4_decks_ship}") + except CountShipsDecks as e: + errors.append(e) + return errors + + def _validate_approaching_ships(self) -> list: + errors = [] + for deck, ship in self.field.items(): + for row in range(deck[0] - 1, deck[0] + 2): + for column in range(deck[1] - 1, deck[1] + 2): + try: + if ( + self.field.get((row, column)) + and self.field.get((row, column)) + is not ship + ): + raise SpaceBetweenShips( + f"next to the {ship} deck {deck}" + f" is the deck {(row, column)}" + f" of another {self.field.get((row, column))}") + except SpaceBetweenShips as e: + errors.append(e) + return errors + + def _validate_field(self) -> None: + errors = ( + self._validate_number_of_ships() + + self._validate_matching_decks() + + self._validate_approaching_ships() + ) + + if errors: + text = "" + for error in errors: + text += str(error) + "\n" + raise FleetError(text) diff --git a/app/ship.py b/app/ship.py new file mode 100644 index 00000000..0bbfcebb --- /dev/null +++ b/app/ship.py @@ -0,0 +1,47 @@ +from dataclasses import dataclass, field + +from app.deck import Deck + + +@dataclass +class Ship: + fleet = [] + + start: tuple + end: tuple + alive_decks: int = field(init=False) + count_of_decks: int = field(init=False) + decks: list = field(default_factory=list) + is_drowned: bool = False + + def __post_init__(self) -> None: + if self.start > self.end: + raise ValueError("the coordinate order is incorrect") + if (self.start[0] != self.end[0]) and (self.start[1] != self.end[1]): + raise ValueError( + "The ship must be either vertical or horizontal," + " x or(and) y coordinates must be the same") + list_of_decks = [] + for row in range(self.start[0], self.end[0] + 1): + for column in range(self.start[1], self.end[1] + 1): + list_of_decks.append(Deck(row, column)) + self.decks = list_of_decks + self.alive_decks = len(self.decks) + self.count_of_decks = len(list_of_decks) + self.__class__.fleet.append(self) + + def __repr__(self) -> str: + return f"ship({self.start}, {self.end})" + + def get_deck(self, row: int, column: int) -> bool: + for deck in self.decks: + if deck.row == row and deck.column == column: + return deck.is_alive + + def fire(self, row: int, column: int) -> None: + for deck in self.decks: + if deck.row == row and deck.column == column: + deck.is_alive = False + self.alive_decks -= 1 + if self.alive_decks == 0: + self.is_drowned = True