Wizards and Warrors

Started by
23 comments, last by LorenzoGatti 6 years, 1 month ago

I came across this blog by Eric Lippert, in it he describes a common problem:

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

With the following conditions:

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

To solve the problem he suggests the implementation of a Rule class that enforces the above rules.  Quote:

Quote

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?

I attempted to try this, but I ran into a problem.

public final class GameRules {

    public static boolean verifyifwizardcancarry(Weapon weapon){
        boolean canCarry  = false
        if weapon is a a staff set canCarry to true
        return canCarry;
    }
 }
public abstract class Player{   

   private List<Weapon> weapons;
   // constructor and OOP goodness left out

   public abstract void add(Weapon weapon);

}

public final class Wizard extends Player{

   @Override 
   public void add(Weapon weapon){

      if(GameRules.verifyifwizardcancarry(weapon){
          // - code to add weapon to inventory
      }
    }
 }

How can I verify that the Weapon being passed to the GameRules class is in fact a Sword? From reading, the use of instanceof or getClass() is considered code smell.

You could trust the developer of the Sword class to declare a parameter that states it's a Sword, for example, an enum, WeaponType.Sword, and Sword could implement a method called verify and you pass that enum in and it returns true if it's a sword, false if it isn't or call a getter that returns the enum value.

Using what is stated in the blog, how would you solve this in a reliable way?

Advertisement

Having a separate class just to hold functions that verify whether something can happen seems kind of overkill. Usually the rule when designing things is to figure out who needs to know about the rules. How you want to do this depends a lot on the style you want to follow.

Is a wizard a player or is a player just a player and has an archetype class that happens to be wizard? Somewhere you need to define which weapons are okay to equip and which are not. That's the one part you can't get around, the program can't know without you defining it somewhere. But there are literally a million ways to do that, some are more simple and some are more complex but offer greater flexibility. I usually opt for the minimalist approach unless I have a good reason not to.

The inventory could handle that, it might let you add items and equip them and then it has a reference to the player that owns it, then it can grab the player's archetype class and see if the weapon is something it can equip. The player class could also do that by looking itself if it didn't have a separate inventory class, or it could simply have a list of weapon types it can equip and you can populate that by using a factory method to create a player with the properties of a wizard or a warrior. Literally dozens of ways to accomplish the same thing.

In terms of the comparison there isn't really getting around using an enum or something similar. Either the weapon or the thing adding the weapon needs to know if it can be added/equipped, and the only way to do that is to compare it to something known ahead of time and determine if it is true or false. Your options are rather limited in that area, if you don't compare against a weapon type then you'll have to compare against the item i'd or if it implements an interface or something else. These all equate to identifying what the thing is.

In your weapon class make a OnEquip function and make a List to hold who can equip this type of item.

So your Staff Object will have a wizard in the list, if there isn't many types then you don't need a list. Your OnEquip function checks that the person trying to equip the item is of the right class.

A Unity example feel free to adapt it, open the spoilers, the code looks like this:

The Player:

Spoiler

 



using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerClasses : MonoBehaviour {

	public float Health;
	public float Mana;

	public string Type;
    //We don't want the weapon to be set without checking 
	Weapon EquipedWeapon = null;

	public PlayerClasses(){
		Health = 100f;
		Mana = 100f;

		Type = "Townsfolk";
	}//Constructor 0

    //Here starts the weapon script
	public void EquipNewWeapon(Weapon WeaponToEquip){
		EquipedWeapon = WeaponToEquip.OnPickUp(this);//tells the object it's this person who wants to wield it
	}
    //So we can see what the weapon is 
	public void WhatIsMyWeapon(){
		if (EquipedWeapon == null) {
			print ("Unarmed");
		} else {
			print (EquipedWeapon.Name);
		}
	}
}

 

 

 

Wizard, Soldier, Thief:

Spoiler



public class Wizard : PlayerClasses {
	public Wizard(){
		Health = 100f;
		Mana = 200f;

		Type = "Wizard";
	}
}

public class Soldier : PlayerClasses {
	public Soldier(){
		Health = 200f;
		Mana = 100f;

		Type = "Soldier";
	}
}

public class Thief : PlayerClasses {
	public Thief(){
		Health = 150f;
		Mana = 150f;

		Type = "Thief";
	}
}

 

And last the weapons:

Spoiler


using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Weapon {

    public string Name ;
    public string Type ;//Lets just use a string for now

    public int Damage;
    public int Value;

    public string[] ListOfUsers;

    public Weapon(string NameOfWeapon, string TypeOfDamage, int AmountOfDamage, int GoldValue, string[] WhoCanUseThis){
        Name = NameOfWeapon;
        Type = TypeOfDamage;
        Damage = AmountOfDamage;
        Value = GoldValue;
        ListOfUsers = WhoCanUseThis;
    }//constructor 0

    public Weapon OnPickUp(PlayerClasses InWielder){
        
        foreach(string AllowedUsers in ListOfUsers){
            if (InWielder.Type == AllowedUsers) {
                return this;//Returns this weapon to the wielder if they are allowed to use it.
            }
        }
        //Failed to find the wielder on the list
        return null;
    }
}

 

 

This is how you would use it:


using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class UseWeaponSystem : MonoBehaviour {

	PlayerClasses Player1;

	void Start () {
		Player1 = new Wizard();

		//Some weapons
		Weapon ItemA = new Weapon("Novice Staff","Magic",8,10,new string[1]{"Wizard"});

		Weapon ItemB = new Weapon("Dagger","Melee",12,15,new string[2]{"Soldier","Thief"});

		Player1.EquipNewWeapon (ItemB);
		Player1.WhatIsMyWeapon (); //Prints "Unarmed"

		Player1.EquipNewWeapon (ItemA);
		Player1.WhatIsMyWeapon (); //Prints "Novice Staff"
	}

}

 

The idea here is that the weapon system will see if the player is worthy of wielding it. If it isn't the weapon doesn't accept the wielder.

This is my prefered way from experience. The player class gets flooded with so many functions that keeping track is hard, so instead I like having the items solve them self.

@Scouting Ninja, why do you have Wizard, Soldier, and Thief classes? They're functionally identical to the PlayerClasses class, just different data. They're objects, not classes. 

That's the whole point of Lipperts article. Don't try to encode things in the type system that don't need to be encoded in the type system.

if you think programming is like sex, you probably haven't done much of either.-------------- - capn_midnight
1 hour ago, ChaosEngine said:

That's the whole point of Lipperts article. Don't try to encode things in the type system that don't need to be encoded in the type system.

I didn't check the article first, I was busy at the time I responded.

1.)As a amateur programmer I have to ask why not? Honestly I am very inexperienced in this and would like to know why not.

