Compare commits

...

42 Commits

Author SHA1 Message Date
Marty d79aceeebf Merge branch '54-as-a-user-i-want-to-save-and-load-cell-configurations' into 'master'
Resolve "As a user, I want to save and load cell configurations."

Closes #54

See merge request marty.oehme/cellular-automata!17
2018-10-19 18:51:50 +00:00
Unknown 92e147028e Add Save and Load Buttons to Interface
Fully functional and tested.
2018-10-19 20:51:14 +02:00
Unknown 22dabda987 Add Save and Load Functions to SimService 2018-10-19 20:50:24 +02:00
Unknown 2ea974bbd5 Fix Snapshot Load and Save Functions
Were just pointers to the map before. Fix to be actual clones of the map.
2018-10-19 20:49:59 +02:00
Unknown 37bff59f83 Remove clear function: Code Duplication 2018-10-19 20:01:52 +02:00
Unknown 0da2d08b74 Add Simulation Saving and Loading Methods 2018-10-19 19:59:00 +02:00
Marty befb345ddd Merge branch '55-decouple-engine-and-simulation' into 'master'
Resolve "Decouple Engine and Simulation"

Closes #55

See merge request marty.oehme/cellular-automata!16
2018-10-19 17:37:55 +00:00
Unknown 2169de16fd Add Tests to Simulation 2018-10-19 19:35:09 +02:00
Unknown e8c1e6ed8b Refactor Engine Methods
Extract method checking for update necessity.
2018-10-19 11:50:34 +02:00
Unknown 2993b33d9e Add tests for CellPattern & GameOfLife classes 2018-10-19 10:24:40 +02:00
Marty 7e51c2d70a Merge branch '55-decouple-engine-and-simulation' into 'master'
Resolve "Decouple Engine and Simulation"

Closes #55

See merge request marty.oehme/cellular-automata!15
2018-10-18 13:23:58 +00:00
Unknown 8afb45e33e Merge remote-tracking branch 'origin/55-decouple-engine-and-simulation' into 55-decouple-engine-and-simulation 2018-10-18 15:23:10 +02:00
Unknown 8db9cd6ff1 Move Simulation Accesses to Simulation Class
Everything has been refactored away from engine, which now only controls updating & rendering within a specific timestep. (As well as stepping forward by calling a single update)

Everything regarding grids, patterns and cells has been moved into the simulation and the Services have been updated to reflect that.
2018-10-18 15:21:50 +02:00
Unknown 32a3676d95 Remove deprecated strict mode 2018-10-18 15:19:54 +02:00
Unknown bbfb2f735b Move Render Methods into SimulationService 2018-10-18 14:52:03 +02:00
Unknown 45e8f01acb Move Render Methods into SimulationService 2018-10-18 14:41:57 +02:00
Unknown 58971016da Remove ConfigurationService
Replaced with direct access to both EngineService and SimulationService.
2018-10-18 14:23:07 +02:00
Unknown 99ead8691b Make gridSize in Services pass correct signature
Both need to conform to Point<int> to be accepted by the engine.
2018-10-18 12:29:46 +02:00
Unknown 7729da3a40 Split ConfigurationService to use SimulationService
Methods concerning engine make use of EngineService, those concerning grid and patterns make use of SimulationService.
2018-10-18 12:16:47 +02:00
Unknown 6b4786fdd0 Add SimulationService to controls
Will eventually attach to the Simulation directly without first going through Engine. For now just redirects calls to EngineService to keep functions intact.
2018-10-18 12:05:02 +02:00
Unknown 72ce25a806 Rename Controls component variable accessing engine 2018-10-18 11:59:36 +02:00
Unknown 17697070ee Move ControlService tasks into EngineService
All ControlService was used for was a redirection to the engine service. This will be further split up in the future into more logical units of responsibility.
2018-10-18 11:58:38 +02:00
Marty b37487a222 Merge branch '53-encapsulate-ruleset-patterns-in-separate-data-structure' into 'master'
Resolve "Encapsulate Ruleset, Patterns in separate Data Structure"

