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