OLD | NEW |
---|---|
(Empty) | |
1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | |
2 // for details. All rights reserved. Use of this source code is governed by a | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 #library('game_of_life_components'); | |
6 | |
7 #import('dart:html'); | |
8 #import('dart:math', prefix: 'Math'); | |
9 #import('package:dart-web-components/lib/js_polyfill/web_components.dart'); | |
10 | |
11 typedef void Ping(); | |
Siggi Cherem (dart-lang)
2012/09/07 22:04:09
Pong? this might need some explanation.
samhop
2012/09/07 23:55:17
Done.
| |
12 | |
13 // We've done things this way because we can't have default values for fields | |
14 // inside a web component right now (see bug 4957). | |
15 | |
16 /** How big should the (square) board be by default? Measured in cells/side. */ | |
17 final int DEFAULT_GAME_SIZE = 40; | |
18 | |
19 /** | |
20 * How many pixels long is the side of a cell by default? | |
21 * (Note: must match the CSS!) | |
22 */ | |
23 final int DEFAULT_CELL_SIZE = 20; | |
24 | |
25 /** How many pixels from the game should the control panel be by default? */ | |
26 final int DEFAULT_PANEL_OFFSET = 20; | |
27 | |
28 /** How many milliseconds between steps by default? */ | |
29 final int DEFAULT_STEP_TIME = 1000; | |
30 | |
31 /** | |
32 * Maps tag names to Dart constructors for components in this library. | |
33 * Singleton. | |
34 */ | |
35 Map<String, WebComponentFactory> get CTOR_MAP { | |
36 if (_CTOR_MAP == null) { | |
37 _CTOR_MAP = { | |
38 'x-cell' : () => new Cell.component(), | |
39 'x-control-panel' : () => new ControlPanel.component(), | |
40 'x-game-of-life' : () => new GameOfLife.component() | |
41 }; | |
42 } | |
43 return _CTOR_MAP; | |
44 } | |
45 | |
46 Map<String, WebComponentFactory> _CTOR_MAP; | |
47 | |
48 /** | |
49 * If the importing code uses only the components in this library, | |
50 * this function will do all necessary component initialization. | |
51 */ | |
52 void gameOfLifeComponentsSetup() { | |
53 initializeComponents((String name) => CTOR_MAP[name], true); | |
54 } | |
55 | |
56 /** | |
57 * A single cell in the Game Of Life. Listens to a GameOfLife parent component | |
58 * to get a clock tick, and interacts on its neighbors on every tick to move the | |
59 * game one step forward. | |
60 */ | |
61 class Cell extends DivElementImpl implements WebComponent, Hashable { | |
62 Collection<Cell> neighbors; | |
63 ShadowRoot _root; | |
64 GameOfLife game; | |
65 bool aliveThisStep; | |
66 bool aliveNextStep; | |
67 | |
68 // BEGIN AUTOGENERATED CODE | |
69 static WebComponentFactory _$constr; | |
70 factory Cell.component() { | |
71 if(_$constr == null) { | |
72 _$constr = () => new Cell._internal(); | |
73 } | |
74 var t1 = new DivElement(); | |
75 t1.attributes['is'] = 'x-cell'; | |
76 rewirePrototypeChain(t1, _$constr, 'Cell'); | |
77 return t1; | |
78 } | |
79 | |
80 factory Cell() { | |
81 return manager.expandHtml('<div is="x-cell"></div>'); | |
82 } | |
83 | |
84 Cell._internal(); | |
85 // END AUTOGENERATED CODE | |
86 | |
87 void created(ShadowRoot root) { | |
88 _root = root; | |
89 neighbors = <Cell>[]; | |
90 classes.add('cell'); | |
91 | |
92 // Cells start dead. | |
93 aliveThisStep = false; | |
94 } | |
95 | |
96 void inserted() { } | |
97 | |
98 /** | |
99 * Set up event listeners and populate [neighbors] by querying [game] for this | |
100 * cell's neighbors. Event listeners can be done here rather than dealt with | |
101 * in [inserted] and [removed] because cells will always be gc'd if removed | |
102 * from the DOM. | |
103 */ | |
104 void bound() { | |
105 on.click.add((event) { | |
106 classes.toggle('alive'); | |
107 aliveThisStep = !aliveThisStep; | |
108 }); | |
109 | |
110 game.on.step.add(step); | |
111 game.on.resolve.add(resolve); | |
112 | |
113 // find neighbors | |
114 var parsedCoordinates = this.id.substring(1).split('y'); | |
115 var x = Math.parseInt(parsedCoordinates[0]); | |
116 var y = Math.parseInt(parsedCoordinates[1]); | |
117 for (int dx = -1; dx <= 1; dx++) { | |
118 for (int dy = -1; dy <= 1; dy++) { | |
119 if (game.inGrid(x + dx, y + dy) && !(dx == 0 && dy == 0)) { | |
120 var neighbor = game._query('#x${x + dx}y${y + dy}'); | |
121 neighbors.add(neighbor); | |
122 } | |
123 } | |
124 } | |
125 } | |
126 | |
127 | |
128 void attributeChanged(String name, String oldValue, String newValue) { } | |
129 | |
130 void removed() { } | |
131 | |
132 /** | |
133 * Each turn of the game is broken into a step and a resolve. On a step, the | |
134 * cell queries its neighbors current states and decides whether or not will | |
135 * be alive or dead next turn. | |
136 */ | |
137 void step() { | |
138 var numAlive = neighbors.filter((n) => n.aliveThisStep).length; | |
139 // We could compress this into one line, but it's clearer this way. | |
140 aliveNextStep = false; | |
141 if (aliveThisStep) { | |
142 if (numAlive == 2 || numAlive == 3) { | |
143 aliveNextStep = true; | |
144 } | |
145 } else { | |
146 if (numAlive == 3) { | |
147 aliveNextStep = true; | |
148 } | |
149 } | |
150 } | |
151 | |
152 /** | |
153 * Each turn of the game is broken in a step and a resolve. On a resolve, the | |
154 * cell uses the information collected in the step phase to update its state | |
155 * and appearance -- black if alive this turn, white if dead this turn. | |
156 */ | |
157 void resolve() { | |
158 if (aliveNextStep) { | |
159 classes.add('alive'); | |
160 } else { | |
161 classes.remove('alive'); | |
162 } | |
163 aliveThisStep = aliveNextStep; | |
164 } | |
165 | |
166 } | |
167 | |
168 /** | |
169 * A control panel for the Game of Life. Has start, stop, and step buttons which | |
170 * start the game, stop the game, and move the game one turn forward, | |
171 * respectively. | |
172 */ | |
173 class ControlPanel extends DivElementImpl implements WebComponent { | |
174 ShadowRoot _root; | |
175 GameOfLife game; | |
176 | |
177 // BEGIN AUTOGENERATED CODE | |
178 static WebComponentFactory _$constr; | |
179 factory ControlPanel.component() { | |
180 if(_$constr == null) { | |
181 _$constr = () => new ControlPanel._internal(); | |
182 } | |
183 var t1 = new DivElement(); | |
184 t1.attributes['is'] = 'x-control-panel'; | |
185 rewirePrototypeChain(t1, _$constr, 'ControlPanel'); | |
186 return t1; | |
187 } | |
188 | |
189 factory ControlPanel() { | |
190 return manager.expandHtml('<div is="x-control-panel"></div>'); | |
191 } | |
192 | |
193 ControlPanel._internal(); | |
194 // END AUTOGENERATED CODE | |
195 | |
196 void created(ShadowRoot root) { | |
197 _root = root; | |
198 } | |
199 | |
200 void inserted() { } | |
201 | |
202 /** | |
203 * Sets up event listeners for the buttons. This must be done here rather than | |
204 * in [inserted] because the events must propogate up to [game]. | |
205 */ | |
206 void bound() { | |
207 _root.query('#start').on.click.add((e) => game.run()); | |
208 _root.query('#stop').on.click.add((e) => game.stop()); | |
209 _root.query('#step').on.click.add((e) => game.step()); | |
210 } | |
211 | |
212 void attributeChanged(String name, String oldValue, String newValue) { } | |
213 | |
214 void removed() { } | |
215 } | |
216 | |
217 /** | |
218 * A Game of Life component, containing an interactive implementation of | |
219 * Conway's Game of Life. | |
220 */ | |
221 class GameOfLife extends DivElementImpl implements WebComponent { | |
222 | |
223 // Implementation Notes: The game consists of a control panel and a board | |
224 // composed of cells. Each cell is a web component, and the control panel is a | |
225 // web component. The top-level widget populates the board with cells and | |
226 // provides a clock tick to which the cells listen. It exposes API to stop and | |
227 // start that tick, which the control panel binds to its buttons. Aside | |
228 // from the tick, no state is maintained in the top level widget -- each cell | |
229 // maintains its own state and talks to its neighbors to move the game | |
230 // forward. | |
231 | |
232 // TODO(samhop): implement wraparound on the board. | |
233 ShadowRoot _root; | |
234 GameOfLifeEvents on; | |
235 Timer timer; | |
236 int lastRefresh; | |
237 bool _stop; | |
238 StyleElement computedStyles; | |
239 | |
240 // These cannot be initialized here right now -- see bug 4957. | |
241 | |
242 /** How big should the (square) board be? Measured in cells/side. */ | |
243 int GAME_SIZE; | |
244 | |
245 /** How many pixels long is the side of a cell? (Note: must match the CSS!) */ | |
246 int CELL_SIZE; | |
247 | |
248 /** How many pixels from the game should the control panel be? */ | |
249 int PANEL_OFFSET; | |
250 | |
251 /** How many milliseconds between steps? */ | |
252 int _stepTime; | |
253 | |
254 void set stepTime(int time) { | |
255 _stepTime = time; | |
256 } | |
257 | |
258 // BEGIN AUTOGENERATED CODE | |
259 static WebComponentFactory _$constr; | |
260 factory GameOfLife.component() { | |
261 if(_$constr == null) { | |
262 _$constr = () => new GameOfLife._internal(); | |
263 } | |
264 var t1 = new DivElement(); | |
265 t1.attributes['is'] = 'x-game-of-life'; | |
266 rewirePrototypeChain(t1, _$constr, 'GameOfLife'); | |
267 return t1; | |
268 } | |
269 | |
270 factory GameOfLife() { | |
271 return manager.expandHtml('<div is="x-game-of-life"></div>'); | |
272 } | |
273 | |
274 GameOfLife._internal(); | |
275 // END AUTOGENERATED CODE | |
276 | |
277 /** On creation, initialize fields and then populate the game. */ | |
278 void created(ShadowRoot root) { | |
279 _root = root; | |
280 on = new GameOfLifeEvents(); | |
281 lastRefresh = 0; | |
282 | |
283 // At present we must do this initialization here -- see bug 4957. | |
284 GAME_SIZE = DEFAULT_GAME_SIZE; | |
285 CELL_SIZE = DEFAULT_CELL_SIZE; | |
286 PANEL_OFFSET = DEFAULT_PANEL_OFFSET; | |
287 _stepTime = DEFAULT_STEP_TIME; | |
288 | |
289 _populate(); | |
290 } | |
291 | |
292 void inserted() { } | |
293 | |
294 void attributeChanged(String name, String oldValue, String newValue) { } | |
295 | |
296 void removed() { } | |
297 | |
298 /** | |
299 * Returns the results of querying on [selector] beneath [_root]. Needed by | |
300 * Cells to determine their neighbors. | |
301 */ | |
302 _query(String selector) { | |
303 return _root.query(selector); | |
304 } | |
305 | |
306 /** Stop ticking. */ | |
307 void stop() { | |
308 _stop = true; | |
309 } | |
310 | |
311 /** | |
312 * Tick once, if it has been at least _stepTime milliseconds since the last | |
313 * tick. Then, if we haven't been told to stop, call set up a | |
314 * requestAnimationFrame callback to tick again. | |
315 */ | |
316 void _increment(int time) { | |
317 if (new Date.now().millisecondsSinceEpoch - lastRefresh >= _stepTime) { | |
318 on.step.forEach((f) => f()); | |
319 on.resolve.forEach((f) => f()); | |
320 lastRefresh = new Date.now().millisecondsSinceEpoch; | |
321 } | |
322 if (!_stop) { | |
323 window.requestAnimationFrame(_increment); | |
324 } | |
325 } | |
326 | |
327 /** Start the game. */ | |
328 void run() { | |
329 _stop = false; | |
330 window.requestAnimationFrame(_increment); | |
331 } | |
332 | |
333 /** | |
334 * Move the game one step forward. If the game was running, stop the game | |
335 * beforehand. | |
336 */ | |
337 void step() { | |
338 _stop = true; | |
339 _increment(null); | |
340 } | |
341 | |
342 /** | |
343 * Fill the game board with cells, position them appropriately, position the | |
344 * control panel, and bind all subcomponents. | |
345 */ | |
346 void _populate() { | |
347 // set up position styles | |
348 computedStyles = new StyleElement(); | |
349 _root.nodes.add(computedStyles); | |
350 var computedStylesBuffer = new StringBuffer(); | |
351 _forEachCell((i, j) => _addPositionId(computedStylesBuffer, i, j)); | |
352 | |
353 // position the control panel | |
354 var panelStyle = | |
355 ''' | |
356 #panel { | |
357 top: ${CELL_SIZE * GAME_SIZE + PANEL_OFFSET}px; | |
358 left: ${PANEL_OFFSET}px; | |
359 } | |
360 '''; | |
361 computedStylesBuffer.add('${computedStyles.innerHTML}\n$panelStyle'); | |
362 | |
363 computedStyles.innerHTML = computedStylesBuffer.toString(); | |
364 | |
365 // add cells | |
366 _forEachCell((i, j) { | |
367 var cell = new Cell(); | |
368 cell.game = this; | |
369 cell.id = _generatePositionString(i, j); | |
370 _root.nodes.add(cell); | |
371 }); | |
372 | |
373 // bind the control panel | |
374 var controlPanel = _root.query('div[is="x-control-panel"]'); | |
375 controlPanel.game = this; | |
376 controlPanel.bound(); | |
377 | |
378 // TODO(samhop): fix webcomponents.dart so that attributes are preserved. | |
379 _root.query('div').id = 'panel'; | |
380 | |
381 // TODO(samhop) fix webcomponents.dart so we don't have to do this | |
382 _root.queryAll('.cell').forEach((cell) => cell.bound()); | |
383 } | |
384 | |
385 /** | |
386 * Calls f exactly once on all pairs (i, j) for ints i, j between 0 and | |
387 * [GAME_SIZE] - 1, inclusive. | |
388 */ | |
389 void _forEachCell(f) { | |
390 for (var i = 0; i < GAME_SIZE; i++) { | |
391 for (var j = 0; j < GAME_SIZE; j++) { | |
392 f(i, j); | |
393 } | |
394 } | |
395 } | |
396 | |
397 /** | |
398 * Appends correct cell positioning information for cell ([i], [j]) to [curr]. | |
399 */ | |
400 String _addPositionId(StringBuffer curr, int i, int j) => | |
401 curr.add( | |
402 ''' | |
403 #${_generatePositionString(i, j)} { | |
404 left: ${CELL_SIZE * i}px; | |
405 top: ${CELL_SIZE * j}px; | |
406 } | |
407 '''); | |
408 | |
409 /** Returns the cell id corresponding to ([i], [j]). */ | |
410 String _generatePositionString(int i, int j) => 'x${i}y${j}'; | |
411 | |
412 /** | |
413 * Is the coordinate ([x],[y]) in the game grid, given the current | |
414 * [GAME_SIZE]? | |
415 */ | |
416 bool inGrid(x, y) => | |
417 (x >=0 && y >=0 && x < GAME_SIZE && y < GAME_SIZE); | |
418 | |
419 /** | |
420 * Set cell ([i],[j])'s aliveness to [alive]. Throws an | |
421 * IllegalArgumentException if ([i],[j]) is not a valid cell (i.e. if it is | |
422 * outside of the grid). | |
423 */ | |
424 void setAliveness(int i, int j, bool alive) { | |
425 _validateGridPosition(i, j); | |
426 _query('#${_generatePositionString(i, j)}').aliveThisStep = alive; | |
427 } | |
428 | |
429 /** | |
430 * Is cell ([i], [j]) currently alive? Throws an IllegalArgumentException if | |
431 * ([i], [j]) is not a valid cell (i.e. it is outside the grid). | |
432 */ | |
433 bool isAlive(int i, int j) { | |
434 _validateGridPosition(i, j); | |
435 return _query('#${_generatePositionString(i,j)}').aliveThisStep; | |
436 } | |
437 | |
438 /** Throw an IllegalArgumentException if ([i], [j]) is not a valid cell. */ | |
439 void _validateGridPosition(int i, int j) { | |
440 if (i < 0 || j < 0 || !inGrid(i,j)) { | |
441 throw new IllegalArgumentException('(${i}, ${j}) is a bad coordinate'); | |
442 } | |
443 assert(inGrid(i,j) && i >= 0 && j >= 0); | |
444 } | |
445 } | |
446 | |
447 /** Events container for a GameOfLife. */ | |
448 class GameOfLifeEvents implements Events { | |
449 List<Ping> _step_list; | |
450 List<Ping> _resolve_list; | |
451 | |
452 GameOfLifeEvents() | |
453 : _step_list = <Ping>[], | |
454 _resolve_list = <Ping>[]; | |
455 | |
456 List<Ping> get step => _step_list; | |
457 List<Ping> get resolve => _resolve_list; | |
458 } | |
OLD | NEW |