CS 489.02 - Computers and Games - Spring 2009
Model/View/Controller


Loyola College > Department of Computer Science > Dr. James Glenn > CS 489.02 > Examples and Lecture Notes > Model/View/Controller

CantStopBoard.java is incomplete and FourDice.java is not given. Both are available in compiled form in the archive.

CantStopGame.java

import java.util.*;

/**
 * A game of solitaire Can't Stop.  Game = board + dice.
 *
 * @author Jim Glenn
 * @version 0.1 11/18/2008
 */

public class CantStopGame extends Observable
{
    /**
     * The board.
     */

    private CantStopBoard board;

    /**
     * The dice.
     */

    private FourDice dice;

    /**
     * The current state of the game.  The possible states are
     * start of turn, selecting move, acknowledging blowing it, deciding
     * whether to roll again, or finished.
     */

    private int state;
    
    /**
     * The 1-based index of the current turn.
     */

    private int turn;

    /**
     * Constants for states.
     */

    public static final int START_STATE = 0;
    public static final int MOVE_STATE = 1;
    public static final int ACK_STATE = 2;
    public static final int WON_STATE = 3;
    public static final int ROLL_STATE = 4;

    /**
     * Creates a new game.
     */

    public CantStopGame()
    {
	board = new CantStopBoard();
	dice = null;
	state = START_STATE;
	turn = 1;
    }

    /**
     * Returns a textual representation of the dice in this game.  If
     * the dice have not been rolled, this method returns the empty
     * string.
     *
     * @return a textual representation of the dice
     */

    public String getDice()
    {
	if (dice != null)
	    {
		return "" + dice.getDie(0) + dice.getDie(1) + dice.getDie(2) + dice.getDie(3);
	    }
	else
	    {
		return "";
	    }
    }

    /**
     * Rolls this game's dice.
     *
     * @throws IllegalStateException if this game is not in a state when it
     * it legal to roll the dice
     */

    public void roll()
    {
	if (state != ROLL_STATE && state != START_STATE)
	    throw new IllegalStateException("Not time to roll");

	dice = new FourDice();

	if (getLegalMoves().size() == 0)
	    state = ACK_STATE;
	else
	    state = MOVE_STATE;

	setChanged();
	notifyObservers();
    }

    /**
     * Moves this game from the blown it state to the start of the next
     * turn.
     */

    public void blowIt()
    {
	if (state != ACK_STATE)
	    throw new IllegalStateException("Haven't blown it");

	board.endTurnNoProgress();
	state = START_STATE;
	turn++;

	setChanged();
	notifyObservers();
    }

    /**
     * Ends the current turn and moves the colored markers to the
     * position of the neutral markers on the board.
     */

    public void endTurn()
    {
	if (state != ROLL_STATE)
	    throw new IllegalStateException("Haven't just moved");

	board.endTurnWithProgress();

	if (board.isGameOver())
	    {
		state = WON_STATE;
	    }
	else
	    {
		state = START_STATE;
		turn++;
	    }

	setChanged();
	notifyObservers();
    }

    /**
     * Returns a list of the legal moves from this game's current state.
     * The elements of the returned list are themselves lists that
     * contain the <CODE>Integer</CODE> labels on the columns the
     * move could use.
     *
     * @return a list of the currently legal moves
     */

    public List getLegalMoves()
    {
	List moves = new LinkedList();

	if (dice != null)
	    {
		// nothing clever here -- go over all the possible totals
		// and check which ones can be made with the dice...

		for (int tot1 = 2; tot1 <= dice.getTotal() / 2; tot1++)
		    {
			if (dice.makeTotal(tot1))
			    {
				int tot2 = dice.getTotal() - tot1;
			
				// ...and then check if we can use both...

				if (board.isLegalMove(tot1, tot2))
				    {
					List move = new LinkedList();
					move.add(new Integer(tot1));
					move.add(new Integer(tot2));
					moves.add(move);
				    }
				
				// ...or just one (yes, this should be
				// in an else, but I'd like to test
				// the students' code)
				
				if (board.isLegalMove(tot1, dice))
				    {
					List move = new LinkedList();
					move.add(new Integer(tot1));
					moves.add(move);
				    }
				
				if (tot2 != tot1 && board.isLegalMove(tot2, dice))
				    {
					List move = new LinkedList();
					move.add(new Integer(tot2));
					moves.add(move);
				    }
			    }
		    }
	    }

	return moves;
    }

    /**
     * Makes the selected move in this game.
     *
     * @param which an index in to the list returned by <CODE>getLegalMoves</CODE>
     */

