-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathMinesweeperPanel.java
635 lines (593 loc) · 19.3 KB
/
MinesweeperPanel.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
import java.awt.BorderLayout;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Scanner;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
@SuppressWarnings("serial")
public class MinesweeperPanel extends JPanel implements MouseListener {
//Logical operation data
//Most self-explanatory, remainingLocations represents number of
//unclicked locations on board to allow accurate checking
//of winning condition
private int width, height, numMines, flags, time, remainingLocations;
private enum Difficulty {EASY, INT, HARD, CUST};
private Difficulty diffLevel;
private Timer gameClock;
//High score file location, file writer, list of player names,
// and list of scores corresponding with players
private String scoreLocation = ".hiScores";
private FileWriter scoreWriter;
private String[] hsNames;
private int[] hsScores;
private final int NUM_SCORES = 20;
//Menu pieces
private JMenuBar menuBar;
private JMenuItem easyGame, midGame, hardGame, customize, restart,
highScores, giveUp, quitGame;
//Organization, interactive pieces, and UI configuration
private JPanel board;
public JLabel infoLabel;
private GameButton[][] grid;
private GameButton[] mines;
/** Basic constructor to init the layout, build a menu and timer, and start a game */
public MinesweeperPanel() {
setLayout(new BorderLayout());
buildMenuBarAndInfo();
readHighScores();
gameClock = new Timer(1000, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
time++;
updateStatus();
}
});
newGame(Difficulty.EASY);
}
/*============================================*/
/*============ GAME LOGIC METHODS ============*/
/*============================================*/
/**New game method that sets up the game based on a difficulty.
* For clean resetting, call this and pass in Difficulty.RESET
*
* @param d Any value of the above Enum Difficulty
*/
private void newGame(Difficulty d){
this.diffLevel = d;
switch (this.diffLevel) {
///For standard type difficulty, standard rebuild
case EASY:
width = height = 9;
numMines = 10;
//TODO Get resizing calibrated
//resize(width, MENU_HEIGHT);
break;
case INT:
width = height = 16;
numMines = 40;
//resize(width, MENU_HEIGHT);
break;
case HARD:
width = 30;
height = 16;
numMines = 1;
//resize(width, MENU_HEIGHT);
break;
case CUST:
//Set's width/height/numMines to user's pref
setupBoardByUser();
break;
}
if(board != null)
remove(board);
//Rebuild and replace grid panel any time this happens.
add(buildGamePanel(), BorderLayout.CENTER);
setRandomizedMines();
remainingLocations = width * height;
time = flags = 0;
updateStatus();
gameClock.start();
}
/**
* Clean reset of the current board, just make it
* replayable. No new content.
*/
private void resetCurrentGame(){
for(int i = 0; i < width; i++){
for(GameButton gb:grid[i]){
gb.setIcon(GameButton.BLANK);
gb.activate();
}
}
flags = time = 0;
remainingLocations = width * height;
updateStatus();
gameClock.start();
}
/**
* @param gb GameButton clicked for current move
*/
private void makeMove(GameButton gb){
reveal(gb);
if(gb.isMine()){
loseGame(gb);
}
else if (remainingLocations == mines.length){
winGame();
}
}
/**
* Handles game winning - this is reached if all non-bomb
* items are clicked, so we reveal the mines and ensure that
* flagged mines are just deactivated, which allows them to
* keep the flag icon.
*/
private void winGame() {
gameClock.stop();
for(GameButton mine:mines){
if(!mine.flagged)
reveal(mine);
mine.deactivate();
}
enterHighScore();
displayHighScores();
}
/**
* Caused by clicking a mine or giving up.
*/
private void loseGame(GameButton explosion){
gameClock.stop();
for(int i = 0; i < width; i++){
for(GameButton gb:grid[i]){
if(gb == explosion){
//Cool guys don't look at exploBOOOOOM
gb.setIcon(GameButton.BANG);
}else if(gb.flagged && !gb.isMine()){
gb.setIcon(GameButton.F_FLAG);
}else if(!gb.flagged){
reveal(gb);
}
gb.deactivate();
}
}
JOptionPane.showMessageDialog(this, "Booo you died :(");
}
/**
* Confirm shutdown. Saves currently loaded scores to disk, pauses game
* clock when activated, and if cancelled, resumes the clock
*
* Currently returns an int as a result of making this a valid
* parameter for the method JFrame.setDefaultCloseOperation,
* which didn't work
*/
public void quitGame(){
gameClock.stop();
writeHighScores();
System.exit(0);
}
/**Updater for the info label */
private void updateStatus() {
infoLabel.setText("Mines Left: "+Math.max(numMines-flags, 0)+" Time: "+time);
}
/**This reveals a game button upon clicking.
* If it's flagged, then we assume the click was in error, and we don't
* proceed.
* Then, we set an index up to pass to the image method of the button.
* Qe loop all neighbors and
* increment index from 0 to find neighboring mines.
* If the resulting index is zero, recurse on all adjacent
* and diagonal items to clear out empty lot.
*
* @param gb Button to reveal, does nothing if it's flagged or a mine
*/
private void reveal(GameButton gb){
if(gb.flagged || gb.isMine() || gb.revealed){
if(gb.isMine())
gb.setIcon(GameButton.BOMB);
return;
}
gb.deactivate();
int numMinesNearby = 0;
remainingLocations --;
numMinesNearby = 0;
for (int dX = -1; dX <= 1; dX++) {
for (int dY = -1; dY <= 1; dY++) {
int pX = gb.x + dX, pY = gb.y + dY;
if(validLocation(pX, pY) && grid[pX][pY].type == ButtonType.MINE){
numMinesNearby++;
}
}
}
if (numMinesNearby==0){
/* Algorithm to clean up any blank squares
* Recursively grab all neighbors and call reveal on
* them if they're blank or adjacent to a blank. */
for (int dX = -1; dX <= 1; dX++) {
for (int dY = -1; dY <= 1; dY++) {
int pX = gb.x + dX, pY = gb.y + dY;
if (validLocation(pX, pY) &&
grid[pX][pY].isEnabled() &&
!grid[pX][pY].flagged){
reveal(grid[pX][pY]);
}
}
}
}
gb.setIcon(numMinesNearby);
}
/**Handles flagging/deflagging a button. Occurs
* on right click of a valid, unrevealed location
*
* @param move Valid, unrevealed button clicked
* on board to flag.
*/
private void flag(GameButton move){
if(move.flagged){
move.setIcon(GameButton.BLANK);
move.flagged = false;
flags--;
} else{
move.flagged = true;
move.setIcon(GameButton.FLAG);
flags++;
}
updateStatus();
}
/** A game logic method thats only used in reveal.
*
* @param pX potential x location to check for validity
* @param pY potential y location to check for validity
*
* @return True if location is valid in the grid
*/
private boolean validLocation(int pX, int pY) {
//If any of the conditions in parens are true, location is invalid
return !(pX < 0 || pX >= grid.length || pY < 0 || pY >= grid[0].length);
}
/*================================================================*/
/*============HIGH SCORE HANDLING, SAVING, AND LOADING============*/
/*================================================================*/
/**
* Display high scores after every game. Entering a high
* score is handled by winGame()
*/
private void displayHighScores() {
//Prevent this from counting as time used to play
gameClock.stop();
//Build a table to use as the message for display on a popup
StringBuilder scoreTable = new StringBuilder();
scoreTable.append(String.format("%-25s%s\n","Player:","Score:"));
int bestScoreIndex = this.diffLevel.ordinal()*5;
int worstScoreIndex = this.diffLevel.ordinal()*5 + 4;
for(int i = bestScoreIndex; i <= worstScoreIndex; i++){
scoreTable.append(String.format("%-36s\n", hsNames[i]));
scoreTable.append(String.format("%36d\n", hsScores[i]));
}
informUser("High Scores for "+this.diffLevel, scoreTable.toString());
//If the game isn't over, resume clock
if(remainingLocations > numMines)
gameClock.start();
}
/**
* Allows entering of a name for high score table.
*/
private void enterHighScore() {
if(this.time >= hsScores[this.diffLevel.ordinal()*5+4]){
return; //Not a high score. Get outta here.
}
//Try twice to get a name, redundancy is redundantly safe
String playerName = promptUser("New High Score on "+this.diffLevel+"!",
"Well done! Would you kindly give us your name?");
if(playerName == null || playerName.trim().length() < 1){
playerName = promptUser("New High Score on "+this.diffLevel+"!",
"Seriously though. Not even a nickname?");
if(playerName == null || playerName.trim().length() < 1){
informUser("Score Not Recorded",
"Well, you're lame. Check out everyone that did better than you!");
return;
}
}
int bestScoreForDifficultyIndex = this.diffLevel.ordinal()*5;
int worstScoreForDifficultyIndex = this.diffLevel.ordinal()*5 + 4;
//Find insertion point from the top down
int insertionIdx = bestScoreForDifficultyIndex;
while(this.time >= hsScores[insertionIdx])
insertionIdx ++;
//Make room for new score, overwrite lowest score
for(int swap = worstScoreForDifficultyIndex - 1;
swap >= insertionIdx; swap--){
hsScores[swap + 1] = hsScores[swap];
hsNames[swap + 1] = hsNames[swap];
}
hsScores[insertionIdx] = time;
hsNames[insertionIdx] = playerName;
}
/*===========================================================*/
/*============ SCORE SAVE AND LOAD FUNCTIONALITY ============*/
/*===========================================================*/
/**
* Reads high scores from disk. Called on construction to read
* in data, at which point all scores are held in memory
* until valid closure of the game, which should always
* call writeHighScores
*
* File structure, 20 lines of:
* [name,score\n]
*
* Lines 1-5 are easy mode high scores, 6-10 are intermediate,
* 11-15 are for hard mode, and 16-20 for custom mode.
*/
private void readHighScores(){
System.out.println("Reading from disk...");
File scoreFile = new File(scoreLocation);
try{
if(!scoreFile.exists() || !(scoreFile.length() > 10)){
//Below method prints a default set of data to
// './.hiScores'
initializeScoreFile(scoreFile);
scoreFile = new File(scoreLocation);
}
//Read the scores file into local mem
Scanner in = new Scanner(scoreFile);
hsNames = new String[NUM_SCORES];
hsScores = new int[NUM_SCORES];
for (int i = 0; i < NUM_SCORES; i++) {
String[] raw = in.nextLine().split(",");
hsNames[i] = raw[0];
hsScores[i] = Integer.parseInt(raw[1]);
// System.out.printf("Line #%d: %s [%s]\n", i+1, hsNames[i], hsScores[i]);
}
in.close();
scoreWriter = new FileWriter(scoreFile);
}catch(Exception e){
System.err.println("Problem reading high scores. Blame Jake.");
System.exit(0);
}
}
/**Function to initialize a first-run high scores file. This should
* only be called on first run per build, or if the previous file
* was corrupted or removed somehow.
*
* Tries to build (or rebuild) score file, fill it with default data,
* then finalize it and close the writer object. Should save it properly
* to disk.
*
* @param scoreFile Problematic file location to set up as high score location
* @param fileWriter Writer attached to the file
* @return Valid file for scores
*/
private void initializeScoreFile(File scores) throws IOException{
//Ensure we have a file to work with before building writer
if(scores.exists())
scores.delete();
scores.createNewFile();
FileWriter overWriter = new FileWriter(scores);
//Default data to plug into file
for (int i = 0; i < NUM_SCORES; i++) {
overWriter.write(String.format("N/A,%d\n",(9985+i)));
}
System.out.println("Error at './.hiScores', scores file rewritten with 20 default values");
overWriter.close();
}
/**
* Write current high score table to disk. It's held in memory
* until this step. This is called by quitGame on confirmation
* of closure, and quitGame should be called on any normal
* close option to ensure these get saved
*/
private void writeHighScores(){
System.out.println("Saving to disk...");
try{
scoreWriter.flush();
//Write scores and names to disk
for (int i = 0; i < NUM_SCORES; i++) {
String tableEntry = hsNames[i]+","+hsScores[i]+"\n";
scoreWriter.write(tableEntry);
// System.out.printf("Line #%d: %s [%s]\n", i+1, hsNames[i], hsScores[i]);
}
scoreWriter.close();
}catch(Exception e){
System.err.println("Problem writing high scores... Blame Jake.");
}
}
//================USER INTERACTION UTILITIES==============//
/**Yes or no confirmation dialog box
*
* @param title Title of the confirmation popup
* @param message Message of the popup
* @return True iff confirmed
*/
// private boolean userConfirms(String title, String message){
// return JOptionPane.showConfirmDialog(this, message, title,
// JOptionPane.YES_NO_OPTION, JOptionPane.PLAIN_MESSAGE)
// == JOptionPane.YES_OPTION;
// }
/**Informational popup to user
*
* @param title Title of the info popup
* @param message Message of the popup
*/
private void informUser(String title, String message){
JOptionPane.showMessageDialog(this, message, title, JOptionPane.PLAIN_MESSAGE);
}
/**Prompt user for input as a string, used one string at a time
*
* @param title Title of the info popup
* @param message Message of the popup
*/
private String promptUser(String title, String message){
return JOptionPane.showInputDialog(this, message, title, JOptionPane.QUESTION_MESSAGE);
}
//==========================================================
//============INTERFACE CONSTRUCTION METHODS================
//==========================================================
/**
* @return A panel with reset/timer/score and game buttons
*/
private JPanel buildGamePanel(){
if(board != null){
remove(board);
revalidate();
}
board = new JPanel();
board.setLayout(new GridLayout(width, height));
grid = new GameButton[width][height];
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
(grid[x][y] = new GameButton(x, y)).addMouseListener(this);
board.add(grid[x][y]);
grid[x][y].activate();
}
}
return board;
}
/**
* Handle a custom game round by the user. Pop up a small form
* to ask for values in a certain range, and input those on
* confirmation.
*/
private void setupBoardByUser(){
//TODO Custom: Any values from 8 × 8 or 9 × 9 to 30 × 24 field, with 10 to 668 mines.
/*
* A type of input panel class may be really handy here?
* Use sliders for all values, then button to fire setup event?
* Run input listener on text to give hints on input?
width = parse(input(Ask for width));
height = parse(input(Ask for height));
numMines = parse(input(Ask for mine num));
*/
informUser("Customize", "This is not what you meant to click on.");
newGame(Difficulty.EASY);
}
/**
* Builds the mine array, then sets the mines in the game board
*/
private void setRandomizedMines() {
mines = new GameButton[numMines];
for(int i = 0; i < numMines; ){
int randX = (int)(Math.random() * width);
int randY = (int)(Math.random() * height);
//Control incrementing to avoid double-mining a spot
if(grid[randX][randY].type != ButtonType.MINE){
mines[i] = grid[randX][randY];
mines[i].type = ButtonType.MINE;
i++;
}
}
}
//============Game Menu Setup================
/**
* Sets up the menu bar at the top of the window
*/
private void buildMenuBarAndInfo(){
//Build up the full menu
menuBar = new JMenuBar();
menuBar.add(buildFileMenu());
//Set up status
infoLabel = new JLabel();
flags = numMines = time = 0;
add(infoLabel, BorderLayout.SOUTH);
updateStatus();
}
/**
* @return Game options menu with Save/Load and other features
*/
private JMenu buildFileMenu() {
JMenu gameMenu = new JMenu(" Menu ");
//Build new game sub-menu
JMenu newGame = new JMenu("New Game");
(easyGame = new JMenuItem("Easy")).addMouseListener(this);
(midGame = new JMenuItem("Intermediate")).addMouseListener(this);
(hardGame = new JMenuItem("Hard")).addMouseListener(this);
(customize = new JMenuItem("Custom...")).addMouseListener(this);
newGame.add(easyGame);
newGame.add(midGame);
newGame.add(hardGame);
newGame.add(customize);
//Build options sub-menu
JMenu opt = new JMenu("Options");
(highScores = new JMenuItem("High Scores")).addMouseListener(this);
(restart = new JMenuItem("Restart Game")).addMouseListener(this);
(giveUp = new JMenuItem("Give Up")).addMouseListener(this);
opt.add(highScores);
opt.addSeparator();
opt.add(restart);
opt.add(giveUp);
//Finish up the file menu
(quitGame = new JMenuItem("Quit")).addMouseListener(this);
gameMenu.add(newGame);
gameMenu.add(opt);
gameMenu.addSeparator();
gameMenu.add(quitGame);
return gameMenu;
}
/**Allows access to the built menu bar for the top-level frame
* to set it's menu to the menu constructed here
*
* @return The menu bar for this program.
*/
public JMenuBar menuBar(){
return this.menuBar;
}
@Override
public void mousePressed(MouseEvent e) {
//All actions are mouse driven, no hotkeys implemented yet
Object selection = e.getComponent();
//New game actions
if(selection == easyGame){
newGame(Difficulty.EASY);
}else if(selection == midGame){
newGame(Difficulty.INT);
}else if(selection == hardGame){
newGame(Difficulty.HARD);
}else if(selection == customize){
newGame(Difficulty.CUST);
}else if(selection == restart){
resetCurrentGame();
}
//Giving up, only works if game clock is moving
else if(selection == giveUp){
if(gameClock.isRunning())
loseGame(null);
}
//High score and quit actions
else if(selection == highScores){
displayHighScores();
} else if(selection == quitGame){
quitGame();
} else {
//This state is inductively reached only on
//an arbitrary random game button
GameButton move = (GameButton)selection;
if(move.isEnabled()){
if(SwingUtilities.isLeftMouseButton(e) &&
!move.flagged)
makeMove(move);
if(SwingUtilities.isRightMouseButton(e) ){
flag(move);
}
}
}
}
/*============ UNIMPLEMENTED METHODS FROM MouseListener============*/
@Override
public void mouseReleased(MouseEvent e) {}
@Override
public void mouseClicked(MouseEvent e) {}
@Override
public void mouseEntered(MouseEvent e) {}
@Override
public void mouseExited(MouseEvent e) {}
}