Joust

<< Rock Paper Scissors | LabTrailIndex | Fancy Joust >>

A game I learned about from Berkeley's Professor Dan Garcia's GamesCrafters Site Two knights on a chess board that move like chess, but as they leave a square, it is "burned" (uninhabitable for either player). Players alternate moving their knight trying to avoid being the loser who is left with no place to go. Click here for a working demo

This lab is set up so you can discover how to program conditional statements (more commonly called "if" statements). I have some starter code with graphics and fancy things like arrays and loops (something that we will be learning about formally very soon--think of this as an introduction to see how terribly wonderful and useful they can be). Your job will be to finish writing some vital boolean methods that will make the code work.

Under the hood

  1. Square objects start out either black or white (like a chess board) and become red when they are "burned" It draws itself, so it needs to know if it is highlighted. Write the isBurned method by checking what the color is.
  2. Each player has a Knight (the player's piece) which needs to keep track of its location, what color it is.
  3. The game's Board has a collection of Square objects and two Knight objects (one for each player. It also keeps track of whose turn it is, what are legal moves, and whether the game is over (and who the winner is). Here you have to write two methods, both involving the possible moves. You'll need to use the isAvailable method. You also need to look at certain squares. The knight can either move one over and two across, or two over and one across. You might get indexOutOfBounds runtime errors if you forget to see if you are looking for a square that is off the board. Thank goodness Java uses 'short circut' conditional statements. Consider if (c<0 && isAvailable(grid[r][c]) If c is less than 0, it won't go to the isAvailable method. Good thing, since it would cause a runtime error. Once the first part of the && is false, that is enough to know the whole expression is false.
  4. The Joust Applet handles the user's activity. If the user is hovering over a valid move, then that particular Square is told it is highlighted. If the user clicks on a valid move, then the current Knight burns the current square, and moved to the new location,. If it is not a game winning move, and the current player switches.

Square.java

Complete the isBurned method

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Rectangle;

public class Square {
    /**
     * constants
     */
    public static final int black=0;
    public static final int white=1;
    public static final int burned=2;
    /**
     * instance fields
     */

    private Color[] colors= {Color.DARK_GRAY, Color.LIGHT_GRAY, Color.RED}; 
    private int color;
    private boolean highlighted;
    private Rectangle box;
    /**
     * constructors 
     */
    public Square(){
        setColor(Square.white);
        setHighlighted(false);
        box=null;
    }

    public Square(int color) {
        this(); //use the default constructor to initialize
        this.setColor(color);
    }

    public Square(int color, Rectangle box) {
        this(); //use the default constructor to initialize
        this.setColor(color);
        this.box=box;
    }

    /**
     * Access Methods
     */
    public int getColor() {
        return color;
    }

    public void setColor(int color) {
        this.color = color;
    }

    /**
     * other methods
     */
    public void draw(Graphics g){
        if (box==null)
            return;
        int w=2;//width of outline
        int x= (int)box.getX();
        int y= (int)box.getY();
        int size = (int)box.getHeight();
        if (highlighted){
            g.setColor(Color.MAGENTA);
        }else{
            g.setColor(Color.BLACK);
        }
        g.fillRect(x, y, size, size);

        g.setColor(colors[color]);
        g.fillRect(x+w, y+w, size-2*w, size-2*w);

    }
    public boolean isHighlighted() {
        return highlighted;
    }

    public void setHighlighted(boolean highlighted) {
        this.highlighted = highlighted;
    }

    /**
     * given screen location x,y sets the highlight on or off 
     * @param x screen coord
     * @param y screen coord
     * @return the Square if highlighted, otherwise null
     */
    public Square select(int x, int y){
        this.setHighlighted(box.contains(x,y));
        if(this.isHighlighted())
            return this;
        else
            return null;
    }

    public boolean isBurned(){
        //your code here

    }
}

Knight.java

import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;

public class Knight {
    /**
     * instance fields
     */

    private int row, col;
    private String color;
    public static final String black="\u2658"; //the unicode filled knight letter
    public static final String white="\u265e"; //the unicode open knight letter
    /**
     * constructors
     */
    public Knight() {
        color=Knight.white;
        row=0;
        col=0;
    }

    public Knight(String color){
        this(); //use default constructor to initialize fields
        this.color=color;		
    }

    public Knight(String color, int row, int col){
        this(color); //use other constructor to initialize fields
        this.row=row;
        this.col=col;
    }

    public void draw(Graphics g, Board board){
        int size = board.getSquareSize();
        int offset = (int)(size/5.0);
        int x=board.getLeft()+col*size;
        int y=board.getTop()+(row)*size;

        Font font=new Font("Arial Unicode MS", 0, size);//Arial is the most complete for WinXP

        g.setFont(font);
        g.setColor(Color.YELLOW);
        g.drawString(this.getColor(), x, y+size-offset);  

    }

    public boolean isWhite()
    {
        return color==Knight.white;
    }

    public boolean isBlack()
    {
        return !isWhite();
    }

    public int getRow() {
        return row;
    }

    public void setRow(int row) {
        this.row = row;
    }

    public int getCol() {
        return col;
    }

    public void setCol(int col) {
        this.col = col;
    }

    public String getColor(){
        return color;
    }

    public void setColor(String color){
        this.color=color;
    }



}

Board.java

Complete the isLegalMove and hasMovesLeft methods

import java.awt.Graphics;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;

/**
 * A Joust game board... free to use for educational use
 * @author Chris Thiel, OFMCap
 * 30 Sept 2012
 *
 */
public class Board {

    private int squareSize, top,left;
    private Square[][] grid;
    private Square selectedSquare; 
    private int selectedRow, selectedCol;
    private Knight[] player;
    private Knight currentPlayer;
    private String message;

    public Board(int rows, int cols) 
    {
        this.setSquareSize(70);
        grid=new Square[rows][cols];
        setTop(20);
        setLeft(50);
        setSelectedSquare(null);
        setSelectedRow(-1);
        setSelectedCol(-1);
        setMessage("White's turn");
        for (int r=0; r<getNumRows();r++)
            for (int c=0; c< getNumCols();c++){
                int color=(r+c+1)%2;//alternate color
                int x=left + c*squareSize;
                int y=top + r*squareSize;
                Rectangle rectangle=new Rectangle(x,y,squareSize,squareSize);
                grid[r][c]=new Square(color, rectangle);
        }
        player = new Knight[2];
        int rand=(int)(getNumCols()*Math.random());
        player[0]=new Knight(Knight.white, getNumRows()-1,rand);
        rand=(int)(getNumCols()*Math.random());
        player[1]=new Knight(Knight.black,0,rand);

        setCurrentPlayer(player[0]);

    }

    /**
     * access methods
     * 
     */
    public int getNumRows(){
        return grid.length;
    }

    public int getNumCols(){
        return grid[0].length;
    }

    public Square getSquare(int row, int col){
        return grid[row][col];
    }

    public int getTop() {
        return top;
    }

    public void setTop(int top) {
        this.top = top;
    }

    public int getLeft() {
        return left;
    }

    public void setLeft(int left) {
        this.left = left;
    }

    public int getSquareSize() {
        return squareSize;
    }

    public void setSquareSize(int squareSize) {
        this.squareSize = squareSize;
    }

    public Knight getCurrentPlayer() {
        return currentPlayer;
    }

    /**
     * Check if the square at r,c is burned
     * or occupied by one of the players
     * @param r
     * @param c
     * @return is the square is availible
     */
    public boolean isAvailable(int r, int c){
        if (r<0 || c<0 || r>=getNumRows() || c>=getNumCols())
            return false;
        if (grid[r][c].isBurned())
            return false;
        for(int i=0;i<2;i++)
            if (player[i].getRow()==r && player[i].getCol()==c)
                return false;
        return true;
    }

    public void setCurrentPlayer(Knight currentPlayer) {
        this.currentPlayer = currentPlayer;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public Square getSelectedSquare() {
        return selectedSquare;
    }

    public void setSelectedSquare(Square selectedSquare) {
        this.selectedSquare = selectedSquare;
    }

    public int getSelectedRow() {
        return selectedRow;
    }

    public void setSelectedRow(int selectedRow) {
        this.selectedRow = selectedRow;
    }

    public int getSelectedCol() {
        return selectedCol;
    }

    public void setSelectedCol(int selectedCol) {
        this.selectedCol = selectedCol;
    }

    /**
     * other methods
     */
    public void draw(Graphics g){
        for (int r=0; r<getNumRows();r++)
            for (int c=0; c< getNumCols();c++){
                grid[r][c].draw(g);
        }
        player[0].draw(g, this);
        player[1].draw(g, this);

    }

    public void setHighlight(int x, int y){
        for (int r=0; r<getNumRows();r++)
            for (int c=0; c< getNumCols();c++){
                Square s=grid[r][c].select(x, y);
                if (s!=null) {
                    setSelectedSquare(s);
                    setSelectedRow(r);
                    setSelectedCol(c);
                }
        }
        message="("+selectedRow+", "+selectedCol+") is ";
        if (isLegalMove(currentPlayer, selectedRow,selectedCol))
            message+="legal";
        else 
            message+="NOT legal";
    }

    public String getSelected(){
        return "("+selectedRow+", "+selectedCol+")";
    }

    public String whoseTurn(){

        if (! gameOver() && currentPlayer.isWhite())
            return "Whites turn";
        if (! gameOver() && currentPlayer.isBlack())
            return "Black's turn";
        if (hasMovesLeft(player[0]))
            return "White wins";
        return "Black Wins";
    }

    public void move(){

        if(gameOver())
            return;
        if (isLegalMove(currentPlayer, selectedRow, selectedCol)){
            grid[currentPlayer.getRow()][currentPlayer.getCol()].setColor(Square.burned);
            currentPlayer.setRow(selectedRow);
            currentPlayer.setCol(selectedCol);	

            if (!gameOver() ){
                if (currentPlayer==player[0])
                    currentPlayer=player[1];
                else
                    currentPlayer=player[0];

            }
        }	
        message=whoseTurn();
    }

    /**
     * Checks if both players have any 
     * availible Moves left 
     * @return whether the game is over
     */
    public boolean gameOver(){
        for (int i=0;i<2;i++)
            if (!hasMovesLeft(player[i]))
                return true;
        return false;
    }

    /**
     * Using a series of conditional statements
     * checks to see if the square at r,c 
     * is a valid place to move this knight
     * @param board
     * @param r the row in question
     * @param c the column in question
     * @return whether the knight can validly go to the square 
     */
    public boolean isLegalMove(Knight player, int r, int c) {
        //your code here 

    }

    public boolean hasMovesLeft(Knight player){
        // your code here
    }
}

JoustApplet.java

import java.applet.Applet;
import java.applet.AudioClip;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.image.BufferedImage;
import java.io.IOException;

import javax.imageio.ImageIO;
/**
 * A applet to hold the Joust Board
 * and deal with user input
 * Free for educational use.
 * @author Chris Thiel, OFMCap
 * @version 30 Sept 2012
 *
 */

public class JoustApplet extends Applet implements MouseMotionListener, MouseListener
{
    /**
     * Instance fields for the Game
     *
     */
    private Board board;
    private Rectangle restartBtn;

    /**
     * These instance fields are for getting rid of flicker
     * on the Windows platform-- the Applet will draw the 
     * picture in memory before putting it on the screen
     */
    private Image virtualMem;
    private Graphics gBuffer;
    private Font font;//The font for our messages
    private String message;
    public void init()
    {

        board=new Board(8,8);

        this.restartBtn = new Rectangle (670, 350, 80,40);
        this.addMouseListener(this);
        this.addMouseMotionListener(this);
        font = new Font("Helvetica", Font.BOLD,  18);  
        message="Joust! "+"White"+"'s move";
    }

    /**
     * On loading the Applet we read the sound and image files
     * from the java archive (jar file), and play the sounds 
     * to initialize them.
     */

    public void paint (Graphics g)
    {
        //make a new buffer in case the applet size changed
        virtualMem = createImage(getWidth(),getHeight());
        gBuffer = virtualMem.getGraphics();
        gBuffer.setColor(Color.WHITE);
        gBuffer.fillRect(0, 0, this.getWidth(), this.getHeight());
        gBuffer.setColor(Color.BLACK);
        gBuffer.setFont(font);
        gBuffer.drawString(message, 625, 20);
        gBuffer.drawString(board.getMessage(), 625, 40);
        if (board.gameOver()){
            gBuffer.setColor(Color.BLUE);
            gBuffer.fillRect(restartBtn.x, restartBtn.y, restartBtn.width, restartBtn.height);
            gBuffer.setColor(Color.YELLOW);
            gBuffer.drawString("Restart", restartBtn.x+10, restartBtn.y+25);
        }
        board.draw(gBuffer);

        g.drawImage(virtualMem,0,0,this);//set new display to Screen
    }

    @Override
    public void mouseClicked(MouseEvent e) {}

    @Override
    public void mousePressed(MouseEvent e) {}

    @Override
    public void mouseReleased(MouseEvent e) {

        board.move();

        if(board.gameOver()){
            if (restartBtn.contains(e.getPoint())){
                board=new Board(8,8);
                repaint();
            }

        }
        repaint();
    }

    @Override
    public void mouseEntered(MouseEvent e) {}

    @Override
    public void mouseExited(MouseEvent e) {}

    @Override
    public void mouseDragged(MouseEvent e) {}

    @Override
    public void mouseMoved(MouseEvent e) {
        int x=e.getX();
        int y=e.getY();

        board.setHighlight(x, y);
        message=board.whoseTurn();
        repaint();
    }

    public void update(Graphics g)
    {
        paint(g); //get rid of flicker with this method
    }

}

Now what?

Once you have it going, you can customize it:

  1. Change the colors or the appearance of the Knights
  2. Make the "Misere" version (where the one who can't move is the winner)
  3. Change the size of the board to 5 by 5 board or a rectangular board
  4. The demo has some fancy images and sounds, which you may wish to incorporate in your final project. See Fancy Joust