diff --git a/packages/flutter/example/mine_digger/lib/main.dart b/packages/flutter/example/mine_digger/lib/main.dart index 0db72e28952..3219837fa31 100644 --- a/packages/flutter/example/mine_digger/lib/main.dart +++ b/packages/flutter/example/mine_digger/lib/main.dart @@ -9,125 +9,152 @@ import 'package:sky/painting/text_style.dart'; import 'package:sky/rendering/flex.dart'; import 'package:sky/theme/colors.dart' as colors; import 'package:sky/widgets/basic.dart'; +import 'package:sky/widgets/widget.dart'; import 'package:sky/widgets/scaffold.dart'; import 'package:sky/widgets/task_description.dart'; import 'package:sky/widgets/theme.dart'; import 'package:sky/widgets/tool_bar.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. +// 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. +// 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 textStyles = const [ + 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 { + + void initState() { + resetGame(); + } -class Game { static const int rows = 9; static const int cols = 9; static const int totalMineCount = 11; - static const int coveredCell = 0; - static const int explodedCell = 1; - static const int clearedCell = 2; - static const int flaggedCell = 3; - static const int shownCell = 4; - - static final List textStyles = new List(); - - final App app; - bool alive; bool hasWon; int detectedCount; - int randomSeed; // |cells| keeps track of the positions of the mines. List> cells; // |uiState| keeps track of the visible player progess. - List> uiState; + List> 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() { + void resetGame() { alive = true; hasWon = false; detectedCount = 0; // Build the arrays. cells = new List>(); - uiState = new List>(); + uiState = new List>(); for (int iy = 0; iy != rows; iy++) { cells.add(new List()); - uiState.add(new List()); + uiState.add(new List()); for (int ix = 0; ix != cols; ix++) { cells[iy].add(false); - uiState[iy].add(coveredCell); + uiState[iy].add(CellState.covered); } } // Place the mines. - Random random = new Random(++randomSeed); - for (int mc = 0; mc != totalMineCount; mc++) { - int rx = random.nextInt(rows); - int ry = random.nextInt(cols); - if (cells[ry][rx]) { - // Mine already there. Try again. - --mc; - } else { - cells[ry][rx] = true; + 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; } } + 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; + }; } Widget buildBoard() { bool hasCoveredCell = false; - List flexRows = new List(); + List flexRows = []; for (int iy = 0; iy != 9; iy++) { - List row = new List(); + List row = []; for (int ix = 0; ix != 9; ix++) { - int state = uiState[iy][ix]; + CellState state = uiState[iy][ix]; int count = mineCount(ix, iy); - if (!alive) { - if (state != explodedCell) - state = cells[iy][ix] ? shownCell : state; + if (state != CellState.exploded) + state = cells[iy][ix] ? CellState.shown : state; } - - if (state == coveredCell) { + 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) { 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)); + posX: ix, posY: iy) + ); } else { row.add(new ExposedMineNode( state: state, - count: count)); + count: count) + ); } } flexRows.add( @@ -147,33 +174,32 @@ class Game { } 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( flexRows, - direction: FlexDirection.vertical, - key: 'flxv')); + direction: FlexDirection.vertical + ) + ); } Widget buildToolBar() { - String banner = hasWon ? + String toolbarCaption = hasWon ? 'Awesome!!' : alive ? 'Mine Digger [$detectedCount-$totalMineCount]': 'Kaboom! [press here]'; return new ToolBar( // FIXME: Strange to have the toolbar be tapable. center: new Listener( - onPointerDown: handleBannerPointerDown, - child: new Text(banner, style: Theme.of(this.app).text.title) + onPointerDown: handleToolbarPointerDown, + child: new Text(toolbarCaption, style: Theme.of(this).text.title) ) ); } - Widget buildUI() { - // FIXME: We need to build the board before we build the toolbar because - // we compute the win state during build step. + Widget build() { + // We build the board before we build the toolbar because we compute the win state during build step. Widget board = buildBoard(); return new TaskDescription( label: 'Mine Digger', @@ -187,39 +213,42 @@ class Game { ); } - void handleBannerPointerDown(sky.PointerEvent event) { - initialize(); - app.scheduleBuild(); + void handleToolbarPointerDown(sky.PointerEvent event) { + setState(() { + resetGame(); + }); } // 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) + if (uiState[y][x] == CellState.flagged) return; - // Allowed to probe. - if (cells[y][x]) { - // Probed on a mine --> dead!! - uiState[y][x] = explodedCell; - alive = false; - } else { - // No mine, uncover nearby if possible. - cull(x, y); - } - app.scheduleBuild(); + 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); + } + }); } // 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.scheduleBuild(); + setState(() { + if (uiState[y][x] == CellState.flagged) { + uiState[y][x] = CellState.covered; + --detectedCount; + } else { + uiState[y][x] = CellState.flagged; + ++detectedCount; + } + }); } // Recursively uncovers cells whose totalMineCount is zero. @@ -229,9 +258,9 @@ class Game { if ((y < 0) || (y > cols - 1)) return; - if (uiState[y][x] == clearedCell) + if (uiState[y][x] == CellState.cleared) return; - uiState[y][x] = clearedCell; + uiState[y][x] = CellState.cleared; if (mineCount(x, y) > 0) return; @@ -269,106 +298,70 @@ class Game { } } -Widget makeCell(Widget widget) { +Widget buildCell(Widget child) { 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); + child: child + ); } -Widget makeInnerCell(Widget widget) { +Widget buildInnerCell(Widget child) { return new Container( padding: new EdgeDims.all(1.0), margin: new EdgeDims.all(3.0), height: 17.0, width: 17.0, - child: widget); + child: child + ); } class CoveredMineNode extends Component { - final Game game; + + CoveredMineNode({ this.flagged, this.posX, this.posY }); + 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. - if (stopwatch.elapsedMilliseconds < 250) { - game.probe(posX, posY); - } else { - // Long press flags. - game.flag(posX, posY); - } - stopwatch = null; - } Widget build() { - Widget text = flagged ? - makeInnerCell(new StyledText(elements : [Game.textStyles[5], '\u2691'])) : - null; + Widget text; + if (flagged) + text = buildInnerCell(new StyledText(elements : [textStyles[5], '\u2691'])); Container inner = new Container( margin: new EdgeDims.all(2.0), height: 17.0, width: 17.0, decoration: new BoxDecoration(backgroundColor: const Color(0xFFD9D9D9)), - child: text); + child: text + ); - return makeCell(new Listener( - child: inner, - onPointerDown: _handlePointerDown, - onPointerUp: _handlePointerUp)); + return buildCell(inner); } } class ExposedMineNode extends Component { - final int state; - final int count; - ExposedMineNode({this.state, this.count}); + ExposedMineNode({ this.state, this.count }); + + final CellState state; + final int count; Widget build() { StyledText text; - if (state == Game.clearedCell) { + if (state == CellState.cleared) { // Uncovered cell with nearby mine count. if (count != 0) - text = new StyledText(elements : [Game.textStyles[count], '$count']); + text = new StyledText(elements : [textStyles[count], '$count']); } else { // Exploded mine or shown mine for 'game over'. - int color = state == Game.explodedCell ? 3 : 0; - text = new StyledText(elements : [Game.textStyles[color], '\u2600']); + int color = state == CellState.exploded ? 3 : 0; + text = new StyledText(elements : [textStyles[color], '\u2600']); } - - return makeCell(makeInnerCell(text)); - } -} - -class MineDiggerApp extends App { - Game game; - - MineDiggerApp() { - game = new Game(this); + return buildCell(buildInnerCell(text)); } - Widget build() { - return game.buildUI(); - } } void main() {