Card 05/ 08
Memento Example: Save Points in a Game
The game saves level, health and inventory at a checkpoint, and the save menu only lists them.
A game's save points are mementos kept by a caretaker that holds many at once and lets the player choose. This card shows the game, which saves and loads itself, and the save menu, which lists the saves by name and time without being able to read the level, health or inventory inside them. It is written in TypeScript.
| Participant | Game class |
|---|---|
| Originator | Game, which holds the level, health and inventory |
| Memento | SavePoint, which shows only a name and a time |
| Caretaker | SaveMenu, which lists save points and returns the one the player picks |
type SaveData = { readonly level: number; readonly health: number; readonly inventory: readonly string[] };
class SavePoint {
constructor(readonly name: string, readonly createdAt: string) {}
}
class Game {
#saves = new WeakMap<SavePoint, SaveData>();
private level = 1;
private health = 100;
private inventory: string[] = [];
play(level: number, health: number, found: string): void {
this.level = level;
this.health = health;
this.inventory.push(found);
this.status("played");
}
save(name: string, createdAt: string): SavePoint {
const point = new SavePoint(name, createdAt);
this.#saves.set(point, { level: this.level, health: this.health, inventory: [...this.inventory] });
return point;
}
load(point: SavePoint): void {
const data = this.#saves.get(point);
if (!data) {
return;
}
this.level = data.level;
this.health = data.health;
this.inventory = [...data.inventory];
this.status(`loaded "${point.name}"`);
}
private status(event: string): void {
console.log(`${event}: level ${this.level}, health ${this.health}, items [${this.inventory.join(", ")}]`);
}
}
class SaveMenu {
private readonly points: SavePoint[] = [];
add(point: SavePoint): void {
this.points.push(point);
}
list(): void {
for (const point of this.points) {
console.log(` ${point.createdAt} ${point.name}`);
}
}
pick(name: string): SavePoint | undefined {
return this.points.find((point) => point.name === name);
}
}
const game = new Game();
const menu = new SaveMenu();
game.play(2, 80, "key");
menu.add(game.save("Before the bridge", "10:05"));
game.play(3, 35, "map");
menu.add(game.save("Castle gate", "10:40"));
game.play(4, 0, "torch");
console.log("save menu:");
menu.list();
const chosen = menu.pick("Before the bridge");
if (chosen) {
game.load(chosen);
}played: level 2, health 80, items [key]
played: level 3, health 35, items [key, map]
played: level 4, health 0, items [key, map, torch]
save menu:
10:05 Before the bridge
10:40 Castle gate
loaded "Before the bridge": level 2, health 80, items [key]The player saves before the bridge and at the castle gate, then loses all their health on level 4. The menu lists both saves by time and name, and loading "Before the bridge" puts the game back on level 2 with 80 health and only the key.
Two things differ from a plain undo history. The caretaker keeps every memento and lets the user choose one, rather than returning the last. And the memento carries a public part, the name and time the menu shows, beside a private part only the game can read. Both share the core of the pattern: only the originator reads the saved state.
The inventory is copied on save and again on load. Without the copies, items picked up after a save would appear in it, because the saved list and the game's list would be one object.
← → move through sections and cards