Closes #53

See merge request marty.oehme/cellular-automata!14
2018-10-18 09:35:12 +00:00
Unknown 4f92c69a82 Rename Simulation function adding random patterns
Rename from addPattern to addRandomPattern to more clearly signify its purpose.
2018-10-18 11:32:00 +02:00
Unknown f1399064a2 Fix Simulation using RuleSet range for neighbor checks 2018-10-18 11:30:14 +02:00
Unknown e13962f371 Shorten RuleSet variable for their checked range
Range is self-explanatory and not as confusing as checkRange.
2018-10-18 11:29:55 +02:00
Unknown 0aa3df30b4 Extract CellPattern and GameOfLife into own files 2018-10-18 11:27:44 +02:00
Unknown fbdf114fed Move RuleSet to its own directory 2018-10-18 11:21:18 +02:00
Unknown 4f63947ab9 Delete unused Cell and Rule Classes
They were used under the old system of every
gridspace being its own cell data structure with its own rules to observe. 

Replaced by the RuleSet class. Cell has vanished in favor
of simple boolean values filling the grid.
2018-10-18 11:20:41 +02:00
Unknown c3244b085e Fix single steps not updating simulation 2018-10-18 11:04:23 +02:00
Unknown de1aa46743 Separate Simulation calculating updates and merging
Simulation updates were one step of calculation and merging the calculations into the map in one function.

Separating the two allows checking for a new update without affecting the grid, allows passing the last Update around and allows custom changes to the grid by passing changes to the merge function that were not derived from the update function.
2018-10-18 10:58:06 +02:00
Unknown e16085153a Rename Simulation in Engine Object 2018-10-18 09:59:26 +02:00
Unknown bac65ef116 Remove unnecessary pattern parameters 2018-10-18 09:57:08 +02:00
Unknown 6d7120650f Add Special Patterns to RuleSet 2018-10-17 21:08:55 +02:00
Unknown e6e82f78f2 Remove unnecessary Switch Case in Simulation 2018-10-17 21:07:48 +02:00
Unknown 9b2b5f3e55 Remove unnecessary Simulation variables 2018-10-17 21:00:14 +02:00
Unknown 27d4879b1b Extract RuleSet Class from Simulation 2018-10-17 20:58:04 +02:00
Marty 07f176be3e Merge branch '43-add-statechange-data-structure' into 'master'
Resolve "Add StateChange data structure"

Closes #43

See merge request marty.oehme/cellular-automata!13
2018-10-16 16:23:29 +00:00
Unknown 245d9a22c2 Remove Cell Data Structure
Cells are only boolean values of true or false for now.
2018-10-16 18:21:21 +02:00
Unknown 71f4df85af Refactor Simulation to be List of dumb cells
Cells are now only an empty struct, they carry no information beyond needing to be re-rendered. All Simulation logic is handled in the Simulation Class.
2018-10-15 17:28:09 +02:00
Unknown 08155b70a5 Fix Pausing after every Update
Updates would pause when any change has happened, not when no change has happened.
2018-10-15 17:16:09 +02:00
Unknown c50e92fb19 Add simple stateChanges map into Simulation 2018-10-02 14:55:39 +02:00
23 changed files with 507 additions and 376 deletions

View File

