Finished everything minus comments

This commit is contained in:
eriedaberrie 2023-05-24 17:34:20 -07:00
parent 5e9d532b95
commit 1d5ade9136
9 changed files with 388 additions and 119 deletions

View file

@ -67,10 +67,11 @@ public class Canvas extends JPanel {
TIMER_DISPLAY.setNum(time);
}
public Canvas(int rows, int cols, int mines, File skin) {
ROWS = rows;
COLS = cols;
MINES = mines;
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;
@ -127,7 +128,7 @@ public class Canvas extends JPanel {
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_KEY_LISTENER = new KeyAdapter() {
// Shift+LMB chords, so we need some way of determining if the shift
// key is being held.
@Override
@ -146,10 +147,6 @@ public class Canvas extends JPanel {
}
}
@Override
public void keyTyped(KeyEvent e) {
}
private void updateCurrentTile() {
if (currentTile != null)
currentTile.updatePressedState();
@ -196,6 +193,7 @@ public class Canvas extends JPanel {
stop();
init();
for (Tile[] row : BOARD)
for (Tile tile : row)
tile.restart();
@ -323,7 +321,7 @@ public class Canvas extends JPanel {
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();
Graphics2D g = (Graphics2D) img.getGraphics();
int rightBorderX = WIDTH - BORDER_WIDTH;
int middleBorderY = BORDER_HEIGHT + TOP_BOX_HEIGHT;
@ -365,8 +363,8 @@ public class Canvas extends JPanel {
// 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);
c.setLocation(x + insets.left, y + insets.top);
c.setSize(c.getPreferredSize());
}
private void stopGame() {
@ -380,6 +378,7 @@ 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);
}
}

70
CustomTextField.java Normal file
View file

@ -0,0 +1,70 @@
import java.awt.event.*;
import javax.swing.*;
public class CustomTextField extends JTextField {
private static final int COLS = 4;
private final int MIN_VALUE;
private final int MAX_VALUE;
public CustomTextField(int defaultValue, int minValue, int maxValue) {
super(Integer.toString(defaultValue), COLS);
MIN_VALUE = minValue;
MAX_VALUE = maxValue;
addFocusListener(new FocusAdapter() {
// Auto select when focused
@Override
public void focusGained(FocusEvent e) {
selectAll();
}
// Auto format input when unfocused
@Override
public void focusLost(FocusEvent e) {
formatText();
}
});
}
protected void formatText() {
String text = getText();
// If tried to input a negative number, set it to the minimum
if (text.isEmpty() || text.charAt(0) == '-') {
setValue(getMinValue());
return;
}
// Remove all the nondigit characters and all leading zeros
String filteredText = text.replaceAll("(^[^1-9]+|\\D)", "");
if (filteredText.isEmpty()) {
setValue(getMinValue());
return;
}
// To prevent integer overflow when parsing, just set it to max
// when it's more than 4 digits
if (filteredText.length() > 4) {
setValue(getMaxValue());
return;
}
// Now that we finally have the intended int, clamp and set it
setValue(Difficulty.clampInt(Integer.parseInt(filteredText), getMinValue(), getMaxValue()));
}
// Use protected getters and setters so we can override them if necessary
protected int getMinValue() {
return MIN_VALUE;
}
protected int getMaxValue() {
return MAX_VALUE;
}
private void setValue(int value) {
setText(Integer.toString(value));
}
}

179
Difficulty.java Normal file
View file

@ -0,0 +1,179 @@
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public enum Difficulty {
BEGINNER (9, 9, 10),
INTERMEDIATE (16, 16, 40),
EXPERT (16, 30, 99),
CUSTOM;
// Custom difficulty constraints
//
// In actual minesweeper these numbers are 9, 24, 30, 10
private static final int MIN_SIZE = 8;
private static final int MAX_ROWS = 40;
private static final int MAX_COLS = 50;
private static final int MIN_MINES = 10;
// In regular minesweeper the mines are capped at just (rows-1)*(cols-1),
// but we additionally cap it at 999 because the expanded board size means
// it's possible to hit the number display cap
private static final int MAX_MINES = 999;
private int rows;
private int cols;
private int mines;
private Difficulty() {
}
private Difficulty(int rows, int cols, int mines) {
setStats(rows, cols, mines);
}
private void setStats(int rows, int cols, int mines) {
this.rows = rows;
this.cols = cols;
this.mines = mines;
}
public int getRows() {
return rows;
}
public int getCols() {
return cols;
}
public int getMines() {
return mines;
}
public static boolean setCustom(JFrame frame) {
Difficulty current = Options.getDifficulty();
// Use single-item array to store a reference to minesField, because
// minesField needs to needs to be initialized after rowsField and
// colsField but RowsColsField needs minesField
CustomTextField[] minesRef = new CustomTextField[1];
// Override CustomTextField in order to make it format minesField after
// editing (when the board size is decreased, so does the max mine
// count, so we may need to update it)
class RowsColsField extends CustomTextField {
private RowsColsField(int defaultValue, int minValue, int maxValue) {
super(defaultValue, minValue, maxValue);
}
@Override
protected void formatText() {
super.formatText();
minesRef[0].formatText();
}
}
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
// the maximum value based on the number of rows and columns
CustomTextField minesField = minesRef[0] = new CustomTextField(current.mines, MIN_MINES, MAX_MINES) {
@Override
protected int getMaxValue() {
try {
int rows = Integer.parseInt(rowsField.getText());
int cols = Integer.parseInt(colsField.getText());
return getMaxMines(rows, cols);
} catch (NumberFormatException e) {
return super.getMaxValue();
}
}
};
// Because Java is lame and doesn't have builtin tuples we make do with
// casting an JComponent[][]
JComponent[][] items = {
{ new JLabel("Height:"), rowsField },
{ new JLabel("Width:"), colsField },
{ new JLabel("Mines:"), minesField },
};
int option = JOptionPane.showConfirmDialog(frame, getMessagePanel(items, 6),
"Custom Board", JOptionPane.OK_CANCEL_OPTION);
if (option != JOptionPane.OK_OPTION)
return false;
int rows, cols, mines;
try {
rows = Integer.parseInt(rowsField.getText());
cols = Integer.parseInt(colsField.getText());
mines = Integer.parseInt(minesField.getText());
} catch (NumberFormatException e) {
return false;
}
// 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));
// If we nothing changed, return false
if (current == CUSTOM && CUSTOM.rows == rows && CUSTOM.cols == cols && CUSTOM.mines == mines)
return false;
CUSTOM.setStats(rows, cols, mines);
return true;
}
public static int clampInt(int value, int minValue, int maxValue) {
return Math.max(Math.min(value, maxValue), minValue);
}
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) {
final int COLS = 2;
JPanel messagePanel = new JPanel(null);
for (JComponent[] row : items) {
JLabel label = (JLabel) row[0];
label.setLabelFor(row[1]);
label.setHorizontalAlignment(JLabel.TRAILING);
messagePanel.add(label);
messagePanel.add(row[1]);
}
int[] widths = new int[COLS];
int[] heights = new int[items.length];
int[] x = new int[COLS + 1];
int[] y = new int[items.length + 1];
x[0] = padding;
for (int c = 0; c < COLS; c++) {
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;
}
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;
}
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]);
messagePanel.setPreferredSize(new Dimension(x[COLS], y[items.length]));
return messagePanel;
}
}

