Chromium Code Reviews| Index: sky/examples/mine_digger/mine_digger.dart |
| diff --git a/sky/examples/mine_digger/mine_digger.dart b/sky/examples/mine_digger/mine_digger.dart |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..0f45daaf3373c08455acf2c93363815b1d4074b2 |
| --- /dev/null |
| +++ b/sky/examples/mine_digger/mine_digger.dart |
| @@ -0,0 +1,368 @@ |
| +// Copyright 2015 The Chromium Authors. All rights reserved. |
| +// Use of this source code is governed by a BSD-style license that can be |
| +// found in the LICENSE file. |
| + |
| +import 'dart:sky' as sky; |
| +import 'dart:math'; |
| +import 'dart:core'; |
|
abarth-chromium
2015/06/23 04:05:17
You don't need to import dart:core. It's imported
|
| + |
| +import 'package:sky/rendering/flex.dart'; |
| +import 'package:sky/widgets/basic.dart'; |
| +import 'package:sky/painting/text_style.dart'; |
| + |
| +// 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. |
| +// |
| +// There are only 3 classes to understand. Game, which is contains all the |
| +// logic and two UI classes: CoveredMineNode and ExposedMineNode, none of them |
| +// holding state. |
| + |
| +class Game { |
| + static final int rows = 9; |
| + static final int cols = 9; |
| + static final int totalMineCount = 11; |
| + |
| + static final int coveredCell = 0; |
| + static final int explodedCell = 1; |
| + static final int clearedCell = 2; |
| + static final int flaggedCell = 3; |
| + static final int shownCell = 4; |
| + |
| + static final List<TextStyle> textStyles = new List<TextStyle>(); |
| + |
| + final App app; |
| + |
| + bool alive; |
| + bool hasWon; |
| + int detectedCount; |
| + int randomSeed; |
| + |
| + // |cells| keeps track of the positions of the mines. |
| + List<List<bool>> cells; |
| + // |uiState| keeps track of the visible player progess. |
| + List<List<int>> uiState; |
| + |
| + Game(this.app) { |
| + randomSeed = 22; |
| + // Colors for each mine count: |
| + // 0 - none, 1 - blue, 2-green, 3-red, 4-black, 5-dark red .. etc. |
| + textStyles.add( |
| + new TextStyle(color: const Color(0xFF555555), fontWeight: bold)); |
| + textStyles.add( |
| + new TextStyle(color: const Color(0xFF0094FF), fontWeight: bold)); |
| + textStyles.add( |
| + new TextStyle(color: const Color(0xFF13A023), fontWeight: bold)); |
| + textStyles.add( |
| + new TextStyle(color: const Color(0xFFDA1414), fontWeight: bold)); |
| + textStyles.add( |
| + new TextStyle(color: const Color(0xFF1E2347), fontWeight: bold)); |
| + textStyles.add( |
| + new TextStyle(color: const Color(0xFF7F0037), fontWeight: bold)); |
| + textStyles.add( |
| + new TextStyle(color: const Color(0xFFE93BE9), fontWeight: bold)); |
| + initialize(); |
| + } |
| + |
| + void initialize() { |
| + alive = true; |
| + hasWon = false; |
| + detectedCount = 0; |
| + // Build the arrays. |
| + cells = new List<List<bool>>(); |
| + uiState = new List<List<int>>(); |
| + for (int iy = 0; iy != rows; iy++) { |
| + cells.add(new List<bool>()); |
| + uiState.add(new List<int>()); |
| + for (int ix = 0; ix != cols; ix++) { |
| + cells[iy].add(false); |
| + uiState[iy].add(coveredCell); |
| + } |
| + } |
| + // Place the mines. |
| + var random = new Random(++randomSeed); |
| + for (int mc = 0; mc != totalMineCount; mc++) { |
| + var rx = random.nextInt(rows); |
| + var ry = random.nextInt(cols); |
| + if (cells[ry][rx]) { |
| + // Mine already there. Try again. |
| + --mc; |
| + } else { |
| + cells[ry][rx] = true; |
| + } |
| + } |
| + } |
| + |
| + Widget generateBoard() { |
| + bool hasCoveredCell = false; |
| + var flex_rows = new List<Flex>(); |
|
abarth-chromium
2015/06/23 04:05:17
s/flex_rows/flexRows/
|
| + for (int iy = 0; iy != 9; iy++) { |
| + var row = new List<Component>(); |
| + for (int ix = 0; ix != 9; ix++) { |
| + int state = uiState[iy][ix]; |
| + int count = mineCount(ix, iy); |
| + |
| + if (!alive) { |
| + if (state != explodedCell) |
| + state = cells[iy][ix] ? shownCell : state; |
| + } |
| + |
| + if (state == coveredCell) { |
| + row.add(new CoveredMineNode( |
| + this, |
| + flagged: false, |
| + posX: ix, posY: iy)); |
| + // Mutating |hasCoveredCell| here is hacky, but convenient, same |
| + // goes for mutating |hasWon| below. |
| + hasCoveredCell = true; |
| + } else if (state == flaggedCell) { |
| + row.add(new CoveredMineNode( |
| + this, |
| + flagged: true, |
| + posX: ix, posY: iy)); |
| + }else { |
|
abarth-chromium
2015/06/23 04:05:17
s/}else {/} else {/
|
| + row.add(new ExposedMineNode( |
| + state: state, |
| + count: count)); |
| + } |
| + } |
| + flex_rows.add( |
| + new Flex( |
| + row, |
| + direction: FlexDirection.horizontal, |
| + justifyContent: FlexJustifyContent.center, |
| + key: 'flex_row($iy)' |
| + )); |
| + } |
| + |
| + if (!hasCoveredCell) { |
| + // all cells uncovered. Are all mines flagged? |
| + if ((detectedCount == totalMineCount) && alive) { |
| + hasWon = true; |
| + print('game won!'); |
|
abarth-chromium
2015/06/23 04:05:17
Mobile first!
|
| + } |
| + } |
| + |
| + return new Container( |
| + key: 'minefield', |
| + padding: new EdgeDims.all(10.0), |
| + margin: new EdgeDims.all(10.0), |
| + decoration: new BoxDecoration(backgroundColor: const Color(0xFF6B6B6B)), |
| + child: new Flex( |
| + flex_rows, |
| + direction: FlexDirection.vertical, |
| + key: 'flxv')); |
| + } |
| + |
| + Widget generateUI() { |
| + var board = generateBoard(); |
| + String banner = hasWon ? |
| + 'Awesome!!' : alive ? |
| + 'Mine Digger [$detectedCount-$totalMineCount]': 'Kaboom! [press here]'; |
| + |
| + return new Flex([ |
| + new Container( |
| + padding: new EdgeDims.all(10.0), |
| + margin: new EdgeDims.all(10.0), |
| + decoration: new BoxDecoration(backgroundColor: const Color(0xFFC0C0C0)), |
| + child: new Listener( |
| + onPointerDown: handleBannerPointerDown, |
| + child: new Text(banner))), |
| + board, |
| + new Container( |
| + height: 100.0, width: 100.0, |
| + decoration: new BoxDecoration(backgroundColor: const Color(0xFFCC1111)) |
| + ) |
| + ], |
| + direction: FlexDirection.vertical, |
| + justifyContent: FlexJustifyContent.spaceAround); |
| + } |
| + |
| + void handleBannerPointerDown(sky.PointerEvent event) { |
| + initialize(); |
| + app.setState((){}); |
| + } |
| + |
| + // User action. The user uncovers the cell which can cause losing the game. |
| + void probe(int x, int y) { |
| + if (!alive) |
| + return; |
| + if (uiState[y][x] == flaggedCell) |
| + return; |
| + // Allowed to probe. |
| + if (cells[y][x]) { |
| + print('kaboom!! $x $y'); |
| + uiState[y][x] = explodedCell; |
| + alive = false; |
| + } else { |
| + // No mine, uncover nearby if possible. |
| + cull(x, y); |
| + } |
| + app.setState((){}); |
| + } |
| + |
| + // User action. The user is sure a mine is at this location. |
| + void flag(int x, int y) { |
| + if (uiState[y][x] == flaggedCell) { |
| + uiState[y][x] = coveredCell; |
| + --detectedCount; |
| + } else { |
| + uiState[y][x] = flaggedCell; |
| + ++detectedCount; |
| + } |
| + app.setState((){}); |
| + } |
| + |
| + // 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; |
| + |
| + if (uiState[y][x] == clearedCell) |
| + return; |
| + uiState[y][x] = clearedCell; |
| + |
| + 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; |
| + } |
| +} |
| + |
| +Widget makeCell(Widget widget) { |
| + 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), |
| + child : widget); |
|
abarth-chromium
2015/06/23 04:05:17
s/margin :/margin:/
s/child :/child:/
|
| +} |
| + |
| +Widget makeInnerCell(Widget widget) { |
| + return new Container( |
| + padding: new EdgeDims.all(1.0), |
| + margin: new EdgeDims.all(3.0), |
| + height: 17.0, width: 17.0, |
| + child: widget); |
| +} |
| + |
| +class CoveredMineNode extends Component { |
| + final Game game; |
| + final bool flagged; |
| + final int posX; |
| + final int posY; |
| + Stopwatch stopwatch; |
| + |
| + CoveredMineNode(this.game, {this.flagged, this.posX, this.posY}); |
| + |
| + void _handlePointerDown(sky.PointerEvent event) { |
| + if (event.buttons == 1) { |
| + game.probe(posX, posY); |
| + } else if (event.buttons == 2) { |
| + game.flag(posX, posY); |
| + } else { |
| + // Touch event. |
| + stopwatch = new Stopwatch()..start(); |
| + } |
| + } |
| + |
| + void _handlePointerUp(sky.PointerEvent event) { |
| + if (stopwatch == null) |
| + return; |
| + // Pointer down was a touch event. |
| + var ms = stopwatch.elapsedMilliseconds; |
| + if (stopwatch.elapsedMilliseconds < 250) { |
| + game.probe(posX, posY); |
| + } else { |
| + // Long press flags. |
| + game.flag(posX, posY); |
| + } |
| + stopwatch = null; |
| + } |
| + |
| + Widget build() { |
| + var text = flagged ? |
| + makeInnerCell(new StyledText(elements : [Game.textStyles[5], '\u2691'])) : |
| + null; |
| + |
| + var inner = new Container( |
| + margin: new EdgeDims.all(2.0), |
| + height: 17.0, width: 17.0, |
| + decoration: new BoxDecoration(backgroundColor: const Color(0xFFD9D9D9)), |
| + child : text); |
|
abarth-chromium
2015/06/23 04:05:17
s/child :/child:/
|
| + |
| + return makeCell(new Listener( |
| + child: inner, |
| + onPointerDown: _handlePointerDown, |
| + onPointerUp: _handlePointerUp)); |
| + } |
| +} |
| + |
| +class ExposedMineNode extends Component { |
| + final int state; |
| + final int count; |
| + |
| + ExposedMineNode({this.state, this.count}); |
| + |
| + Widget build() { |
| + StyledText text; |
| + if (state == Game.clearedCell) { |
| + // Uncovered cell with nearby mine count. |
| + if (count != 0) |
| + text = new StyledText(elements : [Game.textStyles[count], '$count']); |
| + } else { |
| + // Exploded mine or shown mine for 'game over'. |
| + var color = state == Game.explodedCell ? 3 : 0; |
| + text = new StyledText(elements : [Game.textStyles[color], '\u2600']); |
| + } |
| + |
| + return makeCell(makeInnerCell(text)); |
| + } |
| +} |
| + |
| +class MineDiggerApp extends App { |
| + Game game; |
| + |
| + MineDiggerApp() { |
| + game = new Game(this); |
| + } |
| + |
| + Widget build() { |
| + return game.generateUI(); |
| + } |
| +} |
| + |
| +void main() { |
| + runApp(new MineDiggerApp()); |
| +} |