main.dart 9.93 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 6 7 8 9
import 'dart:sky' as sky;
import 'dart:math';

import 'package:sky/painting/text_style.dart';
import 'package:sky/rendering/flex.dart';
import 'package:sky/theme/colors.dart' as colors;
10
import 'package:sky/widgets.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 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
// 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):
const List<TextStyle> textStyles = const <TextStyle>[
  const TextStyle(color: const Color(0xFF555555), fontWeight: bold),
  const TextStyle(color: const Color(0xFF0094FF), fontWeight: bold), // blue
  const TextStyle(color: const Color(0xFF13A023), fontWeight: bold), // green
  const TextStyle(color: const Color(0xFFDA1414), fontWeight: bold), // red
  const TextStyle(color: const Color(0xFF1E2347), fontWeight: bold), // black
  const TextStyle(color: const Color(0xFF7F0037), fontWeight: bold), // dark red
  const TextStyle(color: const Color(0xFF000000), fontWeight: bold),
  const TextStyle(color: const Color(0xFF000000), fontWeight: bold),
  const TextStyle(color: const Color(0xFF000000), fontWeight: bold),
];

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

class MineDiggerApp extends App {
36 37 38 39 40 41 42 43 44 45 46
  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.
47
  List<List<CellState>> uiState;
48

Adam Barth's avatar
Adam Barth committed
49 50
  void initState() {
    resetGame();
51 52
  }

53
  void resetGame() {
54 55 56 57 58
    alive = true;
    hasWon = false;
    detectedCount = 0;
    // Build the arrays.
    cells = new List<List<bool>>();
59
    uiState = new List<List<CellState>>();
60 61
    for (int iy = 0; iy != rows; iy++) {
      cells.add(new List<bool>());
62
      uiState.add(new List<CellState>());
63 64
      for (int ix = 0; ix != cols; ix++) {
        cells[iy].add(false);
65
        uiState[iy].add(CellState.covered);
66 67 68
      }
    }
    // Place the mines.
69 70 71 72 73 74 75 76 77 78 79 80
    Random random = new Random();
    int cellsRemaining = rows * cols;
    int minesRemaining = totalMineCount;
    for (int x = 0; x < cols; x += 1) {
      for (int y = 0; y < rows; y += 1) {
        if (random.nextInt(cellsRemaining) < minesRemaining) {
          cells[y][x] = true;
          minesRemaining -= 1;
          if (minesRemaining <= 0)
            return;
        }
        cellsRemaining -= 1;
81 82
      }
    }
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113
    assert(false);
  }

  Stopwatch longPressStopwatch;

  PointerEventListener _pointerDownHandlerFor(int posX, int posY) {
    return (sky.PointerEvent event) {
      if (event.buttons == 1) {
        probe(posX, posY);
      } else if (event.buttons == 2) {
        flag(posX, posY);
      } else {
        // Touch event.
        longPressStopwatch = new Stopwatch()..start();
      }
    };
  }

  PointerEventListener _pointerUpHandlerFor(int posX, int posY) {
    return (sky.PointerEvent event) {
      if (longPressStopwatch == null)
        return;
      // Pointer down was a touch event.
      if (longPressStopwatch.elapsedMilliseconds < 250) {
        probe(posX, posY);
      } else {
        // Long press flags.
        flag(posX, posY);
      }
      longPressStopwatch = null;
    };
114 115 116 117
  }

  Widget buildBoard() {
    bool hasCoveredCell = false;
118
    List<Flex> flexRows = <Flex>[];
119
    for (int iy = 0; iy != 9; iy++) {
120
      List<Widget> row = <Widget>[];
121
      for (int ix = 0; ix != 9; ix++) {
122
        CellState state = uiState[iy][ix];
123 124
        int count = mineCount(ix, iy);
        if (!alive) {
125 126
          if (state != CellState.exploded)
            state = cells[iy][ix] ? CellState.shown : state;
127
        }
128 129 130 131 132 133 134 135 136 137 138 139 140 141
        if (state == CellState.covered) {
          row.add(new Listener(
            onPointerDown: _pointerDownHandlerFor(ix, iy),
            onPointerUp: _pointerUpHandlerFor(ix, iy),
            child: new CoveredMineNode(
              flagged: false,
              posX: ix,
              posY: iy
            )
          ));
          // Mutating |hasCoveredCell| here is hacky, but convenient, same
          // goes for mutating |hasWon| below.
          hasCoveredCell = true;
        } else if (state == CellState.flagged) {
142 143
          row.add(new CoveredMineNode(
            flagged: true,
144 145 146
            posX: ix,
            posY: iy
          ));
147 148 149
        } else {
          row.add(new ExposedMineNode(
            state: state,
150 151
            count: count
          ));
152 153 154 155 156 157 158
        }
      }
      flexRows.add(
        new Flex(
          row,
          direction: FlexDirection.horizontal,
          justifyContent: FlexJustifyContent.center,
159 160 161
          key: new Key.stringify(iy)
        )
      );
162 163 164 165 166 167 168 169 170 171 172 173 174 175 176
    }

    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)),
      child: new Flex(
        flexRows,
177 178 179
        direction: FlexDirection.vertical
      )
    );
