Using Commands to Build a Proximity Game


The following sample application uses the Command pattern to build a game called Proximity. The game consists of a grid of hexagonal pieces arranged so that each piece is adjacent to 6 pieces (unless the piece is on the edge). The game generally requires two or more players. The game play is as follows.

  1. A new game piece is displayed for a game player. The game piece has a numeric value ranging from 2 to 20.

  2. The game player clicks on an unoccupied grid space to apply the game piece settings. That grid space then belongs to the game player, and the numeric value is applied to that space.

  3. If any of the adjacent grid spaces already belongs to a different player, then a comparison is run between the newly occupied grid space and the adjacent spaces belonging to different players. If the newly occupied space has a higher numeric value than an adjacent space, the owner of the newly occupied space takes ownership of the adjacent space.

  4. If any of the adjacent spaces belongs to the same game player as the newly occupied space, those spaces are fortified by adding 1 to their numeric values.

  5. Steps 1 through 4 repeat until all grid spaces are occupied.

The application requires a fair number of classes, which we'll build in the following sections.

Defining the Player Data Class

Every game has two or more players. Therefore, we'll first define the class that will serve as the data model for each game player. The GamePlayer class basically just stores the color for the player (each game player must be represented with a unique color on the board).

package com.peachpit.aas3wdp.proximity.data {    public class GamePlayer {       private var _color:uint;       public function set color(value:uint):void {          _color = value;       }       public function get color():uint {          return _color;       }       public function GamePlayer() {          _color = 0xEEEEEE;       }    } }


In addition to the standard game player type, we'll also define a null player using NullPlayer. The NullPlayer class extends GamePlayer so that it looks just like a standard player. However, it is a special case we can use in place of an actual player.

package com.peachpit.aas3wdp.proximity.data {    public class NullOwner extends GamePlayer {       } }


We'll use NullPlayer objects as the default owners for all pieces on the board until another game player takes ownership.

Defining a Collection Class for the Game Players

Every game has a collection of players. To keep track of the game players we'll build a new collection class called GamePlayers. The following code defines a Singleton class called com.peachpit.aas3wdp.proximity.data.GamePlayers to serve as this collection:

package com.peachpit.aas3wdp.proximity.data {    import com.peachpit.aas3wdp.proximity.data.GamePlayers;    import com.peachpit.aas3wdp.proximity.data.GamePlayer;    import com.peachpit.aas3wdp.iterators.IIterator;    import com.peachpit.aas3wdp.iterators.ArrayIterator;    public class GamePlayers {       private var _data:Array;       private static var _instance:GamePlayers;       private static const COLORS:Array = [0xFFCCCC, 0xCCFFCC, 0xCCCCFF, 0xFFFFCC,          0xCCFFFF, 0xFFCCFF];       public function GamePlayers(parameter:SingletonEnforcer) {          _data = new Array();       }       public static function getInstance():GamePlayers {          if(_instance == null) {             _instance = new GamePlayers(new SingletonEnforcer());          }          return _instance;       }       public function addGamePlayer(gamePlayer:GamePlayer):void {          gamePlayer.color = COLORS[_data.length];          _data.push(gamePlayer);       }           public function iterator():IIterator {          return new ArrayIterator(_data);       }    } } class SingletonEnforcer {}


Note that this class has just two instance methods: addGamePlayer() to add game player instances and iterator() to retrieve an iterator to access the collection.

Defining Game Pieces

Now that we've defined the game player classes and created a collection for them, we next need to define another basic building block of the game: the game pieces. The com.peachpit.aas3wdp.proximity.data.PieceData class serves as the data model for game pieces and grid spaces.

