/* * @(#)Decoder.java 1.0 97/09/22 * * Copyright (c) 1997 Erica Schulman. All Rights Reserved. * * This file contains the main classes for the Decoder applet, * along with some smaller ones used mainly as drawing elements. * The classes in this file are specific to the Decoder game and are * not expected to be reused. Several extend the Guess and HilitCanvas * classes, which are good candidates for reuse and form the basis * of the key classes. */ import java.awt.*; import java.applet.*; /** * The starting class of the Decoder applet, a variant of the * popular game Mastermind. This class performs initialization * and captures button clicks, but does not do much else. * *
* @version 1.0 22 September 1997 * @author Erica Schulman * @see DimpleCanvas * @see MarbleCanvas */ public class Decoder extends Applet implements DecoderConstants { // define drawing objects and images private Image boardImage; private Image unknownImage; static Image emptyImage; static Image fullImage; static Image [] marbleImage = new Image[NUMTYPES + 1]; static Image [] marbleImageGray = new Image[NUMTYPES + 1]; static Image [] marbleImageFocus = new Image[NUMTYPES + 1]; static Image soundOnImage; static Image soundOffImage; static MarbleCanvas [] marble = new MarbleCanvas[NUMTYPES + 1]; static DimpleCanvas [][] dimple = new DimpleCanvas[MAXNUMGUESSES][]; static MarbleCanvas [] answerMarble = new MarbleCanvas[NUMSLOTS]; static ScoreCanvas scoreboard; private Color boardColor = new Color(192, 192, 192); // audio files static AudioClip niceSound; static AudioClip winSound; static AudioClip loseSound; static AudioClip removeSound; static AudioClip clickSound; // spacing constants static final int xSpacing = 50; static final int ySpacing = 70; static final int xOffset = 70; // board offset + 1/2 board width - spacing factors static final int yOffset = 50 + 150 - (int)(0.5 * ySpacing * (NUMSLOTS -1)); static Button quitButton; static private Button newButton; private MediaTracker t; private static boolean soundOn = true; private static SoundCanvas soundSwitch; private static boolean showIntro = true; private static Font writeFont = new Font("Serif", Font.BOLD, 18); private static int fontSpacing; public void init() { super.init(); //{{INIT_CONTROLS setLayout(null); setSize(520,420); setForeground(new Color(12632256)); setBackground(new Color(16777215)); //}} fontSpacing = getFontMetrics(writeFont).getHeight(); t = new MediaTracker(this); // read in images boardImage = getImage( getCodeBase(), "board.jpg"); t.addImage(boardImage, 0); emptyImage = getImage( getCodeBase(), "dimple.jpg"); t.addImage(emptyImage, -1); fullImage = getImage( getCodeBase(), "active.jpg"); t.addImage(fullImage, -2); soundOnImage = getImage( getCodeBase(), "soundon.gif"); t.addImage(soundOnImage, -3); soundOffImage = getImage( getCodeBase(), "soundoff.gif"); t.addImage(soundOffImage, -4); unknownImage = getImage( getCodeBase(), "unknown.jpg"); t.addImage(unknownImage, -5); // these images are needed to instantiate DimpleCanvas try { t.waitForID(-1); t.waitForID(-2); } catch(InterruptedException e) { } for(int i = 0; i < MAXNUMGUESSES; i++) { int x, y; dimple[i] = new DimpleCanvas[ NUMSLOTS ]; x = xOffset + (i * xSpacing); for(int j = 0; j < NUMSLOTS; j++) { y = yOffset + (j * ySpacing); dimple[i][j] = new DimpleCanvas(x, y, i, j, emptyImage, fullImage); dimple[i][j].disable(); // no input until marble selected add(dimple[i][j]); dimple[i][j].setBackground(boardColor); dimple[i][j].hide(); } } // this image needed to instantiate MarbleCanvas try { t.waitForID(-5); } catch(InterruptedException e) { } for(int j = 0; j < NUMSLOTS; j++) { int y = yOffset + (j * ySpacing); answerMarble[j] = new MarbleCanvas(450, y, j, unknownImage, unknownImage); answerMarble[j].disable(); add(answerMarble[j]); answerMarble[j].setBackground(boardColor); answerMarble[j].hide(); } for(int i = 1; i <= NUMTYPES; i++) { String s = "marble" + i + ".jpg"; String s2 = "marble" + i + "a.jpg"; String s3 = "marble" + i + "c.jpg"; marbleImage[i] = getImage( getCodeBase(), s); marbleImageGray[i] = getImage( getCodeBase(), s2); marbleImageFocus[i] = getImage( getCodeBase(), s3); t.addImage(marbleImage[i], i); t.addImage(marbleImageGray[i], i+10); t.addImage(marbleImageFocus[i], i+20); // images needed for instantiation try { t.waitForID(i); t.waitForID(i+20); } catch(InterruptedException e) {} marble[i] = new MarbleCanvas(60 + 60*(i-1), 390, i, marbleImage[i], marbleImageFocus[i]); add(marble[i]); marble[i].hide(); } // images needed for instantiation of SoundCanvas try { t.waitForID(-3); t.waitForID(-4); } catch(InterruptedException e) { } soundSwitch = new SoundCanvas(soundOnImage); add(soundSwitch); soundSwitch.hide(); scoreboard = new ScoreCanvas(); add(scoreboard); scoreboard.hide(); /* it was very tempting to extend HilitCanvas again instead of using the ugly Java buttons, but I wanted to use a variety of components */ Font f = new Font("Serif", Font.BOLD, 15); quitButton = new Button("Solve"); quitButton.setBackground(boardColor); quitButton.setForeground(Color.black); quitButton.setFont(f); add(quitButton); newButton = new Button("New Game"); newButton.setBackground(boardColor); newButton.setForeground(Color.black); newButton.setFont(f); add(newButton); // read in sound files niceSound = getAudioClip( getCodeBase(), "drip.au"); winSound = getAudioClip( getCodeBase(), "win.au"); loseSound = getAudioClip( getCodeBase(), "lose2.au"); clickSound = getAudioClip( getCodeBase(), "click.au"); removeSound = getAudioClip( getCodeBase(), "delete.au"); } /** * Due to the nature of the game, this paint function is rarely called * except in the very beginning; most of the updating occurs in the canvases * that make up individual components. Therefore this has no double buffering, * and some of the code -- resizing the buttons, for example -- is inefficient * for the sake of an elegant effect. */ public void paint(Graphics g) { // while waiting for images to load, show this if(showIntro) { g.setColor(Color.black); g.setFont(writeFont); g.drawString("Decoder", 50, 50); g.drawString("Last revision: 9/22/97", 50, 50 + fontSpacing); g.drawString("Please wait while images are loading...", 50, 300); } // don't draw anything else until all images are loaded try { t.waitForAll(); } catch(InterruptedException exc) { return; } // if an error occurred in loading the images, display error message if((t.statusAll(true) & MediaTracker.ERRORED) != 0) { g.setColor(getBackground()); g.fillRect(0, 0, size().width, size().height); g.setColor(Color.red); g.drawString("Error -- unable to load images.", 120, 200); g.drawString("Try Shift + Reload button", 120, 260); return; } if(showIntro) { showIntro = false; // once the images are loaded, don't show intro again g.setColor(getBackground()); g.fillRect(0, 0, size().width, size().height); } // draw the board g.drawImage(boardImage, 10, 50, this); // draw the marbles at the bottom for(int i = 1; i<= NUMTYPES; i++) { marble[i].show(); } // draw the dimples in the board for(int i = 0; i < MAXNUMGUESSES; i++) { for(int j = 0; j < NUMSLOTS; j++) { dimple[i][j].show(); } } // draw the puzzle marbles to the right for(int i = 0; i < NUMSLOTS; i++) { answerMarble[i].show(); } // obvious... scoreboard.show(); soundSwitch.show(); /* the buttons are resized here to prevent them from showing up well before anything else */ quitButton.resize(85, 20); quitButton.move(355, 360); newButton.resize(85, 20); newButton.move(355, 390); } /** * Repaints without clearing. */ public void update(Graphics g) { paint(g); } /** * Toggles the sound on or off. */ static void toggleSound() { if(soundOn) { soundOn = false; soundSwitch.setImage(soundOffImage); soundSwitch.repaint(); } else { soundOn = true; soundSwitch.setImage(soundOnImage); soundSwitch.repaint(); } } /** * Tells whether sound is enabled, or not. */ static boolean isSoundOn() { return soundOn; } /** * Detects whether either button was pressed. */ public boolean action(Event e, Object o) { if(e.target instanceof Button) { if( "Solve".equals((String) o)) { DimpleCanvas.showAnswer(); quitButton.disable(); return(true); } else if( "New Game".equals((String) o)) { newGame(); return(true); } } return(super.handleEvent(e)); } // start a new game private void newGame() { MarbleCanvas.clearMarbles(); DimpleCanvas.setNewAnswer(); scoreboard.setMessage(""); scoreboard.repaint(); for(int i = 0; i < MAXNUMGUESSES; i++) { for(int j = 0; j < NUMSLOTS; j++) { dimple[i][j].setImageOff(emptyImage); dimple[i][j].setImageOn(fullImage); dimple[i][j].setEmpty(); dimple[i][j].repaint(); } } DimpleCanvas.setAttemptsEmpty(); for(int i = 0; i < NUMSLOTS; i++) { answerMarble[i].setImageOn(unknownImage); answerMarble[i].hilitOn(); answerMarble[i].repaint(); } quitButton.enable(); } //{{DECLARE_CONTROLS //}} } /** * A class to hold the playing marbles. As an extension of HilitCanvas, the * marbles react to mouse movement. A playing marble must be selected in order * to interact with the board (represented by class DimpleCanvas). * * @see DimpleCanvas */ class MarbleCanvas extends HilitCanvas implements DecoderConstants { private static boolean locked = false; MarbleCanvas(int x, int y, int num, Image off, Image on) { super(x, y, num, off, on); } boolean isLocked() { return(locked); } /** * Overloads parent function -- only reacts if marbles are unlocked. */ public boolean mouseEnter(Event e, int x, int y) { if(locked == true) { return(true); } else { return(super.mouseEnter(e, x, y)); } } /** * Overloads parent function -- only reacts if marbles are unlocked. */ public boolean mouseExit(Event e, int x, int y) { if(locked == true) { return(true); } else{ return(super.mouseExit(e, x, y)); } } /** * All marbles are deselected, and mouse input is enabled. * Interactions with the board are disabled. */ static void clearMarbles() { // unlock all the marbles unlock(); // turn highlighting off for each, and repaint for(int i = 1; i <= NUMTYPES; i++) { Decoder.marble[i].hilitOff(); Decoder.marble[i].repaint(); } // can't make a new guess until a marble is picked DimpleCanvas.setActive(false); } // allow marbles to receive mouse input private static void unlock() { for(int i = 1; i <= NUMTYPES; i++) { Decoder.marble[i].enable(); } locked = false; } /** * Prevent marbles from receiving mouse input. Permit * interactions with the board -- a marble has been chosen. */ static void lock() { locked = true; DimpleCanvas.setActive(true); } /** * Chooses a marble to play with. */ public boolean mouseDown(Event e, int x, int y) { clearMarbles(); hilitOn(); lock(); if(Decoder.isSoundOn()) { try { Decoder.clickSound.play(); } catch(NullPointerException exc) {} } repaint(); return(true); } /** * Repaints without clearing. (Highlighted and * non-highlighted images are the same size for this * class.) */ public void update(Graphics g) { paint(g); } } /** * A class which literally contains the dimples on the board. These * dimples are filled with marbles, column by column, as the player * attempts to match the computer's marbles. As an extension of HilitCanvas, * the dimples will react to mouse movement. * *
Marbles added to the dimples will also be stored in an array of * Guess objects. This class performs all scoring functions. */ class DimpleCanvas extends HilitCanvas implements DecoderConstants { private static int [] nearlyRight = new int[MAXNUMGUESSES]; private static int [] justRight = new int[MAXNUMGUESSES]; /** * Column currently being filled. */ private static int currentAttempt = 0; /** * Row associated with this dimple. */ private int slot; private boolean filled = false; private boolean [] playerSlotFilled = new boolean[NUMSLOTS]; private boolean [] computerSlotFilled = new boolean[NUMSLOTS]; /** * The computer's answer. */ private static Guess right; /** * Array of player's guesses. */ private static Guess [] attempt = new Guess[MAXNUMGUESSES]; static { right = new Guess(NUMSLOTS); right.makeRandom(NUMTYPES, MAXDUPLICATIONS); } DimpleCanvas(int x, int y, int guess, int slt, Image off, Image on) { super(x, y, (guess * NUMSLOTS) + slt, off, on); slot = slt; } /** * Shows the computer's answer. */ static void showAnswer() { MarbleCanvas.clearMarbles(); // deselect playing marbles at bottom for(int i = 1; i <= NUMTYPES; i++) { Decoder.marble[i].disable(); } for(int i = 0; i < NUMSLOTS; i++) { // convert each element in guess object to an integer int j = Integer.parseInt(right.toString(i)); Decoder.answerMarble[i].setImageOn(Decoder.marbleImageGray[j]); Decoder.answerMarble[i].hilitOn(); Decoder.answerMarble[i].repaint(); } } /** * Tell the computer to make a new solution. */ static void setNewAnswer() { right.makeRandom(NUMTYPES, MAXDUPLICATIONS); } /** * Shows which the number of the current guess, * which corresponds to the active dimple column. */ static int getCurrentAttempt() { return(currentAttempt); } /** * Returns the number of player marbles that are of the * right color, but in the wrong place. */ static int numNearlyRight(int numguess) { try { return( nearlyRight[numguess] ); } catch(ArrayIndexOutOfBoundsException exc) { System.err.println("numNearlyRight called for guess " + numguess); return(0); } } /** * Returns the number of player marbles that are of the * right color and in the right place. */ static int numJustRight(int numguess) { try { return( justRight[numguess] ); } catch(ArrayIndexOutOfBoundsException exc) { System.err.println("numJustRight called for guess " + numguess); return(0); } } /** * Allows or prevents a column of dimples from accepting input. A playing * marble must be selected before the dimples will be active. */ static void setActive(boolean activeFlag) { try { if(activeFlag) { for(int i = 0; i < NUMSLOTS; i++) { Decoder.dimple[currentAttempt][i].enable(); } } else { for(int i = 0; i < NUMSLOTS; i++) { Decoder.dimple[currentAttempt][i].disable(); } } } catch(ArrayIndexOutOfBoundsException exc) {} } /** * The main form of interaction with the board. Clicks cause marbles * to be placed down or removed. If the new marble fills a column, the * column is automatically scored. */ public boolean mouseDown(Event e, int x, int y) { int choice; // if the dimple doesn't already have an occupant if(filled == false) { choice = -1; // determine which playing marble was chosen earlier for(int i = 1; i <= NUMTYPES; i++) { if( (Decoder.marble[i]).isHilit() ) { choice = i; break; } } /* Check that a valid marble was chosen. This should always be true; the dimple will not accept mouse input unless a playing marble has been chosen earlier. */ if(( choice > 0 ) && ( choice <= NUMTYPES )) { filled = true; // both images are set identically to prevent flicker setImageOn(Decoder.marbleImageGray[choice]); setImageOff(Decoder.marbleImageGray[choice]); repaint(); // play nice sound if(Decoder.isSoundOn()) { try {Decoder.niceSound.play(); } catch(NullPointerException exc) {} } /* save playing marble choice in Guess object, with position (slot) determined by which dimple canvas was clicked */ try { attempt[currentAttempt].setValue(slot, choice); } catch(NullPointerException exc) { attempt[currentAttempt] = new Guess(NUMSLOTS); attempt[currentAttempt].setValue(slot, choice); } // if we have finished a full column, score it if( attempt[currentAttempt].isFull() ) { MarbleCanvas.clearMarbles(); /* clear playing marbles (and disable current column) */ // guess matches computer's -- player wins if( attempt[currentAttempt].equals(right) ) { victory(); return(true); } // calculate score justRight[currentAttempt] = 0; nearlyRight[currentAttempt] = 0; for(int i = 0; i < NUMSLOTS; i++) { playerSlotFilled[i] = false; computerSlotFilled[i] = false; } // find out how many marbles are right color, right place for(int i = 0; i < NUMSLOTS; i++) { if( right.equals( attempt[currentAttempt], i)) { justRight[currentAttempt]++; playerSlotFilled[i] = true; computerSlotFilled[i] = true; } } /* find out how many marbles are right color, wrong place (no double counting with respect to previous score) */ for(int i = 0; i < NUMSLOTS; i++) { if( !playerSlotFilled[i] ) { for(int j = 0; j < NUMSLOTS; j++) { if((i != j) && (!computerSlotFilled[j])) { if( attempt[currentAttempt].equals(right, i, j)) { nearlyRight[currentAttempt]++; playerSlotFilled[i] = true; // doesn't really matter computerSlotFilled[j] = true; // does matter break; } } } } } currentAttempt++; Decoder.scoreboard.repaint(); // too many guesses, player loses if( currentAttempt >= MAXNUMGUESSES ) { defeat(); return(true); } // disable our new column until a playing marble is selected setActive(false); } } else { // no marble chosen -- we should never reach this point hilitOff(); } } else { // spot is already filled -- take move back setEmpty(); setImageOn(Decoder.fullImage); setImageOff(Decoder.emptyImage); // play remove sound if(Decoder.isSoundOn()) { try {Decoder.removeSound.play(); } catch(NullPointerException exc) {} } } // turn highlighting off hilitOff(); repaint(); return(true); } // the joy of victory private void victory() { showAnswer(); ScoreCanvas.setMessage("You win!"); Decoder.scoreboard.repaint(); Decoder.quitButton.disable(); if(Decoder.isSoundOn()) { try { Decoder.winSound.play(); } catch(NullPointerException exc) {} } } // ... and the agony of defeat private void defeat() { showAnswer(); ScoreCanvas.setMessage("You lose..."); Decoder.scoreboard.repaint(); Decoder.quitButton.disable(); if(Decoder.isSoundOn()) { try { Decoder.loseSound.play(); } catch(NullPointerException exc) {} } } /** * Set the Guess element represented by this dimple to zero. */ void setEmpty() { try { filled = false; attempt[currentAttempt].setValue(slot, 0); } catch(ArrayIndexOutOfBoundsException exc) {} catch(NullPointerException exc) {} } /** * Fill all Guess objects with zero (empty the board). * For the board to _look_ empty, the images must be reset as well. * In general, we will also want to set filled = false for * each dimple (done by the setEmpty() function, which is not static so * cannot be called directly here). */ static void setAttemptsEmpty() { for(int i = 0; i < MAXNUMGUESSES; i++) { for(int j = 0; j < NUMSLOTS; j++) { try { attempt[i].setValue(j, 0); } catch(NullPointerException exc) {} } } currentAttempt = 0; } /** * Repaints without clearing. (Highlighted and * non-highlighted images are the same size for this * class.) */ public void update(Graphics g) { paint(g); } } /** * The canvas to show the score for each of the player's guesses. * It also shows victory/defeat messages. */ class ScoreCanvas extends java.awt.Canvas implements DecoderConstants { private static String message = ""; private static int spacing; private Font writeFont = new Font("Serif", Font.BOLD, 15); ScoreCanvas() { setBackground(null); resize(500, 50); move(0,0); // so we will be centered for all platforms, browsers spacing = getFontMetrics(writeFont).charWidth('0') / 2; } /** * Show the score. For each column, the top number represents the number * of marbles that are of the right color and in the right place; the bottom * number represents marbles that are the right color, but in the wrong place. * There is no double counting; a marble will be counted as one of these at most. */ public void paint(Graphics g) { g.setFont(writeFont); g.setColor(Color.magenta); for(int i = 0; i < (DimpleCanvas.getCurrentAttempt()); i++) { g.drawString("" + DimpleCanvas.numJustRight(i), Decoder.xOffset + (i*Decoder.xSpacing) - spacing, 20); g.drawString("" + DimpleCanvas.numNearlyRight(i), Decoder.xOffset + (i*Decoder.xSpacing) - spacing, 40); } g.setColor(Color.blue); g.drawString(message, Decoder.xOffset + (MAXNUMGUESSES * Decoder.xSpacing), 30); } /** * An additional short message that may be displayed on the scoreboard. */ static void setMessage(String s) { message = s; } } /** * The canvas to show the sound switch and control its state. */ class SoundCanvas extends Canvas { private Image image; SoundCanvas(Image i) { setImage(i); setBackground(null); resize(i.getWidth(this), i.getHeight(this)); move(460, 370); } /** * Change image associated with the switch. */ void setImage(Image i) { image = i; resize(i.getWidth(this), i.getHeight(this)); } public void paint(Graphics g) { g.drawImage(image, 0, 0, this); } /** * When clicked, toggle sound on or off. */ public boolean mouseDown(Event e, int x, int y) { Decoder.toggleSound(); return true; } }