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

Unified 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: addressing sigmund's remarks 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 side-by-side diff with in-line comments
Download patch
Index: dart/samples/webcomponents/game_of_life/components/components.dart
diff --git a/dart/samples/webcomponents/game_of_life/components/components.dart b/dart/samples/webcomponents/game_of_life/components/components.dart
new file mode 100644
index 0000000000000000000000000000000000000000..f5d05d7f07a47626254d34906754727470a6bafe
--- /dev/null
+++ b/dart/samples/webcomponents/game_of_life/components/components.dart
@@ -0,0 +1,459 @@
+// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+#library('game_of_life_components');
+
+#import('dart:html');
+#import('dart:math', prefix: 'Math');
+#import('package:dart-web-components/lib/js_polyfill/web_components.dart');
+
+/** Functions used to propogate a tick to cells. */
+typedef void Ping();
+
+// We've done things this way because we can't have default values for fields
+// inside a web component right now (see bug 4957).
+
+/** How big should the (square) board be by default? Measured in cells/side. */
+final int DEFAULT_GAME_SIZE = 40;
+
+/**
+ * How many pixels long is the side of a cell by default?
+ * (Note: must match the CSS!)
+ */
+final int DEFAULT_CELL_SIZE = 20;
+
+/** How many pixels from the game should the control panel be by default? */
+final int DEFAULT_PANEL_OFFSET = 20;
+
+/** How many milliseconds between steps by default? */
+final int DEFAULT_STEP_TIME = 1000;
+
+/**
+ * Maps tag names to Dart constructors for components in this library.
+ * Singleton.
+ */
+Map<String, WebComponentFactory> get CTOR_MAP {
+ if (_CTOR_MAP == null) {
+ _CTOR_MAP = {
+ 'x-cell' : () => new Cell.component(),
+ 'x-control-panel' : () => new ControlPanel.component(),
+ 'x-game-of-life' : () => new GameOfLife.component()
+ };
+ }
+ return _CTOR_MAP;
+}
+
+Map<String, WebComponentFactory> _CTOR_MAP;
+
+/**
+ * If the importing code uses only the components in this library,
+ * this function will do all necessary component initialization.
+ */
+void gameOfLifeComponentsSetup() {
+ initializeComponents((String name) => CTOR_MAP[name], true);
+}
+
+/**
+ * A single cell in the Game Of Life. Listens to a GameOfLife parent component
+ * to get a clock tick, and interacts on its neighbors on every tick to move the
+ * game one step forward.
+ */
+class Cell extends DivElementImpl implements WebComponent, Hashable {
+ Collection<Cell> neighbors;
+ ShadowRoot _root;
+ GameOfLife game;
+ bool aliveThisStep;
+ bool aliveNextStep;
+
+ // BEGIN AUTOGENERATED CODE
+ static WebComponentFactory _$constr;
+ factory Cell.component() {
+ if(_$constr == null) {
+ _$constr = () => new Cell._internal();
+ }
+ var t1 = new DivElement();
+ t1.attributes['is'] = 'x-cell';
+ rewirePrototypeChain(t1, _$constr, 'Cell');
+ return t1;
+ }
+
+ factory Cell() {
+ return manager.expandHtml('<div is="x-cell"></div>');
+ }
+
+ Cell._internal();
+ // END AUTOGENERATED CODE
+
+ void created(ShadowRoot root) {
+ _root = root;
+ neighbors = <Cell>[];
+ classes.add('cell');
+
+ // Cells start dead.
+ aliveThisStep = false;
+ }
+
+ void inserted() { }
+
+ /**
+ * Set up event listeners and populate [neighbors] by querying [game] for this
+ * cell's neighbors. Event listeners can be done here rather than dealt with
+ * in [inserted] and [removed] because cells will always be gc'd if removed
+ * from the DOM.
+ */
+ void bound() {
+ on.click.add((event) {
+ classes.toggle('alive');
+ aliveThisStep = !aliveThisStep;
+ });
+
+ game.on.step.add(step);
+ game.on.resolve.add(resolve);
+
+ // find neighbors
+ var parsedCoordinates = this.id.substring(1).split('y');
+ var x = Math.parseInt(parsedCoordinates[0]);
+ var y = Math.parseInt(parsedCoordinates[1]);
+ for (int dx = -1; dx <= 1; dx++) {
+ for (int dy = -1; dy <= 1; dy++) {
+ if (game.inGrid(x + dx, y + dy) && !(dx == 0 && dy == 0)) {
+ var neighbor = game._query('#x${x + dx}y${y + dy}');
+ neighbors.add(neighbor);
+ }
+ }
+ }
+ }
+
+
+ void attributeChanged(String name, String oldValue, String newValue) { }
+
+ void removed() { }
+
+ /**
+ * Each turn of the game is broken into a step and a resolve. On a step, the
+ * cell queries its neighbors current states and decides whether or not will
+ * be alive or dead next turn.
+ */
+ void step() {
+ var numAlive = neighbors.filter((n) => n.aliveThisStep).length;
+ // We could compress this into one line, but it's clearer this way.
+ aliveNextStep = false;
+ if (aliveThisStep) {
+ if (numAlive == 2 || numAlive == 3) {
+ aliveNextStep = true;
+ }
+ } else {
+ if (numAlive == 3) {
+ aliveNextStep = true;
+ }
+ }
+ }
+
+ /**
+ * Each turn of the game is broken in a step and a resolve. On a resolve, the
+ * cell uses the information collected in the step phase to update its state
+ * and appearance -- black if alive this turn, white if dead this turn.
+ */
+ void resolve() {
+ if (aliveNextStep) {
+ classes.add('alive');
+ } else {
+ classes.remove('alive');
+ }
+ aliveThisStep = aliveNextStep;
+ }
+
+}
+
+/**
+ * A control panel for the Game of Life. Has start, stop, and step buttons which
+ * start the game, stop the game, and move the game one turn forward,
+ * respectively.
+ */
+class ControlPanel extends DivElementImpl implements WebComponent {
+ ShadowRoot _root;
+ GameOfLife game;
+
+ // BEGIN AUTOGENERATED CODE
+ static WebComponentFactory _$constr;
+ factory ControlPanel.component() {
+ if(_$constr == null) {
+ _$constr = () => new ControlPanel._internal();
+ }
+ var t1 = new DivElement();
+ t1.attributes['is'] = 'x-control-panel';
+ rewirePrototypeChain(t1, _$constr, 'ControlPanel');
+ return t1;
+ }
+
+ factory ControlPanel() {
+ return manager.expandHtml('<div is="x-control-panel"></div>');
+ }
+
+ ControlPanel._internal();
+ // END AUTOGENERATED CODE
+
+ void created(ShadowRoot root) {
+ _root = root;
+ }
+
+ void inserted() { }
+
+ /**
+ * Sets up event listeners for the buttons. This must be done here rather than
+ * in [inserted] because the events must propogate up to [game].
+ */
+ void bound() {
+ _root.query('#start').on.click.add((e) => game.run());
+ _root.query('#stop').on.click.add((e) => game.stop());
+ _root.query('#step').on.click.add((e) => game.step());
+ }
+
+ void attributeChanged(String name, String oldValue, String newValue) { }
+
+ void removed() { }
+}
+
+/**
+ * A Game of Life component, containing an interactive implementation of
+ * Conway's Game of Life.
+ */
+class GameOfLife extends DivElementImpl implements WebComponent {
+
+ // Implementation Notes: The game consists of a control panel and a board
+ // composed of cells. Each cell is a web component, and the control panel is a
+ // web component. The top-level widget populates the board with cells and
+ // provides a clock tick to which the cells listen. It exposes API to stop and
+ // start that tick, which the control panel binds to its buttons. Aside
+ // from the tick, no state is maintained in the top level widget -- each cell
+ // maintains its own state and talks to its neighbors to move the game
+ // forward.
+
+ // TODO(samhop): implement wraparound on the board.
+ ShadowRoot _root;
+ GameOfLifeEvents on;
+ Timer timer;
+ int lastRefresh;
+ bool _stop;
+ StyleElement computedStyles;
+
+ // These cannot be initialized here right now -- see bug 4957.
+
+ /** How big should the (square) board be? Measured in cells/side. */
+ int GAME_SIZE;
+
+ /** How many pixels long is the side of a cell? (Note: must match the CSS!) */
+ int CELL_SIZE;
+
+ /** How many pixels from the game should the control panel be? */
+ int PANEL_OFFSET;
+
+ /** How many milliseconds between steps? */
+ int _stepTime;
+
+ void set stepTime(int time) {
+ _stepTime = time;
+ }
+
+ // BEGIN AUTOGENERATED CODE
+ static WebComponentFactory _$constr;
+ factory GameOfLife.component() {
+ if(_$constr == null) {
+ _$constr = () => new GameOfLife._internal();
+ }
+ var t1 = new DivElement();
+ t1.attributes['is'] = 'x-game-of-life';
+ rewirePrototypeChain(t1, _$constr, 'GameOfLife');
+ return t1;
+ }
+
+ factory GameOfLife() {
+ return manager.expandHtml('<div is="x-game-of-life"></div>');
+ }
+
+ GameOfLife._internal();
+ // END AUTOGENERATED CODE
+
+ /** On creation, initialize fields and then populate the game. */
+ void created(ShadowRoot root) {
+ _root = root;
+ on = new GameOfLifeEvents();
+ lastRefresh = 0;
+
+ // At present we must do this initialization here -- see bug 4957.
+ GAME_SIZE = DEFAULT_GAME_SIZE;
+ CELL_SIZE = DEFAULT_CELL_SIZE;
+ PANEL_OFFSET = DEFAULT_PANEL_OFFSET;
+ _stepTime = DEFAULT_STEP_TIME;
+
+ _populate();
+ }
+
+ void inserted() { }
+
+ void attributeChanged(String name, String oldValue, String newValue) { }
+
+ void removed() { }
+
+ /**
+ * Returns the results of querying on [selector] beneath [_root]. Needed by
+ * Cells to determine their neighbors.
+ */
+ _query(String selector) {
+ return _root.query(selector);
+ }
+
+ /** Stop ticking. */
+ void stop() {
+ _stop = true;
+ }
+
+ /**
+ * Tick once, if it has been at least _stepTime milliseconds since the last
+ * tick. Then, if we haven't been told to stop, call set up a
+ * requestAnimationFrame callback to tick again.
+ */
+ void _increment(int time) {
+ if (new Date.now().millisecondsSinceEpoch - lastRefresh >= _stepTime) {
+ on.step.forEach((f) => f());
+ on.resolve.forEach((f) => f());
+ lastRefresh = new Date.now().millisecondsSinceEpoch;
+ }
+ if (!_stop) {
+ window.requestAnimationFrame(_increment);
+ }
+ }
+
+ /** Start the game. */
+ void run() {
+ _stop = false;
+ window.requestAnimationFrame(_increment);
+ }
+
+ /**
+ * Move the game one step forward. If the game was running, stop the game
+ * beforehand.
+ */
+ void step() {
+ _stop = true;
+ _increment(null);
+ }
+
+ /**
+ * Fill the game board with cells, position them appropriately, position the
+ * control panel, and bind all subcomponents.
+ */
+ void _populate() {
+ // set up position styles
+ computedStyles = new StyleElement();
+ _root.nodes.add(computedStyles);
+ var computedStylesBuffer = new StringBuffer();
+ _forEachCell((i, j) => _addPositionId(computedStylesBuffer, i, j));
+
+ // position the control panel
+ var panelStyle =
+ '''
+ #panel {
+ top: ${CELL_SIZE * GAME_SIZE + PANEL_OFFSET}px;
+ left: ${PANEL_OFFSET}px;
+ }
+ ''';
+ computedStylesBuffer.add('${computedStyles.innerHTML}\n$panelStyle');
+
+ computedStyles.innerHTML = computedStylesBuffer.toString();
+
+ // add cells
+ _forEachCell((i, j) {
+ var cell = new Cell();
+ cell.game = this;
+ cell.id = _generatePositionString(i, j);
+ _root.nodes.add(cell);
+ });
+
+ // bind the control panel
+ var controlPanel = _root.query('div[is="x-control-panel"]');
+ controlPanel.game = this;
+ controlPanel.bound();
+
+ // TODO(samhop): fix webcomponents.dart so that attributes are preserved.
+ _root.query('div').id = 'panel';
+
+ // TODO(samhop) fix webcomponents.dart so we don't have to do this
+ _root.queryAll('.cell').forEach((cell) => cell.bound());
+ }
+
+ /**
+ * Calls f exactly once on all pairs (i, j) for ints i, j between 0 and
+ * [GAME_SIZE] - 1, inclusive.
+ */
+ void _forEachCell(f) {
+ for (var i = 0; i < GAME_SIZE; i++) {
+ for (var j = 0; j < GAME_SIZE; j++) {
+ f(i, j);
+ }
+ }
+ }
+
+ /**
+ * Appends correct cell positioning information for cell ([i], [j]) to [curr].
+ */
+ String _addPositionId(StringBuffer curr, int i, int j) =>
+ curr.add(
+ '''
+ #${_generatePositionString(i, j)} {
+ left: ${CELL_SIZE * i}px;
+ top: ${CELL_SIZE * j}px;
+ }
+ ''');
+
+ /** Returns the cell id corresponding to ([i], [j]). */
+ String _generatePositionString(int i, int j) => 'x${i}y${j}';
+
+ /**
+ * Is the coordinate ([x],[y]) in the game grid, given the current
+ * [GAME_SIZE]?
+ */
+ bool inGrid(x, y) =>
+ (x >=0 && y >=0 && x < GAME_SIZE && y < GAME_SIZE);
+
+ /**
+ * Set cell ([i],[j])'s aliveness to [alive]. Throws an
+ * IllegalArgumentException if ([i],[j]) is not a valid cell (i.e. if it is
+ * outside of the grid).
+ */
+ void setAliveness(int i, int j, bool alive) {
+ _validateGridPosition(i, j);
+ _query('#${_generatePositionString(i, j)}').aliveThisStep = alive;
+ }
+
+ /**
+ * Is cell ([i], [j]) currently alive? Throws an IllegalArgumentException if
+ * ([i], [j]) is not a valid cell (i.e. it is outside the grid).
+ */
+ bool isAlive(int i, int j) {
+ _validateGridPosition(i, j);
+ return _query('#${_generatePositionString(i,j)}').aliveThisStep;
+ }
+
+ /** Throw an IllegalArgumentException if ([i], [j]) is not a valid cell. */
+ void _validateGridPosition(int i, int j) {
+ if (i < 0 || j < 0 || !inGrid(i,j)) {
+ throw new IllegalArgumentException('(${i}, ${j}) is a bad coordinate');
+ }
+ assert(inGrid(i,j) && i >= 0 && j >= 0);
+ }
+}
+
+/** Events container for a GameOfLife. */
+class GameOfLifeEvents implements Events {
+ List<Ping> _step_list;
+ List<Ping> _resolve_list;
+
+ GameOfLifeEvents()
+ : _step_list = <Ping>[],
+ _resolve_list = <Ping>[];
+
+ List<Ping> get step => _step_list;
+ List<Ping> get resolve => _resolve_list;
+}
« 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