diff --git a/lib/service/simulation_service.dart b/lib/service/simulation_service.dart index 8e0c14f..fb2804e 100644 --- a/lib/service/simulation_service.dart +++ b/lib/service/simulation_service.dart @@ -14,11 +14,11 @@ class SimulationService { SimulationService(this._engine, [Simulation sim]) : this._sim = sim ?? Simulation(DEFAULT_GRID_SIZE, DEFAULT_GRID_SIZE) { _engine.simulation = _sim; - _sim.addRandomPattern(amount: 15, dispersal: 5); + _sim.addRandomPattern(amount: 15); } void reset() { - _sim.reset(); + _sim.clearMap(); } void addRandomPattern() { diff --git a/lib/src/Simulation.dart b/lib/src/Simulation.dart index 0955c9a..8341944 100644 --- a/lib/src/Simulation.dart +++ b/lib/src/Simulation.dart @@ -1,87 +1,45 @@ import 'dart:html' as html; import 'dart:math' as math; -import 'dart:math'; 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'; -enum CellPattern { SpaceShip, Blinker } - class Simulation { Grid map; Grid _snapshot; + final int _RANDOM_PATTERN_AMOUNT = 20; + final double _RANDOM_PATTERN_SPREAD_FROM_CENTER = 1 / 3; + RuleSet rules = GameOfLife(); - bool _dirty = true; - bool get dirty => _dirty; + bool dirty = true; bool _renderEdges = true; bool get renderEdges => _renderEdges; - int _amount; - int _dispersal; - - Point get gridSize => Point(map.width, map.height); - void set gridSize(Point value) { + math.Point get gridSize => math.Point(map.width, map.height); + void set gridSize(math.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); } Simulation(int w, int h) : this.map = new Grid(w, h) { - this.map = reset(); + this.map = clearMap(); } Simulation.fromGrid(Grid map) : this.map = map; - Grid reset([Grid map]) { - map ??= this.map; - _dirty = true; + Grid clearMap() { + dirty = true; map.setAll(0, List.filled(map.length, false)); return map; } - void addRandomPattern({int amount, int dispersal}) { - int _startingSeed = DateTime.now().millisecondsSinceEpoch; - math.Random rng = new math.Random(_startingSeed); - _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); - - 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; - } - - _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); - } - - 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); + void toggleCell(int x, int y) { + map.set(x, y, !map.get(x, y)); } Map update() { @@ -91,7 +49,7 @@ class Simulation { void mergeStateChanges(Map stateChanges) { stateChanges.forEach((i, el) => map[i] = el); - if (stateChanges.length != 0) _dirty = true; + if (stateChanges.length != 0) dirty = true; } Map calculateNextState(Grid oldState) { @@ -118,16 +76,50 @@ class Simulation { iy >= 0 && ix < map.width && iy < map.height && - getCellState(ix, iy) == true && + map.get(ix, iy) == true && !(x == ix && y == iy)) count++; } } return count; } + void addRandomPattern({int seed, int amount, num spreadFromCenter}) { + math.Random rng = _getRNG(seed ?? DateTime.now().millisecondsSinceEpoch); + amount ??= rng.nextInt(_RANDOM_PATTERN_AMOUNT); + spreadFromCenter ??= _RANDOM_PATTERN_SPREAD_FROM_CENTER; + + int sanityCheck = 0; + Map changeSet = {}; + for (var i = 0; i < (amount); i++) { + sanityCheck++; + math.Point cell = _getRandomPoint(rng, spreadFromCenter); + map.get(cell.x, cell.y) + ? i-- + : changeSet[map.toIndex(cell.x, cell.y)] = true; + if (sanityCheck > 100 && sanityCheck > i * 3) break; + } + mergeStateChanges(changeSet); + } + + math.Random _getRNG(int seed) { + math.Random rng = new math.Random(seed); + return rng; + } + + math.Point _getRandomPoint(math.Random rng, num spreadFromCenter) { + math.Point absoluteSpread = + math.Point(map.width * spreadFromCenter, map.height * spreadFromCenter); + math.Point center = math.Point(map.width / 2, map.height / 2); + num cx = rng.nextInt(absoluteSpread.x.toInt()) + + (center.x - absoluteSpread.x / 2); + num cy = rng.nextInt(absoluteSpread.y.toInt()) + + (center.y - absoluteSpread.y / 2); + return math.Point(cx.toInt(), cy.toInt()); + } + void render(html.CanvasElement canvas, [num interp]) { // only renders if any cells changed between renders - if (!_dirty) return; + if (!dirty) return; html.CanvasRenderingContext2D ctx = canvas.getContext('2d'); int brickW = (canvas.width ~/ map.width); @@ -146,18 +138,18 @@ class Simulation { ctx.setFillColorRgb(0, 0, 0); ctx.fillRect(p.x * brickW, p.y * brickH, brickW, brickH); } - _dirty = false; + dirty = false; } void set renderEdges(bool on) { _renderEdges = on; - _dirty = true; + dirty = true; } void saveSnapshot() => _snapshot = Grid.from(map); Grid loadSnapshot() { map = Grid.from(_snapshot); - _dirty = true; + dirty = true; return map; } } diff --git a/test/simulation_test.dart b/test/simulation_test.dart index 7b8bf82..4c87191 100644 --- a/test/simulation_test.dart +++ b/test/simulation_test.dart @@ -1,9 +1,9 @@ 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'; +import 'package:test/test.dart'; class MockGrid extends Mock implements Grid {} @@ -24,18 +24,21 @@ void main() { test("creates a new underlying grid on resizing", () { var oldMap = sut.map; sut.gridSize = Point(10, 10); - expect(sut.map, isNot(oldMap)); - }); + expect(sut.map, isNot(same(oldMap))); + }, tags: "nobrowser"); }); - group("reset", () { - test("returns a map filled with 'false' ", () { - expect(sut.reset(), allOf(TypeMatcher(), isNot(contains(true)))); + group("resetMap", () { + test("sets the internal map filled with 'false' ", () { + sut.map.set(1, 1, true); + sut.clearMap(); + expect(sut.map, allOf(TypeMatcher(), isNot(contains(true)))); }); test("sets the simulation to need re-rendering", () { - sut.reset(); + sut.dirty = false; + sut.clearMap(); expect(sut.dirty, true); - }, skip: "can not find a way to set dirty to true first yet"); - }); + }); + }, tags: "nobrowser"); group("save&load", () { test( "saves a copy of the map which does not change when the actual map changes", @@ -46,5 +49,20 @@ void main() { expect(sut.loadSnapshot(), isNot(equals(snapshot))); }); + }, tags: "nobrowser"); + group("toggleCellState", () { + test("throws RangeError if outside the map bounds", () { + expect(() => sut.toggleCell(10, 9), throwsRangeError); + }, tags: const ["nobrowser"]); + test("sets the cell to false if currently true", () { + sut.map.set(1, 1, true); + sut.toggleCell(1, 1); + expect(sut.map.get(1, 1), false); + }, tags: const ["nobrowser"]); + test("sets the cell to true if currently false", () { + sut.map.set(1, 1, false); + sut.toggleCell(1, 1); + expect(sut.map.get(1, 1), true); + }, tags: const ["nobrowser"]); }); }