Commit b215e4d6 authored by Ian Hickson's avatar Ian Hickson

Merge pull request #205 from Hixie/minedigger

Many code improvements to Mine Digger.
parents 136b2709 4d2902f2
...@@ -9,125 +9,152 @@ import 'package:sky/painting/text_style.dart'; ...@@ -9,125 +9,152 @@ import 'package:sky/painting/text_style.dart';
import 'package:sky/rendering/flex.dart'; import 'package:sky/rendering/flex.dart';
import 'package:sky/theme/colors.dart' as colors; import 'package:sky/theme/colors.dart' as colors;
import 'package:sky/widgets/basic.dart'; import 'package:sky/widgets/basic.dart';
import 'package:sky/widgets/widget.dart';
import 'package:sky/widgets/scaffold.dart'; import 'package:sky/widgets/scaffold.dart';
import 'package:sky/widgets/task_description.dart'; import 'package:sky/widgets/task_description.dart';
import 'package:sky/widgets/theme.dart'; import 'package:sky/widgets/theme.dart';
import 'package:sky/widgets/tool_bar.dart'; import 'package:sky/widgets/tool_bar.dart';
// Classic minesweeper-inspired game. The mouse controls are standard except // Classic minesweeper-inspired game. The mouse controls are standard
// for left + right combo which is not implemented. For touch, the duration of // except for left + right combo which is not implemented. For touch,
// the pointer determines probing versus flagging. // the duration of the pointer determines probing versus flagging.
// //
// There are only 3 classes to understand. Game, which is contains all the // There are only 3 classes to understand. MineDiggerApp, which is
// logic and two UI classes: CoveredMineNode and ExposedMineNode, none of them // contains all the logic and two classes that describe the mines:
// holding state. // 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 {
void initState() {
resetGame();
}
class Game {
static const int rows = 9; static const int rows = 9;
static const int cols = 9; static const int cols = 9;
static const int totalMineCount = 11; 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<TextStyle> textStyles = new List<TextStyle>();
final App app;
bool alive; bool alive;
bool hasWon; bool hasWon;
int detectedCount; int detectedCount;
int randomSeed;
// |cells| keeps track of the positions of the mines. // |cells| keeps track of the positions of the mines.
List<List<bool>> cells; List<List<bool>> cells;
// |uiState| keeps track of the visible player progess. // |uiState| keeps track of the visible player progess.
List<List<int>> uiState; List<List<CellState>> 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; alive = true;
hasWon = false; hasWon = false;
detectedCount = 0; detectedCount = 0;
// Build the arrays. // Build the arrays.
cells = new List<List<bool>>(); cells = new List<List<bool>>();
uiState = new List<List<int>>(); uiState = new List<List<CellState>>();
for (int iy = 0; iy != rows; iy++) { for (int iy = 0; iy != rows; iy++) {
cells.add(new List<bool>()); cells.add(new List<bool>());
uiState.add(new List<int>()); uiState.add(new List<CellState>());
for (int ix = 0; ix != cols; ix++) { for (int ix = 0; ix != cols; ix++) {
cells[iy].add(false); cells[iy].add(false);
uiState[iy].add(coveredCell); uiState[iy].add(CellState.covered);
} }
} }
// Place the mines. // Place the mines.
Random random = new Random(++randomSeed); Random random = new Random();
for (int mc = 0; mc != totalMineCount; mc++) { int cellsRemaining = rows * cols;
int rx = random.nextInt(rows); int minesRemaining = totalMineCount;
int ry = random.nextInt(cols); for (int x = 0; x < cols; x += 1) {
if (cells[ry][rx]) { for (int y = 0; y < rows; y += 1) {
// Mine already there. Try again. if (random.nextInt(cellsRemaining) < minesRemaining) {
--mc; cells[y][x] = true;
} else { minesRemaining -= 1;
cells[ry][rx] = true; 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() { Widget buildBoard() {
bool hasCoveredCell = false; bool hasCoveredCell = false;
List<Flex> flexRows = new List<Flex>(); List<Flex> flexRows = <Flex>[];
for (int iy = 0; iy != 9; iy++) { for (int iy = 0; iy != 9; iy++) {
List<Component> row = new List<Component>(); List<Widget> row = <Widget>[];
for (int ix = 0; ix != 9; ix++) { for (int ix = 0; ix != 9; ix++) {
int state = uiState[iy][ix]; CellState state = uiState[iy][ix];
int count = mineCount(ix, iy); int count = mineCount(ix, iy);
if (!alive) { if (!alive) {
if (state != explodedCell) if (state != CellState.exploded)
state = cells[iy][ix] ? shownCell : state; state = cells[iy][ix] ? CellState.shown : state;
} }
if (state == CellState.covered) {
if (state == coveredCell) { row.add(new Listener(
row.add(new CoveredMineNode( onPointerDown: _pointerDownHandlerFor(ix, iy),
this, onPointerUp: _pointerUpHandlerFor(ix, iy),
flagged: false, child: new CoveredMineNode(
posX: ix, posY: iy)); flagged: false,
// Mutating |hasCoveredCell| here is hacky, but convenient, same posX: ix,
// goes for mutating |hasWon| below. posY: iy
hasCoveredCell = true; )
} else if (state == flaggedCell) { ));
// Mutating |hasCoveredCell| here is hacky, but convenient, same
// goes for mutating |hasWon| below.
hasCoveredCell = true;
} else if (state == CellState.flagged) {
row.add(new CoveredMineNode( row.add(new CoveredMineNode(
this,
flagged: true, flagged: true,
posX: ix, posY: iy)); posX: ix, posY: iy)
);
} else { } else {
row.add(new ExposedMineNode( row.add(new ExposedMineNode(
state: state, state: state,
count: count)); count: count)
);
} }
} }
flexRows.add( flexRows.add(
...@@ -147,33 +174,32 @@ class Game { ...@@ -147,33 +174,32 @@ class Game {
} }
return new Container( return new Container(
key: 'minefield',
padding: new EdgeDims.all(10.0), padding: new EdgeDims.all(10.0),
margin: new EdgeDims.all(10.0), margin: new EdgeDims.all(10.0),
decoration: new BoxDecoration(backgroundColor: const Color(0xFF6B6B6B)), decoration: new BoxDecoration(backgroundColor: const Color(0xFF6B6B6B)),
child: new Flex( child: new Flex(
flexRows, flexRows,
direction: FlexDirection.vertical, direction: FlexDirection.vertical
key: 'flxv')); )
);
} }
Widget buildToolBar() { Widget buildToolBar() {
String banner = hasWon ? String toolbarCaption = hasWon ?
'Awesome!!' : alive ? 'Awesome!!' : alive ?
'Mine Digger [$detectedCount-$totalMineCount]': 'Kaboom! [press here]'; 'Mine Digger [$detectedCount-$totalMineCount]': 'Kaboom! [press here]';
return new ToolBar( return new ToolBar(
// FIXME: Strange to have the toolbar be tapable. // FIXME: Strange to have the toolbar be tapable.
center: new Listener( center: new Listener(
onPointerDown: handleBannerPointerDown, onPointerDown: handleToolbarPointerDown,
child: new Text(banner, style: Theme.of(this.app).text.title) child: new Text(toolbarCaption, style: Theme.of(this).text.title)
) )
); );
} }
Widget buildUI() { Widget build() {
// FIXME: We need to build the board before we build the toolbar because // We build the board before we build the toolbar because we compute the win state during build step.
// we compute the win state during build step.
Widget board = buildBoard(); Widget board = buildBoard();
return new TaskDescription( return new TaskDescription(
label: 'Mine Digger', label: 'Mine Digger',
...@@ -187,39 +213,42 @@ class Game { ...@@ -187,39 +213,42 @@ class Game {
); );
} }
void handleBannerPointerDown(sky.PointerEvent event) { void handleToolbarPointerDown(sky.PointerEvent event) {
initialize(); setState(() {
app.scheduleBuild(); resetGame();
});
} }
// User action. The user uncovers the cell which can cause losing the game. // User action. The user uncovers the cell which can cause losing the game.
void probe(int x, int y) { void probe(int x, int y) {
if (!alive) if (!alive)
return; return;
if (uiState[y][x] == flaggedCell) if (uiState[y][x] == CellState.flagged)
return; return;
// Allowed to probe. setState(() {
if (cells[y][x]) { // Allowed to probe.
// Probed on a mine --> dead!! if (cells[y][x]) {
uiState[y][x] = explodedCell; // Probed on a mine --> dead!!
alive = false; uiState[y][x] = CellState.exploded;
} else { alive = false;
// No mine, uncover nearby if possible. } else {
cull(x, y); // No mine, uncover nearby if possible.
} cull(x, y);
app.scheduleBuild(); }
});
} }
// User action. The user is sure a mine is at this location. // User action. The user is sure a mine is at this location.
void flag(int x, int y) { void flag(int x, int y) {
if (uiState[y][x] == flaggedCell) { setState(() {
uiState[y][x] = coveredCell; if (uiState[y][x] == CellState.flagged) {
--detectedCount; uiState[y][x] = CellState.covered;
} else { --detectedCount;
uiState[y][x] = flaggedCell; } else {
++detectedCount; uiState[y][x] = CellState.flagged;
} ++detectedCount;
app.scheduleBuild(); }
});
} }
// Recursively uncovers cells whose totalMineCount is zero. // Recursively uncovers cells whose totalMineCount is zero.
...@@ -229,9 +258,9 @@ class Game { ...@@ -229,9 +258,9 @@ class Game {
if ((y < 0) || (y > cols - 1)) if ((y < 0) || (y > cols - 1))
return; return;
if (uiState[y][x] == clearedCell) if (uiState[y][x] == CellState.cleared)
return; return;
uiState[y][x] = clearedCell; uiState[y][x] = CellState.cleared;
if (mineCount(x, y) > 0) if (mineCount(x, y) > 0)
return; return;
...@@ -269,106 +298,70 @@ class Game { ...@@ -269,106 +298,70 @@ class Game {
} }
} }
Widget makeCell(Widget widget) { Widget buildCell(Widget child) {
return new Container( return new Container(
padding: new EdgeDims.all(1.0), padding: new EdgeDims.all(1.0),
height: 27.0, width: 27.0, height: 27.0, width: 27.0,
decoration: new BoxDecoration(backgroundColor: const Color(0xFFC0C0C0)), decoration: new BoxDecoration(backgroundColor: const Color(0xFFC0C0C0)),
margin: new EdgeDims.all(2.0), margin: new EdgeDims.all(2.0),
child: widget); child: child
);
} }
Widget makeInnerCell(Widget widget) { Widget buildInnerCell(Widget child) {
return new Container( return new Container(
padding: new EdgeDims.all(1.0), padding: new EdgeDims.all(1.0),
margin: new EdgeDims.all(3.0), margin: new EdgeDims.all(3.0),
height: 17.0, width: 17.0, height: 17.0, width: 17.0,
child: widget); child: child
);
} }
class CoveredMineNode extends Component { class CoveredMineNode extends Component {
final Game game;
CoveredMineNode({ this.flagged, this.posX, this.posY });
final bool flagged; final bool flagged;
final int posX; final int posX;
final int posY; 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 build() {
Widget text = flagged ? Widget text;
makeInnerCell(new StyledText(elements : [Game.textStyles[5], '\u2691'])) : if (flagged)
null; text = buildInnerCell(new StyledText(elements : [textStyles[5], '\u2691']));
Container inner = new Container( Container inner = new Container(
margin: new EdgeDims.all(2.0), margin: new EdgeDims.all(2.0),
height: 17.0, width: 17.0, height: 17.0, width: 17.0,
decoration: new BoxDecoration(backgroundColor: const Color(0xFFD9D9D9)), decoration: new BoxDecoration(backgroundColor: const Color(0xFFD9D9D9)),
child: text); child: text
);
return makeCell(new Listener( return buildCell(inner);
child: inner,
onPointerDown: _handlePointerDown,
onPointerUp: _handlePointerUp));
} }
} }
class ExposedMineNode extends Component { 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() { Widget build() {
StyledText text; StyledText text;
if (state == Game.clearedCell) { if (state == CellState.cleared) {
// Uncovered cell with nearby mine count. // Uncovered cell with nearby mine count.
if (count != 0) if (count != 0)
text = new StyledText(elements : [Game.textStyles[count], '$count']); text = new StyledText(elements : [textStyles[count], '$count']);
} else { } else {
// Exploded mine or shown mine for 'game over'. // Exploded mine or shown mine for 'game over'.
int color = state == Game.explodedCell ? 3 : 0; int color = state == CellState.exploded ? 3 : 0;
text = new StyledText(elements : [Game.textStyles[color], '\u2600']); text = new StyledText(elements : [textStyles[color], '\u2600']);
} }
return buildCell(buildInnerCell(text));
return makeCell(makeInnerCell(text));
} }
}
class MineDiggerApp extends App {
Game game;
MineDiggerApp() {
game = new Game(this);
}
Widget build() {
return game.buildUI();
}
} }
void main() { void main() {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment