Pawns and Tiles: concerning the Tiles

sapper_p2The Tiles are central in the game exactly like the Pawns. Naturally refering to Tiles I’m talking about the map in general, the board where our little pawns have their conflict.

I think that to define a map, or in general the level design, is one of the big deal projecting a game. The main Character (or units depending on the game) is important, the skills are important, but if the level design sucks all the experience sucks no matter what.

For me Super Mario Galaxy is a master piece because of its level design.

So I was sure that with designing bad maps the whole game would have been a disaster.

I decided to keep things simple and to introduce more possibilites after a gameplay test.

The dimension was one the first problem. Because Pawns & Tiles is a turn-based game, the map had to be big enough to allow a complex strategy and to grant that every turn can be meaningful, not too big to need too many turns before to have your pawns close to a goal area or in general in the centre of the action, but again big enough to allow to counter an opponent move.

The fastest pawn moves 5 tile, the slowest 4, so we decided to try a map 25×16.

The map is a simple bidimensional array and creating a map we can specify few tipology of tile:

0: free tile a Pawns can walk on and through it.
1: Wall. Nothing can stay or go through it.
2: start tile for pawns of the first army. It has the same rule of a free tile, but in this area you are allows, as army 1, to spawn your pawns.
3: start tile for pawns of the second army. It has the same rule of a free tile, but in this area you are allows, as army 2, to spawn your pawns.
4: goal area. If you keep a pawn in this type of tile you gain points at the begin of your turn for every pawn on a goal area.
8: hole. you can’t step in, but you can overfly it.

-1: it represents a pawn, you can’t stay in the same tile occupied by another pawn nor pass through it.

Even if we decided to start with few simple tiles we defined more, but no maps now are implementing it:

6: trap. it makes one damage to every pawn that steps into it.
7: slow. The cost of move is increased by 1.

I and Giuseppe designed a first map to try the game rules, look and field and the rythm.

var map = [
  [2,2,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,1,1,1,1],
  [2,2,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,1,1,1,1],
  [2,2,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,1,1,1,1,1,1],
  [2,2,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,1,1,1,1,1,1],
  [2,2,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,3,3],
  [2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,3],
  [2,2,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,3,3],
  [2,2,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,3,3],
  [2,2,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,3,3],
  [2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,3],
  [2,2,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,3,3],
  [2,2,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,3,3],
  [2,2,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,1,1,1,1],
  [2,2,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,1,1,1,1],
  [2,2,0,0,0,0,0,0,0,0,0,4,4,0,0,0,0,0,0,0,0,1,1,1,1],
  [2,2,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,1,1,1,1]
]

Here the graphical representation made by Giuseppe.

map_test

Here the map after the renderMapService has rendered the areas (the walls and free tiles don’t have an overlay).

map-test-calculated

Even if the map at this moment is quite simple and the tile’s type are very very base I wanted to be sure not to have inconsistent situations so I wrote a few test for the Map object and for the mapService (responsible to apply game rules on the map).

So, what about the map object?
Here my statements:

  • It should be possible create a Map with an array map in the constructor
  • It should be possible get the value of a tile
  • It should be possible update the value of a tile, not if the tile is a wall
  • It should not be possible update the value of a tile changing it into a wall
  • It should be possible to have a coord with multiple value

Here the test:

var map;
var arr;

module("A Map", {setup:function() {
	arr = [
		[0,0,1,0],
		[0,-1,0,0],
		[0,0,0,0],
		[0,0,0,0]
	];
	map = new Map(arr);
}});

test("It should be possible create a Map with an array map in the constructor", function() {
	var map1 = new Map(arr);

	equal(map1.GetGrid(),arr);
});

test("It should be possible get the value of a tile", function() {
	equal(map.GetTileValue(new Coords(2,0)),1);
	equal(map.GetTileValue(new Coords(1,1)),-1);
});

test("It should be possible update the value of a tile, not if the tile is a wall", function() {

	map.UpdateTileValue(new Coords(1,1),0);

	throws(function() {map.UpdateTileValue(new Coords(2,0),0)});
	equal(map.GetTileValue(new Coords(1,1)),0);
});

test("It should not be possible update the value of a tile changing it into a wall", function() {
	throws(function() {map.UpdateTileValue(new Coords(1,1),1);});
});

test("It should be possible to get number of row and columns of a map", function() {
	var row = map.GetDimensions().rows;
	var col = map.GetDimensions().cols;

	equal(row,4);
	equal(col,4);
});