@ -1,6 +1,6 @@
analyzer:
exclude: [build/**]
strong-mode: true
errors:
uri_has_not_been_generated: ignore
plugins:

View File

@ -4,9 +4,8 @@ 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',
@ -24,8 +23,7 @@ import 'package:rules_of_living/service/engine_service.dart';
providers: [
materialProviders,
ClassProvider(EngineService),
ClassProvider(ConfigurationService),
ClassProvider(ControlService)
ClassProvider(SimulationService)
],
styleUrls: const [
'package:angular_components/app_layout/layout.scss.css',

View File

@ -1,3 +1,5 @@
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';
@ -5,7 +7,8 @@ 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/configuration_service.dart';
import 'package:rules_of_living/service/engine_service.dart';
import 'package:rules_of_living/service/simulation_service.dart';
@Component(
selector: "configuration",
@ -23,28 +26,29 @@ import 'package:rules_of_living/service/configuration_service.dart';
NgModel
])
class ConfigurationComponent {
final ConfigurationService config;
final EngineService engine;
final SimulationService sim;
int get width => config.gridSize.x;
int get width => sim.gridSize.x;
void set width(num value) {
if (value == null || value <= 0) return;
config.setGridSize(x: value.toInt());
sim.gridSize = Point(value, sim.gridSize.y);
}
int get height => config.gridSize.y;
int get height => sim.gridSize.y;
void set height(num value) {
if (value == null || value <= 0) return;
config.setGridSize(y: value.toInt());
sim.gridSize = Point(sim.gridSize.x, value);
}
int get simSpeed => config.simSpeed;
void set simSpeed(int value) => config.simSpeed = value;
int get simSpeed => engine.simSpeed;
void set simSpeed(int value) => engine.simSpeed = value;
String get speedSliderTooltip => "Simulation Speed: $simSpeed";
ConfigurationComponent(this.config);
ConfigurationComponent(this.engine, this.sim);
void onEdgesClicked() {
config.toggleGrid();
sim.toggleGrid();
}
}

View File

@ -1,6 +1,7 @@
import 'package:angular/angular.dart';
import 'package:angular_components/angular_components.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: 'sim-controls',
@ -15,27 +16,33 @@ import 'package:rules_of_living/service/control_service.dart';
styleUrls: const ["controls_component.css"],
)
class ControlsComponent {
final ControlService ctrl;
final EngineService engine;
final SimulationService sim;
ControlsComponent(this.ctrl);
ControlsComponent(this.engine, this.sim);
void onStartClicked() {
ctrl.toggleRunning();
engine.toggleRunning();
}
void onStepClicked() {
ctrl.step();
engine.step();
}
void onResetClicked() {
ctrl.reset();
void onSaveClicked() {
sim.save();
}
void onLoadClicked() {
sim.load();
}
void onRandomClicked() {
ctrl.addRandomPattern();
sim.addRandomPattern();
engine.stop();
}
void onClearClicked() {
ctrl.clear();
sim.reset();
}
}

View File

@ -1,7 +1,8 @@
<div id="controls">
<material-button id="reset" (click)="onResetClicked()"><material-icon icon="replay" baseline></material-icon></material-button>
<material-button id="save" (click)="onSaveClicked()"><material-icon icon="save" baseline></material-icon></material-button>
<material-button id="load" (click)="onLoadClicked()"><material-icon icon="history" baseline></material-icon></material-button>
<material-button id="run" (click)="onStartClicked()">
<span [ngSwitch]="ctrl.isRunning">
<span [ngSwitch]="engine.isRunning">
<material-icon *ngSwitchCase="false" icon="play_arrow" baseline></material-icon>
<material-icon *ngSwitchCase="true" icon="pause" baseline></material-icon>
</span>

View File

@ -1,7 +1,8 @@
import 'dart:html' as html;
import 'package:angular/angular.dart';
import 'package:rules_of_living/service/configuration_service.dart';
import 'package:rules_of_living/service/engine_service.dart';
import 'package:rules_of_living/service/simulation_service.dart';
@Component(
selector: 'gol-simulation',
@ -10,9 +11,10 @@ import 'package:rules_of_living/service/configuration_service.dart';
providers: [],
)
class SimulationComponent implements OnInit {
final ConfigurationService config;
final EngineService engine;
final SimulationService sim;
SimulationComponent(this.config);
SimulationComponent(this.engine, this.sim);
@override
void ngOnInit() {
@ -29,6 +31,6 @@ class SimulationComponent implements OnInit {
the canvas did not load correctly :(
''', canvas.width / 2 - 50, canvas.height / 2);
config.canvas = canvas;
sim.canvas = canvas;
}
}

View File

@ -1,43 +0,0 @@
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<int> get gridSize => _es.engine.gridSize;
}

View File

@ -1,38 +0,0 @@
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;
}

View File

@ -1,8 +1,13 @@
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;
@ -12,4 +17,32 @@ 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;
}

View File

@ -0,0 +1,43 @@
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<int> get gridSize => _sim.gridSize;
void set gridSize(Point<int> 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();
}

View File

@ -1,36 +0,0 @@
import 'package:rules_of_living/src/Rule.dart';
class Cell {
bool state;
bool nextState = false;
List<Rule> surviveRules = new List<Rule>();
List<Rule> birthRules = new List<Rule>();
/// 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;
});
}
}
}

View File

@ -1,5 +1,4 @@
import 'dart:html' as html;
import 'dart:math';
import 'package:rules_of_living/src/Simulation.dart';
@ -29,16 +28,6 @@ class Engine {
// ms stuck in updateloop after which game will declare itself unresponsive
final int SAFETY_TIMEOUT = 2000;
/// Grid Size
///
/// Number of cells on x coordinate and y coordinate. Can be set individually.
Point get gridSize => Point<int>(_grid.w, _grid.h);
void set gridSize(Point<int> value) {
if (value.x <= 0 || value.y <= 0)
throw ArgumentError("grid size must not be smaller than 1");
_grid = Simulation(value.x, value.y);
}
num _updateLag = 0.0;
num _drawLag = 0.0;
@ -48,60 +37,58 @@ class Engine {
/// be used if no canvas was defined at engine creation and it should be
/// rendered later.
html.CanvasElement canvas;
Simulation _grid;
Simulation _simulation;
bool running = false;
Engine([x = 100, y = 100, this.canvas]) {
_grid = Simulation(x, y);
Engine() {
_elapsed.start();
_grid.addPattern(amount: 15, dispersal: 5);
html.window.animationFrame.then(animFrame);
}
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 reset() {
_grid.reset();
running = false;
}
void process(int elapsed, int timeOut, {Function update, Function render}) {
_drawLag += elapsed;
_updateLag += elapsed;
void clear() {
_grid = 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();
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
/// 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 (!_grid.update()) running = false;
if (_simulation == null) return;
Map<int, bool> simulationUpdate = _simulation.update();
_simulation.mergeStateChanges(simulationUpdate);
if (simulationUpdate.length == 0) running = false;
}
/// Advances Logic One Update
@ -110,8 +97,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;
_grid.update();
}
/// Renders the Current Simulation State
@ -120,26 +107,10 @@ class Engine {
/// the internal engine processing. Does not do anything if no canvas is
/// defined.
void render([num interp]) {
if (canvas != null) _grid.render(canvas, interp);
if (canvas == null || _simulation == null) return;
_simulation.render(canvas, interp);
}
void addPattern(
{CellPattern pattern,
int x,
int y,
int amount,
int dispersal,
int seed}) {
_grid.addPattern(
pattern: pattern,
x: x,
y: y,
amount: amount,
dispersal: dispersal,
seed: seed);
}
void toggleEdgeRendering() {
_grid.renderEdges = !_grid.renderEdges;
}
void set simulation(Simulation value) => _simulation = value;
}

View File

@ -1,5 +0,0 @@
class Rule {
final Function evaluate;
Rule(this.evaluate);
}

View File

@ -1,147 +1,116 @@
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/Rule.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 {
final Grid<Cell> map;
Grid<bool> map;
Grid<bool> _snapshot;
RuleSet rules = GameOfLife();
bool _dirty = true;
bool _renderEdges = true;
bool get dirty => _dirty;
bool _renderEdges = true;
bool get renderEdges => _renderEdges;
int _startingSeed;
int _x;
int _y;
int _amount;
int _dispersal;
CellPattern _pattern;
int get w => map.width;
int get h => map.height;
Point get gridSize => Point<int>(map.width, map.height);
void set gridSize(Point<int> 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) {
for (int i = 0; i < map.length; i++) {
map[i] = _getGOLCell();
}
print("Grid creation finished");
this.map = reset();
}
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;
}
Simulation.fromGrid(Grid<bool> map) : this.map = 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);
Grid<bool> reset([Grid<bool> map]) {
map ??= this.map;
_dirty = true;
map.setAll(0, List.filled(map.length, false));
return map;
}
void addPattern(
{CellPattern pattern,
int x,
int y,
int amount,
int dispersal,
int seed}) {
_startingSeed = seed ?? DateTime.now().millisecondsSinceEpoch;
void addRandomPattern({int amount, int dispersal}) {
int _startingSeed = DateTime.now().millisecondsSinceEpoch;
math.Random rng = new math.Random(_startingSeed);
_x = x;
_y = y;
_amount = amount ?? rng.nextInt(20);
_dispersal = dispersal ?? 10;
_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 cx = rng.nextInt(map.width ~/ 3) + (map.width ~/ 3);
int cy = rng.nextInt(map.height ~/ 3) + (map.height ~/ 3);
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;
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) map.get(x, y).state = 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 map.get(x, y).state;
return null;
if (y >= map.height || x >= map.width) return null;
return map.get(x, y);
}
bool update() {
bool stateChanges = false;
void toggleCellState(int x, int y) {
if (y >= map.height || x >= map.width) return null;
for (int i = 0; i < map.length; i++) {
math.Point p = map.toCoordinates(i);
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
map.forEach((Cell el) {
if (el.state != el.nextState) stateChanges = true;
el.advanceState();
});
stateChanges ? _dirty = true : false;
getCellState(x, y) == null
? setCellState(x, y, true)
: setCellState(x, y, false);
}
Map<int, bool> update() {
Map<int, bool> stateChanges = calculateNextState(map);
return stateChanges;
}
int getSurroundingNeighbors(int x, int y, int range) {
void mergeStateChanges(Map<int, bool> stateChanges) {
stateChanges.forEach((i, el) => map[i] = el);
if (stateChanges.length != 0) _dirty = true;
}
Map<int, bool> calculateNextState(Grid<bool> oldState) {
Map<int, bool> 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;
}
}
return stateChanges;
}
int getNeighbors(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++) {
@ -149,7 +118,7 @@ class Simulation {
iy >= 0 &&
ix < map.width &&
iy < map.height &&
map.get(ix, iy).state == true &&
getCellState(ix, iy) == true &&
!(x == ix && y == iy)) count++;
}
}
@ -171,7 +140,7 @@ class Simulation {
ctx.strokeRect(p.x * brickW, p.y * brickH, brickW, brickH);
}
if (map[i].state == true)
if (map[i] == true)
ctx.setFillColorRgb(155, 155, 255);
else
ctx.setFillColorRgb(0, 0, 0);
@ -185,5 +154,10 @@ class Simulation {
_dirty = true;
}
bool get renderEdges => _renderEdges;
void saveSnapshot() => _snapshot = Grid.from(map);
Grid<bool> loadSnapshot() {
map = Grid.from(_snapshot);
_dirty = true;
return map;
}
}

View File

@ -0,0 +1,17 @@
import 'dart:math';
import 'package:collection/collection.dart';
class CellPattern extends DelegatingList<Point> {
final String _name;
CellPattern(String name, List<Point> base)
: _name = name,
super(base);
String get name => _name;
@override
String toString() {
return "$name: ${super.toString()}";
}
}

View File

@ -0,0 +1,38 @@
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<CellPattern> patterns = <CellPattern>[
// 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;
}

View File

@ -0,0 +1,9 @@
import 'package:rules_of_living/src/rules/CellPattern.dart';
abstract class RuleSet {
int range;
List<CellPattern> patterns;
bool checkSurvival(int neighbors);
bool checkBirth(int neighbors);
}

View File

@ -1,46 +0,0 @@
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));
});
});
}

View File

@ -0,0 +1,23 @@
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());
});
}

50
test/simulation_test.dart Normal file
View File

@ -0,0 +1,50 @@
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<bool> {}
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");
});
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)));
});
});
}

View File

@ -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<int, bool> 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));
@ -31,12 +122,4 @@ 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);
});
});
}

View 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)]"));
});
}

View 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));
});
}