Saturday, May 16, 2020

Making Pent Up Anger Part 3 of 5

Making Pent Up Anger part 3 of 5 - Head to Head version of the Game



This is the third part of a five part series on building Pent Up Anger using AnimateCC. The game can be found at http://spelchan.com/games/y2018/PentUpAnger.php .

Preparing the Pieces

The first thing we are going to need to do to get a head-to-head (hot seat) game going is to have more than a single piece on the board. This means that all the pieces for all five potential players will need to be tracked. An array to hold this information is easy enough to build. The big question is where should this information live? It could be a good idea to keep the board separate from the game state and instead have a separate class that manages the game play. This would have the advantage that you would be able to have different variants of the game and that controller would handle the game logic. This is going a bit too overboard and is something that could be done through refactoring at a later point if such a thing ended up being desired so we will follow the principle of only writing the code that we actually need to accomplish our task and put the game logic into the board class.

This means adding a bit of code to the board initialization class to set up the 25 game pieces that are necessary. While I don't like getting ahead of myself, we know that we are going to need to be able to click on the pieces and that this will require a listener so we will set up the click listener right now even though we won't be implementing it until later. The handler for clicking is going to be called choosePiece and we will need. A dummy version of the choosePiece method should be created with a simple console log warning you that the method has not yet been implemented.

As I stated earlier, to use a method as a callback function, you need to bind the method to the class being used. This has a rather strange behavior of being considered a different function call each time it is created which means that while you can add event listeners, removing those listeners is not possible unless you keep a copy of the originally bound function. This information is stored in the pieceListeners field and is used as the event listener.


this.boardMovie = movie;
this.pieces = [
 [movie.p1, movie.p2, movie.p3, movie.p4, movie.p5],
 [movie.r1, movie.r2, movie.r3, movie.r4, movie.r5],
 [movie.y1, movie.y2, movie.y3, movie.y4, movie.y5],
 [movie.g1, movie.g2, movie.g3, movie.g4, movie.g5],
 [movie.b1, movie.b2, movie.b3, movie.b4, movie.b5]
];
this.pieceListeners = [];
this.player = 23;
for (var cntrPlayer = 0; cntrPlayer < 5; ++cntrPlayer) {
this.pieceListeners[cntrPlayer] = [];
for (var cntr = 0; cntr < 5; ++cntr) {
this.pieces[cntrPlayer][cntr].playerID.text = ""+(cntr+1);
this.pieces[cntrPlayer][cntr].useHighlight.visible = false;
this.pieces[cntrPlayer][cntr].playerID = cntrPlayer;
this.pieces[cntrPlayer][cntr].pieceID = cntr;
this.pieceListeners[cntrPlayer][cntr] = this.choosePiece.bind(this, cntrPlayer, cntr);
this.pieces[cntrPlayer][cntr].addEventListener("click", this.pieceListeners[cntrPlayer][cntr]);
}
this.pieces[cntrPlayer][cntrPlayer].useHighlight.visible = true;
}

At the end of initialization, the pieces used by the player exist in a two dimensional array named pieces. This holds the pieces and relevant information about the pieces but does not set up the pieces. This needs to be done to start the game, and as we want to let the player play the game more than once without having to reload the game it makes sense to have a separate method for setting up the starting board. This we will call layoutPieces. This simply loops through the players and their pieces  and then places those pieces on the board in the appropriate starting location. It then loops through all the locations on the board and makes sure that the piece currently assigned to that location is null. This is important as only one piece is allowed on each board location so tracking the piece on a location is required to determine if a piece is going to be sent home.

spelchan.Board.prototype.layoutPieces = function() {
var temp;
for (var cntrcol = 0; cntrcol < 5; ++cntrcol)
{
for (var cntr = 0; cntr < 5; ++cntr)
{
temp = this.BOARD_LAYOUT[cntrcol][this.BOARD_START_INDEX] + cntr;
this.pieces[cntrcol][cntr].x = this.layout_x[temp];
this.pieces[cntrcol][cntr].y = this.layout_y[temp];
this.pieces[cntrcol][cntr].location = temp;
console.log("placing piece on " + temp);
}
}
for (cntr = 0; cntr < 105; ++cntr)
this.layout_piece[cntr] = null;
}

Now that we have the pieces able to appear on the board, the next step is to actually handle the player's turn.


Turn Handling

With the player pieces laid out on the board, we are ready to get the game under way. But for the players to do anything they need to be able to move. This requires a way for the player to roll the die. This can be combined with our solution for determining which players’ turn it is by simply having a roll button for each player. This means that we need to create a roll button for each of the five players. Instead of creating five buttons, we can ``cheat'' by only create one grayscale version of the play button and tint it. The states for this button can be seen in the image below. In addition to the roll button, we will need a skip button for skipping your turn which is set up the same as the roll button.

