Back

Monster Battle System

Low-Level DesignCodingOnsitePhoneMachine Learning EngineerSoftware EngineerReported Apr, 2026Medium Frequency

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.
    """
    pass

Example

# 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 events

Part 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."""

pass

Example

# 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 events

Part 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 events

Complete 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 output

Key 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 (152=30 > 200.5=10)

assert "Electric attacks Water" in log[1]


Auto-save enabled
Loading editor…
Output
Run your code to see the output here.