commit 8d1714777d2d1435c89a99853271051d9830d71c Author: uohlhv Date: Thu Sep 16 20:44:09 2021 +0800 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7f58cea --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.DS_Store +/bin +Minesweeper.dat +Minesweeper.jar diff --git a/README.md b/README.md new file mode 100644 index 0000000..f55e823 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# Requirement + +- Oracle JDK >= 1.8 +- Add JDK's `/bin` directory into environment variable `$PATH`. + +# Compilation + +Execute `compile.sh` (`compile.bat` for Windows). + +# Archive + +Execute `archive.sh` (`archive.bat` for Windows). diff --git a/archive.bat b/archive.bat new file mode 100644 index 0000000..3a3f20b --- /dev/null +++ b/archive.bat @@ -0,0 +1,8 @@ +@echo off +cd bin +jar cvfm "%~dp0Minesweeper.jar" "%~dp0manifest.mf" * +cd .. +echo. +echo I +echo. +pause diff --git a/archive.sh b/archive.sh new file mode 100755 index 0000000..6e82083 --- /dev/null +++ b/archive.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +cd bin || exit +jar cvfm ../Minesweeper.jar ../manifest.mf ./* diff --git a/compile.bat b/compile.bat new file mode 100644 index 0000000..39a8442 --- /dev/null +++ b/compile.bat @@ -0,0 +1,10 @@ +@echo off +if exist bin ( + del /Q bin +) +javac -d bin src\*.java +xcopy /Y "%~dp0res\*" "%~dp0bin" +echo. +echo I +echo. +pause diff --git a/compile.sh b/compile.sh new file mode 100755 index 0000000..2983c11 --- /dev/null +++ b/compile.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +if [ -d bin ]; then rm -rf bin; fi +javac -d bin src/*.java +cp res/* bin + +echo "完成!" diff --git a/manifest.mf b/manifest.mf new file mode 100644 index 0000000..3e22e7e --- /dev/null +++ b/manifest.mf @@ -0,0 +1 @@ +Main-Class: Main diff --git a/res/410.png b/res/410.png new file mode 100644 index 0000000..4a716a4 Binary files /dev/null and b/res/410.png differ diff --git a/res/420.png b/res/420.png new file mode 100644 index 0000000..a04e84d Binary files /dev/null and b/res/420.png differ diff --git a/res/430.png b/res/430.png new file mode 100644 index 0000000..d4c66dc Binary files /dev/null and b/res/430.png differ diff --git a/res/432.wav b/res/432.wav new file mode 100644 index 0000000..b44a95a Binary files /dev/null and b/res/432.wav differ diff --git a/res/433.wav b/res/433.wav new file mode 100644 index 0000000..9cd1e9c Binary files /dev/null and b/res/433.wav differ diff --git a/res/434.wav b/res/434.wav new file mode 100644 index 0000000..b64a2e6 Binary files /dev/null and b/res/434.wav differ diff --git a/res/icon.png b/res/icon.png new file mode 100644 index 0000000..2eb0d73 Binary files /dev/null and b/res/icon.png differ diff --git a/src/BestTimesDialog.java b/src/BestTimesDialog.java new file mode 100644 index 0000000..0f7379b --- /dev/null +++ b/src/BestTimesDialog.java @@ -0,0 +1,150 @@ +import java.awt.*; +import java.awt.event.*; +import javax.swing.*; +import javax.swing.BorderFactory; + +public class BestTimesDialog extends JDialog implements ActionListener, WindowListener { + private static final long serialVersionUID = -459646110674871028L; + + private Minesweeper main; + + private JLabel bRecord; + private JLabel iRecord; + private JLabel eRecord; + private JLabel iName; + private JLabel bName; + private JLabel eName; + + public BestTimesDialog(Minesweeper main) { + super(main, "Fastest Mine Sweepers", true); + this.main = main; + setLayout(new BorderLayout()); + JPanel panelTop = new JPanel(); + JPanel panelBottom = new JPanel(); + Storage data = this.main.getStorage(); + this.bRecord = new JLabel(data.getRecord(this.main.BEGINNER) + " seconds"); + this.iRecord = new JLabel(data.getRecord(this.main.INTERMEDIATE) + " seconds"); + this.eRecord = new JLabel(data.getRecord(this.main.EXPERT) + " seconds"); + this.bName = new JLabel(data.getName(this.main.BEGINNER)); + this.iName = new JLabel(data.getName(this.main.INTERMEDIATE)); + this.eName = new JLabel(data.getName(this.main.EXPERT)); + panelTop.setBorder(BorderFactory.createEmptyBorder(15, 10, 15, 10)); + panelBottom.setBorder(BorderFactory.createEmptyBorder(0, 10, 15, 10)); + add(panelTop, "North"); + add(panelBottom); + panelTop.setLayout(new GridBagLayout()); + panelBottom.setLayout(new GridBagLayout()); + GridBagConstraints ct = + new GridBagConstraints( + 3, + 3, + 1, + 1, + 0.0, + 1.0, + GridBagConstraints.CENTER, + GridBagConstraints.NONE, + new Insets(2, 10, 2, 10), + 0, + 0); + GridBagConstraints cb = + new GridBagConstraints( + 2, + 1, + 1, + 1, + 0.0, + 1.0, + GridBagConstraints.CENTER, + GridBagConstraints.NONE, + new Insets(2, 10, 2, 10), + 0, + 0); + ct.gridx = 0; + ct.anchor = GridBagConstraints.WEST; + ct.gridy = 0; + panelTop.add(new JLabel("Beginner:"), ct); + ct.gridy = 1; + panelTop.add(new JLabel("Intermediate:"), ct); + ct.gridy = 2; + panelTop.add(new JLabel("Expert:"), ct); + ct.gridx = 1; + ct.gridy = GridBagConstraints.RELATIVE; + ct.weightx = 1.0; + ct.anchor = GridBagConstraints.CENTER; + ct.gridy = 0; + panelTop.add(this.bRecord, ct); + ct.gridy = 1; + panelTop.add(this.iRecord, ct); + ct.gridy = 2; + panelTop.add(this.eRecord, ct); + ct.gridx = 2; + ct.gridy = 0; + panelTop.add(this.bName, ct); + ct.gridy = 1; + panelTop.add(this.iName, ct); + ct.gridy = 2; + panelTop.add(this.eName, ct); + cb.gridx = 0; + cb.gridy = 0; + JButton reset = new JButton("Reset Scores"); + panelBottom.add(reset, cb); + reset.addActionListener(this); + cb.gridx = 1; + cb.gridy = 0; + JButton ok = new JButton("OK"); + panelBottom.add(ok, cb); + this.getRootPane().setDefaultButton(ok); + ok.addActionListener(this); + pack(); + setResizable(false); + Rectangle screenSize = getGraphicsConfiguration().getBounds(); + setLocation( + screenSize.x + screenSize.width / 2 - getSize().width / 2, + screenSize.y + screenSize.height / 2 - getSize().height / 2); + setVisible(true); + } + + @Override + public void actionPerformed(ActionEvent e) { + String cmd = e.getActionCommand(); + if (cmd.equals("Reset Scores")) { + Storage data = this.main.getStorage(); + data.setRecord(0, 999); + data.setRecord(1, 999); + data.setRecord(2, 999); + data.setName(0, "Anonymous"); + data.setName(1, "Anonymous"); + data.setName(2, "Anonymous"); + this.bRecord.setText("999 seconds"); + this.iRecord.setText("999 seconds"); + this.eRecord.setText("999 seconds"); + this.bName.setText("Anonymous"); + this.iName.setText("Anonymous"); + this.eName.setText("Anonymous"); + return; + } + this.dispose(); + } + + @Override + public void windowClosing(WindowEvent e) {} + + @Override + public void windowOpened(WindowEvent e) {} + + @Override + public void windowClosed(WindowEvent e) {} + + @Override + public void windowIconified(WindowEvent e) {} + + @Override + public void windowDeiconified(WindowEvent e) {} + + @Override + public void windowActivated(WindowEvent e) {} + + @Override + public void windowDeactivated(WindowEvent e) {} +} diff --git a/src/Board.java b/src/Board.java new file mode 100644 index 0000000..8ced168 --- /dev/null +++ b/src/Board.java @@ -0,0 +1,170 @@ +import java.awt.*; +import javax.swing.*; +import javax.swing.BorderFactory; +import javax.swing.plaf.basic.BasicBorders.ButtonBorder; + +public class Board extends JPanel { + private static final long serialVersionUID = -8979636904753517635L; + + private Minesweeper main; + + private Tile[][] tiles; + + private int height; + private int width; + private int numOfMines; + + private void addMines() { + if (this.numOfMines >= this.width * this.height) { + this.numOfMines = this.width * this.height - 1; + } + int nextRow, nextCol; + int i = 0; + while (i < this.numOfMines) { + nextRow = Math.round((float) Math.random() * (this.height - 1)); + nextCol = Math.round((float) Math.random() * (this.width - 1)); + if (!this.tiles[nextRow][nextCol].isMine()) { + this.tiles[nextRow][nextCol].setMine(); + i += 1; + } + } + this.setTileValues(); + } + + private void setTileValues() { + for (int i = 0; i < this.height; i += 1) { + for (int j = 0; j < this.width; j += 1) { + if (this.tiles[i][j].isMine()) { + continue; + } + int mines = 0; + for (int k = i - 1; k <= i + 1; k += 1) { + for (int l = j - 1; l <= j + 1; l += 1) { + if (!(k < 0 || l < 0 || k >= this.height || l >= this.width) + && !(k == i && l == j) + && this.tiles[k][l].isMine()) { + mines += 1; + } + } + } + this.tiles[i][j].setValue(mines); + } + } + if (this.main.getStorage().isCreepy()) { + this.addDuds(); + } + } + + private void addDuds() { + for (int i = 0; i < this.height; i += 1) { + for (int j = 0; j < this.width; j += 1) { + if (this.tiles[i][j].isMine() && Math.round((float) Math.random() * 5) == 0) { + this.tiles[i][j].setDud(); + } + } + } + } + + public Board(Minesweeper main) { + this.main = main; + setBackground(new Color(192, 192, 192)); + setBorder( + BorderFactory.createCompoundBorder( + BorderFactory.createEmptyBorder(5, 5, 5, 5), + new ButtonBorder( + Color.white, Color.white, new Color(128, 128, 128), new Color(128, 128, 128)))); + } + + public Minesweeper getMain() { + return this.main; + } + + public int getMines() { + return this.numOfMines; + } + + public void setBoard(int width, int height, int mines) { + setVisible(false); + this.height = height; + this.width = width; + this.numOfMines = mines; + setLayout(new GridLayout(this.height, this.width, 0, 0)); + removeAll(); + this.tiles = new Tile[this.height][this.width]; + for (int i = 0; i < this.height; i += 1) { + for (int j = 0; j < this.width; j += 1) { + add(this.tiles[i][j] = new Tile(i, j, this)); + } + } + this.addMines(); + setVisible(true); + } + + public void expand(int row, int col) { + setVisible(false); + for (int i = row - 1; i <= row + 1; i += 1) { + for (int j = col - 1; j <= col + 1; j += 1) { + if (!(i < 0 || j < 0 || i >= this.height || j >= this.width) && !(i == row && j == col)) { + Tile tile = this.tiles[i][j]; + if (!tile.isUncovered() && !tile.isFlagged() && !tile.isUnsure() && !tile.isDud()) { + tile.uncover(true); + } + } + } + } + setVisible(true); + } + + public void showMines() { + setVisible(false); + for (int i = 0; i < this.height; i += 1) { + for (int j = 0; j < this.width; j += 1) { + Tile tile = this.tiles[i][j]; + if (tile.isMine() + && !tile.isUncovered() + && !tile.isFlagged() + && !tile.isUnsure() + && !tile.isDud()) { + tile.setTileIcon("covered mine"); + } else if ((!tile.isMine() || tile.isDud()) && tile.isFlagged()) { + tile.setTileIcon("misflagged"); + } + } + } + setVisible(true); + } + + public void showFlags() { + setVisible(false); + for (int i = 0; i < this.height; i += 1) { + for (int j = 0; j < this.width; j += 1) { + Tile tile = this.tiles[i][j]; + if (tile.isMine() && !tile.isUncovered() && !tile.isFlagged()) { + tile.setTileIcon("flagged"); + } + } + } + setVisible(true); + } + + public void removeAllMouseListeners() { + for (int i = 0; i < this.height; i += 1) { + for (int j = 0; j < this.width; j += 1) { + this.tiles[i][j].removeMouseListener(this.tiles[i][j]); + } + } + } + + public void checkWon(boolean expanding) { + for (int i = 0; i < this.height; i += 1) { + for (int j = 0; j < this.width; j += 1) { + Tile tile = this.tiles[i][j]; + if ((tile.isFlagged() && !tile.isMine()) + || (!tile.isUncovered() && !tile.isFlagged() && !tile.isMine())) { + return; + } + } + } + this.main.complete(expanding); + } +} diff --git a/src/CounterDisplay.java b/src/CounterDisplay.java new file mode 100644 index 0000000..7e6f237 --- /dev/null +++ b/src/CounterDisplay.java @@ -0,0 +1,75 @@ +import java.awt.*; +import javax.swing.*; +import javax.swing.BorderFactory; +import javax.swing.plaf.basic.BasicBorders.ButtonBorder; + +public class CounterDisplay extends JPanel { + private static final long serialVersionUID = 8590228173889847000L; + + private Board board; + + private Digit hundred; + private Digit tenth; + private Digit first; + + private int mines; + + public CounterDisplay(Board board) { + this.board = board; + this.hundred = new Digit(this.board); + this.tenth = new Digit(this.board); + this.first = new Digit(this.board); + this.mines = this.board.getMines(); + this.updateDisplay(); + setBackground(new Color(192, 192, 192)); + setBorder( + BorderFactory.createCompoundBorder( + BorderFactory.createEmptyBorder(5, 5, 5, 5), + new ButtonBorder( + Color.white, Color.white, new Color(128, 128, 128), new Color(128, 128, 128)))); + setVisible(false); + setLayout(new GridLayout(1, 3, 0, 0)); + removeAll(); + add(this.hundred); + add(this.tenth); + add(this.first); + setVisible(true); + } + + public void increase() { + this.mines += 1; + this.updateDisplay(); + } + + public void decrease() { + this.mines -= 1; + if (this.mines < 0) { + this.negativeLoop(); + } else { + this.updateDisplay(); + } + } + + public void reset() { + this.mines = this.board.getMines(); + this.updateDisplay(); + } + + public void clear() { + this.mines = 0; + this.updateDisplay(); + } + + public void updateDisplay() { + this.hundred.setDigitIcon(String.valueOf(mines / 100)); + this.tenth.setDigitIcon(String.valueOf(mines / 10 % 10)); + this.first.setDigitIcon(String.valueOf(mines % 10)); + } + + public void negativeLoop() { + this.hundred.setDigitIcon("-"); + int temp = Math.abs(mines) % 100; + this.tenth.setDigitIcon(String.valueOf(temp / 10 % 10)); + this.first.setDigitIcon(String.valueOf(temp % 10)); + } +} diff --git a/src/CustomDialog.java b/src/CustomDialog.java new file mode 100644 index 0000000..5418eb5 --- /dev/null +++ b/src/CustomDialog.java @@ -0,0 +1,154 @@ +import java.awt.*; +import java.awt.event.*; +import javax.swing.*; +import javax.swing.BorderFactory; + +public class CustomDialog extends JDialog implements ActionListener, WindowListener { + private static final long serialVersionUID = 3593586540539735229L; + + private Minesweeper main; + + private JTextField height = new JTextField(5); + private JTextField width = new JTextField(5); + private JTextField mines = new JTextField(5); + + private int hValue; + private int wValue; + private int mValue; + + public CustomDialog(Minesweeper main) { + super(main, "Custom Field", true); + this.main = main; + JPanel panel = new JPanel(); + Storage data = this.main.getStorage(); + this.hValue = data.getHeight(); + this.wValue = data.getWidth(); + this.mValue = data.getMines(); + this.height.setText(String.valueOf(this.hValue)); + this.width.setText(String.valueOf(this.wValue)); + this.mines.setText(String.valueOf(this.mValue)); + panel.setBorder(BorderFactory.createEmptyBorder(30, 10, 30, 10)); + add(panel); + panel.setLayout(new GridBagLayout()); + GridBagConstraints c = + new GridBagConstraints( + 3, + 3, + 1, + 1, + 0.0, + 1.0, + GridBagConstraints.CENTER, + GridBagConstraints.NONE, + new Insets(2, 10, 2, 10), + 0, + 0); + c.gridx = 3; + c.fill = GridBagConstraints.HORIZONTAL; + c.gridy = 0; + JButton ok = new JButton("OK"); + panel.add(ok, c); + this.getRootPane().setDefaultButton(ok); + ok.addActionListener(this); + c.gridx = 3; + c.gridy = 2; + JButton cancel = new JButton("Cancel"); + panel.add(cancel, c); + cancel.addActionListener(this); + c.gridx = 0; + c.anchor = GridBagConstraints.EAST; + c.gridy = 0; + panel.add(new JLabel("Height:"), c); + c.gridy = 1; + panel.add(new JLabel("Width:"), c); + c.gridy = 2; + panel.add(new JLabel("Mines:"), c); + c.gridx = 1; + c.gridy = GridBagConstraints.RELATIVE; + c.weightx = 1.0; + c.fill = GridBagConstraints.HORIZONTAL; + c.anchor = GridBagConstraints.CENTER; + c.gridy = 0; + panel.add(height, c); + c.gridy = 1; + panel.add(width, c); + c.gridy = 2; + panel.add(mines, c); + pack(); + setResizable(false); + Rectangle screenSize = getGraphicsConfiguration().getBounds(); + setLocation( + screenSize.x + screenSize.width / 2 - getSize().width / 2, + screenSize.y + screenSize.height / 2 - getSize().height / 2); + setVisible(true); + } + + public boolean setValues() { + try { + this.wValue = Integer.decode(width.getText()).intValue(); + if (this.wValue < 9) { + this.wValue = 9; + } else if (this.wValue > 30) { + this.wValue = 30; + } + this.hValue = Integer.decode(height.getText()).intValue(); + if (this.hValue < 9) { + this.hValue = 9; + } else if (this.hValue > 24) { + this.hValue = 24; + } + this.mValue = Integer.decode(mines.getText()).intValue(); + if (this.mValue < 10) { + this.mValue = 10; + } else if (this.mValue > 668) { + this.mValue = 668; + } + } catch (NumberFormatException e) { + return false; + } + return true; + } + + public int getHeightValue() { + return this.hValue; + } + + public int getWidthValue() { + return this.wValue; + } + + public int getMinesValue() { + return this.mValue; + } + + @Override + public void actionPerformed(ActionEvent e) { + String cmd = e.getActionCommand(); + if (cmd.equals("Cancel") || this.setValues()) { + this.dispose(); + } + } + + @Override + public void windowClosing(WindowEvent e) { + this.dispose(); + } + + @Override + public void windowOpened(WindowEvent e) {} + + @Override + public void windowClosed(WindowEvent e) {} + + @Override + public void windowIconified(WindowEvent e) {} + + @Override + public void windowDeiconified(WindowEvent e) {} + + @Override + public void windowActivated(WindowEvent e) {} + + @Override + public void windowDeactivated(WindowEvent e) {} +} diff --git a/src/Digit.java b/src/Digit.java new file mode 100644 index 0000000..f4ffafa --- /dev/null +++ b/src/Digit.java @@ -0,0 +1,49 @@ +import java.awt.*; +import java.awt.image.BufferedImage; +import javax.swing.*; + +public class Digit extends JLabel { + private static final long serialVersionUID = -4719882476458914430L; + private Board board; + private BufferedImage image; + + private int value = 0; + + private final int HEIGHT = 23; + private final int WIDTH = 13; + + public Digit(Board board) { + super("", JLabel.CENTER); + this.board = board; + this.image = this.board.getMain().getImage().getDigit(); + this.setDigitIcon("0"); + setVerticalAlignment(JLabel.CENTER); + setHorizontalAlignment(JLabel.CENTER); + setPreferredSize(new Dimension(this.WIDTH, this.HEIGHT)); + } + + public int topMargin(String number) { + String[] numbers = { + "-", " ", "9", "8", "7", "6", + "5", "4", "3", "2", "1", "0" + }; + for (int i = 0; i < numbers.length; i += 1) { + if (numbers[i].equals(number)) { + return i; + } + } + return 1; + } + + public void setDigitIcon(String value) { + setIcon( + new ImageIcon( + this.image.getSubimage( + 0, this.HEIGHT * this.topMargin(value), this.WIDTH, this.HEIGHT))); + } + + public void setValue(int value) { + this.value = value; + this.setDigitIcon(String.valueOf(this.value)); + } +} diff --git a/src/Info.java b/src/Info.java new file mode 100644 index 0000000..00867d6 --- /dev/null +++ b/src/Info.java @@ -0,0 +1,43 @@ +import java.awt.*; +import javax.swing.*; +import javax.swing.BorderFactory; +import javax.swing.plaf.basic.BasicBorders.ButtonBorder; + +public class Info extends JPanel { + private static final long serialVersionUID = 3806487248823604535L; + + private Board board; + private CounterDisplay counter; + private Smiley smiley; + private TimerDisplay timer; + + public Info(Minesweeper main) { + this.board = main.getBoard(); + this.counter = new CounterDisplay(this.board); + this.smiley = new Smiley(this.board); + this.timer = new TimerDisplay(this.board); + setBackground(new Color(192, 192, 192)); + setBorder( + BorderFactory.createCompoundBorder( + BorderFactory.createEmptyBorder(5, 5, 0, 5), + new ButtonBorder( + Color.white, Color.white, new Color(128, 128, 128), new Color(128, 128, 128)))); + setLayout(new BorderLayout()); + add(this.counter, "West"); + add(this.smiley, "Center"); + add(this.timer, "East"); + setVisible(true); + } + + public CounterDisplay getCounter() { + return this.counter; + } + + public Smiley getSmiley() { + return this.smiley; + } + + public TimerDisplay getTimer() { + return this.timer; + } +} diff --git a/src/Main.java b/src/Main.java new file mode 100644 index 0000000..92e0e57 --- /dev/null +++ b/src/Main.java @@ -0,0 +1,5 @@ +public class Main { + public static void main(String[] args) { + new Minesweeper(); + } +} diff --git a/src/Menubar.java b/src/Menubar.java new file mode 100644 index 0000000..da68123 --- /dev/null +++ b/src/Menubar.java @@ -0,0 +1,118 @@ +import java.awt.event.*; +import javax.swing.*; + +public class Menubar extends JMenuBar implements ActionListener { + private static final long serialVersionUID = 865388774192274414L; + + private Minesweeper main; + + private JCheckBoxMenuItem creepy; + private JCheckBoxMenuItem sound; + + public Menubar(Minesweeper main) { + this.main = main; + JMenu game = new JMenu("Game"); + add(game); + JMenuItem newGame = new JMenuItem("New"); + game.add(newGame); + newGame.addActionListener(this); + game.addSeparator(); + Storage data = this.main.getStorage(); + JRadioButtonMenuItem beginner = + new JRadioButtonMenuItem( + "Beginner", (data.getDifficulty() == this.main.BEGINNER) ? true : false); + JRadioButtonMenuItem intermediate = + new JRadioButtonMenuItem( + "Intermediate", (data.getDifficulty() == this.main.INTERMEDIATE) ? true : false); + JRadioButtonMenuItem expert = + new JRadioButtonMenuItem( + "Expert", (data.getDifficulty() == this.main.EXPERT) ? true : false); + JRadioButtonMenuItem custom = + new JRadioButtonMenuItem( + "Custom...", (data.getDifficulty() == this.main.CUSTOM) ? true : false); + this.creepy = new JCheckBoxMenuItem("Creepy Mode", (data.isCreepy()) ? true : false); + game.add(beginner); + game.add(intermediate); + game.add(expert); + game.add(custom); + game.add(creepy); + ButtonGroup difficulties = new ButtonGroup(); + difficulties.add(beginner); + difficulties.add(intermediate); + difficulties.add(expert); + difficulties.add(custom); + beginner.addActionListener(this); + intermediate.addActionListener(this); + expert.addActionListener(this); + custom.addActionListener(this); + custom.setActionCommand("Custom"); + creepy.addActionListener(this); + game.addSeparator(); + this.sound = new JCheckBoxMenuItem("Sound", (data.isSoundEnabled()) ? true : false); + game.add(sound); + sound.addActionListener(this); + game.addSeparator(); + JMenuItem bestTimes = new JMenuItem("Best Times..."); + game.add(bestTimes); + bestTimes.addActionListener(this); + bestTimes.setActionCommand("Best Times"); + game.addSeparator(); + JMenuItem exit = new JMenuItem("Exit"); + game.add(exit); + exit.addActionListener(this); + } + + @Override + public void actionPerformed(ActionEvent e) { + String cmd = e.getActionCommand(); + if (cmd.equals("New")) { + this.main.newGame(); + } else { + Storage data = this.main.getStorage(); + if (cmd.equals("Beginner")) { + data.setDifficulty(this.main.BEGINNER); + this.main.newGame(); + } else if (cmd.equals("Intermediate")) { + data.setDifficulty(this.main.INTERMEDIATE); + this.main.newGame(); + } else if (cmd.equals("Expert")) { + data.setDifficulty(this.main.EXPERT); + this.main.newGame(); + } else if (cmd.equals("Custom")) { + CustomDialog dialog = new CustomDialog(this.main); + data.setDifficulty(this.main.CUSTOM); + data.setHeight(dialog.getHeightValue()); + data.setWidth(dialog.getWidthValue()); + data.setMines(dialog.getMinesValue()); + this.main.newGame(); + } else if (cmd.equals("Creepy Mode")) { + data.switchCreepy(); + if (!data.isSoundEnabled()) { + data.switchSound(); + this.sound.setSelected(true); + } + if (this.main.isGameStarted()) { + this.main.newGame(); + } + } else if (cmd.equals("Sound")) { + data.switchSound(); + if (data.isCreepy()) { + data.switchCreepy(); + this.creepy.setSelected(false); + this.main.newGame(); + } else if (this.main.isGameStarted()) { + PlaySound play = this.main.getPlay(); + if (play.isPlaying()) { + play.stop(); + } else { + play.tick(); + } + } + } else if (cmd.equals("Best Times")) { + new BestTimesDialog(this.main); + } else if (cmd.equals("Exit")) { + System.exit(0); + } + } + } +} diff --git a/src/Minesweeper.java b/src/Minesweeper.java new file mode 100644 index 0000000..61b75e2 --- /dev/null +++ b/src/Minesweeper.java @@ -0,0 +1,151 @@ +import java.awt.*; +import javax.swing.*; + +public class Minesweeper extends JFrame { + private static final long serialVersionUID = -711696178703199107L; + + private Board board; + private Info info; + private PlaySound play; + private Resource images; + private Storage data; + + private boolean gameStarted; + + private final String RES_GAME_ICON = "icon.png"; + + public final int BEGINNER = 0; + public final int INTERMEDIATE = 1; + public final int EXPERT = 2; + public final int CUSTOM = 3; + + private void setLocationCenter() { + Rectangle screen = getGraphicsConfiguration().getBounds(); + setLocation( + screen.x + screen.width / 2 - getSize().width / 2, + screen.y + screen.height / 2 - getSize().height / 2); + } + + public Minesweeper() { + super("Minesweeper"); + this.data = new Storage(); + this.images = new Resource(); + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + setIconImage(Toolkit.getDefaultToolkit().getImage(getClass().getResource(RES_GAME_ICON))); + setJMenuBar(new Menubar(this)); + setLayout(new BorderLayout()); + this.data = new Storage(); + this.board = new Board(this); + this.board.setBoard( + this.data.getWidth(), + this.data.getHeight(), + this.data.getMines()); // must call setBoard before create instances of Info + this.info = new Info(this); + this.play = new PlaySound(); + this.gameStarted = false; + add(this.info, "North"); + add(this.board); + pack(); + setResizable(false); + this.setLocationCenter(); + setVisible(true); + } + + public void newGame() { + this.board.setBoard(this.data.getWidth(), this.data.getHeight(), this.data.getMines()); + pack(); + if (this.isGameStarted()) { + this.info.getTimer().stop(); + if (this.data.isSoundEnabled()) { + this.play.stop(); + } + } + this.info.getCounter().reset(); + this.info.getSmiley().setSmileyIcon("smile"); + this.info.getTimer().reset(); + this.stopGame(); + } + + public void delGame() { + this.stopGame(); + if (this.data.isSoundEnabled()) { + this.play.stop(); + this.play.boom(); + } + this.info.getTimer().stop(); + this.info.getSmiley().setSmileyIcon("death"); + this.board.removeAllMouseListeners(); + this.board.showMines(); + } + + public Board getBoard() { + return this.board; + } + + public Info getInfo() { + return this.info; + } + + public PlaySound getPlay() { + return this.play; + } + + public Resource getImage() { + return this.images; + } + + public Storage getStorage() { + return this.data; + } + + public boolean isGameStarted() { + return this.gameStarted; + } + + public void startGame() { + this.gameStarted = true; + } + + public void stopGame() { + this.gameStarted = false; + } + + public void complete(boolean expanding) { + this.stopGame(); + this.info.getTimer().stop(); + this.info.getSmiley().setSmileyIcon("boss"); + this.board.removeAllMouseListeners(); + this.board.showFlags(); + this.info.getCounter().clear(); + if (this.data.isSoundEnabled() && !this.data.isCreepy() && !expanding) { + this.play.stop(); + this.play.win(); + } + if (this.isNewRecord() && !this.data.isCreepy()) { + new NewRecordDialog(this); + } + } + + public boolean isNewRecord() { + int seconds = this.info.getTimer().getTime(); + switch (this.data.getDifficulty()) { + case 0: + if (seconds < this.data.getRecord(this.BEGINNER)) { + return true; + } + break; + case 1: + if (seconds < this.data.getRecord(this.INTERMEDIATE)) { + return true; + } + break; + case 2: + if (seconds < this.data.getRecord(this.EXPERT)) { + return true; + } + break; + default: + } + return false; + } +} diff --git a/src/NewRecordDialog.java b/src/NewRecordDialog.java new file mode 100644 index 0000000..18f83fe --- /dev/null +++ b/src/NewRecordDialog.java @@ -0,0 +1,98 @@ +import java.awt.*; +import java.awt.event.*; +import javax.swing.*; +import javax.swing.BorderFactory; + +public class NewRecordDialog extends JDialog implements ActionListener, WindowListener { + private static final long serialVersionUID = -7317311608209462993L; + + private Minesweeper main; + + private JTextField name; + + public NewRecordDialog(Minesweeper main) { + super(main, "", true); + this.main = main; + this.name = new JTextField("Anonymous"); + JPanel panel = new JPanel(); + add(panel); + panel.setLayout(new GridBagLayout()); + panel.setBorder(BorderFactory.createEmptyBorder(15, 15, 15, 15)); + GridBagConstraints c = + new GridBagConstraints( + 1, + 3, + 1, + 1, + 0.0, + 1.0, + GridBagConstraints.CENTER, + GridBagConstraints.NONE, + new Insets(15, 15, 15, 15), + 0, + 0); + c.gridx = 0; + c.gridy = 0; + panel.add( + new JLabel( + "
You have the fastest time
for " + + this.getLevel() + + " level.
Please enter your name.
"), + c); + c.gridy = 1; + panel.add(this.name, c); + c.gridy = 2; + JButton ok = new JButton("OK"); + panel.add(ok, c); + this.getRootPane().setDefaultButton(ok); + ok.addActionListener(this); + pack(); + setResizable(false); + Rectangle screenSize = getGraphicsConfiguration().getBounds(); + setLocation( + screenSize.x + screenSize.width / 2 - getSize().width / 2, + screenSize.y + screenSize.height / 2 - getSize().height / 2); + setVisible(true); + } + + public String getLevel() { + switch (this.main.getStorage().getDifficulty()) { + case 0: + return "beginner"; + case 1: + return "intermediate"; + case 2: + return "expert"; + } + return ""; + } + + @Override + public void actionPerformed(ActionEvent e) { + Storage data = this.main.getStorage(); + data.setRecord(data.getDifficulty(), this.main.getInfo().getTimer().getTime()); + data.setName(data.getDifficulty(), this.name.getText()); + this.dispose(); + } + + @Override + public void windowClosing(WindowEvent e) {} + + @Override + public void windowOpened(WindowEvent e) {} + + @Override + public void windowClosed(WindowEvent e) {} + + @Override + public void windowIconified(WindowEvent e) {} + + @Override + public void windowDeiconified(WindowEvent e) {} + + @Override + public void windowActivated(WindowEvent e) {} + + @Override + public void windowDeactivated(WindowEvent e) {} +} diff --git a/src/PlaySound.java b/src/PlaySound.java new file mode 100644 index 0000000..742beac --- /dev/null +++ b/src/PlaySound.java @@ -0,0 +1,64 @@ +import java.io.*; +import java.net.URL; +import java.util.Timer; +import java.util.TimerTask; +import javax.sound.sampled.*; + +public class PlaySound { + private Timer timer; + + private boolean playing; + + private final String RES_TICK = "432.wav"; + private final String RES_WIN = "433.wav"; + private final String RES_BOOM = "434.wav"; + + public PlaySound() { + this.playing = false; + } + + public boolean isPlaying() { + return this.playing; + } + + public void play(String filename) { + URL f = getClass().getResource(filename); + try { + AudioInputStream in = AudioSystem.getAudioInputStream(f); + Clip clip = AudioSystem.getClip(); + clip.open(in); + clip.start(); + } catch (UnsupportedAudioFileException e) { + } catch (IOException e) { + } catch (LineUnavailableException e) { + } + } + + public void tick() { + this.playing = true; + this.timer = new Timer(); + TimerTask tt = + new TimerTask() { + @Override + public void run() { + play(RES_TICK); + } + }; + this.timer.scheduleAtFixedRate(tt, 0, 1000); + } + + public void win() { + this.play(this.RES_WIN); + } + + public void boom() { + this.play(this.RES_BOOM); + } + + public void stop() { + if (this.playing) { + this.timer.cancel(); + this.playing = false; + } + } +} diff --git a/src/Resource.java b/src/Resource.java new file mode 100644 index 0000000..c1127f5 --- /dev/null +++ b/src/Resource.java @@ -0,0 +1,42 @@ +import java.awt.image.BufferedImage; +import javax.imageio.ImageIO; +import javax.swing.*; + +public class Resource { + private BufferedImage digit; + private BufferedImage smiley; + private BufferedImage tile; + + private final String RES_TILE = "410.png"; + private final String RES_DIGIT = "420.png"; + private final String RES_SMILEY = "430.png"; + + private BufferedImage loadImage(String filename) { + BufferedImage image; + try { + image = ImageIO.read(getClass().getResourceAsStream(filename)); + } catch (Exception e) { + JOptionPane.showMessageDialog(null, "Error: Image Loading Failed."); + image = null; + } + return image; + } + + public Resource() { + this.digit = this.loadImage(this.RES_DIGIT); + this.smiley = this.loadImage(this.RES_SMILEY); + this.tile = this.loadImage(this.RES_TILE); + } + + public BufferedImage getDigit() { + return this.digit; + } + + public BufferedImage getSmiley() { + return this.smiley; + } + + public BufferedImage getTile() { + return this.tile; + } +} diff --git a/src/Smiley.java b/src/Smiley.java new file mode 100644 index 0000000..3374919 --- /dev/null +++ b/src/Smiley.java @@ -0,0 +1,64 @@ +import java.awt.*; +import java.awt.event.*; +import java.awt.image.BufferedImage; +import javax.swing.*; + +public class Smiley extends JLabel implements MouseListener { + private static final long serialVersionUID = -1028805830774712972L; + + private Board board; + private BufferedImage image; + + private final int SIZE = 24; + + public Smiley(Board board) { + super("", JLabel.CENTER); + this.board = board; + this.image = this.board.getMain().getImage().getSmiley(); + this.setSmileyIcon("smile"); + addMouseListener(this); + setVerticalAlignment(JLabel.CENTER); + setHorizontalAlignment(JLabel.CENTER); + setPreferredSize(new Dimension(this.SIZE, this.SIZE)); + } + + public int topMargin(String state) { + String[] states = {"smile down", "boss", "death", "oops", "smile"}; + for (int i = 0; i < states.length; i += 1) { + if (states[i].equals(state)) { + return i; + } + } + return 3; + } + + public void setSmileyIcon(String state) { + setIcon( + new ImageIcon( + this.image.getSubimage(0, this.SIZE * this.topMargin(state), this.SIZE, this.SIZE))); + } + + @Override + public void mousePressed(MouseEvent e) { + if (SwingUtilities.isLeftMouseButton(e)) { + this.setSmileyIcon("smile down"); + } + } + + @Override + public void mouseReleased(MouseEvent e) { + if (SwingUtilities.isLeftMouseButton(e)) { + this.setSmileyIcon("smile"); + this.board.getMain().newGame(); + } + } + + @Override + public void mouseExited(MouseEvent e) {} + + @Override + public void mouseEntered(MouseEvent e) {} + + @Override + public void mouseClicked(MouseEvent e) {} +} diff --git a/src/Storage.java b/src/Storage.java new file mode 100644 index 0000000..d618351 --- /dev/null +++ b/src/Storage.java @@ -0,0 +1,178 @@ +import java.io.*; + +public class Storage { + private int difficulty; + private int height; + private int width; + private int mines; + private boolean creepy; + private boolean soundEnabled; + private int[] records; + private String[] names; + + private final int NUM_OF_DIFFICULTIES = 3; + private final String DATA_FILE = "Minesweeper.dat"; + + private void open(final String filename) throws FileNotFoundException, IOException { + DataInputStream in = new DataInputStream(new FileInputStream(filename)); + this.difficulty = in.readInt(); + this.height = in.readInt(); + this.width = in.readInt(); + this.mines = in.readInt(); + this.creepy = in.readBoolean(); + this.soundEnabled = in.readBoolean(); + for (int i = 0; i < NUM_OF_DIFFICULTIES; i += 1) { + this.records[i] = in.readInt(); + this.names[i] = in.readUTF(); + } + in.close(); + } + + public Storage() { + this.records = new int[this.NUM_OF_DIFFICULTIES]; + this.names = new String[this.NUM_OF_DIFFICULTIES]; + try { + this.open(this.DATA_FILE); + } catch (Exception e) { + this.difficulty = 0; + this.height = 9; + this.width = 9; + this.mines = 10; + this.creepy = false; + this.soundEnabled = false; + this.records = new int[] {999, 999, 999}; + this.names = new String[] {"Anonymous", "Anonymous", "Anonymous"}; + try { + this.save(this.DATA_FILE); + } catch (Exception v) { + } + } + } + + public void save(final String filename) throws FileNotFoundException, IOException { + DataOutputStream out = new DataOutputStream(new FileOutputStream(filename)); + out.writeInt(this.difficulty); + out.writeInt(this.height); + out.writeInt(this.width); + out.writeInt(this.mines); + out.writeBoolean(this.creepy); + out.writeBoolean(this.soundEnabled); + for (int i = 0; i < NUM_OF_DIFFICULTIES; i += 1) { + out.writeInt(this.records[i]); + out.writeUTF(this.names[i]); + } + out.close(); + } + + public int getDifficulty() { + return this.difficulty; + } + + public int getHeight() { + return this.height; + } + + public int getWidth() { + return this.width; + } + + public int getMines() { + return this.mines; + } + + public boolean isCreepy() { + return this.creepy; + } + + public boolean isSoundEnabled() { + return this.soundEnabled; + } + + public int getRecord(int difficulty) { + return this.records[difficulty]; + } + + public String getName(int difficulty) { + return this.names[difficulty]; + } + + public void setDifficulty(int difficulty) { + this.difficulty = difficulty; + switch (difficulty) { + case 0: + this.height = 9; + this.width = 9; + this.mines = 10; + break; + case 1: + this.height = 16; + this.width = 16; + this.mines = 40; + break; + case 2: + this.height = 16; + this.width = 30; + this.mines = 99; + } + try { + this.save(this.DATA_FILE); + } catch (Exception e) { + } + } + + public void setHeight(int height) { + this.height = height; + try { + this.save(this.DATA_FILE); + } catch (Exception e) { + } + } + + public void setWidth(int width) { + this.width = width; + try { + this.save(this.DATA_FILE); + } catch (Exception e) { + } + } + + public void setMines(int mines) { + this.mines = mines; + try { + this.save(this.DATA_FILE); + } catch (Exception e) { + } + } + + public void switchCreepy() { + this.creepy = !this.creepy; + try { + this.save(this.DATA_FILE); + } catch (Exception e) { + } + } + + public void switchSound() { + this.soundEnabled = !this.soundEnabled; + try { + this.save(this.DATA_FILE); + } catch (Exception e) { + } + } + + public void setRecord(int difficulty, int score) { + this.records[difficulty] = score; + try { + this.save(this.DATA_FILE); + } catch (Exception e) { + } + } + + public void setName(int difficulty, String name) { + this.names[difficulty] = name; + try { + this.save(this.DATA_FILE); + } catch (Exception e) { + } + } +} diff --git a/src/Tile.java b/src/Tile.java new file mode 100644 index 0000000..3f0aa05 --- /dev/null +++ b/src/Tile.java @@ -0,0 +1,199 @@ +import java.awt.*; +import java.awt.event.*; +import java.awt.image.BufferedImage; +import javax.swing.*; + +public class Tile extends JLabel implements MouseListener { + private static final long serialVersionUID = -6873580061751396083L; + + private Board board; + private BufferedImage image; + + private boolean uncovered; + private boolean flagged; + private boolean unsure; + private boolean dud; + + private int row; + private int col; + private int value; // -1~8 (-1: mine) + + private final int SIZE = 16; + + public Tile(int row, int col, Board board) { + super("", JLabel.CENTER); + this.board = board; + this.image = this.board.getMain().getImage().getTile(); + this.uncovered = false; + this.flagged = false; + this.unsure = false; + this.dud = false; + this.row = row; + this.col = col; + this.setValue(0); + this.setTileIcon("covered"); + addMouseListener(this); + setVerticalAlignment(JLabel.CENTER); + setHorizontalAlignment(JLabel.CENTER); + setPreferredSize(new Dimension(this.SIZE, this.SIZE)); + } + + public int topMargin(String state) { + String[] states = { + "covered", + "flagged", + "unsure", + "-1", + "misflagged", + "covered mine", + "unsure down", + "8", + "7", + "6", + "5", + "4", + "3", + "2", + "1", + "0" + }; + for (int i = 0; i < states.length; i += 1) { + if (states[i].equals(state)) { + return i; + } + } + return 6; + } + + public void setTileIcon(String state) { + setIcon( + new ImageIcon( + this.image.getSubimage(0, this.SIZE * this.topMargin(state), this.SIZE, this.SIZE))); + } + + public int getValue() { + return this.value; + } + + public boolean isMine() { + return (this.value == -1 || this.dud); + } + + public boolean isUncovered() { + return this.uncovered; + } + + public boolean isFlagged() { + return this.flagged; + } + + public boolean isUnsure() { + return this.unsure; + } + + public boolean isDud() { + return this.dud; + } + + public void setValue(int value) { + this.value = value; + } + + public void setMine() { + this.value = -1; + } + + public void setDud() { + this.dud = true; + } + + public void uncover(boolean expanding) { + this.uncovered = true; + Minesweeper main = this.board.getMain(); + if (this.value >= 0 || this.isDud()) { + if (this.isDud()) { + this.setTileIcon("unsure down"); + } else { + this.setTileIcon(String.valueOf(this.value)); + } + if (this.value == 0 && !this.isDud()) { + this.board.expand(row, col); + } + if (main.getStorage().isCreepy() + && !this.isDud() + && !expanding + && Math.round((float) Math.random() * 10) == 0) { + main.getPlay().boom(); + } + this.board.checkWon(expanding); + } else { + this.setTileIcon("-1"); + main.delGame(); + } + } + + public void uncover() { + this.uncover(false); + } + + public void toggleFlagged() { + Info info = this.board.getMain().getInfo(); + if (!this.flagged && !this.unsure) { + this.flagged = true; + this.setTileIcon("flagged"); + info.getCounter().decrease(); + } else if (this.flagged) { + this.flagged = false; + this.unsure = true; + this.setTileIcon("unsure"); + info.getCounter().increase(); + } else { + this.flagged = false; + this.setTileIcon("covered"); + if (!this.unsure) { + info.getCounter().increase(); + } + this.unsure = false; + } + } + + @Override + public void mousePressed(MouseEvent e) { + Minesweeper main = this.board.getMain(); + if (!main.isGameStarted()) { + main.startGame(); + main.getInfo().getTimer().start(); + if (main.getStorage().isSoundEnabled() && !main.getStorage().isCreepy()) { + main.getPlay().tick(); + } + } + if (SwingUtilities.isLeftMouseButton(e) && !this.uncovered && !this.flagged) { + this.setTileIcon("0"); + } else if (SwingUtilities.isMiddleMouseButton(e)) { + if (!this.flagged) { + this.setTileIcon("0"); + } + main.getInfo().getSmiley().setSmileyIcon("oops"); + } + } + + @Override + public void mouseReleased(MouseEvent e) { + this.board.getMain().getInfo().getSmiley().setSmileyIcon("smile"); + if ((SwingUtilities.isLeftMouseButton(e) || SwingUtilities.isMiddleMouseButton(e)) + && !this.flagged) { + this.uncover(); + } else if (SwingUtilities.isRightMouseButton(e) && !this.uncovered) { + this.toggleFlagged(); + } + } + + @Override + public void mouseExited(MouseEvent e) {} + + @Override + public void mouseEntered(MouseEvent e) {} + + @Override + public void mouseClicked(MouseEvent e) {} +} diff --git a/src/TimerDisplay.java b/src/TimerDisplay.java new file mode 100644 index 0000000..f509aad --- /dev/null +++ b/src/TimerDisplay.java @@ -0,0 +1,83 @@ +import java.awt.*; +import java.util.Timer; +import java.util.TimerTask; +import javax.swing.*; +import javax.swing.BorderFactory; +import javax.swing.plaf.basic.BasicBorders.ButtonBorder; + +public class TimerDisplay extends JPanel { + private static final long serialVersionUID = -1661506846325986431L; + + private Board board; + private Timer timer; + + private Digit hundred; + private Digit tenth; + private Digit first; + + private int seconds; + private boolean stopped; + + public TimerDisplay(Board board) { + this.board = board; + this.hundred = new Digit(this.board); + this.tenth = new Digit(this.board); + this.first = new Digit(this.board); + setBackground(new Color(192, 192, 192)); + setBorder( + BorderFactory.createCompoundBorder( + BorderFactory.createEmptyBorder(5, 5, 5, 5), + new ButtonBorder( + Color.white, Color.white, new Color(128, 128, 128), new Color(128, 128, 128)))); + setVisible(false); + setLayout(new GridLayout(1, 3, 0, 0)); + removeAll(); + add(this.hundred); + add(this.tenth); + add(this.first); + setVisible(true); + } + + public int getTime() { + return this.seconds; + } + + public void start() { + this.stopped = false; + this.timer = new Timer(); + TimerTask tt = + new TimerTask() { + @Override + public void run() { + increase(); + } + }; + this.timer.scheduleAtFixedRate(tt, 0, 1000); + } + + public void stop() { + if (!this.stopped) { + this.timer.cancel(); + this.stopped = true; + } + } + + public void increase() { + this.seconds += 1; + if (this.seconds >= 999) { + this.stop(); + } + this.updateDisplay(); + } + + public void reset() { + this.seconds = 0; + this.updateDisplay(); + } + + public void updateDisplay() { + this.hundred.setDigitIcon(String.valueOf(seconds / 100)); + this.tenth.setDigitIcon(String.valueOf(seconds / 10 % 10)); + this.first.setDigitIcon(String.valueOf(seconds % 10)); + } +}