Card 04/ 08

ExampleDifficulty: Intermediate2 min

Prototype Example: Spawning Game Enemies From Templates

Configure one goblin with its stats and gear, then copy it for every goblin in the level.

A game that places many similar enemies in a level is a natural Prototype, and this example maps the template onto it. Each kind of enemy is configured once, with its health, gear and stats, and registered by name. Spawning an enemy copies the registered one and moves the copy into place.

Template participants and the classes that play them
ParticipantClass in this example
ConcretePrototypeEnemy, whose clone() uses copy.deepcopy
Prototype registryEnemyRegistry, holding the goblin and the goblin-chief
ClientThe code that calls registry.spawn
py
import copy


class Enemy:
    def __init__(self, kind: str, health: int, gear: list[str], stats: dict[str, int]) -> None:
        self.kind = kind
        self.health = health
        self.gear = gear
        self.stats = stats
        self.position = (0, 0)

    def clone(self) -> "Enemy":
        return copy.deepcopy(self)

    def __repr__(self) -> str:
        return f"{self.kind} at {self.position}: {self.health} hp, {self.gear}, {self.stats}"


class EnemyRegistry:
    def __init__(self) -> None:
        self._prototypes: dict[str, Enemy] = {}

    def register(self, name: str, prototype: Enemy) -> None:
        self._prototypes[name] = prototype

    def spawn(self, name: str, position: tuple[int, int]) -> Enemy:
        enemy = self._prototypes[name].clone()
        enemy.position = position
        return enemy


registry = EnemyRegistry()
registry.register("goblin", Enemy("goblin", 30, ["club"], {"attack": 4, "defence": 2}))
registry.register("goblin-chief", Enemy("goblin", 60, ["axe", "shield"], {"attack": 8, "defence": 5}))

first = registry.spawn("goblin", (3, 7))
second = registry.spawn("goblin", (9, 2))
chief = registry.spawn("goblin-chief", (5, 5))
first.gear.append("torch")
first.stats["attack"] += 1

print(first)
print(second)
print(chief)
OutputRan with python:3.12-slim (Python 3.12.13)
goblin at (3, 7): 30 hp, ['club', 'torch'], {'attack': 5, 'defence': 2}
goblin at (9, 2): 30 hp, ['club'], {'attack': 4, 'defence': 2}
goblin at (5, 5): 60 hp, ['axe', 'shield'], {'attack': 8, 'defence': 5}
Two kinds are registered as configured objects, and every spawn is a copy.

The first goblin picks up a torch and gains a point of attack, and the second goblin, spawned from the same prototype, has neither: each copy has its own gear list and stats dictionary. The goblin chief is the same class with different data. A new kind of enemy is one more call to register, which is how games load enemy types from files rather than writing a class for each.

No Prototype interface appears, because Python checks that clone exists when it is called rather than when the code is written. The roles that matter remain: configured objects that copy themselves, and a registry the client asks by name.