Compare commits

...

144 commits

Author SHA1 Message Date
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
92e147028e Add Save and Load Buttons to Interface
Fully functional and tested.
2018-10-19 20:51:14 +02:00
22dabda987 Add Save and Load Functions to SimService 2018-10-19 20:50:24 +02:00
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
37bff59f83 Remove clear function: Code Duplication 2018-10-19 20:01:52 +02:00
0da2d08b74 Add Simulation Saving and Loading Methods 2018-10-19 19:59:00 +02:00
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
2169de16fd Add Tests to Simulation 2018-10-19 19:35:09 +02:00
e8c1e6ed8b Refactor Engine Methods
Extract method checking for update necessity.
2018-10-19 11:50:34 +02:00
2993b33d9e Add tests for CellPattern & GameOfLife classes 2018-10-19 10:24:40 +02:00
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
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
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
32a3676d95 Remove deprecated strict mode 2018-10-18 15:19:54 +02:00
bbfb2f735b Move Render Methods into SimulationService 2018-10-18 14:52:03 +02:00
45e8f01acb Move Render Methods into SimulationService 2018-10-18 14:41:57 +02:00
58971016da Remove ConfigurationService
Replaced with direct access to both EngineService and SimulationService.
2018-10-18 14:23:07 +02:00
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
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
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
72ce25a806 Rename Controls component variable accessing engine 2018-10-18 11:59:36 +02:00
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
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
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
f1399064a2 Fix Simulation using RuleSet range for neighbor checks 2018-10-18 11:30:14 +02:00
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
0aa3df30b4 Extract CellPattern and GameOfLife into own files 2018-10-18 11:27:44 +02:00
fbdf114fed Move RuleSet to its own directory 2018-10-18 11:21:18 +02:00
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
c3244b085e Fix single steps not updating simulation 2018-10-18 11:04:23 +02:00
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
e16085153a Rename Simulation in Engine Object 2018-10-18 09:59:26 +02:00
bac65ef116 Remove unnecessary pattern parameters 2018-10-18 09:57:08 +02:00
6d7120650f Add Special Patterns to RuleSet 2018-10-17 21:08:55 +02:00
e6e82f78f2 Remove unnecessary Switch Case in Simulation 2018-10-17 21:07:48 +02:00
9b2b5f3e55 Remove unnecessary Simulation variables 2018-10-17 21:00:14 +02:00
27d4879b1b Extract RuleSet Class from Simulation 2018-10-17 20:58:04 +02:00
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
245d9a22c2 Remove Cell Data Structure
Cells are only boolean values of true or false for now.
2018-10-16 18:21:21 +02:00
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
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
c50e92fb19 Add simple stateChanges map into Simulation 2018-10-02 14:55:39 +02:00
9c37f87045 Merge branch '44-change-grid-data-structure-to-list' into 'master'
Resolve "Change Grid data structure to List"

Closes #44

See merge request marty.oehme/cellular-automata!12
2018-08-30 10:03:59 +00:00
a324d52df5 Fix Simulation Neighbor Propagation 2018-08-30 12:03:23 +02:00
b95d39d2b4 Fix not carrying width and height in Simulation 2018-08-30 10:58:55 +02:00
227357a745 Implement new Grid into Simulation 2018-08-30 10:57:14 +02:00
2dc1d7fecd Add Grid.fill constructor
Will completely fill the grid with the value passed in.
2018-08-30 10:32:34 +02:00
eef7a23c8f Regroup Instantiation tests 2018-08-30 10:17:31 +02:00
fb481669ed Add Coordinates setter
Sets the corresponding element to the parameter value passed in. Checks against the grid size constraints beforehand and throws RangeError if outside of constraints. Preferred method to set
element via coordinates.
2018-08-30 10:02:29 +02:00
5725757aa0 Add Coordinate getter documentation 2018-08-30 09:55:25 +02:00
5a72783d57 Add .toCoordinates() method to grid
Calculates the 2-D array coordinates from the corresponding list index passed in. Relies on grid width to calculate coordinates. Does not check against grid size constraints.
2018-08-30 09:53:05 +02:00
6c3fcbe7b0 Add .get method for coordinate element retrieval to grid
Coordinates passed in access the correct index in the internal list.
2018-08-30 09:43:46 +02:00
46b11bc33b Add dart_test.yaml file to suppress tag warnings
tags are added in the file without any special options
2018-08-30 09:42:29 +02:00
3f939601b3 Add toIndex method
can be used to get the correct index from coordinates passed in. Will only calculate the index, not take into consideration any grid size constraints etc.
2018-08-30 09:41:43 +02:00
8865af4878 Add tags to Instantiation tests 2018-08-30 09:36:57 +02:00
b0e67d9f85 Add getter and setter methods for wrapper
!!Need testing
2018-08-29 22:13:13 +02:00
27ef72014e Add simple Grid - List Wrapper Data Structure 2018-08-29 22:12:19 +02:00
0c487f3427 Rename Grid to Simulation
in order to craft an actual grid data structure
2018-08-29 20:13:24 +02:00
7db2e73f53 Merge branch '37-make-grid-size-configurable' into 'master'
Resolve "Make Grid Size configurable"

Closes #38, #39, and #37

See merge request marty.oehme/cellular-automata!10
2018-08-28 07:45:43 +00:00
b1221c7c84 Add Grid Size Input to Configuration Bar
Fixes #37
2018-08-27 23:04:02 +02:00
800c85d14f Fix wrong variable being accessed by controls_component 2018-08-27 23:03:13 +02:00
3a1ba1c1e9 Fix Wrong ControlService being provided to Angular 2018-08-27 23:02:48 +02:00
b6919cff6b Move Canvas setting to configuration service
Fix #39
2018-08-27 20:56:33 +02:00
a92b864dfa Extract EngineService into separate service 2018-08-27 20:50:12 +02:00
04d61bfa02 Error out on bad gridsize input 2018-08-27 20:18:19 +02:00
873bd9c881 Allow Optional Injection of grid size into engine on creation 2018-08-27 20:13:19 +02:00
9886f13d69 Add grid changing function 2018-08-27 19:55:30 +02:00
5e8f83cf8a Refactor EngineService to be able to inject custom Engine
fixes #38
2018-08-27 19:37:35 +02:00
dd18fc3bc7 Make Engine Gridsize configurable 2018-08-25 17:23:06 +02:00
8a6538aa5e Merge branch '35-wire-up-speed-slider' into 'master'
Resolve "Wire up Speed Slider"

