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.
Quick Answer: 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.
Input: ([('A1', 6, 4), ('A2', 8, 2)], [('B1', 5, 3), ('B2', 7, 5)])
Expected Output: {'winner': 'Team B wins', 'log': ['Round 1: A1 (Team A) attacks B1 (Team B) for 4 damage (5->1)', 'Round 1: B1 (Team B) counterattacks A1 (Team A) for 3 damage (6->3)', 'Round 2: A1 (Team A) attacks B1 (Team B) for 4 damage (1->-3)', 'Round 2: B1 (Team B) counterattacks A1 (Team A) for 3 damage (3->0) [dying strike]', 'B1 (Team B) dies', 'Team B sends out B2', 'A1 (Team A) dies', 'Team A sends out A2', 'Round 3: A2 (Team A) attacks B2 (Team B) for 2 damage (7->5)', 'Round 3: B2 (Team B) counterattacks A2 (Team A) for 5 damage (8->3)', 'Round 4: A2 (Team A) attacks B2 (Team B) for 2 damage (5->3)', 'Round 4: B2 (Team B) counterattacks A2 (Team A) for 5 damage (3->-2)', 'A2 (Team A) dies', 'Result: Team B wins']}
Explanation: After round 2 both first monsters die and are replaced. B2 then defeats A2 in round 4, leaving Team A with no living monsters.
Input: ([], [('B', 1, 1)])
Expected Output: {'winner': 'Team B wins', 'log': ['Team A has no alive monsters.', 'Result: Team B wins']}
Explanation: Team A starts with no monsters, so it loses immediately.
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").