test("It should be possible to have a coord with multiple value.", function() {
	map.UpdateTileValue(new Coords(1,1), Tile.SLOW);
	var value = map.GetTileValue(new Coords(1,1));

	ok(value instanceof Array, "now has two values");
	ok(map.CoordHasValue(new Coords(1,1),Tile.PAWN),"a value is Pawn");
	ok(map.CoordHasValue(new Coords(1,1),Tile.SLOW), "a value is Slow");

	map.UpdateTileValue(new Coords(1,1), Tile.TRAP);
	var value = map.GetTileValue(new Coords(1,1));
	ok(value instanceof Array, "now has three values");
	ok(map.CoordHasValue(new Coords(1,1),Tile.TRAP), "the new value is Trap");

	map.UpdateTileValue(new Coords(1,1), Tile.FREE);
	var value = map.GetTileValue(new Coords(1,1));
	equal(typeof value,"number", "now has one value");
	equal(value,Tile.FREE, "value is Free");

	map.UpdateTileValue(new Coords(1,1), Tile.PAWN);
	var value = map.GetTileValue(new Coords(1,1));
	equal(typeof value,"number", "still has one value");
	equal(value,Tile.PAWN, "value is Pawn");

	map.UpdateTileValue(new Coords(1,1), Tile.SLOW);
	var value = map.GetTileValue(new Coords(1,1));

	ok(value instanceof Array, "now has two value");
	ok(map.CoordHasValue(new Coords(1,1),Tile.PAWN), "a value is Pawn");
	ok(map.CoordHasValue(new Coords(1,1),Tile.SLOW), "a value is Slow");

	map.RemoveTileValue(new Coords(1,1), Tile.PAWN);
	var value = map.GetTileValue(new Coords(1,1));
	equal(typeof value,"number", "now has one value");
	equal(value,Tile.SLOW, "value is Slow");

	map.RemoveTileValue(new Coords(1,1), Tile.SLOW);
	var value = map.GetTileValue(new Coords(1,1));
	equal(typeof value,"number", "still has one value");
	equal(value,Tile.FREE, "the value is Free");

});

And here the map.js

var Map = Backbone.Model.extend({
	initialize:function(map) {
		this._grid = map;
	},

	CoordHasValue:function(coords, tile) {
		var value = this.GetTileValue(coords);
		if(value instanceof Array) {
			for(var i = 0; i < value.length; i++){
				if(value[i] === tile) {
					return true;
				}
			}
		} else {
			if(value === tile)
				return true;
		}

		return false;
	},
	GetDimensions: function () {
		return new Dimensions(this._grid.length, this._grid[0].length);
	},
	GetGrid:function() {
		return this._grid;
	},
	GetTileValue: function(coords) {
		return this._grid[coords.y][coords.x];
	},
	RemoveTileValue:function(coords, value) {
		var currentValue = this.GetTileValue(coords);
		if(currentValue instanceof Array) {
			for(var i = 0; i < currentValue.length; i++) {
				if(currentValue[i] == value) {
					currentValue.splice(i,1);
					if(currentValue.length == 1) {
						this._grid[coords.y][coords.x] = currentValue[0];
					}
					else if(currentValue.length == 0) {
						this._grid[coords.y][coords.x] = Tile.FREE;
					}
				}
			}
		}
		else {
			if(currentValue == value) {
				this._grid[coords.y][coords.x] = Tile.FREE;
			}
		}
	},
	UpdateTileValue: function(coords, newValue) {
		if(newValue == Tile.WALL)
			throw new Error("You can't turn a Tile in a wall");

		if(this.GetTileValue(coords) == Tile.WALL) {
            if(newValue === Tile.PAWN) {
                throw new Error("You can't put a pawn into a wall");
            } else {
			    throw new Error("You can't turn a Wall's Tile in a different tile");
            }
        }

		var currentValue = this._grid[coords.y][coords.x];
		if(this._isAnExclusiveValue(newValue))
			this._grid[coords.y][coords.x] = newValue;
		else {
			if(this._isAnExclusiveValue(currentValue)	)
				this._grid[coords.y][coords.x] = newValue;
			else {
				if(!this.CoordHasValue(coords, newValue))
					if(currentValue instanceof Array)
						this._grid[coords.y][coords.x].push(newValue);
					else {
						this._grid[coords.y][coords.x] = [];
						this._grid[coords.y][coords.x].push(currentValue);
						this._grid[coords.y][coords.x].push(newValue);
					}
			}
		}
	},
	_isAnExclusiveValue : function(tile) {
		switch (tile){
			case Tile.FREE:
			case Tile.WALL:
				return true;
				break;

			default :
				return false;
		}
	}
});

