Merge branch '55-decouple-engine-and-simulation' into 'master'
Resolve "Decouple Engine and Simulation" Closes #55 See merge request marty.oehme/cellular-automata!16
This commit is contained in:
commit
befb345ddd
7 changed files with 215 additions and 28 deletions
|
@ -1,5 +1,4 @@
|
||||||
import 'dart:html' as html;
|
import 'dart:html' as html;
|
||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:rules_of_living/src/Simulation.dart';
|
import 'package:rules_of_living/src/Simulation.dart';
|
||||||
|
|
||||||
|
@ -47,31 +46,37 @@ class Engine {
|
||||||
}
|
}
|
||||||
|
|
||||||
void animFrame(num now) {
|
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);
|
html.window.animationFrame.then(animFrame);
|
||||||
}
|
}
|
||||||
|
|
||||||
void process(num now) {
|
void process(int elapsed, int timeOut, {Function update, Function render}) {
|
||||||
_drawLag += _elapsed.elapsedMilliseconds;
|
_drawLag += elapsed;
|
||||||
_updateLag += _elapsed.elapsedMilliseconds;
|
_updateLag += elapsed;
|
||||||
_elapsed.reset();
|
|
||||||
|
|
||||||
while (_updateLag >= _MS_PER_STEP) {
|
while (running == true &&
|
||||||
if (_elapsed.elapsedMilliseconds > SAFETY_TIMEOUT) {
|
_shouldUpdate(_updateLag, elapsed, timeOut) == true) {
|
||||||
// TODO stub - give warning etc when this occurs
|
|
||||||
print("ERROR STUCK IN UPDATE LOOP");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (running == true) update();
|
|
||||||
_updateLag -= _MS_PER_STEP;
|
_updateLag -= _MS_PER_STEP;
|
||||||
|
if (update == null) break;
|
||||||
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_drawLag >= _MS_PER_FRAME) {
|
if (_drawLag >= _MS_PER_FRAME) {
|
||||||
render(_updateLag / _MS_PER_STEP);
|
|
||||||
_drawLag = 0;
|
_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
|
/// Update Engine Logic
|
||||||
///
|
///
|
||||||
/// Updates the logic of the engine by one tick. Should usually not be called
|
/// Updates the logic of the engine by one tick. Should usually not be called
|
||||||
|
|
|
@ -13,15 +13,15 @@ class Simulation {
|
||||||
RuleSet rules = GameOfLife();
|
RuleSet rules = GameOfLife();
|
||||||
|
|
||||||
bool _dirty = true;
|
bool _dirty = true;
|
||||||
|
bool get dirty => _dirty;
|
||||||
|
|
||||||
bool _renderEdges = true;
|
bool _renderEdges = true;
|
||||||
|
bool get renderEdges => _renderEdges;
|
||||||
|
|
||||||
int _amount;
|
int _amount;
|
||||||
int _dispersal;
|
int _dispersal;
|
||||||
|
|
||||||
int get w => map.width;
|
Point get gridSize => Point<int>(map.width, map.height);
|
||||||
int get h => map.height;
|
|
||||||
|
|
||||||
Point get gridSize => Point<int>(w, h);
|
|
||||||
void set gridSize(Point<int> value) {
|
void set gridSize(Point<int> value) {
|
||||||
if (value.x <= 0 || value.y <= 0)
|
if (value.x <= 0 || value.y <= 0)
|
||||||
throw ArgumentError("grid size must not be smaller than 1");
|
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) {
|
Simulation(int w, int h) : this.map = new Grid(w, h) {
|
||||||
reset();
|
this.map = reset();
|
||||||
print("Grid Created");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void reset() {
|
Simulation.fromGrid(Grid<bool> map) : this.map = map;
|
||||||
map.setAll(0, List.filled(map.length, false));
|
|
||||||
|
Grid<bool> reset([Grid<bool> map]) {
|
||||||
|
map ??= this.map;
|
||||||
_dirty = true;
|
_dirty = true;
|
||||||
|
map.setAll(0, List.filled(map.length, false));
|
||||||
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
void addRandomPattern({int amount, int dispersal}) {
|
void addRandomPattern({int amount, int dispersal}) {
|
||||||
|
@ -148,6 +151,4 @@ class Simulation {
|
||||||
_renderEdges = on;
|
_renderEdges = on;
|
||||||
_dirty = true;
|
_dirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get renderEdges => _renderEdges;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,17 @@
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
class CellPattern<Point> extends DelegatingList<Point> {
|
class CellPattern extends DelegatingList<Point> {
|
||||||
final String _name;
|
final String _name;
|
||||||
CellPattern(String name, List base)
|
CellPattern(String name, List<Point> base)
|
||||||
: _name = name,
|
: _name = name,
|
||||||
super(base);
|
super(base);
|
||||||
|
|
||||||
String get name => _name;
|
String get name => _name;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return "$name: ${super.toString()}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
37
test/simulation_test.dart
Normal file
37
test/simulation_test.dart
Normal file
|
@ -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<int>(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<Grid>(), 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");
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,15 +1,106 @@
|
||||||
import 'dart:html' as html;
|
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')
|
@TestOn('browser')
|
||||||
import 'package:rules_of_living/src/Engine.dart';
|
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<int, bool> update() {
|
||||||
|
updateNum++;
|
||||||
|
return hasChanges ? {1: true, 2: false} : {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
Engine sut;
|
Engine sut;
|
||||||
setUp(() {
|
setUp(() {
|
||||||
sut = Engine();
|
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", () {
|
group("canvas", () {
|
||||||
test("Engine can be instantiated without canvas", () {
|
test("Engine can be instantiated without canvas", () {
|
||||||
expect(sut, isNot(throwsNoSuchMethodError));
|
expect(sut, isNot(throwsNoSuchMethodError));
|
||||||
|
|
19
test/src/rules/cellpattern_test.dart
Normal file
19
test/src/rules/cellpattern_test.dart
Normal file
|
@ -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)]"));
|
||||||
|
});
|
||||||
|
}
|
27
test/src/rules/gameoflife_test.dart
Normal file
27
test/src/rules/gameoflife_test.dart
Normal file
|
@ -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));
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in a new issue