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.imageio.*;
import javax.swing.*; 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; public static final int TS_WIDTH = 144;
// Height of the tileset
public static final int TS_HEIGHT = 122; public static final int TS_HEIGHT = 122;
// Y-position of the digits in the tileset
public static final int TS_DIGITS_Y = 33; public static final int TS_DIGITS_Y = 33;
// Y-position of the faces in the tilest
public static final int TS_FACES_Y = 55; 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; 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; 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; public static final int TS_COLOR_X = 70;
// Width of the borders
public static final int BORDER_WIDTH = 12; public static final int BORDER_WIDTH = 12;
// Height of most of the borders
public static final int BORDER_HEIGHT = 11; public static final int BORDER_HEIGHT = 11;
// Height of the bottom border
public static final int BOTTOM_HEIGHT = 12; 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; 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; 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; 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; public static final int TIMER_PADDING = 59;
// The important game attributes as final ints
public final int ROWS; public final int ROWS;
public final int COLS; public final int COLS;
public final int MINES; public final int MINES;
public final int WIDTH; public final int WIDTH;
public final int HEIGHT; public final int HEIGHT;
// JPanel containing all the tiles
private final JPanel BOARD_PANEL; private final JPanel BOARD_PANEL;
// Number displays for the flags and the time
private final NumberDisplay FLAGS_DISPLAY; private final NumberDisplay FLAGS_DISPLAY;
private final NumberDisplay TIMER_DISPLAY; private final NumberDisplay TIMER_DISPLAY;
// The face
private final Face FACE; private final Face FACE;
// A KeyListener listening for shift to help chord
private final KeyListener SHIFT_KEY_LISTENER; private final KeyListener SHIFT_KEY_LISTENER;
// Timer to tick up the time every second
private final Timer TIMER; 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 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 // The tile that the mouse is currently over
private Tile currentTile; 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; private boolean gameStarted;
// Whether the game has ended
private boolean gameEnded; private boolean gameEnded;
// Whether the game was lost
private boolean gameLost; 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 holdingShift;
private boolean leftMouseDown; private boolean leftMouseDown;
private boolean rightMouseDown; private boolean rightMouseDown;
private int numTilesLeft;
private int numFlagsLeft;
private int time;
// Set initial values of all the variables
private void init() { private void init() {
currentTile = null; currentTile = null;
gameStarted = false; gameStarted = false;
gameEnded = false; gameEnded = false;
gameLost = false; gameLost = false;
holdingShift = false;
leftMouseDown = false;
rightMouseDown = false;
numTilesLeft = ROWS * COLS - MINES; numTilesLeft = ROWS * COLS - MINES;
numFlagsLeft = MINES; numFlagsLeft = MINES;
time = 0; time = 0;
holdingShift = false;
leftMouseDown = false;
rightMouseDown = false;
addKeyListener(SHIFT_KEY_LISTENER); addKeyListener(SHIFT_KEY_LISTENER);
@ -67,17 +105,13 @@ public class Canvas extends JPanel {
TIMER_DISPLAY.setNum(time); TIMER_DISPLAY.setNum(time);
} }
public Canvas(File skin) { // Get the tileset from Options and use it to set the images of all the
Difficulty difficulty = Options.getDifficulty(); // components inside the canvas and the background image
ROWS = difficulty.getRows(); private void setImages() {
COLS = difficulty.getCols(); // Read the tileset image
MINES = difficulty.getMines();
WIDTH = Tile.SIZE * COLS + 2 * BORDER_WIDTH;
HEIGHT = Tile.SIZE * ROWS + 2 * BORDER_HEIGHT + BOTTOM_HEIGHT + TOP_BOX_HEIGHT;
BufferedImage tileset; BufferedImage tileset;
try { 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 // Reject images that are too small to properly form the tileset
if (tileset.getWidth() < TS_WIDTH || tileset.getHeight() < TS_HEIGHT) if (tileset.getWidth() < TS_WIDTH || tileset.getHeight() < TS_HEIGHT)
throw new IOException(); throw new IOException();
@ -86,20 +120,47 @@ public class Canvas extends JPanel {
tileset = new BufferedImage(TS_WIDTH, TS_HEIGHT, BufferedImage.TYPE_INT_RGB); 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[] numberTiles = new Image[9];
Image[] specialTiles = new Image[Tile.EXPLODED_MINE_INDEX + 1]; Image[] specialTiles = new Image[Tile.EXPLODED_MINE_INDEX + 1];
for (int i = 0; i < 9; i++) for (int i = 0; i < 9; i++)
numberTiles[i] = getTileFromSet(tileset, i, 0); numberTiles[i] = getTileFromSet(tileset, i, 0);
for (int i = 0; i <= Tile.EXPLODED_MINE_INDEX; i++) for (int i = 0; i <= Tile.EXPLODED_MINE_INDEX; i++)
specialTiles[i] = getTileFromSet(tileset, i, Tile.SIZE); 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 = new Tile[ROWS][COLS];
BOARD_PANEL = new JPanel(new GridLayout(ROWS, COLS)); BOARD_PANEL = new JPanel(new GridLayout(ROWS, COLS));
for (int r = 0; r < ROWS; r++) { for (int r = 0; r < ROWS; r++) {
for (int c = 0; c < COLS; c++) { 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]); BOARD_PANEL.add(BOARD[r][c]);
} }
} }
@ -119,38 +180,37 @@ public class Canvas extends JPanel {
} }
} }
Image[] digits = getImageSet(tileset, NumberDisplay.MINUS_INDEX, TS_DIGITS_Y, // Initialize the other components
NumberDisplay.DIGIT_WIDTH, NumberDisplay.DIGIT_HEIGHT); FLAGS_DISPLAY = new NumberDisplay();
Image numberDisplayBackdrop = tileset.getSubimage(TS_DIGIT_BACKDROP_X, TS_MISC_Y, TIMER_DISPLAY = new NumberDisplay();
NumberDisplay.BACKDROP_WIDTH, NumberDisplay.BACKDROP_HEIGHT); FACE = new Face(this);
FLAGS_DISPLAY = new NumberDisplay(numberDisplayBackdrop, digits); // Now that the components are all initialized, set all of their images
TIMER_DISPLAY = new NumberDisplay(numberDisplayBackdrop, digits); setImages();
FACE = new Face(this, getImageSet(tileset, Face.PRESSED_INDEX, TS_FACES_Y, Face.SIZE, Face.SIZE));
// Shift+LMB chords, so we need some way of determining if the shift key
// is being held.
SHIFT_KEY_LISTENER = new KeyAdapter() { SHIFT_KEY_LISTENER = new KeyAdapter() {
// Shift+LMB chords, so we need some way of determining if the shift
// key is being held.
@Override @Override
public void keyPressed(KeyEvent e) { public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_SHIFT) { updateShift(e, true);
holdingShift = true;
updateCurrentTile();
}
} }
@Override @Override
public void keyReleased(KeyEvent e) { public void keyReleased(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_SHIFT) { updateShift(e, false);
holdingShift = false;
updateCurrentTile();
}
} }
private void updateCurrentTile() { // 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) if (currentTile != null)
currentTile.updatePressedState(); currentTile.updatePressedState();
} }
}
}; };
// 1 second delay // 1 second delay
@ -163,12 +223,13 @@ public class Canvas extends JPanel {
return; return;
} }
// Play the tick sound and increment the timer number
Sound.TICK.play(); Sound.TICK.play();
TIMER_DISPLAY.setNum(++time); 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); setLayout(null);
Insets insets = getInsets(); Insets insets = getInsets();
placeComponent(BOARD_PANEL, BORDER_WIDTH, 2 * BORDER_HEIGHT + TOP_BOX_HEIGHT, insets); placeComponent(BOARD_PANEL, BORDER_WIDTH, 2 * BORDER_HEIGHT + TOP_BOX_HEIGHT, insets);
@ -182,11 +243,19 @@ public class Canvas extends JPanel {
init(); init();
} }
// Refresh the skin and repaint
public void newSkin() {
setImages();
repaint();
}
// Stop everything that might be running
public void stop() { public void stop() {
TIMER.stop(); TIMER.stop();
removeKeyListener(SHIFT_KEY_LISTENER); removeKeyListener(SHIFT_KEY_LISTENER);
} }
// Restart the game
public void restart() { public void restart() {
if (!gameStarted) if (!gameStarted)
return; return;
@ -218,20 +287,22 @@ public class Canvas extends JPanel {
TIMER.start(); TIMER.start();
} }
// After every tile revealed, check for game ending
public void postRevealCheck() { public void postRevealCheck() {
if (gameLost) { if (gameLost) { // Loss
Sound.EXPLODE.play(); Sound.EXPLODE.play();
stopGame(); endGame();
FACE.setFace(Face.DEAD_INDEX); FACE.setFace(Face.DEAD_INDEX);
} else if (numTilesLeft == 0) { } else if (numTilesLeft == 0) { // Win
Sound.WIN.play(); Sound.WIN.play();
// Automatically flag all remaining tiles on win // Automatically flag all remaining tiles on win
FLAGS_DISPLAY.setNum(0); FLAGS_DISPLAY.setNum(0);
stopGame(); endGame();
FACE.setFace(Face.COOL_INDEX); FACE.setFace(Face.COOL_INDEX);
} }
} }
// Modify the flag counter
public void modifyFlagCount(boolean addedFlag) { public void modifyFlagCount(boolean addedFlag) {
if (addedFlag) if (addedFlag)
numFlagsLeft--; numFlagsLeft--;
@ -240,18 +311,25 @@ public class Canvas extends JPanel {
FLAGS_DISPLAY.setNum(numFlagsLeft); FLAGS_DISPLAY.setNum(numFlagsLeft);
} }
// Called by tile every single time one is revealed
public void revealedSingleTile() { public void revealedSingleTile() {
numTilesLeft--; numTilesLeft--;
} }
// Called by tile if it is a mine when revealed
public void setLoseFlag() { public void setLoseFlag() {
gameLost = true; 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) { public void setLeftMouseDown(boolean b) {
if (gameEnded) if (gameEnded)
return; return;
// Face gets nervous when clicking
leftMouseDown = b; leftMouseDown = b;
if (leftMouseDown) if (leftMouseDown)
FACE.setFace(Face.SHOCKED_INDEX); FACE.setFace(Face.SHOCKED_INDEX);
@ -259,7 +337,9 @@ public class Canvas extends JPanel {
FACE.setFace(Face.HAPPY_INDEX); FACE.setFace(Face.HAPPY_INDEX);
} }
// Ditto
public void setRightMouseDown(boolean b) { public void setRightMouseDown(boolean b) {
if (!gameEnded)
rightMouseDown = b; rightMouseDown = b;
} }
@ -267,10 +347,16 @@ public class Canvas extends JPanel {
return leftMouseDown; return leftMouseDown;
} }
// Chording can be done with either the right mouse button or the shift key
public boolean isChording() { public boolean isChording() {
return holdingShift || rightMouseDown; 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() { public Tile getCurrentTile() {
return currentTile; return currentTile;
} }
@ -283,14 +369,18 @@ public class Canvas extends JPanel {
return gameLost; 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); return tileset.getSubimage(Tile.SIZE * n, y, Tile.SIZE, Tile.SIZE);
} }
private Image[] getImageSet(BufferedImage tileset, int lastIndex, int y, int width, int height) { // For the other images, which have a 1-pixel gap between them: return an
Image[] img = new Image[lastIndex + 1]; // 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; 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); img[i] = tileset.getSubimage(x, y, width, height);
x += width + 1; x += width + 1;
} }
@ -367,7 +457,8 @@ public class Canvas extends JPanel {
c.setSize(c.getPreferredSize()); c.setSize(c.getPreferredSize());
} }
private void stopGame() { // Stop the moving parts after a win or a loss
private void endGame() {
TIMER.stop(); TIMER.stop();
gameEnded = true; gameEnded = true;
for (Tile[] row : BOARD) 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 the paintComponent method in order to add the background image
@Override @Override
public void paintComponent(Graphics g) { public void paintComponent(Graphics gr) {
super.paintComponent(g); super.paintComponent(gr);
g.drawImage(BACKGROUND_IMAGE, 0, 0, this); Graphics2D g = (Graphics2D) gr;
g.drawImage(backgroundImage, 0, 0, this);
} }
} }

View file

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

View file

@ -1,7 +1,7 @@
import java.awt.*; import java.awt.*;
import java.awt.event.*;
import javax.swing.*; import javax.swing.*;
// Enum with the difficuulty options
public enum Difficulty { public enum Difficulty {
BEGINNER (9, 9, 10), BEGINNER (9, 9, 10),
INTERMEDIATE (16, 16, 40), INTERMEDIATE (16, 16, 40),
@ -20,6 +20,8 @@ public enum Difficulty {
// it's possible to hit the number display cap // it's possible to hit the number display cap
private static final int MAX_MINES = 999; 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 rows;
private int cols; private int cols;
private int mines; private int mines;
@ -49,6 +51,8 @@ public enum Difficulty {
return mines; 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) { public static boolean setCustom(JFrame frame) {
Difficulty current = Options.getDifficulty(); 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 rowsField = new RowsColsField(current.rows, MIN_SIZE, MAX_ROWS);
CustomTextField colsField = new RowsColsField(current.cols, MIN_SIZE, MAX_COLS); CustomTextField colsField = new RowsColsField(current.cols, MIN_SIZE, MAX_COLS);
// Override CustomTextField in order to make it dynamically determine // 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 // 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 = { JComponent[][] items = {
{ new JLabel("Height:"), rowsField }, { new JLabel("Height:"), rowsField },
{ new JLabel("Width:"), colsField }, { new JLabel("Width:"), colsField },
{ new JLabel("Mines:"), minesField }, { 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); "Custom Board", JOptionPane.OK_CANCEL_OPTION);
// If user didn't hit OK, bail out now
if (option != JOptionPane.OK_OPTION) if (option != JOptionPane.OK_OPTION)
return false; return false;
// Bail out if fields can't be parsed into integers
int rows, cols, mines; int rows, cols, mines;
try { try {
rows = Integer.parseInt(rowsField.getText()); rows = Integer.parseInt(rowsField.getText());
@ -110,8 +118,9 @@ public enum Difficulty {
return false; return false;
} }
// Clamp values to the allowed range, just in case something slipped // Even though the text fields should technically only allow valid
// through the focusLost events // 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); rows = clampInt(rows, MIN_SIZE, MAX_ROWS);
cols = clampInt(cols, MIN_SIZE, MAX_COLS); cols = clampInt(cols, MIN_SIZE, MAX_COLS);
mines = clampInt(mines, MIN_MINES, getMaxMines(rows, 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) if (current == CUSTOM && CUSTOM.rows == rows && CUSTOM.cols == cols && CUSTOM.mines == mines)
return false; return false;
// Everything succeeded, so now actually set the new stats
CUSTOM.setStats(rows, cols, mines); CUSTOM.setStats(rows, cols, mines);
return true; 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) { public static int clampInt(int value, int minValue, int maxValue) {
return Math.max(Math.min(value, maxValue), minValue); 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) { private static int getMaxMines(int rows, int cols) {
return Math.min((rows - 1) * (cols - 1), MAX_MINES); 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; 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); JPanel messagePanel = new JPanel(null);
// Add the items to the panel, setting the label alignment along the way
for (JComponent[] row : items) { for (JComponent[] row : items) {
JLabel label = (JLabel) row[0]; JLabel label = (JLabel) row[0];
label.setLabelFor(row[1]); label.setLabelFor(row[1]);
@ -146,32 +169,45 @@ public enum Difficulty {
messagePanel.add(row[1]); messagePanel.add(row[1]);
} }
int[] widths = new int[COLS]; // The maximum width of all the elements of each column
int[] heights = new int[items.length]; 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]; int[] x = new int[COLS + 1];
// The y-position of all the elements of each row
int[] y = new int[items.length + 1]; 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++) { for (int c = 0; c < COLS; c++) {
// Get the maximum width among items of this row
int maxWidth = Integer.MIN_VALUE; int maxWidth = Integer.MIN_VALUE;
for (int r = 0; r < items.length; r++) for (int r = 0; r < items.length; r++)
maxWidth = Math.max(maxWidth, (int) items[r][c].getPreferredSize().getWidth()); maxWidth = Math.max(maxWidth, (int) items[r][c].getPreferredSize().getWidth());
widths[c] = maxWidth; maxWidths[c] = maxWidth;
x[c + 1] = x[c] + maxWidth + padding; // 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++) { for (int r = 0; r < items.length; r++) {
int maxHeight = Integer.MIN_VALUE; int maxHeight = Integer.MIN_VALUE;
for (int c = 0; c < COLS; c++) for (int c = 0; c < COLS; c++)
maxHeight = Math.max(maxHeight, (int) items[r][c].getPreferredSize().getHeight()); maxHeight = Math.max(maxHeight, (int) items[r][c].getPreferredSize().getHeight());
heights[r] = maxHeight; maxHeights[r] = maxHeight;
y[r + 1] = y[r] + maxHeight + padding; 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 r = 0; r < items.length; r++)
for (int c = 0; c < COLS; c++) 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])); messagePanel.setPreferredSize(new Dimension(x[COLS], y[items.length]));
return messagePanel; return messagePanel;

View file

@ -2,29 +2,42 @@ import java.awt.*;
import java.awt.event.*; import java.awt.event.*;
import javax.swing.*; import javax.swing.*;
// The face at the top of the game
public class Face extends JComponent { public class Face extends JComponent {
// The width and height
public static final int SIZE = 26; 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 HAPPY_INDEX = 0;
public static final int SHOCKED_INDEX = 1; public static final int SHOCKED_INDEX = 1;
public static final int DEAD_INDEX = 2; public static final int DEAD_INDEX = 2;
public static final int COOL_INDEX = 3; public static final int COOL_INDEX = 3;
public static final int PRESSED_INDEX = 4; public static final int PRESSED_INDEX = 4;
// The main game canvas to refer back to
private final Canvas GAME_CANVAS; private final Canvas GAME_CANVAS;
private final Image[] FACES;
private Image[] faces;
// The current face index
private int face; private int face;
// The last face index, before being pressed, in order to revert on unpress
private int unpressedFace; private int unpressedFace;
// Whether the face is being pressed right now
private boolean pressed; private boolean pressed;
// Whether the mouse is over the face
private boolean mouseOver; private boolean mouseOver;
public Face(Canvas gameCanvas, Image[] faces) { public Face(Canvas gameCanvas) {
FACES = faces;
GAME_CANVAS = gameCanvas; GAME_CANVAS = gameCanvas;
face = HAPPY_INDEX; face = HAPPY_INDEX;
pressed = false; pressed = false;
mouseOver = false; mouseOver = false;
setPreferredSize(new Dimension(SIZE, SIZE)); 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() { addMouseListener(new MouseAdapter() {
// The only way to initiate a pressed face state is when the mouse
// is over it and is pressed down
@Override @Override
public void mousePressed(MouseEvent e) { public void mousePressed(MouseEvent e) {
pressed = true; pressed = true;
@ -32,6 +45,10 @@ public class Face extends JComponent {
setFace(PRESSED_INDEX); 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 @Override
public void mouseReleased(MouseEvent e) { public void mouseReleased(MouseEvent e) {
pressed = false; 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 @Override
public void mouseEntered(MouseEvent e) { public void mouseEntered(MouseEvent e) {
mouseOver = true; mouseOver = true;
@ -48,6 +67,8 @@ public class Face extends JComponent {
setFace(PRESSED_INDEX); setFace(PRESSED_INDEX);
} }
// Temporarily show the unpressed face, but keep the pressed boolean
// true in case the mouse reenters
@Override @Override
public void mouseExited(MouseEvent e) { public void mouseExited(MouseEvent e) {
mouseOver = false; mouseOver = false;
@ -57,6 +78,10 @@ public class Face extends JComponent {
}); });
} }
public void setImages(Image[] faces) {
this.faces = faces;
}
public void setFace(int face) { public void setFace(int face) {
this.face = face; this.face = face;
repaint(); repaint();
@ -67,6 +92,6 @@ public class Face extends JComponent {
super.paintComponent(gr); super.paintComponent(gr);
Graphics2D g = (Graphics2D) gr; Graphics2D g = (Graphics2D) gr;
g.drawImage(FACES[face], 0, 0, this); g.drawImage(faces[face], 0, 0, this);
} }
} }

104
Main.java
View file

@ -7,21 +7,21 @@ import javax.swing.*;
import javax.swing.event.*; import javax.swing.event.*;
public class Main { public class Main {
public static final String DEFAULT_SKIN = "winxpskin.bmp"; // HTML filenames to be shown in the Help menu
public static final String SKINS_DIR = "Skins/";
public static final String HELP_FILE = "help.html"; public static final String HELP_FILE = "help.html";
public static final String ABOUT_FILE = "about.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"); private static final JFrame FRAME = new JFrame("Minesweeper");
// The current game Canvas
private static Canvas 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) { public static void main(String[] args) {
skin = DEFAULT_SKIN;
FRAME.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); FRAME.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
FRAME.setResizable(false); FRAME.setResizable(false);
FRAME.setJMenuBar(getMenuBar()); FRAME.setJMenuBar(getMenuBar());
@ -30,6 +30,7 @@ public class Main {
FRAME.setVisible(true); FRAME.setVisible(true);
} }
// Create the menu bar of the frame
private static JMenuBar getMenuBar() { private static JMenuBar getMenuBar() {
// Button group for the difficulty buttons // Button group for the difficulty buttons
ButtonGroup difficultyButtons = new ButtonGroup(); ButtonGroup difficultyButtons = new ButtonGroup();
@ -46,12 +47,13 @@ public class Main {
difficultyButtons.add(expertItem); difficultyButtons.add(expertItem);
difficultyButtons.add(customItem); difficultyButtons.add(customItem);
// ActionListener for every button except the skins ones
ActionListener listener = new ActionListener() { ActionListener listener = new ActionListener() {
// Keep track of the previously selected difficulty button in case a // Keep track of the previously selected difficulty button in case a
// Custom press is canceled so we can revert back to whatever it was // Custom press is canceled so we can revert the button selection
// before
private ButtonModel lastDifficultyButton = difficultyButtons.getSelection(); private ButtonModel lastDifficultyButton = difficultyButtons.getSelection();
// Parse the action command and perform the corresponding action
@Override @Override
public void actionPerformed(ActionEvent e) { public void actionPerformed(ActionEvent e) {
switch (e.getActionCommand()) { switch (e.getActionCommand()) {
@ -71,6 +73,8 @@ public class Main {
lastDifficultyButton = expertItem.getModel(); lastDifficultyButton = expertItem.getModel();
break; break;
case "custom": case "custom":
// If canceled, set the currently selected button to
// whatever it was before
if (Difficulty.setCustom(FRAME)) { if (Difficulty.setCustom(FRAME)) {
setDifficulty(Difficulty.CUSTOM); setDifficulty(Difficulty.CUSTOM);
lastDifficultyButton = customItem.getModel(); lastDifficultyButton = customItem.getModel();
@ -102,6 +106,7 @@ public class Main {
JMenuBar menuBar = new JMenuBar(); JMenuBar menuBar = new JMenuBar();
// Create the Game item in the menu bar
JMenu gameMenu = new JMenu("Game"); JMenu gameMenu = new JMenu("Game");
gameMenu.setMnemonic(KeyEvent.VK_G); gameMenu.setMnemonic(KeyEvent.VK_G);
JMenuItem newGameItem = new JMenuItem("New game"); JMenuItem newGameItem = new JMenuItem("New game");
@ -123,6 +128,7 @@ public class Main {
addMenuItem("Exit", gameMenu, KeyEvent.VK_X, "exit", listener); addMenuItem("Exit", gameMenu, KeyEvent.VK_X, "exit", listener);
menuBar.add(gameMenu); menuBar.add(gameMenu);
// Create the Help item in the menu bar
JMenu helpMenu = new JMenu("Help"); JMenu helpMenu = new JMenu("Help");
helpMenu.setMnemonic(KeyEvent.VK_H); helpMenu.setMnemonic(KeyEvent.VK_H);
JMenuItem helpItem = new JMenuItem("Help"); JMenuItem helpItem = new JMenuItem("Help");
@ -132,28 +138,32 @@ public class Main {
addMenuItem("About", helpMenu, KeyEvent.VK_A, "about", listener); addMenuItem("About", helpMenu, KeyEvent.VK_A, "about", listener);
menuBar.add(helpMenu); menuBar.add(helpMenu);
// Create the Skins item in the menu bar (this one is special)
JMenu skinsMenu = new JMenu("Skins"); JMenu skinsMenu = new JMenu("Skins");
skinsMenu.setMnemonic(KeyEvent.VK_S); skinsMenu.setMnemonic(KeyEvent.VK_S);
// The Skins menu should dynamically generate a menu of all image files // 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() { 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() { private static ActionListener skinListener = new ActionListener() {
@Override @Override
public void actionPerformed(ActionEvent e) { public void actionPerformed(ActionEvent e) {
// Set the skin to the new one if it isn't already that // Set the skin to the new one if it isn't already that
String newSkin = e.getActionCommand(); String newSkin = e.getActionCommand();
if (newSkin.equals(skin)) if (newSkin.equals(Options.getSkinName()))
return; return;
skin = newSkin; Options.setSkinName(newSkin);
replaceCanvas(); canvas.newSkin();
} }
}; };
@Override @Override
public void menuSelected(MenuEvent e) { public void menuSelected(MenuEvent e) {
// Get a stream of all files in the skins directory // 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 dirStream
// Filter for only regular files // Filter for only regular files
.filter(Files::isRegularFile) .filter(Files::isRegularFile)
@ -162,19 +172,17 @@ public class Main {
.map(Path::toString) .map(Path::toString)
// Only allow a filename if it ends with one of the // Only allow a filename if it ends with one of the
// valid extensions // valid extensions
.filter(name -> Arrays.stream(VALID_EXTENSIONS) .filter(name -> Arrays.stream(VALID_SKINS_EXTENSIONS)
// Use regionMatches to effectively .anyMatch(ext -> name.toLowerCase().endsWith(ext)))
// case-insensitively endsWith() // Because Files.list() does not guarantee an order,
.anyMatch(ext -> name.regionMatches(true, // and alphabetically sorted files look objectively
name.length() - ext.length(), ext, 0, ext.length()))) // nicer in directory listings
// Because Files.list does not guarantee an order, and
// alphabetical files look objectively nicer in lists
.sorted() .sorted()
// Create a radio button menu item for each one, // Create a radio button menu item for each one,
// remembering to have it pre-selected if it's already // remembering to have it pre-selected if it's already
// the current skin // the current skin
.forEach(name -> addMenuItem(new JRadioButtonMenuItem(name, name.equals(skin)), .forEach(name -> addMenuItem(new JRadioButtonMenuItem(name,
skinsMenu, name, skinListener)); name.equals(Options.getSkinName())), skinsMenu, name, skinListener));
} catch (IOException ignore) { } catch (IOException ignore) {
// It's fine to leave it blank if we can't get results // It's fine to leave it blank if we can't get results
} }
@ -196,27 +204,8 @@ public class Main {
return menuBar; return menuBar;
} }
private static void messageHtmlFile(String file, String failText, String title, int messageType) { // Helper items for adding menu items without needing a million copy-pasted
String text; // lines
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();
}
private static void addMenuItem(String menuTitle, JMenu menu, int mnemonic, String actionCommand, private static void addMenuItem(String menuTitle, JMenu menu, int mnemonic, String actionCommand,
ActionListener listener) { ActionListener listener) {
addMenuItem(new JMenuItem(menuTitle), menu, mnemonic, actionCommand, listener); addMenuItem(new JMenuItem(menuTitle), menu, mnemonic, actionCommand, listener);
@ -235,17 +224,40 @@ public class Main {
menu.add(menuItem); 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.stop();
canvas.removeAll(); canvas.removeAll();
FRAME.remove(canvas); FRAME.remove(canvas);
setCanvas(); setCanvas();
canvas.requestFocusInWindow(); canvas.requestFocusInWindow();
} }
// Create a new canvas and put it in the frame
private static void setCanvas() { private static void setCanvas() {
canvas = new Canvas(new File(SKINS_DIR, skin)); canvas = new Canvas();
FRAME.add(canvas); FRAME.add(canvas);
FRAME.pack(); FRAME.pack();
} }

View file

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

View file

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

View file

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