Pawns and Tiles: concerning the Skills

sapper_p2The skills are the last big component in the Pawns and Tiles game: a skill determinates a possible action of a pawn and in some way defines its role on the board.

Every pawn has a set of skills, two of them are common among pawns, “move” and “combat”.
Move from a logic point of view is not a skill, but for the application’s point of view is managed like any other skill.
Combat represents the possibility to attack an adjacent pawn.

Basically every pawn can move and make a standard melee attack.

A Skill is a simple Value Object with the following properties:

Id, Name, Mode, Type, Domain.
The meaning of Id and Name is pretty straight forward.

Mode can have one of this following values: active, passive, free

Active means the skill should be used by the player and has a cost.
Passive means the skill has an automatic trigger, it can’t be used willingly by the Player and has no cost.
Free means the skill is like an Active skill, but has no cost.

Type can have one of this values: melee, range, area, physical, move

Domain can have one of this values: physical, magical.

The list of existing skills:

Name Mode Type Domain
MOVE Active Move Physical
COMBAT Active Melee Physical
SLASH Active Melee Physical
PARRY Free Physical Physical
HOLYSLASH Active Melee Magical
HEAL Active Range Magical
ASSAULT Active Melee Physical
BLOCK Passive Physical Physical
RUNE Not implemented
RUNEMOVE Not implemented
FIREBOMB Active Area Physical
OOZEBOMB Not implemented
STAB Active Melee Physical
RUSH Active Move Physical
STRIKE Active Range Physical
DODGE Passive Range Physical

that-s-all-folks

Or at least, that’s all for the skill’s model, so where are the rules? Where is the logic? How does a skill work?

At the beginning I had two options about how to manage the skills: to give to every skill the logic to be self-consistent or to create a service to use the skills and to leave a skill like a simple skeleton of properties.

I went on the second way even if I’d never been sure.
The problem was related to the “self-consistent” assumption, that was impossible because both the solutions would have had to have a dependency on the mapService.

Because of that I preferred to  give a dependency on a service to another service instead to a model.

So in the current implementation the actionsService is responsible to apply the game rules and to verify (with the help of the mapService) if a skill is used properly and to apply it.

It has a constructor and a public method Use that is responsible to choose the correct method:

	var ActionsService = Backbone.Model.extend({
		initialize: function(mapService) {
			if(!(mapService instanceof MapService))
				throw new Error("ActionsService requires MapService as dependency");

			this._mapService = mapService;

		},

		Use:function(actionType, pawnActive, receiver, parameters) {
			this._checkIfPawnHasThisSkill(pawnActive, actionType);

			switch(actionType) {
				case Action.COMBAT:
					this._combatSkill(pawnActive, receiver);
					break;
				case Action.SLASH:
					this._slashSkill(pawnActive, receiver);
					break;
				case Action.PARRY:
					this._parrySkill(pawnActive,receiver);
					break;
				case Action.HOLYSLASH:
					this._holySlashSkill(pawnActive, receiver);
					break;
				case Action.HEAL:
					this._heal(pawnActive, receiver);
					break;
				case Action.ASSAULT:
					this._assaultSkill(pawnActive,receiver);
					break;
				case Action.BLOCK:
					//PASSIVE. A pawn with this skill should be initialized with the status DEFEND.
					break;
				case Action.RUNE:
					this._runeSkill(pawnActive,receiver);
					break;
				case Action.RUNEMOVE:
					this._runeMoveSkill(pawnActive,receiver, parameters);
					break;
				case Action.FIREBOMB:
					this._fireBomb(pawnActive, receiver, parameters);
					break;
				case Action.OOZEBOMB:
					this._oozeBomb(pawnActive, receiver, parameters);
					break;
				case Action.STAB:
					this._stab(pawnActive, receiver);
					break;
				case Action.RUSH:
					this._rush(pawnActive);
					break;
				case Action.STRIKE:
					this._strike(pawnActive, receiver, parameters);
					break;
				case Action.DODGE:
					//PASSIVE. A pawn with this skill should be initialized with the status DODGING.
					break;

			}
		},
...
...
});

Taking a look to the implementation of the Combat skill

_combatSkill: function(pawnActive, pawnPassive) {
    if(this._mapService.CheckPawnsAreAdjacent(pawnActive, pawnPassive)){
      if(pawnPassive.CheckStatus(Status.DEFEND)) {
        pawnPassive.RemoveStatus(Status.DEFEND);

        return;
      }
      pawnPassive.ReceiveDamage(1);
    }
  }

There we can see few logic relations between skills and statuses.
Depending on the Domain of a skill the interaction can be different.

In 2011 I sucked in testing, so they are not so conclusive, but at least a few of logic was in there.
In this specific case they were integration test so I didn’t mock the mapService.
I don’t remember why I didn’t create Unit test, but today I can say: better than nothing 🙂

var knight;
var pretorian;
var sniper;
var scout;
var paladin;
var runemaster;
var sapper;
var roster;
var rosterEnemy;
var mapService;
var actionsService;
var arr;
var map;
var game;
var pawnsFactory;

var StubClass = function () { };

