// CSE 143, Winter 2009, Marty Stepp // Homework 7: 20 Questions import java.applet.*; import java.awt.*; import java.awt.event.*; import java.io.*; import java.net.*; import java.util.*; import java.util.concurrent.*; import javax.swing.*; /** * The graphical user interface (GUI) that talks to your QuestionTree * to play a graphical 20 questions game ("The Sith Sense"). */ public class VaderMain implements ActionListener, Runnable, UserInterface { private static final boolean DEBUG = true; // set to true to print I/O messages // sound options private static final boolean MUSIC = true; private static final boolean SOUNDFX = true; private static final double SOUND_PERCENTAGE = 0.7; // how often Vader talks private static final int NUM_SOUNDS = 9; // # of wav files (up to vader9.wav) // text messages that appear on the window // (these don't need to be constants, but it is bad practice to // scatter various strings and messages throughout a GUI program's code.) private static final String TITLE = "CSE 143: The Sith Sense"; private static final String MUSIC_MESSAGE = "Music"; private static final String SFX_MESSAGE = "Sound FX"; private static final String YES_MESSAGE = "Yes"; private static final String NO_MESSAGE = "No"; private static final String ERROR_MESSAGE = "Error"; // file names, paths, URLs private static final String RESOURCE_URL = "http://www.cs.washington.edu/education/courses/cse143/09wi/homework/7/"; private static final String BACKGROUND_IMAGE_FILE_NAME = "background.png"; private static final String MUSIC_FILE_NAME = "empire.mid"; private static final String SAVE_DEFAULT_FILE_NAME = "memory.txt"; private static final String SOUND_FILE_NAME = "vader%d.wav"; // visual elements private static final Font FONT = new Font("SansSerif", Font.BOLD, 18); private static final Color COLOR = new Color(6, 226, 240); // light teal // runs the program public static void main(String[] args) { new VaderMain(); } // data fields private JFrame frame; private JLabel vader, bannerLabel; private JTextArea statsArea, messageLabel; private JTextField inputField; private JButton yesButton, noButton; private JCheckBox musicBox, soundBox; private AudioClip musicClip; private QuestionTree game; // these queues hold boolean or String user input waiting to be read private BlockingQueue booleanQueue = new LinkedBlockingQueue(); private BlockingQueue stringQueue = new LinkedBlockingQueue(); private boolean waitingForBoolean = false; private boolean waitingForString = false; // constructs and sets up GUI and components public VaderMain() { game = new QuestionTree(this); // construct everybody frame = new JFrame(TITLE); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setResizable(false); ensureFile(BACKGROUND_IMAGE_FILE_NAME); vader = new JLabel(new ImageIcon(ClassLoader.getSystemResource(BACKGROUND_IMAGE_FILE_NAME))); vader.setLayout(null); // layout frame.add(vader); frame.pack(); center(frame); // construct other components inputField = new JTextField(30); setupComponent(inputField, new Point(20, 180), new Dimension(300, 25)); inputField.setCaretColor(Color.GREEN); inputField.addActionListener(this); messageLabel = new JTextArea(); setupComponent(messageLabel, new Point(20, 120), new Dimension(365, 125)); messageLabel.setLineWrap(true); messageLabel.setWrapStyleWord(true); messageLabel.setEditable(false); messageLabel.setFocusable(false); bannerLabel = new JLabel(); setupComponent(bannerLabel, new Point(0, 0), new Dimension(vader.getWidth(), 30)); bannerLabel.setHorizontalAlignment(SwingConstants.CENTER); statsArea = new JTextArea(); setupComponent(statsArea, new Point(vader.getWidth() - 200, vader.getHeight() - 50), new Dimension(200, 50)); statsArea.setEditable(false); statsArea.setFocusable(false); yesButton = makeButton(YES_MESSAGE, new Point(340, 50), new Dimension(80, 30)); noButton = makeButton(NO_MESSAGE, new Point(440, 50), new Dimension(80, 30)); musicBox = makeCheckBox(MUSIC_MESSAGE, MUSIC, new Point(vader.getWidth() - 200, vader.getHeight() - 120), new Dimension(120, 20)); soundBox = makeCheckBox(SFX_MESSAGE, SOUNDFX, new Point(vader.getWidth() - 200, vader.getHeight() - 95), new Dimension(120, 20)); doEnabling(); frame.setVisible(true); // start background thread to loop and play the actual games // (it has to be in a thread so that the game loop can wait without // the GUI locking up) new Thread(this).start(); } /** Handles user interactions with the graphical components. */ public void actionPerformed(ActionEvent event) { Object src = event.getSource(); try { if (src == yesButton) { booleanQueue.put(true); } else if (src == noButton) { booleanQueue.put(false); } else if (src == inputField) { // user pressed Enter on input text field; capture input String text = inputField.getText(); inputField.setText(null); stringQueue.put(text); } else if (src == musicBox) { // play or stop music if (musicClip != null) { if (musicBox.isSelected()) { musicClip.loop(); } else { musicClip.stop(); } } } } catch (InterruptedException e) {} doEnabling(); playSound(); } /** Waits for the user to type a line of text and returns that line. */ public String ask(String question) { return ask(question, null); } /** Outputs the given text onto the GUI. */ public void println(String text) { messageLabel.setText(text); if (DEBUG) { System.out.println(text); } } /* The basic game loop, which will be run in a separate thread. */ public void run() { // load audio resources playMusic(); // load data saveLoad(false); // play many games do { game.play(); } while (yesNo(PLAY_AGAIN_MESSAGE)); // save data saveLoad(true); bannerLabel.setVisible(false); // shut down // frame.setVisible(false); // frame.dispose(); // System.exit(0); } /** Waits for the user to press Yes or No and returns the boolean. */ public boolean yesNo(String question) { println(question); setWaitingForBoolean(true); try { boolean result = booleanQueue.take(); messageLabel.setText(null); if (DEBUG) { System.out.println(result); } return result; } catch (InterruptedException e) { return false; } finally { setWaitingForBoolean(false); } } // private helper for asking a question with the given initial text private String ask(String question, String defaultValue) { println(question); inputField.setText(defaultValue); setWaitingForString(true); try { // grab/store text from box; clear the box text String result = stringQueue.take(); messageLabel.setText(null); if (DEBUG) { System.out.println(result); } return result; } catch (InterruptedException e) { return ""; } finally { setWaitingForString(false); } } // sets JFrame's position to be in the center of the screen private void center(JFrame frame) { Dimension screen = Toolkit.getDefaultToolkit().getScreenSize(); frame.setLocation((screen.width - frame.getWidth()) / 2, (screen.height - frame.getHeight()) / 2); } // turns on/off various graphical components when game events occur private void doEnabling() { inputField.setVisible(waitingForString); if (waitingForString) { inputField.requestFocus(); } yesButton.setVisible(waitingForBoolean); noButton.setVisible(waitingForBoolean); bannerLabel.setText(BANNER_MESSAGE); statsArea.setText(String.format(STATUS_MESSAGE, game.totalGames(), game.gamesWon())); } // helper to download from the given URL to the given local file private static void download(String urlString, String filename) throws IOException, MalformedURLException { System.out.println("Downloading " + filename); URL url = new URL(urlString); InputStream stream = url.openStream(); // read bytes from URL into a byte[] ByteArrayOutputStream bytes = new ByteArrayOutputStream(); while (true) { int b = stream.read(); if (b < 0) { break; } bytes.write(b); } // write bytes from byte[] to file FileOutputStream out = new FileOutputStream(filename); out.write(bytes.toByteArray()); out.close(); } // Tries to download the given file if it does not exist. private void ensureFile(String filename) { File file = new File(filename); if (!file.exists()) { try { download(RESOURCE_URL + filename, filename); } catch (Exception e) { JOptionPane.showMessageDialog(frame, e.toString(), ERROR_MESSAGE, JOptionPane.ERROR_MESSAGE); if (DEBUG) { System.out.println(e); e.printStackTrace(); } } } } // helper method to create one button at specified position/size private JButton makeButton(String text, Point location, Dimension size) { JButton button = new JButton(text); button.setMnemonic(text.charAt(0)); setupComponent(button, location, size); button.setOpaque(true); button.setContentAreaFilled(false); button.addActionListener(this); button.setFocusPainted(false); return button; } // helper method to create one button at specified position/size private JCheckBox makeCheckBox(String text, boolean selected, Point location, Dimension size) { JCheckBox box = new JCheckBox(text, selected); box.setMnemonic(text.charAt(0)); setupComponent(box, location, size); box.setOpaque(true); box.setContentAreaFilled(false); box.addActionListener(this); box.setFocusPainted(false); return box; } // plays the background theme music private void playMusic() { if (musicBox.isSelected()) { try { if (musicClip == null) { ensureFile(MUSIC_FILE_NAME); musicClip = Applet.newAudioClip(ClassLoader.getSystemResource(MUSIC_FILE_NAME)); } musicClip.loop(); } catch (NullPointerException e) {} } } // randomly picks a wav file and plays it private void playSound() { if (soundBox.isSelected() && Math.random() >= SOUND_PERCENTAGE) { try { int rand = 1 + (int) (Math.random() * NUM_SOUNDS); String filename = String.format(SOUND_FILE_NAME, rand); ensureFile(filename); Applet.newAudioClip(ClassLoader.getSystemResource(filename)).play(); } catch (NullPointerException e) {} } } // common code for asking the user whether they want to save or load private void saveLoad(boolean save) { if (save ? yesNo(SAVE_MESSAGE) : yesNo(LOAD_MESSAGE)) { String filename = ask(SAVE_LOAD_FILENAME_MESSAGE, SAVE_DEFAULT_FILE_NAME); try { if (save) { PrintStream out = new PrintStream(new File(filename)); game.save(out); } else { Scanner in = new Scanner(new File(filename)); game.load(in); } } catch (FileNotFoundException e) { JOptionPane.showMessageDialog(frame, e.toString(), ERROR_MESSAGE, JOptionPane.ERROR_MESSAGE); if (DEBUG) { System.out.println(e); e.printStackTrace(); } } } } // sets standard fonts, colors, location and such for the given component private void setupComponent(JComponent comp, Point location, Dimension size) { comp.setLocation(location); comp.setSize(size); comp.setForeground(COLOR); comp.setFont(FONT); comp.setOpaque(false); vader.add(comp); } // sets the GUI to wait for a yes/no user input private void setWaitingForBoolean(boolean value) { waitingForBoolean = value; doEnabling(); } // sets the GUI to wait for a text user input private void setWaitingForString(boolean value) { waitingForString = value; doEnabling(); } }