minesweeper/src/main/java/org/apidesign/demo/minesweeper/MinesModel.java
author Jaroslav Tulach <jtulach@netbeans.org>
Thu, 17 Jul 2014 08:23:25 +0200
changeset 174 a57b2414b855
parent 166 e6667c8206fc
child 176 1e482b09b814
permissions -rw-r--r--
One should not be able to win the game by marking all the squares
     1 /**
     2  * The MIT License (MIT)
     3  *
     4  * Copyright (C) 2013 Jaroslav Tulach <jaroslav.tulach@apidesign.org>
     5  *
     6  * Permission is hereby granted, free of charge, to any person obtaining a copy
     7  * of this software and associated documentation files (the "Software"), to deal
     8  * in the Software without restriction, including without limitation the rights
     9  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    10  * copies of the Software, and to permit persons to whom the Software is
    11  * furnished to do so, subject to the following conditions:
    12  *
    13  * The above copyright notice and this permission notice shall be included in
    14  * all copies or substantial portions of the Software.
    15  *
    16  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    17  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    18  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    19  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    20  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    21  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    22  * THE SOFTWARE.
    23  */
    24 package org.apidesign.demo.minesweeper;
    25 
    26 import java.util.ArrayList;
    27 import java.util.List;
    28 import java.util.Random;
    29 import net.java.html.json.ComputedProperty;
    30 import net.java.html.json.Function;
    31 import net.java.html.json.Model;
    32 import net.java.html.json.ModelOperation;
    33 import net.java.html.json.Property;
    34 import net.java.html.sound.AudioClip;
    35 
    36 /** Model of the mine field.
    37  */
    38 @Model(className = "Mines", properties = {
    39     @Property(name = "state", type = MinesModel.GameState.class),
    40     @Property(name = "rows", type = Row.class, array = true),
    41 })
    42 public final class MinesModel {
    43     enum GameState {
    44         IN_PROGRESS, MARKING_MINE, WON, LOST;
    45     }
    46     
    47     @ComputedProperty static String gameStyle(GameState state) {
    48         return state == GameState.MARKING_MINE ? "MARKING" : null;
    49     }
    50     
    51     @Model(className = "Row", properties = {
    52         @Property(name = "columns", type = Square.class, array = true)
    53     })
    54     static class RowModel {
    55     }
    56 
    57     @Model(className = "Square", properties = {
    58         @Property(name = "state", type = SquareType.class),
    59         @Property(name = "mine", type = boolean.class)
    60     })
    61     static class SquareModel {
    62         @ComputedProperty static String style(SquareType state) {
    63             return state == null ? null : state.toString();
    64         }
    65     }
    66     
    67     enum SquareType {
    68         N_0, N_1, N_2, N_3, N_4, N_5, N_6, N_7, N_8,
    69         UNKNOWN, EXPLOSION, DISCOVERED, MARKED;
    70         
    71         final boolean isVisible() {
    72             return name().startsWith("N_");
    73         }
    74 
    75         final SquareType moreBombsAround() {
    76             switch (this) {
    77                 case EXPLOSION:
    78                 case UNKNOWN:
    79                 case DISCOVERED:
    80                 case N_8:
    81                     return this;
    82             }
    83             return values()[ordinal() + 1];
    84         }
    85     }
    86     
    87     @ComputedProperty static boolean fieldShowing(GameState state) {
    88         return state != null;
    89     }
    90     
    91     @Function static void showHelp(Mines model) {
    92         model.setState(null);
    93     }
    94     
    95     @Function static void smallGame(Mines model) {
    96         model.init(5, 5, 5);
    97     }
    98     @Function static void normalGame(Mines model) {
    99         model.init(10, 10, 10);
   100     }
   101     
   102     @Function static void giveUp(Mines model) {
   103         showAllBombs(model, SquareType.EXPLOSION);
   104         model.setState(GameState.LOST);
   105     }
   106     
   107     @Function static void markMine(Mines model) {
   108         if (model.getState() == GameState.IN_PROGRESS) {
   109             model.setState(GameState.MARKING_MINE);
   110         }
   111     }
   112     
   113     @ModelOperation static void init(Mines model, int width, int height, int mines) {
   114         List<Row> rows = model.getRows();
   115         if (rows.size() != height || rows.get(0).getColumns().size() != width) {
   116             rows = new ArrayList<Row>(height);
   117             for (int y = 0; y < height; y++) {
   118                 Square[] columns = new Square[width];
   119                 for (int x = 0; x < width; x++) {
   120                     columns[x] = new Square(SquareType.UNKNOWN, false);
   121                 }
   122                 rows.add(new Row(columns));
   123             }
   124         } else {
   125             for (Row row : rows) {
   126                 for (Square sq : row.getColumns()) {
   127                     sq.setState(SquareType.UNKNOWN);
   128                     sq.setMine(false);
   129                 }
   130             }
   131         }
   132         
   133         Random r = new Random();
   134         while (mines > 0) {
   135             int x = r.nextInt(width);
   136             int y = r.nextInt(height);
   137             final Square s = rows.get(y).getColumns().get(x);
   138             if (s.isMine()) {
   139                 continue;
   140             }
   141             s.setMine(true);
   142             mines--;
   143         }
   144 
   145         model.setState(GameState.IN_PROGRESS);
   146         if (rows != model.getRows()) {
   147             model.getRows().clear();
   148             model.getRows().addAll(rows);
   149         }
   150     }
   151     
   152     @ModelOperation static void computeMines(Mines model) {
   153         List<Integer> xBombs = new ArrayList<Integer>();
   154         List<Integer> yBombs = new ArrayList<Integer>();
   155         final List<Row> rows = model.getRows();
   156         boolean emptyHidden = false;
   157         SquareType[][] arr = new SquareType[rows.size()][];
   158         for (int y = 0; y < rows.size(); y++) {
   159             final List<Square> columns = rows.get(y).getColumns();
   160             arr[y] = new SquareType[columns.size()];
   161             for (int x = 0; x < columns.size(); x++) {
   162                 Square sq = columns.get(x);
   163                 if (sq.isMine()) {
   164                     xBombs.add(x);
   165                     yBombs.add(y);
   166                 }
   167                 if (sq.getState().isVisible()) {
   168                     arr[y][x] = SquareType.N_0;
   169                 } else {
   170                     if (!sq.isMine()) {
   171                         emptyHidden = true;
   172                     }
   173                 }
   174             }
   175         }
   176         for (int i = 0; i < xBombs.size(); i++) {
   177             int x = xBombs.get(i);
   178             int y = yBombs.get(i);
   179             
   180             incrementAround(arr, x, y);
   181         }
   182         for (int y = 0; y < rows.size(); y++) {
   183             final List<Square> columns = rows.get(y).getColumns();
   184             for (int x = 0; x < columns.size(); x++) {
   185                 Square sq = columns.get(x);
   186                 final SquareType newState = arr[y][x];
   187                 if (newState != null && newState != sq.getState()) {
   188                     sq.setState(newState);
   189                 }
   190             }
   191         }
   192         
   193         if (!emptyHidden) {
   194             model.setState(GameState.WON);
   195             showAllBombs(model, SquareType.DISCOVERED);
   196             AudioClip applause = AudioClip.create("applause.mp3");
   197             applause.play();
   198         }
   199     }
   200     
   201     private static void incrementAround(SquareType[][] arr, int x, int y) {
   202         incrementAt(arr, x - 1, y - 1);
   203         incrementAt(arr, x - 1, y);
   204         incrementAt(arr, x - 1, y + 1);
   205 
   206         incrementAt(arr, x + 1, y - 1);
   207         incrementAt(arr, x + 1, y);
   208         incrementAt(arr, x + 1, y + 1);
   209         
   210         incrementAt(arr, x, y - 1);
   211         incrementAt(arr, x, y + 1);
   212     }
   213     
   214     private static void incrementAt(SquareType[][] arr, int x, int y) {
   215         if (y >= 0 && y < arr.length) {
   216             SquareType[] r = arr[y];
   217             if (x >= 0 && x < r.length) {
   218                 SquareType sq = r[x];
   219                 if (sq != null) {
   220                     r[x] = sq.moreBombsAround();
   221                 }
   222             }
   223         }
   224     }
   225     
   226     static void showAllBombs(Mines model, SquareType state) {
   227         for (Row row : model.getRows()) {
   228             for (Square square : row.getColumns()) {
   229                 if (square.isMine()) {
   230                     square.setState(state);
   231                 }
   232             }
   233         }
   234     }
   235     
   236     @Function static void click(Mines model, Square data) {
   237         if (model.getState() == GameState.MARKING_MINE) {
   238             if (data.getState() == SquareType.UNKNOWN) {
   239                 data.setState(SquareType.MARKED);
   240                 if (allMarked(model)) {
   241                     model.setState(GameState.WON);
   242                     return;
   243                 }
   244             }
   245             model.setState(GameState.IN_PROGRESS);
   246             return;
   247         }
   248         if (model.getState() != GameState.IN_PROGRESS) {
   249             return;
   250         }
   251         if (data.getState() == SquareType.MARKED) {
   252             data.setState(SquareType.UNKNOWN);
   253             if (allMarked(model)) {
   254                 model.setState(GameState.WON);
   255             }
   256             return;
   257         }
   258         if (data.getState() != SquareType.UNKNOWN) {
   259             return;
   260         }
   261         if (data.isMine()) {
   262             Square fair = atLeastOnePlaceWhereBombCantBe(model);
   263             if (fair == null) {
   264                 if (placeBombElseWhere(model, data)) {
   265                     cleanedUp(model, data);
   266                     return;
   267                 }
   268             }
   269             explosion(model);
   270         } else {
   271             Square takeFrom = tryStealBomb(model, data);
   272             if (takeFrom != null) {
   273                 final Square fair = atLeastOnePlaceWhereBombCantBe(model);
   274                 if (fair != null) {
   275                     takeFrom.setMine(false);
   276                     data.setMine(true);
   277                     explosion(model);
   278                     return;
   279                 }
   280             }
   281             cleanedUp(model, data);
   282         }
   283     }
   284 
   285     private static void cleanedUp(Mines model, Square data) {
   286         AudioClip touch = AudioClip.create("move.mp3");
   287         touch.play();
   288         expandKnown(model, data);
   289         model.computeMines();
   290     }
   291 
   292     private static void explosion(Mines model) {
   293         showAllBombs(model, SquareType.EXPLOSION);
   294         model.setState(GameState.LOST);
   295         AudioClip oops = AudioClip.create("oops.mp3");
   296         oops.play();
   297     }
   298     
   299     private static Square tryStealBomb(Mines model, Square data) {
   300         data.setMine(true);
   301         final List<Row> rows = model.getRows();
   302         for (int y = 0; y < rows.size(); y++) {
   303             final List<Square> columns = rows.get(y).getColumns();
   304             for (int x = 0; x < columns.size(); x++) {
   305                 Square sq = columns.get(x);
   306                 if (sq == data) {
   307                     continue;
   308                 }
   309                 if (sq.isMine()) {
   310                     sq.setMine(false);
   311                     final boolean ok = isConsistent(model);
   312                     sq.setMine(true);
   313                     if (ok) {
   314                         data.setMine(false);
   315                         return sq;
   316                     }
   317                 }
   318             }
   319         }
   320         data.setMine(false);        
   321         return null;
   322     }
   323     
   324     private static Square atLeastOnePlaceWhereBombCantBe(Mines model) {
   325         final List<Row> rows = model.getRows();
   326         Square cantBe = null;
   327         int discovered = 0;
   328         for (int y = 0; y < rows.size(); y++) {
   329             final List<Square> columns = rows.get(y).getColumns();
   330             for (int x = 0; x < columns.size(); x++) {
   331                 Square sq = columns.get(x);
   332                 if (sq.getState() == SquareType.UNKNOWN) {
   333                     if (!sq.isMine()) {
   334                         if (tryStealBomb(model, sq) == null) {
   335                             cantBe = sq;
   336                         }
   337                     }
   338                 } else {
   339                     discovered++;
   340                 }
   341             }
   342         }
   343         
   344         if (discovered > 5) {
   345             return cantBe;
   346         }
   347         
   348         return null;
   349     }
   350     
   351     private static boolean placeBombElseWhere(Mines model, Square moveBomb) {
   352         List<Square> ok = new ArrayList<Square>();
   353         moveBomb.setMine(false);
   354         final List<Row> rows = model.getRows();
   355         for (int y = 0; y < rows.size(); y++) {
   356             final List<Square> columns = rows.get(y).getColumns();
   357             for (int x = 0; x < columns.size(); x++) {
   358                 Square sq = columns.get(x);
   359                 if (sq == moveBomb || sq.isMine() || sq.getState().isVisible()) {
   360                     continue;
   361                 }
   362                 sq.setMine(true);
   363                 if (isConsistent(model)) {
   364                     ok.add(sq);
   365                 }
   366                 sq.setMine(false);
   367             }
   368         }
   369         if (ok.isEmpty()) {
   370             moveBomb.setMine(true);
   371             return false;
   372         } else {
   373             int r = new Random().nextInt(ok.size());
   374             ok.get(r).setMine(true);
   375             return true;
   376         }
   377     }
   378     
   379     private static void expandKnown(Mines model, Square data) {
   380         final List<Row> rows = model.getRows();
   381         for (int y = 0; y < rows.size(); y++) {
   382             final List<Square> columns = rows.get(y).getColumns();
   383             for (int x = 0; x < columns.size(); x++) {
   384                 Square sq = columns.get(x);
   385                 if (sq == data) {
   386                     expandKnown(model, x, y);
   387                     return;
   388                 }
   389             }
   390         }
   391     }
   392     private static void expandKnown(Mines model, int x , int y) {
   393         if (y < 0 || y >= model.getRows().size()) {
   394             return;
   395         }
   396         final List<Square> columns = model.getRows().get(y).getColumns();
   397         if (x < 0 || x >= columns.size()) {
   398             return;
   399         }
   400         final Square sq = columns.get(x);
   401         if (sq.getState() == SquareType.UNKNOWN) {
   402             int around = around(model, x, y);
   403             final SquareType t = SquareType.valueOf("N_" + around);
   404             sq.setState(t);
   405             if (t == SquareType.N_0) {
   406                 expandKnown(model, x - 1, y - 1);
   407                 expandKnown(model, x - 1, y);
   408                 expandKnown(model, x - 1, y + 1);
   409                 expandKnown(model, x , y - 1);
   410                 expandKnown(model, x, y + 1);
   411                 expandKnown(model, x + 1, y - 1);
   412                 expandKnown(model, x + 1, y);
   413                 expandKnown(model, x + 1, y + 1);
   414             }
   415         }
   416     }
   417 
   418     private static int around(Mines model, int x, int y) {
   419         return 
   420             minesAt(model, x - 1, y - 1) +
   421             minesAt(model, x - 1, y) +
   422             minesAt(model, x - 1, y + 1) +
   423             minesAt(model, x , y - 1) +
   424             minesAt(model, x, y + 1) +
   425             minesAt(model, x + 1, y - 1) +
   426             minesAt(model, x + 1, y) +
   427             minesAt(model, x + 1, y + 1);
   428     }
   429     
   430     private static int minesAt(Mines model, int x, int y) {
   431         if (y < 0 || y >= model.getRows().size()) {
   432             return 0;
   433         }
   434         final List<Square> columns = model.getRows().get(y).getColumns();
   435         if (x < 0 || x >= columns.size()) {
   436             return 0;
   437         }
   438         Square sq = columns.get(x);
   439         return sq.isMine() ? 1 : 0;
   440     }
   441     
   442     private static boolean isConsistent(Mines m) {
   443         for (int row = 0; row < m.getRows().size(); row++) {
   444             Row r = m.getRows().get(row);
   445             for (int col = 0; col < r.getColumns().size(); col++) {
   446                 Square sq = r.getColumns().get(col);
   447                 if (sq.getState().isVisible()) {
   448                     int around = around(m, col, row);
   449                     if (around != sq.getState().ordinal()) {
   450                         return false;
   451                     }
   452                 }
   453             }
   454         }
   455         return true;
   456     }
   457     
   458     private static boolean allMarked(Mines m) {
   459         for (Row r : m.getRows()) {
   460             for (Square sq : r.getColumns()) {
   461                 if (sq.isMine() == (sq.getState() != SquareType.MARKED)) {
   462                     return false;
   463                 }
   464             }
   465         }
   466         for (Row r : m.getRows()) {
   467             for (Square sq : r.getColumns()) {
   468                 if (sq.isMine()) {
   469                     sq.setState(SquareType.DISCOVERED);
   470                 } else {
   471                     sq.setState(SquareType.N_0);
   472                 }
   473             }
   474         }
   475         computeMines(m);
   476         return true;
   477     }
   478 
   479     /**
   480      * Called when page is ready
   481      */
   482     public static void main(String... args) throws Exception {
   483         Mines m = new Mines();
   484         m.applyBindings();
   485     }
   486 }