import java.awt.event.*; import java.io.*; import java.nio.file.*; import java.util.stream.Stream; import java.util.Arrays; import javax.swing.*; import javax.swing.event.*; public class Main { // HTML filenames to be shown in the Help menu public static final String HELP_FILE = "help.html"; public static final String ABOUT_FILE = "about.html"; // Valid file extensions for skins to be listed in the menu private static final String[] VALID_SKINS_EXTENSIONS = {".bmp", ".png", ".webp"}; // Game frame private static final JFrame FRAME = new JFrame("Minesweeper"); // The current game Canvas private static Canvas canvas; // Entry point: simply create the frame, set the canvas, and show the frame public static void main(String[] args) { FRAME.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); FRAME.setResizable(false); FRAME.setJMenuBar(getMenuBar()); setCanvas(); FRAME.setVisible(true); } // Create the menu bar of the frame private static JMenuBar getMenuBar() { // Button group for the difficulty buttons ButtonGroup difficultyButtons = new ButtonGroup(); Difficulty difficulty = Options.getDifficulty(); JRadioButtonMenuItem beginnerItem = new JRadioButtonMenuItem("Beginner", difficulty == Difficulty.BEGINNER); JRadioButtonMenuItem intermediateItem = new JRadioButtonMenuItem("Intermediate", difficulty == Difficulty.INTERMEDIATE); JRadioButtonMenuItem expertItem = new JRadioButtonMenuItem("Expert", difficulty == Difficulty.EXPERT); JRadioButtonMenuItem customItem = new JRadioButtonMenuItem("Custom..."); difficultyButtons.add(beginnerItem); difficultyButtons.add(intermediateItem); difficultyButtons.add(expertItem); difficultyButtons.add(customItem); // ActionListener for every button except the skins ones ActionListener listener = new ActionListener() { // Keep track of the previously selected difficulty button in case a // Custom press is canceled so we can revert the button selection private ButtonModel lastDifficultyButton = difficultyButtons.getSelection(); // Parse the action command and perform the corresponding action @Override public void actionPerformed(ActionEvent e) { switch (e.getActionCommand()) { case "new": canvas.restart(); break; case "beginner": setDifficulty(Difficulty.BEGINNER); lastDifficultyButton = beginnerItem.getModel(); break; case "intermediate": setDifficulty(Difficulty.INTERMEDIATE); lastDifficultyButton = intermediateItem.getModel(); break; case "expert": setDifficulty(Difficulty.EXPERT); lastDifficultyButton = expertItem.getModel(); break; case "custom": // If canceled, set the currently selected button to // whatever it was before if (Difficulty.setCustom(FRAME)) { setDifficulty(Difficulty.CUSTOM); lastDifficultyButton = customItem.getModel(); } else { lastDifficultyButton.setSelected(true); } break; case "protectedstart": Options.toggleProtectedStart(); break; case "sound": Options.toggleSound(); Sound.initSounds(); break; case "exit": FRAME.dispatchEvent(new WindowEvent(FRAME, WindowEvent.WINDOW_CLOSING)); break; case "help": messageHtmlFile(HELP_FILE, "Failed to read help file, just google it lol.", "Minesweeper Help", JOptionPane.QUESTION_MESSAGE); break; case "about": messageHtmlFile(ABOUT_FILE, "I love microsoft!!", "Minesweeper About", JOptionPane.INFORMATION_MESSAGE); break; } } }; JMenuBar menuBar = new JMenuBar(); // Create the Game item in the menu bar JMenu gameMenu = new JMenu("Game"); gameMenu.setMnemonic(KeyEvent.VK_G); JMenuItem newGameItem = new JMenuItem("New game"); newGameItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F2, 0)); addMenuItem(newGameItem, gameMenu, KeyEvent.VK_N, "new", listener); gameMenu.addSeparator(); addMenuItem(beginnerItem, gameMenu, KeyEvent.VK_B, "beginner", listener); addMenuItem(intermediateItem, gameMenu, KeyEvent.VK_I, "intermediate", listener); addMenuItem(expertItem, gameMenu, KeyEvent.VK_E, "expert", listener); addMenuItem(customItem, gameMenu, KeyEvent.VK_C, "custom", listener); gameMenu.addSeparator(); JCheckBoxMenuItem protectedStartItem = new JCheckBoxMenuItem("Protected Start", Options.isProtectedStart()); protectedStartItem.setToolTipText("Guarantees the starting tile has no mines next to it"); addMenuItem(protectedStartItem, gameMenu, KeyEvent.VK_P, "protectedstart", listener); JCheckBoxMenuItem soundItem = new JCheckBoxMenuItem("Sound", Options.hasSound()); addMenuItem(soundItem, gameMenu, KeyEvent.VK_U, "sound", listener); gameMenu.addSeparator(); addMenuItem("Exit", gameMenu, KeyEvent.VK_X, "exit", listener); menuBar.add(gameMenu); // Create the Help item in the menu bar JMenu helpMenu = new JMenu("Help"); helpMenu.setMnemonic(KeyEvent.VK_H); JMenuItem helpItem = new JMenuItem("Help"); helpItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F1, 0)); addMenuItem(helpItem, helpMenu, KeyEvent.VK_H, "help", listener); helpMenu.addSeparator(); addMenuItem("About", helpMenu, KeyEvent.VK_A, "about", listener); menuBar.add(helpMenu); // Create the Skins item in the menu bar (this one is special) JMenu skinsMenu = new JMenu("Skins"); skinsMenu.setMnemonic(KeyEvent.VK_S); // The Skins menu should dynamically generate a menu of all image files // in the Skins directory when clicked in order to select one skinsMenu.addMenuListener(new MenuListener() { // Skins have a separate ActionListener from the other buttons so we // can simply set the action command to the name of the new skin // without worrying about collisions or messiness private static ActionListener skinListener = new ActionListener() { @Override public void actionPerformed(ActionEvent e) { // Set the skin to the new one if it isn't already that String newSkin = e.getActionCommand(); if (newSkin.equals(Options.getSkinName())) return; Options.setSkinName(newSkin); canvas.newSkin(); } }; @Override public void menuSelected(MenuEvent e) { // Get a stream of all files in the skins directory try (Stream dirStream = Files.list(Paths.get(Options.SKINS_DIR))) { dirStream // Filter for only regular files .filter(Files::isRegularFile) // Cut off the directory name from the resulting string .map(Path::getFileName) .map(Path::toString) // Only allow a filename if it ends with one of the // valid extensions .filter(name -> Arrays.stream(VALID_SKINS_EXTENSIONS) .anyMatch(ext -> name.toLowerCase().endsWith(ext))) // Because Files.list() does not guarantee an order, // and alphabetically sorted files look objectively // nicer in directory listings .sorted() // Create a radio button menu item for each one, // remembering to have it pre-selected if it's already // the current skin .forEach(name -> addMenuItem(new JRadioButtonMenuItem(name, name.equals(Options.getSkinName())), skinsMenu, name, skinListener)); } catch (IOException ignore) { // It's fine to leave it blank if we can't get results } } @Override public void menuDeselected(MenuEvent e) { // Remove skins when we're done so they don't duplicate every // time we open the menu skinsMenu.removeAll(); } @Override public void menuCanceled(MenuEvent e) { } }); menuBar.add(skinsMenu); return menuBar; } // Helper items for adding menu items without needing a million copy-pasted // lines private static void addMenuItem(String menuTitle, JMenu menu, int mnemonic, String actionCommand, ActionListener listener) { addMenuItem(new JMenuItem(menuTitle), menu, mnemonic, actionCommand, listener); } private static void addMenuItem(JMenuItem menuItem, JMenu menu, int mnemonic, String actionCommand, ActionListener listener) { menuItem.setMnemonic(mnemonic); addMenuItem(menuItem, menu, actionCommand, listener); } private static void addMenuItem(JMenuItem menuItem, JMenu menu, String actionCommand, ActionListener listener) { menuItem.setActionCommand(actionCommand); menuItem.addActionListener(listener); menu.add(menuItem); } // Read the given HTML file and message it with JOptionPane, using failText // instead in case of failure private static void messageHtmlFile(String file, String failText, String title, int messageType) { String text; try { // For some reason, JLabels just stop parsing HTML after they hit a // newline, so swap the newlines out with spaces text = Files.readString(Paths.get(file)).replace('\n', ' '); } catch (IOException e) { text = failText; } JOptionPane.showMessageDialog(FRAME, text, title, messageType); } // Maybe change the difficulty to the new difficulty private static void setDifficulty(Difficulty newDifficulty) { // Reject switching difficulty to the already-existing one unless // Custom, because that will still be different if (Options.getDifficulty() == newDifficulty && newDifficulty != Difficulty.CUSTOM) return; Options.setDifficulty(newDifficulty); // Replace the canvas with a new one, which will get the new difficulty canvas.stop(); canvas.removeAll(); FRAME.remove(canvas); setCanvas(); canvas.requestFocusInWindow(); } // Create a new canvas and put it in the frame private static void setCanvas() { canvas = new Canvas(); FRAME.add(canvas); FRAME.pack(); } // No construction >:( private Main() { } }