main.dart 9.13 KB
Newer Older
1 2
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
3

4 5
import 'dart:math';

6
import 'package:flutter/gestures.dart';
7 8 9 10
import 'package:flutter/material.dart';
import 'package:flutter/painting.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
11

12 13 14
// Classic minesweeper-inspired game. The mouse controls are standard
// except for left + right combo which is not implemented. For touch,
// the duration of the pointer determines probing versus flagging.
15
//
16 17 18 19 20
// There are only 3 classes to understand. MineDiggerApp, which is
// contains all the logic and two classes that describe the mines:
// CoveredMineNode and ExposedMineNode, none of them holding state.

// Colors for each mine count (0-8):
21 22 23 24 25 26 27 28 29 30
const List<Color> textColors = const <Color>[
  const Color(0xFF555555),
  const Color(0xFF0094FF), // blue
  const Color(0xFF13A023), // green
  const Color(0xFFDA1414), // red
  const Color(0xFF1E2347), // black
  const Color(0xFF7F0037), // dark red
  const Color(0xFF000000),
  const Color(0xFF000000),
  const Color(0xFF000000),
31 32
];

Hixie's avatar
Hixie committed
33
final List<TextStyle> textStyles = textColors.map((Color color) {
34
  return new TextStyle(color: color, fontWeight: FontWeight.bold, textAlign: TextAlign.center);
35
}).toList();
36

37 38
enum CellState { covered, exploded, cleared, flagged, shown }

Adam Barth's avatar
Adam Barth committed
39 40 41 42 43
class MineDigger extends StatefulComponent {
  MineDiggerState createState() => new MineDiggerState();
}

class MineDiggerState extends State<MineDigger> {
44 45 46 47 48 49 50 51 52 53 54
  static const int rows = 9;
  static const int cols = 9;
  static const int totalMineCount = 11;

  bool alive;
  bool hasWon;
  int detectedCount;

  // |cells| keeps track of the positions of the mines.
  List<List<bool>> cells;
  // |uiState| keeps track of the visible player progess.
55
  List<List<CellState>> uiState;
56

57 58
  void initState() {
    super.initState();
Adam Barth's avatar
Adam Barth committed
59
    resetGame();
60 61
  }

62
  void resetGame() {
63 64 65
    alive = true;
    hasWon = false;
    detectedCount = 0;
66
    // Initialize matrices.
Hixie's avatar
Hixie committed
67
    cells = new List<List<bool>>.generate(rows, (int row) {
Mehmet Akin's avatar
Mehmet Akin committed
68
      return new List<bool>.filled(cols, false);
69
    });
Hixie's avatar
Hixie committed
70
    uiState = new List<List<CellState>>.generate(rows, (int row) {
Mehmet Akin's avatar
Mehmet Akin committed
71
      return new List<CellState>.filled(cols, CellState.covered);
72
    });
73
    // Place the mines.
74 75
    Random random = new Random();
    int minesRemaining = totalMineCount;
76 77 78 79 80 81 82
    while (minesRemaining > 0) {
      int pos = random.nextInt(rows * cols);
      int row = pos ~/ rows;
      int col = pos % cols;
      if (!cells[row][col]) {
        cells[row][col] = true;
        minesRemaining--;
83 84
      }
    }
85 86
  }

Ian Hickson's avatar
Ian Hickson committed
87 88
  PointerDownEventListener _pointerDownHandlerFor(int posX, int posY) {
    return (PointerDownEvent event) {
89 90 91 92 93 94
      if (event.buttons == 1) {
        probe(posX, posY);
      } else if (event.buttons == 2) {
        flag(posX, posY);
      }
    };
95 96 97 98
  }

