Skip to content

Memento

The Memento pattern describes how to manage the saved states of an object. Imagine a game (e.g. running on a console) that has auto-save functionality in certain places. In this game you move your character around the world. When such a save occurs, the game "needs to know", e.g. what equipment your character currently has, which tasks in the game have been completed and which are in progress, whether to know which places have been visited and which have not yet been visited to correctly display world map.

When you enter the options menu you see that there is an option "Load previous save". After selecting this option you have been moved to the place of the previous save. Let's assume that at this point you can choose the same option again and be transferred to an even earlier checkpoint.

Design

How are saved game objects stored? Well, they can be stored anywhere, depending on the functionality needed (database, separate files, RAM), but they must keep a copy of a certain state. Of course, it must be possible to create such a copy of the state and then restore it.

All the elements described above constitute the implementation of the Memento pattern, we need:

  • an object representing the state of the application (or the state of a certain part)
  • an object representing the saved state of the application
  • the ability to create a save object from the original state
  • the ability to restore the original state based on the saved
  • an object that somehow manages the saved states

When copying states, most often we have to use the deep copy. The Python assignment operator doesn't copy an object, it just creates another reference to it. We can use the copy module and its methods to make a copy:

  • copy.copy(x) - creates a shallow copy
  • copy.deepcopy(x) - makes a deep copy

Example

The example shows how to save a certain game. This state is kept in the GameState class, while the GameStateSnapshot class represents its record. The GameStateSnapshot instance is created by creating a copy of the GameState class fields. It is also possible to restore the state of the GameState class based on the value in the GameStateSnapshot instance (between the GameState and GameStateSnapshot objects it is possible to make a deep copy both ways).

The object that manages the saved states is the GameStateManager class, which allows you to save the game state and load only the previous state. For this, the stack portion of the list interface representing the stack operations (append and pop) is used.

import copy


class GameState:
    def __init__(self, health=0, mana=0, items=None):
        self._health = health
        self._mana = mana
        if items is None:
            self._items = []
        else:
            self._items = items

    def __str__(self):
        return f"GameState[health={self._health}, mana={self._mana}, items={self._items}]"

    @property
    def health(self):
        return self._health

    @property
    def mana(self):
        return self._mana

    @property
    def items(self):
        return self._items

    def heal(self):
        self._health = 100

    def take_damage(self, damage):
        self._health -= damage

    def add_item(self, item):
        self._items.append(item)

    def lose_all_items(self):
        self._items.clear()

    def restore_from_snapshot(self, snapshot):
        self._health = snapshot.health
        self._mana = snapshot.mana
        self._items = copy.deepcopy(snapshot.items)
class GameStateSnapshot:
    def __init__(self, game_state):
        self._health = game_state.health
        self._mana = game_state.mana
        self._items = copy.deepcopy(game_state.items)

    @property
    def health(self):
        return self._health

    @property
    def mana(self):
        return self._mana

    @property
    def items(self):
        return self._items
class GameStateManager:
    def __init__(self):
        self._snapshots = []

    def save_game(self, game_state):
        self._snapshots.append(GameStateSnapshot(game_state))

    def restore_previous_checkpoint(self):
        return self._snapshots.pop()
def main():
    game_state = GameState(100, 80)

    game_state_manager = GameStateManager()
    game_state_manager.save_game(game_state)
    print(game_state)

    game_state.add_item('Basic Sword')
    game_state.take_damage(10)
    game_state_manager.save_game(game_state)
    print(game_state)

    game_state.take_damage(50)
    game_state.add_item('Shield')
    game_state_manager.save_game(game_state)
    print(game_state)

    game_state_manager.restore_previous_checkpoint()
    game_state_snapshot = game_state_manager.restore_previous_checkpoint()
    game_state.restore_from_snapshot(game_state_snapshot)
    print(game_state)


if __name__ == '__main__':
    main()
przykładowy output:
    GameState[health=100, mana=80, items=[]]
    GameState[health=90, mana=80, items=['Basic Sword']]
    GameState[health=40, mana=80, items=['Basic Sword', 'Shield']]
    GameState[health=90, mana=80, items=['Basic Sword']]

NOTE: Storing multiple saved states in memory can significantly increase memory usage. In this case, it is worth considering additional use of the pattern Flyweight.