LLD Cohort · Week 3 · Sun 29 Mar 2026 · recording 3h13m
Prototype, Decorator & Snake and Ladder
A quick recap of Builder and the Factory family, then the last creational pattern (Prototype), the first structural pattern (Decorator), and a first full LLD walk-through: Snake and Ladder.
Each topic: one-line idea → the problem → real-life picture → the design step by step → Python code → doubts & corrections → remember
- Recap: Builder, Simple Factory, Factory Method, Abstract Factory
- Prototype pattern
- Decorator pattern
- LLD problem: Snake and Ladder
Recap: Builder, Simple Factory, Factory Method, Abstract Factory ▶ 1:20:00 – 1:34:00
All four are creational patterns: they control how objects get made, so the code that uses objects does not have to care.
Builder: fixes "constructor explosion"
A constructor explosion happens when a class has a few mandatory fields and many optional ones (a Student with name, age, id, address, email...). You end up writing a constructor for every combination. Builder fixes it:
- A nested
Builderclass (a static inner class in Java) holds a copy of every field. - Mandatory fields go into the Builder's constructor.
- Each optional field gets a setter that returns the builder itself, so calls chain:
.set_address(...).set_email(...). build()calls the real class's constructor, passing the builder, and the fields are copied across.
class Student:
def __init__(self, builder: "Student.Builder"):
# copy everything the builder collected into the real object
self.name = builder.name
self.age = builder.age
self.address = builder.address
self.email = builder.email
def __repr__(self) -> str:
return f"Student({self.name}, {self.age}, {self.address}, {self.email})"
class Builder: # "static inner class" in Java
def __init__(self, name: str): # mandatory field goes in the constructor
self.name = name
self.age = None # optional fields start empty
self.address = None
self.email = None
def set_age(self, age: int) -> "Student.Builder":
self.age = age
return self # return self -> calls can be chained
def set_address(self, address: str) -> "Student.Builder":
self.address = address
return self
def set_email(self, email: str) -> "Student.Builder":
self.email = email
return self
def build(self) -> "Student":
return Student(self)
# usage
s = Student.Builder("Ravi").set_address("Pune").set_email("r@x.com").build()
print(s) # Student(Ravi, None, Pune, r@x.com)Java needs Builder because it has no default or keyword arguments. In Python, Student(name, age=None, email=None) already avoids the explosion. Builder still helps in Python when building is step-by-step, needs checks before the object exists, or the object should be immutable after build().
Factory family: keep object creation away from the client
The client (the code that uses the object) should only ask for an object, not know how it is built. If the business code writes WhatsAppNotification() directly, it knows too much about concrete classes and must change whenever they change.
| Variant | How it works | Weak spot |
|---|---|---|
| Simple Factory | One NotificationFactory.get_notification(type) with if/else that returns SMS / WhatsApp / Email. | Adding Slack means editing the if/else, which breaks the Open-Closed Principle (OCP: open for extension, closed for modification). |
| Factory Method | One factory class per product: SMSNotificationFactory, EmailNotificationFactory, all behind a NotificationFactory interface. | A new product means a new product class and a new factory class, but no existing code changes. |
| Abstract Factory | A "factory of factories". One factory makes a whole family of related objects: PaytmFactory makes Paytm payment + refund + invoice; RazorpayFactory makes the Razorpay set. | Many classes. Adding a new kind of product (say, Dispute) touches every factory. |
from abc import ABC, abstractmethod
# ---- Simple factory: one class decides with if/elif ----
class Notification(ABC):
@abstractmethod
def send(self, msg: str) -> None: ...
class SMSNotification(Notification):
def send(self, msg: str) -> None: print(f"SMS: {msg}")
class WhatsAppNotification(Notification):
def send(self, msg: str) -> None: print(f"WhatsApp: {msg}")
class NotificationFactory:
@staticmethod
def get_notification(kind: str) -> Notification:
if kind == "SMS":
return SMSNotification()
if kind == "WHATSAPP":
return WhatsAppNotification()
raise ValueError(kind) # adding "SLACK" means editing here -> breaks OCP
# ---- Abstract factory: one factory builds a whole FAMILY ----
class PaymentGatewayFactory(ABC):
@abstractmethod
def create_payment(self) -> str: ...
@abstractmethod
def create_refund(self) -> str: ...
@abstractmethod
def create_invoice(self) -> str: ...
class PaytmFactory(PaymentGatewayFactory):
def create_payment(self) -> str: return "PaytmPayment"
def create_refund(self) -> str: return "PaytmRefund"
def create_invoice(self) -> str: return "PaytmInvoice"
class RazorpayFactory(PaymentGatewayFactory):
def create_payment(self) -> str: return "RazorpayPayment"
def create_refund(self) -> str: return "RazorpayRefund"
def create_invoice(self) -> str: return "RazorpayInvoice"
# usage
NotificationFactory.get_notification("SMS").send("Hi") # SMS: Hi
factory: PaymentGatewayFactory = RazorpayFactory() # swap ONE line to change family
print(factory.create_payment(), factory.create_invoice()) # RazorpayPayment RazorpayInvoiceAbstract Factory is a car company's "electric" factory versus "petrol" factory: ask the electric factory and every vehicle you get (car, truck, bus) is electric. Same idea as the UI theme example: LightThemeFactory gives a light button and a light checkbox; DarkThemeFactory gives the dark pair.
→
Factory Method creates one product per factory. Abstract Factory creates a group (family) of related products that must match, like Paytm payment + Paytm refund + Paytm invoice.
→
Yes, that is a valid use. When everything is mandatory, the gain is that the client no longer calls a huge constructor in a fixed order. You can also add a single construct() method (a "director") that collects the fields, fills in the ones the client does not need to know, and calls the big constructor for them.
- Builder = many optional fields, chained setters that return
self, thenbuild(). - Simple Factory hides creation but breaks OCP when types are added.
- Factory Method = one factory per product, so new types do not touch old code.
- Abstract Factory = one factory per family of products that belong together.
Prototype pattern ▶ 1:34:00 – 2:05:00
Make new objects by cloning a ready-made object instead of building it from scratch: "build + load" becomes "copy + tweak".
The problem
Some objects are very expensive to create. A game "boss enemy" must load a 3D model, animations, an AI engine, sounds, weapons and skills in its constructor. Now the game needs 1,000 zombies, or 100 PUBG players dropping in at the same time. Most of that heavy data is identical for all of them. Running the expensive constructor 1,000 times wastes time and memory.
The teacher's demo: creating 20 game characters the normal way took about 1,100 ms and 181 MB; cloning one base character took about 53 ms and 10 MB.
Zombies in a game. Every zombie looks the same: same graphics, same rendering, same textures. What differs is small: each one's position, health, kill count, and who damaged it. So you load one zombie fully, then clone it and just move the copy to a new spot.
The idea in one line
Give the object a clone() method so the object itself decides how it is copied. Use it when creation is expensive (I/O, DB, heavy computation) and many objects share most of their data.
Building it step by step
- Without the pattern: call
Product(id)10,000 times, so the images, metadata and DB setup are loaded 10,000 times. - Add an interface
ProductPrototypewith one method,clone(). Every prototype must say how it copies itself. - Add a "copy path" that skips the heavy work. In Java the teacher wrote a private copy constructor
Product(Product base)that copies each field from the base. It is private so outsiders must go throughclone(), which keeps cloning under the class's control. - Use it: build one base product, then
base.clone()and tweak what differs (the id, the position...).
import time
from abc import ABC, abstractmethod
class ProductPrototype(ABC):
@abstractmethod
def clone(self) -> "ProductPrototype":
"""Every prototype must say how it copies itself."""
class Product(ProductPrototype):
def __init__(self, product_id: int):
# Normal (expensive) path: "build + load"
self.id = product_id
self.images = self._load_images() # heavy
self.metadata = self._give_metadata() # heavy
self.db_endpoint = self._db_loading() # heavy
# --- pretend-expensive helpers (each sleeps 0.3 s) ---
def _load_images(self) -> list[str]:
time.sleep(0.3); return ["front.png", "back.png"]
def _give_metadata(self) -> dict:
time.sleep(0.3); return {"brand": "Acme", "tags": ["new"]}
def _db_loading(self) -> str:
time.sleep(0.3); return "db://products"
def clone(self) -> "Product":
# Like Java's private copy constructor: make an object WITHOUT running __init__
copy_ = Product.__new__(Product)
copy_.id = self.id
copy_.images = self.images # reuse already-computed values
copy_.metadata = self.metadata
copy_.db_endpoint = self.db_endpoint
return copy_ # "copy + tweak": caller changes what differs
def __str__(self) -> str:
return f"Product(id={self.id}, images={self.images})"
# usage
start = time.perf_counter()
base = Product(100) # pays the full cost once
print(f"base: {time.perf_counter() - start:.2f}s")
start = time.perf_counter()
clones = []
for new_id in range(101, 1101): # 1000 more products
p = base.clone()
p.id = new_id # tweak only the unique field
clones.append(p)
print(f"1000 clones: {time.perf_counter() - start:.3f}s")
print(clones[0], clones[0] is base)
# Output (approx):
# base: 0.90s
# 1000 clones: 0.000s
# Product(id=101, images=['front.png', 'back.png']) FalsePython has no private constructors and no overloading, so you cannot write a second "copy constructor". The trick is Product.__new__(Product), which makes an empty object without running __init__. In everyday Python you simply use the standard copy module: copy.copy(obj) (shallow) or copy.deepcopy(obj) (deep). You can customise them by defining __copy__ / __deepcopy__ on the class. Java has Cloneable + Object.clone(), but most people prefer a copy constructor.
Shallow copy vs deep copy
A shallow copy creates a new outer object, but its fields still point to the same inner objects (lists, dicts, textures). A deep copy also duplicates every inner object, so nothing is shared.
- Shallow is what makes Prototype fast: the heavy texture data is shared, not copied.
- The cost: if someone mutates the shared object, every clone sees it. The teacher's example: if a controller changes the shared character colour to red, all clones turn red, because they all hold the same address.
- So: share data that is read-only and heavy; give each clone its own copy of state that changes (position, health). Deep-copy mutable lists if clones must not affect each other.
import copy
base = {"name": "zombie", "texture": ["skin.png"], "pos": [0, 0]}
shallow = copy.copy(base) # new outer dict, SAME inner lists
deep = copy.deepcopy(base) # new outer dict AND new inner lists
base["texture"].append("blood.png")
print(shallow["texture"]) # ['skin.png', 'blood.png'] -> shared, sees the change
print(deep["texture"]) # ['skin.png'] -> independent
# A clone that shares heavy data but owns its own small state:
z2 = copy.copy(base)
z2["pos"] = [100, 100] # REPLACE the list (don't mutate the shared one)
print(base["pos"], z2["pos"]) # [0, 0] [100, 100]classDiagram
class ProductPrototype {
+clone() ProductPrototype
}
class Product {
-id
-images
-metadata
-db_endpoint
+clone() Product
}
class Client
Product ..|> ProductPrototype
Client --> ProductPrototype : calls clone
Pros, cons, pitfalls
- Pro: big speed and memory savings when creation is costly and data is shared.
- Pro: the client does not need to know the concrete class; it just calls
clone(). - Con: you must decide carefully, field by field, what is shared (shallow) and what is copied (deep). Getting this wrong causes "spooky" bugs where changing one object changes another.
- Pitfall: copying the unique fields (like an id) by mistake. Always tweak them after cloning.
In class it was once said "if cloning an object is expensive, Prototype helps". It is the other way round: Prototype helps when creating an object is expensive and cloning is cheap.
A copy constructor is one way to implement Prototype. The teacher noted Prototype is rarely asked as a full interview problem, but you may get "what is Prototype?" as a quick check.
→
Yes. A copy constructor is one implementation of Prototype. The pattern just says: expose a clone() so others can copy you cheaply, without knowing your internals.
→
The clone is a separate object with its own fields (different address). Only the values inside some fields may point to the same shared data, such as a pre-loaded texture. They are not static/shared variables; each object has its own variables A, B, C.
→
If the clones share that object by reference (shallow copy) and you mutate it, yes, all of them see the change. If you replace the field with a new object, or deep-copied it, they do not.
→
Because each copy needs a few of its own values. 1,000 zombies pointing at one object would all stand at (0, 0) with the same health. Prototype is copy plus tweak: share the expensive parts, keep the small per-object state separate.
clone() return the interface type (ProductPrototype) instead of Product?→
Ideally yes, returning the parent type is cleaner abstraction. Returning Product was done in class only to avoid casting and keep the first example simple.
- Prototype = clone a pre-built object, then tweak; avoid repeating expensive set-up.
- The object itself defines
clone(), so it controls how it is copied. - Shallow copy shares inner objects (fast, but mutations leak); deep copy duplicates them (safe, slower).
- In Python:
copy.copy,copy.deepcopy,__copy__,__deepcopy__. - Classic uses: game characters, product templates, documents created from templates.
Decorator pattern ▶ 2:08:00 – 2:48:37
Add new behaviour to an object at runtime by wrapping it in another object that has the same interface, without changing or subclassing the original.
The problem
You have a working execute() function with 100 lines. Now you need 20 more lines to run before it, and on another day a different 30 lines, before or after, in any combination. Editing the tested code each time breaks the Open-Closed Principle and risks breaking what already works.
Real examples from class:
- An email notification sometimes fails. Someone asks for retry. Adding a retry loop inside the existing code breaks OCP. Instead, wrap it in a "retry notification" object that calls the original.
- A logging library starts with "log to console". Then product asks for file logging, timestamps, log levels, JSON format, remote logging, in any combination. With subclasses you get
ConsoleLogger,FileLogger,ConsoleTimestampLogger,FileTimestampLevelLogger... This is class explosion (the class version of constructor explosion): too many classes, maintenance nightmare.
Pizza. A base pizza costs 200. Add jalapeño topping (+50), then cheese burst (+100). Ask the outer layer "what is your cost?" and it answers "100 + whatever the layer inside me costs", and so on down to the base: 350. After all the toppings it is still a pizza. The teacher's second picture: a computer mouse today has many more features than 20 years ago, but it is still a mouse and you use it the same way.
The idea in one line
A decorator is-a Logger (same interface, so clients don't notice) and has-a Logger (the object it wraps). It does its extra work, then hands the call to the wrapped object. Use it when you need optional features that can be combined in any order at runtime.
Building it step by step
- Component interface:
Loggerwithlog(message). - Concrete components (the plain pizza bases):
ConsoleLogger,FileLogger. - Abstract decorator
LoggerDecorator: implementsLoggerand stores a reference to the logger it wraps. The reference is typed as the interfaceLogger, notConsoleLogger, so the same decorator works on any logger. That is the Dependency Inversion Principle: depend on abstractions, not on low-level details. Keeping the reference in the abstract base means each concrete decorator does not repeat it. - Concrete decorators:
TimestampLoggeradds a timestamp then callsself._logger.log(...);JsonParsingLoggerformats as JSON then delegates. - Stack them like toppings:
JsonParsingLogger(TimestampLogger(ConsoleLogger())). The outer layer runs first, the base runs last.
import json
import time
from abc import ABC, abstractmethod
# 1. The component interface: everyone "is a" Logger
class Logger(ABC):
@abstractmethod
def log(self, message: str) -> None:
...
# 2. Concrete components (the "plain pizza bases")
class ConsoleLogger(Logger):
def log(self, message: str) -> None:
print(f"[console] {message}")
class FileLogger(Logger):
def log(self, message: str) -> None:
print(f"[file] {message}") # pretend we append to a file
# 3. Abstract decorator: IS-A Logger and HAS-A Logger (the wrapped one)
class LoggerDecorator(Logger, ABC):
def __init__(self, logger: Logger):
self._logger = logger # who am I decorating?
# 4. Concrete decorators: add work, then hand over to the inner logger
class TimestampLogger(LoggerDecorator):
def log(self, message: str) -> None:
stamped = f"{int(time.time() * 1000)} {message}" # extra behaviour
self._logger.log(stamped) # delegate
class JsonParsingLogger(LoggerDecorator):
def log(self, message: str) -> None:
print("JSON parsing happening for the message")
self._logger.log(json.dumps({"msg": message}))
# usage
ConsoleLogger().log("Pradeep, without any decorator")
TimestampLogger(FileLogger()).log("message to the file")
# Stack layers like pizza toppings: JSON -> Timestamp -> Console
logger = JsonParsingLogger(TimestampLogger(ConsoleLogger()))
logger.log("Pradeep sending message")
# Output:
# [console] Pradeep, without any decorator
# [file] 1791403953481 message to the file
# JSON parsing happening for the message
# [console] 1791403953481 {"msg": "Pradeep sending message"}Notice: ConsoleLogger and FileLogger were never touched. They don't even know they are being wrapped. Switching the base from console to file is a one-word change at the call site.
classDiagram
class Logger {
+log(message)
}
class ConsoleLogger {
+log(message)
}
class FileLogger {
+log(message)
}
class LoggerDecorator {
#logger Logger
+log(message)
}
class TimestampLogger {
+log(message)
}
class JsonParsingLogger {
+log(message)
}
ConsoleLogger ..|> Logger
FileLogger ..|> Logger
LoggerDecorator ..|> Logger
LoggerDecorator o-- Logger : wraps
TimestampLogger --|> LoggerDecorator
JsonParsingLogger --|> LoggerDecorator
Decorator is the pattern with both an is-a and a has-a link to the same type: LoggerDecorator is a Logger and has a Logger. Draw this in an interview and the pattern is instantly recognisable.
It was said that if someone asks "which pattern has both is-a and has-a", the answer is Decorator. Decorator is the classic answer, but Composite (and often Proxy) has the same shape too. The difference is intent: Decorator adds behaviour to one wrapped object, Composite treats a tree of children like a single object.
Python's @decorator is related, but not the same
Python's @something syntax wraps a function (or class) with another function when it is defined. It shares the idea of "wrap and add behaviour", and is great for retry, logging and timing. But the GoF Decorator pattern wraps objects at runtime, behind a shared interface, and lets you choose and stack layers per object while the program runs. @ is applied once, at definition time, to every call.
import time
def with_timestamp(func): # a Python function decorator
def wrapper(message: str) -> None:
func(f"{int(time.time())} {message}") # extra work, then call the original
return wrapper
@with_timestamp # same as: log = with_timestamp(log)
def log(message: str) -> None:
print(message)
log("hello") # 1774771200 hello -> every call is wrapped, alwaysPros, cons, pitfalls
- Pro: no class explosion; features combine freely at runtime.
- Pro: follows OCP and Single Responsibility: each decorator does one extra thing.
- Con: many small wrapper objects; debugging a deep stack can be confusing.
- Pitfall: order matters. JSON then timestamp is not the same output as timestamp then JSON.
- Pitfall: a decorator cannot exist without a "decoratee". Cheese burst is nothing without the pizza base.
→
To keep parity for the client. A decorated logger must still be usable as a logger, with the same log() method. Otherwise the client would face a new method name and wonder which one to call. Implementing Logger is also what lets you wrap a decorator inside another decorator (stacking).
→
Every decorator needs to know "who am I decorating". Putting that field and constructor in the abstract base means each new decorator just calls super().__init__(logger) and focuses on its own extra work.
→
Yes. Retry, logging, JSON conversion, caching, auth checks: any add-on you want around existing behaviour, without touching that behaviour.
→
Only if each one wraps a base report and adds something on top (final report = base report + add-on). Decorator is always a wrapper around an existing object. If they are just different kinds of report, that is plain polymorphism (or a Factory to create them).
- Decorator adds behaviour dynamically by wrapping, not by subclassing or editing.
- Shape: decorator is-a Component and has-a Component; calls go outer layer to inner layer, down to the base.
- Solves class explosion when features combine in any order.
- Type the wrapped field as the interface, so a decorator works on any base (Dependency Inversion).
- Python's
@decoratoris a cousin: function wrapping at definition time, not runtime object wrapping.
LLD problem: Snake and Ladder ▶ 2:49:30 – 3:12:50
An easy first LLD problem: practise the standard framework of requirements, entities, class design, code and edge cases.
The framework for any LLD problem
- Requirements: functional (what it must do) and non-functional (qualities such as clean code or thread safety).
- Core entities: the classes and interfaces. Tip: pick out the nouns in the requirements.
- Class diagram / class design.
- Working code.
- Edge cases.
Practise 10 to 20 problems this way and the framework becomes automatic. Always write at least 4 or 5 requirement points in an interview. It gives you time to think, and it shows you did not just memorise a solution.
1. Requirements
| Functional | Non-functional |
|---|---|
|
|
2. Core entities
- Player: id, name, current position (starts at 1).
- Board: start (1), end (100), and the snakes and ladders. It answers "I landed on cell X, where do I end up?"
- Dice: returns a random number from 1 to 6. One dice is shared by all players.
- Game: holds the board, the dice and the players, and runs the game loop.
- GameDriver: the orchestrator the client calls to set everything up and start the game.
Design decisions made in class
- No 10x10 grid of Cell objects. A cell has no state worth storing here (who is on it, what is on it). Positions are just numbers from 1 to 100. That saves memory and code. (In chess, by contrast, a Cell class matters because each square has state.)
- Snakes and ladders are both "jumps" with a start and an end. Store them in one dictionary
start -> end. If start > end it is a snake; if start < end it is a ladder. Example:{5: 25}is a ladder,{50: 10}is a snake. (You may also add aJumpclass withstart/end; both are fine.) - The Board computes the next position, because it is the class that knows where the snakes and ladders are. This keeps the Game simple.
- Snakes and ladders are generated randomly in the teacher's version, so every game has a different board.
- Turns: with 2 players,
turn = 1 - turnflips between 0 and 1. For N players use a queue: pop the front player, let them play, push them to the back.
3. Class diagram
classDiagram
class GameDriver {
+run(names)
}
class Game {
-board Board
-dice Dice
-turns Queue
+play() Player
}
class Board {
-start int
-end int
-jumps Dict
+next_position(pos) int
}
class Dice {
-faces int
+roll() int
}
class Player {
-player_id int
-name str
-position int
}
GameDriver --> Game : creates
Game *-- Board
Game *-- Dice
Game o-- Player
4. Code (small, playable version)
A 30-cell board with 2 snakes and 2 ladders so a game ends quickly. Pass a seed to get the same game every time. Change size=100 for the real board.
import random
from collections import deque
from dataclasses import dataclass
@dataclass
class Player:
player_id: int
name: str
position: int = 1 # everyone starts on cell 1
class Dice:
def __init__(self, faces: int = 6):
self.faces = faces
def roll(self) -> int:
return random.randint(1, self.faces) # 1..6, both ends included
class Board:
def __init__(self, size: int = 100, snakes: int = 2, ladders: int = 2):
self.start = 1
self.end = size
# start cell -> end cell. Snake if start > end, ladder if start < end.
self.jumps: dict[int, int] = {}
self._place(snakes, is_snake=True)
self._place(ladders, is_snake=False)
def _place(self, count: int, is_snake: bool) -> None:
placed = 0
while placed < count:
a, b = random.sample(range(2, self.end), 2) # never on 1 or 100
head, tail = (max(a, b), min(a, b)) if is_snake else (min(a, b), max(a, b))
# skip cells already used as a start or end (no chains, no clashes)
used = set(self.jumps) | set(self.jumps.values())
if head in used or tail in used:
continue
self.jumps[head] = tail
placed += 1
def next_position(self, position: int) -> int:
# Board knows the snakes and ladders, so Board answers "where do I land?"
return self.jumps.get(position, position)
class Game:
def __init__(self, board: Board, dice: Dice, players: list[Player]):
self.board = board
self.dice = dice
self.turns = deque(players) # works for 2 or N players
def play(self) -> Player:
while True: # the game loop
player = self.turns.popleft()
roll = self.dice.roll()
moved = player.position + roll
if moved >= self.board.end: # teacher's rule: reach or cross the end -> win
print(f"{player.name} rolled {roll}: {player.position} -> {self.board.end}")
return player # exit the game loop: we have a winner
final = self.board.next_position(moved)
note = ""
if final < moved:
note = f" (snake at {moved})"
elif final > moved:
note = f" (ladder at {moved})"
print(f"{player.name} rolled {roll}: {player.position} -> {final}{note}")
player.position = final
self.turns.append(player) # back of the queue: next player's turn
class GameDriver:
"""Orchestrator: builds the objects and starts the game for the client."""
@staticmethod
def run(names: list[str], seed: int | None = None) -> None:
random.seed(seed)
board = Board(size=30, snakes=2, ladders=2) # small board for a quick game
print("snakes/ladders:", board.jumps)
players = [Player(i, n) for i, n in enumerate(names, start=1)]
winner = Game(board, Dice(), players).play()
print(f"Winner: {winner.name}")
# usage
if __name__ == "__main__":
GameDriver.run(["Alice", "Bob"], seed=7)Sample run (seed 7, trimmed):
snakes/ladders: {12: 6, 22: 14, 3: 4, 19: 28}
Alice rolled 1: 1 -> 2
Bob rolled 3: 1 -> 4
Alice rolled 5: 2 -> 7
...
Alice rolled 5: 7 -> 6 (snake at 12)
...
Bob rolled 6: 15 -> 21
Alice rolled 6: 18 -> 24
Bob rolled 5: 21 -> 26
Alice rolled 1: 24 -> 25
Bob rolled 5: 26 -> 30
Winner: BobIn Java, random.nextInt(6) gives 0 to 5, so the teacher wrote nextInt(6) + 1 to get 1 to 6. Python's random.randint(1, 6) includes both ends, so no + 1 is needed. Java's ArrayDeque maps to Python's collections.deque.
How the game loop works
Every game has a game loop: a while loop that keeps playing turns until the game is finished. In each turn: take the current player, roll the dice, compute position + roll, check whether you won, ask the Board for the final cell (after any snake or ladder), update the player, and pass the turn on.
5. Edge cases
- Overshooting 100. The class version says reaching or crossing 100 wins. Many real rule sets need an exact roll; if you go past 100 you stay put. Ask the interviewer which rule they want.
- Bad random boards. Random generation must not put a snake head on 100, a jump on cell 1, two jumps starting on the same cell, or a jump ending where another starts (chains). The code above skips used cells.
- start == end is not a valid jump.
- Extra rules sometimes asked: rolling a 6 gives another turn; three 6s in a row cancel the turn; several dice.
Which patterns and principles are used?
- Composition: Game has a Board, a Dice and Players (has-a), instead of inheritance.
- Single Responsibility: Board knows jumps, Dice knows rolling, Player knows its position, Game runs the loop, GameDriver wires things up.
- Information Expert: the class that owns the data (Board owns the jumps) does the computation (next position).
- Natural extensions, not coded in class: a Strategy for the dice (normal, crooked, two dice), a Factory/Builder to create boards from a config, Observer to notify a UI on each move. The teacher also promised a "game template" (the shared skeleton of turn-based games, i.e. Template Method) in a later class.
→
In the Board's dictionary (Java HashMap) jumps, as start -> end, filled when the board is created.
→
Yes. Whatever the final cell is after the snake or ladder, that becomes the player's new position. Since there are no chained jumps, one lookup is enough.
nextInt(6) + 1 for the dice?→
Java's nextInt(6) returns 0 to 5 (the upper bound is excluded). Adding 1 shifts it to 1 to 6.
Homework: write your own Snake and Ladder from scratch without looking at the solution code, using a queue for N players. Also read about LRU Cache before the next class.
- Follow the framework: requirements, entities (nouns), class diagram, code, edge cases.
- Don't model what has no state: positions are plain integers, no Cell grid needed.
- Snakes and ladders are both jumps: one
start -> enddict handles both. - Put logic where the data lives: Board gives the next position.
- Game loop + queue of players handles any number of players.