Quick 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.

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

  1. 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.
  2. 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

Loading coding console...

Show the approach

Approach

This is a deterministic turn-based simulation built from three small classes, exactly as the OOD requirement asks.

Data model. Monster holds name/hp/atk with an alive property (hp > 0). Team wraps the ordered roster plus an idx pointer to the current fighter. Its active() method lazily advances idx past any dead monsters and returns the first living one (or None), so the "first monster with HP > 0" rule is encoded as a forward-only pointer — no rescanning from the front each round.

Round loop. BattleEngine.run() first handles the immediate empty-team cases (both empty → Draw; one empty → the other wins). Then it loops:

  1. A attacks B: b.hp -= a.atk, logged with before->after. It records b_mortal = b.hp <= 0 but does not remove B yet.
  2. B counterattacks A: this always fires because B was alive at round start — the "dying strike". When b_mortal is true the line is tagged [dying strike]. Then a_mortal = a.hp <= 0.
  3. Resolve deaths after both hits: B's death/replacement is logged first, then A's (matching the test ordering), each calling advance_after_death() (bump idx, log sends out if a replacement exists).

Termination. After resolution it re-checks has_alive() for both teams: neither alive → Draw, else the team with survivors wins. The final Result: ... line is appended by _finish.

Why correct: deferring removal until after the counterattack faithfully models the dying-strike rule, and ordering B-then-A death logging matches the spec, so the produced log is exactly the required time-ordered trace.

Space complexity:
O(M + R): O(M) for the Monster objects of both teams plus O(R) for the full battle log, which the problem requires materializing.