Notes and exercises for learning design patterns
We continue with the stat system from Exercise 3. The base code — StatQuery, StatBroker, Character, FlatBonusModifier, MultiplierModifier — is already in place.
This exercise adds two real-world requirements: modifiers that clean themselves up automatically, and modifiers that react to live character state.
Make modifiers work as context managers so temporary effects are guaranteed to clean up, even if an exception is raised.
Implement a ScopedModifier base class (or mixin) that adds __enter__ and __exit__:
with FlatBonusModifier(broker, "hero", "attack", 20) as _:
print(hero.get_attack()) # 30 (base 10 + bonus 20)
print(hero.get_attack()) # 10 — modifier removed on exit
Both FlatBonusModifier and MultiplierModifier should support the context manager protocol.
Verify that the modifier is removed even when the with block raises an exception:
try:
with FlatBonusModifier(broker, "hero", "attack", 20):
raise RuntimeError("something went wrong")
except RuntimeError:
pass
assert hero.get_attack() == 10 # still cleaned up
Add a ConditionalModifier that only applies its effect when a runtime condition is true.
ConditionalModifier(
broker,
character_name="hero",
stat="defense",
bonus=15,
condition=lambda: hero.health < hero.max_health * 0.25,
)
This modifier should add +15 defense, but only when the condition returns True at the moment the query fires — not at registration time.
Verify:
hero.health = 100
assert hero.get_defense() == 5 # condition false, no bonus
hero.health = 20 # below 25% of 100
assert hero.get_defense() == 20 # condition true, bonus applied
Wire a scenario with two characters sharing one broker. Use all four modifier types:
hero has a sword (FlatBonusModifier, permanent)hero has a Last Stand effect (ConditionalModifier, activates at low health)villain gets a temporary berserk buff (MultiplierModifier, scoped)villain has armor (FlatBonusModifier, permanent)Walk through a sequence of game events and assert the stats at each step:
with block) — verify villain’s boosted attack.with block) — verify villain’s attack is back to normal.See exercise4.py.
ScopedModifier.__enter__ should return self. __exit__ should call self.remove() and return False (don’t suppress exceptions).ConditionalModifier._handle should call self._condition() at handle time, not at registration time. The lambda captures a reference to hero, so it always reads the current value of hero.health.with block for the villain’s berserk should contain all the assertions that depend on the buff being active.