diff options
| -rw-r--r-- | README | 5 | ||||
| -rw-r--r-- | source/app.d | 132 | ||||
| -rw-r--r-- | source/board.d | 111 | ||||
| -rw-r--r-- | source/lru.d | 63 | ||||
| -rw-r--r-- | source/portals.d | 152 | ||||
| -rw-r--r-- | source/spots.d | 119 | 
6 files changed, 345 insertions, 237 deletions
| @@ -19,3 +19,8 @@ arrow keys are for movement and [space] picks up and sets down rocks.  ## License  This code is made available under the GPLv3 license. + +## TODO + +Escape key for the save and load text queries +Multiverse!?!? diff --git a/source/app.d b/source/app.d index 1d04fc9..7faaa75 100644 --- a/source/app.d +++ b/source/app.d @@ -2,12 +2,12 @@ import deimos.ncurses;  import std.string : toStringz;  import core.stdc.locale;  import std.format; +import std.stdio;  import board;  import spots; -import std.stdio; +import portals; -void main() -{ +void main() {      setlocale(LC_CTYPE, ""); // ncurses locales are weird      auto stdscr = initscr(); @@ -20,19 +20,23 @@ void main()      scrollok(stdscr, true); // allows scrolling in up/down arrows      nonl(); // "return" goes through input -    auto overlay = new OverlaySource(new RandomSource()); -    auto board = new Board(overlay); +    auto source = new OverlaySource(new RandomSource()); +    auto portals = new Portals(source); +    auto board = new Board(source);      auto disp = new BoardDisplay(board, stdscr);      disp.print();      int rocks = 0; -    disp.status = "0"; +    disp.status = "Press [space] to pick up and set down rocks";      refresh(); -    outer: while (true) -    { +    import std.bigint : BigInt; + +    BigInt[][int] portal; +    int lastPortalId; + +    outer: while (true) {          auto c = getch(); -        switch (c) -        { +        switch (c) {          case ',':          case KEY_UP:              disp.up(); @@ -53,16 +57,13 @@ void main()              disp.getdims();              break;          case ' ': -            if (overlay[disp.y, disp.x].isRock()) -            { -                overlay[disp.y, disp.x] = Spot(' '); +            if (source[disp.y, disp.x].isRock()) { +                source[disp.y, disp.x] = Spot(' ');                  rocks++;                  disp.status = format("%d", rocks);                  disp.print(); -            } -            else if (rocks > 0) -            { -                overlay[disp.y, disp.x] = Spot('*'); +            } else if (rocks > 0 && source[disp.y, disp.x].isSpace()) { +                source[disp.y, disp.x] = Spot('*');                  rocks--;                  disp.status = format("%d", rocks);                  disp.print(); @@ -72,38 +73,60 @@ void main()              break outer;          case 's':              auto filename = stdscr.readquery("Save:"); -            try -            { -                auto file = File(filename, "wb"); -                file.writefln!"%s %s"(disp.y, disp.x); -                file.writefln!"%d"(rocks); -                overlay.save(file); -                disp.print(); -            } -            catch (Exception e) -            { +            if (filename) { +                try { +                    auto file = File(filename, "wb"); +                    file.writefln!"%s %s"(disp.y, disp.x); +                    file.writefln!"%d"(rocks); +                    source.save(file); +                    portals.save(file); +                } catch (Exception e) { +                }              }              break;          case 'l':              auto filename = stdscr.readquery("Load:"); -            try -            { -                import std.algorithm.iteration : splitter, map; -                import std.conv : to; -                import std.bigint; +            if (filename) { +                try { +                    import std.algorithm.iteration : splitter, map; +                    import std.conv : to; -                auto file = File(filename); -                auto coord = file.readln.splitter().map!(to!BigInt); -                disp.y = coord.front; -                coord.popFront; -                disp.x = coord.front; -                file.readf!"%d\n"(rocks); -                disp.status = format("%d", rocks); -                overlay.load(file); +                    auto file = File(filename); +                    auto coord = file.readln.splitter().map!(to!BigInt); +                    disp.y = coord.front; +                    coord.popFront; +                    disp.x = coord.front; +                    file.readf!"%d\n"(rocks); +                    disp.status = format("%d", rocks); +                    source.load(file); +                    portals.load(file); +                } catch (Exception e) { +                }              } -            catch (Exception e) -            { -                printw("exception!"); +            disp.print(); +            break; +        case '/': +            auto sp = source[disp.y, disp.x]; +            if (sp.isSpace) { +                if (rocks >= 10) { +                    rocks -= 10; +                    portals.add(disp.y, disp.x); +                } +            } else if (sp.isPortal) { +                rocks += 10; +                portals.remove(disp.y, disp.x); +            } +            disp.status = format("%d", rocks); +            disp.print(); +            break; +        case 'g': +            auto sp = source[disp.y, disp.x]; +            if (sp.isPortal) { +                auto dest = portals.get(sp.id).pair; +                if (dest != null) { +                    disp.y = dest.y; +                    disp.x = dest.x; +                }              }              disp.print();              break; @@ -113,8 +136,9 @@ void main()      }  } -string readquery(WINDOW* stdscr, string query) -{ +immutable char ESC = 27; + +string readquery(WINDOW* stdscr, string query) {      import std.range : replicate;      import std.conv : to;      import std.array : appender; @@ -127,31 +151,29 @@ string readquery(WINDOW* stdscr, string query)      move(height, cast(int) query.length + 1);      scrollok(stdscr, false); +    scope (success) +        scrollok(stdscr, true);      auto build = appender!(char[]);      int p = cast(int) query.length + 1; -    while (auto ch = getch()) -    { +    while (auto ch = getch()) {          if (ch == '\r' || ch == EOF)              break; -        if (ch == '\b' || ch == KEY_BACKSPACE || ch == 127) -        { -            if (build[].length > 0) -            { +        if (ch == '\b' || ch == KEY_BACKSPACE || ch == 127) { +            if (build[].length > 0) {                  p--;                  mvaddch(height, p, ' ');                  build.shrinkTo(build[].length - 1);                  move(height, p);              } -        } -        else -        { +        } else if (ch == ESC) { +            return null; +        } else {              build ~= cast(char) ch;              addch(ch);              p++;          }      } -    scrollok(stdscr, true);      return cast(string) build[];  } diff --git a/source/board.d b/source/board.d index 9ec996b..8460f71 100644 --- a/source/board.d +++ b/source/board.d @@ -8,84 +8,70 @@ import std.format;  import std.stdio;  import spots : Spot, SpotSource; -public class Board -{ +public class Board {      private SpotSource _source;      // uint seed;      // indexed as Spot = Board[y, x] -    this(SpotSource source) -    { +    this(SpotSource source) {          _source = source;      } -    Spot opIndex(BigInt y, BigInt x) -    { +    Spot opIndex(BigInt y, BigInt x) {          return _source[y, x];      } -    Spot opIndex(long y, long x) -    { +    Spot opIndex(long y, long x) {          // possible per improvements          return opIndex(BigInt(y), BigInt(x));      } -    PartialBoard opIndex(BigInt[2] y, BigInt[2] x) -    { +    PartialBoard opIndex(BigInt[2] y, BigInt[2] x) {          return new PartialBoard(y, x, _source);      } -    PartialBoard opIndex(long[2] y, long[2] x) -    { +    PartialBoard opIndex(long[2] y, long[2] x) {          return opIndex([BigInt(y[0]), BigInt(y[1])], [                  BigInt(x[0]), BigInt(x[1])                  ]);      } -    PartialBoard opIndex(BigInt[2] y, BigInt x) -    { +    PartialBoard opIndex(BigInt[2] y, BigInt x) {          return opIndex(y, [x, x + 1]);      } -    PartialBoard opIndex(long[2] y, long x) -    { +    PartialBoard opIndex(long[2] y, long x) {          return opIndex([BigInt(y[0]), BigInt(y[1])], BigInt(x));      } -    PartialBoard opIndex(BigInt y, BigInt[2] x) -    { +    PartialBoard opIndex(BigInt y, BigInt[2] x) {          return opIndex([y, y + 1], x);      } -    PartialBoard opIndex(long y, long[2] x) -    { +    PartialBoard opIndex(long y, long[2] x) {          return opIndex(BigInt(y), [BigInt(x[0]), BigInt(x[1])]);      }      BigInt[2] opSlice(size_t dimension)(BigInt x, BigInt y) -            if (dimension >= 0 && dimension < 2) -    { +            if (dimension >= 0 && dimension < 2) {          return [x, y];      }      long[2] opSlice(size_t dimension)(long x, long y) -            if (dimension >= 0 && dimension < 2) -    { +            if (dimension >= 0 && dimension < 2) {          return [x, y];      }      // be more generic. instead of long use T where IsIntegral!T  } -enum Edge -{ +enum Edge {      LEFT,      RIGHT,      TOP,      BOT,  } -class BoardDisplay -{ +class BoardDisplay {      private Board _board;      private WINDOW* _window;      private int _height, _width; @@ -93,39 +79,32 @@ class BoardDisplay      BigInt x = 0;      BigInt y = 0; -    this(Board board, WINDOW* window) -    { +    this(Board board, WINDOW* window) {          _board = board;          _window = window;          getdims();      } -    public void getdims() -    { +    public void getdims() {          getmaxyx(_window, _height, _width);      } -    private void center() -    { +    private void center() {          mvprintw(_height, 0, toStringz(_status ~ " ".replicate(_width - status.length)));          move(cast(int) ceil(_height / 2.0), cast(int) ceil(_width / 2.0));      } -    @property string status() -    { +    @property string status() {          return _status;      } -    @property status(string s) -    { +    @property status(string s) {          _status = s;          center();      } -    private BigInt edge(Edge e) -    { // templating?? -        switch (e) -        { +    private BigInt edge(Edge e) { // templating?? +        switch (e) {          case Edge.LEFT:              return BigInt(cast(int) floor(-_width / 2.0)) + x;          case Edge.RIGHT: @@ -139,8 +118,7 @@ class BoardDisplay          }      } -    void print() -    { +    void print() {          import std.stdio;          import std.datetime; @@ -149,8 +127,7 @@ class BoardDisplay          center();      } -    void up() -    { // the cursor moves up, the map scrolls down, leaving an empty line at top +    void up() { // the cursor moves up, the map scrolls down, leaving an empty line at top          y--;          scrl(-1);          mvprintw(0, 0, toStringz(_board[edge(Edge.TOP), @@ -158,8 +135,7 @@ class BoardDisplay          center();      } -    void down() -    { +    void down() {          y++;          scrl(1);          mvprintw(_height - 1, 0, toStringz(_board[edge(Edge.BOT) - 1, @@ -168,21 +144,18 @@ class BoardDisplay          center();      } -    void left() -    { +    void left() {          x--;          print();      } -    void right() -    { +    void right() {          x++;          print();      }  } -unittest -{ +unittest {      import std.stdio;      import std.datetime;      import lru : LRUCache; @@ -191,47 +164,37 @@ unittest      SysTime starttime = Clock.currTime();      auto random = new RandomSource();      auto source = new OverlaySource(random); -    foreach (j; iota(100)) -    { -        foreach (i; iota(400)) -        { +    foreach (j; iota(100)) { +        foreach (i; iota(400)) {              source[BigInt(i), BigInt(0)];          }      }      writeln(Clock.currTime() - starttime); -    foreach (j; iota(100)) -    { -        foreach (i; iota(400)) -        { +    foreach (j; iota(100)) { +        foreach (i; iota(400)) {              random[BigInt(i), BigInt(0)];          }      }      writeln(Clock.currTime() - starttime);  } -class PartialBoard : Board -{ +class PartialBoard : Board {      private BigInt[2] _yrange, _xrange; -    this(BigInt[2] y, BigInt[2] x, SpotSource source) -    { +    this(BigInt[2] y, BigInt[2] x, SpotSource source) {          _yrange = y;          _xrange = x;          super(source);      } -    override Spot opIndex(BigInt y, BigInt x) -    { +    override Spot opIndex(BigInt y, BigInt x) {          return _source[y + _yrange[0], x + _xrange[0]];      } -    override string toString() -    { +    override string toString() {          auto strBuilder = appender!string;          strBuilder.reserve(cast(ulong)((_yrange[1] - _yrange[0] + 2) * (_xrange[1] - _xrange[0] + 1))); -        foreach (y; iota(_yrange[0], _yrange[1])) -        { -            foreach (x; iota(_xrange[0], _xrange[1])) -            { +        foreach (y; iota(_yrange[0], _yrange[1])) { +            foreach (x; iota(_xrange[0], _xrange[1])) {                  strBuilder.put(_source[y, x].contents);              }              strBuilder.put("\n"); diff --git a/source/lru.d b/source/lru.d index 6d802df..2e8da73 100644 --- a/source/lru.d +++ b/source/lru.d @@ -1,22 +1,18 @@  import std.array : appender; -private struct Node(T) -{ +private struct Node(T) {      Node!T* prev;      Node!T* next;      T data;  } -private struct List(T) -{ +private struct List(T) {      Node!T* head;      Node!T* tail;      size_t length; -    invariant () -    { -        if (length > 0) -        { +    invariant () { +        if (length > 0) {              assert(head !is null);              assert(tail !is null);          } @@ -37,8 +33,7 @@ private struct List(T)          }      }*/ -    void remove(Node!T* n) -    { +    void remove(Node!T* n) {          if (n.next)              n.next.prev = n.prev;          if (n.prev) @@ -50,8 +45,7 @@ private struct List(T)          length--;      } -    void push(Node!T* n) -    { +    void push(Node!T* n) {          n.prev = null;          n.next = head;          head = n; @@ -63,12 +57,10 @@ private struct List(T)      }      Node!T* pop() -    in -    { +    in {          assert(tail);      } -    do -    { +    do {          Node!T* node = tail;          remove(node);          return node; @@ -76,8 +68,7 @@ private struct List(T)  } -unittest -{ +unittest {      List!int l;      Node!int first;      l.push(&first); @@ -95,60 +86,49 @@ unittest      l.pop();  } -class LRUCache(From, To) -{ +class LRUCache(From, To) {      private size_t _cap; -    private struct Record -    { +    private struct Record {          To val;          From key;      }      private Node!Record*[From] map;      private List!Record queue; -    this(size_t cap) -    { +    this(size_t cap) {          _cap = cap;      } -    private void checksz() -    { -        if (queue.length > _cap) -        { +    private void checksz() { +        if (queue.length > _cap) {              auto node = queue.pop();              map.remove(node.data.key);          }      } -    bool opBinaryRight(string op : "in")(From key) -    { +    bool opBinaryRight(string op : "in")(From key) {          return cast(bool)(key in map);      }      To opIndex(From f) -    in -    { +    in {          assert(f in map);      } -    do -    { +    do {          auto p = map[f];          queue.remove(p);          queue.push(p);          return p.data.val;      } -    void opIndexAssign(To t, From f) -    { +    void opIndexAssign(To t, From f) {          auto p = f in map;          Node!Record* node;          bool alloc = p is null; -        if (!alloc) -        { +        if (!alloc) {              queue.remove(*p);              node = *p; -        } -        else +        } else              node = new Node!Record();          node.data.val = t;          node.data.key = f; @@ -159,8 +139,7 @@ class LRUCache(From, To)      }  } -unittest -{ +unittest {      auto cache = new LRUCache!(int, int)(2);      cache[0] = 1;      cache[4] = 2; diff --git a/source/portals.d b/source/portals.d new file mode 100644 index 0000000..7bb5dc2 --- /dev/null +++ b/source/portals.d @@ -0,0 +1,152 @@ +import std.bigint : BigInt; +import spots : OverlaySource, Spot; +import std.stdio; +import std.format; + +struct Portal { +    BigInt y; +    BigInt x; +    Portal* pair; +    this(BigInt y, BigInt x) { +        this.y = y; +        this.x = x; +    } + +    void pairWith(ref Portal p) { +        this.pair = &p; +        p.pair = &this; +    } + +    void buildMap(ref int[Portal* ] idMap, ref int highId) { +        idMap[&this] = highId++; +    } + +    string toString(int[Portal* ] idMap) +    in { +        assert(pair in idMap); +    } +    do { +        return format!`%s, %s: %d`(y, x, idMap[pair]); +    } + +    void fromString(string s, Portal[int] portalsById) { +        import std.conv : to; +        import std.algorithm : until, findSkip; +        import std.array : array; + +        this.y = to!BigInt(s.until(',').array); +        s.findSkip(", "); +        this.x = to!BigInt(s.until(':').array); +        s.findSkip(": "); +        int id; +        s.formattedRead!"%d"(id); +        auto p = id in portalsById; +        if (p) { +            p.pairWith(this); // this is a REALLY big bug. Portal() is +            // being copied by value into the dict, +            // so it's not using its final ref. +        } + +    } +} + +class Portals { +    private Portal[int] _portals; +    private Portal* _lastPortal; +    private int _nextId; +    private OverlaySource _overlay; + +    this(OverlaySource overlay) { +        _overlay = overlay; +    } + +    int add(BigInt y, BigInt x) { +        _portals[_nextId] = Portal(y, x); +        if (_lastPortal && _lastPortal.pair == null) { +            _lastPortal.pairWith(_portals[_nextId]); +        } +        _lastPortal = &(_portals[_nextId]); +        _overlay[y, x] = Spot('/', _nextId); +        return _nextId++; +    } + +    Portal get(int id) +    in { +        assert(id in _portals); +    } +    do { +        return _portals[id]; +    } + +    void remove(BigInt y, BigInt x) { +        auto id = _overlay[y, x].id; +        auto pair = _portals[id].pair; +        if (pair) { +            if (_lastPortal.pair == null) { +                _lastPortal.pairWith(*pair); +            } else { +                pair.pair = null; +            } +        } +        _lastPortal = _portals[id].pair; +        _portals.remove(id); +        _overlay[y, x] = Spot(' '); +    } + +    immutable string terminator = "END PORTAL DATA\n"; +    void save(File f) { +        int id = 0; +        int[Portal* ] idMap; +        foreach (ref key; _portals.byKey()) { +            (key in _portals).buildMap(idMap, id); +        } +        foreach (ref key; _portals.byKey()) { +            auto portalRef = key in _portals; +            f.writefln!`%d @ %s`(idMap[portalRef], portalRef.toString(idMap)); +        } + +        f.write(terminator); +        if (_lastPortal) +            f.writefln!`%d`(idMap[_lastPortal]); +    } + +    void load(File f) { +        _nextId = 0; +        foreach (char[] line; f.lines) { +            if (line == terminator) +                break; + +            int id; +            string portalText; +            line.formattedRead!`%d @ %s`(id, portalText); +            _portals[id] = Portal(); +            _portals[id].fromString(portalText, _portals); +            _nextId++; +        } +        int id; +        if (_nextId > 0) { +            f.readf("%d\n", id); +            _lastPortal = &(_portals[id]); +        } +    } + +    unittest { +        import spots : RandomSource; +        import std.stdio; + +        auto overlay = new OverlaySource(new RandomSource()); +        auto portals = new Portals(overlay); + +        portals.add(BigInt(0), BigInt(0)); +        portals.remove(BigInt(0), BigInt(0)); +        int orig = portals.add(BigInt(0), BigInt(0)); +        int sec = portals.add(BigInt(0), BigInt(1)); +        assert(*(portals.get(orig).pair) == portals.get(sec)); +        portals.remove(BigInt(0), BigInt(1)); +        sec = portals.add(BigInt(0), BigInt(2)); +        assert(*(portals.get(orig).pair) == portals.get(sec)); +        orig = portals.add(BigInt(0), BigInt(3)); +        portals.remove(BigInt(0), BigInt(0)); +        assert(*(portals.get(orig).pair) == portals.get(sec)); +    } +} diff --git a/source/spots.d b/source/spots.d index 49b9c85..a0f549e 100644 --- a/source/spots.d +++ b/source/spots.d @@ -6,94 +6,96 @@ import std.stdio;  import lru : LRUCache;  import std.traits : isSomeString; -private BigInt positive(BigInt n) -{ +private BigInt positive(BigInt n) {      if (n >= 0)          return n * 2;      else          return -n * 2 - 1;  } -private BigInt pair(BigInt n, BigInt m) -{ +private BigInt pair(BigInt n, BigInt m) {      // https://stackoverflow.com/a/919661      return (positive(n) + positive(m)) * (positive(n) + positive(m) + 1) / 2 + positive(m);  } -struct Spot -{ +struct Spot {      private char _contents; -    this(char c) -    { +    private int _id; +    this(char c) {          _contents = c;      } +    this(char c, int id) { +        this(c); +        _id = id; +    } +      import std.format : format, formattedRead; -    string toString() -    { +    string toString() { -        return format!`'%c'`(_contents); +        return format!`'%c' %d`(_contents, _id);      } -    void fromString(Range)(auto ref Range s) -    { -        s.formattedRead!`'%c'`(_contents); +    void fromString(Range)(auto ref Range s) { +        s.formattedRead!`'%c' %d`(_contents, _id);      } -    @property contents() -    { +    @property contents() {          return _contents;      } -    bool opEqual(const Spot s) -    { +    @property id() { +        return _id; +    } + +    bool opEqual(const Spot s) {          return s._contents == _contents;      } -    bool isRock() -    { +    bool isSpace() { +        return _contents == ' '; +    } + +    bool isRock() {          return _contents == '*';      } + +    bool isPortal() { +        return _contents == '/'; +    }  } -interface SpotSource -{ +interface SpotSource {      Spot opIndex(BigInt y, BigInt x);      void save(File);      void load(File);  } -class RandomSource : SpotSource -{ +class RandomSource : SpotSource {      uint seed;      double density = .0025;      private LRUCache!(BigInt[2], Spot) cache; -    this() -    { +    this() {          seed = unpredictableSeed;          cache = new LRUCache!(BigInt[2], Spot)(50000);      } -    this(uint seed) -    { +    this(uint seed) {          this.seed = seed;      } -    this(uint seed, double density) -    { +    this(uint seed, double density) {          this.density = density;          this(seed);      } -    private uint hash(BigInt n) -    { +    private uint hash(BigInt n) {          CRC32 crc;          crc.start();          crc.put((cast(ubyte*)&seed)[0 .. seed.sizeof]); -        foreach (i; iota(n.ulongLength)) -        { +        foreach (i; iota(n.ulongLength)) {              auto digit = n.getDigit(i);              crc.put((cast(ubyte*)&digit)[0 .. digit.sizeof]);          } @@ -102,8 +104,7 @@ class RandomSource : SpotSource          return hash;      } -    public Spot opIndex(BigInt y, BigInt x) -    { +    public Spot opIndex(BigInt y, BigInt x) {          if ([y, x] in cache)              return cache[[y, x]];          auto roll = cast(double) hash(pair(y, x)) / uint.max; @@ -116,44 +117,35 @@ class RandomSource : SpotSource          return ret;      } -    public void save(File f) -    { +    public void save(File f) {          f.writefln!"%d %f"(seed, density);      } -    public void load(File f) -    { +    public void load(File f) {          f.readf!"%d %f\n"(seed, density);          cache = new LRUCache!(BigInt[2], Spot)(50000);      }  } -class OverlaySource : SpotSource -{ +class OverlaySource : SpotSource {      private SpotSource _base;      private Spot[BigInt[2]] _overlay;      immutable private string terminator = "END OVERLAY\n"; -    this(SpotSource base) -    { +    this(SpotSource base) {          _base = base;      } -    void opIndexAssign(Spot spot, BigInt y, BigInt x) -    { +    void opIndexAssign(Spot spot, BigInt y, BigInt x) {          Spot* p = [y, x] in _overlay; -        if (p !is null && spot == _base[y, x]) -        { +        if (p !is null && spot == _base[y, x]) {              _overlay.remove([y, x]); -        } -        else -        { +        } else {              _overlay[[y, x]] = spot;          }      } -    Spot opIndex(BigInt y, BigInt x) -    { +    Spot opIndex(BigInt y, BigInt x) {          Spot* p = [y, x] in _overlay;          if (p !is null)              return *p; @@ -161,25 +153,21 @@ class OverlaySource : SpotSource              return _base[y, x];      } -    void save(File f) -    { +    void save(File f) {          _base.save(f); -        foreach (coord; _overlay.keys()) -        { +        foreach (coord; _overlay.keys()) {              f.writefln!"%s %s: %s"(coord[0], coord[1], _overlay[coord]);          }          f.write(terminator);      } -    void load(File f) -    { +    void load(File f) {          import std.conv : to;          import std.algorithm;          //_overlay = _overlay.init;          _base.load(f); -        foreach (char[] line; f.lines) -        { +        foreach (char[] line; f.lines) {              if (line == terminator)                  break; @@ -189,14 +177,13 @@ class OverlaySource : SpotSource              ints.popFront;              coord[1] = ints.front; -            auto s = new Spot(); -            (*s).fromString(line.find(':').find('\'')); -            _overlay[coord] = *s; +            auto s = Spot(); +            s.fromString(line.find(':').find('\'')); +            _overlay[coord] = s;          }      } -    unittest -    { +    unittest {          auto base = new RandomSource(0, 0.0);          auto src = new OverlaySource(base);          src[BigInt(0), BigInt(0)] = Spot('*'); | 