Closes #35

See merge request marty.oehme/cellular-automata!9
2018-08-25 14:45:22 +00:00
7a3fdf3681 Fix Engine Test case 2018-08-25 16:43:47 +02:00
952179eee3 Implement Test Case 2018-08-25 16:43:38 +02:00
774e9e3782 Add Simple Documentation 2018-08-25 16:41:11 +02:00
c3f0881454 Wire up Speed slider to Engine Configuration 2018-08-25 15:51:32 +02:00
905af769f6 Add Configurable stepsPerSecond for engine logic 2018-08-25 15:51:17 +02:00
588a3ad2a0 Add explicit getter and setter for Simulation Speed
In preparation for adding additional logic as a quasi-callback to execute whenever the simSpeed changes (i.e. the slider is changed)
2018-08-25 15:26:44 +02:00
7d5b1cfe30 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
2018-08-25 13:02:45 +00:00
3c27f5ef38 Re-Add Canvas to Simulation component 2018-08-25 15:01:00 +02:00
71023de49a Make canvas virtual variable on EngineService 2018-08-25 15:00:46 +02:00
cc33628e5f Enable cached engine returns for EngineService 2018-08-25 14:51:28 +02:00
66bf87d9d8 Allow setting a canvas for an engine at any point 2018-08-25 14:36:50 +02:00
4c6dff35c3 Add Canvas nullcheck to render function 2018-08-25 14:10:56 +02:00
572406b963 Fix undefined running access 2018-08-25 14:10:19 +02:00
2d5cc889f0 Merge remote-tracking branch 'origin/31-allow-engine-to-be-created-without-the-need-for-a-canvas' into 31-allow-engine-to-be-created-without-the-need-for-a-canvas 2018-08-25 09:36:49 +02:00
976ec8097d Separate Engine Service Functionality from Component 2018-08-25 09:36:27 +02:00
842cbeca1f make canvas argument optional on instantiating engine 2018-08-25 09:36:27 +02:00
1bd324a406 Disable superfluous tests
In preparation for first test of the app.
2018-08-25 09:36:27 +02:00
a8cafbac49 Migrate to official dart-sdk version 2.0.0 2018-08-25 09:35:59 +02:00
76d73266f5 Merge remote-tracking branch 'origin/31-allow-engine-to-be-created-without-the-need-for-a-canvas' into 31-allow-engine-to-be-created-without-the-need-for-a-canvas 2018-08-25 09:33:33 +02:00
bbf61f875d Separate Engine Service Functionality from Component 2018-08-25 09:33:15 +02:00
6c9179b833 make canvas argument optional on instantiating engine 2018-08-24 20:02:43 +02:00
8d7575eaf7 Disable superfluous tests
In preparation for first test of the app.
2018-08-24 20:01:36 +02:00
c84b052beb Merge branch '25-add-configuration-sidebar' into 'master'
Resolve "Add Configuration Sidebar"

Closes #25

See merge request marty.oehme/cellular-automata!7
2018-08-24 17:59:17 +00:00
bf0d136d8b Add Configuration Sidebar with ConfigurationService 2018-08-23 14:12:22 +02:00
6ba0ca55cb dartfmt 2018-08-23 12:38:34 +02:00
223f831196 Merge branch '21-switch-from-fontawesome-to-material-glyphs' into 'master'
Resolve "Switch from FontAwesome to Material Glyphs"

Closes #21

See merge request marty.oehme/cellular-automata!6
2018-08-23 10:08:38 +00:00
abd2c3ac23 Add Speed Slider Tooltip 2018-08-23 12:07:54 +02:00
c3368524f9 Change Step Forward Icon 2018-08-23 12:07:43 +02:00
4906bbe54c Switch Control Interface Icons to MaterialIcons 2018-08-23 11:34:08 +02:00
594506081a Merge branch '19-add-material-styling-to-app' into 'master'
Resolve "Add Material Styling to App"

Closes #19

See merge request marty.oehme/cellular-automata!5
2018-08-23 07:43:18 +00:00
bd7990991e Connect Speed Slider to internal Variable 2018-08-23 09:33:15 +02:00
c86d8d8b59 Split Simulation & Controls into Components 2018-08-22 20:16:31 +02:00
917d1c3fab Split App Header into Component 2018-08-22 13:00:41 +02:00
b541353091 Fix Material directives import statement 2018-07-10 14:28:41 +02:00
c5cd600cfc Add Sample Material Styling 2018-07-10 12:52:56 +02:00
8485a96aa3 Add Required Material Font 2018-07-09 18:11:34 +02:00
8a1a050d81 Add Angulard Material Dependency 2018-07-09 17:54:29 +02:00
4b7051a5e1 Fix implicit Overwrite of Dart Pattern Class 2018-07-09 17:45:11 +02:00
436061a629 Merge branch '14-refactor-reliance-on-inner-variables' into 'master'
Resolve "Refactor reliance on inner variables"

Closes #14

See merge request marty.oehme/cellular-automata!4
2018-07-09 15:35:19 +00:00
49803e7a6a Fix Remnants of EdgeRendering toggle compile Error 2018-07-09 17:33:48 +02:00
2d0e24bdf8 Move Reset Grid Implementation to Engine 2018-07-09 17:32:59 +02:00
4c1c805f24 Add addPattern Functionality to Engine 2018-07-09 17:32:35 +02:00
6745f9c9d6 Make grid Private to Engine 2018-07-09 17:31:46 +02:00
dfcd55fe71 Remove App Grid Access 2018-07-09 17:29:09 +02:00
ac9a7c7591 Implement Engine Step Method 2018-07-09 17:27:56 +02:00
9030d31e97 Hide Implementation of edge rendering in grid 2018-07-09 17:27:11 +02:00
7c49531d0e Expose Getter and Setter for Engine Running Variable 2018-07-09 17:00:53 +02:00
e472766bcc Merge branch '13-rename-app-to-engine' into 'master'
Resolve "Rename App to Engine"

Closes #13

See merge request marty.oehme/cellular-automata!3
2018-07-09 13:17:34 +00:00
e18de1d84c Rename App to Engine 2018-07-09 15:16:28 +02:00
1208583c7f Merge branch '11-add-delete-board-button' into 'master'
Resolve "Add Delete Board Button"