And what about the mapService?
Well, I had a more complicated work for this service, so my statements were:

  • It should be initialized receiving a ‘map’. A ‘map’ should be an instance of Map or to throw an exception
  • It should check a move. If the arrival coord is WALL or PAWN or OVERFLY the check should return false
  • If the arrival coords are not adjacent to the current it should return false
  • It should check if two pawns are adjacent or not
  • It should check if a pawn is in a range
  • It should check that a pawn has line of sight to the target tile
  • It should return all pawns of a specific roster, in an area centered on a pawn and having line of sight
  • It should be possible get tiles where to move, adjacent to a pawn
  • It should not be possible get a tile corner if both sides aren’t free
  • It should be possible to get free tiles in an area respecting the line of sight limitation.
  • It should give all tiles of a type in an area centered on a pawn
  • It should be possible to update a set of tiles. If a single update throws an exception all the updates should rollback.
  • It should be possible to remove a value from a set of tiles
  • when a pawn is moved it should lose a ‘parry’ effect if it is casted from another pawn
  • when a knight is moved the ‘parry’ effect should be losed by the defended pawn
  • It should be possible get pawns presents in a list of Tile

Here the test:

<pre>
var knight;
var pretorian;
var sniper;
var scout;
var mapService;
var arr;
var map;
var game;

var StubClass = function () { };

module("A MapService", {
 setup: function () {
  knight = new Knight("fakeid");
  pretorian = new Pretorian("fakeid");
  sniper = new Sniper("fakeid");
  scout = new Scout("fakeid");
  arr = [
   [ 8, -1, 0,  0], /* hole, knight */
   [-1, -1, 0,  0], /* scout, pretorian */
   [ 0,  8, 0,  0], /* hole */
   [ 0,  1, 0, -1]  /* wall, sniper */
  ];

  map = new Map(arr);

  mapService = new MapService(map);

  game = new StubClass();

  knight.SetToMove();
  sniper.SetToMove();
  pretorian.SetToMove();
  scout.SetToMove();

  knight.Move(new Coords(1, 0));
  sniper.Move(new Coords(3, 3));
  pretorian.Move(new Coords(1, 1));
  scout.Move(new Coords(0, 1));
 }
});

test("It should be initialized receiving a 'map'. A 'map' should be an instance of Map or to throw an exception", function () {
  throws(function () { new MapService(); });
  new MapService(map);
});

test("It should check a move. If the arrival coord is WALL or PAWN or OVERFLY the check should return false", function () {
  var checkMove = mapService.CheckIfMoveIsValid(knight, new Coords(2, 0));
  ok(checkMove);
  checkMove = mapService.CheckIfMoveIsValid(knight, new Coords(1, 1));
  equal(checkMove, false);
  checkMove = mapService.CheckIfMoveIsValid(knight, new Coords(0, 0));
  equal(checkMove, false);
});

test("If the arrival coords are not adjacent to the current it should return false", function () {
  var checkMove;
  checkMove = mapService.CheckIfMoveIsValid(knight, new Coords(0, 0));
  equal(checkMove, false);
  checkMove = mapService.CheckIfMoveIsValid(knight, new Coords(1, 0));
  equal(checkMove, false);
  checkMove = mapService.CheckIfMoveIsValid(knight, new Coords(2, 0));
  ok(checkMove);
  checkMove = mapService.CheckIfMoveIsValid(knight, new Coords(3, 0));
  equal(checkMove, false);
  checkMove = mapService.CheckIfMoveIsValid(knight, new Coords(0, 1));
  equal(checkMove, false);
  checkMove = mapService.CheckIfMoveIsValid(knight, new Coords(1, 1));
  equal(checkMove, false);
  checkMove = mapService.CheckIfMoveIsValid(knight, new Coords(2, 1));
  ok(checkMove);
  checkMove = mapService.CheckIfMoveIsValid(knight, new Coords(3, 1));
  equal(checkMove, false);
  checkMove = mapService.CheckIfMoveIsValid(knight, new Coords(0, 2));
  equal(checkMove, false);
  checkMove = mapService.CheckIfMoveIsValid(knight, new Coords(1, 2));
  equal(checkMove, false);
  checkMove = mapService.CheckIfMoveIsValid(knight, new Coords(2, 2));
  equal(checkMove, false);
  checkMove = mapService.CheckIfMoveIsValid(knight, new Coords(3, 2));
  equal(checkMove, false);
  checkMove = mapService.CheckIfMoveIsValid(knight, new Coords(0, 3));
  equal(checkMove, false);
  checkMove = mapService.CheckIfMoveIsValid(knight, new Coords(1, 3));
  equal(checkMove, false);
  checkMove = mapService.CheckIfMoveIsValid(knight, new Coords(2, 3));
  equal(checkMove, false);
  checkMove = mapService.CheckIfMoveIsValid(knight, new Coords(3, 3));
  equal(checkMove, false);
});

