import java.awt.*; import javax.swing.*; // Enum with the difficuulty options 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; // Use nonfinal ints with getter methods because custom needs to be able to // change its stats 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; } // Show a popup window to select attributes of the custom game, return // whether or not it succeeded 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(); } } // Initialize the text fields 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 // a JComponent[][] to store the structure of the input window JComponent[][] items = { { new JLabel("Height:"), rowsField }, { new JLabel("Width:"), colsField }, { new JLabel("Mines:"), minesField }, }; // Show the message window now and get the result int option = JOptionPane.showConfirmDialog(frame, getMessagePanel(items), "Custom Board", JOptionPane.OK_CANCEL_OPTION); // If user didn't hit OK, bail out now if (option != JOptionPane.OK_OPTION) return false; // Bail out if fields can't be parsed into integers 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; } // Even though the text fields should technically only allow valid // inputs, we should 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; // Everything succeeded, so now actually set the new stats CUSTOM.setStats(rows, cols, mines); return true; } // Helper method to implement something that every language except Java // already has builtin public static int clampInt(int value, int minValue, int maxValue) { return Math.max(Math.min(value, maxValue), minValue); } // The regular formula for maximum mines is the product of rows and columns // minus one, but also cap it at 999 private static int getMaxMines(int rows, int cols) { return Math.min((rows - 1) * (cols - 1), MAX_MINES); } // Create a JPanel that has the items in a custom gridlike layout in which // every row is as tall as the tallest element in that row and every column // is as wide as the widest element in that column private static JPanel getMessagePanel(JComponent[][] items) { // The padding between the elements and from the walls final int PADDING = 6; // The number of columns final int COLS = 2; // Null layout because the only layout manager that can do what we we // want is SpringLayout which the same complexity JPanel messagePanel = new JPanel(null); // Add the items to the panel, setting the label alignment along the way for (JComponent[] row : items) { JLabel label = (JLabel) row[0]; label.setLabelFor(row[1]); label.setHorizontalAlignment(JLabel.TRAILING); messagePanel.add(label); messagePanel.add(row[1]); } // The maximum width of all the elements of each column int[] maxWidths = new int[COLS]; // The maximum height of each elements of each row int[] maxHeights = new int[items.length]; // The x-position of all elements of each column int[] x = new int[COLS + 1]; // The y-position of all the elements of each row int[] y = new int[items.length + 1]; // The last value of x and y hold the size of the panel itself // Set x and maxWidths x[0] = PADDING; for (int c = 0; c < COLS; c++) { // Get the maximum width among items of this row int maxWidth = Integer.MIN_VALUE; for (int r = 0; r < items.length; r++) maxWidth = Math.max(maxWidth, (int) items[r][c].getPreferredSize().getWidth()); maxWidths[c] = maxWidth; // x-coordinate of the next item is the x-coordinate of this one // plus this one's width plus padding x[c + 1] = x[c] + maxWidth + PADDING; } // Set y and maxHeights the same way y[0] = 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()); maxHeights[r] = maxHeight; y[r + 1] = y[r] + maxHeight + PADDING; } // Actually put into use the values we just got for (int r = 0; r < items.length; r++) for (int c = 0; c < COLS; c++) items[r][c].setBounds(x[c], y[r], maxWidths[c], maxHeights[r]); // Use the last values of x and y to set the size of the panel messagePanel.setPreferredSize(new Dimension(x[COLS], y[items.length])); return messagePanel; } }