Monster Battle System
Role: Software Engineer, Full-stack Software Engineer
Problem Overview
Design and implement a battle simulation system where two teams of monsters fight each other in a turn-based combat. Each team has a list of monsters, and each monster has health points and attack power. Teams alternate attacking until one team is completely eliminated. Your system should output an event log describing each action in the battle.
This is an Object-Oriented Design problem.
Part 1: Basic Battle System
Requirements
Implement a battle system with the following specifications:
-
Monster: Each monster has:
-
A name (string identifier)
-
Health points (HP) - positive integer
-
Attack power - positive integer
-
Team: Each team has:
-
A team name
-
A list of monsters
-
Battle Rules:
-
Teams take turns attacking, starting with Team A
-
On each turn, the first alive monster in the attacking team attacks the first alive monster in the defending team
-
When monster A attacks monster B:
B.health = B.health - A.attack -
The attacker does NOT receive counter-damage
-
A monster is eliminated when its health drops to 0 or below
-
The battle ends when all monsters on one team are eliminated
-
Event Log: Output a log of events including:
-
Each attack (who attacked whom, damage dealt)
-
Monster eliminations
-
Battle outcome (which team won)
Class Specification
class Monster:
def __init__(self, name: str, health: int, attack: int):
"""
Create a monster with given name, health points, and attack power.
"""
pass
def is_alive(self) -> bool:
"""Return True if the monster's health is above 0."""
pass
def take_damage(self, damage: int) -> None:
"""Reduce health by the damage amount."""
pass
class Team:
def __init__(self, name: str, monsters: list[Monster]):
"""Create a team with a name and list of monsters."""
pass
def get_first_alive(self) -> Monster | None:
"""Return the first monster that is still alive, or None if all are eliminated."""
pass
def is_defeated(self) -> bool:
"""Return True if all monsters are eliminated."""
pass
def battle(team_a: Team, team_b: Team) -> list[str]:
"""
Simulate a battle between two teams.
Returns a list of event log strings describing each action.
"""
passExample
# Create monsters for Team A
dragon = Monster("Dragon", health=100, attack=25)
griffin = Monster("Griffin", health=80, attack=20)
team_a = Team("Heroes", [dragon, griffin])
# Create monsters for Team B
goblin = Monster("Goblin", health=30, attack=10)
orc = Monster("Orc", health=50, attack=15)
troll = Monster("Troll", health=70, attack=12)
team_b = Team("Monsters", [goblin, orc, troll])
# Run the battle
event_log = battle(team_a, team_b)
# Expected event log (example format):
# [
# "Battle begins: Heroes vs Monsters",
# "Dragon attacks Goblin for 25 damage. Goblin has 5 HP remaining.",
# "Goblin attacks Dragon for 10 damage. Dragon has 90 HP remaining.",
# "Dragon attacks Goblin for 25 damage. Goblin is eliminated!",
# "Orc attacks Dragon for 15 damage. Dragon has 75 HP remaining.",
# "Dragon attacks Orc for 25 damage. Orc has 25 HP remaining.",
# "Orc attacks Dragon for 15 damage. Dragon has 60 HP remaining.",
# "Dragon attacks Orc for 25 damage. Orc is eliminated!",
# "Troll attacks Dragon for 12 damage. Dragon has 48 HP remaining.",
# "Dragon attacks Troll for 25 damage. Troll has 45 HP remaining.",
# "Troll attacks Dragon for 12 damage. Dragon has 36 HP remaining.",
# "Dragon attacks Troll for 25 damage. Troll has 20 HP remaining.",
# "Troll attacks Dragon for 12 damage. Dragon has 24 HP remaining.",
# "Dragon attacks Troll for 25 damage. Troll is eliminated!",
# "Battle ends: Heroes wins!"
# ]Sample Solution
class Monster:
def __init__(self, name: str, health: int, attack: int):
self.name = name
self.health = health
self.attack = attack
def is_alive(self) -> bool:
return self.health > 0
def take_damage(self, damage: int) -> None:
self.health -= damage
class Team:
def __init__(self, name: str, monsters: list[Monster]):
self.name = name
self.monsters = monsters
def get_first_alive(self) -> Monster | None:
for monster in self.monsters:
if monster.is_alive():
return monster
return None
def is_defeated(self) -> bool:
return all(not monster.is_alive() for monster in self.monsters)
def battle(team_a: Team, team_b: Team) -> list[str]:
events = []
events.append(f"Battle begins: {team_a.name} vs {team_b.name}")
# Teams take turns, starting with team_a
current_attacker = team_a
current_defender = team_b
while not team_a.is_defeated() and not team_b.is_defeated():
attacker = current_attacker.get_first_alive()
defender_monster = current_defender.get_first_alive()
if attacker is None or defender_monster is None:
break
# Perform attack
damage = attacker.attack
defender_monster.take_damage(damage)
# Log the event
if defender_monster.is_alive():
events.append(
f"{attacker.name} attacks {defender_monster.name} for {damage} damage. "
f"{defender_monster.name} has {defender_monster.health} HP remaining."
)
else:
events.append(
f"{attacker.name} attacks {defender_monster.name} for {damage} damage. "
f"{defender_monster.name} is eliminated!"
)
# Switch turns
current_attacker, current_defender = current_defender, current_attacker
# Determine winner
if team_b.is_defeated():
events.append(f"Battle ends: {team_a.name} wins!")
else:
events.append(f"Battle ends: {team_b.name} wins!")
return eventsPart 2: Type Advantages
Requirements
Extend the battle system to support monster types with damage modifiers. Different types interact with each other to deal bonus or reduced damage.
-
Monster Types: Each monster now has a type (e.g., Fire, Water, Grass, Electric)
-
Type Effectiveness:
-
Some types deal double damage (2x) against certain types
-
Some types deal half damage (0.5x) against certain types
-
Neutral matchups deal normal damage (1x)
-
Example Type Chart:
-
Fire -> Grass: 2x damage
-
Fire -> Water: 0.5x damage
-
Water -> Fire: 2x damage
-
Water -> Grass: 0.5x damage
-
Grass -> Water: 2x damage
-
Grass -> Fire: 0.5x damage
-
Electric -> Water: 2x damage
-
All other matchups: 1x damage
-
Attack Order: Monsters still attack in list order (first alive attacks first alive)
Class Updates
from enum import Enum
class MonsterType(Enum):
FIRE = "Fire"
WATER = "Water"
GRASS = "Grass"
ELECTRIC = "Electric"
class Monster:
def __init__(self, name: str, health: int, attack: int, monster_type: MonsterType):
self.name = name
self.health = health
self.attack = attack
self.monster_type = monster_type
def calculate_damage(self, defender: 'Monster') -> int:
"""Calculate damage considering type effectiveness."""
passExample
# Type effectiveness example
fire_dragon = Monster("FireDragon", health=100, attack=20, monster_type=MonsterType.FIRE)
water_serpent = Monster("WaterSerpent", health=80, attack=15, monster_type=MonsterType.WATER)
# FireDragon attacks WaterSerpent: 20 * 0.5 = 10 damage (Fire weak against Water)
# WaterSerpent attacks FireDragon: 15 * 2.0 = 30 damage (Water strong against Fire)Sample Solution
from enum import Enum
class MonsterType(Enum):
FIRE = "Fire"
WATER = "Water"
GRASS = "Grass"
ELECTRIC = "Electric"
# Type effectiveness chart: (attacker_type, defender_type) -> multiplier
TYPE_CHART = {
(MonsterType.FIRE, MonsterType.GRASS): 2.0,
(MonsterType.FIRE, MonsterType.WATER): 0.5,
(MonsterType.WATER, MonsterType.FIRE): 2.0,
(MonsterType.WATER, MonsterType.GRASS): 0.5,
(MonsterType.GRASS, MonsterType.WATER): 2.0,
(MonsterType.GRASS, MonsterType.FIRE): 0.5,
(MonsterType.ELECTRIC, MonsterType.WATER): 2.0,
}
class Monster:
def __init__(self, name: str, health: int, attack: int, monster_type: MonsterType):
self.name = name
self.health = health
self.attack = attack
self.monster_type = monster_type
def is_alive(self) -> bool:
return self.health > 0
def take_damage(self, damage: int) -> None:
self.health -= damage
def calculate_damage(self, defender: 'Monster') -> int:
"""Calculate damage with type effectiveness."""
multiplier = TYPE_CHART.get(
(self.monster_type, defender.monster_type),
1.0 # Default to neutral damage
)
return int(self.attack * multiplier)
class Team:
def __init__(self, name: str, monsters: list[Monster]):
self.name = name
self.monsters = monsters
def get_first_alive(self) -> Monster | None:
for monster in self.monsters:
if monster.is_alive():
return monster
return None
def is_defeated(self) -> bool:
return all(not monster.is_alive() for monster in self.monsters)
def battle(team_a: Team, team_b: Team) -> list[str]:
events = []
events.append(f"Battle begins: {team_a.name} vs {team_b.name}")
current_attacker = team_a
current_defender = team_b
while not team_a.is_defeated() and not team_b.is_defeated():
attacker = current_attacker.get_first_alive()
defender_monster = current_defender.get_first_alive()
if attacker is None or defender_monster is None:
break
# Calculate damage with type effectiveness
damage = attacker.calculate_damage(defender_monster)
defender_monster.take_damage(damage)
# Determine effectiveness message
base_damage = attacker.attack
if damage > base_damage:
effectiveness = " (Super effective!)"
elif damage < base_damage:
effectiveness = " (Not very effective...)"
else:
effectiveness = ""
# Log the event
if defender_monster.is_alive():
events.append(
f"{attacker.name} attacks {defender_monster.name} for {damage} damage{effectiveness}. "
f"{defender_monster.name} has {defender_monster.health} HP remaining."
)
else:
events.append(
f"{attacker.name} attacks {defender_monster.name} for {damage} damage{effectiveness}. "
f"{defender_monster.name} is eliminated!"
)
current_attacker, current_defender = current_defender, current_attacker
if team_b.is_defeated():
events.append(f"Battle ends: {team_a.name} wins!")
else:
events.append(f"Battle ends: {team_b.name} wins!")
return eventsPart 3: Smart Targeting (Maximum Damage)
Requirements
Extend the battle system so that on each turn, instead of using the first alive monster, the attacking team selects the monster that can deal the maximum damage to the first alive monster on the defending team.
-
Attacker Selection: On each turn:
-
Identify the defending team's first alive monster
-
Among all alive monsters on the attacking team, select the one that would deal the most damage to that defender
-
If multiple monsters deal the same maximum damage, choose the one that appears first in the list
-
Defender Selection: The defender is still the first alive monster on the defending team (unchanged from Part 1)
-
Type Effectiveness: Continue using the type chart from Part 2
Example
# Team A has:
# - FireDragon (attack=20, FIRE type)
# - ElectricEel (attack=15, ELECTRIC type)
# Team B's first alive monster is:
# - WaterSerpent (WATER type)
# Damage calculations:
# - FireDragon vs WaterSerpent: 20 * 0.5 = 10 (Fire weak to Water)
# - ElectricEel vs WaterSerpent: 15 * 2.0 = 30 (Electric strong to Water)
# ElectricEel is selected as the attacker (deals 30 damage vs 10)Sample Solution
class Team:
def __init__(self, name: str, monsters: list[Monster]):
self.name = name
self.monsters = monsters
def get_first_alive(self) -> Monster | None:
for monster in self.monsters:
if monster.is_alive():
return monster
return None
def get_best_attacker(self, defender: Monster) -> Monster | None:
"""
Select the alive monster that deals maximum damage to the defender.
If tied, return the one that appears first in the list.
"""
best_attacker = None
best_damage = -1
for monster in self.monsters:
if monster.is_alive():
damage = monster.calculate_damage(defender)
if damage > best_damage:
best_damage = damage
best_attacker = monster
return best_attacker
def is_defeated(self) -> bool:
return all(not monster.is_alive() for monster in self.monsters)
def battle(team_a: Team, team_b: Team) -> list[str]:
events = []
events.append(f"Battle begins: {team_a.name} vs {team_b.name}")
current_attacker_team = team_a
current_defender_team = team_b
while not team_a.is_defeated() and not team_b.is_defeated():
# Get the defender (first alive on defending team)
defender_monster = current_defender_team.get_first_alive()
if defender_monster is None:
break
# Get the best attacker (max damage to defender)
attacker = current_attacker_team.get_best_attacker(defender_monster)
if attacker is None:
break
# Calculate and apply damage
damage = attacker.calculate_damage(defender_monster)
defender_monster.take_damage(damage)
# Determine effectiveness message
base_damage = attacker.attack
if damage > base_damage:
effectiveness = " (Super effective!)"
elif damage < base_damage:
effectiveness = " (Not very effective...)"
else:
effectiveness = ""
# Log the event
if defender_monster.is_alive():
events.append(
f"{attacker.name} attacks {defender_monster.name} for {damage} damage{effectiveness}. "
f"{defender_monster.name} has {defender_monster.health} HP remaining."
)
else:
events.append(
f"{attacker.name} attacks {defender_monster.name} for {damage} damage{effectiveness}. "
f"{defender_monster.name} is eliminated!"
)
# Switch turns
current_attacker_team, current_defender_team = current_defender_team, current_attacker_team
if team_b.is_defeated():
events.append(f"Battle ends: {team_a.name} wins!")
else:
events.append(f"Battle ends: {team_b.name} wins!")
return eventsComplete Example with Smart Targeting
# Create Team A with mixed types
fire_dragon = Monster("FireDragon", health=100, attack=20, monster_type=MonsterType.FIRE)
electric_eel = Monster("ElectricEel", health=60, attack=15, monster_type=MonsterType.ELECTRIC)
grass_golem = Monster("GrassGolem", health=120, attack=18, monster_type=MonsterType.GRASS)
team_a = Team("Elements", [fire_dragon, electric_eel, grass_golem])
# Create Team B with water types
water_serpent = Monster("WaterSerpent", health=80, attack=15, monster_type=MonsterType.WATER)
water_sprite = Monster("WaterSprite", health=50, attack=12, monster_type=MonsterType.WATER)
team_b = Team("Aquatics", [water_serpent, water_sprite])
event_log = battle(team_a, team_b)
# With smart targeting, ElectricEel (30 damage) attacks WaterSerpent instead of
# FireDragon (10 damage), maximizing damage outputKey Design Considerations
Object-Oriented Principles Applied
-
Encapsulation: Monster state (health) is modified only through methods (
take_damage) -
Single Responsibility: Each class has one clear purpose
-
Open/Closed: Type chart can be extended without modifying monster logic
-
Separation of Concerns: Battle logic is separate from entity definitions
Testing Strategy
def test_basic_battle():
m1 = Monster("A", 50, 30, MonsterType.FIRE)
m2 = Monster("B", 100, 10, MonsterType.WATER)
team_a = Team("TeamA", [m1])
team_b = Team("TeamB", [m2])
log = battle(team_a, team_b)
# Fire (30 * 0.5 = 15) attacks Water
# Water (10 * 2.0 = 20) attacks Fire
# Check battle progresses correctly
def test_smart_targeting():
fire = Monster("Fire", 50, 20, MonsterType.FIRE)
elec = Monster("Electric", 50, 15, MonsterType.ELECTRIC)
water = Monster("Water", 100, 10, MonsterType.WATER)
team_a = Team("A", [fire, elec])
team_b = Team("B", [water])
log = battle(team_a, team_b)
# Electric should attack first (15*2=30 > 20*0.5=10)
assert "Electric attacks Water" in log[1]