    public void makeMove(int which)
    {
	List move = (List)(getLegalMoves().get(which));

	// determine whether the move uses one or two columns
	// and invoke the corresponding version of moveNeutralMarkers

	if (move.size() == 1)
	    {
		board.moveNeutralMarkers(((Integer)(move.get(0))).intValue());
	    }
	else
	    {
      		board.moveNeutralMarkers(((Integer)(move.get(0))).intValue(),
					 ((Integer)(move.get(1))).intValue());
	    }

	state = ROLL_STATE;

	setChanged();
	notifyObservers();
    }

    /**
     * Returns the current state of this game.
     *
     * @return the current state
     */

    public int getState()
    {
	return state;
    }

    /**
     * Returns the current board in this game.
     *
     * @return the board
     */


    public CantStopBoard getBoard()
    {
	return board;
    }

    /**
     * Returns the 1-based index of the current turn.
     *
     * @return the current turn
     */

    public int countTurns()
    {
	return turn;
    }
}

CantStopBoard.java

/**
 * A solitaire Can't Stop game board.
 *
 * This is a skeleton.  Complete code will not be posted to protect it
 * from the prying eyes of CS201 students.
 *
 * @author Jim Glenn
 * @version SKELETON 11/18/2008
 */

public class CantStopBoard
{
    private int[] colored;
    private int[] neutral;

    public CantStopBoard()
    {
    }

    public CantStopBoard(int[] c, int[] n)
    {
	colored = c;
	neutral = n;
    }

    public int getColoredMarkerPosition(int col)
    {
	return 0;
    }

    public int getNeutralMarkerPosition(int col)
    {
	return -1;
    }

    public boolean isLegalMove(int col1, int col2)
    {
	return true;
    }

    public boolean isLegalMove(int col, FourDice dice)
    {
	return true;
    }

    public void moveNeutralMarkers(int col1, int col2)
    {
    }

    public void moveNeutralMarkers(int col)
    {
    }

    public void endTurnWithProgress()
    {
    }

    public void endTurnNoProgress()
    {
    }

    public int countNeutralMarkersUsed()
    {
	return 0;
    }

    public boolean isGameOver()
    {
	return false;
    }

    public int getColumnLength(int col)
    {
	return 13 - 2 * Math.abs(7 - col);
    }

    /**
     * Returns a printable representation of this board.
     *
     * @return a printable representation of this board
     */

    public String toString()
    {
	// change these if you think it is hard to read as is

	final char MARKER = 'O';
	final char NEUTRAL = 'o';
	final char EMPTY =  '.';

	// create a buffer for a drawing of each column

	StringBuffer[] cols = new StringBuffer[11];

	for (int c = 2; c <= 12; c++)
	    {
		int ci = c - 2; // the array index of column c
		cols[ci] = new StringBuffer("             "); // 13 spaces

		int len = getColumnLength(c);
		int bottom = Math.abs(7 - c); // how many spaces below column

		for (int s = 1; s <= len; s++) // draw each space in the column
		    {
			if (s == getColoredMarkerPosition(c))
			    {
				cols[ci].setCharAt(s + bottom - 1, MARKER);
			    }
			else if (s == getNeutralMarkerPosition(c))
			    {
				cols[ci].setCharAt(s + bottom - 1, NEUTRAL);
			    }
			else
			    {
				cols[ci].setCharAt(s + bottom - 1, EMPTY);
			    }
		    }
	    }

	// transpose the columns to get the final string

	StringBuffer result = new StringBuffer();
	for (int r = 0; r < 13; r++)
	    {
		for (int c = 2; c <= 12; c++)
		    {
			result.append(cols[c - 2].charAt(12 - r));
		    }
		result.append('\n');
	    }

	return result.toString();
    }
}

	

CantStopPanel.java

import javax.swing.*;
import java.awt.*;

import java.util.*;

/**
 * A view of a Can't Stop Game as a <CODE>JPanel</CODE>.
 *
 * @author Jim Glenn
 * @version 0.1 11/18/2008
 */

public class CantStopPanel extends JPanel implements Observer
{
    /**
     * The model of the game this panel draws.
     */

    private CantStopGame model;

    /**
     * Creates a panel to display the given game.
     */

    public CantStopPanel(CantStopGame g)
    {
	model = g;
	setBackground(Color.LIGHT_GRAY);
    }

    /**
     * Paints this panel.  The panel will be painted according to
     * the state of the model this panel is associated with.
     */

