diff --git a/analysis_options.yaml b/analysis_options.yaml index 78fb282..caef4c8 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,6 +1,6 @@ analyzer: exclude: [build/**] - + strong-mode: true errors: uri_has_not_been_generated: ignore plugins: diff --git a/lib/app_component.dart b/lib/app_component.dart index 1656739..ae8a931 100644 --- a/lib/app_component.dart +++ b/lib/app_component.dart @@ -4,8 +4,9 @@ import 'package:rules_of_living/components/configuration_component.dart'; import 'package:rules_of_living/components/controls_component.dart'; import 'package:rules_of_living/components/header_component.dart'; import 'package:rules_of_living/components/simulation_component.dart'; +import 'package:rules_of_living/service/configuration_service.dart'; +import 'package:rules_of_living/service/control_service.dart'; import 'package:rules_of_living/service/engine_service.dart'; -import 'package:rules_of_living/service/simulation_service.dart'; @Component( selector: 'my-app', @@ -23,7 +24,8 @@ import 'package:rules_of_living/service/simulation_service.dart'; providers: [ materialProviders, ClassProvider(EngineService), - ClassProvider(SimulationService) + ClassProvider(ConfigurationService), + ClassProvider(ControlService) ], styleUrls: const [ 'package:angular_components/app_layout/layout.scss.css', diff --git a/lib/components/configuration_component.dart b/lib/components/configuration_component.dart index abbd2e5..cc1ecd1 100644 --- a/lib/components/configuration_component.dart +++ b/lib/components/configuration_component.dart @@ -1,5 +1,3 @@ -import 'dart:math'; - import 'package:angular/angular.dart'; import 'package:angular_components/material_button/material_button.dart'; import 'package:angular_components/material_icon/material_icon.dart'; @@ -7,8 +5,7 @@ import 'package:angular_components/material_input/material_input.dart'; import 'package:angular_components/material_input/material_number_accessor.dart'; import 'package:angular_components/material_slider/material_slider.dart'; import 'package:angular_components/material_tooltip/material_tooltip.dart'; -import 'package:rules_of_living/service/engine_service.dart'; -import 'package:rules_of_living/service/simulation_service.dart'; +import 'package:rules_of_living/service/configuration_service.dart'; @Component( selector: "configuration", @@ -26,29 +23,28 @@ import 'package:rules_of_living/service/simulation_service.dart'; NgModel ]) class ConfigurationComponent { - final EngineService engine; - final SimulationService sim; + final ConfigurationService config; - int get width => sim.gridSize.x; + int get width => config.gridSize.x; void set width(num value) { if (value == null || value <= 0) return; - sim.gridSize = Point(value, sim.gridSize.y); + config.setGridSize(x: value.toInt()); } - int get height => sim.gridSize.y; + int get height => config.gridSize.y; void set height(num value) { if (value == null || value <= 0) return; - sim.gridSize = Point(sim.gridSize.x, value); + config.setGridSize(y: value.toInt()); } - int get simSpeed => engine.simSpeed; - void set simSpeed(int value) => engine.simSpeed = value; + int get simSpeed => config.simSpeed; + void set simSpeed(int value) => config.simSpeed = value; String get speedSliderTooltip => "Simulation Speed: $simSpeed"; - ConfigurationComponent(this.engine, this.sim); + ConfigurationComponent(this.config); void onEdgesClicked() { - sim.toggleGrid(); + config.toggleGrid(); } } diff --git a/lib/components/controls_component.dart b/lib/components/controls_component.dart index 538bc27..954d94b 100644 --- a/lib/components/controls_component.dart +++ b/lib/components/controls_component.dart @@ -1,7 +1,6 @@ import 'package:angular/angular.dart'; import 'package:angular_components/angular_components.dart'; -import 'package:rules_of_living/service/engine_service.dart'; -import 'package:rules_of_living/service/simulation_service.dart'; +import 'package:rules_of_living/service/control_service.dart'; @Component( selector: 'sim-controls', @@ -16,33 +15,27 @@ import 'package:rules_of_living/service/simulation_service.dart'; styleUrls: const ["controls_component.css"], ) class ControlsComponent { - final EngineService engine; - final SimulationService sim; + final ControlService ctrl; - ControlsComponent(this.engine, this.sim); + ControlsComponent(this.ctrl); void onStartClicked() { - engine.toggleRunning(); + ctrl.toggleRunning(); } void onStepClicked() { - engine.step(); + ctrl.step(); } - void onSaveClicked() { - sim.save(); - } - - void onLoadClicked() { - sim.load(); + void onResetClicked() { + ctrl.reset(); } void onRandomClicked() { - sim.addRandomPattern(); - engine.stop(); + ctrl.addRandomPattern(); } void onClearClicked() { - sim.reset(); + ctrl.clear(); } } diff --git a/lib/components/controls_component.html b/lib/components/controls_component.html index 764ca37..b4c43f3 100644 --- a/lib/components/controls_component.html +++ b/lib/components/controls_component.html @@ -1,8 +1,7 @@
- - + - + diff --git a/lib/components/simulation_component.dart b/lib/components/simulation_component.dart index 1fcef71..312eef4 100644 --- a/lib/components/simulation_component.dart +++ b/lib/components/simulation_component.dart @@ -1,8 +1,7 @@ import 'dart:html' as html; import 'package:angular/angular.dart'; -import 'package:rules_of_living/service/engine_service.dart'; -import 'package:rules_of_living/service/simulation_service.dart'; +import 'package:rules_of_living/service/configuration_service.dart'; @Component( selector: 'gol-simulation', @@ -11,10 +10,9 @@ import 'package:rules_of_living/service/simulation_service.dart'; providers: [], ) class SimulationComponent implements OnInit { - final EngineService engine; - final SimulationService sim; + final ConfigurationService config; - SimulationComponent(this.engine, this.sim); + SimulationComponent(this.config); @override void ngOnInit() { @@ -31,6 +29,6 @@ class SimulationComponent implements OnInit { the canvas did not load correctly :( ''', canvas.width / 2 - 50, canvas.height / 2); - sim.canvas = canvas; + config.canvas = canvas; } } diff --git a/lib/service/configuration_service.dart b/lib/service/configuration_service.dart new file mode 100644 index 0000000..e11a5e8 --- /dev/null +++ b/lib/service/configuration_service.dart @@ -0,0 +1,43 @@ +import 'dart:html' as html; +import 'dart:math'; + +import 'package:rules_of_living/service/engine_service.dart'; + +class ConfigurationService { + final EngineService _es; + + bool showGrid; + + int _simSpeed; + + ConfigurationService(this._es) { + showGrid = false; + simSpeed = 5; + } + + /// Simulation Speed + /// + /// Sets the number of updates the simulation takes per second. Can range from + /// 1 to arbitrarily high numbers (though setting it too high can potentially + /// make the app brittle). + int get simSpeed => _simSpeed; + void set simSpeed(int val) { + _simSpeed = val; + _es.engine.stepsPerSecond = simSpeed; + } + + void set canvas(html.CanvasElement canvas) => _es.engine.canvas = canvas; + html.CanvasElement get canvas => _es.engine.canvas; + + void toggleGrid() { + showGrid = !showGrid; + } + + void setGridSize({int x, int y}) { + x = x ?? _es.engine.gridSize.x; + y = y ?? _es.engine.gridSize.y; + _es.engine.gridSize = Point(x, y); + } + + Point get gridSize => _es.engine.gridSize; +} diff --git a/lib/service/control_service.dart b/lib/service/control_service.dart new file mode 100644 index 0000000..0066106 --- /dev/null +++ b/lib/service/control_service.dart @@ -0,0 +1,38 @@ +import 'package:rules_of_living/service/engine_service.dart'; + +class ControlService { + EngineService _es; + + ControlService(this._es); + + void run() { + _es.engine.running = true; + } + + void stop() { + _es.engine.running = false; + } + + void toggleRunning() { + _es.engine.running = !_es.engine.running; + } + + void step() { + _es.engine.step(); + } + + void reset() { + _es.engine.reset(); + } + + void addRandomPattern() { + _es.engine.running = false; + _es.engine.addPattern(); + } + + void clear() { + _es.engine.clear(); + } + + bool get isRunning => _es.engine.running; +} diff --git a/lib/service/engine_service.dart b/lib/service/engine_service.dart index b276d28..8168735 100644 --- a/lib/service/engine_service.dart +++ b/lib/service/engine_service.dart @@ -1,13 +1,8 @@ import 'package:rules_of_living/src/Engine.dart'; -import 'package:rules_of_living/src/Simulation.dart'; class EngineService { Engine _uncachedEngineAccess; - EngineService() { - simSpeed = 5; - } - Engine get engine => _uncachedEngineAccess ?? _setCachedAndReturn(Engine()); void set engine(Engine newEngine) { _uncachedEngineAccess = newEngine; @@ -17,32 +12,4 @@ class EngineService { engine = newEngine; return newEngine; } - - void run() { - engine.running = true; - } - - void stop() { - engine.running = false; - } - - void toggleRunning() { - engine.running = !engine.running; - } - - void step() { - engine.step(); - } - - /// Simulation Speed - /// - /// Sets the number of updates the simulation takes per second. Can range from - /// 1 to arbitrarily high numbers (though setting it too high can potentially - /// make the app brittle). - int get simSpeed => engine.stepsPerSecond; - void set simSpeed(int val) => engine.stepsPerSecond = val; - - bool get isRunning => engine.running; - - void set simulation(Simulation value) => engine.simulation = value; } diff --git a/lib/service/simulation_service.dart b/lib/service/simulation_service.dart deleted file mode 100644 index 8e0c14f..0000000 --- a/lib/service/simulation_service.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'dart:html' as html; -import 'dart:math'; - -import 'package:rules_of_living/service/engine_service.dart'; -import 'package:rules_of_living/src/Simulation.dart'; - -class SimulationService { - // DEFAULT VALUES - static final int DEFAULT_GRID_SIZE = 50; - - final EngineService _engine; - final Simulation _sim; - - SimulationService(this._engine, [Simulation sim]) - : this._sim = sim ?? Simulation(DEFAULT_GRID_SIZE, DEFAULT_GRID_SIZE) { - _engine.simulation = _sim; - _sim.addRandomPattern(amount: 15, dispersal: 5); - } - - void reset() { - _sim.reset(); - } - - void addRandomPattern() { - _sim.addRandomPattern(); - } - - Point get gridSize => _sim.gridSize; - void set gridSize(Point size) { - _sim.gridSize = size; - } - - //TODO split into RenderService when rendering is decoupled from engine. - html.CanvasElement get canvas => _engine.engine.canvas; - void set canvas(html.CanvasElement canvas) => _engine.engine.canvas = canvas; - - void toggleGrid() { - _sim.renderEdges = !_sim.renderEdges; - } - - void save() => _sim.saveSnapshot(); - void load() => _sim.loadSnapshot(); -} diff --git a/lib/src/Cell.dart b/lib/src/Cell.dart new file mode 100644 index 0000000..b10142b --- /dev/null +++ b/lib/src/Cell.dart @@ -0,0 +1,36 @@ +import 'package:rules_of_living/src/Rule.dart'; + +class Cell { + bool state; + bool nextState = false; + List surviveRules = new List(); + List birthRules = new List(); + + /// For determining if render updates are necessary in [Grid].render() function + bool dirty = false; + + Cell([bool state = false]) : this.state = state; + + // Sets the actual state to what it should be next update + // Returns the newly assumed actual state of the cell. + bool advanceState() { + this.state = this.nextState; + this.nextState = false; + + this.dirty = true; + + return this.state; + } + + void update(int neighbors) { + if (state == true) { + surviveRules.forEach((Rule rule) { + if (rule.evaluate(neighbors) == true) this.nextState = true; + }); + } else { + birthRules.forEach((Rule rule) { + if (rule.evaluate(neighbors) == true) this.nextState = true; + }); + } + } +} diff --git a/lib/src/Engine.dart b/lib/src/Engine.dart index 5653bb9..17f474a 100644 --- a/lib/src/Engine.dart +++ b/lib/src/Engine.dart @@ -1,5 +1,7 @@ import 'dart:html' as html; +import 'dart:math'; +import 'package:rules_of_living/src/Render.dart'; import 'package:rules_of_living/src/Simulation.dart'; class Engine { @@ -28,67 +30,104 @@ class Engine { // ms stuck in updateloop after which game will declare itself unresponsive final int SAFETY_TIMEOUT = 2000; + bool running = false; + Render _render; + + /// Grid Size + /// + /// Number of cells on x coordinate and y coordinate. Can be set individually. + Point get gridSize => Point(_simulation.w, _simulation.h); + void set gridSize(Point value) { + if (value.x <= 0 || value.y <= 0) + throw ArgumentError("grid size must not be smaller than 1"); + _simulation = Simulation(value.x, value.y); + } + num _updateLag = 0.0; num _drawLag = 0.0; + Simulation __simulation; + Simulation get _simulation => __simulation; + void set _simulation(Simulation value) { + __simulation = value; + _resetRenderer(); + } + /// The Canvas to Paint on /// /// Manually define or change the canvas the engine should render to. Should /// be used if no canvas was defined at engine creation and it should be /// rendered later. - html.CanvasElement canvas; - Simulation _simulation; - bool running = false; + html.CanvasElement _canvas; + html.CanvasElement get canvas => _canvas; + void set canvas(html.CanvasElement canvas) { + _canvas = canvas; + _resetRenderer(); + } + + void _resetRenderer() { + if (this.canvas == null || _simulation == null) return; + _render = Render(canvas, gridSize); + } + + Engine([x = 100, y = 100, canvas]) { + this.canvas = canvas; + _simulation = Simulation(x, y); - Engine() { _elapsed.start(); + _simulation.addPattern(amount: 15, dispersal: 5); html.window.animationFrame.then(animFrame); } void animFrame(num now) { - int elapsed = _elapsed.elapsedMilliseconds; - _elapsed.reset(); - process(elapsed, SAFETY_TIMEOUT, update: this.update, render: this.render); + process(now); html.window.animationFrame.then(animFrame); } - void process(int elapsed, int timeOut, {Function update, Function render}) { - _drawLag += elapsed; - _updateLag += elapsed; + void reset() { + _simulation.reset(); + running = false; + } - while (running == true && - _shouldUpdate(_updateLag, elapsed, timeOut) == true) { + void clear() { + _simulation = new Simulation(gridSize.x, gridSize.y); + running = false; + } + + void process(num now) { + _drawLag += _elapsed.elapsedMilliseconds; + _updateLag += _elapsed.elapsedMilliseconds; + _elapsed.reset(); + + while (_updateLag >= _MS_PER_STEP) { + if (_elapsed.elapsedMilliseconds > SAFETY_TIMEOUT) { + // TODO stub - give warning etc when this occurs + print("ERROR STUCK IN UPDATE LOOP"); + break; + } + if (running == true) update(); _updateLag -= _MS_PER_STEP; - if (update == null) break; - update(); } if (_drawLag >= _MS_PER_FRAME) { - _drawLag = 0; - if (render == null) return; render(_updateLag / _MS_PER_STEP); + _drawLag = 0; } } - bool _shouldUpdate(int updateLag, int elapsed, int timeOut) { - if (updateLag < _MS_PER_STEP) return false; - if (elapsed > timeOut) throw StackOverflowError; - - return true; - } - /// Update Engine Logic /// /// Updates the logic of the engine by one tick. Should usually not be called /// directly, since it is automatically taken care of by the processing function. /// If simulation should be advanced manually one time, prefer using step(). void update() { - if (_simulation == null) return; - - Map simulationUpdate = _simulation.update(); - _simulation.mergeStateChanges(simulationUpdate); - - if (simulationUpdate.length == 0) running = false; + Map stateChanges = _simulation.update(); + if (stateChanges.length == 0) { + running = false; + return; + } else { + _render.mergeChanges(stateChanges); + } } /// Advances Logic One Update @@ -97,8 +136,8 @@ class Engine { /// simulation. Does not automatically re-render the new state /// (though this should usually not pose a problem). void step() { - update(); running = false; + _simulation.update(); } /// Renders the Current Simulation State @@ -106,11 +145,25 @@ class Engine { /// Renders the simulation once. Will usually automatically be called by /// the internal engine processing. Does not do anything if no canvas is /// defined. - void render([num interp]) { - if (canvas == null || _simulation == null) return; + void render([num interp]) {} - _simulation.render(canvas, interp); + void addPattern( + {CellPattern pattern, + int x, + int y, + int amount, + int dispersal, + int seed}) { + _simulation.addPattern( + pattern: pattern, + x: x, + y: y, + amount: amount, + dispersal: dispersal, + seed: seed); } - void set simulation(Simulation value) => _simulation = value; + void toggleEdgeRendering() { +// _grid.renderEdges = !_grid.renderEdges; + } } diff --git a/lib/src/Render.dart b/lib/src/Render.dart new file mode 100644 index 0000000..7eb1a04 --- /dev/null +++ b/lib/src/Render.dart @@ -0,0 +1,121 @@ +import 'dart:html' as html; +import 'dart:math'; +import 'package:stagexl/stagexl.dart' as sxl; + +class ColorScheme { + //TODO make iterable (perhaps through backing list which gets accesses by the variables) + final int ON; + final int EXPANSION; + final int CORE; + final int OFF; + + const ColorScheme(this.ON, this.EXPANSION, this.CORE, + [this.OFF = sxl.Color.Transparent]); +} + +class Render { + final sxl.Stage _stage; + final sxl.RenderLoop _renderLoop = sxl.RenderLoop(); + Map _colorMap; + final sxl.BitmapContainer _renderContainer = sxl.BitmapContainer(); + + bool transparentBG = false; + bool _renderEdges = false; + bool get renderEdges => _renderEdges; + void set renderEdges(bool value) { + _renderEdges = value; + _colorMap = _getColorMap(_cellSize, _colorScheme); + } + + Point _gridSize; + Point get _cellSize => Point( + _stage.stageWidth ~/ _gridSize.x, _stage.stageHeight ~/ _gridSize.y); + + final ColorScheme _colorScheme; + //TODO replace with scheme data structure to enable different color scheme injection + static const defaultScheme = + ColorScheme(sxl.Color.Blue, 1, 1, sxl.Color.Black); + + // TODO gridSize rendering can only be scaled uniformly currently - switch to Point(w,h)? + Render(html.CanvasElement canvas, Point gridSize, + [ColorScheme colorScheme = defaultScheme]) + : _stage = _createStage(canvas), + _colorScheme = colorScheme, + _gridSize = gridSize { + _colorMap = _getColorMap(_cellSize, colorScheme); + _initRenderGrid(_cellSize, gridSize).forEach((sxl.Bitmap bm) { + _renderContainer.addChild(bm); + }); + _renderLoop.addStage(_stage); + _stage.addChild(_renderContainer); + setDirty(); + } + + static sxl.Stage _createStage(html.CanvasElement canvas) { + sxl.StageXL.stageOptions.renderEngine = sxl.RenderEngine.WebGL; + sxl.StageXL.stageOptions.stageRenderMode = sxl.StageRenderMode.AUTO_INVALID; +// sxl.StageXL.stageOptions.backgroundColor = sxl.Color.Yellow; + return sxl.Stage(canvas); + } + + Map _getColorMap(Point size, ColorScheme scheme) { + print("${size.toString()}, ${scheme.toString()}"); + + // Creates a shape with color of each quad in scheme + sxl.Shape shape = sxl.Shape(); + // ON + shape.graphics.beginPath(); + shape.graphics.rect(0 * size.x, 0, size.x, size.y); + shape.graphics.fillColor(scheme.ON ?? sxl.Color.Transparent); + shape.graphics + .strokeColor(renderEdges ? sxl.Color.DarkGray : sxl.Color.Transparent); + shape.graphics.closePath(); + // OFF + shape.graphics.beginPath(); + shape.graphics.rect(1 * size.x, 0, size.x, size.y); + shape.graphics.fillColor(scheme.OFF ?? sxl.Color.Transparent); + shape.graphics + .strokeColor(renderEdges ? sxl.Color.DarkGray : sxl.Color.Transparent); + shape.graphics.closePath(); + + // creates one texture out of shape + sxl.BitmapData texture = sxl.BitmapData(2 * size.x, size.y); + texture.draw(shape); + // re-slice texture into individual BitmapDatas + Map colorMap = Map(); + List colorBitmaps = texture.sliceIntoFrames(size.x, size.y); + print("Found ${colorBitmaps.length} colors."); + // TODO more elegant way through iterables etc; also include EXPANSION, CORE functionality + colorMap[scheme.ON] = colorBitmaps[0]; + colorMap[scheme.OFF] = colorBitmaps[1]; + + return colorMap; + } + + List _initRenderGrid(Point cellSize, Point gridSize) { + List grid = List(); + for (int y = 0; y < gridSize.y; y++) { + for (int x = 0; x < gridSize.x; x++) { + sxl.Bitmap bm = sxl.Bitmap(); + + bm.bitmapData = _colorMap[_colorScheme.OFF]; + bm.x = x * cellSize.x; + bm.y = y * cellSize.y; + grid.add(bm); + } + } + + return grid; + } + + void setDirty() => _stage.invalidate(); + + void mergeChanges(Map stateChanges) { + if (stateChanges.length == 0) return; + stateChanges.forEach((int i, bool state) { + _renderContainer.getChildAt(i).bitmapData = + (state ? _colorMap[_colorScheme.ON] : _colorMap[_colorScheme.OFF]); + }); + setDirty(); + } +} diff --git a/lib/src/Rule.dart b/lib/src/Rule.dart new file mode 100644 index 0000000..723b4d0 --- /dev/null +++ b/lib/src/Rule.dart @@ -0,0 +1,5 @@ +class Rule { + final Function evaluate; + + Rule(this.evaluate); +} diff --git a/lib/src/Simulation.dart b/lib/src/Simulation.dart index 0955c9a..a10c16f 100644 --- a/lib/src/Simulation.dart +++ b/lib/src/Simulation.dart @@ -1,116 +1,148 @@ import 'dart:html' as html; import 'dart:math' as math; -import 'dart:math'; +import 'package:rules_of_living/src/Cell.dart'; import 'package:rules_of_living/src/Grid.dart'; -import 'package:rules_of_living/src/rules/GameOfLife.dart'; -import 'package:rules_of_living/src/rules/RuleSet.dart'; +import 'package:rules_of_living/src/Rule.dart'; enum CellPattern { SpaceShip, Blinker } class Simulation { - Grid map; - Grid _snapshot; - - RuleSet rules = GameOfLife(); + final Grid map; bool _dirty = true; - bool get dirty => _dirty; - bool _renderEdges = true; - bool get renderEdges => _renderEdges; + int _startingSeed; + int _x; + int _y; int _amount; int _dispersal; + CellPattern _pattern; - Point get gridSize => Point(map.width, map.height); - void set gridSize(Point value) { - if (value.x <= 0 || value.y <= 0) - throw ArgumentError("grid size must not be smaller than 1"); - map = Grid(value.x, value.y); - } + int get w => map.width; + int get h => map.height; Simulation(int w, int h) : this.map = new Grid(w, h) { - this.map = reset(); + for (int i = 0; i < map.length; i++) { + map[i] = _getGOLCell(); + } + print("Grid creation finished"); } - Simulation.fromGrid(Grid map) : this.map = map; + Cell _getGOLCell([bool defaultState = false]) { + Cell cell = Cell(defaultState); + Rule threeTrue = new Rule((int n) { + if (n == 3) return true; + return false; + }); + Rule twoTrue = new Rule((int n) { + if (n == 2) return true; + return false; + }); + cell.surviveRules.add(twoTrue); + cell.surviveRules.add(threeTrue); + cell.birthRules.add(threeTrue); + return cell; + } - Grid reset([Grid map]) { - map ??= this.map; + void reset() { + map.setAll(0, List.filled(map.length, _getGOLCell())); + if (_startingSeed != null) + addPattern( + pattern: _pattern, + dispersal: _dispersal, + amount: _amount, + seed: _startingSeed, + x: _x, + y: _y); _dirty = true; - map.setAll(0, List.filled(map.length, false)); - return map; } - void addRandomPattern({int amount, int dispersal}) { - int _startingSeed = DateTime.now().millisecondsSinceEpoch; + void addPattern( + {CellPattern pattern, + int x, + int y, + int amount, + int dispersal, + int seed}) { + _startingSeed = seed ?? DateTime.now().millisecondsSinceEpoch; math.Random rng = new math.Random(_startingSeed); + _x = x; + _y = y; _amount = amount ?? rng.nextInt(20); _dispersal = dispersal ?? 10; - int cx = rng.nextInt(map.width ~/ 3) + (map.width ~/ 3); - int cy = rng.nextInt(map.height ~/ 3) + (map.height ~/ 3); + _pattern = pattern; + int cx = x ?? rng.nextInt(map.width ~/ 3) + (map.width ~/ 3); + int cy = y ?? rng.nextInt(map.height ~/ 3) + (map.height ~/ 3); + switch (pattern) { + // Two blocks, offset + // ## + // ## + case CellPattern.Blinker: + setCellState(cx, cy, true); + setCellState(cx + 1, cy, true); + setCellState(cx, cy + 1, true); + setCellState(cx + 1, cy + 1, true); - int sanityCheck = 0; - for (var i = 0; i < (_amount); i++) { - sanityCheck++; - getCellState(cx, cy) - ? i-- - : setCellState( - cx + rng.nextInt(_dispersal), cy + rng.nextInt(_dispersal), true); - if (sanityCheck > 100 && sanityCheck > i * 3) break; + setCellState(cx + 2, cy + 2, true); + setCellState(cx + 3, cy + 2, true); + setCellState(cx + 2, cy + 3, true); + setCellState(cx + 3, cy + 3, true); + break; + // A 'gliding' Spaceship + // # + // # + // ### + case CellPattern.SpaceShip: + setCellState(1 + cx, 0 + cy, true); + setCellState(2 + cx, 1 + cy, true); + setCellState(2 + cx, 2 + cy, true); + setCellState(1 + cx, 2 + cy, true); + setCellState(0 + cx, 2 + cy, true); + break; + default: + int sanityCheck = 0; + for (var i = 0; i < (_amount); i++) { + sanityCheck++; + getCellState(cx, cy) + ? i-- + : setCellState(cx + rng.nextInt(_dispersal), + cy + rng.nextInt(_dispersal), true); + if (sanityCheck > 100 && sanityCheck > i * 3) break; + } + break; } - _dirty = true; } void setCellState(int x, int y, bool state) { - if (y >= map.height || x >= map.width) return null; - - state ? map.set(x, y, true) : map.set(x, y, false); + if (y < map.height && x < map.width) map.get(x, y).state = state; } bool getCellState(int x, int y) { - if (y >= map.height || x >= map.width) return null; - - return map.get(x, y); - } - - void toggleCellState(int x, int y) { - if (y >= map.height || x >= map.width) return null; - - getCellState(x, y) == null - ? setCellState(x, y, true) - : setCellState(x, y, false); + if (y < map.height && x < map.width) return map.get(x, y).state; + return null; } Map update() { - Map stateChanges = calculateNextState(map); - return stateChanges; - } - - void mergeStateChanges(Map stateChanges) { - stateChanges.forEach((i, el) => map[i] = el); - if (stateChanges.length != 0) _dirty = true; - } - - Map calculateNextState(Grid oldState) { Map stateChanges = Map(); for (int i = 0; i < map.length; i++) { math.Point p = map.toCoordinates(i); - bool cell = map[i]; - int neighbors = getNeighbors(p.x, p.y, rules.range); - if (cell == false && rules.checkBirth(neighbors) == true) { - stateChanges[i] = true; - } else if (cell == true && rules.checkSurvival(neighbors) == false) { - stateChanges[i] = false; - } + map[i].update(getSurroundingNeighbors(p.x, p.y, 1)); } + // TODO when implementing changeSet we can remove this second loop and add to changeSet in the first + for (int i = 0; i < map.length; i++) { + Cell el = map[i]; + if (el.state != el.nextState) stateChanges[i] = el.nextState; + el.advanceState(); + } + (stateChanges.length > 0) ? _dirty = true : false; return stateChanges; } - int getNeighbors(int x, int y, int range) { + int getSurroundingNeighbors(int x, int y, int range) { int count = 0; for (int ix = -range + x; ix <= range + x; ix++) { for (int iy = -range + y; iy <= range + y; iy++) { @@ -118,7 +150,7 @@ class Simulation { iy >= 0 && ix < map.width && iy < map.height && - getCellState(ix, iy) == true && + map.get(ix, iy).state == true && !(x == ix && y == iy)) count++; } } @@ -140,7 +172,7 @@ class Simulation { ctx.strokeRect(p.x * brickW, p.y * brickH, brickW, brickH); } - if (map[i] == true) + if (map[i].state == true) ctx.setFillColorRgb(155, 155, 255); else ctx.setFillColorRgb(0, 0, 0); @@ -154,10 +186,5 @@ class Simulation { _dirty = true; } - void saveSnapshot() => _snapshot = Grid.from(map); - Grid loadSnapshot() { - map = Grid.from(_snapshot); - _dirty = true; - return map; - } + bool get renderEdges => _renderEdges; } diff --git a/lib/src/rules/CellPattern.dart b/lib/src/rules/CellPattern.dart deleted file mode 100644 index ed4cae7..0000000 --- a/lib/src/rules/CellPattern.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'dart:math'; - -import 'package:collection/collection.dart'; - -class CellPattern extends DelegatingList { - final String _name; - CellPattern(String name, List base) - : _name = name, - super(base); - - String get name => _name; - - @override - String toString() { - return "$name: ${super.toString()}"; - } -} diff --git a/lib/src/rules/GameOfLife.dart b/lib/src/rules/GameOfLife.dart deleted file mode 100644 index edb2dde..0000000 --- a/lib/src/rules/GameOfLife.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'dart:math'; - -import 'package:rules_of_living/src/rules/RuleSet.dart'; -import 'package:rules_of_living/src/rules/CellPattern.dart'; - -class GameOfLife implements RuleSet { - int range = 1; - List patterns = [ - // Two blocks, offset - // ## - // ## - CellPattern("Blinker", [ - Point(0, 0), - Point(1, 0), - Point(0, 1), - Point(1, 1), - Point(2, 2), - Point(3, 2), - Point(2, 3), - Point(3, 3) - ]), - // A 'gliding' Spaceship - // # - // # - // ### - CellPattern("SpaceShip", [ - Point(1, 0), - Point(2, 1), - Point(2, 2), - Point(1, 2), - Point(0, 2), - ]) - ]; - - bool checkSurvival(int neighbors) => - neighbors == 2 || neighbors == 3 ? true : false; - bool checkBirth(int neighbors) => neighbors == 3 ? true : false; -} diff --git a/lib/src/rules/RuleSet.dart b/lib/src/rules/RuleSet.dart deleted file mode 100644 index 03aeb36..0000000 --- a/lib/src/rules/RuleSet.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:rules_of_living/src/rules/CellPattern.dart'; - -abstract class RuleSet { - int range; - List patterns; - - bool checkSurvival(int neighbors); - bool checkBirth(int neighbors); -} diff --git a/pubspec.yaml b/pubspec.yaml index a63ea9a..94b94ee 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,6 +10,7 @@ environment: dependencies: angular: ^5.0.0-beta angular_components: ^0.9.0-beta + stagexl: ^1.4.0+1 dev_dependencies: angular_test: ^2.0.0-beta diff --git a/test/service/configuration_service_test.dart b/test/service/configuration_service_test.dart new file mode 100644 index 0000000..44e23b2 --- /dev/null +++ b/test/service/configuration_service_test.dart @@ -0,0 +1,46 @@ +import 'dart:math'; + +import 'package:mockito/mockito.dart'; +import 'package:rules_of_living/service/configuration_service.dart'; +import 'package:rules_of_living/service/engine_service.dart'; +import 'package:rules_of_living/src/Engine.dart'; +@TestOn('browser') +import 'package:test/test.dart'; + +class MockEngine extends Mock implements Engine {} + +void main() { + ConfigurationService sut; + EngineService engineService; + MockEngine me; + setUp(() { + me = MockEngine(); + engineService = EngineService(); + engineService.engine = me; + sut = ConfigurationService(engineService); + }); + + group("simulation speed", () { + test("speed changes propagate to engine", () { + sut.simSpeed = 312; + verify(me.stepsPerSecond = 312); + }); + }); + + group("grid size", () { + test("grid changes are sent to engine", () { + sut.setGridSize(x: 512, y: 388); + verify(me.gridSize = Point(512, 388)); + }); + test("grid can be changed solely on x axis", () { + when(me.gridSize).thenReturn(Point(100, 100)); + sut.setGridSize(x: 555); + verify(me.gridSize = Point(555, 100)); + }); + test("grid can be changed solely on y axis", () { + when(me.gridSize).thenReturn(Point(100, 100)); + sut.setGridSize(y: 556); + verify(me.gridSize = Point(100, 556)); + }); + }); +} diff --git a/test/service/simulation_service_test.dart b/test/service/simulation_service_test.dart deleted file mode 100644 index 3816099..0000000 --- a/test/service/simulation_service_test.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:rules_of_living/service/engine_service.dart'; -import 'package:rules_of_living/service/simulation_service.dart'; -import 'package:rules_of_living/src/Simulation.dart'; -import 'package:test/test.dart'; -import 'package:mockito/mockito.dart'; - -class MockSimulation extends Mock implements Simulation {} - -class MockEngineService extends Mock implements EngineService {} - -void main() { - SimulationService sut; - MockSimulation mockSim = MockSimulation(); - setUp(() => sut = SimulationService(MockEngineService(), mockSim)); - test("calling save calls through to Simulation.saveSnapshot", () { - sut.save(); - verify(mockSim.saveSnapshot()); - }); - test("calling load calls through to Simulation.loadSnapshot", () { - sut.load(); - verify(mockSim.loadSnapshot()); - }); -} diff --git a/test/simulation_test.dart b/test/simulation_test.dart deleted file mode 100644 index 7b8bf82..0000000 --- a/test/simulation_test.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'dart:math'; -import 'package:mockito/mockito.dart'; -import 'package:rules_of_living/src/Grid.dart'; -import 'package:test/test.dart'; - -import 'package:rules_of_living/src/Simulation.dart'; - -class MockGrid extends Mock implements Grid {} - -void main() { - Simulation sut; - setUp(() { - sut = Simulation(10, 10); - }); - group("gridSize", () { - test( - "returns the width and height of the underlying grid", - () => expect( - sut.gridSize, equals(Point(sut.map.width, sut.map.height)))); - test("sets the underlying grid width and height", () { - sut.gridSize = Point(2, 3); - expect(sut.gridSize, equals(Point(2, 3))); - }); - test("creates a new underlying grid on resizing", () { - var oldMap = sut.map; - sut.gridSize = Point(10, 10); - expect(sut.map, isNot(oldMap)); - }); - }); - group("reset", () { - test("returns a map filled with 'false' ", () { - expect(sut.reset(), allOf(TypeMatcher(), isNot(contains(true)))); - }); - test("sets the simulation to need re-rendering", () { - sut.reset(); - expect(sut.dirty, true); - }, skip: "can not find a way to set dirty to true first yet"); - }); - group("save&load", () { - test( - "saves a copy of the map which does not change when the actual map changes", - () { - sut.saveSnapshot(); - sut.mergeStateChanges({1: true, 2: true}); - var snapshot = Grid.from(sut.map); - - expect(sut.loadSnapshot(), isNot(equals(snapshot))); - }); - }); -} diff --git a/test/src/engine_test.dart b/test/src/engine_test.dart index f602069..fcfefc2 100644 --- a/test/src/engine_test.dart +++ b/test/src/engine_test.dart @@ -1,106 +1,15 @@ import 'dart:html' as html; -import 'package:rules_of_living/src/Simulation.dart'; -import 'package:test/test.dart'; -import 'package:mockito/mockito.dart'; +import 'dart:math'; @TestOn('browser') import 'package:rules_of_living/src/Engine.dart'; - -class MockSimulation extends Mock implements Simulation { - int updateNum = 0; - bool hasChanges = false; - - @override - Map update() { - updateNum++; - return hasChanges ? {1: true, 2: false} : {}; - } -} +import 'package:test/test.dart'; void main() { Engine sut; setUp(() { sut = Engine(); }); - group("process", () { - setUp(() => sut.running = true); - test("errors out if updating takes too long", - () => expect(() => sut.process(5000, 10), throwsA(StackOverflowError))); - test("does not update if not enough time elapsed to pass ms per step", () { - bool result = false; - sut.stepsPerSecond = 1000; - - sut.process(999, 2000, - update: () => result = true, render: (double interp) => null); - - expect(result, true); - }); - test("updates only when the ms per step threshold is crossed", () { - int updateNum = 0; - sut.stepsPerSecond = 1; - - sut.process(1001, 2000, update: () => updateNum++); - expect(updateNum, equals(1)); - }); - test("updates until updateLag has been resolved", () { - int updateNum = 0; - sut.stepsPerSecond = 1; - - sut.process(2999, 5000, update: () => updateNum++); - expect(updateNum, equals(2)); - }); - test("works without passing in update or render function arguments", () { - sut.stepsPerSecond = 1000; - expect(() => sut.process(500, 5000), isNot(throwsA(anything))); - }); - }); - group("update", () { - MockSimulation mockSim; - setUp(() { - mockSim = MockSimulation(); - sut.simulation = mockSim; - }); - test("does not error out if no simulation variable is set", () { - sut.simulation = null; - expect(() => sut.update(), isNot(throwsA(anything))); - }); - test("updates simulation one tick for every time it is called", () { - sut.update(); - sut.update(); - sut.update(); - expect(mockSim.updateNum, equals(3)); - }); - test("sets running to false when simulation returns no changes", () { - sut.running = true; - sut.update(); - expect(sut.running, equals(false)); - }); - test("keeps running when simulation returns changes", () { - sut.running = true; - mockSim.hasChanges = true; - - sut.update(); - - expect(sut.running, equals(true)); - }); - }); - group("step", () { - MockSimulation mockSim; - setUp(() { - mockSim = MockSimulation(); - sut.simulation = mockSim; - }); - test("advances the simulation by one update", () { - sut.step(); - expect(mockSim.updateNum, equals(1)); - }); - test("turns off continuous engine updates", () { - sut.running = true; - sut.step(); - - expect(sut.running, equals(false)); - }); - }); group("canvas", () { test("Engine can be instantiated without canvas", () { expect(sut, isNot(throwsNoSuchMethodError)); @@ -122,4 +31,12 @@ void main() { expect(sut.canvas, isNotNull); }); }); + group("gridSize", () { + test("zero gridSizes throw ArgumentErrors", () { + expect(() => sut.gridSize = Point(0, 5), throwsArgumentError); + }); + test("negative gridSizes throw ArgumentErrors", () { + expect(() => sut.gridSize = Point(1, -5), throwsArgumentError); + }); + }); } diff --git a/test/src/render_test.dart b/test/src/render_test.dart new file mode 100644 index 0000000..3c39387 --- /dev/null +++ b/test/src/render_test.dart @@ -0,0 +1,15 @@ +import 'dart:html' as html; +import 'dart:math'; + +@TestOn('browser') +import 'package:rules_of_living/src/Render.dart'; +import 'package:test/test.dart'; + +void main() { + Render sut; + setUp(() { + html.CanvasElement canvas = html.CanvasElement(); + sut = Render(canvas, 8); + }); + group("colorMap", () {}); +} diff --git a/test/src/rules/cellpattern_test.dart b/test/src/rules/cellpattern_test.dart deleted file mode 100644 index a7989f5..0000000 --- a/test/src/rules/cellpattern_test.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'dart:math'; - -import 'package:rules_of_living/src/rules/CellPattern.dart'; -import 'package:test/test.dart'; - -void main() { - CellPattern sut; - setUp(() { - sut = CellPattern("testPattern", [Point(1, 1), Point(0, 0), Point(-1, -1)]); - }); - group("Naming", () { - test("contains the name passed in for name variable", - () => expect(sut.name, "testPattern")); - test( - "Contains the name passed in on being formatted as String", - () => expect(sut.toString(), - "testPattern: [Point(1, 1), Point(0, 0), Point(-1, -1)]")); - }); -} diff --git a/test/src/rules/gameoflife_test.dart b/test/src/rules/gameoflife_test.dart deleted file mode 100644 index 565e4e1..0000000 --- a/test/src/rules/gameoflife_test.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:rules_of_living/src/rules/GameOfLife.dart'; -import 'package:test/test.dart'; - -void main() { - GameOfLife sut; - setUp(() { - sut = GameOfLife(); - }); - group("BirthRules", () { - test("will return true when being passed three neighbors", - () => expect(sut.checkBirth(3), true)); - test("will return false when being passed zero neighbors", - () => expect(sut.checkBirth(0), false)); - test("will return false when being passed two neighbors", - () => expect(sut.checkBirth(2), false)); - }); - group("SurviveRules", () { - test("will return true when being passed two neighbors", - () => expect(sut.checkSurvival(2), true)); - test("will return true when being passed three neighbors", - () => expect(sut.checkSurvival(3), true)); - test("will return false when being passed 0 neighbors", - () => expect(sut.checkSurvival(0), false)); - test("will return false when being passed more than 3 neighbors", - () => expect(sut.checkSurvival(4), false)); - }); -}