Closes #11

See merge request webdevexp/rules-of-living!2
2018-07-08 17:46:30 +00:00
1c756bbda4 Add Functionality to Clear Board Button 2018-07-08 19:45:47 +02:00
1a43be14fa Add Clear Board Button to Interface 2018-07-08 19:45:35 +02:00
016b166d50 Add Clear Board Function to Engine 2018-07-08 19:45:04 +02:00
49b036ffe9 Merge branch '6-stop-simulation-if-all-cells-are-empty' into 'master'
Resolve "Stop Simulation if all cells are empty"

Closes #6

See merge request webdevexp/rules-of-living!1
2018-07-08 17:34:39 +00:00
1146c7d265 Add Check for State Changes During Grid Update 2018-07-08 19:32:43 +02:00
9fb67d0194 Add return of new cell state to advanceState Function 2018-07-08 19:08:40 +02:00
3676264444 dartfmt 2018-07-08 19:05:11 +02:00
4074f49228 Fix Missing return statement 2018-07-08 19:03:48 +02:00
fb014ce5ac Shorten Heading 2018-07-08 19:01:46 +02:00
6f41c63f3c Merge branch 'add-grid-rendering' 2018-07-08 19:01:21 +02:00
8fc3f35321 Add Edge Rendering with Toggle Button 2018-07-08 19:01:14 +02:00
a71d442b45 Merge branch 'random-starting-pattern' 2018-07-08 19:00:07 +02:00
c5ed18f0ad Add Random Button Functionality
Adds random pattern toward the center of the canvas. Does not delete anything.
2018-07-07 22:02:04 +02:00
c5b62e6c9f Fix Reset & Step not working on first click
Rendering dirty flags were not updated accordingly, so the updates happened but were not pushed to be rendered. Added and moved additional dirty flag setters.
2018-07-07 21:43:24 +02:00
2d2365e606 Fix Simulation Reset not Replicating the Starting Pattern
Now keeps the original parameters for the first pattern of a grid stored and can replicate them reliably.
2018-07-07 21:42:07 +02:00
bdc5dc1af1 Add Message for App Failure 2018-07-07 21:07:42 +02:00
34bdd1ae81 Apply dartfmt 2018-07-07 20:49:02 +02:00
2f96712b60 Merge branch 'random-starting-pattern' 2018-07-07 20:47:37 +02:00
4b35dbe5fc Add Parameters for Dispersal and Amount of Cells 2018-07-07 20:47:23 +02:00
9816778d4b Allow Defining x and y for Patterns 2018-07-07 20:35:04 +02:00
964ba69c2e Add Grid Starting Patterns
can be common forms and random
2018-07-07 20:04:35 +02:00
f50f30453d Merge branch 'add-license' 2018-07-07 19:09:40 +02:00
7b02834b54 Add MIT License 2018-07-07 19:09:25 +02:00
59b67be3b0 Merge branch 'change-to-angular-dart' 2018-07-07 19:08:59 +02:00
38 changed files with 1375 additions and 405 deletions

View file

@ -1,2 +1,21 @@
Copyright (C) 2018
The MIT License (MIT)
Copyright (c) 2018 Marty Oehme
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

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

10
dart_test.yaml Normal file
View file

@ -0,0 +1,10 @@
platforms: [chrome]
tags:
nobrowser:
bad:
sad:
happy:

11
lib/app_component.css Normal file
View file

@ -0,0 +1,11 @@
#wrapper {
display: flex;
}
#viewport {
flex: 0 0 65%;
}
#sidebar {
flex: 1;
}

View file