module("An ActionsService", {
	setup: function () {
		pawnsFactory = new PawnsFactory();

		knight = pawnsFactory.CreateByClassName(PawnType.KNIGHT);
		pretorian = pawnsFactory.CreateByClassName(PawnType.PRETORIAN);
		sniper = pawnsFactory.CreateByClassName(PawnType.SNIPER);
		scout = pawnsFactory.CreateByClassName(PawnType.SCOUT);
		paladin = pawnsFactory.CreateByClassName(PawnType.PALADIN);
		sapper = pawnsFactory.CreateByClassName(PawnType.SAPPER);
		runemaster = pawnsFactory.CreateByClassName(PawnType.RUNEMASTER);

		roster = new Roster("fakeplayer");
		rosterEnemy = new Roster("fakeplayer2");

		game = new StubClass();

		roster.Add(knight);
		roster.Add(pretorian);
		roster.Add(sniper);
		roster.Add(scout);
		roster.Add(paladin);
		rosterEnemy.Add(sapper);
		rosterEnemy.Add(runemaster);

		var arr = [
			[0, -1, -1, 1], /* pretorian, scout */
			[0, -1, -1, 0], /* knight, paladin */
			[-1, 0, -1, 0], /* runemaster, sapper */
			[0, 1, 0, -1]  /* sniper */
		];

		map = new Map(arr);
		mapService = new MapService(map);
		actionsService = new ActionsService(mapService);

		knight.SetToMove();
		pretorian.SetToMove();
		sniper.SetToMove();
		scout.SetToMove();
		paladin.SetToMove();
		runemaster.SetToMove();
		sapper.SetToMove();

		knight.Move(new Coords(1, 1));
		pretorian.Move(new Coords(1, 0));
		sniper.Move(new Coords(3, 3));
		scout.Move(new Coords(2, 0));
		paladin.Move(new Coords(2, 1));
		runemaster.Move(new Coords(0, 2));
		sapper.Move(new Coords(2, 2));

	}
});

test("It should not be possible activate a skill that doesn't exist in the specific pawn", function () {
	throws(function () { actionsService.Use(Action.SLASH, sniper, roster); });
	throws(function () { actionsService.Use(Action.HEAL, knight, scout); });
});

test("It should be possible damage a pawn", function () {
	actionsService.Use(Action.COMBAT, knight, scout);
	equal(scout.GetStatistics().hp, 1);
});
test("A knight should damage all pawns adjacent to him", function () {
	actionsService.Use(Action.SLASH, knight, roster);
	equal(pretorian.GetStatistics().hp, 2);
	equal(scout.GetStatistics().hp, 1);
	equal(knight.GetStatistics().hp, 3);
	equal(sniper.GetStatistics().hp, 2);
});

test("A knight should can defend a pawn adjacent to him", function () {
	actionsService.Use(Action.PARRY, knight, scout);
	ok(scout.CheckStatus("defend"));
});

test("If a pawn has status 'defended' the first melee or range attack it receives in the turn shouldn't damage it.", function () {
	actionsService.Use(Action.PARRY, knight, scout);
	actionsService.Use(Action.PARRY, knight, scout);
	actionsService.Use(Action.COMBAT, pretorian, scout);
	equal(scout.GetStatistics().hp, 2);
	actionsService.Use(Action.COMBAT, knight, scout);
	equal(scout.GetStatistics().hp, 1);
});

test("If a pawn with a status 'defended' receive a damage, it lose the status 'defended'.", function () {
	actionsService.Use(Action.PARRY, knight, scout);
	equal(scout.CheckStatus(Status.DEFEND), true);
	scout.ReceiveDamage(1);
	equal(scout.GetStatistics().hp, 1);
	equal(scout.CheckStatus(Status.DEFEND), false);

	actionsService.Use(Action.COMBAT, knight, scout);
	equal(scout.GetStatistics().hp, 0);
});

test("An 'assault' should push and damage a pawn. If push is not possible the damage is +1.", function () {
	stub(game, 'GetRoster', function () {
		var roster = new Roster("fakeid");
		roster.Add(knight);
		roster.Add(scout);
		roster.Add(pretorian);

		return roster;
	});

	actionsService.Use(Action.ASSAULT, pretorian, knight);
	equal(knight.GetStatistics().hp, 2);
	deepEqual(knight.GetPosition(), new Coords(1, 2));

	actionsService.Use(Action.ASSAULT, pretorian, scout);
	deepEqual(scout.GetStatistics().hp, 0);
	deepEqual(scout.GetPosition(), new Coords(2, 0));
});

test("A 'holy slash' should push all enemy pawn adjacent. If push is not possible the damage is 1.", function () {
	stub(game, 'GetRoster', function () {
		var roster = new Roster("fakeid");
		roster.Add(knight);
		roster.Add(scout);
		roster.Add(pretorian);

		return roster;
	});

	actionsService.Use(Action.HOLYSLASH, paladin, roster);
	equal(pretorian.GetStatistics().hp, 2);
	equal(scout.GetStatistics().hp, 1);
	equal(knight.GetStatistics().hp, 3);
	equal(paladin.GetStatistics().hp, 3);

	deepEqual(pretorian.GetPosition(), new Coords(1, 0));
	deepEqual(scout.GetPosition(), new Coords(2, 0));
	deepEqual(knight.GetPosition(), new Coords(0, 1));
	deepEqual(paladin.GetPosition(), new Coords(2, 1));
});