To make use of this button, it actually has on the screen. This is done in animate by adding the buttons to the Only the player who currently is playing can use this button, only the roll button for the current player needs to be on the screen. This is done by manually positioning the components on the game screen symbol as shown in the image below. Note that this screen shot is from later in development so there are messages and a button that would not normally be on the screen at this time.



This, in my opinion, is user interface related and is kept in the main Pent class.  This is set up allows us to set up in the startGameScreen which is the method we use for initializing the main game screen when the game is being set up. This simply sets up the pieces of the board and then sets up all of the roll and skip buttons.

spelchan.Pent.prototype.startGameScreen = function(movie) {
this.gameMovie = movie;
this.currentPlayer = 0;
this.board = new spelchan.Board(movie.board_movie);
this.board.layoutPieces();
this.die = new spelchan.Die(movie.die_movie);
movie.stop();
this.rollBtns = [movie.proll_btn, movie.rroll_btn, movie.yroll_btn, movie.groll_btn, movie.broll_btn];
this.skipBtns = [movie.pskip_btn, movie.rskip_btn, movie.yskip_btn, movie.gskip_btn, movie.bskip_btn];
this.rollHandler = this.startRoll.bind(this);
this.skipHandler = this.skip.bind(this);
for (var cntr = 0; cntr < 5; ++cntr) {
this.rollBtns[cntr].addEventListener("click", this.rollHandler);
this.rollBtns[cntr].addEventListener("mousedown", spelchan.tintedButtonEvent);
this.rollBtns[cntr].addEventListener("pressup", spelchan.tintedButtonEvent);
this.rollBtns[cntr].addEventListener("rollover", spelchan.tintedButtonEvent);
this.rollBtns[cntr].addEventListener("rollout", spelchan.tintedButtonEvent);
this.skipBtns[cntr].addEventListener("click", this.skipHandler);
this.skipBtns[cntr].addEventListener("mousedown", spelchan.tintedButtonEvent);
this.skipBtns[cntr].addEventListener("pressup", spelchan.tintedButtonEvent);
this.skipBtns[cntr].addEventListener("rollover", spelchan.tintedButtonEvent);
this.skipBtns[cntr].addEventListener("rollout", spelchan.tintedButtonEvent);
}
this.winText = movie.win_txt;
this.continueHandler = this.endGame.bind(this);
this.continueBtn = movie.continue_btn;
this.continueBtn.addEventListener("click", this.continueHandler);
this.UI_ROLL = 0;
this.UI_SKIP = 1;
this.UI_WIN = 2;
this.UI_HIDE = 3;
this.ROLL_TIME = 50;
this.PLAYER_LABELS = ["Purple", "Red", "Yellow", "Green", "Blue"];
this.PLAYER_COLORS = [0xFF8800AA", 0xFFAA0000", 0xFFAAAA00", 0xFF00AA00", 0xFF0000AA"];
this.updateGameUI(0, this.UI_ROLL);
}

As you can see by looking at the code, when the player's button is visible, clicking on the button calls the rollHandler function which is the startRoll method bound to the class instance. It simply sets up the roll and then updates the user interface using our updateGameUP which is called whenever the system needs to update the UI with the caller providing the current player and the state the game is in. Note that we defined four states as follows:

  • UI_ROLL Waits for player to start rolling the dice.
  • UI_SKIP Waiting for user to make move or skips the rest of their turn.
  • UI_WIN Indicates that the game has been won.
  • UI_HIDE Turns off all the user interface.

spelchan.Pent.prototype.updateGameUI = function(player, state) {
// hide all buttons
for (var cntr = 0; cntr < 5; ++cntr) {
this.rollBtns[cntr].visible = false;
this.skipBtns[cntr].visible = false;
}
// reveal stuff based on state
switch (state) {
case this.UI_ROLL:
this.rollBtns[player].visible = true;
break;
case this.UI_SKIP:
this.skipBtns[player].visible = true;
break;
}
}

spelchan.Pent.prototype.startRoll = function() {
this.die.startRoll(this, this.ROLL_TIME);
this.updateGameUI(this.currentPlayer, this.UI_HIDE);
}

The roll function calls the roll result method once the roll is finished. This method makes use of the board class to determine which pieces that the player can move based on the results of the roll.

spelchan.Pent.prototype.rollResult = function(n) {
this.updateGameUI(this.currentPlayer, this.UI_SKIP);
console.log("the roll was " + n);
this.board.prepareMove(this.currentPlayer, n, this);
}


The prepareMove function is a bit more complicated task so I am going to break the function up into several sections and comment on each section. The first thing this function does is determine where the relevant board locations for that player are. This is a simple lookup in our BOARD\_LAYOUT array.

spelchan.Board.prototype.prepareMove = function(player, roll, listener) {
var moveCount, cntr, loc, zonedist;
this.lastRoll = roll;
this.clearActions();
this.move_listener = listener;
var home = this.BOARD_LAYOUT[player][this.BOARD_HOME_INDEX];
var start = this.BOARD_LAYOUT[player][this.BOARD_START_INDEX];
var end = this.BOARD_LAYOUT[player][this.BOARD_END_INDEX];
this.player = player;
movecount = 0;

Next we loop through the five pieces that belong to the player. We then determine how far from home that particular piece is. The board is circular (sort of) which means that we have the extra complexity of dealing with the possibility that the target home location is actually less than the actual location. As this will result in a negative distance from home, we simply need to add the size of the board (55 locations) to this value to get the correct distance.

Knowing how far the piece has to travel to get to the home location is the way we determine if the piece is located in the loading zone. It is also going to prove to be a very useful bit of information when we get around to writing the AI next chapter.

for (cntr = 0; cntr < 5; ++cntr)
{
this.pieces[player][cntr].weight = 0;
loc = this.pieces[player][cntr].location;
console.log("piece " + cntr + "is at " + loc);
zonedist = home - loc;
if (zonedist <= 0)
zonedist += 55;
console.log ("piece " + cntr + " is " + zonedist + " from reaching home");

As you have probably already realized, not all pieces are going to be on the board.  In fact, some of the pieces will be in the starting gates. It is important that those pieces be able to move, otherwise we won’t have a game at all since there would never be any pieces on the board. To start a piece on the board, the roll must match that piece. Likewise, to remove the piece the roll must match the piece. This is simply enough to check. If the piece matches the roll, we then see if the piece is in the loading zone or in the starting gate. If the piece is in one of these area’s then the piece is movable.

// see if piece can be launched or finished
if ((cntr+1) == roll)
{
if ( (loc == (start+cntr)) || (zonedist < 5) )
{
console.log("Piece is launchable/exitable");
this.pieces[player][cntr].useHighlight.visible = true;
++movecount;
}
}

We then see if the piece is on the board and can move without going past the loading zone. We know the board starts at location 50, so that check is simply a location check.  The distance from home can be used to see if the roll will take you past home, as if the distance is less than the roll of the die, then the move will take you past the starting position!

if ( (loc >= 50) && (zonedist > roll) )
{
console.log("piece movable on board");
this.pieces[player][cntr].useHighlight.visible = true;
var targetloc = loc + roll;
if (targetloc > 104)
targetloc -= 55;
++movecount;
}
}

Finally, we see if there is actually a move available for the player. As all the moves are counted above, we simply see if there are moves. If not, we are done the move!

if (movecount == 0)
listener.doneMove();
}


When the player chooses a piece by clicking on it, the choosePiece function gets called. 
The choosePiece function will start the animation so it will be covered next.

Moving the Piece

The choosePiece function within the board movie is called when a piece is clicked. The first thing the function does is determine if the piece that was selected belongs to the player and if not simply returns. It then figures out where the piece is going to be moving to. If the piece is waiting to be put on the board, the target location is the player's starting spot on the board. If the piece is already on the board, it moves it the die roll's number of slots. This is not as easy as it sounds as the board is a circle but in memory is treated as a line. To make the board act circular, if a piece goes of the end of the line, it wraps to the beginning of the line.  Finally, it sets up the movingPiece, movingTargetX and movingTargetY variables so the updateMove method will know that it has a piece to move and where to move that piece to.

spelchan.Board.prototype.choosePiece = function(player, piece) {
console.log("***choosePiece called***");
if (player != this.player) {
console.log("Piece does not belong to player! " + player + "!=" + this.player);
console.log("speed = " + this.SPEED);
return;
}
console.log ("Piece was selected to be moved: " + player + "," + piece);
var loc = this.pieces[player][piece].location;
console.log ("Piece was on " + loc);
this.clearActions();
if (loc == (this.BOARD_LAYOUT[player][this.BOARD_START_INDEX]+piece) )
this.movingTargetLoc = this.BOARD_LAYOUT[player][this.BOARD_HOME_INDEX];
else if ( (loc >= this.BOARD_LAYOUT[player][this.BOARD_LOADING_START]) &&
(loc <= this.BOARD_LAYOUT[player][this.BOARD_LOADING_END]) &&
(this.lastRoll == (piece+1)) )
this.movingTargetLoc = this.BOARD_LAYOUT[player][this.BOARD_END_INDEX]+piece;
else
{
this.movingTargetLoc = loc + this.lastRoll;
if (this.movingTargetLoc > 104)
this.movingTargetLoc -= 55;
}
this.movingPiece = this.pieces[player][piece];
this.movingTargetX = this.layout_x[this.movingTargetLoc];
this.movingTargetY = this.layout_y[this.movingTargetLoc];
this.layout_piece[loc] = null;
console.log("Moving from " + loc + " to " + this.movingTargetLoc);
// onEnterFrame = updateMove;
}

The updateMove function is called every frame whether there is something to be animated or not. It knows that it has work to do if the movingPiece variable is not null. The first thing the function does is figures out if the current frame of motion will be the last frame of motion. This is simply a check of the distance from the current position to the ending position. If the distance remaining to move is less than the speed then we obviously are on the last frame of animation. Or at least the last frame of that pieces’ animation.

If the piece has reached the target location we need to determine if there is already a piece on the target location. If there is, we determine the original start-block location for that piece and create a new animation moving that piece back to start. If there is not a piece on the target location we call the doneMove handler assigned to the board which is used for managing what happens once the animation is finished. This handler will be written shortly.

If the piece being animated has not reached the end of it's animation, then we simply move towards the target location by applying the time adjusted movement speed to the piece.

spelchan.Board.prototype.updateMove = function() {
if (this.movingPiece == null)
return;
console.log("moving piece...");
var curX = this.movingPiece.x;
var curY = this.movingPiece.y;
var deltaX = this.movingTargetX - curX;
var deltaY = this.movingTargetY - curY;
var distance = Math.abs(deltaX) + Math.abs(deltaY);
if (distance <= this.SPEED)

this.movingPiece.x = this.movingTargetX;
this.movingPiece.y = this.movingTargetY;
// movingPiece.changeState(0);
this.movingPiece.location = this.movingTargetLoc;
if (this.layout_piece[this.movingTargetLoc] != null)
{
var temp = this.layout_piece[this.movingTargetLoc];
this.layout_piece[this.movingTargetLoc] = this.movingPiece;
this.movingTargetLoc = this.BOARD_LAYOUT[temp.playerID][this.BOARD_START_INDEX]+temp.pieceID;
this.movingTargetX = this.layout_x[this.movingTargetLoc];
this.movingTargetY = this.layout_y[this.movingTargetLoc];
this.movingPiece = temp;
}
else
{
this.layout_piece[this.movingTargetLoc] = this.movingPiece;
this.movingPiece = null;
// onEnterFrame = null;
this.move_listener.doneMove();
}
}
else

var moveX = deltaX * this.SPEED / distance;
var moveY = deltaY * this.SPEED / distance;
this.movingPiece.x += moveX;
this.movingPiece.y += moveY;
}
}