  Widget buildBoard() {
    bool hasCoveredCell = false;
99
    List<Row> flexRows = <Row>[];
100
    for (int iy = 0; iy < rows; iy++) {
101
      List<Widget> row = <Widget>[];
102
      for (int ix = 0; ix < cols; ix++) {
103
        CellState state = uiState[iy][ix];
104 105
        int count = mineCount(ix, iy);
        if (!alive) {
106 107
          if (state != CellState.exploded)
            state = cells[iy][ix] ? CellState.shown : state;
108
        }
109
        if (state == CellState.covered || state == CellState.flagged) {
110
          row.add(new GestureDetector(
111 112 113 114
            onTap: () {
              if (state == CellState.covered)
                probe(ix, iy);
            },
115
            onLongPress: () {
116
              userFeedback.performHapticFeedback(HapticFeedbackType.LONG_PRESS);
117 118
              flag(ix, iy);
            },
119 120 121
            child: new Listener(
              onPointerDown: _pointerDownHandlerFor(ix, iy),
              child: new CoveredMineNode(
122
                flagged: state == CellState.flagged,
123 124 125
                posX: ix,
                posY: iy
              )
126 127
            )
          ));
128 129 130 131 132
          if (state == CellState.covered) {
            // Mutating |hasCoveredCell| here is hacky, but convenient, same
            // goes for mutating |hasWon| below.
            hasCoveredCell = true;
          }
133 134 135
        } else {
          row.add(new ExposedMineNode(
            state: state,
136 137
            count: count
          ));
138 139 140
        }
      }
      flexRows.add(
141
        new Row(
142 143
          row,
          justifyContent: FlexJustifyContent.center,
Hixie's avatar
Hixie committed
144
          key: new ValueKey<int>(iy)
145 146
        )
      );
147 148 149 150 151 152 153 154 155 156 157 158 159
    }

    if (!hasCoveredCell) {
      // all cells uncovered. Are all mines flagged?
      if ((detectedCount == totalMineCount) && alive) {
        hasWon = true;
      }
    }

    return new Container(
      padding: new EdgeDims.all(10.0),
      margin: new EdgeDims.all(10.0),
      decoration: new BoxDecoration(backgroundColor: const Color(0xFF6B6B6B)),
160
      child: new Column(flexRows)
161
    );
162 163
  }

Adam Barth's avatar
Adam Barth committed
164
  Widget buildToolBar(BuildContext context) {
165
    String toolbarCaption = hasWon ?
166 167 168 169 170 171
      'Awesome!!' : alive ?
        'Mine Digger [$detectedCount-$totalMineCount]': 'Kaboom! [press here]';

    return new ToolBar(
      // FIXME: Strange to have the toolbar be tapable.
      center: new Listener(
172
        onPointerDown: handleToolbarPointerDown,
Adam Barth's avatar
Adam Barth committed
173
        child: new Text(toolbarCaption, style: Theme.of(context).text.title)
174 175 176 177
      )
    );
  }

Adam Barth's avatar
Adam Barth committed
178
  Widget build(BuildContext context) {
179
    // We build the board before we build the toolbar because we compute the win state during build step.
180
    Widget board = buildBoard();
181 182
    return new Title(
      title: 'Mine Digger',
183
      child: new Scaffold(
Adam Barth's avatar
Adam Barth committed
184
        toolBar: buildToolBar(context),
185 186
        body: new Container(
          child: new Center(child: board),
187
          decoration: new BoxDecoration(backgroundColor: Colors.grey[50])
188 189 190 191 192
        )
      )
    );
  }

Ian Hickson's avatar
Ian Hickson committed
193
  void handleToolbarPointerDown(PointerDownEvent event) {
194 195 196
    setState(() {
      resetGame();
    });
197 198 199 200 201 202
  }

  // User action. The user uncovers the cell which can cause losing the game.
  void probe(int x, int y) {
    if (!alive)
      return;
203
    if (uiState[y][x] == CellState.flagged)
204
      return;
205 206 207 208 209 210 211 212 213 214 215
    setState(() {
      // Allowed to probe.
      if (cells[y][x]) {
        // Probed on a mine --> dead!!
        uiState[y][x] = CellState.exploded;
        alive = false;
      } else {
        // No mine, uncover nearby if possible.
        cull(x, y);
      }
    });
216 217 218 219
  }

