Add battleship game (#351)

Signed-off-by: Merwane Hamadi <merwanehamadi@gmail.com>
pull/5155/head
merwanehamadi 2023-09-04 14:11:56 -07:00 committed by GitHub
parent de60cf8e56
commit 613dd111f0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1152 additions and 22 deletions

View File

@ -75,4 +75,5 @@ def gamePlay():
if winner(board) == 0:
print("Draw")
gamePlay()
if __name__ == '__main__':
gamePlay()

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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)

View File

@ -0,0 +1,149 @@
from abstract_class import ShipPlacement, Turn
def test_turns_and_results(battleship_game, initialized_game_id):
turn = Turn(target={"row": 1, "column": "A"})
response = battleship_game.create_turn(initialized_game_id, turn)
assert response.result in ["hit", "miss"]
if response.result == "hit":
assert response.ship_type == "carrier"
game = battleship_game.get_game(initialized_game_id)
assert turn in game.turns
def test_game_status_and_winner(battleship_game):
game_id = battleship_game.create_game()
status = battleship_game.get_game_status(game_id)
assert isinstance(status.is_game_over, bool)
if status.is_game_over:
winner = battleship_game.get_winner(game_id)
assert winner is not None
def test_delete_game(battleship_game):
game_id = battleship_game.create_game()
battleship_game.delete_game(game_id)
assert battleship_game.get_game(game_id) is None
def test_ship_rotation(battleship_game):
game_id = battleship_game.create_game()
placement_horizontal = ShipPlacement(
ship_type="battleship", start={"row": 1, "column": "B"}, direction="horizontal"
)
battleship_game.create_ship_placement(game_id, placement_horizontal)
placement_vertical = ShipPlacement(
ship_type="cruiser", start={"row": 3, "column": "D"}, direction="vertical"
)
battleship_game.create_ship_placement(game_id, placement_vertical)
game = battleship_game.get_game(game_id)
assert placement_horizontal in game.ships
assert placement_vertical in game.ships
def test_game_state_updates(battleship_game, initialized_game_id):
turn = Turn(target={"row": 3, "column": "A"})
battleship_game.create_turn(initialized_game_id, turn)
game = battleship_game.get_game(initialized_game_id)
target_key = (3, ord("A") - ord("A"))
assert target_key in game.board and game.board[target_key] == "hit"
def test_ship_sinking_feedback(battleship_game, initialized_game_id):
hits = ["A", "B", "C", "D"]
static_moves = [
{"row": 1, "column": "E"},
{"row": 1, "column": "F"},
{"row": 1, "column": "G"},
{"row": 1, "column": "H"},
]
for index, hit in enumerate(hits):
turn = Turn(target={"row": 2, "column": hit})
response = battleship_game.create_turn(initialized_game_id, turn)
assert response.ship_type == "battleship"
static_turn = Turn(target=static_moves[index])
battleship_game.create_turn(initialized_game_id, static_turn)
assert response.result == "sunk"
def test_restart_game(battleship_game):
game_id = battleship_game.create_game()
battleship_game.delete_game(game_id)
game_id = (
battleship_game.create_game()
) # Use the returned game_id after recreating the game
game = battleship_game.get_game(game_id)
assert game is not None
def test_ship_edge_overlapping(battleship_game):
game_id = battleship_game.create_game()
first_ship = ShipPlacement(
ship_type="battleship", start={"row": 1, "column": "A"}, direction="horizontal"
)
battleship_game.create_ship_placement(game_id, first_ship)
next_ship = ShipPlacement(
ship_type="cruiser", start={"row": 1, "column": "E"}, direction="horizontal"
)
battleship_game.create_ship_placement(game_id, next_ship)
game = battleship_game.get_game(game_id)
assert first_ship in game.ships
assert next_ship in game.ships
def test_game_state_after_ship_placement(battleship_game):
game_id = battleship_game.create_game()
ship_placement = ShipPlacement(
ship_type="battleship", start={"row": 1, "column": "A"}, direction="horizontal"
)
battleship_game.create_ship_placement(game_id, ship_placement)
game = battleship_game.get_game(game_id)
assert ship_placement in game.ships
def test_game_state_after_turn(initialized_game_id, battleship_game):
turn = Turn(target={"row": 1, "column": "A"})
response = battleship_game.create_turn(initialized_game_id, turn)
game = battleship_game.get_game(initialized_game_id)
if response.result == "hit":
assert game.board[(1, 0)] == "hit"
else:
assert game.board[1][0] == "miss"
def test_multiple_hits_on_ship(battleship_game, initialized_game_id):
hit_positions = ["A", "B", "C", "D", "E"]
for index, pos in enumerate(hit_positions):
turn = Turn(target={"row": 1, "column": pos})
response = battleship_game.create_turn(initialized_game_id, turn)
if index == len(hit_positions) - 1:
assert response.result == "sunk"
else:
assert response.result == "hit"
def test_game_over_condition(battleship_game, initialized_game_id):
for row in range(1, 11):
for column in list("ABCDEFGHIJ"):
turn = Turn(target={"row": row, "column": column})
battleship_game.create_turn(initialized_game_id, turn)
battleship_game.create_turn(initialized_game_id, turn)
status = battleship_game.get_game_status(initialized_game_id)
assert status.is_game_over

