Article 8V0Y Wizards and warriors, part five

Wizards and warriors, part five

by
ericlippert
from Fabulous adventures in coding on (#8V0Y)

We've been struggling in the last four episodes to encode the rules of our business domain - which, recall, could be wizards and warriors or papers and paycheques or whatever - into the C# type system. The tool we've chosen seems to be resisting our attempts, and so maybe it's a good time to take a step back and ask if we're on the right track in the first place.

62103645.jpg?w=300&h=182

The fundamental idea in the first and second episodes was use the type system to detect and prevent violations of the rules of the business domain at compile time. That effort has largely failed, due to the difficulty of representing a subtype with a restriction, like "a Wizard is a Player that cannot use a Sword. In several of our attempts we ended up throwing exceptions, so that the rule was enforced by the runtime rather than the compiler. What is the nature of this exception?

I classify exceptions as fatal, boneheaded, vexing and exogenous. Plainly the exception here is neither fatal nor exogenous. I can't advocate for creating vexing exceptions - that is, wrapping every call that sets the Weapon of a Player with a try-catch that catches the entirely-expected exception.

If it is boneheaded then there must be a way for the caller that is trying to set the Weapon of a Player to know that they are about to do something illegal so that they can avoid it. There are two ways to do that.

First, the caller could to know all the rules of the system to make sure that they're not giving a Sword to a Wizard - and now we are encoding the rules of the business domain in many places, which isn't DRY. And it puts a high burden on the developer, which leads to exactly the correctness problems we are attempting to avoid.

Second, Player could provide a TryChangingWeapon method that returns a bool instead of throwing, and then the caller has to deal with the resulting failure case.

No matter how we slice it, if the compiler can't prevent the rules violation then somehow the code has to manage "I tried to do something illegal and failed" at runtime.

Any time I think about dealing with failure at runtime I ask myself "is the failure condition truly exceptional?" What if we said, hold on a minute, it is not exceptional to want a wizard to wield a sword. Doing so might be disallowed by our policies about allowable weapons, but it is not exceptional to make the attempt.

I thought about this a lot when designing the semantic analyzer for Roslyn. We could have used exceptions as our model for reporting compiler errors, but we rejected that immediately. When you're writing code in the IDE, it is correct code that is the exception! Code as you're typing it is almost always wrong; the business domain of the analyzer is dealing with incorrect code and analyzing it for IntelliSense purposes. The last thing we wanted to do was to make it impossible in the type system to represent incorrect C# programs.

In the third and fourth episodes of this series, we saw that it was also difficult to figure out first, how to invoke the right code to handle various specific rules, and second, where to put that code. Even leaving aside the problems with the highly verbose and complex Visitor Pattern, and the dangerous dynamic invocation pattern, we've still got the fundamental problem: why is resolving "a Paladin in the Church attacks a Werewolf with a Sword" a concern of any one of those types, over any other? Why should that code go in the Paladin class as opposed to, say, the Sword class?
lcsof.jpg?w=300&h=240
The fundamental problem is my initial assumption that the business rules of the system are to be expressed by writing code inside methods associated with classes in the business domain - the wizards and daggers and vampires. We keep on talking about "rules", and so apparently the business domain of this program includes something called a "rule", and those rules interact with all the other objects in the business domain. So then should "rule" be a class? I don't see why not! It is what the program is fundamentally about. It seems likely that there could be hundreds or thousands of these rules, and that they could change over time, so it seems reasonable to encode them as classes.

Once we realize that "rule" needs to be a class, suddenly it becomes clear that starting our design with

  • A wizard is a kind of player.
  • A warrior is a kind of player.
  • A staff is a kind of weapon.
  • A sword is a kind of weapon.
  • A player has a weapon.

completely misses the actual business of the program, which is maintaining consistent state in the face of user attempts to edit that state. It would have been better to start with:

  • The fundamental concerns of the program are users, commands, game state, and rules.
  • A user provides a sequence of commands.
  • A command is evaluated in the context of the rules and current game state, and produces an effect.

What are some things we know about effects?

  • Doing nothing is an effect.
  • Mutation of game state is an effect.
  • Playing a sound is an effect.
  • Sequential composition of any number of effects is an effect.
  • "

And what are some of the things we know about rules?

  • Rules determine the effects that result from a particular player taking a particular action; an action may involve arbitrarily many other game elements.
  • Some rules describe universally applicable state invariants that must never be violated.
  • Some rules describe "default" handling of commands; the actions of these rules can be modified by other rules.
  • Some rules weaken other rules by causing the other rule to not apply to a specific situation.
  • Some rules strengthen other rules by adding additional restrictions.
  • "

Now all our previous problems fade away. A player has a weapon, great, that's fine, we'll make a Player class with a property of type Weapon. That code makes no attempt to try to represent that a wizard can only wield a staff or a dagger; all that code does is keep track of game state, because state is its concern.

Then we make a Command object called Wield that takes two game state objects, a Player and a Weapon. When the user issues a command to the system "this wizard should wield that sword", then that command is evaluated in the context of a set of Rules, which produces a sequence of Effects. We have one Rule that says that when a player attempts to wield a weapon, the effect is that the existing weapon, if there is one, is dropped and the new weapon becomes the player's weapon. We have another rule that strengthens the first rule, that says that the first rule's effects do not apply when a wizard tries to wield a sword. The effects for that situation are "make a sad trombone sound, the user loses their action for this turn, no game state is mutated". When a user issues a command "this paladin should attack that werewolf" then again, the relevant rule objects are consulted in the context of the game state (namely, the paladin is wielding a sword and standing in a church), and the effects are produced (make the sword glow, the werewolf is destroyed, add ten points to Griffindor, whatever.)

What problems have we solved?

We no longer have the problem of trying to fit "a wizard can only use a staff or dagger" into the type system of the C# language. We have no reason to believe that the C# type system was designed to have sufficient generality to encode the rules of Dungeons & Dragons, so why are we even trying?

We have solved the problem of "where does the code go that expresses rules of the system?" It goes in objects that represent the rules of the system, not in the objects that represent the game state; the concern of the state objects is keeping their state consistent, not in evaluating the rules of the game.

And we have solved - or rather, sketched a solution for solving - the problem of "how do you figure out which rules apply in a given situation?" Again, we have no reason to suppose that the rules of overload resolution and the rules of Dungeons & Dragons attack resolution have anything in common; if we're building the latter, then we need to design a system that correctly chooses the valid rules out of a database of rules, and composes the effects of those rules sensibly. Yes, you need to build your own resolution logic, but resolving these rules is the concern of the program, so of course you're going to have to write code to do it.

And what new scenarios have we enabled? Rules are now more like data than like code, and that is powerful!

  • We can persist rules into a database, so that rules can be changed over time without writing new code. And we get all the nice benefits of a database, like being able to roll back to previous verions if something goes wrong.
  • We can write a little Domain Specific Language that encodes rules as human-readable text.
  • We can do experiments, trying out tweaks to the rules without recompiling the program.
  • We saw in a previous episode that it might be hard to know which of several rules to choose from, or how to combine the effects when multiple rules apply. We can write test engines that try billions of possible scenarios and see if we ever run into a situation where the choice of applicable rules becomes ambiguous or violates a game invariant, and so on.

This kind of system, where rules are data, not code, seems like it would be quite a bit more heavyweight than just encoding the rules in C# and its type system, but it is also more flexible. I say that when the business domain of the program actually is evaluating complex rules and determining their actions, and particularly when those rules are likely to change over time more rapidly than the program itself changes, then it makes a lot of sense to make rules first-class objects in the program itself.

One of those points was to make a DSL that represents the rules. In fact there are DSLs where these sorts of rules are first-class elements in the language itself. A number of commenters have mentioned Inform7, a brilliant programming language for writing interactive fiction (aka "text adventures"). This series of posts was inspired in part by how Inform7 handles this problem. As some commenters noted in a previous episode, Inform7 lets you write code like this to solve our first problem (somewhat abridged from the original):

A wizard is a kind of person.A warrior is a kind of person.A weapon is a kind of thing.A dagger is a kind of weapon.A sword is a kind of weapon.A staff is a kind of weapon.Wielding is a thing based rulebook. The wielding rules have outcomes allow it (success), it is too heavy (failure), it is too magical (failure).The wielder is a person that varies.To consult the rulebook for (C - a person) wielding (W - a weapon): now the wielder is C; follow the wielding rules for W.Wielding a sword: if the wielder is not a warrior, it is too heavy.Wielding a staff: if the wielder is not a wizard, it is too magical.Wielding a dagger: allow it.Instead of giving a weapon (called W) to someone (called C): consult the rulebook for C wielding W; if the rule failed: let the outcome text be "[outcome of the rulebook]" in sentence case; say "[C] declines. '[outcome text].'"; otherwise: now C carries W; say "[C] gladly accepts [the W]."

And you can write rules which modify other rules like this:

Rule for attacking a werewolf when the time is after midnight: decrease the chance of success by 20.Rule for attacking a werewolf which is not the Werewolf King when the player is a paladin and the player wields the Holy Moon Sword: increase the attack power by 8.

There is no need to figure out "which class does the rule about paladins vs werewolves go in?" The rule goes in a rulebook, end of story. Like I said, Inform7 is amazing.

I started this series by saying "let's write some classes without thinking"; the moral of the story here is of course: think about what the primary concern of your program really is before you start writing it. The classic paradigm for OOP still makes perfect sense: encode the fundamental, unchanging relationships between business elements into the type system. Here the fundamental unchanging relationships are things like "commands are evaluated in the context of state and rules, to produce a sequence of actions", so that's where the design should have started in the first place.


2688 b.gif?host=ericlippert.com&blog=67759120
External Content
Source RSS or Atom Feed
Feed Location http://ericlippert.com/feed
Feed Title Fabulous adventures in coding
Feed Link https://ericlippert.com/
Reply 0 comments