2023-05-21 15:13:45 -07:00
|
|
|
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 {
|
2023-05-25 19:44:14 -07:00
|
|
|
// HTML filenames to be shown in the Help menu
|
2023-05-21 15:13:45 -07:00
|
|
|
public static final String HELP_FILE = "help.html";
|
|
|
|
public static final String ABOUT_FILE = "about.html";
|
|
|
|
|
2023-05-25 19:44:14 -07:00
|
|
|
// Valid file extensions for skins to be listed in the menu
|
|
|
|
private static final String[] VALID_SKINS_EXTENSIONS = {".bmp", ".png", ".webp"};
|
2023-05-21 15:13:45 -07:00
|
|
|
|
2023-05-25 19:44:14 -07:00
|
|
|
// Game frame
|
2023-05-21 15:13:45 -07:00
|
|
|
private static final JFrame FRAME = new JFrame("Minesweeper");
|
|
|
|
|
2023-05-25 19:44:14 -07:00
|
|
|
// The current game Canvas
|
2023-05-21 15:13:45 -07:00
|
|
|
private static Canvas canvas;
|
|
|
|
|
2023-05-25 19:44:14 -07:00
|
|
|
// Entry point: simply create the frame, set the canvas, and show the frame
|
2023-05-21 15:13:45 -07:00
|
|
|
public static void main(String[] args) {
|
|
|
|
FRAME.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
|
|
|
FRAME.setResizable(false);
|
|
|
|
FRAME.setJMenuBar(getMenuBar());
|
2023-05-24 17:34:20 -07:00
|
|
|
setCanvas();
|
2023-05-21 15:13:45 -07:00
|
|
|
|
2023-05-24 17:34:20 -07:00
|
|
|
FRAME.setVisible(true);
|
2023-05-21 15:13:45 -07:00
|
|
|
}
|
|
|
|
|
2023-05-25 19:44:14 -07:00
|
|
|
// Create the menu bar of the frame
|
2023-05-21 15:13:45 -07:00
|
|
|
private static JMenuBar getMenuBar() {
|
2023-05-24 17:34:20 -07:00
|
|
|
// 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);
|
|
|
|
|
2023-05-25 19:44:14 -07:00
|
|
|
// ActionListener for every button except the skins ones
|
2023-05-21 15:13:45 -07:00
|
|
|
ActionListener listener = new ActionListener() {
|
2023-05-24 17:34:20 -07:00
|
|
|
// Keep track of the previously selected difficulty button in case a
|
2023-05-25 19:44:14 -07:00
|
|
|
// Custom press is canceled so we can revert the button selection
|
2023-05-24 17:34:20 -07:00
|
|
|
private ButtonModel lastDifficultyButton = difficultyButtons.getSelection();
|
|
|
|
|
2023-05-25 19:44:14 -07:00
|
|
|
// Parse the action command and perform the corresponding action
|
2023-05-21 15:13:45 -07:00
|
|
|
@Override
|
|
|
|
public void actionPerformed(ActionEvent e) {
|
|
|
|
switch (e.getActionCommand()) {
|
|
|
|
case "new":
|
|
|
|
canvas.restart();
|
|
|
|
break;
|
|
|
|
case "beginner":
|
|
|
|
setDifficulty(Difficulty.BEGINNER);
|
2023-05-24 17:34:20 -07:00
|
|
|
lastDifficultyButton = beginnerItem.getModel();
|
2023-05-21 15:13:45 -07:00
|
|
|
break;
|
|
|
|
case "intermediate":
|
|
|
|
setDifficulty(Difficulty.INTERMEDIATE);
|
2023-05-24 17:34:20 -07:00
|
|
|
lastDifficultyButton = intermediateItem.getModel();
|
2023-05-21 15:13:45 -07:00
|
|
|
break;
|
|
|
|
case "expert":
|
|
|
|
setDifficulty(Difficulty.EXPERT);
|
2023-05-24 17:34:20 -07:00
|
|
|
lastDifficultyButton = expertItem.getModel();
|
2023-05-21 15:13:45 -07:00
|
|
|
break;
|
|
|
|
case "custom":
|
2023-05-25 19:44:14 -07:00
|
|
|
// If canceled, set the currently selected button to
|
|
|
|
// whatever it was before
|
2023-05-24 17:34:20 -07:00
|
|
|
if (Difficulty.setCustom(FRAME)) {
|
|
|
|
setDifficulty(Difficulty.CUSTOM);
|
|
|
|
lastDifficultyButton = customItem.getModel();
|
|
|
|
} else {
|
|
|
|
lastDifficultyButton.setSelected(true);
|
|
|
|
}
|
2023-05-21 15:13:45 -07:00
|
|
|
break;
|
|
|
|
case "protectedstart":
|
2023-05-24 17:34:20 -07:00
|
|
|
Options.toggleProtectedStart();
|
2023-05-21 15:13:45 -07:00
|
|
|
break;
|
|
|
|
case "sound":
|
2023-05-24 17:34:20 -07:00
|
|
|
Options.toggleSound();
|
2023-05-21 15:13:45 -07:00
|
|
|
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();
|
|
|
|
|
2023-05-25 19:44:14 -07:00
|
|
|
// Create the Game item in the menu bar
|
2023-05-21 15:13:45 -07:00
|
|
|
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();
|
2023-05-24 17:34:20 -07:00
|
|
|
JCheckBoxMenuItem protectedStartItem = new JCheckBoxMenuItem("Protected Start",
|
|
|
|
Options.isProtectedStart());
|
2023-05-21 15:13:45 -07:00
|
|
|
protectedStartItem.setToolTipText("Guarantees the starting tile has no mines next to it");
|
|
|
|
addMenuItem(protectedStartItem, gameMenu, KeyEvent.VK_P, "protectedstart", listener);
|
2023-05-24 17:34:20 -07:00
|
|
|
JCheckBoxMenuItem soundItem = new JCheckBoxMenuItem("Sound", Options.hasSound());
|
|
|
|
addMenuItem(soundItem, gameMenu, KeyEvent.VK_U, "sound", listener);
|
2023-05-21 15:13:45 -07:00
|
|
|
gameMenu.addSeparator();
|
|
|
|
addMenuItem("Exit", gameMenu, KeyEvent.VK_X, "exit", listener);
|
|
|
|
menuBar.add(gameMenu);
|
|
|
|
|
2023-05-25 19:44:14 -07:00
|
|
|
// Create the Help item in the menu bar
|
2023-05-21 15:13:45 -07:00
|
|
|
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);
|
|
|
|
|
2023-05-25 19:44:14 -07:00
|
|
|
// Create the Skins item in the menu bar (this one is special)
|
2023-05-21 15:13:45 -07:00
|
|
|
JMenu skinsMenu = new JMenu("Skins");
|
|
|
|
skinsMenu.setMnemonic(KeyEvent.VK_S);
|
|
|
|
// The Skins menu should dynamically generate a menu of all image files
|
2023-05-25 19:44:14 -07:00
|
|
|
// in the Skins directory when clicked in order to select one
|
2023-05-21 15:13:45 -07:00
|
|
|
skinsMenu.addMenuListener(new MenuListener() {
|
2023-05-25 19:44:14 -07:00
|
|
|
// 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
|
2023-05-21 15:13:45 -07:00
|
|
|
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();
|
2023-05-25 19:44:14 -07:00
|
|
|
if (newSkin.equals(Options.getSkinName()))
|
2023-05-21 15:13:45 -07:00
|
|
|
return;
|
|
|
|
|
2023-05-25 19:44:14 -07:00
|
|
|
Options.setSkinName(newSkin);
|
|
|
|
canvas.newSkin();
|
2023-05-21 15:13:45 -07:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void menuSelected(MenuEvent e) {
|
|
|
|
// Get a stream of all files in the skins directory
|
2023-05-25 19:44:14 -07:00
|
|
|
try (Stream<Path> dirStream = Files.list(Paths.get(Options.SKINS_DIR))) {
|
2023-05-21 15:13:45 -07:00
|
|
|
dirStream
|
2023-05-24 17:34:20 -07:00
|
|
|
// 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
|
2023-05-25 19:44:14 -07:00
|
|
|
.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
|
2023-05-24 17:34:20 -07:00
|
|
|
.sorted()
|
|
|
|
// Create a radio button menu item for each one,
|
|
|
|
// remembering to have it pre-selected if it's already
|
|
|
|
// the current skin
|
2023-05-25 19:44:14 -07:00
|
|
|
.forEach(name -> addMenuItem(new JRadioButtonMenuItem(name,
|
|
|
|
name.equals(Options.getSkinName())), skinsMenu, name, skinListener));
|
2023-05-21 15:13:45 -07:00
|
|
|
} 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;
|
|
|
|
}
|
|
|
|
|
2023-05-25 19:44:14 -07:00
|
|
|
// Helper items for adding menu items without needing a million copy-pasted
|
|
|
|
// lines
|
2023-05-21 15:13:45 -07:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2023-05-25 19:44:14 -07:00
|
|
|
// 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
|
2023-05-21 15:13:45 -07:00
|
|
|
canvas.stop();
|
|
|
|
canvas.removeAll();
|
|
|
|
FRAME.remove(canvas);
|
|
|
|
setCanvas();
|
2023-05-24 17:34:20 -07:00
|
|
|
canvas.requestFocusInWindow();
|
2023-05-25 19:44:14 -07:00
|
|
|
}
|
2023-05-21 15:13:45 -07:00
|
|
|
|
2023-05-25 19:44:14 -07:00
|
|
|
// Create a new canvas and put it in the frame
|
2023-05-21 15:13:45 -07:00
|
|
|
private static void setCanvas() {
|
2023-05-25 19:44:14 -07:00
|
|
|
canvas = new Canvas();
|
2023-05-21 15:13:45 -07:00
|
|
|
FRAME.add(canvas);
|
|
|
|
FRAME.pack();
|
|
|
|
}
|
|
|
|
|
|
|
|
// No construction >:(
|
|
|
|
private Main() {
|
|
|
|
}
|
|
|
|
}
|