/** * Copyright (C) 2013 Tokanagrammar Team * * This is a jigsaw-like puzzle game, * except each piece is token from a source file, * and the 'complete picture' is the program. * * 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 3 of the License, or * 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, see <http://www.gnu.org/licenses/>. */ package edu.umb.cs.gui; import edu.umb.cs.Tokanagrammar; import edu.umb.cs.api.APIs; import edu.umb.cs.entity.Category; import edu.umb.cs.entity.Hint; import edu.umb.cs.entity.Puzzle; import edu.umb.cs.gui.screens.SecondaryScreen; import edu.umb.cs.parser.BracingStyle; import edu.umb.cs.source.*; import java.util.*; import javafx.collections.ObservableList; import javafx.scene.Node; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.effect.BoxBlur; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.AnchorPane; import javafx.scene.text.Font; import javafx.scene.text.Text; import javax.swing.ImageIcon; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JPanel; /** * Handle game states and also work as a main GUI API. * @author Matt */ public class GUI { public enum GameState {INIT_GUI, START_GAME, FULL_LHS, COMPILING}; // maximum time allowed for compilation private static long TIME_OUT = 10000; private static GameBoard gameBoard; private static GUITimer timer; private static OutputPanel outputPanel; private static GameState curGameState; /**maps the gameState to a list of active buttons while in this state**/ // TODO: use Map and List private static HashMap<GameState, ArrayList<String>> activeButtons; private int curDifficulty; private List<Category> categories; private List<SourceToken> tokenBayTokens; private List<SourceToken> tokenBoardTokens; private List<Category> curCategories; private Iterator<Puzzle> puzzlesIter; private Set<Puzzle> puzzles; private static BracingStyle curBracingStyle = BracingStyle.ALLMAN; /** Used to blur screen on pausing*/ private static boolean init = false; private static boolean inGame; private static final GUI gui = new GUI(); private static Puzzle curPuzzle; private static int curHint = 0; private static ShuffledSource currentSource; private static final Font defaultFont = new Font(14); private GUI(){} // ----- /** * GUI uses the singleton pattern. * @return */ public static GUI getInstance(){ if(!init){ setupActiveButtonsTable(); gameBoard = GameBoard.getInstance(); outputPanel = OutputPanel.getInstance(); timer = GUITimer.getInstance(); init = true; } return gui; } //-------------------------------------------------------------------------- //GAMESTATES /** * Initialize the GUI: * gameState is "initGUI" * Welcome the user and give prompt to choose category. */ public void gameState_initGUI(){ inGame = false; curGameState = GameState.INIT_GUI; blurOff(); printWelcomeMessage(); initButtons(activeButtons.get(curGameState)); } /** * Start the game gameState is "startGame" The user has selected a category * (or categories) and pressed "start". */ public void gameState_startGame(boolean newGame) { curGameState = GameState.START_GAME; blurOff(); //initialization of new start of game if (!inGame) { if (newGame || curPuzzle == null || currentSource == null) { SourceFile orig = null; try { // wrap-around to the beginning of the collection if (!puzzlesIter.hasNext()) puzzlesIter = puzzles.iterator(); curPuzzle = puzzlesIter.next(); orig = curPuzzle.getSourceFile(curBracingStyle); } catch (Exception ex) { ex.printStackTrace(); outputPanel.compilerMessage("Error retrieving puzzles: " + ex.getMessage()); } if (orig != null) { currentSource = edu.umb.cs.api.APIs.shuffle(orig, curDifficulty); tokenBayTokens = currentSource.getRemovedTokens(); gameBoard.initTokenBoard(currentSource.getShuffledSource()); gameBoard.initTokenBay(RHSTokenIconizer.iconizeTokens(tokenBayTokens)); outputPanel.clear(); printCategoryAndDifficultyMessage(); // Some message on the puzzle if (orig != null) { outputPanel.infoMessage("Total (removable) tokens: " + currentSource.totalRemovable()); outputPanel.infoMessage("Removed: " + currentSource.removedCount() + "(" + curDifficulty + "%)"); } } } else { tokenBayTokens = currentSource.getRemovedTokens(); gameBoard.initTokenBoard(currentSource.getShuffledSource()); gameBoard.initTokenBay(RHSTokenIconizer.iconizeTokens(tokenBayTokens)); //outputPanel.clear(); //printCategoryAndDifficultyMessage(); } } if (GameBoard.getInstance().isRHSempty()) { enableCompileButton(); curGameState = GameState.FULL_LHS; } timer.start(); inGame = true; initButtons(activeButtons.get(curGameState)); } //-------------------------------------------------------------------------- //UTIL /** * Pause the game * All secondary screens go here. */ public void pauseGame(SecondaryScreen screen){ timer.stop(); blurOn(); screen.setupScreen(); } /** * Blurs the main frame of the GUI (it's AnchorPane). */ public void blurOn(){ AnchorPane mainScreen = Tokanagrammar.getAnchorPane(); ObservableList<Node> screenComponents = mainScreen.getChildren(); BoxBlur bb = new BoxBlur(); bb.setIterations(3); for(Node node: screenComponents) node.effectProperty().set(bb); } /** * Turn blur off the main frame. */ public void blurOff(){ AnchorPane mainScreen = Tokanagrammar.getAnchorPane(); ObservableList<Node> screenComponents = mainScreen.getChildren(); for(Node node: screenComponents) node.effectProperty().set(null); } /** * Reset the Game * Warning: This reboots the game completely! * If you want to place the orig rhs and lhs tokens * back to their original state, use refresh. * * GameState is Reset */ public void resetGame(){ LHSTokenIconizer.resetIndex(); RHSTokenIconizer.resetIndex(); gameBoard.resetTokenBay(); gameBoard.resetTokenBoard(); timer.reset(); outputPanel.clear(); } /** * Refresh the Game * Places all original tokens back to their * original place -- DOES NOT RESET TIMER. * * GameState is Refresh */ public void refreshGame() { //TODO @mhs this is same as reset for now. LHSTokenIconizer.resetIndex(); RHSTokenIconizer.resetIndex(); gameBoard.resetTokenBay(); gameBoard.resetTokenBoard(); inGame = false; gameState_startGame(false); } /** * Skips the current board and goes to the next. //TODO backend */ public void skipPuzzle(){ System.out.println("<<<Back end for getting the next puzzle>>>"); //TODO backend outputPanel.clear(); printCategoryAndDifficultyMessage(); resetGame(); inGame = false; gameState_startGame(true); } /** * Called when all of the RHS is empty. */ public void enableCompileButton(){ System.out.println("ENABLE COMPILE BUTTON CALLED"); initButtons(activeButtons.get(GameState.FULL_LHS)); } /** * Called when there is at least one iToken on the RHS. */ public void disableCompileButton(){ initButtons(activeButtons.get(GameState.START_GAME)); } /** * Called after all the RHS tokens are on the LHS and the compile btn * was enabled, then pressed. */ public void compileNewSource(){ //TODO Backend specialty! List<LHSIconizedToken> tokenList; //For now, just print formated source code! GameBoard gb = GameBoard.getInstance(); tokenList = gb.getTokenBoardItokens(); System.out.println("\n\nCompiling New Source Code."); //Stop the timer to save the user precious ms while //compiling -- restart it immediatly below if there are errors. timer.stop(); // build the content of the file // (ie., just dump it to a string) final StringBuilder bd = new StringBuilder(); for (LHSIconizedToken tk : tokenList) { SourceToken srcTk = tk.getSourceToken(); if (srcTk.kind() != SourceTokenKind.EMPTY) bd.append(tk.getSourceToken().image()); } enableStopButton(); final JFrame waitPopup = new WaitWindow(); waitPopup.setVisible(true); compileTask.src = bd.toString(); new Thread(compileTask).start(); try { // wait for compilation synchronized(COMPILE_LOCK) { COMPILE_LOCK.wait(TIME_OUT); } System.out.println("done compiling:"); Output out = compileTask.out; if (out == null) { waitPopup.dispose(); blurOff(); outputPanel.compilerMessage("Compilation took too long! Please try again!"); } else if (out.isError()) { outputPanel.compilerMessage("The program has the following errors:"); outputPanel.compilerMessage(out.getOuput()); timer.start(); } else { if (out.getOuput().equals(curPuzzle.getExpectedOutput())) { outputPanel.infoMessage("Congratulations! You have successfully solved the puzzle!"); outputPanel.infoMessage("The output is:\n-----"); outputPanel.outputText(out.getOuput()); outputPanel.infoMessage("-----"); } else { outputPanel.compilerMessage("Your program's output does NOT match the expected! Please try again"); } // TODO: record score timer.stop(); } waitPopup.dispose(); disableStopButton(); blurOff(); } catch (InterruptedException ex) { outputPanel.compilerMessage("Something went wrong!"); ex.printStackTrace(); // recover waitPopup.dispose(); blurOff(); } } /** * Enable stop button. * Only used in "compile state" */ public void enableStopButton(){ initButtons(activeButtons.get(GameState.COMPILING)); } /** * Disable stop button. * Fired when returning from "compile state". */ public void disableStopButton(){ initButtons(activeButtons.get(GameState.START_GAME)); } /** * The compile button is turned on while compiling only. * Use this as a fail safe to stop compiling (avoids stack overflow etc). */ public void stopCompile(){ // Text text = new Text("Stop button needs to be hooked up -- enable WHILE COMPILING ONLY"); // System.out.println("Stop button needs to be hooked up -- enable WHILE COMPILING ONLY"); // outputPanel.writeNodes(text); //Last, send the user back to the full LHS pseudo state and restart the timer. initButtons(activeButtons.get(GameState.FULL_LHS)); timer.start(); } //-------------------------------------------------------------------------- //Static message printing private void printWelcomeMessage(){ Text welcomeText = new Text("Welcome to Tokanagrammar, Java Edition! "); welcomeText.setFont(defaultFont); outputPanel.writeNodes(welcomeText); Text categoryText = new Text("Please select a category "); categoryText.setFont(defaultFont); Image img = new Image(OutputPanel.class. getResourceAsStream("/images/ui/categoryButton_console_display_size.fw.png")); ImageView imgView = new ImageView(img); Text text = new Text(" to continue."); text.setFont(defaultFont); outputPanel.writeNodes(categoryText, imgView, text); } private void printCategoryAndDifficultyMessage(){ /* * Message to user "Category <categories> has been selected on difficulty <difficulty> * Hints: <hints> */ StringBuilder concatCategories = new StringBuilder(); for(int i=0; i< curCategories.size(); i++) concatCategories.append(' ').append(curCategories.get(i)).append(','); Label categoryText; // chop off the last comma int len = concatCategories.length(); if (len != 0 && concatCategories.charAt(len - 1) == ',') categoryText = new Label(concatCategories.substring(0, len -1)); else categoryText = new Label(concatCategories.toString()); outputPanel.infoMessage((categories.size() > 1 ? "Categories: " : "Category") +concatCategories); StringBuilder diffBd = new StringBuilder("Difficulty: "); diffBd.append(curDifficulty); if(curDifficulty >= 0 && curDifficulty <= 32) diffBd.append("(EASY)"); else if(curDifficulty >= 33 && curDifficulty <= 64) diffBd.append("(MEDIUM)"); else if(curDifficulty >= 65 && curDifficulty <= 90) diffBd.append("(HARD)"); else if(curDifficulty >= 91 && curDifficulty <= 100) diffBd.append("(INSANE)"); outputPanel.infoMessage(diffBd.toString()); StringBuilder hintsBd = new StringBuilder("Hint: "); List<Hint> hints = curPuzzle.getHints(); hintsBd.append(hints.isEmpty() ? "<NO hints available!>" :curPuzzle.getHints().get(curHint).getHintContent()); outputPanel.infoMessage(hintsBd.toString()); } //-------------------------------------------------------------------------- //Getters / Setters /** * GameState is set by Controller or logic classes. * @return the current game state */ public GameState getCurGameState(){ return curGameState; } /** * Get the current difficulty */ public int getCurDifficulty(){ return curDifficulty; } /** * Get the current categories being played */ public List<Category> getCurCategories(){ return curCategories; } /** * Get the OutputPanel */ public OutputPanel getOutputPanel(){ return outputPanel; } /** * Get the Timer */ public GUITimer getTimer(){ return timer; } /** * Get the LegalDragZone */ public GameBoard getLegalDragZone(){ return gameBoard; } /** * Get the current RHS tokens -- tokenBay tokens */ public List<RHSIconizedToken> getRHSIconizedTokens(){ return GameBoard.getInstance().getTokenBayItokens(); } public BracingStyle getCurBracingStyle() { return curBracingStyle; } public void setCurBracingStyle(BracingStyle style) { curBracingStyle = style; } /** * Set the current difficulty */ public void setCurDifficulty(int curDifficulty){ this.curDifficulty = curDifficulty; } /** * Set the current categories being played. */ public void setCurCategories(List<Category> categories){ this.curCategories = categories; puzzles = new HashSet<Puzzle>(); for (Category c : categories) puzzles.addAll(c.getPuzzles()); puzzlesIter = puzzles.iterator(); } /** * Set the AVAILABLE categories */ public void setAvailableCategories(List<Category> categories){ this.categories = categories; } public List<Category> getAvailableCategories() { if (categories == null) categories = APIs.getCategories(); return categories; } /** * Sets the tokenBay tokens * Used by external API */ public void setTokenBayTokens(LinkedList<SourceToken> tokens){ this.tokenBayTokens = tokens; } /** * Sets the tokenBoard tokens * Used by external API */ public void setTokenBoardTokens(LinkedList<SourceToken> tokens){ this.tokenBoardTokens = tokens; } //-------------------------------------------------------------------------- //PRIVATE HELPERS /** * Activate only the buttons the user is allowed to click. * Get the buttons from the controller * @param buttons */ private void initButtons(ArrayList<String> buttonNames){ LinkedList<Button> buttons = Controller.getButtons(); for(Button button: buttons){ String buttonID = button.getId(); for(String str: buttonNames) if(buttonID.equals(str)){ button.setDisable(false); break; }else button.setDisable(true); } } /** * Uses the global activeButtons to map the game's state * to a particular list of active buttons. * * This is only run once per game. * * Buttons are runButton, stopButton, pauseButton, skipButton, * categoryButton, difficultyButton, resetBoardButton, * logoButton */ private static void setupActiveButtonsTable(){ activeButtons = new HashMap<GameState, ArrayList<String>>(); //initialization state ("initGUI") ArrayList<String> initGuiBtns = new ArrayList<String>(); initGuiBtns.add("categoryButton"); initGuiBtns.add("logoButton"); initGuiBtns.add("difficultyButton"); activeButtons.put(GameState.INIT_GUI, initGuiBtns); //starting the game state ("startGame") ArrayList<String> startGameBtns = new ArrayList<String>(); startGameBtns.add("runButton"); startGameBtns.add("pauseButton"); startGameBtns.add("skipButton"); startGameBtns.add("categoryButton"); startGameBtns.add("difficultyButton"); startGameBtns.add("resetBoardButton"); startGameBtns.add("logoButton"); activeButtons.put(GameState.START_GAME, startGameBtns); //Pseudo game state "full left hand side" (all tokens placed on LHS) ArrayList<String> fullLHSbtns = new ArrayList<String>(); fullLHSbtns.add("runButton"); fullLHSbtns.add("pauseButton"); fullLHSbtns.add("skipButton"); fullLHSbtns.add("categoryButton"); fullLHSbtns.add("difficultyButton"); fullLHSbtns.add("resetBoardButton"); fullLHSbtns.add("logoButton"); activeButtons.put(GameState.FULL_LHS, fullLHSbtns); //Pseudo game state "compiling" ArrayList<String> compilingBtns = new ArrayList<String>(); compilingBtns.add("stopButton"); activeButtons.put(GameState.COMPILING, compilingBtns); } private static class WaitWindow extends JFrame { WaitWindow() { super("Compiling!"); setSize(200, 100); // TODO: replace the JLabel with an animated pic here JPanel pn = new JPanel(); pn.add(hourGlass); this.add(pn); this.setLocationRelativeTo(null); this.setAlwaysOnTop(true); this.setVisible(false); // do not let the user close the window // this will be disposed once the compilation is done this.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); this.setResizable(false); } } private static class CompileTask implements Runnable { Output out; String src; CompileTask(String src) { this.src = src; } @Override public void run() { out = edu.umb.cs.api.APIs.compile(src, currentSource.getOrinalSource().getClassName()); synchronized(COMPILE_LOCK) { COMPILE_LOCK.notifyAll(); } } } private static final JLabel hourGlass = getHourGlass(); private static final Object COMPILE_LOCK = 0; private static final CompileTask compileTask = new CompileTask(null); private static final String WAIT_ICON_PATH = "/images/ui/hourglass.gif"; private static JLabel getHourGlass() { try { return new JLabel(new ImageIcon(GUI.class.getResource(WAIT_ICON_PATH))); } catch (Exception ex) { System.out.println("Image not found!"); ex.printStackTrace(); return new JLabel("PLEASE WAIT"); // use text instead } } }