Add all comments
This commit is contained in:
parent
c9d70d737d
commit
11e5379d23
195
Canvas.java
195
Canvas.java
|
@ -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,38 +180,37 @@ 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() {
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 1 second delay
|
||||
|
@ -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,7 +337,9 @@ public class Canvas extends JPanel {
|
|||
FACE.setFace(Face.HAPPY_INDEX);
|
||||
}
|
||||
|
||||
// Ditto
|
||||
public void setRightMouseDown(boolean b) {
|
||||
if (!gameEnded)
|
||||
rightMouseDown = b;
|
||||
}
|
||||
|
||||
|
@ -267,10 +347,16 @@ public class Canvas extends JPanel {
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
33
Face.java
33
Face.java
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
104
Main.java
104
Main.java
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
13
Options.java
13
Options.java
|
@ -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() {
|
||||
}
|
||||
|
|
72
Tile.java
72
Tile.java
|
@ -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,37 +236,39 @@ public class Tile extends JComponent {
|
|||
|
||||
if (mine) {
|
||||
GAME_CANVAS.setLoseFlag();
|
||||
} else {
|
||||
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
|
||||
private Image getImage() {
|
||||
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
|
||||
|
|
Loading…
Reference in a new issue