test("It should check if two pawns are adjacent or not", function () {
  ok(mapService.CheckPawnsAreAdjacent(knight, pretorian));
  equal(mapService.CheckPawnsAreAdjacent(knight, sniper), false);
});

test("It should check if a pawn is in a range", function () {
  //arr = [
  //[ 8, -1, 0,  0], /* hole, knight */
  //[-1, -1, 0,  0], /* scout, pretorian */
  //[ 0,  8, 0,  0], /* hole */
  //[ 0,  1, 0, -1]  /* wall, sniper */
  //];

  ok(mapService.CheckPawnIsInAreaRange(knight, sniper, 3));
});

test("It should check that a pawn has line of sight to the target tile", function () {
  ok(mapService.CheckLineOfSight(knight.GetPosition(), new Coords(3, 0)));
  ok(mapService.CheckLineOfSight(knight.GetPosition(), new Coords(1, 3)));
});

test("It should return all pawns of a specific roster, in an area centered on a pawn and having line of sight", function () {

  var roster = new Roster("fakeid");
  roster.Add(knight);
  roster.Add(pretorian);
  roster.Add(sniper);
  var pawns = mapService.GetAllPawnsInArea(knight, 3, roster);

  equal(pawns.length, 2);

  pawns = mapService.GetAllPawnsInArea(knight, 1, roster);
  equal(pawns.length, 1);

  sniper.Move(new Coords(3, 0));

  pawns = mapService.GetAllPawnsInArea(knight, 3, roster);
  equal(pawns.length, 2);

});

test("It should be possible get tiles where to move, adjacent to a pawn", function () {
  var freeTiles = mapService.GetFreeTilesAdjacent(knight);
  equal(freeTiles.length, 3);
});

test("It should not be possible get a tile corner if both side aren't free", function () {
  var newMap = new Map([
   [0,  1, 0],
   [0, -1, 1],
   [0,  0, 0]
  ]);

  var newPS = new MapService(newMap);
  knight.Move(new Coords(1, 1));
  var freeTiles = newPS.GetFreeTilesAdjacent(knight);
  equal(freeTiles.length, 5);
});

test("It should be possible to get free tiles in an area respecting the line of sight limitation.", function () {
  var arr = [
   [0, 0, 0,  0, 0, 0, 0],
   [0, 1, 0,  0, 0, 1, 0],
   [0, 0, 0,  0, 0, 0, 0],
   [0, 0, 0, -1, 0, 1, 0], /* a sniper */
   [0, 1, 0,  0, 0, 1, 0],
   [0, 0, 0,  0, 0, 1, 0],
   [0, 0, 0,  0, 0, 0, 0]
  ];
  var m = new Map(arr);
  var s = new Sniper();
  s.SetToMove();
  s.Move(new Coords(3, 3));
  var ps = new MapService(m);
  var listOfTiles = ps.GetAllTypeOfTileInArea(s, 5, Tile.FREE);
  equal(listOfTiles.length, 34);
});

test("It should give all tiles of a type in an area centered on a pawn", function () {
  var trapTiles = mapService.GetAllTypeOfTileInArea(sniper, 5, Tile.TRAP);
  equal(trapTiles.length, 3);
});

test("It should be possible to update a set of tiles. If a single update throws an exception all the updates should rollback.", function () {
  var trapTiles = [];
  trapTiles.push(new Coords(0, 3));
  trapTiles.push(new Coords(0, 2));
  trapTiles.push(new Coords(1, 3));

  throws(function () { mapService.UpdateTilesValue(trapTiles, Tile.TRAP) });
  equal(mapService._map.GetTileValue(trapTiles[0]), Tile.FREE);
  equal(mapService._map.GetTileValue(trapTiles[1]), Tile.FREE);
  equal(mapService._map.GetTileValue(trapTiles[2]), Tile.WALL);

  trapTiles = [];
  trapTiles.push(new Coords(0, 3));
  trapTiles.push(new Coords(0, 2));
  trapTiles.push(new Coords(2, 3));

  mapService.UpdateTilesValue(trapTiles, Tile.TRAP);
  equal(mapService._map.GetTileValue(trapTiles[0]), Tile.TRAP);
  equal(mapService._map.GetTileValue(trapTiles[1]), Tile.TRAP);
  equal(mapService._map.GetTileValue(trapTiles[2]), Tile.TRAP);
});

