Add all comments

This commit is contained in:
eriedaberrie 2023-05-25 19:44:14 -07:00
parent c9d70d737d
commit 11e5379d23
8 changed files with 375 additions and 154 deletions

View file

@ -5,61 +5,99 @@ import java.io.*;
import javax.imageio.*;
import javax.swing.*;
public class Canvas extends JPanel {
// Canvas class, which draws the board and also acts as a central location of
// game state that tiles and the face can refer back to
public class Canvas extends JComponent {
// Width of the tileset
public static final int TS_WIDTH = 144;
// Height of the tileset
public static final int TS_HEIGHT = 122;
// Y-position of the digits in the tileset
public static final int TS_DIGITS_Y = 33;
// Y-position of the faces in the tilest
public static final int TS_FACES_Y = 55;
// Y-position of the miscellaneous stuff in the tileset
public static final int TS_MISC_Y = 82;
// X-position of the digit backdrop in the tileset
public static final int TS_DIGIT_BACKDROP_X = 28;
// X-position of the color pixel in the tileset
public static final int TS_COLOR_X = 70;
// Width of the borders
public static final int BORDER_WIDTH = 12;
// Height of most of the borders
public static final int BORDER_HEIGHT = 11;
// Height of the bottom border
public static final int BOTTOM_HEIGHT = 12;
// Height of the box at the top, without the borders
public static final int TOP_BOX_HEIGHT = 33;
// The distance of the top items from the top of the screen
public static final int TOP_PADDING = 15;
// The distance of the flags display from the left of the screen
public static final int FLAGS_PADDING = 16;
// The distance of the left side of the timer display from the right of the
// screen
public static final int TIMER_PADDING = 59;
// The important game attributes as final ints
public final int ROWS;
public final int COLS;
public final int MINES;
public final int WIDTH;
public final int HEIGHT;
// JPanel containing all the tiles
private final JPanel BOARD_PANEL;
// Number displays for the flags and the time
private final NumberDisplay FLAGS_DISPLAY;
private final NumberDisplay TIMER_DISPLAY;
// The face
private final Face FACE;
// A KeyListener listening for shift to help chord
private final KeyListener SHIFT_KEY_LISTENER;
// Timer to tick up the time every second
private final Timer TIMER;
// The virtual representation of the board if we want to actually reference
// the tiles instead of casting BOARD_PANEL.getComponents() becuase that's
// weird
private final Tile[][] BOARD;
private final Image BACKGROUND_IMAGE;
// The image to draw in the background
private Image backgroundImage;
// The tile that the mouse is currently over
private Tile currentTile;
// Whether the game has started yet (before the first click, no mines exist
// and the timer doesn't start)
private boolean gameStarted;
// Whether the game has ended
private boolean gameEnded;
// Whether the game was lost
private boolean gameLost;
// The number of tiles left to reveal, to know when to win
private int numTilesLeft;
// The number of flags left to place, for the digit display
private int numFlagsLeft;
// The time, also for the digit display
private int time;
// Input state; not used direclty, but getters for these exist in Tile, and
// this is where we can store the data for the entire game
private boolean holdingShift;
private boolean leftMouseDown;
private boolean rightMouseDown;
private int numTilesLeft;
private int numFlagsLeft;
private int time;
// Set initial values of all the variables
private void init() {
currentTile = null;
gameStarted = false;
gameEnded = false;
gameLost = false;
holdingShift = false;
leftMouseDown = false;
rightMouseDown = false;
numTilesLeft = ROWS * COLS - MINES;
numFlagsLeft = MINES;
time = 0;
holdingShift = false;
leftMouseDown = false;
rightMouseDown = false;
addKeyListener(SHIFT_KEY_LISTENER);
@ -67,17 +105,13 @@ public class Canvas extends JPanel {
TIMER_DISPLAY.setNum(time);
}
public Canvas(File skin) {
Difficulty difficulty = Options.getDifficulty();
ROWS = difficulty.getRows();
COLS = difficulty.getCols();
MINES = difficulty.getMines();
WIDTH = Tile.SIZE * COLS + 2 * BORDER_WIDTH;
HEIGHT = Tile.SIZE * ROWS + 2 * BORDER_HEIGHT + BOTTOM_HEIGHT + TOP_BOX_HEIGHT;
// Get the tileset from Options and use it to set the images of all the
// components inside the canvas and the background image
private void setImages() {
// Read the tileset image
BufferedImage tileset;
try {
tileset = ImageIO.read(skin);
tileset = ImageIO.read(new File(Options.SKINS_DIR, Options.getSkinName()));
// Reject images that are too small to properly form the tileset
if (tileset.getWidth() < TS_WIDTH || tileset.getHeight() < TS_HEIGHT)
throw new IOException();
@ -86,20 +120,47 @@ public class Canvas extends JPanel {
tileset = new BufferedImage(TS_WIDTH, TS_HEIGHT, BufferedImage.TYPE_INT_RGB);
}
BACKGROUND_IMAGE = getBackgroundImage(tileset);
// Set the images of the tiles
Image[] numberTiles = new Image[9];
Image[] specialTiles = new Image[Tile.EXPLODED_MINE_INDEX + 1];
for (int i = 0; i < 9; i++)
numberTiles[i] = getTileFromSet(tileset, i, 0);
for (int i = 0; i <= Tile.EXPLODED_MINE_INDEX; i++)
specialTiles[i] = getTileFromSet(tileset, i, Tile.SIZE);
for (Tile[] row : BOARD)
for (Tile tile : row)
tile.setImages(numberTiles, specialTiles);
// Set the images of the the number displays
Image[] digits = getImageSet(tileset, NumberDisplay.MINUS_INDEX + 1, TS_DIGITS_Y,
NumberDisplay.DIGIT_WIDTH, NumberDisplay.DIGIT_HEIGHT);
Image numberDisplayBackdrop = tileset.getSubimage(TS_DIGIT_BACKDROP_X, TS_MISC_Y,
NumberDisplay.BACKDROP_WIDTH, NumberDisplay.BACKDROP_HEIGHT);
FLAGS_DISPLAY.setImages(digits, numberDisplayBackdrop);
TIMER_DISPLAY.setImages(digits, numberDisplayBackdrop);
// Set the images of the face
FACE.setImages(getImageSet(tileset, Face.PRESSED_INDEX + 1, TS_FACES_Y, Face.SIZE, Face.SIZE));
// Set the background image
backgroundImage = getBackgroundImage(tileset);
}
public Canvas() {
// Set game attributes based on the difficulty
Difficulty difficulty = Options.getDifficulty();
ROWS = difficulty.getRows();
COLS = difficulty.getCols();
MINES = difficulty.getMines();
WIDTH = Tile.SIZE * COLS + 2 * BORDER_WIDTH;
HEIGHT = Tile.SIZE * ROWS + 2 * BORDER_HEIGHT + BOTTOM_HEIGHT + TOP_BOX_HEIGHT;
// Initialize the tiles
BOARD = new Tile[ROWS][COLS];
BOARD_PANEL = new JPanel(new GridLayout(ROWS, COLS));
for (int r = 0; r < ROWS; r++) {
for (int c = 0; c < COLS; c++) {
BOARD[r][c] = new Tile(this, numberTiles, specialTiles);
BOARD[r][c] = new Tile(this);
BOARD_PANEL.add(BOARD[r][c]);
}
}
@ -119,37 +180,36 @@ public class Canvas extends JPanel {
}
}
Image[] digits = getImageSet(tileset, NumberDisplay.MINUS_INDEX, TS_DIGITS_Y,
NumberDisplay.DIGIT_WIDTH, NumberDisplay.DIGIT_HEIGHT);
Image numberDisplayBackdrop = tileset.getSubimage(TS_DIGIT_BACKDROP_X, TS_MISC_Y,
NumberDisplay.BACKDROP_WIDTH, NumberDisplay.BACKDROP_HEIGHT);
// Initialize the other components
FLAGS_DISPLAY = new NumberDisplay();
TIMER_DISPLAY = new NumberDisplay();
FACE = new Face(this);
FLAGS_DISPLAY = new NumberDisplay(numberDisplayBackdrop, digits);
TIMER_DISPLAY = new NumberDisplay(numberDisplayBackdrop, digits);
FACE = new Face(this, getImageSet(tileset, Face.PRESSED_INDEX, TS_FACES_Y, Face.SIZE, Face.SIZE));
// Now that the components are all initialized, set all of their images
setImages();
// Shift+LMB chords, so we need some way of determining if the shift key
// is being held.
SHIFT_KEY_LISTENER = new KeyAdapter() {
// Shift+LMB chords, so we need some way of determining if the shift
// key is being held.
@Override
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_SHIFT) {
holdingShift = true;
updateCurrentTile();
}
updateShift(e, true);
}
@Override
public void keyReleased(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_SHIFT) {
holdingShift = false;
updateCurrentTile();
}
updateShift(e, false);
}
private void updateCurrentTile() {
if (currentTile != null)
currentTile.updatePressedState();
// Helper method to update holdingShift to the new value if shift
// was pressed, and additionally update the pressed state of the
// current tile if that exists
private void updateShift(KeyEvent e, boolean newValue) {
if (e.getKeyCode() == KeyEvent.VK_SHIFT) {
holdingShift = newValue;
if (currentTile != null)
currentTile.updatePressedState();
}
}
};
@ -163,12 +223,13 @@ public class Canvas extends JPanel {
return;
}
// Play the tick sound and increment the timer number
Sound.TICK.play();
TIMER_DISPLAY.setNum(++time);
}
});
// Null layout, for manual placement of all game components
// Null layout, for fine-tuned placement of all game components
setLayout(null);
Insets insets = getInsets();
placeComponent(BOARD_PANEL, BORDER_WIDTH, 2 * BORDER_HEIGHT + TOP_BOX_HEIGHT, insets);
@ -182,11 +243,19 @@ public class Canvas extends JPanel {
init();
}
// Refresh the skin and repaint
public void newSkin() {
setImages();
repaint();
}
// Stop everything that might be running
public void stop() {
TIMER.stop();
removeKeyListener(SHIFT_KEY_LISTENER);
}
// Restart the game
public void restart() {
if (!gameStarted)
return;
@ -218,20 +287,22 @@ public class Canvas extends JPanel {
TIMER.start();
}
// After every tile revealed, check for game ending
public void postRevealCheck() {
if (gameLost) {
if (gameLost) { // Loss
Sound.EXPLODE.play();
stopGame();
endGame();
FACE.setFace(Face.DEAD_INDEX);
} else if (numTilesLeft == 0) {
} else if (numTilesLeft == 0) { // Win
Sound.WIN.play();
// Automatically flag all remaining tiles on win
FLAGS_DISPLAY.setNum(0);
stopGame();
endGame();
FACE.setFace(Face.COOL_INDEX);
}
}
// Modify the flag counter
public void modifyFlagCount(boolean addedFlag) {
if (addedFlag)
numFlagsLeft--;
@ -240,18 +311,25 @@ public class Canvas extends JPanel {
FLAGS_DISPLAY.setNum(numFlagsLeft);
}
// Called by tile every single time one is revealed
public void revealedSingleTile() {
numTilesLeft--;
}
// Called by tile if it is a mine when revealed
public void setLoseFlag() {
gameLost = true;
}
// Every tile has a MouseListener, but the canvas must be aware of the mouse
// button state because other tiles that weren't the one that was initially
// clicked also need to know the mouse button state, so Canvas has a setter
// for it
public void setLeftMouseDown(boolean b) {
if (gameEnded)
return;
// Face gets nervous when clicking
leftMouseDown = b;
if (leftMouseDown)
FACE.setFace(Face.SHOCKED_INDEX);
@ -259,18 +337,26 @@ public class Canvas extends JPanel {
FACE.setFace(Face.HAPPY_INDEX);
}
// Ditto
public void setRightMouseDown(boolean b) {
rightMouseDown = b;
if (!gameEnded)
rightMouseDown = b;
}
public boolean isLeftMouseDown() {
return leftMouseDown;
}
// Chording can be done with either the right mouse button or the shift key
public boolean isChording() {
return holdingShift || rightMouseDown;
}
// Tiles need to know what tile the mouse is currently over because the
// MouseReleased event is triggered on the tile that the mouse was pressed
// down on, not the one it was released on, but the tile that it was
// released on is the one that actually gets revealed, which we would
// otherwise have no way of knowing
public Tile getCurrentTile() {
return currentTile;
}
@ -283,14 +369,18 @@ public class Canvas extends JPanel {
return gameLost;
}
private static Image getTileFromSet(BufferedImage tileset, int n, int y) {
// Get the nth tile from the tileset, with the initial y-value of y
private Image getTileFromSet(BufferedImage tileset, int n, int y) {
return tileset.getSubimage(Tile.SIZE * n, y, Tile.SIZE, Tile.SIZE);
}
private Image[] getImageSet(BufferedImage tileset, int lastIndex, int y, int width, int height) {
Image[] img = new Image[lastIndex + 1];
// For the other images, which have a 1-pixel gap between them: return an
// array of images by slicing subimages of the given size from the image,
// with a 1-pixel gap between them
private Image[] getImageSet(BufferedImage tileset, int n, int y, int width, int height) {
Image[] img = new Image[n];
int x = 0;
for (int i = 0; i <= lastIndex; i++) {
for (int i = 0; i < n; i++) {
img[i] = tileset.getSubimage(x, y, width, height);
x += width + 1;
}
@ -367,7 +457,8 @@ public class Canvas extends JPanel {
c.setSize(c.getPreferredSize());
}
private void stopGame() {
// Stop the moving parts after a win or a loss
private void endGame() {
TIMER.stop();
gameEnded = true;
for (Tile[] row : BOARD)
@ -377,8 +468,10 @@ public class Canvas extends JPanel {
// Override the paintComponent method in order to add the background image
@Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
g.drawImage(BACKGROUND_IMAGE, 0, 0, this);
public void paintComponent(Graphics gr) {
super.paintComponent(gr);
Graphics2D g = (Graphics2D) gr;
g.drawImage(backgroundImage, 0, 0, this);
}
}

View file

@ -1,7 +1,9 @@
import java.awt.event.*;
import javax.swing.*;
// Custom text field for use when creating new custom games
public class CustomTextField extends JTextField {
// The number of columns of characters to have
private static final int COLS = 4;
private final int MIN_VALUE;

View file

@ -1,7 +1,7 @@
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
// Enum with the difficuulty options
public enum Difficulty {
BEGINNER (9, 9, 10),
INTERMEDIATE (16, 16, 40),
@ -20,6 +20,8 @@ public enum Difficulty {
// it's possible to hit the number display cap
private static final int MAX_MINES = 999;
// Use nonfinal ints with getter methods because custom needs to be able to
// change its stats
private int rows;
private int cols;
private int mines;
@ -49,6 +51,8 @@ public enum Difficulty {
return mines;
}
// Show a popup window to select attributes of the custom game, return
// whether or not it succeeded
public static boolean setCustom(JFrame frame) {
Difficulty current = Options.getDifficulty();
@ -71,6 +75,7 @@ public enum Difficulty {
}
}
// Initialize the text fields
CustomTextField rowsField = new RowsColsField(current.rows, MIN_SIZE, MAX_ROWS);
CustomTextField colsField = new RowsColsField(current.cols, MIN_SIZE, MAX_COLS);
// Override CustomTextField in order to make it dynamically determine
@ -89,18 +94,21 @@ public enum Difficulty {
};
// Because Java is lame and doesn't have builtin tuples we make do with
// casting an JComponent[][]
// a JComponent[][] to store the structure of the input window
JComponent[][] items = {
{ new JLabel("Height:"), rowsField },
{ new JLabel("Width:"), colsField },
{ new JLabel("Mines:"), minesField },
};
int option = JOptionPane.showConfirmDialog(frame, getMessagePanel(items, 6),
// Show the message window now and get the result
int option = JOptionPane.showConfirmDialog(frame, getMessagePanel(items),
"Custom Board", JOptionPane.OK_CANCEL_OPTION);
// If user didn't hit OK, bail out now
if (option != JOptionPane.OK_OPTION)
return false;
// Bail out if fields can't be parsed into integers
int rows, cols, mines;
try {
rows = Integer.parseInt(rowsField.getText());
@ -110,8 +118,9 @@ public enum Difficulty {
return false;
}
// Clamp values to the allowed range, just in case something slipped
// through the focusLost events
// Even though the text fields should technically only allow valid
// inputs, we should clamp values to the allowed range, just in case
// something slipped through the focusLost events
rows = clampInt(rows, MIN_SIZE, MAX_ROWS);
cols = clampInt(cols, MIN_SIZE, MAX_COLS);
mines = clampInt(mines, MIN_MINES, getMaxMines(rows, cols));
@ -120,24 +129,38 @@ public enum Difficulty {
if (current == CUSTOM && CUSTOM.rows == rows && CUSTOM.cols == cols && CUSTOM.mines == mines)
return false;
// Everything succeeded, so now actually set the new stats
CUSTOM.setStats(rows, cols, mines);
return true;
}
// Helper method to implement something that every language except Java
// already has builtin
public static int clampInt(int value, int minValue, int maxValue) {
return Math.max(Math.min(value, maxValue), minValue);
}
// The regular formula for maximum mines is the product of rows and columns
// minus one, but also cap it at 999
private static int getMaxMines(int rows, int cols) {
return Math.min((rows - 1) * (cols - 1), MAX_MINES);
}
private static JPanel getMessagePanel(JComponent[][] items, int padding) {
// Create a JPanel that has the items in a custom gridlike layout in which
// every row is as tall as the tallest element in that row and every column
// is as wide as the widest element in that column
private static JPanel getMessagePanel(JComponent[][] items) {
// The padding between the elements and from the walls
final int PADDING = 6;
// The number of columns
final int COLS = 2;
// Null layout because the only layout manager that can do what we we
// want is SpringLayout which the same complexity
JPanel messagePanel = new JPanel(null);
// Add the items to the panel, setting the label alignment along the way
for (JComponent[] row : items) {
JLabel label = (JLabel) row[0];
label.setLabelFor(row[1]);
@ -146,32 +169,45 @@ public enum Difficulty {
messagePanel.add(row[1]);
}
int[] widths = new int[COLS];
int[] heights = new int[items.length];
// The maximum width of all the elements of each column
int[] maxWidths = new int[COLS];
// The maximum height of each elements of each row
int[] maxHeights = new int[items.length];
// The x-position of all elements of each column
int[] x = new int[COLS + 1];
// The y-position of all the elements of each row
int[] y = new int[items.length + 1];
// The last value of x and y hold the size of the panel itself
x[0] = padding;
// Set x and maxWidths
x[0] = PADDING;
for (int c = 0; c < COLS; c++) {
// Get the maximum width among items of this row
int maxWidth = Integer.MIN_VALUE;
for (int r = 0; r < items.length; r++)
maxWidth = Math.max(maxWidth, (int) items[r][c].getPreferredSize().getWidth());
widths[c] = maxWidth;
x[c + 1] = x[c] + maxWidth + padding;
maxWidths[c] = maxWidth;
// x-coordinate of the next item is the x-coordinate of this one
// plus this one's width plus padding
x[c + 1] = x[c] + maxWidth + PADDING;
}
// Set y and maxHeights the same way
y[0] = PADDING;
for (int r = 0; r < items.length; r++) {
int maxHeight = Integer.MIN_VALUE;
for (int c = 0; c < COLS; c++)
maxHeight = Math.max(maxHeight, (int) items[r][c].getPreferredSize().getHeight());
heights[r] = maxHeight;
y[r + 1] = y[r] + maxHeight + padding;
maxHeights[r] = maxHeight;
y[r + 1] = y[r] + maxHeight + PADDING;
}
// Actually put into use the values we just got
for (int r = 0; r < items.length; r++)
for (int c = 0; c < COLS; c++)
items[r][c].setBounds(x[c], y[r], widths[c], heights[r]);
items[r][c].setBounds(x[c], y[r], maxWidths[c], maxHeights[r]);
// Use the last values of x and y to set the size of the panel
messagePanel.setPreferredSize(new Dimension(x[COLS], y[items.length]));
return messagePanel;

View file

@ -2,29 +2,42 @@ import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
// The face at the top of the game
public class Face extends JComponent {
// The width and height
public static final int SIZE = 26;
// The indexes of each face in the array
public static final int HAPPY_INDEX = 0;
public static final int SHOCKED_INDEX = 1;
public static final int DEAD_INDEX = 2;
public static final int COOL_INDEX = 3;
public static final int PRESSED_INDEX = 4;
// The main game canvas to refer back to
private final Canvas GAME_CANVAS;
private final Image[] FACES;
private Image[] faces;
// The current face index
private int face;
// The last face index, before being pressed, in order to revert on unpress
private int unpressedFace;
// Whether the face is being pressed right now
private boolean pressed;
// Whether the mouse is over the face
private boolean mouseOver;
public Face(Canvas gameCanvas, Image[] faces) {
FACES = faces;
public Face(Canvas gameCanvas) {
GAME_CANVAS = gameCanvas;
face = HAPPY_INDEX;
pressed = false;
mouseOver = false;
setPreferredSize(new Dimension(SIZE, SIZE));
// We want to show the pressed sprite only if the mouse is down while
// the mouse is over the face, reverting back when it's no longer over
// the face but coming back as soon as the mouse comes back
addMouseListener(new MouseAdapter() {
// The only way to initiate a pressed face state is when the mouse
// is over it and is pressed down
@Override
public void mousePressed(MouseEvent e) {
pressed = true;
@ -32,6 +45,10 @@ public class Face extends JComponent {
setFace(PRESSED_INDEX);
}
// If the face recieves a mouseReleased event while the mouse is
// over it, that means that it was a completed click, so restart the
// game on the canvas, also resetting back to the happy face that
// should be shown at the start of every game
@Override
public void mouseReleased(MouseEvent e) {
pressed = false;
@ -41,6 +58,8 @@ public class Face extends JComponent {
}
}
// If it was previously being pressed, reshow the pressed sprite as
// soon as mouse enters
@Override
public void mouseEntered(MouseEvent e) {
mouseOver = true;
@ -48,6 +67,8 @@ public class Face extends JComponent {
setFace(PRESSED_INDEX);
}
// Temporarily show the unpressed face, but keep the pressed boolean
// true in case the mouse reenters
@Override
public void mouseExited(MouseEvent e) {
mouseOver = false;
@ -57,6 +78,10 @@ public class Face extends JComponent {
});
}
public void setImages(Image[] faces) {
this.faces = faces;
}
public void setFace(int face) {
this.face = face;
repaint();
@ -67,6 +92,6 @@ public class Face extends JComponent {
super.paintComponent(gr);
Graphics2D g = (Graphics2D) gr;
g.drawImage(FACES[face], 0, 0, this);
g.drawImage(faces[face], 0, 0, this);
}
}

106
Main.java
View file

@ -7,21 +7,21 @@ import javax.swing.*;
import javax.swing.event.*;
public class Main {
public static final String DEFAULT_SKIN = "winxpskin.bmp";
public static final String SKINS_DIR = "Skins/";
// 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";
private static final String[] VALID_EXTENSIONS = {".bmp", ".png", ".webp"};
// 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;
private static String skin;
// Entry point: simply create the frame, set the canvas, and show the frame
public static void main(String[] args) {
skin = DEFAULT_SKIN;
FRAME.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
FRAME.setResizable(false);
FRAME.setJMenuBar(getMenuBar());
@ -30,6 +30,7 @@ public class Main {
FRAME.setVisible(true);
}
// Create the menu bar of the frame
private static JMenuBar getMenuBar() {
// Button group for the difficulty buttons
ButtonGroup difficultyButtons = new ButtonGroup();
@ -46,12 +47,13 @@ public class Main {
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 back to whatever it was
// before
// 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()) {
@ -71,6 +73,8 @@ public class Main {
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();
@ -102,6 +106,7 @@ public class Main {
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");
@ -123,6 +128,7 @@ public class Main {
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");
@ -132,28 +138,32 @@ public class Main {
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
// 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(skin))
if (newSkin.equals(Options.getSkinName()))
return;
skin = newSkin;
replaceCanvas();
Options.setSkinName(newSkin);
canvas.newSkin();
}
};
@Override
public void menuSelected(MenuEvent e) {
// Get a stream of all files in the skins directory
try (Stream<Path> dirStream = Files.list(Paths.get(SKINS_DIR))) {
try (Stream<Path> dirStream = Files.list(Paths.get(Options.SKINS_DIR))) {
dirStream
// Filter for only regular files
.filter(Files::isRegularFile)
@ -162,19 +172,17 @@ public class Main {
.map(Path::toString)
// Only allow a filename if it ends with one of the
// valid extensions
.filter(name -> Arrays.stream(VALID_EXTENSIONS)
// Use regionMatches to effectively
// case-insensitively endsWith()
.anyMatch(ext -> name.regionMatches(true,
name.length() - ext.length(), ext, 0, ext.length())))
// Because Files.list does not guarantee an order, and
// alphabetical files look objectively nicer in lists
.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(skin)),
skinsMenu, name, skinListener));
.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
}
@ -196,27 +204,8 @@ public class Main {
return menuBar;
}
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);
}
private static void setDifficulty(Difficulty newDifficulty) {
// Reject switching difficulty to the already-existing one unless Custom
if (Options.getDifficulty() == newDifficulty && newDifficulty != Difficulty.CUSTOM)
return;
Options.setDifficulty(newDifficulty);
replaceCanvas();
}
// 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);
@ -235,17 +224,40 @@ public class Main {
menu.add(menuItem);
}
private static void replaceCanvas() {
// 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(new File(SKINS_DIR, skin));
canvas = new Canvas();
FRAME.add(canvas);
FRAME.pack();
}

View file

@ -1,28 +1,37 @@
import java.awt.*;
import javax.swing.*;
// The number displays in the game
public class NumberDisplay extends JComponent {
// The dimensions of each individual digit
public static final int DIGIT_WIDTH = 11;
public static final int DIGIT_HEIGHT = 21;
// The dimensions of the big box
public static final int BACKDROP_WIDTH = 41;
public static final int BACKDROP_HEIGHT = 25;
// The index of the minus sign
public static final int MINUS_INDEX = 10;
// The x-positions of each digit: units -> tens -> hundreds
private static final int[] DIGIT_X = {28, 15, 2};
// The y-position of each digit
private static final int DIGIT_Y = 2;
public final Image BACKDROP;
public final Image[] DIGITS;
public Image backdrop;
public Image[] digits;
private int num;
private boolean negative;
public NumberDisplay(Image backdrop, Image[] digits) {
BACKDROP = backdrop;
DIGITS = digits;
public NumberDisplay() {
setPreferredSize(new Dimension(BACKDROP_WIDTH, BACKDROP_HEIGHT));
}
public void setImages(Image[] digits, Image backdrop) {
this.digits = digits;
this.backdrop = backdrop;
}
public void setNum(int num) {
setClamped(num);
repaint();
@ -44,17 +53,20 @@ public class NumberDisplay extends JComponent {
super.paintComponent(gr);
Graphics2D g = (Graphics2D) gr;
g.drawImage(BACKDROP, 0, 0, this);
// Preserve the original num for divison, in case we repaint twice
// without updating the number
g.drawImage(backdrop, 0, 0, this);
// Preserve the original num for in-place divison, because we don't want
// non-graphical side effects in paintComponent
int num = this.num;
for (int i = 0; i < 3; i++) {
int digitsIndex;
// Get the index of the proper digit for this position, drawing the
// minus sign in the hundreds digit if necessary
if (negative && i == 2)
digitsIndex = MINUS_INDEX;
else
digitsIndex = num % 10;
g.drawImage(DIGITS[digitsIndex], DIGIT_X[i], DIGIT_Y, this);
g.drawImage(digits[digitsIndex], DIGIT_X[i], DIGIT_Y, this);
// Next digit
num /= 10;
}
}

View file

@ -1,11 +1,16 @@
// Public class that stores all the global static options
public class Options {
// Directory with all the skins files
public static final String SKINS_DIR = "Skins/";
// Whether or not sound is enabled
private static boolean sound = false;
// Whether or not to force starting at 0
private static boolean protectedStart = false;
// The difficulty
private static Difficulty difficulty = Difficulty.INTERMEDIATE;
// The skin
private static String skinName = "winxpskin.bmp";
public static boolean hasSound() {
return sound;
@ -31,6 +36,14 @@ public class Options {
Options.difficulty = difficulty;
}
public static String getSkinName() {
return skinName;
}
public static void setSkinName(String skinName) {
Options.skinName = skinName;
}
// No constructing >:(
private Options() {
}

View file

@ -1,10 +1,13 @@
import java.awt.*;
import java.util.HashSet;
import java.util.ArrayList;
import java.awt.event.*;
import javax.swing.*;
// A single tile on the game board
public class Tile extends JComponent {
// The size of the tile
public static final int SIZE = 16;
// The indexes of each special image in the special tiles array
public static final int REGULAR_INDEX = 0;
public static final int PRESSED_INDEX = 1;
public static final int MINE_INDEX = 2;
@ -12,27 +15,35 @@ public class Tile extends JComponent {
public static final int NOT_MINE_INDEX = 4;
public static final int EXPLODED_MINE_INDEX = 5;
// The main game canvas to refer back to
private final Canvas GAME_CANVAS;
private final Image[] NUMBER_TILES;
private final Image[] SPECIAL_TILES;
// Store the mouse listener for convenient removal once the game ends
private final MouseListener TILE_MOUSE_LISTENER;
// A set of all the Tiles that are adjacent to this one, for calculating
// numbers and autorevealing zeros and chording
private final HashSet<Tile> ADJACENT_TILES;
// A collection of all the Tiles that are adjacent to this one, for
// calculating numbers and autorevealing zeros and chording
private final ArrayList<Tile> ADJACENT_TILES;
// The numbered tiles 0-9
private Image[] numberTiles;
// All the other possible tiles (see *_INDEX above)
private Image[] specialTiles;
// Whether or not the tile is a mine
private boolean mine;
// The number of mines adjacent to the current one
private int num;
// Whether or not this mine has been revealed already
// Tile state
private boolean revealed;
// Whether or not this mine has been flagged
private boolean flagged;
// Whether this tile should show the pressed sprite (the mouse is down and
// this tile is either the currently moused-over tile or is adjacent to it
// while chording)
private boolean pressed;
// Whether the mouse is over the tile
private boolean mouseOver;
// Whether the game has ended
private boolean gameEnded;
private void init() {
@ -46,12 +57,13 @@ public class Tile extends JComponent {
addMouseListener(TILE_MOUSE_LISTENER);
}
public Tile(Canvas gameCanvas, Image[] numberTiles, Image[] specialTiles) {
public Tile(Canvas gameCanvas) {
GAME_CANVAS = gameCanvas;
NUMBER_TILES = numberTiles;
SPECIAL_TILES = specialTiles;
ADJACENT_TILES = new HashSet<Tile>();
ADJACENT_TILES = new ArrayList<Tile>();
// Every tile has its own mouse listener in order for the game to always
// know when the mouse moves over from one tile to the other in order to
// update the pressed state of the tiles
TILE_MOUSE_LISTENER = new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
@ -122,11 +134,17 @@ public class Tile extends JComponent {
init();
}
public void setImages(Image[] numberTiles, Image[] specialTiles) {
this.numberTiles = numberTiles;
this.specialTiles = specialTiles;
}
public void restart() {
removeMouseListener(TILE_MOUSE_LISTENER);
init();
}
// Update ADJACENT_TILES at the start of the game
public void addAdjacentTile(Tile tile) {
ADJACENT_TILES.add(tile);
}
@ -145,6 +163,7 @@ public class Tile extends JComponent {
return true;
}
// Flip gameEnded and repaint one last time
public void gameEnd() {
removeMouseListener(TILE_MOUSE_LISTENER);
gameEnded = true;
@ -153,6 +172,7 @@ public class Tile extends JComponent {
repaint();
}
// Update the pressed state, whenever something pressing-related happens
public void updatePressedState() {
if (pressed != (GAME_CANVAS.isLeftMouseDown() && mouseOver)) {
pressed = !pressed;
@ -167,6 +187,7 @@ public class Tile extends JComponent {
}
}
// Flag on unrevealed tiles on right click
private void rightClickAction() {
if (revealed)
return;
@ -176,11 +197,13 @@ public class Tile extends JComponent {
repaint();
}
// Chord or reveal tiles on left click
private void leftClickAction() {
if (GAME_CANVAS.isChording()) {
if (!revealed)
return;
// Check that chording should happen
int adjacentFlags = 0;
for (Tile tile : ADJACENT_TILES)
if (tile.flagged)
@ -188,12 +211,14 @@ public class Tile extends JComponent {
if (adjacentFlags != num)
return;
// Chord
for (Tile tile : ADJACENT_TILES)
if (!tile.flagged)
tile.reveal();
} else {
if (flagged || revealed)
return;
// Ensure that mines are placed before revealing anything
GAME_CANVAS.tryStartGame(this);
reveal();
@ -201,6 +226,7 @@ public class Tile extends JComponent {
GAME_CANVAS.postRevealCheck();
}
// Reveal the current tile
private void reveal() {
if (revealed)
return;
@ -210,13 +236,15 @@ public class Tile extends JComponent {
if (mine) {
GAME_CANVAS.setLoseFlag();
} else {
GAME_CANVAS.revealedSingleTile();
if (num == 0)
for (Tile tile : ADJACENT_TILES)
if (!tile.flagged)
tile.reveal();
return;
}
GAME_CANVAS.revealedSingleTile();
// Recursively reveal all adjacent tiles if this one is 0
if (num == 0)
for (Tile tile : ADJACENT_TILES)
if (!tile.flagged)
tile.reveal();
}
// Determine what image should be drawn at this tile
@ -224,23 +252,23 @@ public class Tile extends JComponent {
if (gameEnded && mine) {
// Automatically flag mines at wins, losses keep correct flags
if (!GAME_CANVAS.isGameLost() || flagged)
return SPECIAL_TILES[FLAGGED_INDEX];
return specialTiles[FLAGGED_INDEX];
if (revealed)
return SPECIAL_TILES[EXPLODED_MINE_INDEX];
return SPECIAL_TILES[MINE_INDEX];
return specialTiles[EXPLODED_MINE_INDEX];
return specialTiles[MINE_INDEX];
}
if (revealed)
return NUMBER_TILES[num];
return numberTiles[num];
if (flagged) {
// Correctly flagged mines would have been caught earlier
if (gameEnded)
return SPECIAL_TILES[NOT_MINE_INDEX];
return SPECIAL_TILES[FLAGGED_INDEX];
return specialTiles[NOT_MINE_INDEX];
return specialTiles[FLAGGED_INDEX];
}
// Tiles are only interactable if the game is ongoing
if (pressed && !gameEnded)
return SPECIAL_TILES[PRESSED_INDEX];
return SPECIAL_TILES[REGULAR_INDEX];
return specialTiles[PRESSED_INDEX];
return specialTiles[REGULAR_INDEX];
}
@Override