diff --git a/requirements.txt b/requirements.txt index 3d9fd87..99f1b7b 100755 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,6 @@ nose==1.3.7 pylint==1.7.4 coverage==4.4.2 colorama==0.3.9 -opencensus-ext-azure==1.1.1 \ No newline at end of file +opencensus-ext-azure==1.1.1 +playsound +PyObjC \ No newline at end of file diff --git a/sound/explosion.mp3 b/sound/explosion.mp3 new file mode 100644 index 0000000..f2e4d08 Binary files /dev/null and b/sound/explosion.mp3 differ diff --git a/sound/explosion2.mp3 b/sound/explosion2.mp3 new file mode 100644 index 0000000..0c34dd0 Binary files /dev/null and b/sound/explosion2.mp3 differ diff --git a/sound/fireworks.mp3 b/sound/fireworks.mp3 new file mode 100644 index 0000000..96cba5a Binary files /dev/null and b/sound/fireworks.mp3 differ diff --git a/sound/splash.mp3 b/sound/splash.mp3 new file mode 100644 index 0000000..c53f4f1 Binary files /dev/null and b/sound/splash.mp3 differ diff --git a/sound/splash2.mp3 b/sound/splash2.mp3 new file mode 100644 index 0000000..4f9b09b Binary files /dev/null and b/sound/splash2.mp3 differ diff --git a/tests/test_battleship.py b/tests/test_battleship.py index c155feb..4d9148a 100644 --- a/tests/test_battleship.py +++ b/tests/test_battleship.py @@ -1,10 +1,40 @@ import unittest -from torpydo.battleship import parse_position +from torpydo.battleship import parse_position, is_fleet_down, get_random_position +from torpydo.ship import Color, Letter, Position, Ship +from unittest.mock import patch class TestBattleship(unittest.TestCase): + def setUp(self): + self.ships = [] + self.ships.append(init_ship(Ship("Test", 2, Color.RED), [Position(Letter.A, 1), Position(Letter.A, 2)])) + def test_parse_position_true(self): self.assertTrue(parse_position("A1")) + def test_is_fleet_down(self): + self.assertFalse(is_fleet_down(self.ships)) + self.ships[0].is_sunk = True + self.assertTrue(is_fleet_down(self.ships)) + + @patch("torpydo.battleship.NUMBER_ROWS", 2) + @patch("torpydo.battleship.NUMBER_COL", 2) + def test_random_duplicate_position(self): + for i in range(10): + board = [] + x = [ + get_random_position(board), + get_random_position(board), + get_random_position(board), + get_random_position(board), + ] + y = list(set(x)) + self.assertTrue(len(y) == 4) + +def init_ship(ship: Ship, positions: list): + ship.positions = positions + + return ship + if '__main__' == __name__: unittest.main() diff --git a/tests/test_colours.py b/tests/test_colours.py new file mode 100644 index 0000000..c83f8ec --- /dev/null +++ b/tests/test_colours.py @@ -0,0 +1,8 @@ +from torpydo.battleship import right_colour +from colorama import Fore + +def test_right_colour_hit(): + assert right_colour(True) == Fore.RED + +def test_right_colour_water(): + assert right_colour(False) == Fore.BLUE diff --git a/tests/test_game_controller.py b/tests/test_game_controller.py index 8bda74e..1d68adf 100644 --- a/tests/test_game_controller.py +++ b/tests/test_game_controller.py @@ -10,6 +10,7 @@ def setUp(self): def test_check_is_hit_true(self): self.assertTrue(GameController.check_is_hit(self.ships, Position(Letter.A, 1))) + self.assertTrue(self.ships[0].positions[0].is_shot) def test_check_is_hit_false(self): self.assertFalse(GameController.check_is_hit(self.ships, Position(Letter.B, 1))) diff --git a/tests/test_random.py b/tests/test_random.py new file mode 100644 index 0000000..624a5d0 --- /dev/null +++ b/tests/test_random.py @@ -0,0 +1,44 @@ +from torpydo.battleship import overlaps, set_direction, set_forward_rear +from torpydo.ship import Letter, Position, Ship, Color + +def test_not_overlaps(): + ship = Ship("test",5,Color.RED) + ship.add_position("B1") + assert overlaps( + [ + Position(Letter.A,1), + Position(Letter.A,2) + ], + fleet=[ + ship + ] + ) == False + +def test_overlaps(): + ship = Ship("test",5, Color.RED) + ship.add_position("A2") + assert overlaps( + [ + Position(Letter.A,1), + Position(Letter.A,2) + ], + fleet=[ + ship + ] + ) == True + +def test_set_forward_read(): + assert set_forward_rear(5,3,6) == 0 + assert set_forward_rear(5,1,7) == 1 + assert set_forward_rear(2,3,8) == 1 + assert set_forward_rear(3,5,7) == 1 + assert set_forward_rear(3,5,6) == -1 + +def test_set_direction(): + axis, st_value = set_direction(Position(Letter.A,1)) + assert axis == "horizontal" + assert st_value == 1 + axis, st_value = set_direction(Position(Letter.A,2)) + assert axis == "vertical" + assert st_value == 2 + \ No newline at end of file diff --git a/tests/test_ship.py b/tests/test_ship.py new file mode 100644 index 0000000..864c19a --- /dev/null +++ b/tests/test_ship.py @@ -0,0 +1,23 @@ +import unittest + +from torpydo.ship import Color, Letter, Position, Ship +from torpydo.game_controller import GameController + +class TestBattleship(unittest.TestCase): + def setUp(self): + self.ships = [] + self.ships.append(init_ship(Ship("Test", 2, Color.RED), [Position(Letter.A, 1), Position(Letter.A, 2)])) + + def test_check_sunk(self): + self.assertFalse(self.ships[0].is_sunk) + GameController.check_is_hit(self.ships, Position(Letter.A, 1)) + self.assertFalse(self.ships[0].is_sunk) + GameController.check_is_hit(self.ships, Position(Letter.A, 2)) + self.assertTrue(self.ships[0].is_sunk) + +def init_ship(ship: Ship, positions: list): + ship.positions = positions + + return ship +if '__main__' == __name__: + unittest.main() diff --git a/tests/test_sound.py b/tests/test_sound.py new file mode 100644 index 0000000..67a4d56 --- /dev/null +++ b/tests/test_sound.py @@ -0,0 +1,12 @@ +from unittest import mock +from torpydo.battleship import is_hit_sound + +def test_call_right_sound_fire(): + with mock.patch("torpydo.battleship.playsound") as mf: + is_hit_sound(True) + mf.assert_called_once_with('sound/explosion2.mp3') + +def test_call_right_sound_water(): + with mock.patch("torpydo.battleship.playsound") as mf: + is_hit_sound(False) + mf.assert_called_once_with('sound/splash2.mp3', block=False) \ No newline at end of file diff --git a/torpydo/__init__.py b/torpydo/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/torpydo/battleship.py b/torpydo/battleship.py index 7c9ce95..ee10df9 100755 --- a/torpydo/battleship.py +++ b/torpydo/battleship.py @@ -3,15 +3,22 @@ import colorama import platform -from colorama import Fore, Back, Style +from typing import List, Tuple +from colorama import Fore, Style from torpydo.ship import Color, Letter, Position, Ship from torpydo.game_controller import GameController from torpydo.telemetryclient import TelemetryClient - -print("Starting") +from playsound import playsound +import time myFleet = [] enemyFleet = [] +board = [] +BAD_POSITION_INPUT_MSG = "The position is invalid. Please try enter a letter and a number like: 'A1'" +NUMBER_ROWS = 8 +NUMBER_COL = 8 + +print("Starting") def main(): TelemetryClient.init() @@ -36,13 +43,26 @@ def main(): start_game() +def right_colour(is_hit:bool): + if is_hit: + colour = Fore.RED + else: + colour = Fore.BLUE + return colour + +def start_colouring(colour): + print(colour) + +def end_colouring(): + print(Style.RESET_ALL) + def start_game(): global myFleet, enemyFleet # clear the screen if(platform.system().lower()=="windows"): cmd='cls' else: - cmd='clear' + cmd='clear' os.system(cmd) print(r''' __ @@ -58,103 +78,243 @@ def start_game(): while True: print() + + # Player + start_colouring(Fore.GREEN) print("Player, it's your turn") - position = parse_position(input("Enter coordinates for your shot :")) + print("Coordinates should be written in the following format 'LetterNumber' as in C1, F4") + try: + position = parse_position(check_position_input("Enter coordinates (A-H, 1-8) for your shot :")) + except Exception: + print(BAD_POSITION_INPUT_MSG) + continue + end_colouring() + is_hit = GameController.check_is_hit(enemyFleet, position) - if is_hit: - print(r''' - \ . ./ - \ .:"";'.:.."" / - (M^^.^~~:.'""). - - (/ . . . \ \) - - ((| :. ~ ^ :. .|)) - - (\- | \ / | /) - - -\ \ / /- - \ \ / /''') + add_position_to_board(board, position, True) + is_hit_sound(is_hit) + start_colouring(right_colour(is_hit)) print("Yeah ! Nice hit !" if is_hit else "Miss") TelemetryClient.trackEvent('Player_ShootPosition', {'custom_dimensions': {'Position': str(position), 'IsHit': is_hit}}) - position = get_random_position() + print( r''' + \ . ./ + \ .:"";'.:.."" / + (M^^.^~~:.'""). + - (/ . . . \ \) - + ((| :. ~ ^ :. .|)) + - (\- | \ / | /) - + -\ \ / /- + \ \ / /''') + + end_colouring() + if is_fleet_down(enemyFleet): + playsound("sound/fireworks.mp3") + start_colouring(Fore.MAGENTA) + print("Congratulations! You are the winner \o/") + end_colouring() + break + + print("\n\nComputer is thinking...") + time.sleep(3) + # Computer + position = get_random_position(board) is_hit = GameController.check_is_hit(myFleet, position) + is_hit_sound(is_hit) + add_position_to_board(board, position, True) + start_colouring(right_colour(is_hit)) + print() print(f"Computer shoot in {str(position)} and {'hit your ship!' if is_hit else 'miss'}") TelemetryClient.trackEvent('Computer_ShootPosition', {'custom_dimensions': {'Position': str(position), 'IsHit': is_hit}}) - if is_hit: - print(r''' - \ . ./ - \ .:"";'.:.."" / - (M^^.^~~:.'""). - - (/ . . . \ \) - - ((| :. ~ ^ :. .|)) - - (\- | \ / | /) - - -\ \ / /- - \ \ / /''') + print(r''' + \ . ./ + \ .:"";'.:.."" / + (M^^.^~~:.'""). + - (/ . . . \ \) - + ((| :. ~ ^ :. .|)) + - (\- | \ / | /) - + -\ \ / /- + \ \ / /''') + end_colouring() + if is_fleet_down(myFleet): + start_colouring(Fore.LIGHTRED_EX) + print("Sorry, you lost...") + end_colouring() + break + + print("Thank you for playing!") + +def is_hit_sound(is_hit): + if is_hit: + playsound("sound/explosion2.mp3") + else: + playsound("sound/splash2.mp3", block=False) + +def is_fleet_down(fleet): + return all(ship.is_sunk for ship in fleet) def parse_position(input: str): letter = Letter[input.upper()[:1]] number = int(input[1:]) - position = Position(letter, number) return Position(letter, number) -def get_random_position(): - rows = 8 - lines = 8 +def add_position_to_board(board: list, position: Position, is_shot = None): + if is_shot is True: + position.is_shot = True - letter = Letter(random.randint(1, lines)) - number = random.randint(1, rows) - position = Position(letter, number) + if position not in board: + board.append(position) - return position +def get_random_position(board: list): + rows = NUMBER_ROWS + lines = NUMBER_COL + + while True: + letter = Letter(random.randint(1, lines)) + number = random.randint(1, rows) + position = Position(letter, number) + for pos in board: + if position == pos: + position = pos + break + + add_position_to_board(board, position) + if not position.is_shot: + position.is_shot = True + return position def initialize_game(): - initialize_myFleet() initialize_enemyFleet() + initialize_myFleet() + + def initialize_myFleet(): global myFleet myFleet = GameController.initialize_ships() - print("Please position your fleet (Game board has size from A to H and 1 to 8) :") + quick_and_dirty = False + if quick_and_dirty: + myFleet[0].positions.append(Position(Letter.B, 4)) + myFleet[0].positions.append(Position(Letter.B, 5)) + myFleet[0].positions.append(Position(Letter.B, 6)) + myFleet[0].positions.append(Position(Letter.B, 7)) + myFleet[0].positions.append(Position(Letter.B, 8)) + + myFleet[1].positions.append(Position(Letter.E, 6)) + myFleet[1].positions.append(Position(Letter.E, 7)) + myFleet[1].positions.append(Position(Letter.E, 8)) + myFleet[1].positions.append(Position(Letter.E, 9)) + + myFleet[2].positions.append(Position(Letter.A, 3)) + myFleet[2].positions.append(Position(Letter.B, 3)) + myFleet[2].positions.append(Position(Letter.C, 3)) + + myFleet[3].positions.append(Position(Letter.F, 8)) + myFleet[3].positions.append(Position(Letter.G, 8)) + myFleet[3].positions.append(Position(Letter.H, 8)) + + myFleet[4].positions.append(Position(Letter.C, 5)) + myFleet[4].positions.append(Position(Letter.C, 6)) + + else: + print("Please position your fleet (Game board has size from A to H and 1 to 8) :") + + for ship in myFleet: + print() + print(f"Please enter the positions for the {ship.name} (size: {ship.size})") + + i = 0 + while i < ship.size: + try: + position_input = check_position_input(f"Enter position {i+1} of {ship.size} (i.e A3):") + except Exception: + print(BAD_POSITION_INPUT_MSG) + continue + else: + ship.add_position(position_input) + i += 1 + TelemetryClient.trackEvent('Player_PlaceShipPosition', {'custom_dimensions': {'Position': position_input, 'Ship': ship.name, 'PositionInShip': i}}) - for ship in myFleet: - print() - print(f"Please enter the positions for the {ship.name} (size: {ship.size})") - for i in range(ship.size): - position_input = input(f"Enter position {i+1} of {ship.size} (i.e A3):") - ship.add_position(position_input) - TelemetryClient.trackEvent('Player_PlaceShipPosition', {'custom_dimensions': {'Position': position_input, 'Ship': ship.name, 'PositionInShip': i}}) +def overlaps(positions, fleet:List[Ship]): + for ship in fleet: + for ship_pos in ship.positions: + if ship_pos in positions: + return True + return False + +def set_forward_rear(ship_size, init_value, max_size) -> int: + if init_value + ship_size - 1 > max_size: + if init_value - ship_size + 1 > 0: + return -1 + else: + return 0 + else: + return 1 + + +def set_direction(random_position:Position) -> Tuple[str,int]: + if random_position.row % 2: + axis = "horizontal" + st_value = random_position.column.value + else: + axis = "vertical" + st_value = random_position.row + return axis, st_value + +def place_this_ship(ship:Ship, st_point:Position, enemyFleet:List[Ship]): + # Take a direction: either vertical or horizontal: + positions = [] + positions.append(st_point) + + axis, st_value = set_direction(st_point) + factor = set_forward_rear(ship.size, st_value, 8) + + if factor: + for i in range(1,ship.size): + if axis == "horizontal": + column = Letter(st_point.column.value + (i * factor)) + row = st_point.row + else: + column = st_point.column + row = st_point.row + (i * factor) + positions.append(Position(row=row,column=column)) + else: + return False + + if not overlaps(positions,enemyFleet): + # insert positions into the ship + for p in positions: + ship.positions.append(p) + return True + else: + return False def initialize_enemyFleet(): global enemyFleet + global board enemyFleet = GameController.initialize_ships() - enemyFleet[0].positions.append(Position(Letter.B, 4)) - enemyFleet[0].positions.append(Position(Letter.B, 5)) - enemyFleet[0].positions.append(Position(Letter.B, 6)) - enemyFleet[0].positions.append(Position(Letter.B, 7)) - enemyFleet[0].positions.append(Position(Letter.B, 8)) + for ship in enemyFleet: + ship_strating_point = get_random_position(board) + while not place_this_ship(ship,ship_strating_point, enemyFleet): + ship_strating_point = get_random_position(board) - enemyFleet[1].positions.append(Position(Letter.E, 6)) - enemyFleet[1].positions.append(Position(Letter.E, 7)) - enemyFleet[1].positions.append(Position(Letter.E, 8)) - enemyFleet[1].positions.append(Position(Letter.E, 9)) - enemyFleet[2].positions.append(Position(Letter.A, 3)) - enemyFleet[2].positions.append(Position(Letter.B, 3)) - enemyFleet[2].positions.append(Position(Letter.C, 3)) + #print(enemyFleet) - enemyFleet[3].positions.append(Position(Letter.F, 8)) - enemyFleet[3].positions.append(Position(Letter.G, 8)) - enemyFleet[3].positions.append(Position(Letter.H, 8)) +def check_position_input(msg: str): + string = input(msg) + assert len(string) == 2 and string[0].isalpha() and string[1:].isnumeric() + return string - enemyFleet[4].positions.append(Position(Letter.C, 5)) - enemyFleet[4].positions.append(Position(Letter.C, 6)) if __name__ == '__main__': main() diff --git a/torpydo/game_controller.py b/torpydo/game_controller.py index 70abe12..115981c 100644 --- a/torpydo/game_controller.py +++ b/torpydo/game_controller.py @@ -13,6 +13,8 @@ def check_is_hit(ships: list, shot: Position): for ship in ships: for position in ship.positions: if position == shot: + position.is_shot = True + ship.check_sunk() return True return False @@ -27,7 +29,7 @@ def initialize_ships(): def is_ship_valid(ship: Ship): is_valid = len(ship.positions) == ship.size - + return is_valid def get_random_position(size: int): diff --git a/torpydo/ship.py b/torpydo/ship.py index 7856773..adfce8f 100644 --- a/torpydo/ship.py +++ b/torpydo/ship.py @@ -21,9 +21,13 @@ class Position(object): def __init__(self, column: Letter, row: int): self.column = column self.row = row + self.is_shot = False def __eq__(self, other): - return self.__dict__ == other.__dict__ + return self.column == other.column and self.row == other.row + + def __hash__(self): + return hash(f"{self.column}_{self.row}") def __str__(self): return f"{self.column.name}{self.row}" @@ -36,14 +40,17 @@ def __init__(self, name: str, size: int, color: Color): self.size = size self.color = color self.positions = [] + self.is_sunk = False def add_position(self, input: str): letter = Letter[input.upper()[:1]] number = int(input[1:]) - position = Position(letter, number) self.positions.append(Position(letter, number)) + def check_sunk(self): + self.is_sunk = all(position.is_shot for position in self.positions) + def __str__(self): return f"{self.color.name} {self.name} ({self.size}): {self.positions}"