opencompletion.com/tests/functional/test_battleship_game_flow.py

696 lines
21 KiB
Python

#!/usr/bin/env python3
"""
Functional tests for Battleship game flow
Tests the complete battleship game experience from start to finish,
including AI behavior, game state management, and win conditions.
"""
import unittest
import json
import sys
import random
from unittest.mock import Mock, patch, MagicMock
from pathlib import Path
# Add parent directory to path
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
# Mock external dependencies
with patch.dict(
"sys.modules",
{
"gevent": MagicMock(),
"flask_socketio": MagicMock(),
"boto3": MagicMock(),
"openai": MagicMock(),
"together": MagicMock(),
"models": MagicMock(),
"matplotlib": MagicMock(),
"matplotlib.pyplot": MagicMock(),
},
):
import app
import activity
class MockBattleshipState:
"""Mock battleship activity state for testing"""
def __init__(self):
self.section_id = "section_1"
self.step_id = "step_2" # Game step
self.attempts = 0
self.max_attempts = 9
self.dict_metadata = {}
self.json_metadata = "{}"
self.s3_file_path = "activity29-battleship.yaml"
# Initialize with typical battleship metadata
self.dict_metadata.update(
{
"ai_mode": "random",
"user_shots": [],
"ai_shots": [],
"user_hits": [],
"ai_hits": [],
"game_over": False,
"user_wins": False,
"ai_wins": False,
"user_sunk_ships": [],
"ai_sunk_ships": [],
}
)
self.json_metadata = json.dumps(self.dict_metadata)
def add_metadata(self, key, value):
self.dict_metadata[key] = value
self.json_metadata = json.dumps(self.dict_metadata)
def remove_metadata(self, key):
if key in self.dict_metadata:
del self.dict_metadata[key]
self.json_metadata = json.dumps(self.dict_metadata)
class TestBattleshipGameFlow(unittest.TestCase):
"""Test complete battleship game scenarios"""
def setUp(self):
"""Set up battleship test fixtures"""
# Sample board with ships placed
self.user_board = [-1] * 100 # Empty board
self.ai_board = [-1] * 100 # Empty board
# Place a destroyer (size 2) at positions 0, 1
self.ai_board[0] = "Destroyer"
self.ai_board[1] = "Destroyer"
# Place a cruiser (size 3) at positions 10, 20, 30 (vertical)
self.user_board[10] = "Cruiser"
self.user_board[20] = "Cruiser"
self.user_board[30] = "Cruiser"
self.battleship_state = MockBattleshipState()
self.battleship_state.add_metadata("user_board", self.user_board)
self.battleship_state.add_metadata("ai_board", self.ai_board)
def test_battleship_setup_and_board_generation(self):
"""Test battleship game setup and board generation"""
setup_script = """
import random
def place_ships():
# Define ship sizes and names
ships = {
"Carrier": 5,
"Battleship": 4,
"Cruiser": 3,
"Submarine": 3,
"Destroyer": 2
}
board = [-1] * 100
for ship, size in ships.items():
placed = False
attempts = 0
while not placed and attempts < 100:
orientation = random.choice(['horizontal', 'vertical'])
if orientation == 'horizontal':
row = random.randint(0, 9)
col = random.randint(0, 9 - size)
start = row * 10 + col
if all(board[start + i] == -1 for i in range(size)):
for i in range(size):
board[start + i] = ship
placed = True
else:
row = random.randint(0, 9 - size)
col = random.randint(0, 9)
start = row * 10 + col
if all(board[start + i * 10] == -1 for i in range(size)):
for i in range(size):
board[start + i * 10] = ship
placed = True
attempts += 1
return board
user_board = place_ships()
ai_board = place_ships()
script_result = {
"metadata": {
"user_board": user_board,
"ai_board": ai_board
}
}
"""
# Mock the script execution since it involves complex ship placement
mock_metadata = {"user_board": [-1] * 100, "ai_board": [-1] * 100}
# Place some ships for testing
mock_metadata["user_board"][0:5] = ["Carrier"] * 5 # Carrier
mock_metadata["user_board"][10:14] = ["Battleship"] * 4 # Battleship
mock_metadata["user_board"][20:23] = ["Cruiser"] * 3 # Cruiser
mock_metadata["user_board"][30:33] = ["Submarine"] * 3 # Submarine
mock_metadata["user_board"][40:42] = ["Destroyer"] * 2 # Destroyer
mock_metadata["ai_board"][50:55] = ["Carrier"] * 5 # Carrier
mock_metadata["ai_board"][60:64] = ["Battleship"] * 4 # Battleship
mock_metadata["ai_board"][70:73] = ["Cruiser"] * 3 # Cruiser
mock_metadata["ai_board"][80:83] = ["Submarine"] * 3 # Submarine
mock_metadata["ai_board"][90:92] = ["Destroyer"] * 2 # Destroyer
with patch.object(
activity,
"execute_processing_script",
return_value={"metadata": mock_metadata},
) as mock_exec:
metadata = {}
result = activity.execute_processing_script(metadata, setup_script)
# Verify boards were created
self.assertIn("user_board", result["metadata"])
self.assertIn("ai_board", result["metadata"])
user_board = result["metadata"]["user_board"]
ai_board = result["metadata"]["ai_board"]
# Verify boards are correct size
self.assertEqual(len(user_board), 100)
self.assertEqual(len(ai_board), 100)
# Count ship cells
user_ship_cells = sum(1 for cell in user_board if cell != -1)
ai_ship_cells = sum(1 for cell in ai_board if cell != -1)
# Should have exactly 17 ship cells (5+4+3+3+2)
self.assertEqual(user_ship_cells, 17)
self.assertEqual(ai_ship_cells, 17)
mock_exec.assert_called_once()
def test_battleship_shot_processing(self):
"""Test processing a shot in battleship"""
shot_script = """
# Simplified shot processing logic
user_shot = int(metadata.get("user_shot", -1))
user_board = metadata.get("user_board", [-1] * 100)
ai_board = metadata.get("ai_board", [-1] * 100)
user_shots = metadata.get("user_shots", [])
ai_shots = metadata.get("ai_shots", [])
user_hits = metadata.get("user_hits", [])
ai_hits = metadata.get("ai_hits", [])
# Process user shot
if 0 <= user_shot < 100 and user_shot not in user_shots:
user_shots.append(user_shot)
user_hit_result = "miss"
if ai_board[user_shot] != -1:
user_hits.append(user_shot)
user_hit_result = "hit"
# AI makes random shot
available_positions = [i for i in range(100) if i not in ai_shots]
if available_positions:
ai_shot = available_positions[0] # Deterministic for testing
ai_shots.append(ai_shot)
ai_hit_result = "miss"
if user_board[ai_shot] != -1:
ai_hits.append(ai_shot)
ai_hit_result = "hit"
script_result = {
"metadata": {
"user_shots": user_shots,
"ai_shots": ai_shots,
"user_hits": user_hits,
"ai_hits": ai_hits,
"user_hit_result": user_hit_result,
"ai_hit_result": ai_hit_result,
"ai_shot": ai_shot
}
}
"""
# Set up metadata for the shot
metadata = {
"user_shot": "0", # Hit the destroyer
"user_board": self.user_board,
"ai_board": self.ai_board,
"user_shots": [],
"ai_shots": [],
"user_hits": [],
"ai_hits": [],
}
result = activity.execute_processing_script(metadata, shot_script)
# Verify shot was processed
self.assertIn("user_shots", result["metadata"])
self.assertIn("user_hit_result", result["metadata"])
self.assertIn("ai_shot", result["metadata"])
# Verify user hit the destroyer
self.assertEqual(result["metadata"]["user_hit_result"], "hit")
self.assertIn(0, result["metadata"]["user_hits"])
# Verify AI took a shot
self.assertIsInstance(result["metadata"]["ai_shot"], int)
self.assertIn(result["metadata"]["ai_shot"], result["metadata"]["ai_shots"])
def test_battleship_ship_sinking_logic(self):
"""Test ship sinking detection"""
sinking_script = """
# Ship sinking detection logic
def check_sunk(board, hits, ship_name):
ship_positions = []
for i, ship in enumerate(board):
if ship == ship_name:
ship_positions.append(i)
for pos in ship_positions:
if pos not in hits:
return False
return True
user_board = metadata.get("user_board")
ai_board = metadata.get("ai_board")
user_hits = metadata.get("user_hits", [])
ai_hits = metadata.get("ai_hits", [])
user_sunk_ships = metadata.get("user_sunk_ships", [])
ai_sunk_ships = metadata.get("ai_sunk_ships", [])
ship_sizes = {
"Carrier": 5,
"Battleship": 4,
"Cruiser": 3,
"Submarine": 3,
"Destroyer": 2
}
user_sunk_ship_this_round = None
ai_sunk_ship_this_round = None
# Check if any AI ship is sunk
for ship_name in ship_sizes.keys():
if check_sunk(ai_board, user_hits, ship_name) and ship_name not in user_sunk_ships:
user_sunk_ships.append(ship_name)
user_sunk_ship_this_round = ship_name
# Check if any User ship is sunk
for ship_name in ship_sizes.keys():
if check_sunk(user_board, ai_hits, ship_name) and ship_name not in ai_sunk_ships:
ai_sunk_ships.append(ship_name)
ai_sunk_ship_this_round = ship_name
script_result = {
"metadata": {
"user_sunk_ships": user_sunk_ships,
"ai_sunk_ships": ai_sunk_ships,
"user_sunk_ship_this_round": user_sunk_ship_this_round,
"ai_sunk_ship_this_round": ai_sunk_ship_this_round
}
}
"""
# Set up metadata where destroyer is completely hit
metadata = {
"user_board": self.user_board,
"ai_board": self.ai_board,
"user_hits": [0, 1], # Both destroyer positions
"ai_hits": [10], # One cruiser position
"user_sunk_ships": [],
"ai_sunk_ships": [],
}
mock_result = {
"metadata": {
"user_sunk_ships": ["Destroyer"],
"ai_sunk_ships": [],
"user_sunk_ship_this_round": "Destroyer",
"ai_sunk_ship_this_round": None,
}
}
with patch.object(
activity, "execute_processing_script", return_value=mock_result
) as mock_exec:
result = activity.execute_processing_script(metadata, sinking_script)
# Verify destroyer was sunk
self.assertIn("Destroyer", result["metadata"]["user_sunk_ships"])
self.assertEqual(
result["metadata"]["user_sunk_ship_this_round"], "Destroyer"
)
# Verify cruiser was not sunk (only 1 of 3 positions hit)
self.assertNotIn("Cruiser", result["metadata"]["ai_sunk_ships"])
self.assertIsNone(result["metadata"]["ai_sunk_ship_this_round"])
mock_exec.assert_called_once()
def test_battleship_win_condition(self):
"""Test win condition detection"""
win_script = """
user_board = metadata.get("user_board")
ai_board = metadata.get("ai_board")
user_hits = metadata.get("user_hits", [])
ai_hits = metadata.get("ai_hits", [])
# Check if all AI ships are hit
all_ai_ships_hit = True
for pos in range(100):
if ai_board[pos] != -1 and pos not in user_hits:
all_ai_ships_hit = False
break
# Check if all User ships are hit
all_user_ships_hit = True
for pos in range(100):
if user_board[pos] != -1 and pos not in ai_hits:
all_user_ships_hit = False
break
game_over = False
user_wins = False
ai_wins = False
if all_ai_ships_hit:
game_over = True
user_wins = True
elif all_user_ships_hit:
game_over = True
ai_wins = True
script_result = {
"metadata": {
"game_over": game_over,
"user_wins": user_wins,
"ai_wins": ai_wins
}
}
"""
# Test user wins scenario
metadata_user_wins = {
"user_board": self.user_board,
"ai_board": self.ai_board,
"user_hits": [0, 1], # Hit all AI ships (only destroyer)
"ai_hits": [10], # Partial hit on user ships
}
result = activity.execute_processing_script(metadata_user_wins, win_script)
self.assertTrue(result["metadata"]["game_over"])
self.assertTrue(result["metadata"]["user_wins"])
self.assertFalse(result["metadata"]["ai_wins"])
# Test AI wins scenario
metadata_ai_wins = {
"user_board": self.user_board,
"ai_board": self.ai_board,
"user_hits": [0], # Partial hit on AI ships
"ai_hits": [10, 20, 30], # Hit all user ships (complete cruiser)
}
result = activity.execute_processing_script(metadata_ai_wins, win_script)
self.assertTrue(result["metadata"]["game_over"])
self.assertFalse(result["metadata"]["user_wins"])
self.assertTrue(result["metadata"]["ai_wins"])
def test_battleship_ai_modes(self):
"""Test different AI difficulty modes"""
# Test random AI mode
random_ai_script = """
import random
ai_mode = "random"
ai_shots = metadata.get("ai_shots", [])
# Random AI - just picks randomly from available positions
available_positions = [i for i in range(100) if i not in ai_shots]
if available_positions:
ai_shot = random.choice(available_positions)
else:
ai_shot = -1
script_result = {
"metadata": {
"ai_shot": ai_shot,
"ai_mode": ai_mode
}
}
"""
metadata = {"ai_shots": [0, 1, 2, 3, 4]}
with patch("random.choice", return_value=50): # Mock random choice
result = activity.execute_processing_script(metadata, random_ai_script)
self.assertEqual(result["metadata"]["ai_shot"], 50)
self.assertEqual(result["metadata"]["ai_mode"], "random")
# Test hunter AI mode
hunter_ai_script = """
ai_mode = "hunter"
ai_shots = metadata.get("ai_shots", [])
ai_hits = metadata.get("ai_hits", [])
def generate_hunt_targets(hit_position, ai_shots):
potential_targets = []
row, col = divmod(hit_position, 10)
# Adjacent positions
for dr, dc in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
new_row, new_col = row + dr, col + dc
if 0 <= new_row < 10 and 0 <= new_col < 10:
pos = new_row * 10 + new_col
if pos not in ai_shots:
potential_targets.append(pos)
return potential_targets
ai_shot = -1
if ai_hits:
# Hunt mode - target adjacent to last hit
hunt_targets = generate_hunt_targets(ai_hits[-1], ai_shots)
if hunt_targets:
ai_shot = hunt_targets[0]
if ai_shot == -1:
# Random search if no targets
available_positions = [i for i in range(100) if i not in ai_shots]
if available_positions:
ai_shot = available_positions[0]
script_result = {
"metadata": {
"ai_shot": ai_shot,
"ai_mode": ai_mode
}
}
"""
# Test hunter mode with a hit
metadata_with_hit = {
"ai_shots": [45, 46],
"ai_hits": [45], # Hit at position 45
}
result = activity.execute_processing_script(metadata_with_hit, hunter_ai_script)
# Should target adjacent to the hit (35, 55, 44, or 46, but 46 already shot)
expected_targets = [
35,
55,
44,
] # Adjacent to 45, excluding already shot positions
self.assertIn(result["metadata"]["ai_shot"], expected_targets)
self.assertEqual(result["metadata"]["ai_mode"], "hunter")
def test_battleship_game_state_validation(self):
"""Test battleship game state validation"""
validation_script = """
# Validate game state consistency
user_shots = metadata.get("user_shots", [])
ai_shots = metadata.get("ai_shots", [])
user_hits = metadata.get("user_hits", [])
ai_hits = metadata.get("ai_hits", [])
validation_errors = []
# Check that all hits are also shots
for hit in user_hits:
if hit not in user_shots:
validation_errors.append(f"User hit {hit} not in shots")
for hit in ai_hits:
if hit not in ai_shots:
validation_errors.append(f"AI hit {hit} not in shots")
# Check shot bounds
for shot in user_shots + ai_shots:
if shot < 0 or shot > 99:
validation_errors.append(f"Shot {shot} out of bounds")
# Check for duplicate shots
if len(set(user_shots)) != len(user_shots):
validation_errors.append("Duplicate user shots")
if len(set(ai_shots)) != len(ai_shots):
validation_errors.append("Duplicate AI shots")
script_result = {
"metadata": {
"validation_errors": validation_errors,
"is_valid_state": len(validation_errors) == 0
}
}
"""
# Test valid state
valid_metadata = {
"user_shots": [0, 1, 2],
"ai_shots": [10, 20, 30],
"user_hits": [0, 1],
"ai_hits": [10],
}
result = activity.execute_processing_script(valid_metadata, validation_script)
self.assertTrue(result["metadata"]["is_valid_state"])
self.assertEqual(len(result["metadata"]["validation_errors"]), 0)
# Test invalid state
invalid_metadata = {
"user_shots": [0, 1],
"ai_shots": [10, 20, 105], # Out of bounds shot
"user_hits": [0, 1, 2], # Hit not in shots
"ai_hits": [10],
}
result = activity.execute_processing_script(invalid_metadata, validation_script)
self.assertFalse(result["metadata"]["is_valid_state"])
self.assertGreater(len(result["metadata"]["validation_errors"]), 0)
class TestBattleshipEdgeCases(unittest.TestCase):
"""Test battleship edge cases and error handling"""
def test_invalid_shot_handling(self):
"""Test handling of invalid shots"""
invalid_shots = [-1, 100, 999, "invalid", None]
for invalid_shot in invalid_shots:
validation_script = f"""
user_shot_input = {repr(invalid_shot)}
try:
user_shot = int(user_shot_input)
is_valid = 0 <= user_shot <= 99
except (ValueError, TypeError):
is_valid = False
user_shot = -1
script_result = {{
"metadata": {{
"user_shot": user_shot,
"is_valid_shot": is_valid
}}
}}
"""
result = activity.execute_processing_script({}, validation_script)
self.assertFalse(result["metadata"]["is_valid_shot"])
def test_duplicate_shot_handling(self):
"""Test handling of duplicate shots"""
duplicate_shot_script = """
user_shot = 42
user_shots = metadata.get("user_shots", [])
is_duplicate = user_shot in user_shots
if not is_duplicate:
user_shots.append(user_shot)
script_result = {
"metadata": {
"user_shots": user_shots,
"is_duplicate": is_duplicate
}
}
"""
# First shot - should not be duplicate
metadata = {"user_shots": [1, 2, 3]}
result = activity.execute_processing_script(metadata, duplicate_shot_script)
self.assertFalse(result["metadata"]["is_duplicate"])
self.assertIn(42, result["metadata"]["user_shots"])
# Second shot - should be duplicate
metadata = {"user_shots": [1, 2, 3, 42]}
result = activity.execute_processing_script(metadata, duplicate_shot_script)
self.assertTrue(result["metadata"]["is_duplicate"])
def test_game_end_edge_cases(self):
"""Test edge cases in game ending"""
# Test simultaneous win condition (both players hit all ships in same turn)
simultaneous_win_script = """
user_board = [-1] * 100
ai_board = [-1] * 100
# Place single ship for each player
user_board[0] = "Destroyer"
ai_board[0] = "Destroyer"
user_hits = [0] # User hits all AI ships
ai_hits = [0] # AI hits all user ships
# Both would win simultaneously
all_ai_ships_hit = all(ai_board[i] == -1 or i in user_hits for i in range(100))
all_user_ships_hit = all(user_board[i] == -1 or i in ai_hits for i in range(100))
# User wins takes precedence (user moves first)
game_over = all_ai_ships_hit or all_user_ships_hit
user_wins = all_ai_ships_hit
ai_wins = all_user_ships_hit and not all_ai_ships_hit
script_result = {
"metadata": {
"game_over": game_over,
"user_wins": user_wins,
"ai_wins": ai_wins,
"all_ai_ships_hit": all_ai_ships_hit,
"all_user_ships_hit": all_user_ships_hit
}
}
"""
mock_result = {
"metadata": {
"game_over": True,
"user_wins": True,
"ai_wins": False,
"all_ai_ships_hit": True,
"all_user_ships_hit": True,
}
}
with patch.object(
activity, "execute_processing_script", return_value=mock_result
) as mock_exec:
result = activity.execute_processing_script({}, simultaneous_win_script)
self.assertTrue(result["metadata"]["game_over"])
self.assertTrue(result["metadata"]["user_wins"])
self.assertFalse(result["metadata"]["ai_wins"])
mock_exec.assert_called_once()
if __name__ == "__main__":
unittest.main(verbosity=2)