diff --git a/dart_test.yaml b/dart_test.yaml new file mode 100644 index 0000000..a7c6d02 --- /dev/null +++ b/dart_test.yaml @@ -0,0 +1,10 @@ +platforms: [chrome] + +tags: + nobrowser: + + bad: + + sad: + + happy: diff --git a/lib/src/Engine.dart b/lib/src/Engine.dart index b06e53a..84d52e8 100644 --- a/lib/src/Engine.dart +++ b/lib/src/Engine.dart @@ -1,7 +1,7 @@ import 'dart:html' as html; import 'dart:math'; -import 'package:rules_of_living/src/Grid.dart'; +import 'package:rules_of_living/src/Simulation.dart'; class Engine { // Elapsed Time Counter - useful for Safety Timeout @@ -36,7 +36,7 @@ class Engine { void set gridSize(Point value) { if (value.x <= 0 || value.y <= 0) throw ArgumentError("grid size must not be smaller than 1"); - _grid = Grid(value.x, value.y); + _grid = Simulation(value.x, value.y); } num _updateLag = 0.0; @@ -48,11 +48,11 @@ class Engine { /// be used if no canvas was defined at engine creation and it should be /// rendered later. html.CanvasElement canvas; - Grid _grid; + Simulation _grid; bool running = false; Engine([x = 100, y = 100, this.canvas]) { - _grid = Grid(x, y); + _grid = Simulation(x, y); _elapsed.start(); _grid.addPattern(amount: 15, dispersal: 5); @@ -70,7 +70,7 @@ class Engine { } void clear() { - _grid = new Grid(gridSize.x, gridSize.y); + _grid = new Simulation(gridSize.x, gridSize.y); running = false; } diff --git a/lib/src/Grid.dart b/lib/src/Grid.dart index d95c25e..e2a62fc 100644 --- a/lib/src/Grid.dart +++ b/lib/src/Grid.dart @@ -1,222 +1,68 @@ -import 'dart:html' as html; -import 'dart:math' as math; +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'; -enum CellPattern { SpaceShip, Blinker } +class Grid extends DelegatingList { + final List _internal; + final width; + final height; -class Grid { - final int w; - final int h; - final List> map; + Grid(int width, int height) : this._(List(width * height), width, height); - bool _dirty = true; - bool _renderEdges = true; + Grid.fill(int width, int height, E fillValue) + : this._(List.filled(width * height, fillValue), width, height); - int _startingSeed; - int _x; - int _y; - int _amount; - int _dispersal; - CellPattern _pattern; + Grid.from(Grid l) + : this._(List.from(l.getRange(0, l.length)), l.width, l.height); - Grid(int w, int h) - : this.w = w, - this.h = h, - this.map = new List() { - map.addAll(_buildGrid(w, h)); + Grid.fromList(List 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 reset() { - map.setAll(0, _buildGrid(w, h)); - if (_startingSeed != null) - addPattern( - pattern: _pattern, - dispersal: _dispersal, - amount: _amount, - seed: _startingSeed, - x: _x, - y: _y); - _dirty = true; + /// 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 addPattern( - {CellPattern pattern, - int x, - int y, - int amount, - int dispersal, - int seed}) { - _startingSeed = seed ?? DateTime.now().millisecondsSinceEpoch; - math.Random rng = new math.Random(_startingSeed); - _x = x; - _y = y; - _amount = amount ?? rng.nextInt(20); - _dispersal = dispersal ?? 10; - _pattern = pattern; - int cx = x ?? rng.nextInt(w ~/ 3) + (w ~/ 3); - int cy = y ?? rng.nextInt(h ~/ 3) + (h ~/ 3); - switch (pattern) { - // Two blocks, offset - // ## - // ## - case CellPattern.Blinker: - setCellState(cx, cy, true); - setCellState(cx + 1, cy, true); - setCellState(cx, cy + 1, true); - setCellState(cx + 1, cy + 1, true); + /// 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; - setCellState(cx + 2, cy + 2, true); - setCellState(cx + 3, cy + 2, true); - setCellState(cx + 2, cy + 3, true); - setCellState(cx + 3, cy + 3, true); - break; - // A 'gliding' Spaceship - // # - // # - // ### - case CellPattern.SpaceShip: - setCellState(1 + cx, 0 + cy, true); - setCellState(2 + cx, 1 + cy, true); - setCellState(2 + cx, 2 + cy, true); - setCellState(1 + cx, 2 + cy, true); - setCellState(0 + cx, 2 + cy, true); - break; - default: - int sanityCheck = 0; - for (var i = 0; i < (_amount); i++) { - sanityCheck++; - getCellState(cx, cy) - ? i-- - : setCellState(cx + rng.nextInt(_dispersal), - cy + rng.nextInt(_dispersal), true); - if (sanityCheck > 100 && sanityCheck > i * 3) break; - } - break; - } - _dirty = true; - } - - void setCellState(int x, int y, bool state) { - if (y < map.length && x < map[y].length) map[y][x].state = state; - } - - bool getCellState(int x, int y) { - if (y < map.length && x < map[y].length) return map[y][x].state; - return null; - } - - List> _buildGrid(int w, int h) { - print("grid being created"); - List> 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; - }); - - // 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(threeTrue); - cell.surviveRules.add(twoTrue); - cell.birthRules.add(threeTrue); - - grid[y][x] = cell; - } - } - return grid; - } - - bool update() { - bool stateChanges = false; - 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)); - } - } - for (int y = 0; y < h; y++) { - for (int x = 0; x < w; x++) { - Cell c = map[y][x]; - if (c.state != c.nextState) stateChanges = true; - c.advanceState(); - - if (!_dirty && map[y][x].dirty) _dirty = true; - } - } - return stateChanges; - } - - 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++) { - if (_renderEdges) { - ctx.setStrokeColorRgb(100, 100, 100); - ctx.strokeRect(x * brickW, y * brickH, brickW, brickH); - } - - 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; - } - - void set renderEdges(bool on) { - _renderEdges = on; - _dirty = true; - } - - bool get renderEdges => _renderEdges; + /// 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 toCoordinates(int index) => (index < 0) + ? throw RangeError("Index for Grid Coordinates must not be negative") + : Point(index % width, index ~/ width); } diff --git a/lib/src/Simulation.dart b/lib/src/Simulation.dart new file mode 100644 index 0000000..ca7bb71 --- /dev/null +++ b/lib/src/Simulation.dart @@ -0,0 +1,189 @@ +import 'dart:html' as html; +import 'dart:math' as math; + +import 'package:rules_of_living/src/Cell.dart'; +import 'package:rules_of_living/src/Grid.dart'; +import 'package:rules_of_living/src/Rule.dart'; + +enum CellPattern { SpaceShip, Blinker } + +class Simulation { + final Grid map; + + bool _dirty = true; + bool _renderEdges = true; + + int _startingSeed; + int _x; + int _y; + int _amount; + int _dispersal; + CellPattern _pattern; + + int get w => map.width; + int get h => map.height; + + Simulation(int w, int h) : this.map = new Grid(w, h) { + for (int i = 0; i < map.length; i++) { + map[i] = _getGOLCell(); + } + print("Grid creation finished"); + } + + Cell _getGOLCell([bool defaultState = false]) { + Cell cell = Cell(defaultState); + Rule threeTrue = new Rule((int n) { + if (n == 3) return true; + return false; + }); + Rule twoTrue = new Rule((int n) { + if (n == 2) return true; + return false; + }); + cell.surviveRules.add(twoTrue); + cell.surviveRules.add(threeTrue); + cell.birthRules.add(threeTrue); + return cell; + } + + void reset() { + map.setAll(0, List.filled(map.length, _getGOLCell())); + if (_startingSeed != null) + addPattern( + pattern: _pattern, + dispersal: _dispersal, + amount: _amount, + seed: _startingSeed, + x: _x, + y: _y); + _dirty = true; + } + + void addPattern( + {CellPattern pattern, + int x, + int y, + int amount, + int dispersal, + int seed}) { + _startingSeed = seed ?? DateTime.now().millisecondsSinceEpoch; + math.Random rng = new math.Random(_startingSeed); + _x = x; + _y = y; + _amount = amount ?? rng.nextInt(20); + _dispersal = dispersal ?? 10; + _pattern = pattern; + int cx = x ?? rng.nextInt(map.width ~/ 3) + (map.width ~/ 3); + int cy = y ?? rng.nextInt(map.height ~/ 3) + (map.height ~/ 3); + switch (pattern) { + // Two blocks, offset + // ## + // ## + case CellPattern.Blinker: + setCellState(cx, cy, true); + setCellState(cx + 1, cy, true); + setCellState(cx, cy + 1, true); + setCellState(cx + 1, cy + 1, true); + + setCellState(cx + 2, cy + 2, true); + setCellState(cx + 3, cy + 2, true); + setCellState(cx + 2, cy + 3, true); + setCellState(cx + 3, cy + 3, true); + break; + // A 'gliding' Spaceship + // # + // # + // ### + case CellPattern.SpaceShip: + setCellState(1 + cx, 0 + cy, true); + setCellState(2 + cx, 1 + cy, true); + setCellState(2 + cx, 2 + cy, true); + setCellState(1 + cx, 2 + cy, true); + setCellState(0 + cx, 2 + cy, true); + break; + default: + int sanityCheck = 0; + for (var i = 0; i < (_amount); i++) { + sanityCheck++; + getCellState(cx, cy) + ? i-- + : setCellState(cx + rng.nextInt(_dispersal), + cy + rng.nextInt(_dispersal), true); + if (sanityCheck > 100 && sanityCheck > i * 3) break; + } + break; + } + _dirty = true; + } + + void setCellState(int x, int y, bool state) { + if (y < map.height && x < map.width) map.get(x, y).state = state; + } + + bool getCellState(int x, int y) { + if (y < map.height && x < map.width) return map.get(x, y).state; + return null; + } + + bool update() { + bool stateChanges = false; + + for (int i = 0; i < map.length; i++) { + math.Point p = map.toCoordinates(i); + map[i].update(getSurroundingNeighbors(p.x, p.y, 1)); + } + // TODO when implementing changeSet we can remove this second loop and add to changeSet in the first + map.forEach((Cell el) { + if (el.state != el.nextState) stateChanges = true; + el.advanceState(); + }); + stateChanges ? _dirty = true : false; + return stateChanges; + } + + int getSurroundingNeighbors(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 && + map.get(ix, iy).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.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].state == 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; + } + + bool get renderEdges => _renderEdges; +} diff --git a/test/src/grid_test.dart b/test/src/grid_test.dart new file mode 100644 index 0000000..fce1b3f --- /dev/null +++ b/test/src/grid_test.dart @@ -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 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 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 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"]); + }); +}