We are finished with the board movie for the moment, so the last thing we need to do is in the main movie. This task is simply a function to handle the game once the move is done. This function simply checks to see if the game has been won and if so ends the game. If the game is not over it goes to the next player.

spelchan.Pent.prototype.doneMove = function() {
if (this.board.checkWin(this.currentPlayer) == true) {
this.updateGameUI(this.currentPlayer, this.UI_WIN);
return;
}
this.currentPlayer = (this.currentPlayer + 1) % 5;
this.updateGameUI(this.currentPlayer, this.UI_ROLL);
}

The method for determining if the game has been won will be developed in the next section.

Winning the Game

As shown last section, whenever a player finishes a move, we need to determine if the game has been won. This is a fairly simple function as we simply need to check to see if all the pieces are in their end positions. A simple function in the board movie can handle this.

spelchan.Board.prototype.checkWin = function(player) {
var cntr, result = true;
var end = this.BOARD_LAYOUT[player][this.BOARD_END_INDEX];
for (cntr = 0; cntr < 5; ++cntr)
{
if (this.pieces[player][cntr].location != (end + cntr))
{
result = false;
break;
}
}
return result;
}

If the player has won, however, the state for showing the win message has not been handled. To do this we are going to need to create a continue button to add to the screen as well as a large text message that displays the win message. The continue button is a simple gray button with the states as shown in the image below.


The Pent class updateGameUI now needs to be updated to handle the win message and showing the win button. At the top of this method add the following:

this.winText.visible = false;
this.continueBtn.visible = false;

The actual code within the switch statement add the case for the UI\_WIN state as follows:

case this.UI_WIN:
    this.winText.color = this.PLAYER_COLORS[this.currentPlayer];
    this.winText.text = this.PLAYER_LABELS[this.currentPlayer] + "\nPlayer\nWINS!";
    this.winText.visible = true;
    this.continueBtn.visible = true;
    break;

Now the game is playable in hotseat, but it would be sure nice to be able to control the number of players, and be able to play against the computer. To do that we are going to need to delve into the world of Artificial Intelligence, at least as it applies to games. See you next month!