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