test("It should be possible to remove a value from a set of tiles", function () {
  var freeTiles = [];
  freeTiles.push(new Coords(1, 0));

  equal(mapService.CoordHasValue(freeTiles[0], Tile.PAWN), true, "There's a pawn");
  mapService.RemoveTilesValue(freeTiles, Tile.PAWN);
  equal(mapService.CoordHasValue(freeTiles[0], Tile.FREE), true, "The pawn is removed");
});

test("when a pawn is moved it should lose a 'parry' effect if it is casted from another pawn", function () {
  // arr = [
  //[ 8, -1, 0,  0], /* hole, knight */
  //[-1, -1, 0,  0], /* scout, pretorian */
  //[ 0,  8, 0,  0], /* hole */
  //[ 0,  1, 0, -1]  /* wall, sniper */
  // ];*/
  stub(game, 'GetRoster', function () {
    var roster = new Roster("fakeid");
    roster.Add(knight);
    roster.Add(scout);
    roster.Add(pretorian);

    return roster;
  });

  scout.SetStatus({ name: Status.DEFEND, by: knight.GetId() });
  mapService.MovePawn(scout, new Coords(0, 2));
  mapService.EndPawnMove(scout);
  equal(scout.CheckStatus(Status.DEFEND), false, "The scout should lose the status parry");

  scout.RemoveStatus('moved');
  scout.SetToMove();

  scout.SetStatus({ name: Status.DEFEND, by: 'self' });
  mapService.MovePawn(scout, new Coords(0, 1));
  mapService.EndPawnMove(scout);
  equal(scout.CheckStatus(Status.DEFEND), true, "The scout should have a self " +
  "given status DEFEND, even after moved");

  scout.SetToMove();

  mapService.MovePawn(pretorian, new Coords(1, 2));
  mapService.EndPawnMove(pretorian);
  equal(pretorian.CheckStatus(Status.DEFEND), true, "The pretorian should have a self " +
  "given status DEFEND, even after moved");
});

test("when a knight is moved the 'parry' effect should be losed by the defended pawn", function () {
  // arr = [
  //[ 8, -1, 0,  0], /* hole, knight */
  //[-1, -1, 0,  0], /* scout, pretorian */
  //[ 0,  8, 0,  0], /* hole */
  //[ 0,  1, 0, -1]  /* wall, sniper */
  // ];*/
  stub(game, 'GetRoster', function () {
    var roster = new Roster("fakeid");
    roster.Add(knight);
    roster.Add(scout);
    roster.Add(pretorian);

    return roster;
  });

  scout.SetStatus({ name: Status.DEFEND, by: knight.GetId() });
  mapService.MovePawn(knight, new Coords(0, 0));
  mapService.EndPawnMove(knight);
  equal(scout.CheckStatus(Status.DEFEND), false, "The scout should lose the status parry");
});

test("It should be possible get pawns presents in a list of Tile", function () {
  //[ 8, -1, 0,  0], /* hole, knight */
  //[-1, -1, 0,  0], /* scout, pretorian */
  //[ 0,  8, 0,  0], /* hole */
  //[ 0,  1, 0, -1]  /* wall, sniper */

  var listOfTiles = [];
  listOfTiles.push(new Coords(1, 0));
  listOfTiles.push(new Coords(2, 0));
  listOfTiles.push(new Coords(1, 1));
  listOfTiles.push(new Coords(3, 0));

  var listOfPawns = [];
  listOfPawns.push(knight);
  listOfPawns.push(pretorian);
  listOfPawns.push(sniper);

  var finalPawns = mapService.SearchPawnsInAListOfTiles(listOfPawns, listOfTiles);
  deepEqual(finalPawns[0], knight);
  deepEqual(finalPawns[1], pretorian);
  equal(finalPawns.length, 2)
});

And here the mapService.js

