Wizards and warriors, part one

A common problem I see in object-oriented design is:

  • 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.

But before we get into the details, I just want to point out that I am not really talking about anything specific to the fantasy RPG genre here. Everything in this series applies equally well to Papers and Paychecks, but wizards and warriors are more fun to write about, so there you go.

OK, great, we have five bullet points so let’s write some classes without thinking about it! What could possibly go wrong?

abstract class Weapon { }
sealed class Staff : Weapon { }
sealed class Sword : Weapon { }
abstract class Player 
{ 
  public Weapon Weapon { get; set; }
}
sealed class Wizard : Player { }
sealed class Warrior : Player { }

Designing good class hierarchies is all about capturing the semantics of the business domain in the type system, right? And we’ve done a great job here. If there is behavior common to all players, that goes in the abstract base class. If there is behavior unique to wizards or warriors, that can go in the derived classes. Clearly we’re on track for success here.

Right until we add…

  • A warrior can only use a sword.
  • A wizard can only use a staff.

What an unexpected development!

(As I have often pointed out, foreshadowing is the sign of a quality blog.)

Now what do we do? Readers familiar with type theory will know that the highfalutin name for the problem is that we’re in violation of the Liskov Substitution Principle. But we don’t need to understand the underlying type theory to see what’s going horribly wrong. All we have to do is try to modify the code to support these new criteria.

Attempt #1

abstract class Player 
{ 
  public abstract Weapon Weapon { get; set; }
}
sealed class Wizard : Player
{
  public override Staff Weapon { get; set; }
}

Nope, that’s illegal in C#. An overriding member must match the signature (and return type) of the overridden member.

Attempt #2

abstract class Player 
{ 
  public abstract Weapon { get; set; }
}
sealed class Wizard : Player
{
  private Staff weapon;
  public override Weapon Weapon 
  {
     get { return weapon; }
     set { weapon = (Staff) value; }
  } 
}

Now we’ve turned violations of the rule into runtime exceptions. This is incredibly error-prone; a caller could easily have a Wizard in hand and assign a Sword to Weapon. The whole point of capturing this thing in the type system is that the violation gets discovered at compile time.

Next time on FAIC: what other techniques do we have to try to represent this rule in the type system?

108 thoughts on “Wizards and warriors, part one

  1. Pingback: Использование Strategy Pattern в случае, если конкретные стратегии зависят от типа использующего их объекта - c# разработка-игр проектирование

    • I seal every class unless I have specifically designed it to be extensible. It’s easy enough to un-seal a class if you need to, but it’s hard to seal a class when you discover a security defect in your app because someone is providing their own implementation of one of your classes.

      • That makes sense. The one problem is that it makes it more annoying for external extensions. (Although your code *might* not be able to handle them anyways.)

  2. That will be little off topic. I understand that this is only an example but such examples always make my think why…

    Why the player would have a single property called weapon? What if the player have one weapon in right hand and some other in left hand? Or what if a player is some kind of mutant with four hands, carrying a blade in each one, or it has a tail that is used to hold a weapon… Why to restrict a player to having only single or any fixed amount of weapons? Finally why to even make property such as “weapon” or even “weapons” in class such as “Player”? OK, the player may carry a weapon but it’t its not an inherent property of a such broad concept as player to carry a weapon. If you know we talk about some RPG game than you may not be surprised by that, but the concept of player is not exclusively bound to RPG games. Such approach make things much more simpler in first place, but we can see it can cause much more problems in very near future.

    Another thing that I really don’t understand is why developers tend to make such illogical constrains as “wizzard can’t use a sword” or “you need minimum strength of 100 to carry this weapon” so if you have 99 your muscles are to weak to even pick up the weapon and an at 100 you suddenly can hit with full power of it. My favorite laugh of this approach https://www.youtube.com/watch?v=y5C3itd89yY 🙂

    You just need to crate list of abilities for the player where you can specify how well it can handle particular type of weapon, decrease effectiveness of sword usage for the Wizard and such problem magically disappears, plus you have much more natural game mechanic.

    • Though you make good arguments about game mechanics, the point of this series of articles was not really about how to design good games. It was about how you get in trouble when you try to put restrictions of your business domain into the type system of a language; the type system is not designed to represent the same sorts of restrictions that a business domain has. I chose wizards and swords just because it was fun, not because I am particularly interested in solving the problem for that domain.

  3. Pingback: What’s the difference between “as” and cast operators? | Fabulous adventures in coding

  4. Pingback: Polymorphism on Steroids – Dive Into Multiple Dispatch (Multimethods) - Vasil Kosturski

  5. Pingback: Choosing Allowed Methods From Abstract Class - Programming Questions And Solutions Blog

Leave a comment