Tino APCS

Lab 20.2 GameDesign

Introduction

With the exception of Spider Solitaire, most of the programs you have written this year did not require or ask you to think about class design. You've had to plan out code logic for specific methods but you haven't been asked to design the overall system and relationships between classes. In this lab, you will focus on understanding the design decisions involved in creating a larger program that is generalized and extendible.

Objective

Your objective is to design a system for making 2D, text-based board games. Once you build a general system, you can use it as a base to make an infinite number of games.

Class Design

First we need to think about the most general features that any 2D board game will have.

Board

Every game will have some kind of board to play on. Even if players put things down on a blank table and there is no board that players move on, the table itself can be considered the board that things go onto.

Player

Every game needs players. Players can keep track of whatever they need to.

Game

A Game has a board and players. Separating the game from its board and players is usually a good design. If designed properly, you can swap out different kinds of Players and Boards and run them on the same Game.

Class Diagram

The diagram below shows the relationship between the classes.

Class Diagram

Now, the Board has to keep track of its data in some kind of way. We will decide that a Board keeps track of a 2D array of Tile objects. Tiles represent spaces on the board. Think of the Board as a 2D surface where you can put things. These things are Tiles and not every array location must have a Tile. For example, you could have null or empty Tiles everywhere except for one Tile that holds a draw pile of cards and another Tile that holds a discard pile. Or, if you're making a chess game, every Tile could have a color (black/white) and possibly have a chess piece on it. Tiles can keep track of whatever they need to.

Packages

Packages are used in many programming languages. A package is just a subfolder used to separate code into related groups. In this lab, we will create at least two packages:

  1. A package for the core game engine classes.
  2. A package for each specific game you create that extends the engine.

By convention, packages names are all lowercase

Eclipse project

  1. Create a new Java project in Eclipse and name it PX_LastName_FirstName_GameDesign.

  2. Then right-click on your project folder and choose New --> Package.
    Name the package engine.

    If you can't see the package you just created, try switching to 'Package Explorer' instead of Project Explorer in Eclipse. To do this, go to Window --> Show View --> Other --> Java --> Package Explorer

  3. Finally, right-click your package and choose New --> Class.
    Name the class Game.

    Notice at the top of your Game class, Eclipse automatically inserted package engine; This is called a package declaration and it is required for all classes placed a package.

  4. Repeat step 3 above to create the following classes, all of which belong in the engine package.

    • Board
    • Player
    • Tile

Game class

