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