View File

@ -0,0 +1,31 @@
Setup and Start
As a player, I want to start a new game so I can compete against my opponent.
As a player, I want to position my ships on a 10x10 grid so that I can set up my strategy.
As a player, I want to rotate my ships horizontally or vertically so I can choose their orientation.
As a player, I want to be ensured that ships do not overlap when placing them so that the game rules are maintained.
As a player, I want to hide my ship placements from my opponent so that my strategy remains a secret.
Gameplay
As a player, I want to call out a grid square during my turn so I can try to hit my opponent's ships.
As a player, when I successfully hit a ship, I want to take another turn immediately so I can capitalize on my successful guess.
As a player, when it's not my turn, I want to respond if the grid square called by my opponent is a "hit" or "miss" so that the game progresses.
As a player, I want feedback on whether my guess was a "hit" or "miss" so that I can adjust my strategy.
As a player, when my ship is completely hit, I want to inform my opponent which of my ships they have sunk, so they know their progress.
As a player, I want to keep track of my hits and misses so I can strategize my future moves.
Endgame
As a player, I want to be notified when all my ships have been sunk so I know I've lost.
As a player, I want to be notified when I have sunk all my opponent's ships so I know I've won.
As a player, I want to have the option to start a new game after one ends so I can play again.
User Experience
As a player, I want clear visuals of my grid and my opponent's grid (with hits and misses) so I can easily understand the game state.
As a player, I want audible feedback (like a splash or explosion) so that hits and misses are more engaging.
As a player, I want to be able to pause or exit the game if needed so that I can resume or quit as per my convenience.
Not Allowed
As a player, I shouldn't be able to start hitting ships until all the ships are placed

View File

@ -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

View File

