Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(882)

Side by Side Diff: dart/samples/webcomponents/game_of_life/components/components.dart

Issue 10918082: adding game_of_life sample (Closed) Base URL: git@github.com:samhopkins/bleeding_edge.git@master
Patch Set: ready for second round of review Created 8 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(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 }
OLDNEW
« no previous file with comments | « dart/samples/webcomponents/game_of_life/README.md ('k') | dart/samples/webcomponents/game_of_life/components/components.html » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698