Simulate turn-based monster team battle
Company: OpenAI
Role: Software Engineer
Category: Coding & Algorithms
Difficulty: medium
Interview Round: Technical Screen
Design an object-oriented model and implement the core battle logic for a turn-based fight between two teams of monsters. The system should simulate the battle and produce a detailed, chronological battle log, as well as the final result.
---
## Game Setup
There are two teams: **Team A** and **Team B**.
Each team has an **ordered list of monsters**.
Each **Monster** has the following attributes:
- `name`: string
- `health`: positive integer (HP)
- `attack`: positive integer (attack power)
The battle continues until at least one team has no living monsters.
---
## Active Monster Rules
For each team, the **active monster** is defined as:
- The first monster in that team's list whose `health > 0`.
- If a team has no monsters with `health > 0`, that team immediately loses (unless this happens to both teams in the same round, see draw rule below).
---
## Round and Attack Rules
The battle proceeds in **rounds**. Each round follows this exact sequence:
1. Determine the active monster for Team A and Team B as described above.
2. **Team A's active monster attacks Team B's active monster.**
- Damage calculation: `defender.health -= attacker.attack`.
- If the defender's health becomes `<= 0`, that monster dies.
3. **If Team B's active monster is still alive after step 2**, it immediately counterattacks:
- Team B's active monster attacks Team A's active monster using the same damage rule: `defender.health -= attacker.attack`.
- If the defender's health becomes `<= 0`, that monster dies.
4. The round ends, and the next round begins with newly determined active monsters based on the updated health values.
**Death and replacement:**
- When a monster's `health <= 0`, it is considered dead and removed from battle.
- On the **next round**, the next monster in that team's list with `health > 0` becomes the new active monster (if any).
---
## Win / Draw Conditions
- **Win:**
- If, after processing the attacks in a round, one team has **no living monsters** and the other team still has at least one living monster, the surviving team wins.
- **Draw:**
- If the last remaining monsters of both teams die **in the same round** (for example, Team A's monster kills Team B's monster, but Team B's monster's counterattack in the same round kills Team A's monster as well), the result is a draw.
---
## Logging Requirements
You must generate a **battle log** in strict chronological order. The log should include at least:
1. **Each attack event**, with:
- Attacker name and team.
- Defender name and team.
- Damage dealt (equal to attacker's `attack`).
- Defender HP change: `HP_before -> HP_after`.
2. **Each death event**, including:
- Which monster died (name, team).
- If a new monster becomes active for that team in the **next round**, log which monster is coming in as the replacement.
3. **Final result**, as one of the strings:
- `"Team A wins"`
- `"Team B wins"`
- `"Draw"`
You may choose the exact log format (e.g., a list of strings or structured records), but it must be sufficient to reconstruct the full sequence of events.
---
## Implementation / OOD Requirements
Use good object-oriented design. Do **not** put all logic into a single class or function. At minimum, design the following classes (you may add more helper classes if needed):
- `Monster`
- Holds `name`, `health`, `attack` and exposes any behavior needed (e.g., takeDamage, isAlive).
- `Team`
- Holds an ordered collection of `Monster` instances.
- Provides methods to get the current active monster and to determine if the team is defeated.
- `BattleEngine`
- Orchestrates the battle rounds between two `Team` instances.
- Runs the main battle loop according to the rules above.
- Generates the chronological battle log.
- Determines the final result.
- `BattleResult`
- Contains at least:
- `winner`: one of `"Team A"`, `"Team B"`, or `"Draw"`.
- `log`: the ordered battle log.
**Task:**
Design and implement these classes and the core battle logic so that, given two teams of monsters as input, you can run the battle with `BattleEngine` and obtain a `BattleResult` containing the winner and the full battle log.
Overview: This question evaluates object-oriented design, stateful simulation, and event-logging competency for modeling turn-based combat mechanics, including active-entity selection, damage resolution, death and replacement, and simultaneous-death draw conditions.
Implement the core battle engine for a **turn-based monster team fight** between **Team A** and **Team B**, and produce a detailed, time-ordered battle log.
## Function
```python
def solution(team_a, team_b):
...
```
- `team_a` and `team_b` are **ordered lists of monsters**. Each monster is a tuple `(name, hp, atk)`:
- `name` — non-empty string
- `hp` — integer health
- `atk` — integer attack power
- Return a **dict** of the form `{'winner': <result string>, 'log': [<line>, ...]}`, where `'log'` is the list of battle-log lines in the exact order they occur, and `'winner'` is the result string (see **Result** below).
## Active monster
- A team's **Active Monster** is the **first monster in its list with HP > 0**. Monsters are used strictly in list order; once a monster dies it is never revisited.
- A team with no living monster has no Active Monster.
## Round flow (fixed order)
The battle proceeds in numbered rounds starting at **Round 1**. In each round, using each team's Active Monster:
1. **Team A's Active Monster attacks Team B's Active Monster.**
2. **Team B's Active Monster counterattacks Team A's Active Monster.**
3. Deaths and replacements are resolved, then the next round begins.
**Damage:** when X attacks Y, `Y.hp -= X.atk`.
## Dying strike (important)
- The **counterattack always happens** if Team B's monster was alive at the start of the round — **even if Team A's attack already reduced it to HP ≤ 0**. This is a **dying strike**.
- Resolution of deaths is deferred: a monster whose HP drops to **≤ 0** during the round is only **removed after both the attack and the counterattack have been logged**.
## Replacement
When the Active Monster of a team dies, it is removed and the **next living monster (if any)** in that team's list becomes Active for the following round.
## Logging (exact text and order)
Use these exact line formats. `before`/`after` are the defender's HP immediately before and after the hit (`after` may be ≤ 0):
- **Attack:**
`Round {n}: {attacker} (Team A) attacks {defender} (Team B) for {atk} damage ({before}->{after})`
- **Counterattack:**
`Round {n}: {attacker} (Team B) counterattacks {defender} (Team A) for {atk} damage ({before}->{after})`
Append ` [dying strike]` to this line when Team A's attack this round already brought Team B's defender to HP ≤ 0.
- **Death:** `{name} (Team X) dies`
- **Replacement:** `Team X sends out {name}` — logged only when a next living monster exists.
- **Result (always the last line):** `Result: Team A wins` / `Result: Team B wins` / `Result: Draw`.
**Within a round, resolve and log Team B's death/replacement first, then Team A's** (matching the A-attacks-then-B-counterattacks order).
## Empty teams at the start
Before Round 1, if a team has no living monster, log it and end immediately:
- Both teams empty → log `Team A has no alive monsters.`, then `Team B has no alive monsters.`, then `Result: Draw`.
- Only Team A empty → log `Team A has no alive monsters.`, then `Result: Team B wins`.
- Only Team B empty → log `Team B has no alive monsters.`, then `Result: Team A wins`.
(These `... has no alive monsters.` lines are only emitted for an already-empty team at the start of the battle, not for teams that are wiped out mid-battle.)
## Result
After each round's deaths are resolved, check end conditions:
- **Team A wins** if Team B has no living monster left.
- **Team B wins** if Team A has no living monster left.
- **Draw** if **both** teams have no living monster after the same round.
The `'winner'` value in the returned dict is the result phrase: `"Team A wins"`, `"Team B wins"`, or `"Draw"`.
## Design requirement
Keep the engine reasonably object-oriented: define **Monster**, **Team**, and **BattleEngine** (they may be nested inside `solution`). Return the result as a dict containing the winner and the full log.
## Constraints
- `0 <= len(team_a), len(team_b) <= 50`
- Monster `name` is a non-empty string.
- `1 <= hp <= 10^4`
- `1 <= atk <= 10^4`
- The total number of rounds (and thus the log) is small enough to materialize in memory for the given tests.
Constraints
- 0 <= len(team_a), len(team_b) <= 50
- Monster name is non-empty string
- 1 <= health <= 10^4
- 1 <= attack <= 10^4
- Total number of rounds (and thus log size) will be small enough to fit in memory for the given tests (this problem requires generating the full log).
Examples
Input: ([('A1', 10, 4)], [('B1', 9, 3)])
Expected Output: {'winner': 'Team A wins', 'log': ['Round 1: A1 (Team A) attacks B1 (Team B) for 4 damage (9->5)', 'Round 1: B1 (Team B) counterattacks A1 (Team A) for 3 damage (10->7)', 'Round 2: A1 (Team A) attacks B1 (Team B) for 4 damage (5->1)', 'Round 2: B1 (Team B) counterattacks A1 (Team A) for 3 damage (7->4)', 'Round 3: A1 (Team A) attacks B1 (Team B) for 4 damage (1->-3)', 'Round 3: B1 (Team B) counterattacks A1 (Team A) for 3 damage (4->1) [dying strike]', 'B1 (Team B) dies', 'Result: Team A wins']}
Explanation: B1 can still counterattack as a dying strike in round 3, but then dies. Team B has no monsters left, so Team A wins.
Input: ([('A', 5, 5)], [('B', 5, 5)])
Expected Output: {'winner': 'Draw', 'log': ['Round 1: A (Team A) attacks B (Team B) for 5 damage (5->0)', 'Round 1: B (Team B) counterattacks A (Team A) for 5 damage (5->0) [dying strike]', 'B (Team B) dies', 'A (Team A) dies', 'Result: Draw']}
Explanation: Both last monsters reach HP <= 0 within the same round (B’s counterattack is a dying strike), so the result is Draw.
Hints
- Keep a pointer (index) to each team’s current active monster and only move it forward when a monster dies to avoid rescanning from the beginning every round.
- To handle draws correctly, decide precisely when a monster is considered dead versus when it can still perform its counterattack ("dying strike").
Community answers
Answer by EL
Here is the fully completed, cohesive code. It incorporates the O(1) cached property optimization for the Team class and features a highly readable, clean BattleEngine implementation.
from typing import List, Optional
class Monster:
def init(self, name: str, health: int, attack: int):
self.name = name
self.health = health
self.attack = attack
def take_damage(self, damage: int) -> None:
self.health -= damage
def is_alive(self) -> bool:
return self.health > 0
class Team:
def init(self, name: str, monsters: List[Monster]):
self.name = name
self.monsters = monsters
self._active_index = 0
self._active_monster = monsters[0] if monsters else None
@property
def active_monster(self) -> Optional[Monster]:
"""Returns the cached active monster. Updates the cache lazily if the current one dies."""
while self._active_monster and not self._active_monster.is_alive():
self._active_index += 1
self._active_monster = (
self.monsters[self._active_index]
if self._active_index < len(self.monsters)
else None
)
return self._active_monster
@property
def is_defeated(self) -> bool:
return self.active_monster is None
class BattleResult:
def init(self, winner: str, log: List[str]):
self.winner = winner
self.log = log
class BattleEngine:
def init(self, team_a: Team, team_b: Team):
self.team_a = team_a
self.team_b = team_b
self.log: List[str] =[]
def run(self) -> BattleResult:
# The battle loops until at least one team's is_defeated property evaluates to True
while not self.team_a.is_defeated and not self.t