@ -0,0 +1,159 @@
from typing import Dict
from abstract_class import (
AbstractBattleship,
Game,
GameStatus,
ShipPlacement,
Turn,
TurnResponse,
)
class Battleship(AbstractBattleship):
def __init__(self):
self.games: Dict[int, Game] = {}
def create_game(self) -> int:
game_id = str(len(self.games))
new_game = Game(
game_id=game_id,
players=[],
board={},
ships=[],
turns=[],
)
self.games[game_id] = new_game
return new_game.game_id
def create_ship_placement(self, game_id: str, placement: ShipPlacement) -> None:
game = self.games.get(game_id)
if not game:
raise ValueError(f"Game with ID {game_id} not found.")
if placement.direction not in ["horizontal", "vertical"]:
raise ValueError("Invalid ship direction")
if self.all_ships_placed(game):
raise ValueError("All ships are already placed. Cannot place more ships.")
ship_length = self.SHIP_LENGTHS.get(placement.ship_type)
if not ship_length:
raise ValueError(f"Invalid ship type {placement.ship_type}")
start_row, start_col = placement.start["row"], ord(
placement.start["column"]
) - ord("A")
if start_row < 1 or start_row > 10 or start_col < 0 or start_col > 9:
raise ValueError("Placement out of bounds")
if placement.direction == "horizontal" and start_col + ship_length > 10:
raise ValueError("Ship extends beyond board boundaries")
elif placement.direction == "vertical" and start_row + ship_length > 10:
raise ValueError("Ship extends beyond board boundaries")
for i in range(ship_length):
if placement.direction == "horizontal":
if game.board.get((start_row, start_col + i)):
raise ValueError("Ship overlaps with another ship!")
elif placement.direction == "vertical":
if game.board.get((start_row + i, start_col)):
raise ValueError("Ship overlaps with another ship!")
for i in range(ship_length):
if placement.direction == "horizontal":
game.board[(start_row, start_col + i)] = placement.ship_type
else:
game.board[(start_row + i, start_col)] = placement.ship_type
game.ships.append(placement)
def create_turn(self, game_id: str, turn: Turn) -> TurnResponse:
game = self.games.get(game_id)
if not game:
raise ValueError(f"Game with ID {game_id} not found.")
if not self.all_ships_placed(game):
raise ValueError("All ships must be placed before starting turns")
target_row, target_col = turn.target["row"], ord(turn.target["column"]) - ord(
"A"
)
hit_ship = game.board.get((target_row, target_col))
game.turns.append(turn)
if hit_ship == "hit":
return TurnResponse(
result="miss", ship_type=None
)
if hit_ship:
ship_placement = next(sp for sp in game.ships if sp.ship_type == hit_ship)
if hit_ship:
ship_placement = next(sp for sp in game.ships if sp.ship_type == hit_ship)
start_row, start_col = ship_placement.start["row"], ord(
ship_placement.start["column"]
) - ord("A")
ship_positions = [
(
start_row + (i if ship_placement.direction == "vertical" else 0),
start_col + (i if ship_placement.direction == "horizontal" else 0),
)
for i in range(self.SHIP_LENGTHS[hit_ship])
]
targeted_positions = {
(t.target["row"], ord(t.target["column"]) - ord("A"))
for t in game.turns
}
game.board[(target_row, target_col)] = "hit"
if set(ship_positions).issubset(targeted_positions):
for pos in ship_positions:
game.board[pos] = "hit"
return TurnResponse(result="sunk", ship_type=hit_ship)
else:
return TurnResponse(result="hit", ship_type=hit_ship)
def get_game_status(self, game_id: str) -> GameStatus:
game = self.games.get(game_id)
if not game:
raise ValueError(f"Game with ID {game_id} not found.")
hits = sum(1 for _, status in game.board.items() if status == "hit")
total_ships_length = sum(
self.SHIP_LENGTHS[ship.ship_type] for ship in game.ships
)
if hits == total_ships_length:
return GameStatus(
is_game_over=True, winner="player"
)
else:
return GameStatus(is_game_over=False, winner=None)
def get_winner(self, game_id: str) -> str:
game_status = self.get_game_status(game_id)
if game_status.is_game_over:
return game_status.winner
else:
return None
def get_game(self, game_id: str) -> Game:
return self.games.get(game_id)
def delete_game(self, game_id: str) -> None:
if game_id in self.games:
del self.games[game_id]
def all_ships_placed(self, game: Game) -> bool:
placed_ship_types = set([placement.ship_type for placement in game.ships])
return placed_ship_types == set(self.SHIP_LENGTHS.keys())

View File

@ -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

View File

@ -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)

View File