@ -1,47 +1,35 @@
import 'package:angular/angular.dart';
import 'dart:html' as html;
import 'package:rules_of_living/src/App.dart';
import 'package:angular_components/angular_components.dart';
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/engine_service.dart';
import 'package:rules_of_living/service/simulation_service.dart';
@Component(
selector: 'my-app',
templateUrl: "app_component.html",
directives: [coreDirectives]
directives: [
coreDirectives,
MaterialButtonComponent,
MaterialIconComponent,
MaterialSliderComponent,
HeaderComponent,
SimulationComponent,
ControlsComponent,
ConfigurationComponent
],
providers: [
materialProviders,
ClassProvider(EngineService),
ClassProvider(SimulationService)
],
styleUrls: const [
'package:angular_components/app_layout/layout.scss.css',
'app_component.css'
],
)
class AppComponent implements OnInit {
class AppComponent {
var name = "World";
App engine;
@ViewChild("caCanvas")
html.CanvasElement canvas;
@override
void ngOnInit() {
canvas.context2D.setFillColorRgb(255, 0, 0);
canvas.context2D.fillRect(0, 0, 200, 150);
engine = new App(canvas);
html.window.animationFrame.then(animFrame);
}
void animFrame(num now) {
engine.process(now);
html.window.animationFrame.then(animFrame);
}
void onStartClicked() {
engine.running = !engine.running;
}
void onStepClicked() {
engine.running = false;
engine.update();
}
void onResetClicked() {
engine.reset();
}
void onRandomClicked() {}
}

View file

@ -1,20 +1,10 @@
<h1>Cellular Automata - The Rules of Life</h1>
<div id="rules-input">
Ruleset: <input type="text" title="ruleset" content="S23/B3">
<i class="fas fa-paint-brush"></i>
</div>
<div id="output">
<canvas #caCanvas width="500" height="500"></canvas>
</div>
<div id="controls">
<button id="run" (click)="onStartClicked()">
<span [ngSwitch]="engine.running">
<i *ngSwitchCase="false" class="fas fa-play"></i>
<i *ngSwitchCase="true" class="fas fa-stop"></i>
</span>
</button>
<button id="step" (click)="onStepClicked()"><i class="fas fa-step-forward"></i></button>
<button id="reset" (click)="onResetClicked()"><i class="fas fa-undo"></i></button>
<button id="random" (click)="onRandomClicked()"><i class="fas fa-random"></i></button>
<i class="fas fa-clock"> Speed:</i><input type="text" title="speed" value="1">
<app_header></app_header>
<div id="wrapper">
<div id="viewport">
<gol-simulation></gol-simulation>
<sim-controls></sim-controls>
</div>
<div id="sidebar">
<configuration></configuration>
</div>
</div>

View file

@ -0,0 +1,4 @@
material-slider {
display: inline-block;
width: 150px;
}

View file

@ -0,0 +1,54 @@
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';
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/engine_service.dart';
import 'package:rules_of_living/service/simulation_service.dart';
@Component(
selector: "configuration",
templateUrl: "configuration_component.html",
styleUrls: [
"configuration_component.css"
],
directives: [
MaterialButtonComponent,
MaterialIconComponent,
MaterialSliderComponent,
MaterialTooltipDirective,
materialInputDirectives,
materialNumberInputDirectives,
NgModel
])
class ConfigurationComponent {
final EngineService engine;
final SimulationService sim;
int get width => sim.gridSize.x;
void set width(num value) {
if (value == null || value <= 0) return;
sim.gridSize = Point(value, sim.gridSize.y);
}
int get height => sim.gridSize.y;
void set height(num value) {
if (value == null || value <= 0) return;
sim.gridSize = Point(sim.gridSize.x, value);
}
int get simSpeed => engine.simSpeed;
void set simSpeed(int value) => engine.simSpeed = value;
String get speedSliderTooltip => "Simulation Speed: $simSpeed";
ConfigurationComponent(this.engine, this.sim);
void onEdgesClicked() {
sim.toggleGrid();
}
}

View file

@ -0,0 +1,21 @@
<div id="config">
<material-button id="edges" (click)="onEdgesClicked()">
<material-icon icon="border_all" baseline></material-icon>
</material-button>
<span [materialTooltip]="speedSliderTooltip"><material-icon icon="alarm" baseline></material-icon><material-slider
[max]="10" [min]="1" [(value)]="simSpeed"></material-slider></span>
Ruleset: <input type="text" title="ruleset" content="S23/B3"><i class="fas fa-paint-brush"></i>
<material-input
leadingGlyph="swap_horiz"
type="number"
label="Grid Width"
[(ngModel)]="width"
required
checkPositive
requiredErrorMsg="Enter a number greater than 0"
trailingText="Cells">
</material-input>
<material-input leadingGlyph="swap_vert" type="number" label="Grid Height" [(ngModel)]="height" required
checkPositive requiredErrorMsg="Enter a number greater than 0"
trailingText="Cells"></material-input>
</div>

View file

View file

@ -0,0 +1,48 @@
import 'package:angular/angular.dart';
import 'package:angular_components/angular_components.dart';
import 'package:rules_of_living/service/engine_service.dart';
import 'package:rules_of_living/service/simulation_service.dart';
@Component(
selector: 'sim-controls',
templateUrl: "controls_component.html",
directives: [
coreDirectives,
MaterialButtonComponent,
MaterialIconComponent,
MaterialTooltipDirective
],
providers: [],
styleUrls: const ["controls_component.css"],
)
class ControlsComponent {
final EngineService engine;
final SimulationService sim;
ControlsComponent(this.engine, this.sim);
void onStartClicked() {
engine.toggleRunning();
}
void onStepClicked() {
engine.step();
}
void onSaveClicked() {
sim.save();
}
void onLoadClicked() {
sim.load();
}
void onRandomClicked() {
sim.addRandomPattern();
engine.stop();
}
void onClearClicked() {
sim.reset();
}
}

View file

@ -0,0 +1,13 @@
<div id="controls">
<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]="engine.isRunning">
<material-icon *ngSwitchCase="false" icon="play_arrow" baseline></material-icon>
<material-icon *ngSwitchCase="true" icon="pause" baseline></material-icon>
</span>
</material-button>
<material-button id="step" (click)="onStepClicked()" materialTooltip="Step Forward"><material-icon icon="skip_next" baseline></material-icon></material-button>
<material-button id="random" (click)="onRandomClicked()" materialTooltip="Add Random Pattern"><material-icon icon="add" baseline></material-icon></material-button>
<material-button id="clear" (click)="onClearClicked()" materialTooltip="Clear"><material-icon icon="clear" baseline></material-icon></material-button>
</div>

View file

@ -0,0 +1,16 @@
import 'package:angular/angular.dart';
import 'package:angular_components/angular_components.dart';
@Component(
selector: 'app_header',
templateUrl: "header_component.html",
directives: [
coreDirectives,
MaterialButtonComponent,
MaterialIconComponent,
MaterialSliderComponent
],
providers: [],
styleUrls: const ['package:angular_components/app_layout/layout.scss.css'],
)
class HeaderComponent {}

View file

@ -0,0 +1,12 @@
<header class="material-header">
<div class="material-header-row">
<material-button icon class="material-drawer-button">
<material-icon icon="menu"></material-icon>
</material-button>
<span class="material-header-title">Cellular Automata</span>
<div class="material-spacer"></div>
<nav class="material-navigation">
<a>Link 1</a>
</nav>
</div>
</header>

View file

@ -0,0 +1,36 @@
import 'dart:html' as html;
import 'package:angular/angular.dart';
import 'package:rules_of_living/service/engine_service.dart';
import 'package:rules_of_living/service/simulation_service.dart';
@Component(
selector: 'gol-simulation',
templateUrl: "simulation_component.html",
directives: [coreDirectives],
providers: [],
)
class SimulationComponent implements OnInit {
final EngineService engine;
final SimulationService sim;
SimulationComponent(this.engine, this.sim);
@override
void ngOnInit() {
html.CanvasElement canvas = html.CanvasElement()..id = "simulation";
html.querySelector("#simulation")..append(canvas);
canvas.width = 500;
canvas.height = 500;
canvas.context2D.setFillColorRgb(200, 0, 0);
canvas.context2D.fillRect(0, 0, canvas.width, canvas.height);
canvas.context2D.setFillColorRgb(0, 255, 0);
canvas.context2D.fillText('''
If you see this
the canvas did not load correctly :(
''', canvas.width / 2 - 50, canvas.height / 2);
sim.canvas = canvas;
}
}

View file

@ -0,0 +1 @@
<div id="simulation"></div>

View file

