diff --git a/dev/lobster/language.vcxproj b/dev/lobster/language.vcxproj
index bc435c765..f895a23c1 100644
--- a/dev/lobster/language.vcxproj
+++ b/dev/lobster/language.vcxproj
@@ -341,6 +341,7 @@
+
diff --git a/dev/lobster/language.vcxproj.filters b/dev/lobster/language.vcxproj.filters
index 93c5950c6..989a894f8 100644
--- a/dev/lobster/language.vcxproj.filters
+++ b/dev/lobster/language.vcxproj.filters
@@ -136,5 +136,8 @@
common
+
+ common
+
\ No newline at end of file
diff --git a/dev/src/builtins.cpp b/dev/src/builtins.cpp
index 1885b4a6f..9db8d833b 100644
--- a/dev/src/builtins.cpp
+++ b/dev/src/builtins.cpp
@@ -17,6 +17,7 @@
#include "lobster/natreg.h"
#include "lobster/unicode.h"
+#include "lobster/wfc.h"
namespace lobster {
@@ -1007,6 +1008,36 @@ void AddBuiltins(NativeRegistry &natreg) {
" a vector of vectors of indices of the circles that are within dist of eachothers radius."
" pre-filter indicates objects that should appear in the inner vectors.");
+ STARTDECL(wave_function_collapse) (VM &vm, Value &tilemap, Value &size) {
+ auto sz = ValueDecToINT<2>(vm, size);
+ auto rows = tilemap.vval()->len;
+ vector inmap(rows);
+ intp cols = 0;
+ for (intp i = 0; i < rows; i++) {
+ auto sv = tilemap.vval()->At(i).sval()->strv();
+ if (i) { if ((intp)sv.size() != cols) vm.Error("all columns must be equal length"); }
+ else cols = sv.size();
+ inmap[i] = sv.data();
+ }
+ tilemap.DECRT(vm);
+ auto outstrings = ToValueOfVectorOfStringsEmpty(vm, sz, 0);
+ vector outmap(sz.y, nullptr);
+ for (int i = 0; i < sz.y; i++) outmap[i] = (char *)outstrings.vval()->At(i).sval()->data();
+ int num_contradictions = 0;
+ auto ok = WaveFunctionCollapse(int2(cols, inmap.size()), inmap.data(), sz, outmap.data(),
+ rnd, num_contradictions);
+ if (!ok)
+ vm.Error("tilemap contained too many tile ids");
+ vm.Push(outstrings);
+ return num_contradictions;
+ }
+ ENDDECL2(wave_function_collapse, "tilemap,size", "S]I}:2", "S]I",
+ "returns a tilemap of given size modelled after the possible shapes in the input"
+ " tilemap. Tilemap should consist of chars in the 0..127 range. Second return value"
+ " the number of failed neighbor matches, this should"
+ " ideally be 0, but can be non-0 for larger maps. Simply call this function"
+ " repeatedly until it is 0");
+
STARTDECL(resume) (VM &vm, Value &co, Value &ret) {
vm.CoResume(co.cval());
// By the time CoResume returns, we're now back in the context of co, meaning that the
diff --git a/dev/src/lobster/wfc.h b/dev/src/lobster/wfc.h
new file mode 100644
index 000000000..47dd04144
--- /dev/null
+++ b/dev/src/lobster/wfc.h
@@ -0,0 +1,211 @@
+// Copyright 2018 Wouter van Oortmerssen. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+
+// Very simple tile based Wave Function Collapse ("Simple Tiled Model") implementation.
+// See: https://github.com/mxgmn/WaveFunctionCollapse
+// Derives adjacencies from an example rather than explicitly specified neighbors.
+// Does not do any symmetries/rotations unless they're in the example.
+
+// Algorithm has a lot of similarities to A* in how its implemented.
+// Uses bitmasks to store the set of possible tiles, which currently limits the number of
+// unique tiles to 64. This restriction cool be lifted by using std::bitset instead.
+
+// In my testing, generates a 50x50 tile map in <1 msec. 58% of such maps are conflict free.
+// At 100x100 that is 3 msec and 34%.
+// At 200x200 that is 24 msec and 13%
+// At 400x400 that is 205 msec and ~1%
+// Algorithm may need to extended to flood more than 2 neighbor levels to make it suitable
+// for really gigantic maps.
+
+// inmap & outmap must point to row-major 2D arrays of the given size.
+// each in tile char must be in range 0..127, of which max 64 may actually be in use (may be
+// sparse).
+// Returns false if too many unique tiles in input.
+template bool WaveFunctionCollapse(const int2 &insize, const char **inmap,
+ const int2 &outsize, char **outmap,
+ RandomNumberGenerator &rnd,
+ int &num_contradictions) {
+ num_contradictions = 0;
+ typedef uint64_t bitmask_t;
+ const auto nbits = sizeof(bitmask_t) * 8;
+ array tile_lookup;
+ tile_lookup.fill(-1);
+ struct Tile { bitmask_t sides[4] = {}; size_t freq = 0; char tidx = 0; };
+ vector tiles;
+ int2 neighbors[] = { { 0, 1 }, { 1, 0 }, { 0, -1 }, { -1, 0 } };
+ // Collect unique tiles and their frequency of occurrence.
+ for (int iny = 0; iny < insize.y; iny++) {
+ for (int inx = 0; inx < insize.x; inx++) {
+ auto t = inmap[iny][inx];
+ if (tile_lookup[t] < 0) {
+ // We use a bitmask_t mask for valid neighbors.
+ if (tiles.size() == nbits - 1) return false;
+ tile_lookup[t] = (int)tiles.size();
+ tiles.push_back(Tile());
+ }
+ auto &tile = tiles[tile_lookup[t]];
+ tile.freq++;
+ tile.tidx = t;
+ }
+ }
+ // Construct valid neighbor bitmasks.
+ for (int iny = 0; iny < insize.y; iny++) {
+ for (int inx = 0; inx < insize.x; inx++) {
+ auto t = inmap[iny][inx];
+ auto &tile = tiles[tile_lookup[t]];
+ int ni = 0;
+ for (auto n : neighbors) {
+ auto p = (n + int2(inx, iny) + insize) % insize;
+ auto tn = inmap[p.y][p.x];
+ assert(tile_lookup[tn] >= 0);
+ tile.sides[ni] |= 1 << tile_lookup[tn];
+ ni++;
+ }
+ }
+ }
+ size_t most_common_tile_id = 0;
+ size_t most_common_tile_freq = 0;
+ for (auto &tile : tiles) if (tile.freq > most_common_tile_freq) {
+ most_common_tile_freq = tile.freq;
+ most_common_tile_id = &tile - &tiles[0];
+ }
+ // Track an open list (much like A*) of next options, sorted by best candidate at the end.
+ list> open, temp;
+ // Store a bitmask per output cell of remaining possible choices.
+ auto max_bitmask = (1 << tiles.size()) - 1;
+ enum class State : uchar { NEW, OPEN, CLOSED };
+ struct Cell {
+ bitmask_t wf;
+ uchar popcnt = 0;
+ State state = State::NEW;
+ decltype(open)::iterator it;
+ Cell(bitmask_t wf, uchar popcnt) : wf(wf), popcnt(popcnt) {}
+ };
+ vector> cells(outsize.y, vector(outsize.x, Cell(max_bitmask, tiles.size())));
+ auto start = rndivec(rnd, outsize);
+ open.push_back({ start, 0 }); // Start.
+ auto &scell = cells[start.y][start.x];
+ scell.state = State::OPEN;
+ scell.it = open.begin();
+ // Pick tiles until no more possible.
+ while (!open.empty()) {
+ // Simply picking the first list item results in the same chance of conflicts as
+ // random picks over equal options, but it is assumed the latter could generate more
+ // interesting maps.
+ size_t num_candidates = 1;
+ auto numopts_0 = cells[open.back().first.y][open.back().first.x].popcnt;
+ for (auto it = ++open.rbegin(); it != open.rend(); ++it)
+ if (numopts_0 == cells[it->first.y][it->first.x].popcnt &&
+ open.back().second == it->second)
+ num_candidates++;
+ else
+ break;
+ auto candidate_i = rnd(num_candidates);
+ auto candidate_it = --open.end();
+ for (int i = 0; i < candidate_i; i++) --candidate_it;
+ auto cur = candidate_it->first;
+ temp.splice(temp.end(), open, candidate_it);
+ auto &cell = cells[cur.y][cur.x];
+ assert(cell.state == State::OPEN);
+ cell.state = State::CLOSED;
+ bool contradiction = !cell.popcnt;
+ if (contradiction) {
+ num_contradictions++;
+ // Rather than failing right here, fill in the whole map as best as possible just in
+ // case a map with bad tile neighbors is still useful to the caller.
+ // As a heuristic lets just use the most common tile, as that will likely have the
+ // most neighbor options.
+ cell.wf = 1 << most_common_tile_id;
+ cell.popcnt = 1;
+ }
+ // From our options, pick one randomly, weighted by frequency of tile occurrence.
+ // First find total frequency.
+ size_t total_freq = 0;
+ for (size_t i = 0; i < tiles.size(); i++) if (cell.wf & (1 << i)) total_freq += tiles[i].freq;
+ auto freqpick = rnd(total_freq);
+ // Now pick.
+ size_t picked = 0;
+ for (size_t i = 0; i < tiles.size(); i++) if (cell.wf & (1 << i)) {
+ picked = i;
+ if ((freqpick -= tiles[i].freq) <= 0) break;
+ }
+ assert(freqpick <= 0);
+ // Modify the picked tile.
+ auto &tile = tiles[picked];
+ outmap[cur.y][cur.x] = tile.tidx;
+ cell.wf = 1 << picked; // Exactly one option remains.
+ cell.popcnt = 1;
+ // Now lets cycle thru neighbors, reduce their options (and maybe their neighbors options),
+ // and add them to the open list for next pick.
+ int ni = 0;
+ for (auto n : neighbors) {
+ auto p = (cur + n + outsize) % outsize;
+ auto &ncell = cells[p.y][p.x];
+ if (ncell.state != State::CLOSED) {
+ ncell.wf &= tile.sides[ni]; // Reduce options.
+ ncell.popcnt = PopCount(ncell.wf);
+ int totalnnumopts = 0;
+ if (!contradiction) {
+ // Hardcoded second level of neighbors of neighbors, to reduce chance of
+ // contradiction.
+ // Only do this when our current tile isn't a contradiction, to avoid
+ // artificially shrinking options.
+ int nni = 0;
+ for (auto nn : neighbors) {
+ auto pnn = (p + nn + outsize) % outsize;
+ auto &nncell = cells[pnn.y][pnn.x];
+ if (nncell.state != State::CLOSED) {
+ // Collect the superset of possible options. If we remove anything but
+ // these, we are guaranteed the direct neigbor always has a possible
+ //pick.
+ bitmask_t superopts = 0;
+ for (size_t i = 0; i < tiles.size(); i++)
+ if (ncell.wf & (1 << i))
+ superopts |= tiles[i].sides[nni];
+ nncell.wf &= superopts;
+ nncell.popcnt = PopCount(nncell.wf);
+ }
+ totalnnumopts += nncell.popcnt;
+ nni++;
+ }
+ }
+ if (ncell.state == State::OPEN) {
+ // Already in the open list, remove it for it to be re-added just in case
+ // its location is not optimal anymore.
+ totalnnumopts = min(totalnnumopts, ncell.it->second);
+ temp.splice(temp.end(), open, ncell.it); // Avoid alloc.
+ }
+ // Insert this neighbor, sorted by lowest possibilities.
+ // Use total possibilities of neighbors as a tie-breaker to avoid causing
+ // contradictions by needless surrounding of tiles.
+ decltype(open)::iterator dit = open.begin();
+ for (auto it = open.rbegin(); it != open.rend(); ++it) {
+ auto onumopts = cells[it->first.y][it->first.x].popcnt;
+ if (onumopts > ncell.popcnt ||
+ (onumopts == ncell.popcnt && it->second >= totalnnumopts)) {
+ dit = it.base();
+ break;
+ }
+ }
+ if (temp.empty()) temp.push_back({});
+ open.splice(dit, temp, ncell.it = temp.begin());
+ *ncell.it = { p, totalnnumopts };
+ ncell.state = State::OPEN;
+ }
+ ni++;
+ }
+ }
+ return true;
+}
diff --git a/lobster/samples/wave_function_collapse.lobster b/lobster/samples/wave_function_collapse.lobster
new file mode 100644
index 000000000..74cebf2d8
--- /dev/null
+++ b/lobster/samples/wave_function_collapse.lobster
@@ -0,0 +1,96 @@
+// Example of using Wave Function Collapse to generate gameworlds based on tiles.
+// Using ascii chars here for simplicity.
+
+// Using """ string literals so we don't have to escape \ :(
+
+tilemap :== [
+ """ /--\ """,
+ """ | | """,
+ """ | | """,
+ """/--J L--\ """,
+ """| | """,
+ """| | """,
+ """L--\ /--J """,
+ """ | | """,
+ """ | | """,
+ """ L--J """,
+ """ """,
+]
+
+benchmark :== false
+
+if benchmark:
+ no_conflicts := 0
+ for(1000) i:
+ outmap, conflicts := wave_function_collapse(tilemap, xy { 100, 100 })
+ print i + ": " + conflicts
+ if not conflicts: no_conflicts++
+ print no_conflicts
+ print seconds_elapsed()
+
+else:
+ // Just print a single no-conflict example.
+ for(100) i:
+ outmap, conflicts := wave_function_collapse(tilemap, xy { 100, 50 })
+ if not conflicts:
+ print "iteration: " + i
+ for(outmap) s: print s
+ return from program
+
+/*
+
+prints:
+
+ | | /-\ /-------J | | L-J /--J
+ | /--J | | | | | |
+ | | | L-J | | /-\ |
+-----------J /-J | | | /------------\ | | | /
+ | L-\ /----J | | | | | | |
+ | | | /--J | | | L----J |
+ | | L-\ | | | | |
+----\ | L-\ | | /-\ /-J | | L
+ | | | | | | | | L-------------\ | /---\
+ | | | /-----J L-J L--J | L-\ | |
+ | | | | | | | |
+ | | | | /-------\ | | | |
+ | | | | | | | L----J |
+ | | /---------J L-----\ | /---J | |
+ /-J | | | /-----J | /-J /----J
+ | | | | /----\ | | | |
+ | | | /-\ /----J | | | | | |
+ | | | | | | | | | | | /------J
+ | | | | | /-J /--\ /-----J | | | | |
+ | L-J | | | | | | | | | /--J |
+ L-----\ | | | | L--J | L---\ | | | /--\ /----\
+ | /---\ | | | | | | L------J | | | | |
+ | | | | | | | | | | | | | |
+--\ | | L-----J | | | | | | | | | L--
+ | | | | L--\ | /--------J | | | | /--J
+--J /--J | /-\ | | | | | /--J | | | /-----
+ | L---\ | | | | | | | | | L---J |
+ | | | | | | | | | /----\ | | |
+ | L--J | | | | | /-\ | | | | | /----\ L----\
+ | | | L-J | | | | | | | | | | |
+ L------------\ | | L-------J | | | | | | | | |
+ | | | | | | | | | | L--\ |
+ /-------------J | | /---\ | L------J | | | | | |
+ | /-J | | | /---J | | | | | |
+ | | L------\ | L---\ | | | | L--\ /--J |
+ | /-\ L-\ | | | | | L-----\ | | | |
+ | | | | | | | | /----\ | /-\ | L--\ | | |
+--\ | | | | L---J | L--\ | | | | | | | | | L
+ | | | | | | | | /-J | L-J | | | |
+ L-J L-J L---------\ | | | | | /--J | | |
+ | L-\ | | | | | | | L---\
+----\ /---\ | | L------J | | | | | | /--
+ | | | | | | | L--\ | | | |
+ | L---J | /---J | | | L--J | |
+ | | /--\ | /--\ | | | | |
+ L------\ /-----J | | | | | | | /--J | |
+ | /--\ | | | | L--J | | | | |
+ | | | | | | | /-\ L-------\ | L----------\ | |
+ | | L---J | | | | | | | | L--J
+ | | | | | | | L-J |
+
+*/
+
|