478 lines
15 KiB
Java
478 lines
15 KiB
Java
import java.awt.*;
|
|
import java.awt.event.*;
|
|
import java.awt.image.*;
|
|
import java.io.*;
|
|
import javax.imageio.*;
|
|
import javax.swing.*;
|
|
|
|
// 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;
|
|
|
|
// 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;
|
|
|
|
// Set initial values of all the variables
|
|
private void init() {
|
|
currentTile = null;
|
|
gameStarted = false;
|
|
gameEnded = false;
|
|
gameLost = false;
|
|
numTilesLeft = ROWS * COLS - MINES;
|
|
numFlagsLeft = MINES;
|
|
time = 0;
|
|
holdingShift = false;
|
|
leftMouseDown = false;
|
|
rightMouseDown = false;
|
|
|
|
addKeyListener(SHIFT_KEY_LISTENER);
|
|
|
|
FLAGS_DISPLAY.setNum(numFlagsLeft);
|
|
TIMER_DISPLAY.setNum(time);
|
|
}
|
|
|
|
// 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(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();
|
|
} catch (IOException e) {
|
|
// Just use an empty tileset if it didn't work out
|
|
tileset = new BufferedImage(TS_WIDTH, TS_HEIGHT, BufferedImage.TYPE_INT_RGB);
|
|
}
|
|
|
|
// 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);
|
|
BOARD_PANEL.add(BOARD[r][c]);
|
|
}
|
|
}
|
|
// Before doing anything else, we want each tile to know what tiles are
|
|
// adjacent to them
|
|
for (int r = 0; r < ROWS; r++) {
|
|
for (int c = 0; c < COLS; c++) {
|
|
// Liberal use of min/max to ensure we don't go out of bounds
|
|
int rStart = Math.max(r - 1, 0);
|
|
int rEnd = Math.min(r + 2, ROWS);
|
|
int cStart = Math.max(c - 1, 0);
|
|
int cEnd = Math.min(c + 2, COLS);
|
|
for (int rAdj = rStart; rAdj < rEnd; rAdj++)
|
|
for (int cAdj = cStart; cAdj < cEnd; cAdj++)
|
|
if (rAdj != r || cAdj != c) // Ignore yourself
|
|
BOARD[r][c].addAdjacentTile(BOARD[rAdj][cAdj]);
|
|
}
|
|
}
|
|
|
|
// Initialize the other components
|
|
FLAGS_DISPLAY = new NumberDisplay();
|
|
TIMER_DISPLAY = new NumberDisplay();
|
|
FACE = new Face(this);
|
|
|
|
// 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() {
|
|
@Override
|
|
public void keyPressed(KeyEvent e) {
|
|
updateShift(e, true);
|
|
}
|
|
|
|
@Override
|
|
public void keyReleased(KeyEvent e) {
|
|
updateShift(e, false);
|
|
}
|
|
|
|
// 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
|
|
TIMER = new Timer(1000, new ActionListener() {
|
|
@Override
|
|
public void actionPerformed(ActionEvent e) {
|
|
// Can't display past 999 anyways
|
|
if (time == 999) {
|
|
TIMER.stop();
|
|
return;
|
|
}
|
|
|
|
// Play the tick sound and increment the timer number
|
|
Sound.TICK.play();
|
|
TIMER_DISPLAY.setNum(++time);
|
|
}
|
|
});
|
|
|
|
// 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);
|
|
placeComponent(FLAGS_DISPLAY, FLAGS_PADDING, TOP_PADDING, insets);
|
|
placeComponent(TIMER_DISPLAY, WIDTH - TIMER_PADDING, TOP_PADDING, insets);
|
|
placeComponent(FACE, (WIDTH - Face.SIZE) / 2, TOP_PADDING, insets);
|
|
|
|
setPreferredSize(new Dimension(WIDTH, HEIGHT));
|
|
setFocusable(true);
|
|
|
|
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;
|
|
|
|
stop();
|
|
init();
|
|
|
|
for (Tile[] row : BOARD)
|
|
for (Tile tile : row)
|
|
tile.restart();
|
|
BOARD_PANEL.repaint();
|
|
}
|
|
|
|
// Start the game if it's not already started
|
|
public void tryStartGame(Tile startTile) {
|
|
if (gameStarted)
|
|
return;
|
|
gameStarted = true;
|
|
|
|
// Place all the mines after the the first tile was selected, in such a
|
|
// way that the first selected tile must be blank
|
|
for (int mineCount = 0; mineCount < MINES;) {
|
|
int row = (int) (Math.random() * ROWS);
|
|
int col = (int) (Math.random() * COLS);
|
|
if (BOARD[row][col].makeMine(startTile))
|
|
mineCount++;
|
|
}
|
|
|
|
TIMER.start();
|
|
}
|
|
|
|
// After every tile revealed, check for game ending
|
|
public void postRevealCheck() {
|
|
if (gameLost) { // Loss
|
|
Sound.EXPLODE.play();
|
|
endGame();
|
|
FACE.setFace(Face.DEAD_INDEX);
|
|
} else if (numTilesLeft == 0) { // Win
|
|
Sound.WIN.play();
|
|
// Automatically flag all remaining tiles on win
|
|
FLAGS_DISPLAY.setNum(0);
|
|
endGame();
|
|
FACE.setFace(Face.COOL_INDEX);
|
|
}
|
|
}
|
|
|
|
// Modify the flag counter
|
|
public void modifyFlagCount(boolean addedFlag) {
|
|
if (addedFlag)
|
|
numFlagsLeft--;
|
|
else
|
|
numFlagsLeft++;
|
|
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);
|
|
else
|
|
FACE.setFace(Face.HAPPY_INDEX);
|
|
}
|
|
|
|
// Ditto
|
|
public void setRightMouseDown(boolean b) {
|
|
if (!gameEnded)
|
|
rightMouseDown = b;
|
|
}
|
|
|
|
public boolean isLeftMouseDown() {
|
|
return leftMouseDown;
|
|
}
|
|
|
|
// Chording can be done with either the right mouse button or the shift key
|
|
public boolean isChording() {
|
|
return holdingShift || rightMouseDown;
|
|
}
|
|
|
|
// Tiles need to know what tile the mouse is currently over because the
|
|
// MouseReleased event is triggered on the tile that the mouse was pressed
|
|
// down on, not the one it was released on, but the tile that it was
|
|
// released on is the one that actually gets revealed, which we would
|
|
// otherwise have no way of knowing
|
|
public Tile getCurrentTile() {
|
|
return currentTile;
|
|
}
|
|
|
|
public void setCurrentTile(Tile tile) {
|
|
currentTile = tile;
|
|
}
|
|
|
|
public boolean isGameLost() {
|
|
return gameLost;
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
// 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 < n; i++) {
|
|
img[i] = tileset.getSubimage(x, y, width, height);
|
|
x += width + 1;
|
|
}
|
|
return img;
|
|
}
|
|
|
|
// Create the background image, borders and all
|
|
private BufferedImage getBackgroundImage(BufferedImage tileset) {
|
|
// Get all the images out of the tileset, going row-by-row, hence tsY
|
|
// marking the y-index of the current row
|
|
int tsY = TS_MISC_Y;
|
|
Image topLeftCorner = tileset.getSubimage(0, tsY, BORDER_WIDTH, BORDER_HEIGHT);
|
|
Image topBorder = tileset.getSubimage(BORDER_WIDTH + 1, tsY, 1, BORDER_HEIGHT);
|
|
Image topRightCorner = tileset.getSubimage(BORDER_WIDTH + 3, tsY, BORDER_WIDTH, BORDER_HEIGHT);
|
|
tsY += BORDER_HEIGHT + 1;
|
|
Image topLeftBorder = tileset.getSubimage(0, tsY, BORDER_WIDTH, 1);
|
|
Image topRightBorder = tileset.getSubimage(BORDER_WIDTH + 3, tsY, BORDER_WIDTH, 1);
|
|
tsY += 2;
|
|
Image middleLeftCorner = tileset.getSubimage(0, tsY, BORDER_WIDTH, BORDER_HEIGHT);
|
|
Image middleBorder = tileset.getSubimage(BORDER_WIDTH + 1, tsY, 1, BORDER_HEIGHT);
|
|
Image middleRightCorner = tileset.getSubimage(BORDER_WIDTH + 3, tsY, BORDER_WIDTH, BORDER_HEIGHT);
|
|
tsY += BORDER_HEIGHT + 1;
|
|
Image bottomLeftBorder = tileset.getSubimage(0, tsY, BORDER_WIDTH, 1);
|
|
Image bottomRightBorder = tileset.getSubimage(BORDER_WIDTH + 3, tsY, BORDER_WIDTH, 1);
|
|
tsY += 2;
|
|
Image bottomLeftCorner = tileset.getSubimage(0, tsY, BORDER_WIDTH, BOTTOM_HEIGHT);
|
|
Image bottomBorder = tileset.getSubimage(BORDER_WIDTH + 1, tsY, 1, BOTTOM_HEIGHT);
|
|
Image bottomRightCorner = tileset.getSubimage(BORDER_WIDTH + 3, tsY, BORDER_WIDTH, BOTTOM_HEIGHT);
|
|
|
|
BufferedImage img = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB);
|
|
Graphics2D g = (Graphics2D) img.getGraphics();
|
|
|
|
int rightBorderX = WIDTH - BORDER_WIDTH;
|
|
int middleBorderY = BORDER_HEIGHT + TOP_BOX_HEIGHT;
|
|
int bottomBorderY = HEIGHT - BOTTOM_HEIGHT;
|
|
|
|
// Draw all the corners
|
|
g.drawImage(topLeftCorner, 0, 0, this);
|
|
g.drawImage(topRightCorner, rightBorderX, 0, this);
|
|
g.drawImage(middleLeftCorner, 0, middleBorderY, this);
|
|
g.drawImage(middleRightCorner, rightBorderX, middleBorderY, this);
|
|
g.drawImage(bottomLeftCorner, 0, bottomBorderY, this);
|
|
g.drawImage(bottomRightCorner, rightBorderX, bottomBorderY, this);
|
|
|
|
// Draw all the horizontal borders
|
|
for (int x = BORDER_WIDTH; x < rightBorderX; x++) {
|
|
g.drawImage(topBorder, x, 0, this);
|
|
g.drawImage(middleBorder, x, middleBorderY, this);
|
|
g.drawImage(bottomBorder, x, bottomBorderY, this);
|
|
}
|
|
// Draw the top set of vertical borders
|
|
for (int y = BORDER_HEIGHT; y < middleBorderY; y++) {
|
|
g.drawImage(topLeftBorder, 0, y, this);
|
|
g.drawImage(topRightBorder, rightBorderX, y, this);
|
|
}
|
|
// Draw the bottom set of vertical borders
|
|
for (int y = middleBorderY + BORDER_HEIGHT; y < bottomBorderY; y++) {
|
|
g.drawImage(bottomLeftBorder, 0, y, this);
|
|
g.drawImage(bottomRightBorder, rightBorderX, y, this);
|
|
}
|
|
|
|
// Get the background color out of the tileset and fill the box with it
|
|
g.setColor(new Color(tileset.getRGB(TS_COLOR_X, TS_MISC_Y)));
|
|
g.fillRect(BORDER_WIDTH, BORDER_HEIGHT, WIDTH - 2 * BORDER_WIDTH, TOP_BOX_HEIGHT);
|
|
|
|
g.dispose();
|
|
return img;
|
|
}
|
|
|
|
// Helper for placing components in the null layout at specific coordinates
|
|
private void placeComponent(JComponent c, int x, int y, Insets insets) {
|
|
add(c);
|
|
c.setLocation(x + insets.left, y + insets.top);
|
|
c.setSize(c.getPreferredSize());
|
|
}
|
|
|
|
// Stop the moving parts after a win or a loss
|
|
private void endGame() {
|
|
TIMER.stop();
|
|
gameEnded = true;
|
|
for (Tile[] row : BOARD)
|
|
for (Tile tile : row)
|
|
tile.gameEnd();
|
|
}
|
|
|
|
// Override the paintComponent method in order to add the background image
|
|
@Override
|
|
public void paintComponent(Graphics gr) {
|
|
super.paintComponent(gr);
|
|
Graphics2D g = (Graphics2D) gr;
|
|
|
|
g.drawImage(backgroundImage, 0, 0, this);
|
|
}
|
|
}
|