Back to OpenAI questions
CodingSoftware Engineer

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

python
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

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

python
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

python
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

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

python
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

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

python
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

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

python
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]