@ -0,0 +1,149 @@
from abstract_class import ShipPlacement, Turn
def test_turns_and_results(battleship_game, initialized_game_id):
turn = Turn(target={"row": 1, "column": "A"})
response = battleship_game.create_turn(initialized_game_id, turn)
assert response.result in ["hit", "miss"]
if response.result == "hit":
assert response.ship_type == "carrier"
game = battleship_game.get_game(initialized_game_id)
assert turn in game.turns
def test_game_status_and_winner(battleship_game):
game_id = battleship_game.create_game()
status = battleship_game.get_game_status(game_id)
assert isinstance(status.is_game_over, bool)
if status.is_game_over:
winner = battleship_game.get_winner(game_id)
assert winner is not None
def test_delete_game(battleship_game):
game_id = battleship_game.create_game()
battleship_game.delete_game(game_id)
assert battleship_game.get_game(game_id) is None
def test_ship_rotation(battleship_game):
game_id = battleship_game.create_game()
placement_horizontal = ShipPlacement(
ship_type="battleship", start={"row": 1, "column": "B"}, direction="horizontal"
)
battleship_game.create_ship_placement(game_id, placement_horizontal)
placement_vertical = ShipPlacement(
ship_type="cruiser", start={"row": 3, "column": "D"}, direction="vertical"
)
battleship_game.create_ship_placement(game_id, placement_vertical)
game = battleship_game.get_game(game_id)
assert placement_horizontal in game.ships
assert placement_vertical in game.ships
def test_game_state_updates(battleship_game, initialized_game_id):
turn = Turn(target={"row": 3, "column": "A"})
battleship_game.create_turn(initialized_game_id, turn)
game = battleship_game.get_game(initialized_game_id)
target_key = (3, ord("A") - ord("A"))
assert target_key in game.board and game.board[target_key] == "hit"
def test_ship_sinking_feedback(battleship_game, initialized_game_id):
hits = ["A", "B", "C", "D"]
static_moves = [
{"row": 1, "column": "E"},
{"row": 1, "column": "F"},
{"row": 1, "column": "G"},
{"row": 1, "column": "H"},
]
for index, hit in enumerate(hits):
turn = Turn(target={"row": 2, "column": hit})
response = battleship_game.create_turn(initialized_game_id, turn)
assert response.ship_type == "battleship"
static_turn = Turn(target=static_moves[index])
battleship_game.create_turn(initialized_game_id, static_turn)
assert response.result == "sunk"
def test_restart_game(battleship_game):
game_id = battleship_game.create_game()
battleship_game.delete_game(game_id)
game_id = (
battleship_game.create_game()
) # Use the returned game_id after recreating the game
game = battleship_game.get_game(game_id)
assert game is not None
def test_ship_edge_overlapping(battleship_game):
game_id = battleship_game.create_game()
first_ship = ShipPlacement(
ship_type="battleship", start={"row": 1, "column": "A"}, direction="horizontal"
)
battleship_game.create_ship_placement(game_id, first_ship)
next_ship = ShipPlacement(
ship_type="cruiser", start={"row": 1, "column": "E"}, direction="horizontal"
)
battleship_game.create_ship_placement(game_id, next_ship)
game = battleship_game.get_game(game_id)
assert first_ship in game.ships
assert next_ship in game.ships
def test_game_state_after_ship_placement(battleship_game):
game_id = battleship_game.create_game()
ship_placement = ShipPlacement(
ship_type="battleship", start={"row": 1, "column": "A"}, direction="horizontal"
)
battleship_game.create_ship_placement(game_id, ship_placement)
game = battleship_game.get_game(game_id)
assert ship_placement in game.ships
def test_game_state_after_turn(initialized_game_id, battleship_game):
turn = Turn(target={"row": 1, "column": "A"})
response = battleship_game.create_turn(initialized_game_id, turn)
game = battleship_game.get_game(initialized_game_id)
if response.result == "hit":
assert game.board[(1, 0)] == "hit"
else:
assert game.board[1][0] == "miss"
def test_multiple_hits_on_ship(battleship_game, initialized_game_id):
hit_positions = ["A", "B", "C", "D", "E"]
for index, pos in enumerate(hit_positions):
turn = Turn(target={"row": 1, "column": pos})
response = battleship_game.create_turn(initialized_game_id, turn)
if index == len(hit_positions) - 1:
assert response.result == "sunk"
else:
assert response.result == "hit"
def test_game_over_condition(battleship_game, initialized_game_id):
for row in range(1, 11):
for column in list("ABCDEFGHIJ"):
turn = Turn(target={"row": row, "column": column})
battleship_game.create_turn(initialized_game_id, turn)
battleship_game.create_turn(initialized_game_id, turn)
status = battleship_game.get_game_status(initialized_game_id)
assert status.is_game_over

