π Machine Coding: Modular Snake & Ladder Game
π Overview
Design and implement a robust Snake & Ladder game engine. This challenge focuses on creating a modular, turn-based system that handles multiple players, dynamic board configurations with "jumps" (snakes and ladders), and fair dice mechanics.
Why This Challenge?
- Entity Modeling Mastery: Tests your ability to represent real-world game components (Board, Players, Snakes, Ladders) as interacting objects.
- State Machine Logic: Evaluates your management of turn-based transitions and game states until a victory condition is met.
- Extensibility: Challenges you to design a system where new rules (e.g., special squares, multiple dice) can be added without refactoring core logic.
π The Scenario & Requirements
π‘ The Problem (The Villain)
"The God Class." A messy implementation where board layout, player turns, and dice logic are all crammed into one giant 500-line while True loop. Adding a simple "Special Booster" square or a second die requires rewriting half the codebase, and the game is prone to infinite loops between snakes and ladders.
π¦Έ The System (The Hero)
"The Decoupled Engine." An automated game manager that separates the Board (the physical layout) from the Game Controller (the turn logic). By treating snakes and ladders as generic "jumps" and using a strict player queue, the system ensures fairness, prevents infinite loops, and scales easily.
π Requirements & Constraints
- Functional:
- Dynamic Board: Support for a configurable grid (default 10x10) with custom snake and ladder positions.
- Multi-Player: Handle \(N\) players with unique identifiers and a persistent turn order.
- Dice Mechanics: Implement a standard 6-sided die with an option for configurable \(K\)-sided dice.
- Victory Logic: Detect and announce the winner exactly when they reach the final square.
- Technical:
- Exact Finish: A player must roll the exact number required to land on square 100; otherwise, they remain in place.
- Fairness: Maintain a strict queue of players to ensure turn integrity.
- Encapsulation: Players should not be able to modify board state directly.
ποΈ Design & Architecture
π§ Thinking Process
To transform these requirements into a clean system, we identify four core entities:
1. Player: Tracks identity and current position on the board.
2. Board: Acts as a data structure containing the "jump" mappings (Snakes/Ladders).
3. Dice: A utility for generating random movements.
4. Game: The orchestrator that manages the loop, turn transitions, and win conditions.
π§© Class Diagram
classDiagram
direction TB
class Game {
-List~Player~ players
-Board board
+start_game(num_players)
-_play_turn(player)
+check_winner(player)
}
class Board {
-dict snake
-dict ladder
+check_position(player)
}
class Player {
+string name
+int position
+roll_dice()
+move(steps)
}
Game --> Board : owns
Game --> Player : manages
βοΈ Design Patterns Applied
- Strategy Pattern: Used for the dice mechanics (easily swap a
StandardDiefor aLoadedDieorMultiDie). - State Pattern: (Implicitly) Managing the game flow through states:
Initialization,Running, andFinished. - Command Pattern: Moves are encapsulated, allowing for future "Undo" functionality or move logging.
π» Solution Implementation
The Code
from random import randint
class Player:
# Constructor
def __init__(self: Player, name: str, position: int = 0):
self.name = name
self.position = position
def roll_dice(self) -> int:
return randint(1, 6)
def move(self, steps: int) -> None:
self.position += steps
class Board:
# Constructor
def __init__(self: Board):
self.snake = {
99: 13,
78: 21,
81: 6,
45: 34,
}
self.ladder = {
5: 22,
14: 45,
60: 76,
77: 91,
44: 66,
}
def check_position(self, player: Player) -> None:
"""Check if the player's current position is a snake or ladder and update accordingly."""
if player.position in self.snake:
print(f"Oops! {player.name} landed on a snake!")
player.position = self.snake[player.position]
print(f"{player.name} slides down to position {player.position}")
elif player.position in self.ladder:
print(f"Yay! {player.name} landed on a ladder!")
player.position = self.ladder[player.position]
print(f"{player.name} climbs up to position {player.position}")
else:
print(f"{player.name} is on square {player.position}")
class Game:
# Constructor
def __init__(self: Game):
self.players: list[Player] = []
self.board = Board()
def start_game(self: Game, num_players: int) -> None:
self._initialize_players(num_players)
# Game loop
game_over: bool = False
while not game_over:
for player in self.players:
game_over = self._play_turn(player)
if game_over:
break
print("\nCurrent positions of all players:")
for p in self.players:
print(f"{p.name}: {p.position}")
def check_winner(self: Game, player: Player) -> bool:
# Check if player reached position 100
return player.position == 100
def _initialize_players(self: Game, num_players: int) -> None:
# Initialize players
for _ in range(num_players):
name = input("Enter your name: ")
self.players.append(Player(name))
def _play_turn(self: Game, player: Player) -> bool:
print(f"\n{player.name}'s turn:")
steps = player.roll_dice()
print(f"{player.name} rolled a {steps}")
if steps + player.position > 100:
print(f"{player.name} can't move, roll too high. Try again next turn!")
input("Press Enter to continue to the next turn...")
return False
player.move(steps)
self.board.check_position(player)
# Check if player won
if self.check_winner(player):
print(f"\nCongratulations {player.name}, you won the game!")
return True
input("Press Enter to continue to the next turn...")
return False
game = Game()
try:
num_players = int(input("Enter the number of players: "))
if num_players <= 0:
print("Number of players must be at least 1!")
else:
game.start_game(num_players)
except ValueError:
print("Invalid input! Please enter a valid number of players.")
π¬ Why This Works (Evaluation)
The implementation separates movement logic from board constraints. When a player moves, the Game first updates their position and then asks the Board to "validate" it. If the player lands on a snake or ladder, the Board updates the player's position internally. This decoupling ensures that the Game loop doesn't need to know why a player moved from square 14 to 45βit only cares that the move was processed.
βοΈ Trade-offs & Limitations
| Decision | Pros | Cons / Limitations |
|---|---|---|
| Dictionary-based Jumps | \(O(1)\) lookups for snakes and ladders. | Harder to implement complex "conditional" squares (e.g., move back 2 spots only on your first turn). |
| Simple Integer Position | Easy to calculate and compare. | Doesn't natively support 2D grid coordinates for UI visualization. |
Blocking input() loop |
Simple to implement for a CLI. | Cannot be easily converted to a real-time multiplayer web app without refactoring to an event-driven model. |
π€ Interview Toolkit
- Concurrency Probe: How would you handle 4 players playing simultaneously from different computers? (Focus on a central game server and state synchronization).
- Extensibility: How would you add a "Booster" square that gives an extra turn? (Implement as a special jump type that returns a flag to the
Gameloop). - Data Persistence: If the game crashes mid-way, how do we resume? (Serialize the
Gamestate, including player queue and positions, to a JSON file).
π Related Challenges
- Scalable Tic-Tac-Toe β Another grid-based game focusing on \(O(1)\) win detection.
- Elevator System β Focuses on state transitions and request queuing.