OLD | NEW |
(Empty) | |
| 1 // Copyright 2015 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 import 'dart:sky' as sky; |
| 6 import 'dart:math'; |
| 7 |
| 8 import 'package:sky/rendering/flex.dart'; |
| 9 import 'package:sky/widgets/basic.dart'; |
| 10 import 'package:sky/painting/text_style.dart'; |
| 11 |
| 12 // Classic minesweeper-inspired game. The mouse controls are standard except |
| 13 // for left + right combo which is not implemented. For touch, the duration of |
| 14 // the pointer determines probing versus flagging. |
| 15 // |
| 16 // There are only 3 classes to understand. Game, which is contains all the |
| 17 // logic and two UI classes: CoveredMineNode and ExposedMineNode, none of them |
| 18 // holding state. |
| 19 |
| 20 class Game { |
| 21 static const int rows = 9; |
| 22 static const int cols = 9; |
| 23 static const int totalMineCount = 11; |
| 24 |
| 25 static const int coveredCell = 0; |
| 26 static const int explodedCell = 1; |
| 27 static const int clearedCell = 2; |
| 28 static const int flaggedCell = 3; |
| 29 static const int shownCell = 4; |
| 30 |
| 31 static final List<TextStyle> textStyles = new List<TextStyle>(); |
| 32 |
| 33 final App app; |
| 34 |
| 35 bool alive; |
| 36 bool hasWon; |
| 37 int detectedCount; |
| 38 int randomSeed; |
| 39 |
| 40 // |cells| keeps track of the positions of the mines. |
| 41 List<List<bool>> cells; |
| 42 // |uiState| keeps track of the visible player progess. |
| 43 List<List<int>> uiState; |
| 44 |
| 45 Game(this.app) { |
| 46 randomSeed = 22; |
| 47 // Colors for each mine count: |
| 48 // 0 - none, 1 - blue, 2-green, 3-red, 4-black, 5-dark red .. etc. |
| 49 textStyles.add( |
| 50 new TextStyle(color: const Color(0xFF555555), fontWeight: bold)); |
| 51 textStyles.add( |
| 52 new TextStyle(color: const Color(0xFF0094FF), fontWeight: bold)); |
| 53 textStyles.add( |
| 54 new TextStyle(color: const Color(0xFF13A023), fontWeight: bold)); |
| 55 textStyles.add( |
| 56 new TextStyle(color: const Color(0xFFDA1414), fontWeight: bold)); |
| 57 textStyles.add( |
| 58 new TextStyle(color: const Color(0xFF1E2347), fontWeight: bold)); |
| 59 textStyles.add( |
| 60 new TextStyle(color: const Color(0xFF7F0037), fontWeight: bold)); |
| 61 textStyles.add( |
| 62 new TextStyle(color: const Color(0xFFE93BE9), fontWeight: bold)); |
| 63 initialize(); |
| 64 } |
| 65 |
| 66 void initialize() { |
| 67 alive = true; |
| 68 hasWon = false; |
| 69 detectedCount = 0; |
| 70 // Build the arrays. |
| 71 cells = new List<List<bool>>(); |
| 72 uiState = new List<List<int>>(); |
| 73 for (int iy = 0; iy != rows; iy++) { |
| 74 cells.add(new List<bool>()); |
| 75 uiState.add(new List<int>()); |
| 76 for (int ix = 0; ix != cols; ix++) { |
| 77 cells[iy].add(false); |
| 78 uiState[iy].add(coveredCell); |
| 79 } |
| 80 } |
| 81 // Place the mines. |
| 82 Random random = new Random(++randomSeed); |
| 83 for (int mc = 0; mc != totalMineCount; mc++) { |
| 84 int rx = random.nextInt(rows); |
| 85 int ry = random.nextInt(cols); |
| 86 if (cells[ry][rx]) { |
| 87 // Mine already there. Try again. |
| 88 --mc; |
| 89 } else { |
| 90 cells[ry][rx] = true; |
| 91 } |
| 92 } |
| 93 } |
| 94 |
| 95 Widget generateBoard() { |
| 96 bool hasCoveredCell = false; |
| 97 List<Flex> flexRows = new List<Flex>(); |
| 98 for (int iy = 0; iy != 9; iy++) { |
| 99 List<Component> row = new List<Component>(); |
| 100 for (int ix = 0; ix != 9; ix++) { |
| 101 int state = uiState[iy][ix]; |
| 102 int count = mineCount(ix, iy); |
| 103 |
| 104 if (!alive) { |
| 105 if (state != explodedCell) |
| 106 state = cells[iy][ix] ? shownCell : state; |
| 107 } |
| 108 |
| 109 if (state == coveredCell) { |
| 110 row.add(new CoveredMineNode( |
| 111 this, |
| 112 flagged: false, |
| 113 posX: ix, posY: iy)); |
| 114 // Mutating |hasCoveredCell| here is hacky, but convenient, same |
| 115 // goes for mutating |hasWon| below. |
| 116 hasCoveredCell = true; |
| 117 } else if (state == flaggedCell) { |
| 118 row.add(new CoveredMineNode( |
| 119 this, |
| 120 flagged: true, |
| 121 posX: ix, posY: iy)); |
| 122 } else { |
| 123 row.add(new ExposedMineNode( |
| 124 state: state, |
| 125 count: count)); |
| 126 } |
| 127 } |
| 128 flexRows.add( |
| 129 new Flex( |
| 130 row, |
| 131 direction: FlexDirection.horizontal, |
| 132 justifyContent: FlexJustifyContent.center, |
| 133 key: 'flex_row($iy)' |
| 134 )); |
| 135 } |
| 136 |
| 137 if (!hasCoveredCell) { |
| 138 // all cells uncovered. Are all mines flagged? |
| 139 if ((detectedCount == totalMineCount) && alive) { |
| 140 hasWon = true; |
| 141 } |
| 142 } |
| 143 |
| 144 return new Container( |
| 145 key: 'minefield', |
| 146 padding: new EdgeDims.all(10.0), |
| 147 margin: new EdgeDims.all(10.0), |
| 148 decoration: new BoxDecoration(backgroundColor: const Color(0xFF6B6B6B)), |
| 149 child: new Flex( |
| 150 flexRows, |
| 151 direction: FlexDirection.vertical, |
| 152 key: 'flxv')); |
| 153 } |
| 154 |
| 155 Widget generateUI() { |
| 156 Widget board = generateBoard(); |
| 157 String banner = hasWon ? |
| 158 'Awesome!!' : alive ? |
| 159 'Mine Digger [$detectedCount-$totalMineCount]': 'Kaboom! [press here]'; |
| 160 |
| 161 return new Flex([ |
| 162 new Container( |
| 163 padding: new EdgeDims.all(10.0), |
| 164 margin: new EdgeDims.all(10.0), |
| 165 decoration: new BoxDecoration(backgroundColor: const Color(0xFFC0C0C0)), |
| 166 child: new Listener( |
| 167 onPointerDown: handleBannerPointerDown, |
| 168 child: new Text(banner))), |
| 169 board, |
| 170 new Container( |
| 171 height: 100.0, width: 100.0, |
| 172 decoration: new BoxDecoration(backgroundColor: const Color(0xFFCC1111)) |
| 173 ) |
| 174 ], |
| 175 direction: FlexDirection.vertical, |
| 176 justifyContent: FlexJustifyContent.spaceAround); |
| 177 } |
| 178 |
| 179 void handleBannerPointerDown(sky.PointerEvent event) { |
| 180 initialize(); |
| 181 app.setState((){}); |
| 182 } |
| 183 |
| 184 // User action. The user uncovers the cell which can cause losing the game. |
| 185 void probe(int x, int y) { |
| 186 if (!alive) |
| 187 return; |
| 188 if (uiState[y][x] == flaggedCell) |
| 189 return; |
| 190 // Allowed to probe. |
| 191 if (cells[y][x]) { |
| 192 // Probed on a mine --> dead!! |
| 193 uiState[y][x] = explodedCell; |
| 194 alive = false; |
| 195 } else { |
| 196 // No mine, uncover nearby if possible. |
| 197 cull(x, y); |
| 198 } |
| 199 app.setState((){}); |
| 200 } |
| 201 |
| 202 // User action. The user is sure a mine is at this location. |
| 203 void flag(int x, int y) { |
| 204 if (uiState[y][x] == flaggedCell) { |
| 205 uiState[y][x] = coveredCell; |
| 206 --detectedCount; |
| 207 } else { |
| 208 uiState[y][x] = flaggedCell; |
| 209 ++detectedCount; |
| 210 } |
| 211 app.setState((){}); |
| 212 } |
| 213 |
| 214 // Recursively uncovers cells whose totalMineCount is zero. |
| 215 void cull(int x, int y) { |
| 216 if ((x < 0) || (x > rows - 1)) |
| 217 return; |
| 218 if ((y < 0) || (y > cols - 1)) |
| 219 return; |
| 220 |
| 221 if (uiState[y][x] == clearedCell) |
| 222 return; |
| 223 uiState[y][x] = clearedCell; |
| 224 |
| 225 if (mineCount(x, y) > 0) |
| 226 return; |
| 227 |
| 228 cull(x - 1, y); |
| 229 cull(x + 1, y); |
| 230 cull(x, y - 1); |
| 231 cull(x, y + 1 ); |
| 232 cull(x - 1, y - 1); |
| 233 cull(x + 1, y + 1); |
| 234 cull(x + 1, y - 1); |
| 235 cull(x - 1, y + 1); |
| 236 } |
| 237 |
| 238 int mineCount(int x, int y) { |
| 239 int count = 0; |
| 240 int my = cols - 1; |
| 241 int mx = rows - 1; |
| 242 |
| 243 count += x > 0 ? bombs(x - 1, y) : 0; |
| 244 count += x < mx ? bombs(x + 1, y) : 0; |
| 245 count += y > 0 ? bombs(x, y - 1) : 0; |
| 246 count += y < my ? bombs(x, y + 1 ) : 0; |
| 247 |
| 248 count += (x > 0) && (y > 0) ? bombs(x - 1, y - 1) : 0; |
| 249 count += (x < mx) && (y < my) ? bombs(x + 1, y + 1) : 0; |
| 250 count += (x < mx) && (y > 0) ? bombs(x + 1, y - 1) : 0; |
| 251 count += (x > 0) && (y < my) ? bombs(x - 1, y + 1) : 0; |
| 252 |
| 253 return count; |
| 254 } |
| 255 |
| 256 int bombs(int x, int y) { |
| 257 return cells[y][x] ? 1 : 0; |
| 258 } |
| 259 } |
| 260 |
| 261 Widget makeCell(Widget widget) { |
| 262 return new Container( |
| 263 padding: new EdgeDims.all(1.0), |
| 264 height: 27.0, width: 27.0, |
| 265 decoration: new BoxDecoration(backgroundColor: const Color(0xFFC0C0C0)), |
| 266 margin: new EdgeDims.all(2.0), |
| 267 child: widget); |
| 268 } |
| 269 |
| 270 Widget makeInnerCell(Widget widget) { |
| 271 return new Container( |
| 272 padding: new EdgeDims.all(1.0), |
| 273 margin: new EdgeDims.all(3.0), |
| 274 height: 17.0, width: 17.0, |
| 275 child: widget); |
| 276 } |
| 277 |
| 278 class CoveredMineNode extends Component { |
| 279 final Game game; |
| 280 final bool flagged; |
| 281 final int posX; |
| 282 final int posY; |
| 283 Stopwatch stopwatch; |
| 284 |
| 285 CoveredMineNode(this.game, {this.flagged, this.posX, this.posY}); |
| 286 |
| 287 void _handlePointerDown(sky.PointerEvent event) { |
| 288 if (event.buttons == 1) { |
| 289 game.probe(posX, posY); |
| 290 } else if (event.buttons == 2) { |
| 291 game.flag(posX, posY); |
| 292 } else { |
| 293 // Touch event. |
| 294 stopwatch = new Stopwatch()..start(); |
| 295 } |
| 296 } |
| 297 |
| 298 void _handlePointerUp(sky.PointerEvent event) { |
| 299 if (stopwatch == null) |
| 300 return; |
| 301 // Pointer down was a touch event. |
| 302 var ms = stopwatch.elapsedMilliseconds; |
| 303 if (stopwatch.elapsedMilliseconds < 250) { |
| 304 game.probe(posX, posY); |
| 305 } else { |
| 306 // Long press flags. |
| 307 game.flag(posX, posY); |
| 308 } |
| 309 stopwatch = null; |
| 310 } |
| 311 |
| 312 Widget build() { |
| 313 Widget text = flagged ? |
| 314 makeInnerCell(new StyledText(elements : [Game.textStyles[5], '\u2691'])) : |
| 315 null; |
| 316 |
| 317 Container inner = new Container( |
| 318 margin: new EdgeDims.all(2.0), |
| 319 height: 17.0, width: 17.0, |
| 320 decoration: new BoxDecoration(backgroundColor: const Color(0xFFD9D9D9)), |
| 321 child: text); |
| 322 |
| 323 return makeCell(new Listener( |
| 324 child: inner, |
| 325 onPointerDown: _handlePointerDown, |
| 326 onPointerUp: _handlePointerUp)); |
| 327 } |
| 328 } |
| 329 |
| 330 class ExposedMineNode extends Component { |
| 331 final int state; |
| 332 final int count; |
| 333 |
| 334 ExposedMineNode({this.state, this.count}); |
| 335 |
| 336 Widget build() { |
| 337 StyledText text; |
| 338 if (state == Game.clearedCell) { |
| 339 // Uncovered cell with nearby mine count. |
| 340 if (count != 0) |
| 341 text = new StyledText(elements : [Game.textStyles[count], '$count']); |
| 342 } else { |
| 343 // Exploded mine or shown mine for 'game over'. |
| 344 int color = state == Game.explodedCell ? 3 : 0; |
| 345 text = new StyledText(elements : [Game.textStyles[color], '\u2600']); |
| 346 } |
| 347 |
| 348 return makeCell(makeInnerCell(text)); |
| 349 } |
| 350 } |
| 351 |
| 352 class MineDiggerApp extends App { |
| 353 Game game; |
| 354 |
| 355 MineDiggerApp() { |
| 356 game = new Game(this); |
| 357 } |
| 358 |
| 359 Widget build() { |
| 360 return game.generateUI(); |
| 361 } |
| 362 } |
| 363 |
| 364 void main() { |
| 365 runApp(new MineDiggerApp()); |
| 366 } |
OLD | NEW |