  // User action. The user is sure a mine is at this location.
  void flag(int x, int y) {
220 221 222 223 224 225 226 227 228
    setState(() {
      if (uiState[y][x] == CellState.flagged) {
        uiState[y][x] = CellState.covered;
        --detectedCount;
      } else {
        uiState[y][x] = CellState.flagged;
        ++detectedCount;
      }
    });
229 230 231 232
  }

  // Recursively uncovers cells whose totalMineCount is zero.
  void cull(int x, int y) {
233
    if (!inBoard(x, y))
234
      return;
235
    if (uiState[y][x] == CellState.cleared)
236
      return;
237
    uiState[y][x] = CellState.cleared;
238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253

    if (mineCount(x, y) > 0)
      return;

    cull(x - 1, y);
    cull(x + 1, y);
    cull(x, y - 1);
    cull(x, y + 1 );
    cull(x - 1, y - 1);
    cull(x + 1, y + 1);
    cull(x + 1, y - 1);
    cull(x - 1, y + 1);
  }

  int mineCount(int x, int y) {
    int count = 0;
254 255 256 257 258 259 260 261
    count += bombs(x - 1, y);
    count += bombs(x + 1, y);
    count += bombs(x, y - 1);
    count += bombs(x, y + 1 );
    count += bombs(x - 1, y - 1);
    count += bombs(x + 1, y + 1);
    count += bombs(x + 1, y - 1);
    count += bombs(x - 1, y + 1);
262 263 264
    return count;
  }

265 266
  int bombs(int x, int y) => inBoard(x, y) && cells[y][x] ? 1 : 0;

267
  bool inBoard(int x, int y) => x >= 0 && x < cols && y >= 0 && y < rows;
268 269
}

270
Widget buildCell(Widget child) {
271 272 273 274 275
  return new Container(
    padding: new EdgeDims.all(1.0),
    height: 27.0, width: 27.0,
    decoration: new BoxDecoration(backgroundColor: const Color(0xFFC0C0C0)),
    margin: new EdgeDims.all(2.0),
276 277
    child: child
  );
278 279
}

280
Widget buildInnerCell(Widget child) {
281 282 283 284
  return new Container(
    padding: new EdgeDims.all(1.0),
    margin: new EdgeDims.all(3.0),
    height: 17.0, width: 17.0,
285 286
    child: child
  );
287 288
}

Adam Barth's avatar
Adam Barth committed
289
class CoveredMineNode extends StatelessComponent {
290 291 292

  CoveredMineNode({ this.flagged, this.posX, this.posY });

293 294 295 296
  final bool flagged;
  final int posX;
  final int posY;

Adam Barth's avatar
Adam Barth committed
297
  Widget build(BuildContext context) {
298 299 300
    Widget text;
    if (flagged)
      text = buildInnerCell(new StyledText(elements : [textStyles[5], '\u2691']));
301 302 303 304 305

    Container inner = new Container(
      margin: new EdgeDims.all(2.0),
      height: 17.0, width: 17.0,
      decoration: new BoxDecoration(backgroundColor: const Color(0xFFD9D9D9)),
306 307
      child: text
    );
308

309
    return buildCell(inner);
310 311 312
  }
}

Adam Barth's avatar
Adam Barth committed
313
class ExposedMineNode extends StatelessComponent {
314

315 316 317 318
  ExposedMineNode({ this.state, this.count });

  final CellState state;
  final int count;
319

Adam Barth's avatar
Adam Barth committed
320
  Widget build(BuildContext context) {
321
    StyledText text;
322
    if (state == CellState.cleared) {
323 324
      // Uncovered cell with nearby mine count.
      if (count != 0)
325
        text = new StyledText(elements : [textStyles[count], '$count']);
326 327
    } else {
      // Exploded mine or shown mine for 'game over'.
328 329
      int color = state == CellState.exploded ? 3 : 0;
      text = new StyledText(elements : [textStyles[color], '\u2600']);
330
    }
331
    return buildCell(buildInnerCell(text));
332 333 334 335
  }
}

void main() {
Adam Barth's avatar
Adam Barth committed
336
  runApp(new MineDigger());
337
}