minesweeper/src/main/java/org/apidesign/demo/minesweeper/MinesModel.java
author Jaroslav Tulach <jaroslav.tulach@apidesign.org>
Sun, 25 May 2014 21:29:22 +0200
changeset 152 e2c6e9a946f1
parent 138 f4d6b81c2f07
child 153 6d2eb47e966b
permissions -rw-r--r--
If possible, move the mine into just clicked square
     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, WON, LOST;
    45     }
    46     
    47     @Model(className = "Row", properties = {
    48         @Property(name = "columns", type = Square.class, array = true)
    49     })
    50     static class RowModel {
    51     }
    52 
    53     @Model(className = "Square", properties = {
    54         @Property(name = "state", type = SquareType.class),
    55         @Property(name = "mine", type = boolean.class)
    56     })
    57     static class SquareModel {
    58         @ComputedProperty static String html(SquareType state) {
    59             if (state == null) return "&nbsp;";
    60             switch (state) {
    61                 case EXPLOSION: return "&#x2717;";
    62                 case UNKNOWN: return "&nbsp;";
    63                 case DISCOVERED: return "&#x2714;";  
    64                 case N_0: return "&nbsp;";
    65             }
    66             return "&#x278" + (state.ordinal() - 1);
    67         }
    68         
    69         @ComputedProperty static String style(SquareType state) {
    70             return state == null ? null : state.toString();
    71         }
    72     }
    73     
    74     enum SquareType {
    75         N_0, N_1, N_2, N_3, N_4, N_5, N_6, N_7, N_8,
    76         UNKNOWN, EXPLOSION, DISCOVERED;
    77         
    78         final boolean isVisible() {
    79             return name().startsWith("N_");
    80         }
    81 
    82         final SquareType moreBombsAround() {
    83             switch (this) {
    84                 case EXPLOSION:
    85                 case UNKNOWN:
    86                 case DISCOVERED:
    87                 case N_8:
    88                     return this;
    89             }
    90             return values()[ordinal() + 1];
    91         }
    92     }
    93     
    94     @ComputedProperty static boolean fieldShowing(GameState state) {
    95         return state != null;
    96     }
    97     
    98     @Function static void showHelp(Mines model) {
    99         model.setState(null);
   100     }
   101     
   102     @Function static void smallGame(Mines model) {
   103         model.init(5, 5, 5);
   104     }
   105     @Function static void normalGame(Mines model) {
   106         model.init(10, 10, 10);
   107     }
   108     
   109     @Function static void giveUp(Mines model) {
   110         showAllBombs(model, SquareType.EXPLOSION);
   111         model.setState(GameState.LOST);
   112     }
   113     
   114     @ModelOperation static void init(Mines model, int width, int height, int mines) {
   115         List<Row> rows = model.getRows();
   116         if (rows.size() != height || rows.get(0).getColumns().size() != width) {
   117             rows = new ArrayList<Row>(height);
   118             for (int y = 0; y < height; y++) {
   119                 Square[] columns = new Square[width];
   120                 for (int x = 0; x < width; x++) {
   121                     columns[x] = new Square(SquareType.UNKNOWN, false);
   122                 }
   123                 rows.add(new Row(columns));
   124             }
   125         } else {
   126             for (Row row : rows) {
   127                 for (Square sq : row.getColumns()) {
   128                     sq.setState(SquareType.UNKNOWN);
   129                     sq.setMine(false);
   130                 }
   131             }
   132         }
   133         
   134         Random r = new Random();
   135         while (mines > 0) {
   136             int x = r.nextInt(width);
   137             int y = r.nextInt(height);
   138             final Square s = rows.get(y).getColumns().get(x);
   139             if (s.isMine()) {
   140                 continue;
   141             }
   142             s.setMine(true);
   143             mines--;
   144         }
   145 
   146         model.setState(GameState.IN_PROGRESS);
   147         if (rows != model.getRows()) {
   148             model.getRows().clear();
   149             model.getRows().addAll(rows);
   150         }
   151     }
   152     
   153     @ModelOperation static void computeMines(Mines model) {
   154         List<Integer> xBombs = new ArrayList<Integer>();
   155         List<Integer> yBombs = new ArrayList<Integer>();
   156         final List<Row> rows = model.getRows();
   157         boolean emptyHidden = false;
   158         SquareType[][] arr = new SquareType[rows.size()][];
   159         for (int y = 0; y < rows.size(); y++) {
   160             final List<Square> columns = rows.get(y).getColumns();
   161             arr[y] = new SquareType[columns.size()];
   162             for (int x = 0; x < columns.size(); x++) {
   163                 Square sq = columns.get(x);
   164                 if (sq.isMine()) {
   165                     xBombs.add(x);
   166                     yBombs.add(y);
   167                 }
   168                 if (sq.getState().isVisible()) {
   169                     arr[y][x] = SquareType.N_0;
   170                 } else {
   171                     if (!sq.isMine()) {
   172                         emptyHidden = true;
   173                     }
   174                 }
   175             }
   176         }
   177         for (int i = 0; i < xBombs.size(); i++) {
   178             int x = xBombs.get(i);
   179             int y = yBombs.get(i);
   180             
   181             incrementAround(arr, x, y);
   182         }
   183         for (int y = 0; y < rows.size(); y++) {
   184             final List<Square> columns = rows.get(y).getColumns();
   185             for (int x = 0; x < columns.size(); x++) {
   186                 Square sq = columns.get(x);
   187                 final SquareType newState = arr[y][x];
   188                 if (newState != null && newState != sq.getState()) {
   189                     sq.setState(newState);
   190                 }
   191             }
   192         }
   193         
   194         if (!emptyHidden) {
   195             model.setState(GameState.WON);
   196             showAllBombs(model, SquareType.DISCOVERED);
   197             AudioClip applause = AudioClip.create("applause.wav");
   198             applause.play();
   199         }
   200     }
   201     
   202     private static void incrementAround(SquareType[][] arr, int x, int y) {
   203         incrementAt(arr, x - 1, y - 1);
   204         incrementAt(arr, x - 1, y);
   205         incrementAt(arr, x - 1, y + 1);
   206 
   207         incrementAt(arr, x + 1, y - 1);
   208         incrementAt(arr, x + 1, y);
   209         incrementAt(arr, x + 1, y + 1);
   210         
   211         incrementAt(arr, x, y - 1);
   212         incrementAt(arr, x, y + 1);
   213     }
   214     
   215     private static void incrementAt(SquareType[][] arr, int x, int y) {
   216         if (y >= 0 && y < arr.length) {
   217             SquareType[] r = arr[y];
   218             if (x >= 0 && x < r.length) {
   219                 SquareType sq = r[x];
   220                 if (sq != null) {
   221                     r[x] = sq.moreBombsAround();
   222                 }
   223             }
   224         }
   225     }
   226     
   227     static void showAllBombs(Mines model, SquareType state) {
   228         for (Row row : model.getRows()) {
   229             for (Square square : row.getColumns()) {
   230                 if (square.isMine()) {
   231                     square.setState(state);
   232                 }
   233             }
   234         }
   235     }
   236     
   237     @Function static void click(Mines model, Square data) {
   238         if (model.getState() != GameState.IN_PROGRESS) {
   239             return;
   240         }
   241         if (data.getState() != SquareType.UNKNOWN) {
   242             return;
   243         }
   244         if (data.isMine()) {
   245             Square fair = atLeastOnePlaceWhereBombCantBe(model);
   246             if (fair == null) {
   247                 if (placeBombElseWhere(model, data)) {
   248                     cleanedUp(model, data);
   249                     return;
   250                 }
   251             }
   252             explosion(model);
   253         } else {
   254             Square takeFrom = tryStealBomb(model, data);
   255             if (takeFrom != null) {
   256                 final Square fair = atLeastOnePlaceWhereBombCantBe(model);
   257                 if (fair != null) {
   258                     takeFrom.setMine(false);
   259                     data.setMine(true);
   260                     explosion(model);
   261                     return;
   262                 }
   263             }
   264             cleanedUp(model, data);
   265         }
   266     }
   267 
   268     private static void cleanedUp(Mines model, Square data) {
   269         AudioClip touch = AudioClip.create("move.mp3");
   270         touch.play();
   271         expandKnown(model, data);
   272         model.computeMines();
   273     }
   274 
   275     private static void explosion(Mines model) {
   276         showAllBombs(model, SquareType.EXPLOSION);
   277         model.setState(GameState.LOST);
   278         AudioClip oops = AudioClip.create("oops.wav");
   279         oops.play();
   280     }
   281     
   282     private static Square tryStealBomb(Mines model, Square data) {
   283         data.setMine(true);
   284         final List<Row> rows = model.getRows();
   285         for (int y = 0; y < rows.size(); y++) {
   286             final List<Square> columns = rows.get(y).getColumns();
   287             for (int x = 0; x < columns.size(); x++) {
   288                 Square sq = columns.get(x);
   289                 if (sq == data) {
   290                     continue;
   291                 }
   292                 if (sq.isMine()) {
   293                     sq.setMine(false);
   294                     final boolean ok = isConsistent(model);
   295                     sq.setMine(true);
   296                     if (ok) {
   297                         data.setMine(false);
   298                         return sq;
   299                     }
   300                 }
   301             }
   302         }
   303         data.setMine(false);        
   304         return null;
   305     }
   306     
   307     private static Square atLeastOnePlaceWhereBombCantBe(Mines model) {
   308         final List<Row> rows = model.getRows();
   309         Square cantBe = null;
   310         int discovered = 0;
   311         for (int y = 0; y < rows.size(); y++) {
   312             final List<Square> columns = rows.get(y).getColumns();
   313             for (int x = 0; x < columns.size(); x++) {
   314                 Square sq = columns.get(x);
   315                 if (sq.getState() == SquareType.UNKNOWN) {
   316                     if (!sq.isMine()) {
   317                         if (tryStealBomb(model, sq) == null) {
   318                             cantBe = sq;
   319                         }
   320                     }
   321                 } else {
   322                     discovered++;
   323                 }
   324             }
   325         }
   326         
   327         if (discovered > 5) {
   328             return cantBe;
   329         }
   330         
   331         return null;
   332     }
   333     
   334     private static boolean placeBombElseWhere(Mines model, Square moveBomb) {
   335         List<Square> ok = new ArrayList<Square>();
   336         moveBomb.setMine(false);
   337         final List<Row> rows = model.getRows();
   338         for (int y = 0; y < rows.size(); y++) {
   339             final List<Square> columns = rows.get(y).getColumns();
   340             for (int x = 0; x < columns.size(); x++) {
   341                 Square sq = columns.get(x);
   342                 if (sq == moveBomb || sq.isMine() || sq.getState().isVisible()) {
   343                     continue;
   344                 }
   345                 sq.setMine(true);
   346                 if (isConsistent(model)) {
   347                     ok.add(sq);
   348                 }
   349                 sq.setMine(false);
   350             }
   351         }
   352         if (ok.isEmpty()) {
   353             moveBomb.setMine(true);
   354             return false;
   355         } else {
   356             int r = new Random().nextInt(ok.size());
   357             ok.get(r).setMine(true);
   358             return true;
   359         }
   360     }
   361     
   362     private static void expandKnown(Mines model, Square data) {
   363         final List<Row> rows = model.getRows();
   364         for (int y = 0; y < rows.size(); y++) {
   365             final List<Square> columns = rows.get(y).getColumns();
   366             for (int x = 0; x < columns.size(); x++) {
   367                 Square sq = columns.get(x);
   368                 if (sq == data) {
   369                     expandKnown(model, x, y);
   370                     return;
   371                 }
   372             }
   373         }
   374     }
   375     private static void expandKnown(Mines model, int x , int y) {
   376         if (y < 0 || y >= model.getRows().size()) {
   377             return;
   378         }
   379         final List<Square> columns = model.getRows().get(y).getColumns();
   380         if (x < 0 || x >= columns.size()) {
   381             return;
   382         }
   383         final Square sq = columns.get(x);
   384         if (sq.getState() == SquareType.UNKNOWN) {
   385             int around = around(model, x, y);
   386             final SquareType t = SquareType.valueOf("N_" + around);
   387             sq.setState(t);
   388             if (t == SquareType.N_0) {
   389                 expandKnown(model, x - 1, y - 1);
   390                 expandKnown(model, x - 1, y);
   391                 expandKnown(model, x - 1, y + 1);
   392                 expandKnown(model, x , y - 1);
   393                 expandKnown(model, x, y + 1);
   394                 expandKnown(model, x + 1, y - 1);
   395                 expandKnown(model, x + 1, y);
   396                 expandKnown(model, x + 1, y + 1);
   397             }
   398         }
   399     }
   400 
   401     private static int around(Mines model, int x, int y) {
   402         return 
   403             minesAt(model, x - 1, y - 1) +
   404             minesAt(model, x - 1, y) +
   405             minesAt(model, x - 1, y + 1) +
   406             minesAt(model, x , y - 1) +
   407             minesAt(model, x, y + 1) +
   408             minesAt(model, x + 1, y - 1) +
   409             minesAt(model, x + 1, y) +
   410             minesAt(model, x + 1, y + 1);
   411     }
   412     
   413     private static int minesAt(Mines model, int x, int y) {
   414         if (y < 0 || y >= model.getRows().size()) {
   415             return 0;
   416         }
   417         final List<Square> columns = model.getRows().get(y).getColumns();
   418         if (x < 0 || x >= columns.size()) {
   419             return 0;
   420         }
   421         Square sq = columns.get(x);
   422         return sq.isMine() ? 1 : 0;
   423     }
   424     
   425     private static boolean isConsistent(Mines m) {
   426         for (int row = 0; row < m.getRows().size(); row++) {
   427             Row r = m.getRows().get(row);
   428             for (int col = 0; col < r.getColumns().size(); col++) {
   429                 Square sq = r.getColumns().get(col);
   430                 if (sq.getState().isVisible()) {
   431                     int around = around(m, col, row);
   432                     if (around != sq.getState().ordinal()) {
   433                         return false;
   434                     }
   435                 }
   436             }
   437         }
   438         return true;
   439     }
   440 
   441     /**
   442      * Called when page is ready
   443      */
   444     public static void main(String... args) throws Exception {
   445         Mines m = new Mines();
   446         m.applyBindings();
   447     }
   448 }