@ -0,0 +1,48 @@
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;
}
Engine _setCachedAndReturn(Engine newEngine) {
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,66 +0,0 @@
import 'dart:html' as html;
import 'package:rules_of_living/src/Grid.dart';
class App {
// Elapsed Time Counter - useful for Safety Timeout
Stopwatch _elapsed = new Stopwatch();
// Game Tick Rate - *does* impact game speed
int _MS_PER_STEP = 1000 ~/ 3;
// Max Frame (i.e. Rendering) rate - does *not* impact game speed
final int _MS_PER_FRAME = 1000 ~/ 30;
// ms stuck in updateloop after which game will declare itself unresponsive
final int SAFETY_TIMEOUT = 2000;
num _updateLag = 0.0;
num _drawLag = 0.0;
final html.CanvasElement canvas;
Grid grid = new Grid(100,100);
bool running = false;
App(this.canvas) {
_elapsed.start();
}
void reset () {
grid = new Grid(100, 100);
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();
_updateLag -= _MS_PER_STEP;
}
if (_drawLag >= _MS_PER_FRAME) {
render(_updateLag / _MS_PER_STEP);
_drawLag = 0;
}
}
void update() {
// print("updating");
grid.update();
}
void render([num interp]) {
// print("rendering");
grid.render(canvas, interp);
}
}

View file

@ -1,32 +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;
void advanceState() {
this.state = this.nextState;
this.nextState = false;
this.dirty = true;
}
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;
});
}
}
}

116
lib/src/Engine.dart Normal file
View file

@ -0,0 +1,116 @@
import 'dart:html' as html;
import 'package:rules_of_living/src/Simulation.dart';
class Engine {
// Elapsed Time Counter - useful for Safety Timeout
Stopwatch _elapsed = new Stopwatch();
/// Game Tick Rate
///
/// *does* impact game speed; dictates how long each logic step takes. Only
/// interesting for engine internal calculations, to set simulation speed
/// from the outside use stepsPerSecond instead.
int _MS_PER_STEP = 1000 ~/ 3;
/// Set Logic Updates per Second
///
/// Dictates simulation speed. Sets the amount of (logic) updates per second.
/// Translations between number of updates and their timings is not exact so
/// comparing this variable to fixed ints might not yield the expected results.
/// Does not affect render frequency, which is handled by framesPerSecond.
int get stepsPerSecond => 1000 ~/ _MS_PER_STEP;
set stepsPerSecond(int val) => _MS_PER_STEP = 1000 ~/ val;
// Max Frame (i.e. Rendering) rate - does *not* impact game speed
final int _MS_PER_FRAME = 1000 ~/ 30;
// ms stuck in updateloop after which game will declare itself unresponsive
final int SAFETY_TIMEOUT = 2000;
num _updateLag = 0.0;
num _drawLag = 0.0;
/// The Canvas to Paint on
///
/// 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;
Simulation _simulation;
bool running = false;
Engine() {
_elapsed.start();
html.window.animationFrame.then(animFrame);
}
void animFrame(num now) {
int elapsed = _elapsed.elapsedMilliseconds;
_elapsed.reset();
process(elapsed, SAFETY_TIMEOUT, update: this.update, render: this.render);
html.window.animationFrame.then(animFrame);
}
void process(int elapsed, int timeOut, {Function update, Function render}) {
_drawLag += elapsed;
_updateLag += elapsed;
while (running == true &&
_shouldUpdate(_updateLag, elapsed, timeOut) == true) {
_updateLag -= _MS_PER_STEP;
if (update == null) break;
update();
}
if (_drawLag >= _MS_PER_FRAME) {
_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 (_simulation == null) return;
Map<int, bool> simulationUpdate = _simulation.update();
_simulation.mergeStateChanges(simulationUpdate);
if (simulationUpdate.length == 0) 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() {
update();
running = false;
}
/// 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]) {
if (canvas == null || _simulation == null) return;
_simulation.render(canvas, interp);
}
void set simulation(Simulation value) => _simulation = value;
}

View file

@ -1,136 +1,68 @@
import 'dart:html' as html;
import 'dart:core';
import 'dart:math';
import 'package:rules_of_living/src/Cell.dart';
import 'package:rules_of_living/src/Rule.dart';
import 'package:collection/collection.dart';
class Grid {
final int w;
final int h;
final List<List<Cell>> map;
class Grid<E> extends DelegatingList<E> {
final List<E> _internal;
final width;
final height;
bool _dirty = true;
Grid(int width, int height) : this._(List<E>(width * height), width, height);
Grid(int w, int h)
: this.w = w,
this.h = h,
this.map = new List() {
map.addAll(_buildGrid(w, h));
Grid.fill(int width, int height, E fillValue)
: this._(List<E>.filled(width * height, fillValue), width, height);
// BLINKER
// map[5][5].state = true;
// map[5][6].state = true;
// map[6][5].state = true;
// map[6][6].state = true;
//
// map[7][7].state = true;
// map[7][8].state = true;
// map[8][7].state = true;
// map[8][8].state = true;
Grid.from(Grid<E> l)
: this._(List<E>.from(l.getRange(0, l.length)), l.width, l.height);
// SPACESHIP
setState(1 + 5, 0 + 5, true);
setState(2 + 5, 1 + 5, true);
setState(2 + 5, 2 + 5, true);
setState(1 + 5, 2 + 5, true);
setState(0 + 5, 2 + 5, true);
Grid.fromList(List<E> l, int width) : this._(l, width, l.length ~/ width);
print("Grid creation finished");
Grid._(l, int w, int h)
: _internal = l,
width = w,
height = h,
super(l);
/// Return element at coordinate position
///
/// Returns the corresponding element after checking the parameters
/// for the correct constraints along the width and height of the grid.
/// Throws [RangeError] if outside of constraints. Preferred method
/// to access elements via coordinates.
E get(int x, int y) {
int i = toIndex(x, y);
if (i >= length || x > width - 1) throw RangeError.index(i, this);
return _internal[i];
}
void setState(int x, int y, bool state) {
if (y < map.length && x < map[y].length) map[y][x].state = state;
/// Sets element at coordinate position
///
/// Sets the corresponding element to the [E] parameter [value] passed in.
/// Checks against the grid size constraints beforehand and throws
/// [RangeError] if outside of constraints. Preferred method to set
/// elements via coordinates.
void set(int x, int y, E value) {
int i = toIndex(x, y);
if (i >= length || x > width - 1) throw RangeError.index(i, this);
_internal[i] = value;
}
List<List<Cell>> _buildGrid(int w, int h) {
print("grid being created");
List<List<Cell>> grid = new List(h);
// GENERAL RULE LAYOUT
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;
});
/// Calculate list index from coordinates
///
/// Can be used to get the correct index from coordinates passed in.
/// Will only calculate the index, not take into consideration any grid size
/// constraints etc; use [get] for that (generally recommended).
int toIndex(int x, int y) => (x < 0 || y < 0)
? throw RangeError("Coordinates for Grid Indexing must not be negative.")
: y * width + x;
// DEBUG RULE TESTING FOR PATTERNS
Rule coagSurvive = new Rule((int n) {
if (n==1) return true;
return false;
});
Rule coagBirth = new Rule((int n) {
if (n==1) return true;
return false;
});
for (int y = 0; y < h; y++) {
grid[y] = new List(w);
for (int x = 0; x < w; x++) {
// GIVES RULES FOR CONWAY GAME OF LIFE BY DEFAULT S23/B3
Cell cell = new Cell();
// cell.surviveRules.add(twoTrue);
cell.surviveRules.add(coagSurvive);
cell.birthRules.add(coagBirth);
grid[y][x] = cell;
}
}
return grid;
}
void update() {
for (int y = 0; y < h; y++) {
for (int x = 0; x < w; x++) {
// DEFAULTS TO CONWAY GAME OF LIFE RANGE OF ONE
map[y][x].update(getSurroundingNeighbors(x, y, 1));
if (!_dirty && map[y][x].dirty) _dirty = true;
}
}
for (int y = 0; y < h; y++) {
for (int x = 0; x < w; x++) {
// DEFAULTS TO CONWAY GAME OF LIFE RANGE OF ONE
map[y][x].advanceState();
}
}
}
int getSurroundingNeighbors(int x, int y, int range) {
int count = 0;
for (int iy = y - range; iy <= y + range; iy++) {
for (int ix = x - range; ix <= x + range; ix++) {
if (ix > 0 &&
iy > 0 &&
iy < map.length &&
ix < map[iy].length &&
map[iy][ix].state == true &&
!(x == ix && y == iy)) {
count++;
}
}
}
return count;
}
void render(html.CanvasElement canvas, [num interp]) {
// only renders if any cells changed between renders
if (!_dirty) return;
html.CanvasRenderingContext2D ctx = canvas.getContext('2d');
int brickW = (canvas.width ~/ map[0].length);
int brickH = (canvas.height ~/ map.length);
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (int y = 0; y < map.length; y++) {
for (int x = 0; x < map[y].length; x++) {
Cell c = map[y][x];
if (c.state == true)
ctx.setFillColorRgb(155, 155, 255);
else
ctx.setFillColorRgb(0, 0, 0);
ctx.fillRect(x * brickW, y * brickH, brickW, brickH);
}
}
_dirty = false;
}
/// Calculate coordinates from list index
///
/// Calculates the 2-D array coordinates from the corresponding list index
/// passed in. Relies on grid width to calculate coordinates. Does not check
/// against grid size constraints; use [set] for that (generally recommended).
Point<int> toCoordinates(int index) => (index < 0)
? throw RangeError("Index for Grid Coordinates must not be negative")
: Point<int>(index % width, index ~/ width);
}

View file

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

163
lib/src/Simulation.dart Normal file
View file

@ -0,0 +1,163 @@
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<bool> map;
Grid<bool> _snapshot;
RuleSet rules = GameOfLife();
bool _dirty = true;
bool get dirty => _dirty;
bool _renderEdges = true;
bool get renderEdges => _renderEdges;
int _amount;
int _dispersal;
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) {
this.map = reset();
}
Simulation.fromGrid(Grid<bool> map) : this.map = map;
Grid<bool> reset([Grid<bool> map]) {
map ??= this.map;
_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);
}
Map<int, bool> update() {
Map<int, bool> stateChanges = calculateNextState(map);
return stateChanges;
}
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++) {
if (ix >= 0 &&
iy >= 0 &&
ix < map.width &&
iy < map.height &&
getCellState(ix, iy) == true &&
!(x == ix && y == iy)) count++;
}
}
return count;
}
void render(html.CanvasElement canvas, [num interp]) {
// only renders if any cells changed between renders
if (!_dirty) return;
html.CanvasRenderingContext2D ctx = canvas.getContext('2d');
int brickW = (canvas.width ~/ map.width);
int brickH = (canvas.height ~/ map.height);
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (int i = 0; i < map.length; i++) {
math.Point p = map.toCoordinates(i);
if (_renderEdges) {
ctx.setStrokeColorRgb(100, 100, 100);
ctx.strokeRect(p.x * brickW, p.y * brickH, brickW, brickH);
}
if (map[i] == true)
ctx.setFillColorRgb(155, 155, 255);
else
ctx.setFillColorRgb(0, 0, 0);
ctx.fillRect(p.x * brickW, p.y * brickH, brickW, brickH);
}
_dirty = false;
}
void set renderEdges(bool on) {
_renderEdges = on;
_dirty = true;
}
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