var MapService = Backbone.Model.extend({
  initialize:function(map) {
    if(!(map instanceof Map))
      throw new Error("A move service need a map array");

    this._map = map;
  },
  CheckIfMoveIsValid:function(pawn,coords) {

  	if (this._map.CoordHasValue(coords, Tile.WALL)
			|| this._map.CoordHasValue(coords, Tile.PAWN)
			|| this._map.CoordHasValue(coords, Tile.OVERFLY))
      return false;

    if(!this._checkIfGivenTileIsAdjacentFromTheCurrent(pawn.GetPosition(), coords))
      return false;

    return true;
  },
  CheckSetOfCoordsContainsCoords:function(setOfCoords, targetCoords) {
    for (var i = 0; i < setOfCoords.length; i++){
      if(setOfCoords[i].x == targetCoords.x && setOfCoords[i].y == targetCoords.y)
        return true;
    }

    return false;
  },
  CheckPawnsAreAdjacent: function(pawn1, pawn2) {
    var pawn1Coords = pawn1.GetPosition();
    var pawn2Coords = pawn2.GetPosition();

    return this._checkIfGivenTileIsAdjacentFromTheCurrent(pawn1Coords, pawn2Coords);
  },
  CheckPawnIsInAreaRange: function(pawn1, pawn2, distance) {
    var pawn1Coords = pawn1.GetPosition();
    var pawn2Coords = pawn2.GetPosition();

    var startX = this._getStartX(pawn1Coords, distance);
    var startY = this._getStartY(pawn1Coords, distance);
    var endX = this._getEndX(pawn1Coords, distance);
    var endY = this._getEndY(pawn1Coords, distance);

    if(pawn2Coords.x >= startX && pawn2Coords.x <= endX	&& pawn2Coords.y >= startY && pawn2Coords.y <= endY)
    {
      if(this.CheckLineOfSight(pawn1Coords, pawn2Coords))
      {
        return true;
      }
    }

    return false;
  },
  CheckLineOfSight: function(center, coords) {
    var _coords = center;

    var losMap = new _losMap(this._map.GetGrid());

    return _checkVisible(losMap, _coords.x, _coords.y, coords.x, coords.y);
  },
  CoordHasValue:function(coords, tile) {
    return this._map.CoordHasValue(coords, tile);
  },

  EndPawnMove:function(pawn) {
    var ret = [];
    pawn.EndMove();
    if(!pawn.CheckStatusSetBy(Status.DEFEND, 'self')) {
      pawn.RemoveStatus(Status.DEFEND);
    }

    if(pawn.GetSkillById(Action.PARRY) != null) {
      var targetPawns =   game.GetRoster().GetPawns();
      for(var i = 0; i < targetPawns.length; i++) {
        var targetPawn = targetPawns[i];
        if(targetPawn.CheckStatusSetBy(Status.DEFEND, pawn.GetId())) {
          targetPawn.RemoveStatusSetBy(Status.DEFEND, pawn.GetId());
          ret.push(targetPawn);
        }
      }
    }

    return ret;
  },
  GetAllPawnsInArea: function(pawn, distance, roster, isIncludedCenter) {
    var coords;
    if(pawn instanceof Pawn)
      coords = pawn.GetPosition();
    if(pawn instanceof Coords)
      coords = pawn;

    var startX = this._getStartX(coords, distance);
    var startY = this._getStartY(coords, distance);
    var endX = this._getEndX(coords, distance);
    var endY = this._getEndY(coords, distance);

    var listOfPawns = roster.GetPawns();

    var ret = [];
    for (var i = 0; i < roster.Count();i++){
      if(isIncludedCenter === false || typeof(isIncludedCenter) === "undefined") {
        if(listOfPawns[i].GetPosition().x === coords.x && listOfPawns[i].GetPosition().y === coords.y)
          continue;
      }

      var newCoords = listOfPawns[i].GetPosition();
      if(newCoords.x >= startX && newCoords.x <= endX	&& newCoords.y >= startY && newCoords.y <= endY)
      {
        if(this.CheckLineOfSight(coords, newCoords))
        {
          ret.push(listOfPawns[i]);
        }
      }
    }

    return ret;
  },
  GetAllPawnsInAreaIncludedCenter: function(center, distance, roster) {
    return this.GetAllPawnsInArea(center, distance, roster, true);
  },
  GetAllTypeOfTileInArea : function(center, distance, type) {
    if(typeof type === "undefined")
      type = Tile.FREE;

    var isAPawn = false;
    var coords;
    if(center instanceof Pawn) {
      coords = center.GetPosition();
      isAPawn = true;
    }
    if(center instanceof Coords)
      coords = center;

    var startX = this._getStartX(coords, distance);
    var startY = this._getStartY(coords, distance);
    var endX = this._getEndX(coords, distance);
    var endY = this._getEndY(coords, distance);

    var ret = [];

    var i = 0;
    for(var y = startY; y <= endY; y++) {
      for(var x = startX; x <= endX; x++) {
        var c = new Coords(x,y);
        if(this._coordIs(c, type)){
          if(this.CheckLineOfSight(coords,c)){
            if(isAPawn) {
              if(this._isACorner(i)){
                if(this._cornerIsReachable(i, coords, startX, startY, endX, endY)) {
                  ret.push(c);
                }
              } else {
                ret.push(c);
              }
            } else {
              ret.push(c);
            }
          }
        }
        i++;
      }
    }
    return ret;
  },
  GetFreeTilesAdjacent : function(pawn) {
    return this.GetAllTypeOfTileInArea(pawn, 1, Tile.FREE);
  },
  GetFreeTilesAdjacentExcludingMovesDone : function(pawn) {
    var coords = this.GetAllTypeOfTileInArea(pawn, 1, Tile.FREE);
    var movesDone = pawn.GetMovesDone();
    var starting = pawn._startingCoords;

    var existingStartCoord = false;
    for(var c = 0; c < movesDone.length;c++) {
      if(movesDone[c].x == starting.x && movesDone[c].y == starting.y) {
        existingStartCoord = true;
      }
    }

    if(!existingStartCoord)
      movesDone.push(starting);

    var newCoords = [];

    for(var c = 0; c < coords.length; c++) {
      var thereIs = false;
      for(var m = 0; m < movesDone.length; m++) {
        if(coords[c].x == movesDone[m].x && coords[c].y == movesDone[m].y) {
          thereIs = true;
        }
      }
      if(!thereIs)
        newCoords.push(coords[c]);
    }

    return newCoords;
  },
  GetTileValue: function(coords) {
    return this._map.GetTileValue(coords);
  },

  GetMoveCost: function(pawn){
    var currentCoords = pawn.GetPosition();
    if(this.CoordHasValue(currentCoords, Tile.SLOW)) {
      return 2;
    } else {
      return 1;
    }
  },

  MovePawn:function(pawn, coords){
    var currentCoords = pawn.GetPosition();
    if(this.CheckIfMoveIsValid(pawn, coords)) {

      pawn.Move(coords, this.GetMoveCost(pawn));

      if(this.CoordHasValue(coords, Tile.SLOW)) {
        pawn.SetStatus({name: Status.SLOWED, by:"map" });
      } else {
        if(pawn.CheckStatus(Status.SLOWED)) {
          pawn.RemoveStatus(Status.SLOWED);
        }
      }
      this._map.RemoveTileValue(currentCoords, Tile.PAWN);
      this._map.UpdateTileValue(coords, Tile.PAWN);
    }
  },

  RemoveBonusGrantedFromPawn: function(pawn, roster) {
    if(!pawn.CheckStatus(Status.DEAD))
      return;

    if(!pawn.GetSkillByName(ActionName.PARRY))
      return;

    var pawns = roster.GetPawns();
    for(var i = 0; i < pawns.length; i++) {
      var p = pawns[i];
      p.RemoveStatusSetBy(Status.DEFEND, pawn.GetId());
    }
  },

  RemoveTilesValue:function(arrCoords, tileValue) {
    for(var c =0; c < arrCoords.length; c++) {
      this._map.RemoveTileValue(arrCoords[c], tileValue);
    }
  },

  ResetMovePawn:function(pawn) {
    var currentCoords = pawn.GetPosition();
    var previousCoords;

    pawn.ResetMove();
    previousCoords = pawn.GetPosition();
    this._map.RemoveTileValue(currentCoords, Tile.PAWN);
    this._map.UpdateTileValue(previousCoords, Tile.PAWN);
  },
  SearchPawnsInAListOfTiles:function(roster, listOfTiles) {
    var correctPawns = [];
    if(roster instanceof Roster) {
      var pawns = roster.GetPawns();
    } else if(roster instanceof Array) {
      var pawns = roster;
    } else {
      throw new Error("roster parameter should be an array of pawns or a roster");
    }

    for(var p = 0; p < pawns.length; p++) {
      for(var t = 0; t < listOfTiles.length; t++) {
        var pos = pawns[p].GetPosition()
        if(pos.x == listOfTiles[t].x && pos.y == listOfTiles[t].y){
          correctPawns.push(pawns[p]);
        }
      }
    }

    return correctPawns;
  },
  SetPawnsInMap: function (roster) {
    var pawns = roster.GetPawns();
    for(var p = 0; p < pawns.length; p++) {
      var id = pawns[p].GetId();
      var coord = pawns[p].GetPosition();
      this._getMap().UpdateTileValue(coord, Tile.PAWN);
    }
  },

  UndoMovePawn:function(pawn){
    var currentCoords = pawn.GetPosition();
    var previousCoords;

    pawn.UndoMove();
    previousCoords = pawn.GetPosition();
    this._map.RemoveTileValue(currentCoords, Tile.PAWN);
    this._map.UpdateTileValue(previousCoords, Tile.PAWN);
  },

  UpdateTilesValue: function(tiles, newValue) {
    var oldCoords = []
    var oldValues = [];
    try {
      for (var j = 0; j < tiles.length; j++) {
        oldCoords.push(tiles[j]);
        oldValues.push(this._getMap().GetTileValue(tiles[j]));
        this._getMap().UpdateTileValue(tiles[j],newValue);
      }
    }
    catch(err) {
      if(oldCoords.length == 0)
        throw new Error(err);

      for(var i = 0; i < tiles.length; i++) {
        for (var j = 0; j < oldCoords.length; j++) {
          if(oldCoords[j].x == tiles[i].x && oldCoords[j].y == tiles[i].y) {
            this._getMap().UpdateTileValue(oldCoords[j],oldValues[j]);
          }
        }
      }

      throw new Error(err);
    }
  },
  _checkIfGivenTileIsAdjacentFromTheCurrent:function(objCoords, coords) {
    if(objCoords.x == coords.x && objCoords.y == coords.y)
      return false;

    if(((objCoords.x + 1) == coords.x) || ((objCoords.x - 1) == coords.x) || ((objCoords.x) == coords.x))
      if(((objCoords.y + 1) == coords.y) || ((objCoords.y - 1) == coords.y) || ((objCoords.y) == coords.y))
        return true;

    return false;
  },
  _getMap:function() {
    return this._map;
  },
  _getStartX : function(coords, distance) {
    return ((coords.x - distance) < 0) ? 0 : (coords.x - distance);
  },
  _getStartY : function(coords, distance) {
    return ((coords.y - distance) < 0) ? 0 : (coords.y - distance);
  },
  _getEndX : function(coords, distance) {
    return ((coords.x + distance) > this._map.GetDimensions().cols - 1)? this._map.GetDimensions().cols -1
      : (coords.x + distance);
  },
  _getEndY: function(coords, distance) {
    return ((coords.y + distance) > this._map.GetDimensions().rows - 1)
      ? this._map.GetDimensions().rows -1
      : (coords.y + distance);
  },
  _coordIs:function(coords, type) {
    if(type == Tile.NOTAWALL)
      return (!this._map.CoordHasValue(coords, Tile.WALL));
    else if(type == Tile.FREE)
      return (!this._map.CoordHasValue(coords, Tile.WALL) && !this._map.CoordHasValue(coords, Tile.PAWN));
    else if(type == Tile.FREENOSPECIAL)
      return (!this._map.CoordHasValue(coords, Tile.WALL)
        && !this._map.CoordHasValue(coords, Tile.STARTP1)
        && !this._map.CoordHasValue(coords, Tile.STARTP2)
        && !this._map.CoordHasValue(coords, Tile.GOAL)
        && !this._map.CoordHasValue(coords, Tile.SCORE));
    else
      return (this._map.CoordHasValue(coords, type));
  },
  _cornerIsReachable:function(corner, coords, startX, startY, endX, endY){
    switch (corner){
      case 0:
        if(this._coordIs(new Coords(startX, coords.y), Tile.FREE) || this._coordIs(new Coords(coords.x, startY), Tile.FREE)) {
          return true;
        }
        break;

      case 2:
        if(this._coordIs(new Coords(endX, coords.y), Tile.FREE) || this._coordIs(new Coords(coords.x, startY), Tile.FREE)) {
          return true;
        }
        break;

      case 6:
        if(this._coordIs(new Coords(startX, coords.y), Tile.FREE) || this._coordIs(new Coords(coords.x, endY), Tile.FREE)) {
          return true;
        }
        break;

      case 8:
        if(this._coordIs(new Coords(endX, coords.y), Tile.FREE) || this._coordIs(new Coords(coords.x, endY), Tile.FREE)) {
          return true;
        }
        break;
    }
    return false;
  },
  _isACorner:function(index) {
    if(index == 0 || index == 2 || index == 6 || index == 8)
      return true;

    return false;
  }
});

To solve the line of sight problem I used the Bresenham’s line algorithm.

At the time when I defined test and code I wasn’t so good with TDD, so like for the mapService.js, I didn’t wrote test and then the code, but I tested only the behaviours that I decided they were critical, so you will find a lot of methods not tested at all (naughty, naughty child!).

Another important thought about the mapService is related on its responsability, I think it takes care of too much aspects, in the future a pawnService will be put in place.

Talking about the overall experience and gameplay test: I can say that the map is fun to play even if there is something missing.

Maybe 3 actions per turn + free moves are not enough, or maybe the map is too simple, but for sure there is something missing, because I don’t feel the correct flow of adrenaline.

After this first test map we designed 2 more maps, just to be sure that the feeling wasn’t depending on a bad map.

Nothing changed so or we really suck in designing maps (probable!) or we didn’t find the correct balance to make the game more engaging (probable!)

2 thoughts on “Pawns and Tiles: concerning the Tiles

  1. Pingback: Pawns and Tiles: concerning the Skills | Agile. Angular/Js. Asp.NET & TDD

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