test("A 'heal' should give 1 hp to a pawn. The pawn should not exceed his maximum amount of hp", function () {
	scout.ReceiveDamage(1);
	paladin.ReceiveDamage(1);

	equal(scout.GetStatistics().hp, 1);
	actionsService.Use(Action.HEAL, paladin, scout);
	equal(scout.GetStatistics().hp, 2);
	actionsService.Use(Action.HEAL, paladin, scout);
	equal(scout.GetStatistics().hp, 2);

	equal(paladin.GetStatistics().hp, 2);
	actionsService.Use(Action.HEAL, paladin, paladin);
	equal(paladin.GetStatistics().hp, 3);
	actionsService.Use(Action.HEAL, paladin, paladin);
	equal(paladin.GetStatistics().hp, 3);
});

test("A 'firebomb' should damage all pawns in the area of explosion. The area of explosion is a 3x3.", function () {
	/*
   [
   [ 0,-1,-1, 1], //  pretorian, scout
   [ 0,-1,-1, 0], // knight, paladin
   [-1, 0,-1, 0], // runemaster, sapper
   [ 0, 1, 0,-1]  // sniper
   ];
   */
	actionsService.Use(Action.FIREBOMB, sapper, new Coords(1, 1), roster);
	actionsService.Use(Action.FIREBOMB, sapper, new Coords(1, 1), rosterEnemy);

	equal(pretorian.GetStatistics().hp, 1);
	equal(scout.GetStatistics().hp, 1);
	equal(knight.GetStatistics().hp, 2);
	equal(paladin.GetStatistics().hp, 2);
	equal(runemaster.GetStatistics().hp, 1);
	equal(sapper.GetStatistics().hp, 1);
	equal(sniper.GetStatistics().hp, 2);
});

test("A 'stab' should damage an adjacent enemy pawn. If the pawn that performs the skill hasn't move yet the damage is +1", function () {
	/*
   [
   [ 0,-1,-1, 1], //  pretorian, scout
   [ 0,-1,-1, 0], // knight, paladin
   [-1, 0,-1, 0], // runemaster, sapper
   [ 0, 1, 0,-1]  // sniper
   ];
   */
	actionsService.Use(Action.STAB, scout, knight);
	actionsService.Use(Action.STAB, scout, pretorian);

	equal(pretorian.GetStatistics().hp, 2);
	equal(knight.GetStatistics().hp, 1);

	actionsService.Use(Action.STAB, scout, pretorian);
	equal(pretorian.GetStatistics().hp, 0);

	knight.Heal(2);
	equal(knight.GetStatistics().hp, 3);
	scout.Move(new Coords(0, 0));
	scout.EndMove();

	actionsService.Use(Action.STAB, scout, knight);
	equal(knight.GetStatistics().hp, 2);
});

test("A 'rush' should make the move to 10 tiles.", function () {
	/*
   [
   [ 0,-1,-1, 1], //  pretorian, scout
   [ 0,-1,-1, 0], // knight, paladin
   [-1, 0,-1, 0], // runemaster, sapper
   [ 0, 1, 0,-1]  // sniper
   ];
   */
	var newScout = pawnsFactory.CreateByClassName("Scout");
	actionsService.Use(Action.RUSH, newScout);
	equal(newScout.GetRemainingMoves(), 10);
});

test("A 'strike' should damage a non adjacent enemy pawn. " +
  "If there's a enemy pawn adjacent to the pawn the skill doesn't work. " +
  "If performed a strike as a first action of the pawn the damage is +1", function () {
  	/*
		 [
		 [ 0,-1,-1, 1], //  pretorian, scout
		 [ 0,-1,-1, 0], // knight, paladin
		 [-1, 0,-1, 0], // runemaster, sapper
		 [ 0, 1, 0,-1]  // sniper
		 ];
		 */
  	throws(function () { actionsService.Use(Action.STRIKE, sniper, knight, roster); });
  	throws(function () { actionsService.Use(Action.STRIKE, sniper, knight, rosterEnemy); });

  	sapper.Move(new Coords(1, 2));

  	actionsService.Use(Action.STRIKE, sniper, knight, rosterEnemy);

  	equal(knight.GetStatistics().hp, 1);

  	knight.Heal(2);
  	equal(knight.GetStatistics().hp, 3);
  	sniper.EndMove();

  	actionsService.Use(Action.STRIKE, sniper, knight, rosterEnemy);
  	equal(knight.GetStatistics().hp, 2);
  });

Reading these test after long time, I have to say that to be or not to be Unit Test is not the real problem.
Oh goodness they are a mess, but at least should be clear the intent.

The good thing in the idea to refactor the game using angularjs is to have the occasion to wipe out all this mess 🙂

The bad thing when the legacy code is your own code is that you don’t have anyone to complain 😦

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s