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