    public void paint(Graphics g)
    {
	g.clearRect(0, 0, getWidth(), getHeight());

	// compute the size of the board so we fill 80% of the smaller dimension

	int size = Math.min(getWidth(), getHeight()) * 4 / 5;

	// compute some important coordinates

	int sideLength = (int)(size / (1 + Math.sqrt(2)));
	int midX = getWidth() / 2;
	int midY = getHeight() / 2;
	int topY = midY - size / 2;
	int bottomY = topY + size - 1;
	int leftX = midX - size / 2;
	int rightX = leftX + size - 1;

	// draw the board with an outline (should see about drawing a thicker
	// outline)

	Polygon octagon = new Polygon();
	octagon.addPoint(midX - sideLength / 2, topY);
	octagon.addPoint(midX + sideLength / 2, topY);
	octagon.addPoint(rightX, midY - sideLength / 2);
	octagon.addPoint(rightX, midY + sideLength / 2);
	octagon.addPoint(midX + sideLength / 2, bottomY);
	octagon.addPoint(midX - sideLength / 2, bottomY);
	octagon.addPoint(leftX, midY + sideLength / 2);
	octagon.addPoint(leftX, midY - sideLength / 2);

	g.setColor(Color.RED.darker());
	g.fillPolygon(octagon);
	g.setColor(Color.RED);
	g.drawPolygon(octagon);

	// figure out size of the squares from the size of the board
	// (we're assuming that since the bounding box is a square everything
	// will work out so when we pick a size for the squares based
	// on the vertical size of a row that will work horizontally too

	int columnSpacing = (int)(size / 11.5); // left edge to left edge
	int rowSpacing = size / 16; // top to top
	int squareSize = rowSpacing * 4 / 5; // fill 80% of space for tow

	CantStopBoard board = model.getBoard();

	for (int c = 2; c <= 12; c++)
	    {
		// compute left edge of current column and top edge of
		// bottom row in that column

		int columnX = midX - squareSize / 2 - (7 - c) * columnSpacing;
		int columnY = midY - squareSize / 2 + ((board.getColumnLength(c) + 1) / 2 - 1) * rowSpacing;
	    
		// draw all the spaces in the current row

		for (int space = 1; space <= board.getColumnLength(c); space++)
		    {
			if (board.getColoredMarkerPosition(c) == space)
			    g.setColor(Color.GREEN);
			else if (board.getNeutralMarkerPosition(c) == space)
			    g.setColor(Color.WHITE);
			else
			    g.setColor(Color.GRAY);

			g.fillRect(columnX, columnY - (space - 1) * rowSpacing,
				   squareSize, squareSize);
		    }
		g.setColor(Color.WHITE);
		g.drawString("" + c, columnX, columnY + rowSpacing * 3 / 2);
	    }

	// draw the dice in the lower left corner

	g.setColor(Color.WHITE);
	g.fillRect(0, getHeight() - 30, 50, 25);
	g.setColor(Color.BLACK);
	g.setFont(new Font("SansSerif", Font.BOLD, 20));
	g.drawString(model.getDice(), 0, getHeight() - 5);

	// draw the turn in the lower right corner
	// (maybe use FontMetrics to get this exactly right)

	g.drawString("Turn " + model.countTurns(), getWidth() - 150, getHeight() - 5);
    }

    /**
     * Repaints this view.
     */

    public void update(Observable obs, Object o)
    {
	repaint();
    }
}

CantStopPanel.java

import javax.swing.*;
import java.awt.*;

import java.util.*;

/**
 * A view of a Can't Stop Game as a <CODE>JPanel</CODE>.
 *
 * @author Jim Glenn
 * @version 0.1 11/18/2008
 */

public class CantStopPanel extends JPanel implements Observer
{
    /**
     * The model of the game this panel draws.
     */

    private CantStopGame model;

    /**
     * Creates a panel to display the given game.
     */

    public CantStopPanel(CantStopGame g)
    {
	model = g;
	setBackground(Color.LIGHT_GRAY);
    }

    /**
     * Paints this panel.  The panel will be painted according to
     * the state of the model this panel is associated with.
     */