View file

@ -24,7 +24,7 @@ public class Face extends JComponent {
pressed = false;
mouseOver = false;
setPreferredSize(new Dimension(SIZE, SIZE));
addMouseListener(new MouseListener() {
addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
pressed = true;
@ -54,10 +54,6 @@ public class Face extends JComponent {
if (pressed)
setFace(unpressedFace);
}
@Override
public void mouseClicked(MouseEvent e) {
}
});
}
@ -67,7 +63,10 @@ public class Face extends JComponent {
}
@Override
public void paintComponent(Graphics g) {
public void paintComponent(Graphics gr) {
super.paintComponent(gr);
Graphics2D g = (Graphics2D) gr;
g.drawImage(FACES[face], 0, 0, this);
}
}

106
Main.java
View file

@ -7,22 +7,6 @@ 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";
@ -33,36 +17,41 @@ public class Main {
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();
setCanvas();
FRAME.setVisible(true);
}
public static boolean isProtectedStart() {
return protectedStart;
}
public static boolean hasSound() {
return sound;
}
private static JMenuBar getMenuBar() {
// Button group for the difficulty buttons
ButtonGroup difficultyButtons = new ButtonGroup();
Difficulty difficulty = Options.getDifficulty();
JRadioButtonMenuItem beginnerItem = new JRadioButtonMenuItem("Beginner",
difficulty == Difficulty.BEGINNER);
JRadioButtonMenuItem intermediateItem = new JRadioButtonMenuItem("Intermediate",
difficulty == Difficulty.INTERMEDIATE);
JRadioButtonMenuItem expertItem = new JRadioButtonMenuItem("Expert",
difficulty == Difficulty.EXPERT);
JRadioButtonMenuItem customItem = new JRadioButtonMenuItem("Custom...");
difficultyButtons.add(beginnerItem);
difficultyButtons.add(intermediateItem);
difficultyButtons.add(expertItem);
difficultyButtons.add(customItem);
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
private ButtonModel lastDifficultyButton = difficultyButtons.getSelection();
@Override
public void actionPerformed(ActionEvent e) {
switch (e.getActionCommand()) {
@ -71,21 +60,29 @@ public class Main {
break;
case "beginner":
setDifficulty(Difficulty.BEGINNER);
lastDifficultyButton = beginnerItem.getModel();
break;
case "intermediate":
setDifficulty(Difficulty.INTERMEDIATE);
lastDifficultyButton = intermediateItem.getModel();
break;
case "expert":
setDifficulty(Difficulty.EXPERT);
lastDifficultyButton = expertItem.getModel();
break;
case "custom":
setDifficulty(null);
if (Difficulty.setCustom(FRAME)) {
setDifficulty(Difficulty.CUSTOM);
lastDifficultyButton = customItem.getModel();
} else {
lastDifficultyButton.setSelected(true);
}
break;
case "protectedstart":
protectedStart = !protectedStart;
Options.toggleProtectedStart();
break;
case "sound":
sound = !sound;
Options.toggleSound();
Sound.initSounds();
break;
case "exit":
@ -111,25 +108,17 @@ public class Main {
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);
JCheckBoxMenuItem protectedStartItem = new JCheckBoxMenuItem("Protected Start",
Options.isProtectedStart());
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);
JCheckBoxMenuItem soundItem = new JCheckBoxMenuItem("Sound", Options.hasSound());
addMenuItem(soundItem, gameMenu, KeyEvent.VK_U, "sound", listener);
gameMenu.addSeparator();
addMenuItem("Exit", gameMenu, KeyEvent.VK_X, "exit", listener);
menuBar.add(gameMenu);
@ -167,7 +156,7 @@ public class Main {
try (Stream<Path> dirStream = Files.list(Paths.get(SKINS_DIR))) {
dirStream
// Filter for only regular files
.filter(file -> Files.isRegularFile(file))
.filter(Files::isRegularFile)
// Cut off the directory name from the resulting string
.map(Path::getFileName)
.map(Path::toString)
@ -220,11 +209,11 @@ public class Main {
}
private static void setDifficulty(Difficulty newDifficulty) {
if (newDifficulty == null);
else if (difficulty == newDifficulty)
// Reject switching difficulty to the already-existing one unless Custom
if (Options.getDifficulty() == newDifficulty && newDifficulty != Difficulty.CUSTOM)
return;
difficulty = newDifficulty;
Options.setDifficulty(newDifficulty);
replaceCanvas();
}
@ -250,24 +239,17 @@ public class Main {
canvas.stop();
canvas.removeAll();
FRAME.remove(canvas);
setCanvas();
canvas.requestFocusInWindow();
}
private static void setCanvas() {
canvas = getCanvas();
canvas = new Canvas(new File(SKINS_DIR, skin));
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() {
}

View file

@ -40,7 +40,10 @@ public class NumberDisplay extends JComponent {
}
@Override
public void paintComponent(Graphics g) {
public void paintComponent(Graphics gr) {
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

37
Options.java Normal file
View file

@ -0,0 +1,37 @@
// Public class that stores all the global static options
public class Options {
// 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;
public static boolean hasSound() {
return sound;
}
public static void toggleSound() {
sound = !sound;
}
public static boolean isProtectedStart() {
return protectedStart;
}
public static void toggleProtectedStart() {
protectedStart = !protectedStart;
}
public static Difficulty getDifficulty() {
return difficulty;
}
public static void setDifficulty(Difficulty difficulty) {
Options.difficulty = difficulty;
}
// No constructing >:(
private Options() {
}
}

View file

@ -14,7 +14,8 @@ public enum Sound {
// 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
// Use env vars and the existance of the above pipe to determine if we are
// running inside Replit and should use its sound API
public static final boolean REPLIT_API = System.getenv("REPL_ID") != null && REPLIT_PIPE.exists();
// Whether the clips have been initialized yet
@ -29,15 +30,15 @@ public enum Sound {
// 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();
// the game directly on my laptop, we have an abstract SoundBackend class
// that is extended by both a builtin javax.sound.sampled-powered backend
// and a Replit specific one
private abstract class SoundBackend {
// Plays the specific sound
abstract public void play();
}
private class NativeBackend implements SoundBackend {
private class NativeBackend extends SoundBackend {
// The audio clip associated with the sound
private final Clip CLIP;
@ -68,7 +69,7 @@ public enum Sound {
}
}
private class ReplitBackend implements SoundBackend {
private class ReplitBackend extends 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)
@ -99,7 +100,7 @@ public enum Sound {
}
}
Sound(String fileName) {
private Sound(String fileName) {
FILE = new File(AUDIO_DIR, fileName);
}
@ -135,7 +136,7 @@ public enum Sound {
// 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())
if (Options.hasSound())
soundBackend.play();
}
}

View file

@ -52,7 +52,7 @@ public class Tile extends JComponent {
SPECIAL_TILES = specialTiles;
ADJACENT_TILES = new HashSet<Tile>();
TILE_MOUSE_LISTENER = new MouseListener() {
TILE_MOUSE_LISTENER = new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
boolean rightClicked = false;
@ -115,10 +115,6 @@ public class Tile extends JComponent {
mouseOver = false;
updatePressedState();
}
@Override
public void mouseClicked(MouseEvent e) {
}
};
setPreferredSize(new Dimension(SIZE, SIZE));
@ -139,7 +135,7 @@ public class Tile extends JComponent {
// 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))
if (mine || this == startTile || Options.isProtectedStart() && ADJACENT_TILES.contains(startTile))
return false;
mine = true;
@ -248,7 +244,10 @@ public class Tile extends JComponent {
}
@Override
public void paintComponent(Graphics g) {
public void paintComponent(Graphics gr) {
super.paintComponent(gr);
Graphics2D g = (Graphics2D) gr;
g.drawImage(getImage(), 0, 0, this);
}
}