Merge branch '31-allow-engine-to-be-created-without-the-need-for-a-canvas' into 'master'

Resolve "Allow Engine to be created without the need for a canvas"

Closes #31

See merge request marty.oehme/cellular-automata!8
This commit is contained in:
Marty 2018-08-25 13:02:45 +00:00
commit 7d5b1cfe30
8 changed files with 137 additions and 34 deletions

View file

@ -1,8 +1,6 @@
import 'package:angular/angular.dart'; import 'package:angular/angular.dart';
import 'package:angular_components/angular_components.dart'; import 'package:angular_components/angular_components.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/engine_service.dart';
import 'package:rules_of_living/src/Engine.dart';
@Component( @Component(
selector: 'sim-controls', selector: 'sim-controls',
@ -17,13 +15,12 @@ import 'package:rules_of_living/src/Engine.dart';
styleUrls: const ["controls_component.css"], styleUrls: const ["controls_component.css"],
) )
class ControlsComponent { class ControlsComponent {
final EngineService engineService; final EngineService engine;
Engine get engine => engineService.engine; ControlsComponent(this.engine);
ControlsComponent(this.engineService);
void onStartClicked() { void onStartClicked() {
engine.running = !engine.running; engine.toggleRunning();
} }
void onStepClicked() { void onStepClicked() {
@ -35,8 +32,7 @@ class ControlsComponent {
} }
void onRandomClicked() { void onRandomClicked() {
engine.running = false; engine.addRandomPattern();
engine.addPattern();
} }
void onClearClicked() { void onClearClicked() {

View file

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

View file

@ -8,7 +8,6 @@ import 'package:rules_of_living/service/engine_service.dart';
templateUrl: "simulation_component.html", templateUrl: "simulation_component.html",
directives: [coreDirectives], directives: [coreDirectives],
providers: [], providers: [],
// styleUrls: const ['package:angular_components/app_layout/layout.scss.css'],
) )
class SimulationComponent implements OnInit { class SimulationComponent implements OnInit {
final EngineService engineService; final EngineService engineService;
@ -26,16 +25,10 @@ class SimulationComponent implements OnInit {
canvas.context2D.fillRect(0, 0, canvas.width, canvas.height); canvas.context2D.fillRect(0, 0, canvas.width, canvas.height);
canvas.context2D.setFillColorRgb(0, 255, 0); canvas.context2D.setFillColorRgb(0, 255, 0);
canvas.context2D.fillText(''' canvas.context2D.fillText('''
If you see this\n If you see this
the app is broken :(
the canvas did not load correctly :(
''', canvas.width / 2 - 50, canvas.height / 2); ''', canvas.width / 2 - 50, canvas.height / 2);
engineService.create(canvas); engineService.canvas = canvas;
html.window.animationFrame.then(animFrame);
}
void animFrame(num now) {
engineService.engine.process(now);
html.window.animationFrame.then(animFrame);
} }
} }

View file

@ -5,7 +5,45 @@ import 'package:rules_of_living/src/Engine.dart';
class EngineService { class EngineService {
Engine _engine; Engine _engine;
Engine get engine => _engine; Engine get engine => _engine ?? createEngine(null);
Engine createEngine(html.CanvasElement canvas) {
_engine = Engine(canvas);
return _engine;
}
void set canvas(html.CanvasElement canvas) => engine.canvas = canvas;
html.CanvasElement get canvas => engine.canvas;
void run() {
engine.running = true;
}
void stop() {
engine.running = false;
}
void toggleRunning() {
engine.running = !engine.running;
}
void step() {
engine.step();
}
void reset() {
engine.reset();
}
void addRandomPattern() {
engine.running = false;
engine.addPattern();
}
void clear() {
engine.clear();
}
bool get isRunning => engine.running;
void create(html.CanvasElement canvas) => _engine = Engine(canvas);
} }

View file

@ -6,7 +6,7 @@ class Engine {
// Elapsed Time Counter - useful for Safety Timeout // Elapsed Time Counter - useful for Safety Timeout
Stopwatch _elapsed = new Stopwatch(); Stopwatch _elapsed = new Stopwatch();
// Game Tick Rate - *does* impact game speed // Game Tick Rate - *does* impact game speed TODO add configurable option
int _MS_PER_STEP = 1000 ~/ 3; int _MS_PER_STEP = 1000 ~/ 3;
// Max Frame (i.e. Rendering) rate - does *not* impact game speed // Max Frame (i.e. Rendering) rate - does *not* impact game speed
@ -15,16 +15,31 @@ class Engine {
// ms stuck in updateloop after which game will declare itself unresponsive // ms stuck in updateloop after which game will declare itself unresponsive
final int SAFETY_TIMEOUT = 2000; final int SAFETY_TIMEOUT = 2000;
// Grid Size TODO add as configurable option
static final GRID_X = 100;
static final GRID_Y = 100;
num _updateLag = 0.0; num _updateLag = 0.0;
num _drawLag = 0.0; num _drawLag = 0.0;
final html.CanvasElement canvas; /// The Canvas to Paint on
Grid _grid = new Grid(100, 100); ///
bool _running = false; /// 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;
Grid _grid = new Grid(GRID_X, GRID_Y);
bool running = false;
Engine(this.canvas) { Engine([this.canvas]) {
_elapsed.start(); _elapsed.start();
_grid.addPattern(amount: 15, dispersal: 5); _grid.addPattern(amount: 15, dispersal: 5);
html.window.animationFrame.then(animFrame);
}
void animFrame(num now) {
process(now);
html.window.animationFrame.then(animFrame);
} }
void reset() { void reset() {
@ -33,7 +48,7 @@ class Engine {
} }
void clear() { void clear() {
_grid = new Grid(100, 100); _grid = new Grid(GRID_X, GRID_Y);
running = false; running = false;
} }
@ -58,19 +73,32 @@ class Engine {
} }
} }
/// 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() { void update() {
// print("updating");
if (!_grid.update()) running = false; if (!_grid.update()) running = false;
} }
/// Advances Logic One Update
///
/// Moves logic of the engine one step forward and pauses the
/// simulation. Does not automatically re-render the new state
/// (though this should usually not pose a problem).
void step() { void step() {
running = false; running = false;
_grid.update(); _grid.update();
} }
/// Renders the Current Simulation State
///
/// 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]) { void render([num interp]) {
// print("rendering"); if(canvas != null) _grid.render(canvas, interp);
_grid.render(canvas, interp);
} }
void addPattern( void addPattern(
@ -92,7 +120,4 @@ class Engine {
void toggleEdgeRendering() { void toggleEdgeRendering() {
_grid.renderEdges = !_grid.renderEdges; _grid.renderEdges = !_grid.renderEdges;
} }
void set running(bool on) => _running = on;
bool get running => _running;
} }

View file

@ -0,0 +1,19 @@
@TestOn('browser')
import 'package:test/test.dart';
import 'package:rules_of_living/service/engine_service.dart';
void main() {
EngineService sut;
setUp(() {
sut = EngineService();
});
test("EngineService creates an engine on demand", () {
expect(sut.engine, isNotNull);
});
test("EngineService returns the cached engine on subsequent requests", () {
expect(sut.engine, allOf(isNotNull, equals(sut.engine)));
});
}

32
test/src/engine_test.dart Normal file
View file

@ -0,0 +1,32 @@
import 'dart:html' as html;
@TestOn('browser')
import 'package:rules_of_living/src/Engine.dart';
import 'package:test/test.dart';
void main() {
Engine sut;
setUp(() {
sut = Engine();
});
test("Engine can be instantiated without canvas", () {
expect(sut, isNot(throwsNoSuchMethodError));
});
test("Engine does not throw errors when calling render directly", () {
// anonymous function necessary since throws can not use functions with args
expect(() => sut.render(), isNot(throwsNoSuchMethodError));
});
test("Engine does not throw errors when processing without attached canvas", () {
// anonymous function necessary since throws can not use functions with args
expect(() => sut.process(1000), isNot(throwsNoSuchMethodError));
});
test("setCanvas allows setting a canvas for an engine at any point", () {
sut.canvas = new html.CanvasElement();
expect(sut.canvas, isNotNull);
});
}