    public void paint(Graphics g)
    {
	g.clearRect(0, 0, getWidth(), getHeight());

	// compute the size of the board so we fill 80% of the smaller dimension

	int size = Math.min(getWidth(), getHeight()) * 4 / 5;

	// compute some important coordinates

	int sideLength = (int)(size / (1 + Math.sqrt(2)));
	int midX = getWidth() / 2;
	int midY = getHeight() / 2;
	int topY = midY - size / 2;
	int bottomY = topY + size - 1;
	int leftX = midX - size / 2;
	int rightX = leftX + size - 1;

	// draw the board with an outline (should see about drawing a thicker
	// outline)

	Polygon octagon = new Polygon();
	octagon.addPoint(midX - sideLength / 2, topY);
	octagon.addPoint(midX + sideLength / 2, topY);
	octagon.addPoint(rightX, midY - sideLength / 2);
	octagon.addPoint(rightX, midY + sideLength / 2);
	octagon.addPoint(midX + sideLength / 2, bottomY);
	octagon.addPoint(midX - sideLength / 2, bottomY);
	octagon.addPoint(leftX, midY + sideLength / 2);
	octagon.addPoint(leftX, midY - sideLength / 2);

	g.setColor(Color.RED.darker());
	g.fillPolygon(octagon);
	g.setColor(Color.RED);
	g.drawPolygon(octagon);

	// figure out size of the squares from the size of the board
	// (we're assuming that since the bounding box is a square everything
	// will work out so when we pick a size for the squares based
	// on the vertical size of a row that will work horizontally too

	int columnSpacing = (int)(size / 11.5); // left edge to left edge
	int rowSpacing = size / 16; // top to top
	int squareSize = rowSpacing * 4 / 5; // fill 80% of space for tow

	CantStopBoard board = model.getBoard();

	for (int c = 2; c <= 12; c++)
	    {
		// compute left edge of current column and top edge of
		// bottom row in that column

		int columnX = midX - squareSize / 2 - (7 - c) * columnSpacing;
		int columnY = midY - squareSize / 2 + ((board.getColumnLength(c) + 1) / 2 - 1) * rowSpacing;
	    
		// draw all the spaces in the current row

		for (int space = 1; space <= board.getColumnLength(c); space++)
		    {
			if (board.getColoredMarkerPosition(c) == space)
			    g.setColor(Color.GREEN);
			else if (board.getNeutralMarkerPosition(c) == space)
			    g.setColor(Color.WHITE);
			else
			    g.setColor(Color.GRAY);

			g.fillRect(columnX, columnY - (space - 1) * rowSpacing,
				   squareSize, squareSize);
		    }
		g.setColor(Color.WHITE);
		g.drawString("" + c, columnX, columnY + rowSpacing * 3 / 2);
	    }

	// draw the dice in the lower left corner

	g.setColor(Color.WHITE);
	g.fillRect(0, getHeight() - 30, 50, 25);
	g.setColor(Color.BLACK);
	g.setFont(new Font("SansSerif", Font.BOLD, 20));
	g.drawString(model.getDice(), 0, getHeight() - 5);

	// draw the turn in the lower right corner
	// (maybe use FontMetrics to get this exactly right)

	g.drawString("Turn " + model.countTurns(), getWidth() - 150, getHeight() - 5);
    }

    /**
     * Repaints this view.
     */

    public void update(Observable obs, Object o)
    {
	repaint();
    }
}

CantStopControl.java

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;

import java.util.*;

/**
 * The controller for a Can't Atop GUI.
 *
 * @author Jim Glenn
 * @version 0.1 11/24/2008
 */

public class CantStopControl extends JPanel
{
    /**
     * The model this controller manipulates.
     */

    private CantStopGame model;

    /**
     * The text at the bottom of the panel that tells the player what
     * input is expected.
     */

    private JTextField messageField;

    /**
     * The buttons that allow the player to select a move.  The action
     * command on these buttons will be set to the index of the
     * corresponding move into the model's getLegalMoves list.
     */

    private JButton[] moveButtons;

    /**
     * The "Roll" button.
     */

    private JButton rollButton;

    /**
     * The "Stop" button.  When the current roll "blows it" this button
     * displays "Blow It" to let the player acknowledge the bad roll.
     */

    private JButton stopButton;


    /**
     * Creates a controller connected to the given model.
     *
     * @param g a model of Can't Stop
     */

