Skip to content
This repository has been archived by the owner on Jun 9, 2024. It is now read-only.

Commit

Permalink
Add battleship game (#351)
Browse files Browse the repository at this point in the history
Signed-off-by: Merwane Hamadi <[email protected]>
  • Loading branch information
waynehamadi authored Sep 4, 2023
1 parent de60cf8 commit 613dd11
Show file tree
Hide file tree
Showing 18 changed files with 1,152 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,5 @@ def gamePlay():
if winner(board) == 0:
print("Draw")

gamePlay()
if __name__ == '__main__':
gamePlay()
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
from abc import ABC, abstractmethod
from typing import Optional

from pydantic import BaseModel, validator


# Models for the request and response payloads
class ShipPlacement(BaseModel):
ship_type: str
start: dict # {"row": int, "column": str}
direction: str

@validator("start")
def validate_start(cls, start):
row, column = start.get("row"), start.get("column")

if not (1 <= row <= 10):
raise ValueError("Row must be between 1 and 10 inclusive.")

if column not in list("ABCDEFGHIJ"):
raise ValueError("Column must be one of A, B, C, D, E, F, G, H, I, J.")

return start


class Turn(BaseModel):
target: dict # {"row": int, "column": str}


class TurnResponse(BaseModel):
result: str
ship_type: Optional[str] # This would be None if the result is a miss


class GameStatus(BaseModel):
is_game_over: bool
winner: Optional[str]


from typing import List


class Game(BaseModel):
game_id: str
players: List[str]
board: dict # This could represent the state of the game board, you might need to flesh this out further
ships: List[ShipPlacement] # List of ship placements for this game
turns: List[Turn] # List of turns that have been taken


class AbstractBattleship(ABC):
SHIP_LENGTHS = {
"carrier": 5,
"battleship": 4,
"cruiser": 3,
"submarine": 3,
"destroyer": 2,
}

@abstractmethod
def create_ship_placement(self, game_id: str, placement: ShipPlacement) -> None:
"""
Place a ship on the grid.
"""
pass

@abstractmethod
def create_turn(self, game_id: str, turn: Turn) -> TurnResponse:
"""
Players take turns to target a grid cell.
"""
pass

@abstractmethod
def get_game_status(self, game_id: str) -> GameStatus:
"""
Check if the game is over and get the winner if there's one.
"""
pass

@abstractmethod
def get_winner(self, game_id: str) -> str:
"""
Get the winner of the game.
"""
pass

@abstractmethod
def get_game(self) -> Game:
"""
Retrieve the state of the game.
"""
pass

@abstractmethod
def delete_game(self, game_id: str) -> None:
"""
Delete a game given its ID.
"""
pass

@abstractmethod
def create_game(self, game_id: str) -> None:
"""
Create a new game.
"""
pass
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import pytest

from abstract_class import ShipPlacement, Turn

from battleship import Battleship

@pytest.fixture
def battleship_game():
return Battleship()


@pytest.fixture
def initialized_game_id(battleship_game):
# Create a game instance
game_id = battleship_game.create_game()

# Place all the ships using battleship_game's methods
sample_ship_placements = [
ShipPlacement(
ship_type="carrier", start={"row": 1, "column": "A"}, direction="horizontal"
),
ShipPlacement(
ship_type="battleship",
start={"row": 2, "column": "A"},
direction="horizontal",
),
ShipPlacement(
ship_type="cruiser", start={"row": 3, "column": "A"}, direction="horizontal"
),
ShipPlacement(
ship_type="submarine",
start={"row": 4, "column": "A"},
direction="horizontal",
),
ShipPlacement(
ship_type="destroyer",
start={"row": 5, "column": "A"},
direction="horizontal",
),
]

for ship_placement in sample_ship_placements:
# Place ship using battleship_game's methods
battleship_game.create_ship_placement(game_id, ship_placement)

return game_id


@pytest.fixture
def game_over_fixture(battleship_game, initialized_game_id):
# Assuming 10x10 grid, target all possible positions
for row in range(1, 11):
for column in list("ABCDEFGHIJ"):
# Player 1 takes a turn
turn = Turn(target={"row": row, "column": column})
battleship_game.create_turn(initialized_game_id, turn)

# Player 2 takes a turn, targeting the same position as Player 1
battleship_game.create_turn(initialized_game_id, turn)

# At the end of this fixture, the game should be over
return initialized_game_id
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
Specifications for Battleship

Overview: Battleship is a two-player strategy game where each player places their fleet of ships on a grid and tries to sink the opponent's fleet by guessing their locations.
Players take turns calling out a row and column, attempting to name a square containing one of the opponent's ships.

The Grid: Each player's grid is a 10x10 grid, identified by rows (using numbers 1-10) and columns (using letters A-J).

Ships:

Carrier - 5 squares
Battleship - 4 squares
Cruiser - 3 squares
Submarine - 3 squares
Destroyer - 2 squares
Each ship occupies contiguous squares on the grid, arranged either horizontally or vertically.

Setup:

At the start of the game, each player places their fleet on their grid. This setup is hidden from the opponent.
The game begins with Player 1, followed by Player 2, and so on.
Taking Turns:

On a player's turn, they announce a grid square (e.g., "D5").
The opponent announces whether that square is a "hit" (if there's a part of a ship on that square) or "miss" (if the square is empty).
If a player hits a square occupied by a ship, they get another turn to guess. This continues until they make a miss, at which point their turn ends.
If a player hits all the squares occupied by a ship, the opponent must announce the sinking of that specific ship, e.g., "You sank my Battleship!"

Objective: The goal is to sink all of your opponent's ships before they sink yours.

End of the Game: The game ends when one player has sunk all of the opponent's ships. The winner is the player who sinks all the opposing fleet first.
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import pytest
from pydantic import ValidationError

from abstract_class import ShipPlacement, Turn


def test_ship_placement_out_of_bounds(battleship_game):
game_id = battleship_game.create_game()

try:
out_of_bounds_ship = ShipPlacement(
ship_type="battleship",
start={"row": 11, "column": "Z"},
direction="horizontal",
)
except ValidationError: # Use the directly imported ValidationError class
pass
else:
with pytest.raises(ValueError, match="Placement out of bounds"):
battleship_game.create_ship_placement(game_id, out_of_bounds_ship)


def test_no_ship_overlap(battleship_game):
game_id = battleship_game.create_game()
placement1 = ShipPlacement(
ship_type="battleship", start={"row": 1, "column": "A"}, direction="horizontal"
)
battleship_game.create_ship_placement(game_id, placement1)
placement2 = ShipPlacement(
ship_type="cruiser", start={"row": 1, "column": "A"}, direction="horizontal"
)
with pytest.raises(ValueError):
battleship_game.create_ship_placement(game_id, placement2)


def test_cant_hit_before_ships_placed(battleship_game):
game_id = battleship_game.create_game()
placement1 = ShipPlacement(
ship_type="battleship", start={"row": 1, "column": "A"}, direction="horizontal"
)
battleship_game.create_ship_placement(game_id, placement1)
placement2 = ShipPlacement(
ship_type="cruiser", start={"row": 4, "column": "D"}, direction="horizontal"
)
battleship_game.create_ship_placement(game_id, placement2)
turn = Turn(target={"row": 1, "column": "A"})
with pytest.raises(
ValueError, match="All ships must be placed before starting turns"
):
battleship_game.create_turn(game_id, turn)


def test_cant_place_ship_after_all_ships_placed(battleship_game, initialized_game_id):
game = battleship_game.get_game(
initialized_game_id
)
additional_ship = ShipPlacement(
ship_type="carrier", start={"row": 2, "column": "E"}, direction="horizontal"
)

with pytest.raises(
ValueError, match="All ships are already placed. Cannot place more ships."
):
battleship_game.create_ship_placement(initialized_game_id, additional_ship)


def test_ship_placement_invalid_direction(battleship_game):
game_id = battleship_game.create_game()

with pytest.raises(ValueError, match="Invalid ship direction"):
invalid_direction_ship = ShipPlacement(
ship_type="battleship",
start={"row": 1, "column": "A"},
direction="diagonal",
)
battleship_game.create_ship_placement(game_id, invalid_direction_ship)


def test_invalid_ship_type(battleship_game):
game_id = battleship_game.create_game()
invalid_ship = ShipPlacement(
ship_type="spacecraft", start={"row": 1, "column": "A"}, direction="horizontal"
)
with pytest.raises(ValueError, match="Invalid ship type"):
battleship_game.create_ship_placement(game_id, invalid_ship)


def test_ship_placement_extends_beyond_boundaries(battleship_game):
game_id = battleship_game.create_game()

with pytest.raises(ValueError, match="Ship extends beyond board boundaries"):
ship_extending_beyond = ShipPlacement(
ship_type="battleship",
start={"row": 1, "column": "H"},
direction="horizontal",
)
battleship_game.create_ship_placement(game_id, ship_extending_beyond)

with pytest.raises(ValueError, match="Ship extends beyond board boundaries"):
ship_extending_beyond = ShipPlacement(
ship_type="cruiser", start={"row": 9, "column": "A"}, direction="vertical"
)
battleship_game.create_ship_placement(game_id, ship_extending_beyond)
Loading

0 comments on commit 613dd11

Please sign in to comment.