180 181 182
  }

  Widget buildToolBar() {
183
    String toolbarCaption = hasWon ?
184 185 186 187 188 189
      'Awesome!!' : alive ?
        'Mine Digger [$detectedCount-$totalMineCount]': 'Kaboom! [press here]';

    return new ToolBar(
      // FIXME: Strange to have the toolbar be tapable.
      center: new Listener(
190 191
        onPointerDown: handleToolbarPointerDown,
        child: new Text(toolbarCaption, style: Theme.of(this).text.title)
192 193 194 195
      )
    );
  }

196 197
  Widget build() {
    // We build the board before we build the toolbar because we compute the win state during build step.
198 199 200 201 202 203 204 205 206 207 208 209 210
    Widget board = buildBoard();
    return new TaskDescription(
      label: 'Mine Digger',
      child: new Scaffold(
        toolbar: buildToolBar(),
        body: new Container(
          child: new Center(child: board),
          decoration: new BoxDecoration(backgroundColor: colors.Grey[50])
        )
      )
    );
  }

211
  EventDisposition handleToolbarPointerDown(sky.PointerEvent event) {
212 213 214
    setState(() {
      resetGame();
    });
215
    return EventDisposition.processed;
216 217 218 219 220 221
  }

  // User action. The user uncovers the cell which can cause losing the game.
  void probe(int x, int y) {
    if (!alive)
      return;
222
    if (uiState[y][x] == CellState.flagged)
223
      return;
224 225 226 227 228 229 230 231 232 233 234
    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);
      }
    });
235 236 237 238
  }

  // User action. The user is sure a mine is at this location.
  void flag(int x, int y) {
239 240 241 242 243 244 245 246 247
    setState(() {
      if (uiState[y][x] == CellState.flagged) {
        uiState[y][x] = CellState.covered;
        --detectedCount;
      } else {
        uiState[y][x] = CellState.flagged;
        ++detectedCount;
      }
    });
248 249 250 251 252 253 254 255 256
  }

  // Recursively uncovers cells whose totalMineCount is zero.
  void cull(int x, int y) {
    if ((x < 0) || (x > rows - 1))
      return;
    if ((y < 0) || (y > cols - 1))
      return;

257
    if (uiState[y][x] == CellState.cleared)
258
      return;
259
    uiState[y][x] = CellState.cleared;
260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296

    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;
    int my = cols - 1;
    int mx = rows - 1;

    count += x > 0 ? bombs(x - 1, y) : 0;
    count += x < mx ? bombs(x + 1, y) : 0;
    count += y > 0 ? bombs(x, y - 1) : 0;
    count += y < my ? bombs(x, y + 1 ) : 0;

    count += (x > 0) && (y > 0) ? bombs(x - 1, y - 1) : 0;
    count += (x < mx) && (y < my) ? bombs(x + 1, y + 1) : 0;
    count += (x < mx) && (y > 0) ? bombs(x + 1, y - 1) : 0;
    count += (x > 0) && (y < my) ? bombs(x - 1, y + 1) : 0;

    return count;
  }

  int bombs(int x, int y) {
    return cells[y][x] ? 1 : 0;
  }
}

297
Widget buildCell(Widget child) {
298 299 300 301 302
  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),
303 304
    child: child
  );
305 306
}

307
Widget buildInnerCell(Widget child) {
308 309 310 311
  return new Container(
    padding: new EdgeDims.all(1.0),
    margin: new EdgeDims.all(3.0),
    height: 17.0, width: 17.0,
312 313
    child: child
  );
314 315 316
}

class CoveredMineNode extends Component {
317 318 319

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

320 321 322 323 324
  final bool flagged;
  final int posX;
  final int posY;

  Widget build() {
325 326 327
    Widget text;
    if (flagged)
      text = buildInnerCell(new StyledText(elements : [textStyles[5], '\u2691']));
328 329 330 331 332

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

336
    return buildCell(inner);
337 338 339 340 341
  }
}

class ExposedMineNode extends Component {

342 343 344 345
  ExposedMineNode({ this.state, this.count });

  final CellState state;
  final int count;
346 347 348

  Widget build() {
    StyledText text;
349
    if (state == CellState.cleared) {
350 351
      // Uncovered cell with nearby mine count.
      if (count != 0)
352
        text = new StyledText(elements : [textStyles[count], '$count']);
353 354
    } else {
      // Exploded mine or shown mine for 'game over'.
355 356
      int color = state == CellState.exploded ? 3 : 0;
      text = new StyledText(elements : [textStyles[color], '\u2600']);
357
    }
358
    return buildCell(buildInnerCell(text));
359 360 361 362 363 364 365
  }

}

void main() {
  runApp(new MineDiggerApp());
}