package com.peachpit.aas3wdp.proximity.data {    import flash.events.EventDispatcher;    import flash.events.Event;    import com.peachpit.aas3wdp.proximity.data.GamePlayer;    import com.peachpit.aas3wdp.proximity.data.NullOwner;    public class PieceData extends EventDispatcher {       protected var _row:int;       protected var _column:int;       protected var _count:uint;        protected var _owner:GamePlayer;       protected var _radius:Number;       // Keep track of the radius to use for the game piece       // when it is displayed.       public function set radius(value:Number):void {          _radius = value;          dispatchEvent(new Event(Event.CHANGE));       }       public function get radius():Number {          return _radius;       }       // Keep track of the count (the value) for the game piece.       public function set count(value:uint):void {          _count = value;          dispatchEvent(new Event(Event.CHANGE));       }       public function get count():uint {          return _count;       }       // Every game piece belongs to a game player.       public function set owner(value:GamePlayer):void {          _owner = value;          dispatchEvent(new Event(Event.CHANGE));       }       public function get owner():GamePlayer {          return _owner;       }       // Which row is the game piece in?       public function set row(value:int):void {          _row = value;       }       public function get row():int {          return _row;       }       // Which column is the game piece in?       public function set column(value:int):void {          _column = value;       }           public function get column():int {          return _column;       }       // Use the constructor to set default property values.       public function PieceData() {          _row = -1;          _column = -1;          _count = 0;             // Use a NullOwner by default.          _owner = new NullOwner();       }     } }


This class is yet another basic data model class. This time, however, it's important to note that PieceData inherits from EvenTDispatcher; when the values change, it dispatches events notifying listeners that the data model has changed.

Defining the Game Board Data Class

The game needs a game board. Our game board data model keeps track of all the pieces, placing them in rows and columns. Because there can be only one game board per game, the game board data model class is defined as a Singleton class.

package com.peachpit.aas3wdp.proximity.data {    import com.peachpit.aas3wdp.proximity.data.PieceData;    import com.peachpit.aas3wdp.proximity.data.GameboardData;    import com.peachpit.aas3wdp.iterators.IIterator;    import flash.events.EventDispatcher;    import flash.events.Event;    import com.peachpit.aas3wdp.iterators.ArrayIterator;    public class GameboardData extends EventDispatcher {       private var _pieces:Array;       private var _rows:uint;       private var _columns:uint;       private var _newGamePiece:PieceData;       private var _iterator:IIterator;       private static var _instance:GameboardData;       // Set the number of rows in the game board.       public function set rows(value:uint):void {          _rows = value;          update();       }       // Set the number of columns in the garme board.       public function set columns(value:uint):void {          _columns = value;          update();       }       // Request a new game piece. The game board is responsible       // for returning the next game piece to play.       public function get newGamePiece():PieceData {          return _newGamePiece;       }       // Set defaults for all the properties.       public function GameboardData(parameter:SingletonEnforcer) {          _rows = 10;          _columns = 10;          _newGamePiece = new PieceData();          _newGamePiece.radius = 40;          _iterator = GamePlayers.getInstance().iterator();          update();       }       public static function getInstance():GameboardData {          if(_instance == null) {             _instance = new GameboardData(new SingletonEnforcer());          }          return _instance;       }       // Re-add the game pieces. This method is called every time       // one of the properties changes (columns, rows, etc.) This       // code then creates all the game pieces, sets the rows and       // columns, and adds the pieces to the pieces array.       public function update():void {          var i:uint;          var j:uint;          var piece:PieceData;          _pieces = new Array();          for(i = 0; i < _rows; i++) {             for(j = 0; j < _columns; j++) {                piece = new PieceData();                piece.row = i;                piece.column = j;                piece.radius = 20;                addPiece(piece);             }       }       dispatchEvent(new Event(Event.CHANGE));    }    private function addPiece(piece:PieceData):void {       if(_pieces[piece.row] == null) {          _pieces[piece.row] = new Array();       }       _pieces[piece.row][piece.column] = piece;    }    // Return an iterator that allows access to each game     // piece.    public function iterator():IIterator {       var pieces:Array = new Array();       var i:uint;       var j:uint;       for(i = 0; i < _rows; i++) {          for(j = 0; j < _columns; j++) {             pieces.push(_pieces[i][j]);          }       }       return new ArrayIterator(pieces);       }       // Calculate all the pieces that are adjacent to a given        // piece, and return an iterator that allows access to        // those pieces.       public function getProximityPieces(piece:PieceData):IIterator {          var pieces:Array = new Array();          var row:uint = piece.row;          var column:uint = piece.column;          if(piece.row > 0) {             pieces.push(_pieces[row - 1][column]);             if(row % 2 == 0 && column > 0) {                pieces.push(_pieces[row - 1][column - 1]);             }             else if(row % 2 != 0 && column < _pieces[row - 1].length - 1) {                pieces.push(_pieces[row - 1][column + 1]);             }          }          if(piece.column > 0) {             pieces.push(_pieces[row][column - 1]);          }          if(column < _pieces[row].length - 1) {             pieces.push(_pieces[row][column + 1]);          }          if(row < _pieces.length - 1) {             pieces.push(_pieces[row + 1][column]);             if(row % 2 == 0 && column > 0) {                pieces.push(_pieces[row + 1][column - 1]);             }             else if(row % 2 != 0 && column < _pieces[row + 1].length - 1) {                pieces.push(_pieces[row + 1][column + 1]);             }          }          return new ArrayIterator(pieces);       }       // Advance to the next game piece to play.       public function nextGamePiece():void {          if(!_iterator.hasNext()) {             _iterator.reset();          }          _newGamePiece.count = Math.round(Math.random() * 18) + 2;          _newGamePiece.owner = GamePlayer(_iterator.next());          if(!_iterator.hasNext()) {             _iterator.reset();          }       }    } } class SingletonEnforcer {}


The GameboardData class is responsible for several things. First, it is responsible for keeping track of all the game pieces. Additionally, it is responsible for determining what game pieces are adjacent to other game pieces. And it is also responsible for keeping track of the game piece that can next be played. The nextGamePiece() method accomplishes this task by retrieving the next item from the game player iterator and generating a random number from 2 to 20, assigning those values to the _newGamePiece instance.

Defining the Game Play Command Class

Now that we've defined all the data model classes, we'll next create the command class used for game play. The com.peachpit.aas3wdp.proximity.commands.GamePlayCommand class encapsulates the command for game play.

package com.peachpit.aas3wdp.proximity.commands {    import com.peachpit.aas3wdp.proximity.data.PieceData;    import com.peachpit.aas3wdp.proximity.data.GamePlayer;    import com.peachpit.aas3wdp.proximity.data.GamePlayers;    import com.peachpit.aas3wdp.proximity.data.GameboardData;    import com.peachpit.aas3wdp.proximity.data.NullOwner;    import com.peachpit.aas3wdp.commands.ICommand;    import com.peachpit.aas3wdp.iterators.IIterator;       public class GamePlayCommand implements ICommand {       protected var _piece:PieceData;       public function GamePlayCommand(piece:PieceData) {          _piece = piece;       }       public function execute():void {          var gameboard:GameboardData = GameboardData.getInstance();          var newGamePiece:PieceData = gameboard.newGamePiece;          var currentGamePlayer:GamePlayer = newGamePiece.owner;          // If the game piece's owner is a NullOwner (and           // only if) then it's a valid click, so apply the           //command.          if(_piece.owner is NullOwner) {             _piece.owner = currentGamePlayer;             _piece.count = newGamePiece.count;                          // Retrieve all adjacent pieces.             var iterator:IIterator = gameboard.getProximityPieces(_piece);             var piece:PieceData;             while(iterator.hasNext()) {                piece = iterator.next() as PieceData;                // If the game piece has the same                 // owner as the clicked game piece,                // increment the count. If they have                 // different owners (and the owner                // isn't NullOwner) then test if the                 // clicked game piece has a higher                // count. If so, make it the new                 // owner.                if(piece.owner == _piece.owner) {                   piece.count++;                }                else if(!(piece.owner is NullOwner)) {                   if(piece.count < _piece.count) {                      piece.owner = currentGamePlayer;                   }                }             }             // Get the next game piece.             GameboardData.getInstance().nextGamePiece();           }        }     } }


In this command type, the game piece is the receiver. When the user triggers the execute() method, the method requests the new game piece from the game board and applies it to the receiver. The method also requests all the adjacent pieces and uses game play rules to determine how and if to change those values.

Defining the Game Factory Class

In the next chapter, we'll update the application by adding undo and redo functionality in the context of a our discussion of the Memento pattern. To minimize the impact of those future changes to the code we're creating now, we'll use a factory (see Chapter 5, "Factory Method Pattern") to make the command objects. Define com.peachpit.aas3wdp.proximity.commands.CommandFactory as follows:

package com.peachpit.aas3wdp.proximity.commands {    import com.peachpit.aas3wdp.commands.ICommand;    import com.peachpit.aas3wdp.proximity.commands.GamePlayCommand;    import com.peachpit.aas3wdp.proximity.data.PieceData;    public class CommandFactory {       private static var _type:String = NORMAL;       public static const NORMAL:String = "normal";       public static const UNDOABLE:String = "undoable";       public static const REDOABLE:String = "redoable";       public static function set type(value:String):void {          _type = value;       }       public static function getGamePlayCommand(data:PieceData):ICommand {          if(_type == NORMAL) {             return new GamePlayCommand(data);          }          return null;       }    } }


This class allows us to globally set the type of commands it should create. Then we can use getGamePlayCommand() to request the command for a specific receiver. Currently, we're only ever returning one type, but subsequently, we'll enable undoable and redoable versions.

Defining the Game Piece View and Controller Class

The com.peachpit.aas3wdp.proximity.views.Piece class is the view (and controller) for the game pieces/grid spaces. The Piece class uses a PieceData object as its data model, and it draws itself based on the data model values. It also stores a command object that it executes when the user clicks the object.

package com.peachpit.aas3wdp.proximity.views {    import flash.display.Sprite;    import flash.text.TextField;    import flash.events.Event;    import flash.events.MouseEvent;    import flash.text.TextFormat;    import flash.text.TextFieldAutoSize;    import com.peachpit.aas3wdp.proximity.data.PieceData;    import com.peachpit.aas3wdp.proximity.commands.CommandFactory;    import com.peachpit.aas3wdp.commands.ICommand;    public class Piece extends Sprite {       private var _background:Sprite;       private var _label:TextField;       private var _data:PieceData;       private var _command:ICommand;       public function set data(value:PieceData):void {          _data = value;          _data.addEventListener(Event.CHANGE, draw);          // Retrieve the command from the factory.          _command = CommandFactory.getGamePlayCommand(_data);          draw();       }       public function get data():PieceData {          return _data;       }       public function Piece() {        // Listen for mouse events.        addEventListener(MouseEvent.MOUSE_OVER, onMouseOver);        addEventListener(MouseEvent.MOUSE_OUT, onMouseOut);        addEventListener(MouseEvent.CLICK, onClick);         // Create the background into which to draw the          // hexagon.         _background = new Sprite();         addChild(_background);          // Create the text field into which to display the           // count.          _label = new TextField();          addChild(_label);          _label.selectable = false;          _label.autoSize = TextFieldAutoSize.LEFT;        }        // Draw the game piece based on the data model.        public function draw(event:Event = null):void {           var color:uint = _data.owner.color;           var newX:Number;           var newY:Number;           var angle:Number = -Math.PI / 6;           var angleDelta:Number = Math.PI / 3;           _background.graphics.clear();           _background.graphics.lineStyle(0, 0, 0);           _background.graphics.beginFill(color, 1);           newX = Math.cos(angle) * _data.radius;           newY = Math.sin(angle) * _data.radius;           _background.graphics.moveTo(newX, newY);           for(var i:uint = 0; i < 6; i++) {               angle += angleDelta;               newX = Math.cos(angle) * _data.radius;               newY = Math.sin(angle) * _data.radius;               _background.graphics.lineTo(newX, newY);            }            _background.graphics.endFill();            if(_data.row != -1) {               x = (_data.row % 2 == 0 ? 0 : _data.radius) + _data.column * _data.radius * 2;               y = _data.row * _data.radius * 2;            }            _label.text = String(_data.count);            _label.x = -_label.width / 2;            _label.y = -_label.height / 2;         }         private function onMouseOver(event:MouseEvent):void {            _background.alpha = .1;         }          private function onMouseOut(event:MouseEvent):void {             _background.alpha = 1;          }          // When the user clicks on the game piece, call the           // command's execute() method          private function onClick(event:MouseEvent):void {             _command.execute();          }       }    }


The key thing about this class is that it uses a command object to neatly encapsulate its behavior. When the user clicks the piece, it executes the command. However, the exact command implementation might change because we can simply change what is getting returned by the factory (as we'll see in subsequent versions of this application).

Defining the Game Board View and Controller

The game board also requires a view and controller, for which we'll define com.peachpit.aas3wdp.proximity.Gameboard. The Gameboard class uses a GameboardData object as its data model.

package com.peachpit.aas3wdp.proximity.views {    import flash.display.Sprite;    import com.peachpit.aas3wdp.proximity.data.GameboardData;    import com.peachpit.aas3wdp.iterators.IIterator;    import com.peachpit.aas3wdp.proximity.data.PieceData;    import flash.events.Event;    public class Gameboard extends flash.display.Sprite {       private var _data:GameboardData;       private var _newGamePiece:Piece;       public function set data(value:GameboardData):void {          _data = value;          onUpdate();          // Redraw the gameboard every time the data model           // changes.          _data.addEventListener(Event.CHANGE, onUpdate);       }         public function Gameboard() {       }       private function onUpdate(event:Event = null):void {          _pieces = new Sprite();          addChild(_pieces);          var iterator:IIterator = _data.iterator();          var piece:Piece;          while(iterator.hasNext()) {             piece = new Piece();             piece.data = PieceData(iterator.next());             _pieces.addChild(piece);          }          if(_newGamePiece == null) {          // The new game piece shows what piece can           // next be played.          _newGamePiece = new Piece();          _newGamePiece.data = _data.newGamePiece;          _newGamePiece.data.radius = 40;          addChild(_newGamePiece);       }       _newGamePiece.x = _pieces.width / 2;       _newGamePiece.y = _pieces.height + _pieces.y + 40;     }   } }


Because most of the work is already handled in the data model classes and in the game piece view/controller, the implementation for Gameboard is relatively simple. All it has to do is add the game pieces based on the data model, and it has to display the new game piece as well.

Defining the Main Class

Next we have to create a main class to put the application together and test it. The main class for the application is called Proximity and is defined as follows:

package {    import flash.display.Sprite;    import flash.display.StageScaleMode;    import flash.display.StageAlign;    import flash.events.MouseEvent;    import com.peachpit.aas3wdp.commands.ICommandStack;    import com.peachpit.aas3wdp.proximity.views.Piece;    import com.peachpit.aas3wdp.proximity.data.GameboardData;    import com.peachpit.aas3wdp.proximity.data.PieceData;    import com.peachpit.aas3wdp.proximity.data.GamePlayer;    import com.peachpit.aas3wdp.proximity.data.GamePlayers;    import com.peachpit.aas3wdp.proximity.data.NullOwner;    import com.peachpit.aas3wdp.proximity.commands.CommandFactory;    import com.peachpit.aas3wdp.iterators.IIterator;    import com.peachpit.aas3wdp.iterators.NullIterator;    import com.peachpit.aas3wdp.commands.ICommand;    import com.peachpit.aas3wdp.proximity.views.Gameboard;    public class Proximity extends Sprite {       private var _newGamePiece:Piece;       public function Proximity() {          // Set the command type. The valid types are NORMAL,           // UNDOABLE, and REDOABLE. This determines          // what sort of commands the factory returns.          CommandFactory.type = CommandFactory.NORMAL;          // Set the stage scaleMode and align properties.          stage.scaleMode = StageScaleMode.NO_SCALE;          stage.align = StageAlign.TOP_LEFT;          // Add a new gameboard and its datamodel.          var gameboard:Gameboard = new Gameboard();          var gameboardData:GameboardData = GameboardData.getInstance();          // Set the number of columns for the gameboard to 20           // (the default is 10).          gameboardData.columns = 20;          gameboard.data = gameboardData;          addChild(gameboard);          gameboard.x = 20;          gameboard.y = 20;                    // Add game players.          var gamePlayers:GamePlayers = GamePlayers.getInstance();          gamePlayers.addGamePlayer(new GamePlayer());          gamePlayers.addGamePlayer(new GamePlayer());          GameboardData.getInstance().nextGamePiece();       }    } }


When you test the application, you will see an image like the one shown in Figure 10.1.

Clicking a grid space applies the game piece settings (the player who owns the piece and the value of the piece) by calling the command object's execute() method. The execute() method also advances the game play to the next player.

Figure 10.1. The Proximity gameboard with 10 rows and 20 columns.





Advanced ActionScript 3 with Design Patterns
Advanced ActionScript 3 with Design Patterns
ISBN: 0321426568
EAN: 2147483647
Year: 2004
Pages: 132

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net