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