2.)Why do so many people say classes are overkill when I find they don't cost much resources and solve most problems in a stable way? They are also very easy to use and keep track of.

 

Lets take my code as an example and make it all a instance of the first PlayerClass:

Spoiler




public class PlayerClasses : MonoBehaviour {

	public float Health;
	public float Mana;

	public string Type;
    //We don't want the weapon to be set without checking 
	Weapon EquipedWeapon = null;

	public PlayerClasses(float InHealth, float InMana, String InType){
		Health = InHealth;
		Mana = InMana;

		Type = "InType";
	}//Constructor 0

    //Here starts the weapon script
	public void EquipNewWeapon(Weapon WeaponToEquip){
		EquipedWeapon = WeaponToEquip.OnPickUp(this);//tells the object it's this person who wants to wield it
	}
    //So we can see what the weapon is 
	public void WhatIsMyWeapon(){
		if (EquipedWeapon == null) {
			print ("Unarmed");
		} else {
			print (EquipedWeapon.Name);
		}
	}


 

The new setup would look like this:


PlayerClass Wizard = new PlayerClass(100f,200f,"Wizard");
PlayerClass Soldier = new PlayerClass(200f,100f,"Soldier");
PlayerClass Thief = new PlayerClass(150,150,"Thief");

However it has a huge problem, what if I wanted to give each of these a unique function? Say we want the Wizard to cast a Shield, the Soldier to use Warcry and the Thief should steal.

Do we now make 3 If() branches checking the types, because that way we don't need classes? What if we want lot's of abilities for the characters?

My response would be to either make a new class holding abilities and the rules for them or, use what I had:

Spoiler




public class Wizard : PlayerClasses {
	public Wizard(){
		Health = 100f;
		Mana = 200f;

		Type = "Wizard";
	}
  
  void CastShield(){
    //Shield code here
  }
}

public class Soldier : PlayerClasses {
	public Soldier(){
		Health = 200f;
		Mana = 100f;

		Type = "Soldier";
	}
  void WarCry(){
    //War cry code
  }
}

public class Thief : PlayerClasses {
	public Thief(){
		Health = 150f;
		Mana = 150f;

		Type = "Thief";
	}
  void Steal(){
    //Code for stealing
  }
}

 

 

22 hours ago, QueenSvetlana said:
  • 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.

This is just miscategorisation of the various types, but it's not uncommon for folks to structure OOP hierarchies in such ways.

If instead we said that:

