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