javasweeper/Canvas.java

385 lines
12 KiB
Java
Raw Normal View History

2023-05-21 15:13:45 -07:00
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import java.io.*;
import javax.imageio.*;
import javax.swing.*;
public class Canvas extends JPanel {
public static final int TS_WIDTH = 144;
public static final int TS_HEIGHT = 122;
public static final int TS_DIGITS_Y = 33;
public static final int TS_FACES_Y = 55;
public static final int TS_MISC_Y = 82;
public static final int TS_DIGIT_BACKDROP_X = 28;
public static final int TS_COLOR_X = 70;
public static final int BORDER_WIDTH = 12;
public static final int BORDER_HEIGHT = 11;
public static final int BOTTOM_HEIGHT = 12;
public static final int TOP_BOX_HEIGHT = 33;
public static final int TOP_PADDING = 15;
public static final int FLAGS_PADDING = 16;
public static final int TIMER_PADDING = 59;
public final int ROWS;
public final int COLS;
public final int MINES;
public final int WIDTH;
public final int HEIGHT;
private final JPanel BOARD_PANEL;
private final NumberDisplay FLAGS_DISPLAY;
private final NumberDisplay TIMER_DISPLAY;
private final Face FACE;
private final KeyListener SHIFT_KEY_LISTENER;
private final Timer TIMER;
private final Tile[][] BOARD;
private final Image BACKGROUND_IMAGE;
// The tile that the mouse is currently over
private Tile currentTile;
private boolean gameStarted;
private boolean gameEnded;
private boolean gameLost;
private boolean holdingShift;
private boolean leftMouseDown;
private boolean rightMouseDown;
private int numTilesLeft;
private int numFlagsLeft;
private int time;
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;
addKeyListener(SHIFT_KEY_LISTENER);
FLAGS_DISPLAY.setNum(numFlagsLeft);
TIMER_DISPLAY.setNum(time);
}
2023-05-24 17:34:20 -07:00
public Canvas(File skin) {
Difficulty difficulty = Options.getDifficulty();
ROWS = difficulty.getRows();
COLS = difficulty.getCols();
MINES = difficulty.getMines();
2023-05-21 15:13:45 -07:00
WIDTH = Tile.SIZE * COLS + 2 * BORDER_WIDTH;
HEIGHT = Tile.SIZE * ROWS + 2 * BORDER_HEIGHT + BOTTOM_HEIGHT + TOP_BOX_HEIGHT;
BufferedImage tileset;
try {
tileset = ImageIO.read(skin);
// 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);
}
BACKGROUND_IMAGE = getBackgroundImage(tileset);
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);
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_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]);
}
}
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);
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));
2023-05-24 17:34:20 -07:00
SHIFT_KEY_LISTENER = new KeyAdapter() {
2023-05-21 15:13:45 -07:00
// 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();
}
}
@Override
public void keyReleased(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_SHIFT) {
holdingShift = false;
updateCurrentTile();
}
}
private void updateCurrentTile() {
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;
}
Sound.TICK.play();
TIMER_DISPLAY.setNum(++time);
}
});
// Null layout, for manual 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();
}
public void stop() {
TIMER.stop();
removeKeyListener(SHIFT_KEY_LISTENER);
}
public void restart() {
if (!gameStarted)
return;
stop();
init();
2023-05-24 17:34:20 -07:00
2023-05-21 15:13:45 -07:00
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();
}
public void postRevealCheck() {
if (gameLost) {
Sound.EXPLODE.play();
stopGame();
FACE.setFace(Face.DEAD_INDEX);
} else if (numTilesLeft == 0) {
Sound.WIN.play();
// Automatically flag all remaining tiles on win
FLAGS_DISPLAY.setNum(0);
stopGame();
FACE.setFace(Face.COOL_INDEX);
}
}
public void modifyFlagCount(boolean addedFlag) {
if (addedFlag)
numFlagsLeft--;
else
numFlagsLeft++;
FLAGS_DISPLAY.setNum(numFlagsLeft);
}
public void revealedSingleTile() {
numTilesLeft--;
}
public void setLoseFlag() {
gameLost = true;
}
public void setLeftMouseDown(boolean b) {
if (gameEnded)
return;
leftMouseDown = b;
if (leftMouseDown)
FACE.setFace(Face.SHOCKED_INDEX);
else
FACE.setFace(Face.HAPPY_INDEX);
}
public void setRightMouseDown(boolean b) {
rightMouseDown = b;
}
public boolean isLeftMouseDown() {
return leftMouseDown;
}
public boolean isChording() {
return holdingShift || rightMouseDown;
}
public Tile getCurrentTile() {
return currentTile;
}
public void setCurrentTile(Tile tile) {
currentTile = tile;
}
public boolean isGameLost() {
return gameLost;
}
private static 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];
int x = 0;
for (int i = 0; i <= lastIndex; 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);
2023-05-24 17:34:20 -07:00
Graphics2D g = (Graphics2D) img.getGraphics();
2023-05-21 15:13:45 -07:00
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);
2023-05-24 17:34:20 -07:00
c.setLocation(x + insets.left, y + insets.top);
c.setSize(c.getPreferredSize());
2023-05-21 15:13:45 -07:00
}
private void stopGame() {
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 g) {
2023-05-24 17:34:20 -07:00
super.paintComponent(g);
2023-05-21 15:13:45 -07:00
g.drawImage(BACKGROUND_IMAGE, 0, 0, this);
}
}