/* MissileCommando.java - based on the arcade game Missile Command. */ /* * Copyright (C) 1995 Mark Boyns * * MissileCommando * * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ import java.applet.*; import java.awt.*; import java.util.Enumeration; import java.util.Vector; /* Abstract class used for moving objects, etc. */ abstract class Thing extends Thread { MissileCommando parent; public int X; public int Y; Color color; abstract void paint (Graphics g); /* paint the object */ abstract void erase (Graphics g); /* erase the object */ abstract boolean hit (Missile m); /* did this missile hit the object? */ abstract void explode (); /* explode the object */ } /* A simple rectangular base. This object just sits around waiting to explode. */ class Base extends Thing { private int W = 0; private int H = 0; Base (MissileCommando parent, Color color, int x, int y, int w, int h) { this.parent = parent; X = x; Y = y; this.color = color; W = w; H = h; start (); } public void run () { Thread.currentThread().setPriority (Thread.MIN_PRIORITY); waitToDie (); } public synchronized void waitToDie () { try { wait (); } catch (InterruptedException e) { } } public synchronized void timeToDie () { notify (); } public boolean hit (Missile m) { return (m.X >= X && m.X <= (X + W) && m.Y >= Y && m.Y <= (Y + H)); } public void explode () { timeToDie (); stop (); } public void paint (Graphics g) { g.setColor (color); g.fillRect (X, Y, W, H); } public void erase (Graphics g) { g.setColor (Color.lightGray); g.fillRect (X, Y, W, H); } } /* A missile which moves from X1,Y1 to X2,Y2 with velocity V. A missile can randomly replicate itself into 3 missiles. */ class Missile extends Thing { private boolean paintme = false; private int X1 = 0; private int Y1 = 0; private int X2 = 0; private int Y2 = 0; private int V = 0; private float m = 0; private float b = 0; private float x = 0; private float y = 0; private boolean replicate = false; Missile (MissileCommando parent, Color color, int x1, int y1, int x2, int y2, int v) { this.parent = parent; X = X1; Y = Y1; this.color = color; X1 = x1; Y1 = y1; X2 = x2; Y2 = y2; V = v; start (); } public void run () { Thread.currentThread().setPriority (Thread.MIN_PRIORITY); m = (float) (Y2 - Y1) / (X2 - X1); b = Y1 - (m*X1); x = X1; y = Y1; /* 10% chance this missile will replicate. */ replicate = Math.random () > 0.90; if (replicate) { color = Color.magenta; } while (y <= Y2) { /* 25% chance this missile will replicate now. */ if (replicate && y > Y1+5*V && Math.random () > 0.75) { for (int i = 0; i < 3; i++) { parent.createMissile (X, Y, V); } /* stop the current missile */ break; } paintme = true; parent.repaint (); try { Thread.sleep (100); } catch (InterruptedException e) { } } paintme = true; parent.repaint (); } public boolean hit (Missile m) { return m.X == X && m.Y == Y; } public void explode () { stop (); } public void paint (Graphics g) { if (paintme) { g.setColor (Color.lightGray); g.drawLine (X1, Y1, X, Y); y += V; x = (y - b) / m; X = (int)x; Y = (int)y; g.setColor (color); g.drawLine (X1, Y1, X, Y); paintme = false; } } public void erase (Graphics g) { g.setColor (Color.lightGray); g.drawLine (X1, Y1, X, Y); } } /* Generic explosion which draws a circle that grows and shrinks. The explosion is draw at X,Y and has a maximum size of S. */ class Explosion extends Thing { private boolean paintme = false; private int S = 0; public int size = 0; private int scale = 0; Explosion (MissileCommando parent, Color color, int x, int y, int s) { this.parent = parent; X = x; Y = y; this.color = color; S = s; start (); } public void run () { Thread.currentThread().setPriority (Thread.MIN_PRIORITY); /* grow */ scale = 10; size = 1; do { paintme = true; parent.repaint (); try { Thread.sleep (50); } catch (InterruptedException e) { } } while (size <= S); /* shrink */ scale = -10; do { paintme = true; parent.repaint (); try { Thread.sleep (100); } catch (InterruptedException e) { } } while (size >= 0); paintme = true; parent.repaint (); } public boolean hit (Missile m) { return (m.X >= (X - size/2) && m.X <= (X + size/2) && m.Y >= (Y - size/2) && m.Y <= (Y + size/2)); } public void explode () { } public void paint (Graphics g) { if (paintme) { if (scale < 0) { g.setColor (Color.lightGray); g.fillOval (X - size/2, Y - size/2, size, size); } size += scale; g.setColor (color); g.fillOval (X - size/2, Y - size/2, size, size); paintme = false; } } public void erase (Graphics g) { } } /* A shot explosion base on Explosion. */ class ShotExplosion extends Explosion { ShotExplosion (MissileCommando parent, int x, int y) { super (parent, Color.black, x, y, 60); } } /* A base explosion base on Explosion. */ class BaseExplosion extends Explosion { BaseExplosion (MissileCommando parent, int x, int y) { super (parent, Color.red, x, y, 100); } } /* Draw a blinking message. */ class Message extends Thing { private String message; private int blinks; private int delay; private int blinkCount; Message (MissileCommando parent, String message, int x, int y, int blinks, int delay) { this.parent = parent; this.message = message; X = x; Y = y; this.blinks = blinks; this.delay = delay; color = Color.black; start (); } Message (MissileCommando parent, String message, int x, int y, int blinks) { this (parent, message, x, y, blinks, 500); } Message (MissileCommando parent, String message, int x, int y) { this (parent, message, x, y, 3, 500); } public void run () { Thread.currentThread().setPriority (Thread.MIN_PRIORITY); for (blinkCount = 0; blinkCount < 2*blinks; blinkCount++) { parent.repaint (); try { Thread.sleep (delay); } catch (InterruptedException e) { } } parent.repaint (); } public void paint (Graphics g) { if ((blinkCount % 2) == 0) { g.setColor (color); } else { g.setColor (Color.lightGray); } g.drawString (message, X, Y); } public void erase (Graphics g) { g.setColor (Color.lightGray); g.drawString (message, X, Y); } public boolean hit (Missile m) { return false; } public void explode () { } } /* Semaphore class used to synchronize threads. */ class Semaphore { private boolean taken; Semaphore () { taken = false; } Semaphore (boolean taken) { this.taken = taken; } public synchronized void take () { while (taken) { try { wait (); } catch (Exception e) { } } taken = true; } public synchronized boolean peek () { return taken; } public synchronized void give () { taken = false; notify (); } } /* The main applet. */ public class MissileCommando extends java.applet.Applet implements Runnable { /* Screen sizes. */ private final int SCORE_WIDTH = 70; private final int WORLD_WIDTH = 430; private final int WORLD_HEIGHT = 300; /* Base parameters. */ private final int BASES = 5; private final int BASE_SPACING = 30; private final int BASE_WIDTH = 50; private final int BASE_HEIGHT = 40; /* Points */ private final int POINTS_MISSILE = 100; private final int POINTS_EXTRA_SHOTS = 50; private final int POINTS_BASE = 100; private final int POINTS_NEW_BASE = 5000; /* Limit the number of concurrent shots. */ private final int MAX_SHOTS = 10; private int shots = 0; /* The sounds. */ private AudioClip startSound = null; private AudioClip applauseSound = null; private AudioClip missileSound = null; private AudioClip shotExplosionSound = null; private AudioClip missileExplosionSound = null; private AudioClip baseExplosionSound = null; private AudioClip music = null; /* State variables. */ private boolean playing = false; private boolean clearScreen = false; private boolean loadingSounds = false; /* Current game parameters. */ private int score = 0; private int level = 0; private int speed = 0; private int delay = 0; private int missileCount = 0; private int shotCount = 0; /* Synchronization semaphores. */ private Semaphore missileSemaphore; private Semaphore messageSemaphore; /* Fonts */ private Font font; private FontMetrics fontMetrics; /* Strings */ private String scoreString = "Score"; private String levelString = "Level"; private String shotString = "Shots"; private String welcomeString = "Click to start"; private String loadingString = "Loading sounds..."; /* Vector used to keep track of all moving objects (Things). */ private Vector things; /* The main applet's thread. */ private Thread thread = null; /* Initialize the applet. */ public void init () { font = new Font ("TimesRoman", Font.BOLD, 24); fontMetrics = getFontMetrics (font); setFont (font); resize (WORLD_WIDTH + SCORE_WIDTH, WORLD_HEIGHT); } /* Start a new game. */ public synchronized void newGame () { playing = true; clearScreen = true; stopThreads (); thread = new Thread (this); thread.start (); } /* The game engine. */ public void run () { int n; int newBases = 0; Thread.currentThread().setPriority (Thread.MIN_PRIORITY); score = 0; shotCount = 0; shots = 0; things = new Vector (32); missileSemaphore = new Semaphore (); messageSemaphore = new Semaphore (); getSounds (); createBases (); if (music != null) { music.loop (); } for (level = 1; ; level++) { if (level == 1) { if (startSound != null) { startSound.play (); } } else { if (applauseSound != null) { applauseSound.play (); } } messageSemaphore.take (); createMessage ("Level " + level); messageSemaphore.take (); messageSemaphore.give (); /* Missile speed. */ speed = 5 + (level - 1); if (speed > (WORLD_HEIGHT / 10)) { speed = WORLD_HEIGHT / 10; } /* Delay between missiles. */ delay = 2000 - ((level-1)*200); if (delay < 500) { delay = 500; } /* Number of missiles. */ missileCount = 5 + ((level-1)*5); /* Number of shots. */ shotCount = missileCount * 2; /* Wait until missiles are ready. */ missileSemaphore.take (); while (missileCount > 0) { try { Thread.sleep (delay); } catch (InterruptedException e) { } /* Fire a missile. */ createMissile (speed); missileCount--; } /* Wait until missiles are dead. */ missileSemaphore.take (); missileSemaphore.give (); /* See if any new bases can be created. */ n = score - newBases*POINTS_NEW_BASE; while (countBases () < BASES && n >= POINTS_NEW_BASE) { createBase (); n -= POINTS_NEW_BASE; newBases++; } /* Game over? */ n = countBases (); if (n == 0) { break; } else { score += n * POINTS_BASE; } } playing = false; if (music != null) { music.stop (); } /* Destroy the world! */ for (int i = 0; i < 5; i++) { int x = (int) (Math.random () * WORLD_WIDTH); int y = (int) (Math.random () * WORLD_HEIGHT); things.addElement (new Explosion (this, Color.red, x, y, 500)); } messageSemaphore.take (); createMessage ("GAME OVER"); messageSemaphore.take (); messageSemaphore.give (); } /* Stop all running threads. */ public void stopThreads () { if (things != null) { Enumeration e; e = things.elements (); while (e.hasMoreElements ()) { Thing thing = (Thing) e.nextElement (); thing.explode (); } } } /* Stop this applet. */ public void stop () { if (music != null) { music.stop (); } stopThreads (); if (thread != null) { thread.stop (); thread = null; } } /* Get all the sounds. */ void getSounds () { loadingSounds = true; repaint (); startSound = getAudioClip (getCodeBase (), "sounds/sub_dive_horn.au"); applauseSound = getAudioClip (getCodeBase (), "sounds/applause.au"); missileSound = getAudioClip (getCodeBase (), "sounds/missile.au"); shotExplosionSound = getAudioClip (getCodeBase (), "sounds/shot.au"); missileExplosionSound = getAudioClip (getCodeBase (), "sounds/beep_multi.au"); baseExplosionSound = getAudioClip (getCodeBase (), "sounds/bzzzt.au"); /* background music is disabled since the sound file is too large. */ music = null; /* getAudioClip (getCodeBase (), "sounds/pink_panther.au"); */ loadingSounds = false; } /* Create the bases. */ void createBases () { for (int i = 0; i < BASES; i++) { createBase (); } } /* Create one base, if possible. */ void createBase () { Enumeration e; boolean found; int i, x; for (i = 0, x = BASE_SPACING; i < BASES; i++, x += BASE_WIDTH + BASE_SPACING) { found = false; e = things.elements (); while (e.hasMoreElements ()) { Thing thing = (Thing) e.nextElement (); if (thing instanceof Base && thing.X == x) { found = true; } } if (!found) { things.addElement (new Base (this, Color.blue, x, WORLD_HEIGHT - BASE_HEIGHT - 1, BASE_WIDTH, BASE_HEIGHT)); break; } } } /* Create a shot explosion at x,y. */ void createShotExplosion (int x, int y) { if (shots > MAX_SHOTS) { return; } things.addElement (new ShotExplosion (this, x, y)); if (shotExplosionSound != null) { shotExplosionSound.play (); } shots++; } /* Create a base explosion at x,y. */ void createBaseExplosion (int x, int y) { things.addElement (new BaseExplosion (this, x, y)); if (baseExplosionSound != null) { baseExplosionSound.play (); } } /* Create a missile starting at x,y with speed. */ void createMissile (int x1, int y1, int speed) { int x2, y2; Color color = Color.red; x2 = (int)(Math.random () * WORLD_WIDTH); if (x2 == 0) x2 = 1; y2 = WORLD_HEIGHT - 2; things.addElement (new Missile (this, color, x1, y1, x2, y2, speed)); if (missileSound != null) { missileSound.play (); } } /* Create a missile at a random location with speed. */ void createMissile (int speed) { int x1, y1; x1 = (int)(Math.random () * WORLD_WIDTH); if (x1 == 0) x1 = 1; y1 = 2; createMissile (x1, y1, speed); } /* Create a blinking message. */ void createMessage (String string) { int w = fontMetrics.stringWidth (string); int x = WORLD_WIDTH/2 - w/2; int y = WORLD_HEIGHT/2; things.addElement (new Message (this, string, x, y)); } /* Handle events. */ public boolean handleEvent (Event e) { if (e.id == Event.MOUSE_DOWN) /* mouse click */ { if (playing && shotCount > 0 && e.x >= 0 && e.x <= WORLD_WIDTH && e.y >= 0 && e.y <= WORLD_HEIGHT) { createShotExplosion (e.x, e.y); shotCount--; } else if (!playing) { newGame (); } return true; } else { return super.handleEvent (e); } } /* Check all things for a collision with this missile. */ public void checkCollision (Missile missile) { Enumeration e; int nbases = 0, nexplosions = 0; e = things.elements (); while (e.hasMoreElements ()) { Thing thing = (Thing) e.nextElement (); if (thing != missile && thing.isAlive ()) { if (thing.hit (missile)) { thing.explode (); if (thing instanceof Base) { nbases++; } else if (thing instanceof ShotExplosion) { nexplosions++; score += POINTS_MISSILE; if (missileExplosionSound != null) { missileExplosionSound.play (); } } } } } if (nbases > 0) { createBaseExplosion (missile.X, missile.Y); } if (nbases > 0 || nexplosions > 0) { missile.explode (); } } /* Update (paint, erase) things. */ public synchronized void updateThings (Graphics g) { int i, j; Enumeration e; e = things.elements (); while (e.hasMoreElements ()) { Thing thing = (Thing) e.nextElement (); if (thing.isAlive ()) { thing.paint (g); if (thing instanceof Missile) { checkCollision ((Missile) thing); } } else { thing.erase (g); things.removeElement (thing); if (thing instanceof Message) { messageSemaphore.give (); } else if (thing instanceof ShotExplosion) { shots--; } } } } /* Return the number of remaining missiles. */ public int countMissiles () { Enumeration e; int count = 0; e = things.elements (); while (e.hasMoreElements ()) { Thing thing = (Thing) e.nextElement (); if (thing instanceof Missile && thing.isAlive ()) { count++; } } return count; } /* Return the number of remaining bases. */ public int countBases () { Enumeration e; int count = 0; e = things.elements (); while (e.hasMoreElements ()) { Thing thing = (Thing) e.nextElement (); if (thing instanceof Base && thing.isAlive ()) { count++; } } return count; } /* Draw the borders. */ public void updateBorder (Graphics g) { g.setColor (Color.black); g.drawRect (0, 0, WORLD_WIDTH + SCORE_WIDTH - 1, WORLD_HEIGHT - 1); g.drawLine (WORLD_WIDTH, 0, WORLD_WIDTH, WORLD_HEIGHT); } String numberToZeroPaddedString (int number, int length) { StringBuffer s; s = new StringBuffer (Integer.toString (number)); while (s.length () < length) { s.insert (0, "0"); } return s.toString (); } /* Draw the score. */ public synchronized void updateScore (Graphics g) { int h, w; int x, y; int n; StringBuffer s; w = fontMetrics.stringWidth ("00000"); h = fontMetrics.getHeight (); x = WORLD_WIDTH + 5; g.setColor (Color.black); y = h; g.drawString (scoreString, x, y); y += h; g.clearRect (x, y - h, w, h); g.drawString (numberToZeroPaddedString (score, 5), x, y); y += 2*h; g.drawString (shotString, x, y); y += h; g.clearRect (x, y - h, w, h); g.drawString (numberToZeroPaddedString (shotCount, 5), x, y); } /* Don't clear the screen; call paint. */ public void update (Graphics g) { paint (g); } /* Paint the screen. */ public void paint (Graphics g) { if (clearScreen) { g.setColor (Color.lightGray); g.fillRect (0, 0, WORLD_WIDTH + SCORE_WIDTH, WORLD_HEIGHT); clearScreen = false; } if (loadingSounds) { int w = fontMetrics.stringWidth (loadingString); int x = WORLD_WIDTH/2 - w/2; int y = WORLD_HEIGHT/2; g.setColor (Color.black); g.drawString (loadingString, x, y); clearScreen = true; } else if (!playing && (things == null || things.size () == 0)) { int w = fontMetrics.stringWidth (welcomeString); int x = WORLD_WIDTH/2 - w/2; int y = WORLD_HEIGHT/2; g.setColor (Color.black); g.drawString (welcomeString, x, y); } else if (things != null) { Graphics gc = g.create (0, 0, WORLD_WIDTH, WORLD_HEIGHT); updateThings (gc); /* No more missiles. */ if (missileSemaphore != null && countMissiles () == 0 && missileCount == 0) { missileSemaphore.give (); } } updateBorder (g); updateScore (g); } } /* Local variables: eval: (progn (make-local-variable 'compile-command) (setq compile-command (concat "javac " buffer-file-name))) End: */