Let's start with the Game class.

  1. Change the class declaration to

    public abstract class Game {
    

    Remember from the Lesson 20 reading that an abstract class is a base class used as a template for subclasses. Abstract classes usually have one or more abstract methods that are required to be overridden by subclasses. Unlike interfaces, abstract classes can include instance variables and non abstract methods that are fully defined.

  2. Add the following instance variables:

    private List<Player> players;   // Keeps track of all the players
    private Board board;            // Keeps track of the board
    private Player activePlayer;    // Keeps track of whose turn it is
    
  3. Add the following abstract method declarations.

    public abstract List<Player> createPlayers();
    public abstract boolean gameIsOver();
    public abstract void printRules();
    public abstract void printGameResults();
    

    Notice that all these methods are declared as abstract using the abstract keyword. In an abstract class, some methods are declared as abstract and only the method header is defined. This is similar to an interface where subclasses must override certain methods and define the exact behavior. This is the reason it is not possible to instantiate an abstract class. Abstract classes usually contain at least some abstract methods which are left undefined.

    createPlayers() Subclasses will override this method to initialize and create the starting players.

    gameIsOver() Subclasses will override this method to determine whether the game is over. The game can be over for a variety of reasons (won, lost, etc.) but this method only decides if the game has ended for any reason.

    printRules() Subclasses will override this method to print the game instructions and rules.

    printGameResults() Subclasses will override this method to print the result after a game has ended. The method should find out why the game ended and print a message accordingly.

  4. Add the following constructor:

    public Game(Board b) {
        players = createPlayers();
        board = b;
    }
    
  5. Add a startGame() method:

    public void startGame() {
        printRules();
        playGame();
    }
    
  6. Add the main game loop:

    private void playGame() {
        do {
            activePlayer = getNextPlayer();
            System.out.println("It's Player " + activePlayer.getPlayerNum() + "'s turn.");
            activePlayer.takeTurn();
            System.out.println("Turn finished.\n");
        } while (!gameIsOver());
        printGameResults();
    }
    

    Take a moment to read through the playGame() method logic. Every game will follow this pattern. Note that the getNextPlayer() method does not necessarily have to return the next player in the list of players. If we made an Uno card game and a player drew a reverse card, the getNextPlayer() method should respond accordingly. To do this, your game subclass would override the method to change the behavior.

  7. Add these two getter methods:

    public Player getActivePlayer() {
        return activePlayer;
    }
    
    public Board getBoard() {
        return board;
    }
    
  8. Finally, add these methods:

    // Returns a COPY of the player list so that the original cannot be messed with.
    public List<Player> getPlayers() {
        return new ArrayList<Player>(players);
    }
    
    // Default behavior: returns the next player in the player list, wrapping around to the first player at the end
    // You can override this method in your subclass to change the behavior
    public Player getNextPlayer() {
        return activePlayer == null ? players.get(0) : players.get((activePlayer.getPlayerNum() + 1) % players.size());
    }   
    

    Note that abstract classes can have some methods, like these, fully defined while leaving other methods up to subclasses.

Board class

  1. Change the class declaration to

    public abstract class Board {
    
  2. Add the following instance variable. It is declared as protected so that subclasses can access it directly.

    protected Tile[][] board;
    
  3. Add the following constructor and corresponding abstract method declaration. Subclasses of Board will override createBoard() to set up the Board however they want.

    public Board() {
        board = createBoard();
    }
    
    public abstract Tile[][] createBoard();
    
  4. Add the following instance methods:

    // Gets the Tile at (row, col) in this board
    public Tile getTile(int row, int col) {
        return board[row][col];
    }
    
    public int getNumRows() {
        return board.length;
    }
    
    public int getNumCols() {
        return board[0].length;
    }
    

Tile class

Update your Tile class using the code below. The Tile class was chosen not to be abstract so that you can create a default Tile that does nothing. You will create different subclasses of Tile for different kinds of tiles that can do whatever they want or hold whatever information they should hold.

package engine;

public class Tile {

    protected Board board;

    public Tile(Board b) {
        board = b;
    }

    public void onMoveTo(Player p) {
        // Does nothing by default
    }
}

Player class

  1. Change the class declaration to

    public abstract class Player {
    
  2. Add the following instance variables and constructors.
    Note that one instance variable is protected while the other two are private. Read the program comments to understand why. There are two constructors in case Players don't have a board position to start with.

    // Declared as protected to be directly accessible by subclasses
    protected Game game;
    
    // Declared as private so subclasses are forced to change these
    // using the moveTo(row, col) method.  Remember that moveTo is
    // part of our design and that it automatically notifies a Tile
    // to take action when a Player moves there.    
    private int row, col;
    
    public Player(Game g) {
        this.game = g;
        this.row = -1;  // We'll say -1 means no row
        this.col = -1;  // We'll say -1 means no col
    }
    
    public Player(Game g, int row, int col) {
        this.game = g;
        this.row = row;
        this.col = col;
    }
    

    Game is not a copy of the game, nor do Players create it. The Game instance variable is just a reference, or link, back to the Game object the player is already in.

  3. Add the following getters and abstract method

    // Getters
    public int getRow() {
        return row;
    }
    
    public int getCol() {
        return col;
    }
    
    public Game getGame() {
        return game;
    }
    
    // abstract method left to subclasses to define
    public abstract void takeTurn();
    

    Note: takeTurn is abstract because it depends on your game. Player subclasses will decide what to do on their turn.

  4. Add the following methods and read the comment headers to learn what they do.

    // Player number is this player's index position in the game's player list
    public int getPlayerNum() {
        return game.getPlayers().indexOf(this);
    }
    
    // This method should be called/used by your player's takeTurn method.
    // If your player moves, call this method so that the Tile the player is
    // on can decide what to do about it.
    public void moveTo(int row, int col) {
        printMoveToMessage(this.row, this.col, row, col);
        this.row = row;
        this.col = col;
        game.getBoard().getTile(row, col).onMoveTo(this);
    }
    
    // This method is used by moveTo() to print info whenever the player moves
    public void printMoveToMessage(int fromRow, int fromCol, int toRow, int toCol) {
        System.out.println("Player " + getPlayerNum() + " moved from (" + fromRow + ", " + fromCol +
                           ") to (" + toRow + ", " + toCol + ")");
    }
    

Compiler errors

If you followed everything above correctly, you will not have any compiler errors. If you do, please stop and review the instructions to find out what you missed. Continue once everything in your engine package compiles.

LineRaceGame

As a demonstration using the game engine above, we will make a sample game called LineRace. This game is not fancy in any way but it will help you understand how to make your own game.

  1. Begin by creating a new package for the game. Name the package linerace.

  2. Add the following Driver class to the linerace package:

    package linerace;
    
    public class Driver {
    
        public static void main(String[] args) {
            LineRaceBoard b = new LineRaceBoard();
            LineRaceGame g = new LineRaceGame(b);
            g.startGame();
        }
    
    }
    
  3. It's important that you understand how this system works.
    (a) A LineRaceBoard is a subclass of Board that creates a 1-dimensional game by ignoring all but the first row of the 2D Tile[][] array in Board.
    (b) It is needed as a parameter for the LineRaceGame class and any Game subclass.
    (c) When startGame() is called, the following method from the parent Board class is called:

    public void startGame() {
        printRules();
        playGame();
    }
    

    (d) printRules() is straightforward. It is responsible for printing game instructions and how to play.
    (e) playGame() is the main game loop. It is defined in the parent Board class:

    private void playGame() {
        do {
            // By default the next player in the player list goes next, cycling back to 0
            activePlayer = getNextPlayer();
            System.out.println("It's Player " + activePlayer.getPlayerNum() + "'s turn.");
    
            // Players must override this method and say what to do on their turn.
            // Your takeTurn method should call moveTo(int row, int col) if it makes sense for your game.
            // Calling moveTo (see Player class) automatically prints a move message and tells the Tile the player moved to
            // to take action, if needed.
            activePlayer.takeTurn(); 
    
            System.out.println("Turn finished.\n");
        } while (!gameIsOver());
    
        printGameResults();
    }
    

    (f) Once the player's turn is finished, the game state is checked for gameIsOver() and if it's not game over, the next player takes a turn. This process continues until the game ends for any reason.

  4. Create a new subclass of Player in the linerace package:

    public class LineRacePlayer extends Player {    
    

    Let's say we want players to get stuck sometimes, being forced to skip one or more turns. We can keep track of this using an instance variable:

    private int numTurnsFrozen;
    

    Because our Parent superclass does not have a default constructor, we must explicity create a constructor.

    public LineRacePlayer(Game g, int row, int col) {
        super(g, row, col);
    }
    

    Add the remaining methods and study them to see how they work.

    // We were forced to override this method because it was abstract in our parent.
    // This method either skips a turn if the player is frozen or rolls a 3-sided
    // die and moves that many tiles on the board.
    @Override
    public void takeTurn() {
        if (numTurnsFrozen > 0) {
            System.out.println("Player " + getPlayerNum() + " is in a Pit.");
            numTurnsFrozen--;
        }
        else {
            int randNum = (int)(Math.random() * 3 + 1);
            int newCol = Math.min(game.getBoard().getNumCols() - 1,  getCol() + randNum);
            System.out.println("Player " + getPlayerNum() + " rolled a " + randNum + ".");
            this.moveTo(getRow(), newCol);
        }
    }
    
    // We were not forced to override this method because it was not abstract.
    // However, we are choosing to override it to change the way it prints
    // information in 1D format rather than 2D.
    @Override
    public void printMoveToMessage(int fromRow, int fromCol, int toRow, int toCol) {
        System.out.println("Player " + getPlayerNum() + " moved from tile " + fromCol +
                           " to tile " + toCol + ".");
    }
    
    public int getNumTurnsFrozen() {
        return numTurnsFrozen;
    }
    
    public void setNumTurnsFrozen(int numTurnsFrozen) {
        this.numTurnsFrozen = numTurnsFrozen;
    }
    
  5. Let's make some special Tiles that do something to a Player when they land on the tile.

    PitTile
    Create a new subclass of Tile in the linerace package named PitTile. This will be a Tile that makes players lose a turn.

    package linerace;
    
    // In order to use classes in another package, you must import them
    import engine.Board;
    import engine.Player;
    import engine.Tile;
    
    public class PitTile extends Tile {
    
        public PitTile(Board b) {
            super(b);
        }
    
        @Override
        public void onMoveTo(Player p) {
            // onMoveTo receives a Player parameter, but our player is a subclass of player: LineRacePlayer
            // We need to cast it back into its actual type in order to call LineRacePlayer methods
            LineRacePlayer lp = (LineRacePlayer)p;
            lp.setNumTurnsFrozen(1);
            System.out.println("Player " + p.getPlayerNum() + " fell in a Pit and lost 1 turn.");
        }
    }
    

    ExtraTurnTile
    Create a new subclass of Tile in the linerace package named ExtraTurnTile. This will be a Tile that gives players an extra turn.

    package linerace;
    
    import engine.Board;
    import engine.Player;
    import engine.Tile;
    
    public class ExtraTurnTile extends Tile {
    
        public ExtraTurnTile(Board b) {
            super(b);
        }
    
        @Override
        public void onMoveTo(Player p) {
            System.out.println("Player " + p.getPlayerNum() + " gets an extra turn.");
            // Extra turn happens here
            p.takeTurn();
        }
    }
    
  6. Create a new subclass of Board named LineRaceBoard in the linerace package:

    public class LineRaceBoard extends Board {
    

    Remember that at the top of this class, you need a package declaration and import statements for any classes you want to use in the engine package.

    Add the following method. This method creates and returns the 2D array that becomes part of our Board

    @Override
    public Tile[][] createBoard() {
        Tile[][] tiles = new Tile[1][10];
        tiles[0][0] = new Tile(this);
        tiles[0][tiles[0].length - 1] = new Tile(this);
        for (int i = 1; i < tiles[0].length - 1; i++) {
            double rand = Math.random();
            Tile tile;
            if (rand < 0.3) tile = new PitTile(this);
            else if (rand < 0.5) tile = new ExtraTurnTile(this);
            else tile = new Tile(this);
            tiles[0][i] = tile;
        }
        return tiles;
    }
    

    When creating your own board, make sure you initialize a new board array AND fill every spot with some kind of Tile. If you have a specific reason to leave some locations empty (as null), that's ok as long as it won't crash your program.

  7. Finally, create a subclass of Game named LineRaceGame in the linerace package:

    package linerace;
    
    import java.util.ArrayList;
    import java.util.List;
    
    import engine.Board;
    import engine.Game;
    import engine.Player;
    
    public class LineRaceGame extends Game {
    
        public LineRaceGame(Board b) {
            super(b);
        }
    
        // Creates the initial Players and returns them in a list.
        // This method is used by the Game constructor to initialize
        // the list of players.
        @Override
        public List<Player> createPlayers() {
            LineRacePlayer p1 = new LineRacePlayer(this, 0, 0);
            LineRacePlayer p2 = new LineRacePlayer(this, 0, 0);
            List<Player> list = new ArrayList<Player>();
            list.add(p1);
            list.add(p2);
            return list;
        }
    
        // The game is over when a player reaches the last tile
        @Override
        public boolean gameIsOver() {
            return getActivePlayer().getCol() >= getBoard().getNumCols() - 1;
        }
    
        @Override
        public void printRules() {
            System.out.println("In 'LineRaceGame' players roll a 3-sided die to");
            System.out.println("decide how many spaces to move. There are 10 tiles");
            System.out.println("numbered 0 to 9 and there are three types of tiles:");
            System.out.println("  Pit tiles - You lose a turn");
            System.out.println("  Extra turn tiles - You get an extra turn");
            System.out.println("  Normal tiles - Nothing special happens");
            System.out.println("The game runs automatically until a player reaches");
            System.out.println("the last tile.\n");
        }
    
        // When the game ends, print out why
        @Override
        public void printGameResults() {
            System.out.println("Player " + getActivePlayer().getPlayerNum() + " won!");
        }
    }
    
  8. You should be able to run the game by running the Driver. The game plays automatically. Here is a sample run:

    In 'LineRaceGame' players roll a 3-sided die to
    decide how many spaces to move. There are 10 tiles
    numbered 0 to 9 and there are three types of tiles:
        Pit tiles - You lose a turn
        Extra turn tiles - You get an extra turn
        Normal tiles - Nothing special happens
    The game runs automatically until a player reaches
    the last tile.
    
    It's Player 0's turn.
    Player 0 rolled a 1.
    Player 0 moved from tile 0 to tile 1.
    Player 0 gets an extra turn.
    Player 0 rolled a 2.
    Player 0 moved from tile 1 to tile 3.
    Player 0 gets an extra turn.
    Player 0 rolled a 1.
    Player 0 moved from tile 3 to tile 4.
    Player 0 gets an extra turn.
    Player 0 rolled a 2.
    Player 0 moved from tile 4 to tile 6.
    Player 0 fell in a Pit and lost 1 turn.
    Turn finished.
    
    It's Player 1's turn.
    Player 1 rolled a 3.
    Player 1 moved from tile 0 to tile 3.
    Player 1 gets an extra turn.
    Player 1 rolled a 3.
    Player 1 moved from tile 3 to tile 6.
    Player 1 fell in a Pit and lost 1 turn.
    Turn finished.
    
    It's Player 0's turn.
    Player 0 is in a Pit.
    Turn finished.
    
    It's Player 1's turn.
    Player 1 is in a Pit.
    Turn finished.
    
    It's Player 0's turn.
    Player 0 rolled a 1.
    Player 0 moved from tile 6 to tile 7.
    Turn finished.
    
    It's Player 1's turn.
    Player 1 rolled a 3.
    Player 1 moved from tile 6 to tile 9.
    Turn finished.
    
    Player 1 won!
    

Assignment

Your assignment is to write a custom game using the engine package. Your game can play automatically like the Line Race Game or you can have human players. If you have computer players, it's ok just to have them make random legal moves. Create a new package for your game, just like you did for the Line Race Game. Here are some game ideas:

  • A classic 2-player board game like Tic Tac Toe, Connect 4, Checkers, or Chess.
  • An adventure game where the board tiles represent things like grass, dirt, houses, caves, etc. Keep in mind that Tile subclasses can contain any number of things, so a player could be on a CaveTile that contains a Treasure item and an NPC Player that interacts with the main player.
  • A board game like Candyland. This is similar to the Line Race Game, but it's 2D and has more kinds of tiles and rules.

Here is example output from an automatically-running Tic Tac Toe game your APCS teachers wrote using the engine:

In 'Tic Tac Toe' there are two players trying to get
three-in-a-row on a 3x3 board.  Player 'X' draws Xs
on the board and Player 'O' draws Os on the board.
Players X goes first and the game ends when either
one player gets three of their symbol in a row, col,
or diagonal - or when there are no more moves available.

It's Player 0's turn.
Player 0 put an X at (2, 1).
_ _ _ 
_ _ _ 
_ X _ 

Turn finished.

It's Player 1's turn.
Player 1 put an O at (2, 2).
_ _ _ 
_ _ _ 
_ X O 

Turn finished.

It's Player 0's turn.
Player 0 put an X at (2, 0).
_ _ _ 
_ _ _ 
X X O 

Turn finished.

It's Player 1's turn.
Player 1 put an O at (1, 0).
_ _ _ 
O _ _ 
X X O 

Turn finished.

It's Player 0's turn.
Player 0 put an X at (0, 1).
_ X _ 
O _ _ 
X X O 

Turn finished.

It's Player 1's turn.
Player 1 put an O at (0, 0).
O X _ 
O _ _ 
X X O 

Turn finished.

It's Player 0's turn.
Player 0 put an X at (0, 2).
O X X 
O _ _ 
X X O 

Turn finished.

It's Player 1's turn.
Player 1 put an O at (1, 1).
O X X 
O O _ 
X X O 

Turn finished.

Player 1 won!

Game creation walkthough

As demonstrated above, here's how to create your own game:

  1. Create subclasses of Board, Game, Player, and Tile to create specific kinds of them.
  2. The board subclass is where you'll override createBoard() to create the initial board.
    • When creating specific kinds of tiles, declare them as Tile but instantiate them as a specific kind.
    • For example, Tile t = new PortalTile(this);
    • You can add any methods you feel your board should be responsible for. An example might be a method that returns a list of possible Tiles a player can move to from a given location (r, c).
    • If you want a way to display the board, you can override toString().
  3. The game subclass is where you'll override createPlayers() to create the initial players and return a list of them.
    • List is an abstract class in Java, so you will need to declare as List but instantiate as a specific kind of list such as an ArrayList.
    • For example, List<Player> list = new ArrayList<Player>();
    • Other methods in this class you should override: the constructor, gameIsOver(), printRules(), printGameResults()
  4. The tile subclasses are where you'll override the onMoveTo() method to define what happens when a Player lands on that kind of tile.

    • Because the parameter is a general Player but the actual data being passed in is some kind of specifc player subclass, you will need to typecast the parameter back into its specific type in order to run any special methods your player subclass may have.
    • For example, EnemyPlayer ep = (EnemyPlayer)p;
    • If you have different kinds of player subclasses, you can check which kind of Player p is using the instanceof Java keyword.

      if (p instanceof EnemyPlayer)
          // then typcast to EnemyPlayer and do what you want
      else if (p instanceof MainPlayer)
          // then typecast to MainPlayer and do what you want
      
    • Remember that tiles can do whatever they want and keep track of whatever they want. Add attributes to your tile subclasses as needed.

  5. The player subclasses are where you'll define different kinds of players. You'll need to override these methods:

    • A constructor. Remember that the first line of any subclass constructor must be a call to the parent constructor using super(...)
    • takeTurn() decides where the player will move to next. You can ask the board questions by getting a reference to it. Here's an example:

      CandylandBoard b = (Candyland)(game.getBoard());
      if (b.someMethod()) {
          // do something
      }
      
    • printMoveMessage() is where you'll print out what happens when a player moves from a location to another location

    • Add any other methods and attributes your player needs.
  6. You need a Driver class that
    • Creates a specific kind of board
    • Creates a new game, passing the board as a parameter
    • Calls startGame() on the game object

File submission

Compress your entire project folder into a ZIP file and upload the compressed folder. The folder name should be PX_LastName_FirstName_GameDesign.zip

You must Sign In to submit to this assignment

Last modified: February 15, 2023

Back to Lab 20.1 Old Macdonald

Dark Mode

Outline