File diff suppressed because one or more lines are too long

View File

@ -24,6 +24,7 @@ GLOBAL_TIMEOUT = (
)
pytest_plugins = ["agbenchmark.utils.dependencies"]
collect_ignore = ["challenges"]
def resolve_workspace(workspace: str) -> str:

View File

@ -126,6 +126,18 @@ class Challenge(ABC):
else:
with open(file_path, "r") as f:
files_contents.append(f.read())
else:
if ground.eval.type == "pytest":
result = subprocess.run(
[sys.executable, "-m", "pytest"],
cwd=os.path.abspath(workspace),
capture_output=True,
text=True,
)
if "error" in result.stderr or result.returncode != 0:
print(result.stderr)
assert False, result.stderr
files_contents.append(f"Output: {result.stdout}\n")
return files_contents

View File

@ -50,45 +50,78 @@ def get_reports():
for test_name, test_data in report.tests.items():
test_json = {
"agent": agent_name.lower(),
"benchmark_start_time": report.benchmark_start_time
"benchmark_start_time": report.benchmark_start_time,
}
if isinstance(test_data, SuiteTest):
if test_data.category: # this means it's a same task test
if (
test_data.category
): # this means it's a same task test
test_json["challenge"] = test_name
test_json["attempted"] = test_data.tests[list(test_data.tests.keys())[0]].metrics.attempted
test_json["categories"] = ", ".join(test_data.category)
test_json["attempted"] = test_data.tests[
list(test_data.tests.keys())[0]
].metrics.attempted
test_json["categories"] = ", ".join(
test_data.category
)
test_json["task"] = test_data.task
test_json["success"] = test_data.metrics.percentage
test_json["difficulty"] = test_data.metrics.highest_difficulty
test_json["success_%"] = test_data.metrics.percentage
test_json[
"difficulty"
] = test_data.metrics.highest_difficulty
test_json[
"success_%"
] = test_data.metrics.percentage
test_json["run_time"] = test_data.metrics.run_time
test_json["is_regression"] = test_data.tests[list(test_data.tests.keys())[0]].is_regression
else: # separate tasks in 1 suite
for suite_test_name, suite_data in test_data.tests.items():
test_json["is_regression"] = test_data.tests[
list(test_data.tests.keys())[0]
].is_regression
else: # separate tasks in 1 suite
for (
suite_test_name,
suite_data,
) in test_data.tests.items():
test_json["challenge"] = suite_test_name
test_json["attempted"] = suite_data.metrics.attempted
test_json["categories"] = ", ".join(suite_data.category)
test_json[
"attempted"
] = suite_data.metrics.attempted
test_json["categories"] = ", ".join(
suite_data.category
)
test_json["task"] = suite_data.task
test_json["success"] = 100.0 if suite_data.metrics.success else 0
test_json["difficulty"] = suite_data.metrics.difficulty
test_json["success_%"] = suite_data.metrics.success_percent
test_json["run_time"] = suite_data.metrics.run_time
test_json["is_regression"] = suite_data.is_regression
test_json["success"] = (
100.0 if suite_data.metrics.success else 0
)
test_json[
"difficulty"
] = suite_data.metrics.difficulty
test_json[
"success_%"
] = suite_data.metrics.success_percent
test_json[
"run_time"
] = suite_data.metrics.run_time
test_json[
"is_regression"
] = suite_data.is_regression
else:
test_json["challenge"] = test_name
test_json["attempted"] = test_data.metrics.attempted
test_json["categories"] = ", ".join(test_data.category)
test_json["task"] = test_data.task
test_json["success"] = 100.0 if test_data.metrics.success else 0
test_json["success"] = (
100.0 if test_data.metrics.success else 0
)
test_json["difficulty"] = test_data.metrics.difficulty
test_json["success_%"] = test_data.metrics.success_percent
test_json[
"success_%"
] = test_data.metrics.success_percent
test_json["run_time"] = test_data.metrics.run_time
test_json["is_regression"] = test_data.is_regression
report_data.append(test_json)
return pd.DataFrame(report_data)