  • A wizard is a magical_weapon_user.
  • A warrior is a sharp_pointy_thing_user.
  • A staff is a magical_weapon.
  • A sword is a sharp_pointy_thing.
  • A magical_weapon_user has a magical_weapon.
  • A sharp_point_thing_user has a sharp_pointy_thing.

The we don't have a problem any more. Via suitable use of multiple interfaces, you can (almost) always restructure hierarchies so that they make sense.

Tristam MacDonald. Ex-BigTech Software Engineer. Future farmer. [https://trist.am]

Exactly, writing down a flawed inheritance tree doesn't imply a working solution doesn't exist.

15 hours ago, Scouting Ninja said:

As a amateur programmer I have to ask why not? Honestly I am very inexperienced in this and would like to know why not.

Read the article. It will explain in greater detail than I can here.

 

15 hours ago, Scouting Ninja said:

Why do so many people say classes are overkill when I find they don't cost much resources and solve most problems in a stable way? They are also very easy to use and keep track of.

Those people are, IMHO, wrong. Classes are not "overkill" and OOP is not "bad". You can write bad OOP and design the wrong classes, but that's a flaw in the way you write code, not in the paradigm itself. Identify the correct problem space and solve it within the constraints you have to work with, e.g. performance, etc.

15 hours ago, Scouting Ninja said:

However it has a huge problem, what if I wanted to give each of these a unique function? Say we want the Wizard to cast a Shield, the Soldier to use Warcry and the Thief should steal.

That's fine, but it's not part of the problem description. Even then, be very wary of any kind of solution that relies on "magic strings".  At the very least, use the nameof operator.

 

14 hours ago, swiftcoder said:

If instead we said that:

  • A wizard is a magical_weapon_user.
  • A warrior is a sharp_pointy_thing_user.
  • A staff is a magical_weapon.
  • A sword is a sharp_pointy_thing.
  • A magical_weapon_user has a magical_weapon.
  • A sharp_point_thing_user has a sharp_pointy_thing.

The we don't have a problem any more. Via suitable use of multiple interfaces, you can (almost) always restructure hierarchies so that they make sense.

I don't see how that solves any of the problems outlined in the article? You've basically disconnected player types and weapon types into separate inheritance trees. That's fine, but I'm not even sure why you'd bother with inheritance at all.

Part of the point of the article was the ability to write code like 


var players = new List<Player>();
players.Add(new Wizard
{
   Weapon = new Staff()
});
players.Add(new Warrior
{
   Weapon = new Sword()
});

Monster w = new Werewolf();
foreach(var p in players)
{
   p.Attack(w);  // expect some kind of multiple dispatch here
}

Now, you can argue that's a naive way to write code (and I'd agree with you), and no-one would (or more accurately should) design a game that way, but Lippert isn't really coming at this from a gamedev POV. He's really talking more about general software design and just using this as an example and this kind of thing is incredibly common.

 

if you think programming is like sex, you probably haven't done much of either.-------------- - capn_midnight
33 minutes ago, ChaosEngine said:

I don't see how that solves any of the problems outlined in the article?

I didn't read the article. This was in response to the OP's statement that you couldn't encode the specified conditions via the type system.

35 minutes ago, ChaosEngine said:

That's fine, but I'm not even sure why you'd bother with inheritance at all.

If you want to enforce constraints in a type system that only gives you lemons... you make lemonade. Entirely agreed that OO is the wrong way to approach this problem, but since the discussion was about OO, I elected not to derail it :)

Now that I have read the article (and all the rest of the series, because Mr Lippert neglected to provide a conclusion). I'm not sure that the article has much of a point. It's basically a strawman attack against OO on the basis that "single inheritance == bad", which, yes, no argument there. But the introduction of multiple interface hierarchies could have solved all of his stated issues, and it's not clear that Mr Lippert ever realised that in the course of writing the series.

Tristam MacDonald. Ex-BigTech Software Engineer. Future farmer. [https://trist.am]

Hi there,

I am not a really experienced programmer, so tell me if I am wrong, but, can't we use the 'is' key word to simply check if the weapon that we are using is a sword or a staff?

Like this:

image.png.acb7a2729572aac027e6d91931a1b47f.png

TheSpire

This topic is closed to new replies.

Advertisement