@ -5,14 +5,16 @@ author: Marty Oehme <marty.oehme@gmail.com>
homepage: https://www.martyoehme.org/
environment:
sdk: '>=2.0.0-dev.66.0 <2.0.0'
sdk: '>=2.0.0'
dependencies:
angular: ^5.0.0-beta
angular_components: ^0.9.0-beta
dev_dependencies:
angular_test: ^2.0.0-beta
build_runner: ^0.9.0
build_test: ^0.10.2
build_test: ^0.10.3+1
build_web_compilers: ^0.4.0
test: ^1.0.0
test: ^1.3.0
mockito: ^3.0.0

View file

@ -0,0 +1,41 @@
import 'package:mockito/mockito.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() {
EngineService sut;
MockEngine me;
setUp(() {
me = MockEngine();
sut = EngineService();
});
group("Dependency Injection", () {
test("EngineService can be passed a custom Engine", () {
sut.engine = me;
Engine result = sut.engine;
expect(result, equals(me));
});
});
group("caching", () {
test("EngineService creates an engine on demand", () {
Engine result = sut.engine;
expect(result, TypeMatcher<Engine>());
});
test("EngineService returns the cached engine on subsequent requests", () {
Engine result = sut.engine;
expect(sut.engine, equals(result));
});
test("caching can be overriden by providing a custom engine", () {
Engine first = sut.engine;
sut.engine = me;
Engine second = sut.engine;
expect(second, isNot(equals(first)));
});
});
}

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

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

