Init
This commit is contained in:
commit
5e9d532b95
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
*.class
|
||||
.breakpoints
|
||||
.replit
|
||||
replit.nix
|
BIN
Audio/explode.wav
Normal file
BIN
Audio/explode.wav
Normal file
Binary file not shown.
BIN
Audio/silent.wav
Normal file
BIN
Audio/silent.wav
Normal file
Binary file not shown.
BIN
Audio/tick.wav
Normal file
BIN
Audio/tick.wav
Normal file
Binary file not shown.
BIN
Audio/win.wav
Normal file
BIN
Audio/win.wav
Normal file
Binary file not shown.
385
Canvas.java
Normal file
385
Canvas.java
Normal file
|
@ -0,0 +1,385 @@
|
|||
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);
|
||||
}
|
||||
|
||||
public Canvas(int rows, int cols, int mines, File skin) {
|
||||
ROWS = rows;
|
||||
COLS = cols;
|
||||
MINES = mines;
|
||||
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));
|
||||
|
||||
SHIFT_KEY_LISTENER = new KeyListener() {
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void keyTyped(KeyEvent e) {
|
||||
}
|
||||
|
||||
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();
|
||||
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);
|
||||
Graphics g = 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);
|
||||
Dimension size = c.getPreferredSize();
|
||||
c.setBounds(x + insets.left, y + insets.top, size.width, size.height);
|
||||
}
|
||||
|
||||
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) {
|
||||
g.drawImage(BACKGROUND_IMAGE, 0, 0, this);
|
||||
}
|
||||
}
|
73
Face.java
Normal file
73
Face.java
Normal file
|
@ -0,0 +1,73 @@
|
|||
import java.awt.*;
|
||||
import java.awt.event.*;
|
||||
import javax.swing.*;
|
||||
|
||||
public class Face extends JComponent {
|
||||
public static final int SIZE = 26;
|
||||
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;
|
||||
|
||||
private final Canvas GAME_CANVAS;
|
||||
private final Image[] FACES;
|
||||
private int face;
|
||||
private int unpressedFace;
|
||||
private boolean pressed;
|
||||
private boolean mouseOver;
|
||||
|
||||
public Face(Canvas gameCanvas, Image[] faces) {
|
||||
FACES = faces;
|
||||
GAME_CANVAS = gameCanvas;
|
||||
face = HAPPY_INDEX;
|
||||
pressed = false;
|
||||
mouseOver = false;
|
||||
setPreferredSize(new Dimension(SIZE, SIZE));
|
||||
addMouseListener(new MouseListener() {
|
||||
@Override
|
||||
public void mousePressed(MouseEvent e) {
|
||||
pressed = true;
|
||||
unpressedFace = face;
|
||||
setFace(PRESSED_INDEX);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseReleased(MouseEvent e) {
|
||||
pressed = false;
|
||||
if (mouseOver) {
|
||||
setFace(HAPPY_INDEX);
|
||||
GAME_CANVAS.restart();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseEntered(MouseEvent e) {
|
||||
mouseOver = true;
|
||||
if (pressed)
|
||||
setFace(PRESSED_INDEX);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseExited(MouseEvent e) {
|
||||
mouseOver = false;
|
||||
if (pressed)
|
||||
setFace(unpressedFace);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void setFace(int face) {
|
||||
this.face = face;
|
||||
repaint();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void paintComponent(Graphics g) {
|
||||
g.drawImage(FACES[face], 0, 0, this);
|
||||
}
|
||||
}
|
274
Main.java
Normal file
274
Main.java
Normal file
|
@ -0,0 +1,274 @@
|
|||
import java.awt.event.*;
|
||||
import java.io.*;
|
||||
import java.nio.file.*;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.Arrays;
|
||||
import javax.swing.*;
|
||||
import javax.swing.event.*;
|
||||
|
||||
public class Main {
|
||||
private static enum Difficulty {
|
||||
BEGINNER (9, 9, 10),
|
||||
INTERMEDIATE (16, 16, 40),
|
||||
EXPERT (16, 30, 99);
|
||||
|
||||
public final int ROWS;
|
||||
public final int COLS;
|
||||
public final int MINES;
|
||||
|
||||
Difficulty(int rows, int cols, int mines) {
|
||||
this.ROWS = rows;
|
||||
this.COLS = cols;
|
||||
this.MINES = mines;
|
||||
}
|
||||
}
|
||||
|
||||
public static final String DEFAULT_SKIN = "winxpskin.bmp";
|
||||
public static final String SKINS_DIR = "Skins/";
|
||||
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"};
|
||||
|
||||
private static final JFrame FRAME = new JFrame("Minesweeper");
|
||||
|
||||
private static Canvas canvas;
|
||||
private static Difficulty difficulty;
|
||||
private static String skin;
|
||||
private static boolean protectedStart;
|
||||
private static boolean sound;
|
||||
|
||||
public static void main(String[] args) {
|
||||
difficulty = Difficulty.INTERMEDIATE;
|
||||
skin = DEFAULT_SKIN;
|
||||
protectedStart = false;
|
||||
sound = false;
|
||||
setCanvas();
|
||||
|
||||
FRAME.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
||||
FRAME.setResizable(false);
|
||||
FRAME.setJMenuBar(getMenuBar());
|
||||
FRAME.add(canvas);
|
||||
FRAME.pack();
|
||||
FRAME.setVisible(true);
|
||||
}
|
||||
|
||||
public static boolean isProtectedStart() {
|
||||
return protectedStart;
|
||||
}
|
||||
|
||||
public static boolean hasSound() {
|
||||
return sound;
|
||||
}
|
||||
|
||||
private static JMenuBar getMenuBar() {
|
||||
ActionListener listener = new ActionListener() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
switch (e.getActionCommand()) {
|
||||
case "new":
|
||||
canvas.restart();
|
||||
break;
|
||||
case "beginner":
|
||||
setDifficulty(Difficulty.BEGINNER);
|
||||
break;
|
||||
case "intermediate":
|
||||
setDifficulty(Difficulty.INTERMEDIATE);
|
||||
break;
|
||||
case "expert":
|
||||
setDifficulty(Difficulty.EXPERT);
|
||||
break;
|
||||
case "custom":
|
||||
setDifficulty(null);
|
||||
break;
|
||||
case "protectedstart":
|
||||
protectedStart = !protectedStart;
|
||||
break;
|
||||
case "sound":
|
||||
sound = !sound;
|
||||
Sound.initSounds();
|
||||
break;
|
||||
case "exit":
|
||||
FRAME.dispatchEvent(new WindowEvent(FRAME, WindowEvent.WINDOW_CLOSING));
|
||||
break;
|
||||
case "help":
|
||||
messageHtmlFile(HELP_FILE, "Failed to read help file, just google it lol.",
|
||||
"Minesweeper Help", JOptionPane.QUESTION_MESSAGE);
|
||||
break;
|
||||
case "about":
|
||||
messageHtmlFile(ABOUT_FILE, "I love microsoft!!",
|
||||
"Minesweeper About", JOptionPane.INFORMATION_MESSAGE);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
JMenuBar menuBar = new JMenuBar();
|
||||
|
||||
JMenu gameMenu = new JMenu("Game");
|
||||
gameMenu.setMnemonic(KeyEvent.VK_G);
|
||||
JMenuItem newGameItem = new JMenuItem("New game");
|
||||
newGameItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F2, 0));
|
||||
addMenuItem(newGameItem, gameMenu, KeyEvent.VK_N, "new", listener);
|
||||
gameMenu.addSeparator();
|
||||
ButtonGroup difficultyButtons = new ButtonGroup();
|
||||
JRadioButtonMenuItem beginnerItem = new JRadioButtonMenuItem("Beginner");
|
||||
JRadioButtonMenuItem intermediateItem = new JRadioButtonMenuItem("Intermediate", true);
|
||||
JRadioButtonMenuItem expertItem = new JRadioButtonMenuItem("Advanced");
|
||||
JRadioButtonMenuItem customItem = new JRadioButtonMenuItem("Custom...");
|
||||
difficultyButtons.add(beginnerItem);
|
||||
difficultyButtons.add(intermediateItem);
|
||||
difficultyButtons.add(expertItem);
|
||||
difficultyButtons.add(customItem);
|
||||
addMenuItem(beginnerItem, gameMenu, KeyEvent.VK_B, "beginner", listener);
|
||||
addMenuItem(intermediateItem, gameMenu, KeyEvent.VK_I, "intermediate", listener);
|
||||
addMenuItem(expertItem, gameMenu, KeyEvent.VK_E, "expert", listener);
|
||||
addMenuItem(customItem, gameMenu, KeyEvent.VK_C, "custom", listener);
|
||||
gameMenu.addSeparator();
|
||||
JCheckBoxMenuItem protectedStartItem = new JCheckBoxMenuItem("Protected Start", protectedStart);
|
||||
protectedStartItem.setToolTipText("Guarantees the starting tile has no mines next to it");
|
||||
addMenuItem(protectedStartItem, gameMenu, KeyEvent.VK_P, "protectedstart", listener);
|
||||
JCheckBoxMenuItem soundItem = new JCheckBoxMenuItem("Sound", sound);
|
||||
addMenuItem(soundItem, gameMenu, KeyEvent.VK_O, "sound", listener);
|
||||
gameMenu.addSeparator();
|
||||
addMenuItem("Exit", gameMenu, KeyEvent.VK_X, "exit", listener);
|
||||
menuBar.add(gameMenu);
|
||||
|
||||
JMenu helpMenu = new JMenu("Help");
|
||||
helpMenu.setMnemonic(KeyEvent.VK_H);
|
||||
JMenuItem helpItem = new JMenuItem("Help");
|
||||
helpItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F1, 0));
|
||||
addMenuItem(helpItem, helpMenu, KeyEvent.VK_H, "help", listener);
|
||||
helpMenu.addSeparator();
|
||||
addMenuItem("About", helpMenu, KeyEvent.VK_A, "about", listener);
|
||||
menuBar.add(helpMenu);
|
||||
|
||||
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
|
||||
skinsMenu.addMenuListener(new MenuListener() {
|
||||
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))
|
||||
return;
|
||||
|
||||
skin = newSkin;
|
||||
replaceCanvas();
|
||||
}
|
||||
};
|
||||
|
||||
@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))) {
|
||||
dirStream
|
||||
// Filter for only regular files
|
||||
.filter(file -> Files.isRegularFile(file))
|
||||
// Cut off the directory name from the resulting string
|
||||
.map(Path::getFileName)
|
||||
.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
|
||||
.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));
|
||||
} catch (IOException ignore) {
|
||||
// It's fine to leave it blank if we can't get results
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void menuDeselected(MenuEvent e) {
|
||||
// Remove skins when we're done so they don't duplicate every
|
||||
// time we open the menu
|
||||
skinsMenu.removeAll();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void menuCanceled(MenuEvent e) {
|
||||
}
|
||||
});
|
||||
menuBar.add(skinsMenu);
|
||||
|
||||
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) {
|
||||
if (newDifficulty == null);
|
||||
else if (difficulty == newDifficulty)
|
||||
return;
|
||||
|
||||
difficulty = newDifficulty;
|
||||
replaceCanvas();
|
||||
}
|
||||
|
||||
private static void addMenuItem(String menuTitle, JMenu menu, int mnemonic, String actionCommand,
|
||||
ActionListener listener) {
|
||||
addMenuItem(new JMenuItem(menuTitle), menu, mnemonic, actionCommand, listener);
|
||||
}
|
||||
|
||||
private static void addMenuItem(JMenuItem menuItem, JMenu menu, int mnemonic, String actionCommand,
|
||||
ActionListener listener) {
|
||||
menuItem.setMnemonic(mnemonic);
|
||||
addMenuItem(menuItem, menu, actionCommand, listener);
|
||||
}
|
||||
|
||||
private static void addMenuItem(JMenuItem menuItem, JMenu menu, String actionCommand,
|
||||
ActionListener listener) {
|
||||
menuItem.setActionCommand(actionCommand);
|
||||
menuItem.addActionListener(listener);
|
||||
menu.add(menuItem);
|
||||
}
|
||||
|
||||
private static void replaceCanvas() {
|
||||
canvas.stop();
|
||||
canvas.removeAll();
|
||||
FRAME.remove(canvas);
|
||||
setCanvas();
|
||||
}
|
||||
|
||||
private static void setCanvas() {
|
||||
canvas = getCanvas();
|
||||
FRAME.add(canvas);
|
||||
FRAME.pack();
|
||||
}
|
||||
|
||||
private static Canvas getCanvas() {
|
||||
File skinFile = new File(SKINS_DIR, skin);
|
||||
if (difficulty == null) {
|
||||
// TODO: Make the difficulty actually change
|
||||
return new Canvas(40, 50, 500, skinFile);
|
||||
}
|
||||
return new Canvas(difficulty.ROWS, difficulty.COLS, difficulty.MINES, skinFile);
|
||||
}
|
||||
|
||||
// No construction >:(
|
||||
private Main() {
|
||||
}
|
||||
}
|
58
NumberDisplay.java
Normal file
58
NumberDisplay.java
Normal file
|
@ -0,0 +1,58 @@
|
|||
import java.awt.*;
|
||||
import javax.swing.*;
|
||||
|
||||
public class NumberDisplay extends JComponent {
|
||||
public static final int DIGIT_WIDTH = 11;
|
||||
public static final int DIGIT_HEIGHT = 21;
|
||||
public static final int BACKDROP_WIDTH = 41;
|
||||
public static final int BACKDROP_HEIGHT = 25;
|
||||
public static final int MINUS_INDEX = 10;
|
||||
|
||||
private static final int[] DIGIT_X = {28, 15, 2};
|
||||
private static final int DIGIT_Y = 2;
|
||||
|
||||
public final Image BACKDROP;
|
||||
public final Image[] DIGITS;
|
||||
|
||||
private int num;
|
||||
private boolean negative;
|
||||
|
||||
public NumberDisplay(Image backdrop, Image[] digits) {
|
||||
BACKDROP = backdrop;
|
||||
DIGITS = digits;
|
||||
setPreferredSize(new Dimension(BACKDROP_WIDTH, BACKDROP_HEIGHT));
|
||||
}
|
||||
|
||||
public void setNum(int num) {
|
||||
setClamped(num);
|
||||
repaint();
|
||||
}
|
||||
|
||||
private void setClamped(int num) {
|
||||
// Clamp all number assignments between -99 and 999, inclusive
|
||||
if (num < 0) {
|
||||
this.num = Math.min(-num, 99);
|
||||
negative = true;
|
||||
} else {
|
||||
this.num = Math.min(num, 999);
|
||||
negative = false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void paintComponent(Graphics g) {
|
||||
g.drawImage(BACKDROP, 0, 0, this);
|
||||
// Preserve the original num for divison, in case we repaint twice
|
||||
// without updating the number
|
||||
int num = this.num;
|
||||
for (int i = 0; i < 3; i++) {
|
||||
int digitsIndex;
|
||||
if (negative && i == 2)
|
||||
digitsIndex = MINUS_INDEX;
|
||||
else
|
||||
digitsIndex = num % 10;
|
||||
g.drawImage(DIGITS[digitsIndex], DIGIT_X[i], DIGIT_Y, this);
|
||||
num /= 10;
|
||||
}
|
||||
}
|
||||
}
|
44
Skins/Notes.txt
Normal file
44
Skins/Notes.txt
Normal file
|
@ -0,0 +1,44 @@
|
|||
With the exception of the following 2 skins, all other skins here are curated
|
||||
from the skins pack at https://minesweepergame.com/download/minesweeper-x.php
|
||||
|
||||
winxpblindskin
|
||||
|
||||
Below is the original Notes.txt that came with the Minesweeper X Skins Pack
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
Minesweeper X Skins
|
||||
|
||||
The following skins are included in this pack:
|
||||
|
||||
winbwskin Curtis Bright inspired by Windows 3.1 monochrome
|
||||
win98skin Curtis Bright inspired by Windows 98
|
||||
winxpskin Curtis Bright inspired by Windows XP
|
||||
|
||||
cloneskin Rodrigo Camargo inspired by Minesweeper Clone
|
||||
coloursonlyskin Curtis Bright
|
||||
hibbelerskin Nicholas Hibbeler
|
||||
icicleskin Aryeh Draeger
|
||||
marioskin Jonathan Aldrich
|
||||
mavizskin Mantas Vykertas
|
||||
mineskin Philip Jovim
|
||||
narkomaniaskin Kostas Symeonidis inspired by Narkomania
|
||||
oceanskin C. L.
|
||||
pacmanskin Jonathan Aldrich
|
||||
predatorskin Dmitriy Sukhomlynov inspired by Arbiter
|
||||
scratchskin Kris Decker
|
||||
symbolskin Tobias Banzhaf
|
||||
unknownskin Unknown
|
||||
|
||||
vistagreenflowerskin Mohammad Hosseyni 2011 inspired by Windows Vista
|
||||
vistagreenmineskin Mohammad Hosseyni 2011 inspired by Windows Vista
|
||||
vistablueflowerskin Mohammad Hosseyni 2011 inspired by Windows Vista
|
||||
vistabluemineskin Mohammad Hosseyni 2011 inspired by Windows Vista
|
||||
|
||||
greenfarm Josh Scorpius 2021
|
||||
classicblue Josh Scorpius 2021
|
||||
redgarden Josh Scorpius 2021
|
||||
yellowrings Josh Scorpius 2021
|
||||
aquarium Josh Scorpius 2021
|
||||
|
||||
|
BIN
Skins/cloneskin.bmp
Normal file
BIN
Skins/cloneskin.bmp
Normal file
Binary file not shown.
After Width: | Height: | Size: 52 KiB |
BIN
Skins/coloursonlyskin.bmp
Normal file
BIN
Skins/coloursonlyskin.bmp
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.7 KiB |
BIN
Skins/win98skin.bmp
Normal file
BIN
Skins/win98skin.bmp
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.7 KiB |
BIN
Skins/winbwskin.bmp
Normal file
BIN
Skins/winbwskin.bmp
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.7 KiB |
BIN
Skins/winxpblindskin.bmp
Normal file
BIN
Skins/winxpblindskin.bmp
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.8 KiB |
BIN
Skins/winxpskin.bmp
Normal file
BIN
Skins/winxpskin.bmp
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.7 KiB |
141
Sound.java
Normal file
141
Sound.java
Normal file
|
@ -0,0 +1,141 @@
|
|||
import java.io.*;
|
||||
import javax.sound.sampled.*;
|
||||
|
||||
public enum Sound {
|
||||
// The available sound filenames
|
||||
WIN("win.wav"),
|
||||
TICK("tick.wav"),
|
||||
EXPLODE("explode.wav"),
|
||||
SILENT("silent.wav");
|
||||
|
||||
// The directory all the audio files are in
|
||||
public static final String AUDIO_DIR = "Audio";
|
||||
|
||||
// Named pipe to send audio requests to on Replit
|
||||
public static final File REPLIT_PIPE = new File("/tmp/audio");
|
||||
|
||||
// Use env vars and the existance of the above pipe to determine if running inside Replit
|
||||
public static final boolean REPLIT_API = System.getenv("REPL_ID") != null && REPLIT_PIPE.exists();
|
||||
|
||||
// Whether the clips have been initialized yet
|
||||
private static boolean soundsInitialized = false;
|
||||
|
||||
// File containing the sound
|
||||
private final File FILE;
|
||||
|
||||
// SoundBackend that will play the sound
|
||||
private SoundBackend soundBackend;
|
||||
|
||||
// Because Replit does audio totally differently from desktop Java (it
|
||||
// supposedly supports PulseAudio over VNC but I just could not get that
|
||||
// working), and I would like to still be able to hear things when I play
|
||||
// the game directly on my laptop, we have a SoundBackend interface that is
|
||||
// implemented by both a builtin javax.sound.sampled-powered backend and a
|
||||
// Replit specific one
|
||||
private interface SoundBackend {
|
||||
// plays the specific sound
|
||||
public void play();
|
||||
}
|
||||
|
||||
private class NativeBackend implements SoundBackend {
|
||||
// The audio clip associated with the sound
|
||||
private final Clip CLIP;
|
||||
|
||||
public NativeBackend() {
|
||||
// Create the clip, if possible
|
||||
Clip clip;
|
||||
try (AudioInputStream audioIn = AudioSystem.getAudioInputStream(FILE)) {
|
||||
clip = AudioSystem.getClip();
|
||||
clip.open(audioIn);
|
||||
} catch (Exception e) {
|
||||
// If it didn't work, don't leave a broken clip behind that
|
||||
// would try to get played
|
||||
clip = null;
|
||||
}
|
||||
CLIP = clip;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void play() {
|
||||
// Don't play if the clip couldn't be initialized
|
||||
if (CLIP == null)
|
||||
return;
|
||||
|
||||
// Rewind the clip and play it
|
||||
CLIP.stop();
|
||||
CLIP.setFramePosition(0);
|
||||
CLIP.start();
|
||||
}
|
||||
}
|
||||
|
||||
private class ReplitBackend implements SoundBackend {
|
||||
// The JSON format to send requests to Replit with; the volume is 0.1
|
||||
// because my ears got blown out the first time I exploded when it was
|
||||
// at 100% volume (Replit is LOUD)
|
||||
private static final String REQUEST_FORMAT = """
|
||||
{
|
||||
"Volume": 0.1,
|
||||
"Type": "wav",
|
||||
"Args": {
|
||||
"Path": "%s"
|
||||
}
|
||||
}""";
|
||||
|
||||
// The actual request string to send
|
||||
private final String REQUEST;
|
||||
|
||||
public ReplitBackend() {
|
||||
REQUEST = String.format(REQUEST_FORMAT, FILE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void play() {
|
||||
// Attempt to send the request
|
||||
try (FileWriter writer = new FileWriter(REPLIT_PIPE)) {
|
||||
writer.write(REQUEST);
|
||||
} catch (IOException ignore) {
|
||||
// If it didn't work, no biggie, just don't play anything
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Sound(String fileName) {
|
||||
FILE = new File(AUDIO_DIR, fileName);
|
||||
}
|
||||
|
||||
// Initialize the backends for all sounds, if they haven't already been
|
||||
// initialized yet
|
||||
public static void initSounds() {
|
||||
if (soundsInitialized)
|
||||
return;
|
||||
|
||||
soundsInitialized = true;
|
||||
|
||||
if (REPLIT_API) {
|
||||
for (Sound sound : values())
|
||||
sound.soundBackend = sound.new ReplitBackend();
|
||||
|
||||
// HACK: Play an audible sound immediately, so the popup message
|
||||
// happens now rather than in the middle of a game; SILENT doesn't
|
||||
// work here, but the popup will block the first sound anyways so it
|
||||
// doesn't matter
|
||||
TICK.play();
|
||||
return;
|
||||
}
|
||||
|
||||
for (Sound sound : values())
|
||||
sound.soundBackend = sound.new NativeBackend();
|
||||
|
||||
// HACK: Play a silent sound as soon as possible to eagerly load all the
|
||||
// sound stuff now to prevent lag when the first real sound is played
|
||||
// (yes this actually helps, I tested it)
|
||||
SILENT.play();
|
||||
}
|
||||
|
||||
// The actual method that users outside this file will call to play sound
|
||||
public void play() {
|
||||
// Here is the check for if the option for sound is actually enabled
|
||||
if (Main.hasSound())
|
||||
soundBackend.play();
|
||||
}
|
||||
}
|
254
Tile.java
Normal file
254
Tile.java
Normal file
|
@ -0,0 +1,254 @@
|
|||
import java.awt.*;
|
||||
import java.util.HashSet;
|
||||
import java.awt.event.*;
|
||||
import javax.swing.*;
|
||||
|
||||
public class Tile extends JComponent {
|
||||
public static final int SIZE = 16;
|
||||
public static final int REGULAR_INDEX = 0;
|
||||
public static final int PRESSED_INDEX = 1;
|
||||
public static final int MINE_INDEX = 2;
|
||||
public static final int FLAGGED_INDEX = 3;
|
||||
public static final int NOT_MINE_INDEX = 4;
|
||||
public static final int EXPLODED_MINE_INDEX = 5;
|
||||
|
||||
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;
|
||||
|
||||
// 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
|
||||
private boolean revealed;
|
||||
// Whether or not this mine has been flagged
|
||||
private boolean flagged;
|
||||
private boolean pressed;
|
||||
private boolean mouseOver;
|
||||
private boolean gameEnded;
|
||||
|
||||
private void init() {
|
||||
mine = false;
|
||||
num = 0;
|
||||
revealed = false;
|
||||
flagged = false;
|
||||
pressed = false;
|
||||
mouseOver = false;
|
||||
gameEnded = false;
|
||||
addMouseListener(TILE_MOUSE_LISTENER);
|
||||
}
|
||||
|
||||
public Tile(Canvas gameCanvas, Image[] numberTiles, Image[] specialTiles) {
|
||||
GAME_CANVAS = gameCanvas;
|
||||
NUMBER_TILES = numberTiles;
|
||||
SPECIAL_TILES = specialTiles;
|
||||
ADJACENT_TILES = new HashSet<Tile>();
|
||||
|
||||
TILE_MOUSE_LISTENER = new MouseListener() {
|
||||
@Override
|
||||
public void mousePressed(MouseEvent e) {
|
||||
boolean rightClicked = false;
|
||||
switch (e.getButton()) {
|
||||
case MouseEvent.BUTTON1:
|
||||
GAME_CANVAS.setLeftMouseDown(true);
|
||||
break;
|
||||
case MouseEvent.BUTTON3:
|
||||
rightClicked = true;
|
||||
GAME_CANVAS.setRightMouseDown(true);
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
Tile tile = GAME_CANVAS.getCurrentTile();
|
||||
if (tile == null)
|
||||
return;
|
||||
|
||||
if (rightClicked)
|
||||
tile.rightClickAction();
|
||||
tile.updatePressedState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseReleased(MouseEvent e) {
|
||||
boolean leftClicked = false;
|
||||
switch (e.getButton()) {
|
||||
case MouseEvent.BUTTON1:
|
||||
leftClicked = true;
|
||||
GAME_CANVAS.setLeftMouseDown(false);
|
||||
break;
|
||||
case MouseEvent.BUTTON3:
|
||||
GAME_CANVAS.setRightMouseDown(false);
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
Tile tile = GAME_CANVAS.getCurrentTile();
|
||||
if (tile == null)
|
||||
return;
|
||||
|
||||
if (leftClicked)
|
||||
tile.leftClickAction();
|
||||
tile.updatePressedState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseEntered(MouseEvent e) {
|
||||
GAME_CANVAS.setCurrentTile(Tile.this);
|
||||
mouseOver = true;
|
||||
updatePressedState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseExited(MouseEvent e) {
|
||||
if (GAME_CANVAS.getCurrentTile() == Tile.this)
|
||||
GAME_CANVAS.setCurrentTile(null);
|
||||
mouseOver = false;
|
||||
updatePressedState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
}
|
||||
};
|
||||
|
||||
setPreferredSize(new Dimension(SIZE, SIZE));
|
||||
|
||||
init();
|
||||
}
|
||||
|
||||
public void restart() {
|
||||
removeMouseListener(TILE_MOUSE_LISTENER);
|
||||
init();
|
||||
}
|
||||
|
||||
public void addAdjacentTile(Tile tile) {
|
||||
ADJACENT_TILES.add(tile);
|
||||
}
|
||||
|
||||
// At the start of the game, place a mine at this tile, unless it is
|
||||
// adjacent to or is the starting tile, or is already a mine. Returns
|
||||
// whether or not a mine was actually placed.
|
||||
public boolean makeMine(Tile startTile) {
|
||||
if (mine || this == startTile || Main.isProtectedStart() && ADJACENT_TILES.contains(startTile))
|
||||
return false;
|
||||
|
||||
mine = true;
|
||||
// Increment the number on the adjacent tiles when a new mine is decided
|
||||
for (Tile tile : ADJACENT_TILES)
|
||||
tile.num++;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void gameEnd() {
|
||||
removeMouseListener(TILE_MOUSE_LISTENER);
|
||||
gameEnded = true;
|
||||
// Only repaint the tiles that actually change
|
||||
if (mine || flagged)
|
||||
repaint();
|
||||
}
|
||||
|
||||
public void updatePressedState() {
|
||||
if (pressed != (GAME_CANVAS.isLeftMouseDown() && mouseOver)) {
|
||||
pressed = !pressed;
|
||||
repaint();
|
||||
}
|
||||
boolean pressAdjacent = pressed && GAME_CANVAS.isChording();
|
||||
for (Tile tile : ADJACENT_TILES) {
|
||||
if (tile.pressed != pressAdjacent) {
|
||||
tile.pressed = pressAdjacent;
|
||||
tile.repaint();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void rightClickAction() {
|
||||
if (revealed)
|
||||
return;
|
||||
|
||||
flagged = !flagged;
|
||||
GAME_CANVAS.modifyFlagCount(flagged);
|
||||
repaint();
|
||||
}
|
||||
|
||||
private void leftClickAction() {
|
||||
if (GAME_CANVAS.isChording()) {
|
||||
if (!revealed)
|
||||
return;
|
||||
|
||||
int adjacentFlags = 0;
|
||||
for (Tile tile : ADJACENT_TILES)
|
||||
if (tile.flagged)
|
||||
adjacentFlags++;
|
||||
if (adjacentFlags != num)
|
||||
return;
|
||||
|
||||
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();
|
||||
}
|
||||
GAME_CANVAS.postRevealCheck();
|
||||
}
|
||||
|
||||
private void reveal() {
|
||||
if (revealed)
|
||||
return;
|
||||
|
||||
revealed = true;
|
||||
repaint();
|
||||
|
||||
if (mine) {
|
||||
GAME_CANVAS.setLoseFlag();
|
||||
} else {
|
||||
GAME_CANVAS.revealedSingleTile();
|
||||
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];
|
||||
if (revealed)
|
||||
return SPECIAL_TILES[EXPLODED_MINE_INDEX];
|
||||
return SPECIAL_TILES[MINE_INDEX];
|
||||
}
|
||||
if (revealed)
|
||||
return NUMBER_TILES[num];
|
||||
if (flagged) {
|
||||
// Correctly flagged mines would have been caught earlier
|
||||
if (gameEnded)
|
||||
return SPECIAL_TILES[NOT_MINE_INDEX];
|
||||
return SPECIAL_TILES[FLAGGED_INDEX];
|
||||
}
|
||||
// Tiles are only interactable if the game is ongoing
|
||||
if (pressed && !gameEnded)
|
||||
return SPECIAL_TILES[PRESSED_INDEX];
|
||||
return SPECIAL_TILES[REGULAR_INDEX];
|
||||
}
|
||||
|
||||
@Override
|
||||
public void paintComponent(Graphics g) {
|
||||
g.drawImage(getImage(), 0, 0, this);
|
||||
}
|
||||
}
|
27
about.html
Normal file
27
about.html
Normal file
|
@ -0,0 +1,27 @@
|
|||
<html>
|
||||
|
||||
<body style='width: 600px;'>
|
||||
|
||||
<h1>About</h1>
|
||||
|
||||
<p>
|
||||
This is a Minesweeper clone in Java that mirrors Minesweeper for
|
||||
Windows XP, minus the question marks because they are an objectively
|
||||
bad feature that nobody uses and only exist to slow you down when
|
||||
you accidentally flag something. The game uses Minesweeper X,
|
||||
another free Minesweeper clone by Curtis Bright, as reference, and
|
||||
uses the same custom skin format (see the <code>Skins/</code>
|
||||
directory).
|
||||
</p>
|
||||
|
||||
<h2>Credits</h2>
|
||||
|
||||
<ul>
|
||||
<li><b>Skins</b> — refer to <code>Skins/Notes.txt</code></li>
|
||||
<li><b>Audio</b> — directly from Minesweeper for Windows XP</li>
|
||||
<li><b>Everything else</b> — me</li>
|
||||
</ul>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
46
help.html
Normal file
46
help.html
Normal file
|
@ -0,0 +1,46 @@
|
|||
<html>
|
||||
|
||||
<body style='width: 600px;'>
|
||||
|
||||
<h1>Minesweeper</h1>
|
||||
|
||||
<p>
|
||||
Minesweeper is a classic game about sweeping everything except
|
||||
mines. Reveal all the non-mine tiles to win, but attempt to reveal a
|
||||
mine and you won't be having such a good time. Revealed tiles will
|
||||
have a number on them to tell you how many mines are directly
|
||||
adjacent to them. Have fun, and watch out for the mines!
|
||||
</p>
|
||||
|
||||
<br>
|
||||
|
||||
<p>
|
||||
Flag tiles to mark them as mines. Chord numbered tiles to reveal all
|
||||
adjacent non-flagged tiles if there are exactly that many flags
|
||||
adjacent to the tile.
|
||||
</p>
|
||||
|
||||
<h2>Controls</h2>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td><b>New game</b></td>
|
||||
<td>Game->New, or F2, or click the face</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>Reveal tile</b></td>
|
||||
<td>Left click</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>Flag tile</b></td>
|
||||
<td>Right click</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>Chord tile</b></td>
|
||||
<td>Left click while right mouse button or shift is held</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
Loading…
Reference in a new issue