import java.applet.*;
import java.awt.*;
import java.awt.event.*;
/**
* An applet to provide an interactive, 2D visual explanation of football's
* 'offside rule'. The applet places a number of icons representing players
* (attackers of one team and defenders of the other) on a football pitch,
* seen from plan view.
* <p>
* The user has the ability to drag each of these players into any desired
* position on the pitch, in order to create a mock-up of a situation that could
* arise in a real football match. The onside/offside status of the play at
* that point (were the ball to be played forward) can then be determined
* by clicking a button. The applet marks the 'offside threshold' (the point
* beyond which attackers must not advance if they are to remain onside)
* and reports the status of the play.
* <p>
* The user also has the ability to draw arbitrary temporary shapes on the
* screen (in the manner of Andy Gray on Sky Sports...).
*
* @author Michael Fitzmaurice, April 2003
*/
public class OffsideApplet extends Applet implements ActionListener
{
// :TODO:
//
// - make number and properties of players configurable via <PARAM> tags
// - factor out all hardcoded config & magic numbers (see above)
// - highlight multiple offside players (return Player[] ?)
// - use a canvas in the center of the applet so that image is not clipped
// - add a ball to one of the attackers (option?) and incorporate the
// more complex rules concerning running from behind ball, etc
// - add a colour key indicating which team are attacking
private Player[] m_defenders,
m_attackers;
private Player m_offsidePlayer;
private boolean m_drawOffsideThreshold;
private Label m_messageLabel;
private int m_mouseXLocation,
m_mouseYLocation;
private int m_canvasHeight,
m_canvasWidth;
private Player m_selectedPlayer;
private Button m_playBallButton;
private Image m_pitchImage;
// note the use of the jdk 1.2 style color constants (lower case) -
// in jdk 1.4 the more correct upper case constants have been added,
// but the old style ones are still supported for backwards compatibility
private static final Color PLAYER_OUTLINE_COLOUR = Color.white;
private static final Color OFFSIDE_BOUNDARY_COLOUR = Color.red;
private static final Color ATTACKERS_SHIRT_COLOR = new Color(72, 100, 255);
private static final Color DEFENDERS_SHIRT_COLOR = new Color(255, 25, 16);
private static final Color GOALKEEPER_SHIRT_COLOR = new Color(10, 150, 10);
private static final int PLAYER_HEIGHT = 30;
private static final int PLAYER_WIDTH = 30;
public void init()
{
setLayout( new BorderLayout() );
m_canvasHeight = getBounds().height;
m_canvasWidth = getBounds().width;
// how wide is 1 / 10 of the applet? use this to position
// players in different scetions around the pitch
int tenPercentOfWidth = m_canvasWidth / 10;
m_defenders = new Player[5];
m_defenders[0] = new Player( DEFENDERS_SHIRT_COLOR,
5,
tenPercentOfWidth,
m_canvasHeight / 2);
m_defenders[1] = new Player( DEFENDERS_SHIRT_COLOR,
3,
tenPercentOfWidth * 3,
m_canvasHeight / 3);
m_defenders[2] = new Player( DEFENDERS_SHIRT_COLOR,
2,
tenPercentOfWidth * 6,
m_canvasHeight / 4);
m_defenders[3] = new Player( DEFENDERS_SHIRT_COLOR,
4,
tenPercentOfWidth * 8,
m_canvasHeight / 2);
m_defenders[4] = new Player( GOALKEEPER_SHIRT_COLOR,
1,
(m_canvasWidth / 2) - PLAYER_WIDTH,
PLAYER_HEIGHT);
m_attackers = new Player[2];
m_attackers[0] = new Player( ATTACKERS_SHIRT_COLOR,
9,
tenPercentOfWidth * 4,
m_canvasHeight / 2);
m_attackers[1] = new Player( ATTACKERS_SHIRT_COLOR,
7,
(m_canvasWidth / 2),
m_canvasHeight / 3);
this.addMouseListener( new MouseClickHandler() );
this.addMouseMotionListener( new MouseMotionHandler() );
// load up the background image & scale it to
// fit the applet height / width
m_pitchImage = getImage(getDocumentBase(), "footballPitch.gif");
m_pitchImage = m_pitchImage.getScaledInstance( m_canvasWidth,
m_canvasHeight,
Image.SCALE_DEFAULT);
// set up GUI components
m_messageLabel = new Label();
m_messageLabel.setAlignment(Label.CENTER);
m_messageLabel.setBackground( new Color(230, 230, 10) );
m_messageLabel.setForeground(OFFSIDE_BOUNDARY_COLOUR);
m_playBallButton = new Button("CHECK FOR OFFSIDE");
m_playBallButton.addActionListener(this);
Panel buttonPanel = new Panel();
buttonPanel.setBackground(Color.gray);
buttonPanel.add(m_playBallButton);
this.add(m_messageLabel, BorderLayout.NORTH);
this.add(buttonPanel, BorderLayout.SOUTH);
}
public void update(Graphics g)
{
// override update() to reduce flicker - no need to bother
// redrawing the background on the on-screen Graphics object,
// since the whole thing is copied from the off-screen Graphics
// object once it is complete. To clear the on-screen background
// first is the default behaviour - do not want this duplication
paint(g);
}
/**
* Paints the current scene onto the screen.
*
* @param g The Graphics context supplied by the applet's container
*/
public void paint(Graphics g)
{
// use double buffering to reduce flicker - draw
// the overall picture in stages offscreen
Image offScreenImage = createImage(m_canvasWidth, m_canvasHeight);
Graphics offScreenGraphics = offScreenImage.getGraphics();
this.drawPitch(offScreenGraphics);
for (int i = 0; i < m_defenders.length; i++)
{
m_defenders[i].drawSelf(offScreenGraphics);
}
for (int i = 0; i < m_attackers.length; i++)
{
m_attackers[i].drawSelf(offScreenGraphics);
}
if (m_drawOffsideThreshold)
{
this.drawOffsideBoundary(offScreenGraphics);
m_drawOffsideThreshold = false;
}
// copy the finished off-screen image onto the canvas in one go
g.drawImage (offScreenImage, 0, 0, this);
}
/**
* Helper method to draw a line on screen marking the threshold of the
* offside region, given the current scene.
*/
private void drawOffsideBoundary(Graphics g)
{
// get the location of the last defender (used later
// on to find the second-to-last defender)
int lastDefenderPosition = m_canvasHeight;
for (int i = 0; i < m_defenders.length; i++)
{
Player defender = m_defenders[i];
int defenderPosition = defender.getYPosition();
if (defenderPosition < lastDefenderPosition)
{
lastDefenderPosition = defenderPosition;
}
}
// offside threshold is the position of the second-to-last defender
int offsideThreshold = m_canvasHeight;
for (int i = 0; i < m_defenders.length; i++)
{
Player defender = m_defenders[i];
int defenderPosition = defender.getYPosition();
// does this player represent the offside threshold?
if ( (defenderPosition > lastDefenderPosition) &&
(defenderPosition < offsideThreshold) )
{
offsideThreshold = defenderPosition;
}
}
g.setColor(OFFSIDE_BOUNDARY_COLOUR);
g.drawLine(0, offsideThreshold, m_canvasWidth, offsideThreshold);
}
private void drawPitch(Graphics g)
{
g.drawImage(m_pitchImage, 0, 0, this);
}
/**
* Helper method to return the (most) offside attacking player given
* the current scene. If no players are offside, null is returned.
*/
private Player getOffsidePlayer()
{
Player offsideAttacker = null;
// relative position of the foremost attacker in the Y dimension
// determines onside/offside status - record this position
Player foremostAttacker = m_attackers[0];
int foremostAttackerPosition = foremostAttacker.getYPosition();
for (int i = 1; i < m_attackers.length; i++)
{
Player attacker = m_attackers[i];
int attackerPosition = attacker.getYPosition();
// values of a lesser magnitude are further forward, since
// the top of the screen is at 0 pixels in the Y plane, and
// the goal line is at the top end of the scene