@ -0,0 +1,125 @@
import 'dart:html' as html;
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';
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));
});
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, isNot(throwsNoSuchMethodError));
});
test("setCanvas allows setting a canvas for an engine at any point", () {
sut.canvas = new html.CanvasElement();
expect(sut.canvas, isNotNull);
});
});
}

189
test/src/grid_test.dart Normal file
View file

@ -0,0 +1,189 @@
import 'dart:math';
import 'package:rules_of_living/src/Grid.dart';
import 'package:test/test.dart';
@Tags(const ["nobrowser"])
void main() {
group("Instantiation", () {
List<String> l;
setUp(() {
l = [
"Hey",
"you",
"me",
"together",
"Hello",
"World",
"I",
"am",
"ready."
];
});
test("gets created with the correct length for given quadratic gridsize",
() {
Grid sut = Grid(3, 3);
expect(sut.length, 9);
}, tags: const ["happy"]);
test("gets created with the correct length for given rectangular gridsize",
() {
Grid sut = Grid(87, 85);
expect(sut.length, 7395);
}, tags: const ["happy"]);
group(".from", () {
test("copies the content of another grid on .from Constructor call", () {
Grid original = Grid(2, 2);
original[0] = "Hey";
original[1] = "you";
original[2] = "me";
original[3] = "together";
Grid sut = Grid.from(original);
expect(sut, containsAllInOrder(["Hey", "you", "me", "together"]));
}, tags: const ["happy"]);
test("copies the length of another grid on .from Constructor call", () {
Grid original = Grid(2, 2);
original[0] = "Hey";
original[1] = "you";
original[2] = "me";
original[3] = "together";
Grid sut = Grid.from(original);
expect(sut.length, 4);
}, tags: const ["happy"]);
});
group(".fromList", () {
test("sets the length for list passed in on .fromList Constructor call",
() {
Grid sut = Grid.fromList(l, 3);
expect(sut.length, 9);
}, tags: const ["happy"]);
test("sets the contents of list passed in on .fromList Constructor call",
() {
Grid sut = Grid.fromList(l, 3);
expect(sut[3], "together");
}, tags: const ["happy"]);
test(
"sets the correct height for list passed in on .fromList Constructor call",
() {
Grid sut = Grid.fromList(l, 3);
expect(sut.width, 3);
}, tags: const ["happy"]);
});
group(".fill", () {
test("fills list with results of function passed in", () {
Grid<String> sut = Grid.fill(3, 3, "testValue");
expect(
sut,
containsAllInOrder([
"testValue",
"testValue",
"testValue",
"testValue",
"testValue",
"testValue",
"testValue",
"testValue",
"testValue"
]));
}, tags: const ["happy"]);
});
});
group("toIndex", () {
Grid sut;
setUp(() {
sut = Grid(3, 3);
});
test("throws RangeError on negative x argument", () {
expect(() => sut.toIndex(-1, 2), throwsA(isRangeError));
}, tags: const ["bad"]);
test("throws RangeError on negative y argument", () {
expect(() => sut.toIndex(2, -1), throwsA(isRangeError));
}, tags: const ["bad"]);
test("calculates correct index for first element", () {
expect(sut.toIndex(0, 0), equals(0));
}, tags: const ["happy"]);
test("calculates correct index for last element", () {
expect(sut.toIndex(2, 2), equals(8));
}, tags: const ["happy"]);
test("calculates correct index for element on first row", () {
expect(sut.toIndex(2, 0), equals(2));
}, tags: const ["happy"]);
test("calculates correct index for example element", () {
expect(sut.toIndex(1, 1), equals(4));
}, tags: const ["happy"]);
});
group("coordinates getter", () {
Grid sut;
setUp(() {
sut = Grid(3, 3);
sut.setAll(0,
["Hey", "you", "me", "together", "Hello", null, "I", "am", "ready."]);
});
test("returns null if no element exists at the position requested", () {
expect(sut.get(2, 1), null);
}, tags: const ["sad"]);
test("throws RangeError if requesting element outside of grid width", () {
expect(() => sut.get(4, 1), throwsRangeError);
}, tags: const ["bad"]);
test("throws RangeError if requesting element outside of grid height", () {
expect(() => sut.get(1, 4), throwsRangeError);
}, tags: const ["bad"]);
test("returns element at correct index", () {
expect(sut.get(1, 0), "you");
}, tags: const ["happy"]);
test("returns last element correctly", () {
expect(sut.get(2, 2), "ready.");
}, tags: const ["happy"]);
});
group("toCoords", () {
Grid sut;
setUp(() {
sut = Grid(3, 3);
});
test("throws RangeError on negative index argument", () {
expect(() => sut.toCoordinates(-1), throwsA(isRangeError));
}, tags: const ["bad"]);
test("calculates correct index for first element", () {
expect(sut.toCoordinates(0), equals(Point(0, 0)));
}, tags: const ["happy"]);
test("calculates correct index for last element", () {
expect(sut.toCoordinates(8), equals(Point(2, 2)));
}, tags: const ["happy"]);
test("calculates correct index for last element on first row", () {
expect(sut.toCoordinates(2), equals(Point(2, 0)));
}, tags: const ["happy"]);
test("calculates correct index for example element", () {
expect(sut.toCoordinates(6), equals(Point(0, 2)));
}, tags: const ["happy"]);
});
group("coordinates setter", () {
Grid<String> sut;
setUp(() {
sut = Grid(3, 3);
sut.setAll(0,
["Hey", "you", "me", "together", "Hello", null, "I", "am", "ready."]);
});
test("sets element to null if passing null in", () {
sut.set(1, 1, null);
expect(sut.get(1, 1), null);
}, tags: const ["sad"]);
test("throws RangeError if setting element outside of grid width", () {
expect(() => sut.set(4, 1, "testValue"), throwsRangeError);
}, tags: const ["bad"]);
test("throws RangeError if setting element outside of grid height", () {
expect(() => sut.set(1, 4, "testValue"), throwsRangeError);
}, tags: const ["bad"]);
test("sets element at correct index", () {
sut.set(1, 0, "testValue");
expect(sut.get(1, 0), "testValue");
}, tags: const ["happy"]);
test("sets last element correctly", () {
sut.set(2, 2, "testValue");
expect(sut.get(2, 2), "testValue");
}, tags: const ["happy"]);
});
}

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

