Compare commits

..

No commits in common. "master" and "change-to-angular-dart" have entirely different histories.

38 changed files with 405 additions and 1375 deletions

View file

@ -1,21 +1,2 @@
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.
Copyright (C) 2018

View file

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

View file

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

View file

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

View file

@ -1,35 +1,47 @@
import 'package:angular/angular.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';
import 'dart:html' as html;
import 'package:rules_of_living/src/App.dart';
@Component(
selector: 'my-app',
templateUrl: "app_component.html",
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'
],
directives: [coreDirectives]
)
class AppComponent {
class AppComponent implements OnInit {
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,10 +1,20 @@
<app_header></app_header>
<div id="wrapper">
<div id="viewport">
<gol-simulation></gol-simulation>
<sim-controls></sim-controls>
<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="sidebar">
<configuration></configuration>
<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">
</div>

View file

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

View file

@ -1,54 +0,0 @@
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

@ -1,21 +0,0 @@
<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

@ -1,48 +0,0 @@
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

@ -1,13 +0,0 @@
<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

@ -1,16 +0,0 @@
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

@ -1,12 +0,0 @@
<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

@ -1,36 +0,0 @@
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

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

View file

@ -1,48 +0,0 @@
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

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

66
lib/src/App.dart Normal file
View file

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

32
lib/src/Cell.dart Normal file
View file

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

View file

@ -1,116 +0,0 @@
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,68 +1,136 @@
import 'dart:core';
import 'dart:math';
import 'dart:html' as html;
import 'package:collection/collection.dart';
import 'package:rules_of_living/src/Cell.dart';
import 'package:rules_of_living/src/Rule.dart';
class Grid<E> extends DelegatingList<E> {
final List<E> _internal;
final width;
final height;
class Grid {
final int w;
final int h;
final List<List<Cell>> map;
Grid(int width, int height) : this._(List<E>(width * height), width, height);
bool _dirty = true;
Grid.fill(int width, int height, E fillValue)
: this._(List<E>.filled(width * height, fillValue), width, height);
Grid(int w, int h)
: this.w = w,
this.h = h,
this.map = new List() {
map.addAll(_buildGrid(w, h));
Grid.from(Grid<E> l)
: this._(List<E>.from(l.getRange(0, l.length)), l.width, l.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.fromList(List<E> l, int width) : this._(l, width, l.length ~/ width);
// 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._(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];
print("Grid creation finished");
}
/// 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;
void setState(int x, int y, bool state) {
if (y < map.length && x < map[y].length) map[y][x].state = state;
}
/// 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;
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 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);
// 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;
}
}

5
lib/src/Rule.dart Normal file
View file

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

View file

@ -1,163 +0,0 @@
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

@ -1,17 +0,0 @@
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

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

@ -1,9 +0,0 @@
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,16 +5,14 @@ author: Marty Oehme <marty.oehme@gmail.com>
homepage: https://www.martyoehme.org/
environment:
sdk: '>=2.0.0'
sdk: '>=2.0.0-dev.66.0 <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.3+1
build_test: ^0.10.2
build_web_compilers: ^0.4.0
test: ^1.3.0
mockito: ^3.0.0
test: ^1.0.0

View file

@ -1,41 +0,0 @@
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

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

View file

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

View file

@ -1,189 +0,0 @@
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

@ -1,19 +0,0 @@
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

@ -1,27 +0,0 @@
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,10 +15,11 @@
<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,126 +1,117 @@
@import url(https://fonts.googleapis.com/css?family=Roboto);
@import url(https://fonts.googleapis.com/css?family=Material+Icons);
html, body {
height: 100%;
margin: 0;
padding: 0;
width: 100%;
font-family: 'Roboto', 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;
}
/*!* 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;*/
/*}*/
/* 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;
}