diff --git a/lib/src/Engine.dart b/lib/src/Engine.dart index ad101ca..5653bb9 100644 --- a/lib/src/Engine.dart +++ b/lib/src/Engine.dart @@ -1,5 +1,4 @@ import 'dart:html' as html; -import 'dart:math'; import 'package:rules_of_living/src/Simulation.dart'; @@ -47,31 +46,37 @@ class Engine { } void animFrame(num now) { - process(now); + int elapsed = _elapsed.elapsedMilliseconds; + _elapsed.reset(); + process(elapsed, SAFETY_TIMEOUT, update: this.update, render: this.render); html.window.animationFrame.then(animFrame); } - void process(num now) { - _drawLag += _elapsed.elapsedMilliseconds; - _updateLag += _elapsed.elapsedMilliseconds; - _elapsed.reset(); + void process(int elapsed, int timeOut, {Function update, Function render}) { + _drawLag += elapsed; + _updateLag += elapsed; - 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(); + while (running == true && + _shouldUpdate(_updateLag, elapsed, timeOut) == true) { _updateLag -= _MS_PER_STEP; + if (update == null) break; + update(); } if (_drawLag >= _MS_PER_FRAME) { - render(_updateLag / _MS_PER_STEP); _drawLag = 0; + if (render == null) return; + render(_updateLag / _MS_PER_STEP); } } + 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 diff --git a/lib/src/Simulation.dart b/lib/src/Simulation.dart index fd8bbcb..54c7efa 100644 --- a/lib/src/Simulation.dart +++ b/lib/src/Simulation.dart @@ -13,15 +13,15 @@ class Simulation { RuleSet rules = GameOfLife(); bool _dirty = true; + bool get dirty => _dirty; + bool _renderEdges = true; + bool get renderEdges => _renderEdges; int _amount; int _dispersal; - int get w => map.width; - int get h => map.height; - - Point get gridSize => Point(w, h); + 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"); @@ -29,13 +29,16 @@ class Simulation { } Simulation(int w, int h) : this.map = new Grid(w, h) { - reset(); - print("Grid Created"); + this.map = reset(); } - void reset() { - map.setAll(0, List.filled(map.length, false)); + Simulation.fromGrid(Grid map) : this.map = map; + + Grid reset([Grid map]) { + map ??= this.map; _dirty = true; + map.setAll(0, List.filled(map.length, false)); + return map; } void addRandomPattern({int amount, int dispersal}) { @@ -148,6 +151,4 @@ class Simulation { _renderEdges = on; _dirty = true; } - - bool get renderEdges => _renderEdges; } diff --git a/lib/src/rules/CellPattern.dart b/lib/src/rules/CellPattern.dart index 9798948..ed4cae7 100644 --- a/lib/src/rules/CellPattern.dart +++ b/lib/src/rules/CellPattern.dart @@ -1,10 +1,17 @@ +import 'dart:math'; + import 'package:collection/collection.dart'; -class CellPattern extends DelegatingList { +class CellPattern extends DelegatingList { final String _name; - CellPattern(String name, List base) + CellPattern(String name, List base) : _name = name, super(base); String get name => _name; + + @override + String toString() { + return "$name: ${super.toString()}"; + } } diff --git a/test/simulation_test.dart b/test/simulation_test.dart new file mode 100644 index 0000000..b8f5db6 --- /dev/null +++ b/test/simulation_test.dart @@ -0,0 +1,37 @@ +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'; + +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"); + }); +} diff --git a/test/src/engine_test.dart b/test/src/engine_test.dart index bd184a4..f602069 100644 --- a/test/src/engine_test.dart +++ b/test/src/engine_test.dart @@ -1,15 +1,106 @@ import 'dart:html' as html; -import 'dart:math'; +import 'package:rules_of_living/src/Simulation.dart'; +import 'package:test/test.dart'; +import 'package:mockito/mockito.dart'; @TestOn('browser') import 'package:rules_of_living/src/Engine.dart'; -import 'package:test/test.dart'; + +class MockSimulation extends Mock implements Simulation { + int updateNum = 0; + bool hasChanges = false; + + @override + Map update() { + updateNum++; + return hasChanges ? {1: true, 2: false} : {}; + } +} 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)); diff --git a/test/src/rules/cellpattern_test.dart b/test/src/rules/cellpattern_test.dart new file mode 100644 index 0000000..a7989f5 --- /dev/null +++ b/test/src/rules/cellpattern_test.dart @@ -0,0 +1,19 @@ +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 new file mode 100644 index 0000000..565e4e1 --- /dev/null +++ b/test/src/rules/gameoflife_test.dart @@ -0,0 +1,27 @@ +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)); + }); +}