View file

@ -15,11 +15,10 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css">
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700">
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<link rel="icon" type="image/png" href="favicon.png">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.1.0/css/solid.css" integrity="sha384-TbilV5Lbhlwdyc4RuIV/JhD8NR+BfMrvz4BL5QFa2we1hQu6wvREr3v6XSRfCTRp" crossorigin="anonymous">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.1.0/css/fontawesome.css" integrity="sha384-ozJwkrqb90Oa3ZNb+yKFW2lToAWYdTiF1vt8JiH5ptTGHTGcN7qdoR1F95e0kYyG" crossorigin="anonymous">
<script defer src="main.dart.js"></script>
</head>

View file

@ -1,117 +1,126 @@
@import url(https://fonts.googleapis.com/css?family=Roboto);
@import url(https://fonts.googleapis.com/css?family=Material+Icons);
/* Master Styles */
h1 {
color: #369;
font-family: Arial, Helvetica, sans-serif;
font-size: 250%;
}
h2, h3 {
color: #444;
font-family: Arial, Helvetica, sans-serif;
font-weight: lighter;
}
body {
margin: 2em;
}
body, input[text], button {
color: #888;
font-family: Cambria, Georgia;
}
a {
cursor: pointer;
cursor: hand;
}
button {
font-family: Arial;
background-color: #eee;
border: none;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
cursor: hand;
}
button:hover {
background-color: #cfd8dc;
}
button:disabled {
background-color: #eee;
color: #aaa;
cursor: auto;
}
label {
padding-right: 0.5em;
}
/* Navigation link styles */
nav a {
padding: 5px 10px;
text-decoration: none;
margin-right: 10px;
margin-top: 10px;
display: inline-block;
background-color: #eee;
border-radius: 4px;
}
nav a:visited, a:link {
color: #607D8B;
}
nav a:hover {
color: #039be5;
background-color: #CFD8DC;
}
nav a.active {
color: #039be5;
html, body {
height: 100%;
margin: 0;
padding: 0;
width: 100%;
font-family: 'Roboto', sans-serif;
}
/* items class */
.items {
margin: 0 0 2em 0;
list-style-type: none;
padding: 0;
width: 24em;
}
.items li {
cursor: pointer;
position: relative;
left: 0;
background-color: #EEE;
margin: .5em;
padding: .3em 0;
height: 1.6em;
border-radius: 4px;
}
.items li:hover {
color: #607D8B;
background-color: #DDD;
left: .1em;
}
.items li.selected {
background-color: #CFD8DC;
color: white;
}
.items li.selected:hover {
background-color: #BBD8DC;
}
.items .text {
position: relative;
top: -3px;
}
.items .badge {
display: inline-block;
font-size: small;
color: white;
padding: 0.8em 0.7em 0 0.7em;
background-color: #607D8B;
line-height: 1em;
position: relative;
left: -1px;
top: -4px;
height: 1.8em;
margin-right: .8em;
border-radius: 4px 0 0 4px;
}
/* everywhere else */
* {
font-family: Arial, Helvetica, sans-serif;
}
/*!* Master Styles *!*/
/*h1 {*/
/*color: #369;*/
/*font-family: Arial, Helvetica, sans-serif;*/
/*font-size: 250%;*/
/*}*/
/*h2, h3 {*/
/*color: #444;*/
/*font-family: Arial, Helvetica, sans-serif;*/
/*font-weight: lighter;*/
/*}*/
/*body {*/
/*margin: 2em;*/
/*}*/
/*body, input[text], button {*/
/*color: #888;*/
/*font-family: Cambria, Georgia;*/
/*}*/
/*a {*/
/*cursor: pointer;*/
/*cursor: hand;*/
/*}*/
/*button {*/
/*font-family: Arial;*/
/*background-color: #eee;*/
/*border: none;*/
/*padding: 5px 10px;*/
/*border-radius: 4px;*/
/*cursor: pointer;*/
/*cursor: hand;*/
/*}*/
/*button:hover {*/
/*background-color: #cfd8dc;*/
/*}*/
/*button:disabled {*/
/*background-color: #eee;*/
/*color: #aaa;*/
/*cursor: auto;*/
/*}*/
/*label {*/
/*padding-right: 0.5em;*/
/*}*/
/*!* Navigation link styles *!*/
/*nav a {*/
/*padding: 5px 10px;*/
/*text-decoration: none;*/
/*margin-right: 10px;*/
/*margin-top: 10px;*/
/*display: inline-block;*/
/*background-color: #eee;*/
/*border-radius: 4px;*/
/*}*/
/*nav a:visited, a:link {*/
/*color: #607D8B;*/
/*}*/
/*nav a:hover {*/
/*color: #039be5;*/
/*background-color: #CFD8DC;*/
/*}*/
/*nav a.active {*/
/*color: #039be5;*/
/*}*/
/*!* items class *!*/
/*.items {*/
/*margin: 0 0 2em 0;*/
/*list-style-type: none;*/
/*padding: 0;*/
/*width: 24em;*/
/*}*/
/*.items li {*/
/*cursor: pointer;*/
/*position: relative;*/
/*left: 0;*/
/*background-color: #EEE;*/
/*margin: .5em;*/
/*padding: .3em 0;*/
/*height: 1.6em;*/
/*border-radius: 4px;*/
/*}*/
/*.items li:hover {*/
/*color: #607D8B;*/
/*background-color: #DDD;*/
/*left: .1em;*/
/*}*/
/*.items li.selected {*/
/*background-color: #CFD8DC;*/
/*color: white;*/
/*}*/
/*.items li.selected:hover {*/
/*background-color: #BBD8DC;*/
/*}*/
/*.items .text {*/
/*position: relative;*/
/*top: -3px;*/
/*}*/
/*.items .badge {*/
/*display: inline-block;*/
/*font-size: small;*/
/*color: white;*/
/*padding: 0.8em 0.7em 0 0.7em;*/
/*background-color: #607D8B;*/
/*line-height: 1em;*/
/*position: relative;*/
/*left: -1px;*/
/*top: -4px;*/
/*height: 1.8em;*/
/*margin-right: .8em;*/
/*border-radius: 4px 0 0 4px;*/
/*}*/
/*!* everywhere else *!*/
/** {*/
/*font-family: Arial, Helvetica, sans-serif;*/
/*}*/