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.
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.
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.
The diagram below shows the relationship between the classes.
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 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:
By convention, packages names are all lowercase
Create a new Java project in Eclipse and name it PX_LastName_FirstName_GameDesign
.
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
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.
Repeat step 3 above to create the following classes, all of which belong in the engine package.
Let's start with the Game class.
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.
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
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.
Add the following constructor:
public Game(Board b) {
players = createPlayers();
board = b;
}
Add a startGame() method:
public void startGame() {
printRules();
playGame();
}
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, thegetNextPlayer()
method should respond accordingly. To do this, your game subclass would override the method to change the behavior.
Add these two getter methods:
public Player getActivePlayer() {
return activePlayer;
}
public Board getBoard() {
return board;
}
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.
Change the class declaration to
public abstract class Board {
Add the following instance variable. It is declared as protected
so that subclasses can access it directly.
protected Tile[][] board;
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();
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;
}
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
}
}
Change the class declaration to
public abstract class Player {
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.
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.
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 + ")");
}
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.
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.
Begin by creating a new package for the game. Name the package linerace
.
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();
}
}
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.
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;
}
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();
}
}
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.
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!");
}
}
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!
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:
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!
As demonstrated above, here's how to create your own game:
Tile t = new PortalTile(this);
List<Player> list = new ArrayList<Player>();
The tile subclasses are where you'll override the onMoveTo() method to define what happens when a Player lands on that kind of tile.
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.
The player subclasses are where you'll define different kinds of players. You'll need to override these methods:
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
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