parent
de60cf8e56
commit
613dd111f0
|
@ -75,4 +75,5 @@ def gamePlay():
|
|||
if winner(board) == 0:
|
||||
print("Draw")
|
||||
|
||||
gamePlay()
|
||||
if __name__ == '__main__':
|
||||
gamePlay()
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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.
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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())
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
@ -24,6 +24,7 @@ GLOBAL_TIMEOUT = (
|
|||
)
|
||||
|
||||
pytest_plugins = ["agbenchmark.utils.dependencies"]
|
||||
collect_ignore = ["challenges"]
|
||||
|
||||
|
||||
def resolve_workspace(workspace: str) -> str:
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue