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