CricInfo
Observer for live score updates pushed to every connected client. State pattern manages match lifecycle transitions from NOT_STARTED through IN_PROGRESS and INNINGS_BREAK to COMPLETED.
Key Abstractions
Top-level orchestrator holding teams, innings, toss result, lifecycle state, and observer management
Named squad of players with a batting order
Individual with accumulated batting stats (runs, balls faced) and bowling stats (wickets, runs conceded)
One team's batting session containing overs, running total, and wicket count
Collection of up to 6 legal deliveries bowled by one bowler
Live score tracker that computes batting card, bowling card, and summary from raw ball data
Observer interface for pushing live updates to subscribers: website, mobile app, TV overlay
Class Diagram
The Key Insight
Cricket has a natural hierarchy: match contains innings, innings contain overs, overs contain balls. When you design the scoring system, the temptation is to store running totals at each level. Innings knows its total runs. Over tracks its own count. Seems clean.
It is a trap. Every time you store a derived total, you create a place where data can drift out of sync. Miss an update after an extra? Scoreboard shows the wrong number. Retroactively correct a wide that was actually a no-ball? You need to fix totals at every level of the hierarchy.
Instead, let Ball be the single source of truth. Every aggregate, runs, wickets, batting strike rate, bowling economy, computes by walking the ball records. For a cricket match with at most 600 deliveries, that computation is trivially fast. And you never deal with stale data.
Layer the observer pattern on top so every ball event pushes live updates to every connected client: the website, the mobile app, the TV overlay, and push notifications. The match does not know who is listening. It just broadcasts.
Requirements
Functional
- Model a cricket match with two teams, innings, overs, and ball-by-ball scoring
- Track runs, wickets, and extras (wides, no-balls, byes, leg-byes) per delivery
- Compute live scorecard: batting card, bowling card, innings summary
- Push live score updates, wicket alerts, and over completion events to all observers
- Enforce match lifecycle transitions: NOT_STARTED to IN_PROGRESS to INNINGS_BREAK to COMPLETED
Non-Functional
- Scorecard computes from raw ball data on every call. No stale cached aggregates.
- Observer-based updates for multiple consumers without polling
- State machine prevents invalid lifecycle transitions like COMPLETED back to IN_PROGRESS
- Clean separation between data model (Ball, Over, Innings) and presentation (Scorecard)
Design Decisions
Why compute totals from balls instead of caching?
Cricket has retroactive corrections. Umpire reviews can overturn a decision. Scoring errors happen under pressure. If totals are cached at every level, a correction means hunting down and updating every cached value up the chain. When totals derive from balls, you fix one ball record and every query returns the right number. For a T20 with 240 legal deliveries, computing a sum on the fly takes microseconds.
Why a state machine for match lifecycle?
Without explicit state transitions, nothing prevents code from calling startInnings() on a completed match. Or completing a match that never started. A state machine with can_transition_to() validation catches these bugs at runtime with a clear error message instead of silently corrupting match data.
Why Observer for live updates?
A live cricket match feeds data to the stadium scoreboard, the TV overlay, the website, the mobile app, and push notifications at the same time. Polling from each consumer wastes bandwidth and introduces inconsistent delay. Observer pushes the update once and every listener gets it at the same moment. Adding a new consumer, like an analytics pipeline, means registering one more observer. No changes to the CricketMatch class.
Why separate Scorecard from Innings?
Innings manages raw ball data. Scorecard formats that data into human-readable cards and summaries. Mixing them means the Innings class needs to know about display formatting, column alignment, and string building. Different responsibility. Keeping them separate also means you can have multiple Scorecard implementations (compact for mobile, detailed for TV, simplified for notifications) without touching the data model.
Interview Follow-ups
- "How would you add DRS (Decision Review System)?" Introduce a
Reviewobject associated with a Ball. If the review overturns the decision, update the Ball's wicket field. Since totals derive from balls, the scorecard auto-corrects on the next query. - "How would you support different formats (T20, ODI, Test)?" Add a
MatchFormatconfig that controls innings count, max overs, and powerplay rules. CricketMatch reads these constraints for format-specific enforcement. - "How would you persist ball-by-ball data for replay?" Event sourcing. Each Ball is an event appended to a log. Replaying the log rebuilds the entire match state. This also enables ball-by-ball replay for highlights.
- "How would you handle live commentary alongside scores?" Commentary is another observer. It receives ball events and generates text descriptions. Keeps commentary decoupled from scoring logic entirely.
45-Minute LLD Playbook
Each phase is what the interviewer expects you to do and say. Concrete steps, not topic hints. Diagrams are what you would sketch on the board.
- 15 min
Clarify scope and lock the process
GoalPin down the ball-by-ball model, the live-update fan-out, and the lifecycle transitions. Ask the interviewer whether they want a class diagram or code-first.
Do & Say- ASK·1Open with: Ball is the atomic scoring unit. Every aggregate (runs, wickets, strike rate, economy) computes by walking the ball records, no cached totals at any level. That single decision avoids retroactive-correction bugs.
- SAY·2Pin the extras semantics: Wides and no-balls do not count as legal deliveries, so the over does not advance on them. Byes and leg-byes are legal deliveries but the runs are not credited to the batsman. is_legal_delivery encodes that rule.
- SAY·3Lock the lifecycle: Match states NOT_STARTED, IN_PROGRESS, INNINGS_BREAK, COMPLETED. Transitions enforced by can_transition_to. A completed match cannot start a new innings. An innings break can jump straight to completed for the rain-rule edge case, which stays in scope.
- SAY·4Park what is out of scope: No DRS. No Super Over. No T20 vs ODI vs Test format differences for v1. Single observer interface, no separate push-vs-pull channels.
- ASK·5Ask the process question: Do you want me to draw the Match-Innings-Over-Ball hierarchy plus the state diagram on the whiteboard, or jump to code with the hierarchy expressed in the class structure?. I want to budget the remaining 40 minutes deliberately.
Interviewer is grading: You name 'no cached totals, every aggregate computes from balls' as the design's core invariant before anything else. You commit to the four-state lifecycle and explicitly defend the INNINGS_BREAK -> COMPLETED transition for the second-innings-ends case.
- 25-10 min
Sketch the API and (optionally) the hierarchy diagram
GoalLock the Ball signature, the MatchState transition rules, the Observer events, and the Scorecard interface. Draw the hierarchy if requested.
Do & Say- SAY·1Name the abstractions: MatchState enum with can_transition_to, ExtraType and WicketType enums, Player with cumulative stats, Team, Wicket dataclass, Ball dataclass, Over, Innings, MatchObserver interface, Scorecard as a stateless formatter, CricketMatch orchestrator.
- WRITE·2Write the Ball signature: Fields batsman, bowler, runs_scored, extra_type (default NONE), extra_runs (default 0), wicket (Optional). is_legal_delivery returns extra_type not in (WIDE, NO_BALL). total_runs returns runs_scored + extra_runs.
- WRITE·3Write the Over API: Fields over_number, bowler, _balls list. add_ball appends and updates bowler stats (runs_conceded += total_runs, balls_bowled += 1 only if legal, wickets_taken += 1 on wicket). is_complete returns legal_deliveries >= 6. Say: A wide does not advance the over count. That is why is_legal_delivery exists on Ball.
- WRITE·4Write the Innings API: Fields batting_team, bowling_team, innings_number, _overs list, _current_over, _wickets. start_over(bowler) appends a new Over. add_ball delegates to current_over, updates batsman.balls_faced (only on legal), batsman.runs_scored (always), and _wickets on wicket. total_runs sums over.runs. overs_completed formats as completed.balls.
- WRITE·5Write the MatchState.can_transition_to rules: NOT_STARTED -> [IN_PROGRESS]. IN_PROGRESS -> [INNINGS_BREAK, COMPLETED]. INNINGS_BREAK -> [IN_PROGRESS, COMPLETED]. COMPLETED -> []. Any other transition raises.
- WRITE·6Write the MatchObserver interface: on_score_update(innings, ball). on_wicket(innings, ball). on_over_complete(innings, over). on_state_change(old, new). Four events because the consumers care about different cuts: scoreboard wants every ball, push-notif wants only wickets and overs.
- WRITE·7Write the Scorecard API as static methods: summary(innings). batting_card(innings). bowling_card(innings). Stateless, walks innings.overs and innings.overs[i].balls to compute every number on demand.
- SAY·8If a diagram was requested, draw Match at top with Innings stacked under it, Over under Innings, Ball under Over. State machine to the right with four circles. Observer fan-out below Match showing four event types branching to scoreboard, mobile, TV, push.
Interviewer is grading: You name the no-cached-totals rule and walk through one retroactive-correction scenario (umpire reviews a wicket call) to show why it matters. You explicitly defend INNINGS_BREAK -> COMPLETED as a valid transition for the end-of-second-innings case.
- 325 min
Code in this sequence (bottom-up)
GoalType the code in the order MatchState + transition table, ExtraType, WicketType, Player, Team, Wicket, Ball, Over, Innings, MatchObserver + console concrete, Scorecard, CricketMatch so each layer has its dependencies defined when referenced.
Do & Say- SAY·1Code MatchState enum with values NOT_STARTED, IN_PROGRESS, INNINGS_BREAK, COMPLETED, plus can_transition_to(target) returning whether target is in the allowed set for the current state. (~3 min)
- SAY·2Code ExtraType (NONE, WIDE, NO_BALL, BYE, LEG_BYE) and WicketType (BOWLED, CAUGHT, LBW, RUN_OUT, STUMPED) enums. (~1 min)
- SAY·3Code Player: Fields name, runs_scored, balls_faced, wickets_taken, runs_conceded, balls_bowled. strike_rate property = runs_scored / balls_faced * 100 (guard div-zero). economy = runs_conceded / (balls_bowled / 6) (guard div-zero). (~1 min)
- SAY·4Say: Player stats accumulate by walking the ball stream. Strike rate and economy are derived properties, not stored fields. (~1 min)
- SAY·5Code Team with name and players list. Code Wicket dataclass with wicket_type, batsman_out, fielder (Optional). (~1 min)
- SAY·6Code Ball dataclass: Fields batsman, bowler, runs_scored=0, extra_type=NONE, extra_runs=0, wicket=None. is_legal_delivery returns extra_type not in (WIDE, NO_BALL). total_runs returns runs_scored + extra_runs. Say: Frozen-ish dataclass. The atomic unit. Every aggregate ultimately walks a list of these. (~2 min)
- SAY·7Code Over fields: over_number, bowler, _balls list. add_ball(ball) appends, then mutates bowler stats: runs_conceded += ball.total_runs, balls_bowled += 1 only if is_legal_delivery, wickets_taken += 1 if ball.wicket. (~2 min)
- SAY·8Add Over helpers: legal_deliveries counts ball.is_legal_delivery in _balls. is_complete returns legal_deliveries >= 6. runs sums total_runs over _balls. Say: is_legal_delivery is what makes a wide not advance the over count. (~2 min)
- SAY·9Code Innings fields: batting_team, bowling_team, innings_number, _overs list, _current_over, _wickets. start_over(bowler) creates a new Over, appends to _overs, sets _current_over. (~1 min)
- SAY·10Code Innings.add_ball: delegates to _current_over.add_ball, then mutates batsman: balls_faced += 1 only if legal, runs_scored += ball.runs_scored (not total_runs), _wickets += 1 if ball.wicket. (~2 min)
- SAY·11Add Innings.total_runs (sums over.runs over _overs) and overs_completed formatted as f'{completed_count}.{current_over_legal_balls}' with the dot dropped when balls=0. (~1 min)
- SAY·12Say: Byes credit the team but not the batsman. That is why I use runs_scored, not total_runs, for batsman accumulation. (~1 min)
- SAY·13Code MatchObserver ABC with on_score_update, on_wicket, on_over_complete, on_state_change. Code ConsoleMatchObserver printing each event with the innings score in the format {team}: {runs}/{wickets} ({overs} ov). (~3 min)
- SAY·14Code Scorecard.summary as a static method returning f'{team}: {runs}/{wickets} ({overs} overs)'. (~1 min)
- SAY·15Code Scorecard.batting_card: walks innings.overs and innings.overs[i].balls, tracks seen-batsmen via a set, formats name, runs, balls, strike rate as fixed-width columns. (~1 min)
- SAY·16Code Scorecard.bowling_card: walks overs once, tracks seen-bowlers, formats name, overs (balls_bowled // 6 dot balls_bowled % 6), runs_conceded, wickets_taken, economy. Say: Stateless. Every call recomputes from balls, retroactive corrections propagate automatically. (~1 min)
- SAY·17Code CricketMatch fields: teams, toss_winner, _innings list, _current_innings, _state = NOT_STARTED, _observers list, _scorecard = Scorecard(). _transition_to(new_state) checks can_transition_to, raises if invalid, then sets and fires on_state_change. (~1 min)
- SAY·18Code CricketMatch.start_innings: auto-transitions NOT_STARTED or INNINGS_BREAK to IN_PROGRESS, creates Innings, sets _current_innings. record_ball delegates to _current_innings.add_ball, fires on_score_update, fires on_wicket if ball.wicket, fires on_over_complete if the current over just completed. (~1 min)
- SAY·19Add end_innings (transitions to INNINGS_BREAK and clears _current_innings) and complete_match (transitions to COMPLETED). (~1 min)
- SAY·20Walk-through over part one: Rohit hits 4 off Starc (legal, balls_faced 1, runs_scored 4). Dot ball (legal, 2). Single (legal, 3). Wide (NOT legal, batsman stats unchanged, total_runs += 1). (~1 min)
- SAY·21Walk-through over part two: Six (legal, balls_faced 4 for Virat on strike, runs_scored 6). Wicket (legal, 5, _wickets 1). Single (legal, 6). is_complete returns True, on_over_complete fires. (~1 min)
Interviewer is grading: You make is_legal_delivery the gate for both balls_bowled and balls_faced increments. You credit ball.runs_scored (not total_runs) to the batsman because byes do not count for batsmen. You volunteer the over-with-a-wide walk-through as the self-check before declaring done.
- 45 min
Trade-offs, extensions, and wrap-up
GoalName two trade-offs, volunteer two extensions, close with one sentence.
Do & Say- SAY·1Trade-off one, compute totals from balls vs cache at each level: Caching innings.total_runs and over.runs forces every update path to maintain them. A retroactive correction like an umpire overturning a wicket means hunting every cached value up the chain.
- SAY·2Defend computing: Walking a max-600-ball list takes microseconds and is always consistent. Cost is a tiny constant factor on every query.
- SAY·3Trade-off two, State machine on MatchState vs free transitions: Without can_transition_to, nothing prevents calling start_innings on a COMPLETED match or complete_match on a NOT_STARTED match. The state machine catches the bug at the bad call with a clear error, not later when match data is silently corrupted.
- SAY·4Extension to volunteer, DRS (Decision Review System): Introduce a Review object associated with a Ball. If the review overturns the original call, mutate ball.wicket (set to None or to a new Wicket). Since totals derive from balls, the scorecard auto-corrects on the next summary call. No cached state to invalidate.
- SAY·5Extension to volunteer, format-specific rules (T20, ODI, Test): Add a MatchFormat dataclass with innings_count, max_overs, powerplay_rules. CricketMatch reads it for format-specific enforcement (e.g. T20 max_overs=20, ODI=50, Test innings_count=4). Same Ball, Over, Innings, Match shape; only the constraints differ.
- SAY·6Close with one sentence: Ball as the atomic unit so every aggregate stays consistent under retroactive corrections. State machine on MatchState so invalid lifecycle jumps raise at the call. Observer on every ball event so scoreboard, mobile, TV, and push notifications all stay in sync without coupling.
Interviewer is grading: You name the retroactive-correction failure mode that cached totals create. You volunteer DRS and format-specific rules as the natural extensions. You can summarize the design in under 20 seconds.
Code Implementation
from __future__ import annotations
from enum import Enum
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Optional
class MatchState(Enum):
NOT_STARTED = "not_started"
IN_PROGRESS = "in_progress"
INNINGS_BREAK = "innings_break"
COMPLETED = "completed"
def can_transition_to(self, target: "MatchState") -> bool:
valid = {
MatchState.NOT_STARTED: [MatchState.IN_PROGRESS],
MatchState.IN_PROGRESS: [MatchState.INNINGS_BREAK, MatchState.COMPLETED],
# A match can finish straight from the innings break (second innings ended).
MatchState.INNINGS_BREAK: [MatchState.IN_PROGRESS, MatchState.COMPLETED],
MatchState.COMPLETED: [],
}
return target in valid[self]
class ExtraType(Enum):
NONE = "none"
WIDE = "wide"
NO_BALL = "no_ball"
BYE = "bye"
LEG_BYE = "leg_bye"
class WicketType(Enum):
BOWLED = "bowled"
CAUGHT = "caught"
LBW = "lbw"
RUN_OUT = "run_out"
STUMPED = "stumped"
class Player:
def __init__(self, name: str):
self.name = name
self.runs_scored = 0
self.balls_faced = 0
self.wickets_taken = 0
self.runs_conceded = 0
self.balls_bowled = 0
@property
def strike_rate(self) -> float:
return (self.runs_scored / self.balls_faced * 100) if self.balls_faced > 0 else 0.0
@property
def economy(self) -> float:
overs = self.balls_bowled / 6
return (self.runs_conceded / overs) if overs > 0 else 0.0
def __str__(self) -> str:
return self.name
class Team:
def __init__(self, name: str, players: list[Player]):
self.name = name
self.players = players
def __str__(self) -> str:
return self.name
@dataclass
class Wicket:
wicket_type: WicketType
batsman_out: Player
fielder: Optional[Player] = None
def __str__(self) -> str:
if self.fielder:
return f"{self.wicket_type.value} ({self.fielder.name})"
return self.wicket_type.value
@dataclass
class Ball:
"""Atomic scoring unit. Every aggregate computes upward from here."""
batsman: Player
bowler: Player
runs_scored: int = 0
extra_type: ExtraType = ExtraType.NONE
extra_runs: int = 0
wicket: Optional[Wicket] = None
def is_legal_delivery(self) -> bool:
return self.extra_type not in (ExtraType.WIDE, ExtraType.NO_BALL)
def total_runs(self) -> int:
return self.runs_scored + self.extra_runs
def __str__(self) -> str:
parts = []
if self.extra_type != ExtraType.NONE:
parts.append(self.extra_type.value)
if self.runs_scored > 0:
parts.append(f"{self.runs_scored} run{'s' if self.runs_scored > 1 else ''}")
if self.wicket:
parts.append(f"WICKET! {self.wicket.batsman_out.name} {self.wicket}")
if not parts:
parts.append("dot ball")
return ", ".join(parts)
class Over:
def __init__(self, over_number: int, bowler: Player):
self.over_number = over_number
self.bowler = bowler
self._balls: list[Ball] = []
def add_ball(self, ball: Ball) -> None:
self._balls.append(ball)
ball.bowler.runs_conceded += ball.total_runs()
if ball.is_legal_delivery():
ball.bowler.balls_bowled += 1
if ball.wicket:
ball.bowler.wickets_taken += 1
@property
def legal_deliveries(self) -> int:
return sum(1 for b in self._balls if b.is_legal_delivery())
def is_complete(self) -> bool:
return self.legal_deliveries >= 6
@property
def runs(self) -> int:
return sum(b.total_runs() for b in self._balls)
@property
def balls(self) -> list[Ball]:
return list(self._balls)
def __str__(self) -> str:
ball_str = " ".join(str(b.total_runs()) for b in self._balls)
return f"Over {self.over_number}: {ball_str} ({self.runs} runs)"
class Innings:
def __init__(self, batting_team: Team, bowling_team: Team, innings_number: int):
self.batting_team = batting_team
self.bowling_team = bowling_team
self.innings_number = innings_number
self._overs: list[Over] = []
self._current_over: Optional[Over] = None
self._wickets = 0
def start_over(self, bowler: Player) -> Over:
over_num = len(self._overs) + 1
self._current_over = Over(over_num, bowler)
self._overs.append(self._current_over)
return self._current_over
def add_ball(self, ball: Ball) -> None:
if self._current_over is None:
raise RuntimeError("No over in progress. Call start_over() first.")
self._current_over.add_ball(ball)
if ball.is_legal_delivery():
ball.batsman.balls_faced += 1
ball.batsman.runs_scored += ball.runs_scored
if ball.wicket:
self._wickets += 1
@property
def total_runs(self) -> int:
return sum(over.runs for over in self._overs)
@property
def wickets(self) -> int:
return self._wickets
@property
def overs_completed(self) -> str:
completed = sum(1 for o in self._overs if o.is_complete())
current_balls = 0
if self._current_over and not self._current_over.is_complete():
current_balls = self._current_over.legal_deliveries
if current_balls > 0:
return f"{completed}.{current_balls}"
return str(completed)
@property
def overs(self) -> list[Over]:
return list(self._overs)
@property
def current_over(self) -> Optional[Over]:
return self._current_over
class MatchObserver(ABC):
"""Observer interface for live score consumers."""
@abstractmethod
def on_score_update(self, innings: Innings, ball: Ball) -> None: ...
@abstractmethod
def on_wicket(self, innings: Innings, ball: Ball) -> None: ...
@abstractmethod
def on_over_complete(self, innings: Innings, over: Over) -> None: ...
@abstractmethod
def on_state_change(self, old_state: MatchState, new_state: MatchState) -> None: ...
class ConsoleMatchObserver(MatchObserver):
def __init__(self, name: str):
self._name = name
def on_score_update(self, innings: Innings, ball: Ball) -> None:
print(f" [{self._name}] {innings.batting_team}: "
f"{innings.total_runs}/{innings.wickets} "
f"({innings.overs_completed} ov) | {ball}")
def on_wicket(self, innings: Innings, ball: Ball) -> None:
print(f" [{self._name}] WICKET! {ball.wicket.batsman_out.name} "
f"is out {ball.wicket}")
def on_over_complete(self, innings: Innings, over: Over) -> None:
print(f" [{self._name}] End of {over} | "
f"Score: {innings.total_runs}/{innings.wickets}")
def on_state_change(self, old_state: MatchState, new_state: MatchState) -> None:
print(f" [{self._name}] Match state: {old_state.value} -> {new_state.value}")
class Scorecard:
"""Stateless. Computes everything from raw ball data on each call."""
@staticmethod
def summary(innings: Innings) -> str:
return (f"{innings.batting_team.name}: "
f"{innings.total_runs}/{innings.wickets} "
f"({innings.overs_completed} overs)")
@staticmethod
def batting_card(innings: Innings) -> str:
lines = [f"{'Batsman':<20} {'R':>4} {'B':>4} {'SR':>7}"]
lines.append("-" * 38)
seen = set()
for over in innings.overs:
for ball in over.balls:
p = ball.batsman
if p.name not in seen:
seen.add(p.name)
lines.append(
f"{p.name:<20} {p.runs_scored:>4} "
f"{p.balls_faced:>4} {p.strike_rate:>7.1f}"
)
return "\n".join(lines)
@staticmethod
def bowling_card(innings: Innings) -> str:
lines = [f"{'Bowler':<20} {'O':>4} {'R':>4} {'W':>4} {'Econ':>7}"]
lines.append("-" * 42)
seen = set()
for over in innings.overs:
b = over.bowler
if b.name not in seen:
seen.add(b.name)
overs_display = f"{b.balls_bowled // 6}.{b.balls_bowled % 6}"
lines.append(
f"{b.name:<20} {overs_display:>4} "
f"{b.runs_conceded:>4} {b.wickets_taken:>4} "
f"{b.economy:>7.1f}"
)
return "\n".join(lines)
class CricketMatch:
def __init__(self, team1: Team, team2: Team, toss_winner: Team):
self.teams = [team1, team2]
self.toss_winner = toss_winner
self._innings: list[Innings] = []
self._current_innings: Optional[Innings] = None
self._state = MatchState.NOT_STARTED
self._observers: list[MatchObserver] = []
self._scorecard = Scorecard()
@property
def state(self) -> MatchState:
return self._state
@property
def scorecard(self) -> Scorecard:
return self._scorecard
@property
def innings(self) -> list[Innings]:
return list(self._innings)
def add_observer(self, observer: MatchObserver) -> None:
self._observers.append(observer)
def _transition_to(self, new_state: MatchState) -> None:
if not self._state.can_transition_to(new_state):
raise RuntimeError(
f"Invalid transition: {self._state.value} -> {new_state.value}"
)
old_state = self._state
self._state = new_state
for obs in self._observers:
obs.on_state_change(old_state, new_state)
def start_match(self) -> None:
self._transition_to(MatchState.IN_PROGRESS)
def start_innings(self, batting_team: Team, bowling_team: Team) -> Innings:
if self._state == MatchState.NOT_STARTED:
self._transition_to(MatchState.IN_PROGRESS)
elif self._state == MatchState.INNINGS_BREAK:
self._transition_to(MatchState.IN_PROGRESS)
innings_num = len(self._innings) + 1
innings = Innings(batting_team, bowling_team, innings_num)
self._innings.append(innings)
self._current_innings = innings
print(f"\n=== Innings {innings_num}: {batting_team.name} batting ===")
return innings
def record_ball(self, ball: Ball) -> None:
if self._current_innings is None:
raise RuntimeError("No innings in progress")
self._current_innings.add_ball(ball)
for obs in self._observers:
obs.on_score_update(self._current_innings, ball)
if ball.wicket:
obs.on_wicket(self._current_innings, ball)
cur_over = self._current_innings.current_over
if cur_over and cur_over.is_complete():
for obs in self._observers:
obs.on_over_complete(self._current_innings, cur_over)
def end_innings(self) -> None:
self._transition_to(MatchState.INNINGS_BREAK)
self._current_innings = None
def complete_match(self) -> None:
self._transition_to(MatchState.COMPLETED)
if __name__ == "__main__":
# Create players
rohit = Player("Rohit Sharma")
virat = Player("Virat Kohli")
bumrah = Player("Jasprit Bumrah")
warner = Player("David Warner")
smith = Player("Steve Smith")
starc = Player("Mitchell Starc")
cummins = Player("Pat Cummins")
india = Team("India", [rohit, virat, bumrah])
australia = Team("Australia", [warner, smith, starc, cummins])
# Set up match with observer
match = CricketMatch(india, australia, toss_winner=india)
match.add_observer(ConsoleMatchObserver("TV Overlay"))
# First innings: India batting
innings1 = match.start_innings(india, australia)
innings1.start_over(starc)
print("\nOver 1 (Starc bowling to Rohit):")
match.record_ball(Ball(rohit, starc, runs_scored=4))
match.record_ball(Ball(rohit, starc, runs_scored=0))
match.record_ball(Ball(rohit, starc, runs_scored=1))
match.record_ball(Ball(virat, starc, runs_scored=0,
extra_type=ExtraType.WIDE, extra_runs=1))
match.record_ball(Ball(virat, starc, runs_scored=6))
match.record_ball(Ball(virat, starc, runs_scored=0,
wicket=Wicket(WicketType.CAUGHT, virat, cummins)))
match.record_ball(Ball(rohit, starc, runs_scored=2))
# Second over
innings1.start_over(cummins)
print("\nOver 2 (Cummins bowling to Rohit):")
match.record_ball(Ball(rohit, cummins, runs_scored=0))
match.record_ball(Ball(rohit, cummins, runs_scored=4))
match.record_ball(Ball(rohit, cummins, runs_scored=1))
match.record_ball(Ball(rohit, cummins, runs_scored=0,
extra_type=ExtraType.NO_BALL, extra_runs=1))
match.record_ball(Ball(rohit, cummins, runs_scored=2))
match.record_ball(Ball(rohit, cummins, runs_scored=0))
match.record_ball(Ball(rohit, cummins, runs_scored=1))
# End first innings
match.end_innings()
# Display scorecard
scorecard = match.scorecard
print("\n" + "=" * 42)
print("SCORECARD")
print("=" * 42)
print(scorecard.summary(innings1))
print()
print(scorecard.batting_card(innings1))
print()
print(scorecard.bowling_card(innings1))
print(f"\nTotal runs: {innings1.total_runs}")
print(f"Wickets: {innings1.wickets}")
print(f"Overs: {innings1.overs_completed}")
print(f"Match state: {match.state.value}")
# Verify state machine
assert innings1.wickets == 1
assert match.state == MatchState.INNINGS_BREAK
# Start second innings
innings2 = match.start_innings(australia, india)
assert match.state == MatchState.IN_PROGRESS
print(f"\nSecond innings started. Match state: {match.state.value}")
# Complete match
match.end_innings()
match.complete_match()
assert match.state == MatchState.COMPLETED
print(f"Match completed. Final state: {match.state.value}")
print("All assertions passed.")Interview Grading by Level
What an interviewer at each level expects to see in your answer. Use this to calibrate, not to perform.
Junior Engineer (L3)
Names Observer and State and writes Ball, Over, and Innings, but extras semantics and the cached-vs-computed-totals choice stay vague.
- Names Observer and State correctly when asked which patterns apply.
- Writes a Ball class with batsman, bowler, and runs_scored fields.
- Models the hierarchy Match -> Innings -> Over -> Ball correctly.
- Recognizes that wickets are a special outcome of a ball, not a separate event type.
- Implements a basic observer interface that gets notified on every ball.
- Caches innings.total_runs and over.runs as mutable fields that diverge from the ball list under bug conditions.
- Counts wides and no-balls as legal deliveries, so the over advances on illegal balls.
- Credits byes and leg-byes to the batsman's runs_scored, inflating personal totals.
- Lets the match state jump freely (COMPLETED -> IN_PROGRESS) because there is no transition table.
- Stores strike rate and economy as fields instead of computing them on demand from runs and balls.
Mid-Level Engineer (L4)
Drives the design end-to-end with ball-as-atomic-unit, correct extras semantics, a four-state lifecycle, and observer notifications for every ball event.
- Implements is_legal_delivery on Ball as the gate for both balls_bowled (bowler) and balls_faced (batsman).
- Credits ball.runs_scored (not total_runs) to the batsman so byes and leg-byes go to the team but not the personal score.
- Computes innings.total_runs and player stats by walking the ball list, never as a cached mutable field.
- Enforces the MatchState transitions via can_transition_to so invalid jumps raise immediately.
- Fires four distinct observer events (on_score_update, on_wicket, on_over_complete, on_state_change) so different consumers can subscribe to the cuts they care about.
- Implements Scorecard as a stateless formatter with static methods so it can be swapped per consumer (compact mobile vs detailed TV).
- Handles the INNINGS_BREAK -> COMPLETED transition for the end-of-second-innings case rather than forcing INNINGS_BREAK -> IN_PROGRESS first.
- Does not volunteer DRS as a natural extension unless prompted.
- Misses format-specific rules (T20 max_overs=20, ODI=50, Test innings=4) as a MatchFormat config.
- Treats event sourcing for replay as 'just log the balls' without naming append-only event log and state reconstruction explicitly.
Senior Engineer (L5+)
Volunteers DRS, format-specific rules, event sourcing, and live commentary before being asked, and frames each pattern around the specific failure it prevents.
- Volunteers DRS via a Review object on Ball that mutates ball.wicket on overturn, naming auto-correction-on-next-query as the benefit of not caching totals.
- Volunteers format-specific rules as a MatchFormat dataclass with innings_count, max_overs, and powerplay_rules consumed by CricketMatch enforcement.
- Names the no-cached-totals rule as the failure-prevention for retroactive corrections under pressure, walking through the umpire-overturns-wicket scenario.
- Frames the State machine on MatchState as the fix for invalid lifecycle calls (start_innings on COMPLETED, complete_match on NOT_STARTED) with raised errors at the call site.
- Proposes event sourcing for ball-by-ball persistence and replay: each Ball is an event in an append-only log, replaying the log rebuilds the entire match state for highlights and historical analysis.
- Suggests live commentary as another MatchObserver that consumes ball events and generates human text, decoupling commentary from scoring logic entirely.
- Names byes vs leg-byes vs wides as three distinct extras with different rules and walks through how is_legal_delivery and the runs_scored vs total_runs split encode each correctly.
- Closes with a one-sentence summary that names ball-as-atomic-unit, State machine on MatchState, Observer fan-out, and Scorecard as a stateless formatter in under 20 seconds.
Common Mistakes
- ✗Caching total runs as a mutable field on Innings. One missed update and the scoreboard silently shows wrong numbers.
- ✗Using string comparisons for match state transitions. An enum with a state machine prevents invalid jumps like COMPLETED to IN_PROGRESS.
- ✗Putting all scoring logic inside the Match class. Match should delegate to Innings, which delegates to Over. Keep each level focused.
- ✗Treating extras as regular runs. Wides and no-balls have different rules: a wide does not count as a legal delivery, so the over does not advance.
Key Points
- ✓Observer pattern pushes live score updates to website, mobile app, TV overlay, and push notifications simultaneously
- ✓State pattern manages match lifecycle: NOT_STARTED to IN_PROGRESS to INNINGS_BREAK to COMPLETED with explicit transitions
- ✓Scorecard recomputes from raw ball data on every call. No cached totals that can drift out of sync.
- ✓Ball-by-ball event tracking is the atomic unit. Every aggregate (innings total, strike rate, economy) derives from individual deliveries.