    public CantStopControl(CantStopGame g)
    {
	model = g;

	setLayout(new BorderLayout());
	
	// create the message display the the bottom of the panel

	messageField = new JTextField();
	messageField.setEditable(false);
	add(messageField, BorderLayout.SOUTH);

	// create a 3x3 array of buttons with the move buttons in the top
	// 2 rows

	JPanel buttonPanel = new JPanel(new GridLayout(3, 3));
	moveButtons = new JButton[6];
	for (int i = 0; i < 6; i++)
	    {
		moveButtons[i] = new JButton();
		moveButtons[i].setActionCommand("" + i);
		moveButtons[i].addActionListener(new MoveListener());
		buttonPanel.add(moveButtons[i]);
	    }
	
	// create the roll and stop buttons and place them in the bottom row

	rollButton = new JButton("Roll");
	rollButton.addActionListener(new RollListener());
	stopButton = new JButton();
	stopButton.addActionListener(new StopListener());
	buttonPanel.add(rollButton);
	buttonPanel.add(new JPanel());
	buttonPanel.add(stopButton);

	// add all the buttons to the center of the panel

	add(buttonPanel, BorderLayout.CENTER);

	// set the text on the buttons

	setButtons();
    }

    /**
     * Sets the text on the buttons (and the message text) according
     * to the current state of the model.
     */

    private void setButtons()
    {
	switch (model.getState())
	    {
	    case CantStopGame.START_STATE:
		clearMoveButtons();
		messageField.setText("Click roll to roll the dice.");
		rollButton.setEnabled(true);
		stopButton.setText("Stop");
		stopButton.setEnabled(false);
		break;

	    case CantStopGame.ROLL_STATE:
		clearMoveButtons();
		messageField.setText("Roll again or end your turn.");
		rollButton.setEnabled(true);
		stopButton.setText("Stop");
		stopButton.setEnabled(true);
		break;
  
	    case CantStopGame.ACK_STATE:
		clearMoveButtons();
		messageField.setText("No moves possible -- click \"Blow it\".");
		rollButton.setEnabled(false);
		stopButton.setText("Blow it");
		stopButton.setEnabled(true);
		break;

	    case CantStopGame.MOVE_STATE:
		setMoveButtons();
		messageField.setText("Choose your move");
		rollButton.setEnabled(false);
		stopButton.setText("Stop");
		stopButton.setEnabled(false);
		break;

	    case CantStopGame.WON_STATE:
		clearMoveButtons();
		messageField.setText("You won!");
		rollButton.setEnabled(false);
		stopButton.setText("Stop");
		stopButton.setEnabled(false);
		break;
	    }
    }

    /**
     * Sets all the move buttons to blank and disables them.
     * This is intended to be used to set the buttons at the
     * start of a turn before a roll has been made.
     */

    private void clearMoveButtons()
    {
	for (int i = 0; i < moveButtons.length; i++)
	    {
		moveButtons[i].setText("");
		moveButtons[i].setEnabled(false);
	    }
    }

    /**
     * Sets the text on the move buttons according to the currently
     * level moves and enables them.  This is intended to be used
     * after a good roll (one that doesn't blow it.
     */

    private void setMoveButtons()
    {
	java.util.List moves = model.getLegalMoves();

	for (int i = 0; i < moveButtons.length; i++)
	    {
		if (i < moves.size())
		    {
			java.util.List move = (java.util.List)(moves.get(i));
			String label = "";
			Iterator j = move.iterator();
			while (j.hasNext())
			    {
				label = label + j.next();
				if (j.hasNext())
				    label = label + " - ";
			    }
			moveButtons[i].setText(label);
			moveButtons[i].setEnabled(true);
		    }
		else
		    {
			// disable buttons if fewer than 6 legal moves
			
			moveButtons[i].setText("");
			moveButtons[i].setEnabled(false);
		    }
	    }
    }

    /**
     * Relays clicks on the move buttons to the model.
     */

    private class MoveListener implements ActionListener
    {
	public void actionPerformed(ActionEvent e)
	{
	    model.makeMove(Integer.parseInt(e.getActionCommand()));
	    setButtons();
	}
    }

    /**
     * Relays clicks on the roll button to the model.
     */

    private class RollListener implements ActionListener
    {
	public void actionPerformed(ActionEvent e)
	{
	    // tell the model to roll the dice

	    model.roll();

	    // reset the buttons

	    setButtons();
	}
    }

    /**
     * Relays clicks on the roll/blow it button to the model.
     */

    private class StopListener implements ActionListener
    {
	public void actionPerformed(ActionEvent e)
	{
	    // determine if we're stopping or blowing it

	    if (model.getState() == CantStopGame.ACK_STATE)
		{
		    model.blowIt();
		}
	    else 
		{
		    model.endTurn();
		}

	    // reset the buttons accordingly

	    setButtons();
	}
    }

}
This code can also be downloaded from the files
CantStopGame.java, CantStopBoard.java, CantStopPanel.java, CantStopPanel.java, and CantStopControl.java.