Skip to content

Commit 703f674

Browse files
committed
Added Wave Function Collapse functionality
1 parent 3353e50 commit 703f674

File tree

5 files changed

+342
-0
lines changed

5 files changed

+342
-0
lines changed

dev/lobster/language.vcxproj

+1
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,7 @@
341341
<ClInclude Include="..\src\lobster\unicode.h" />
342342
<ClInclude Include="..\src\lobster\vmdata.h" />
343343
<ClInclude Include="..\src\lobster\wentropy.h" />
344+
<ClInclude Include="..\src\lobster\wfc.h" />
344345
</ItemGroup>
345346
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
346347
<ImportGroup Label="ExtensionTargets">

dev/lobster/language.vcxproj.filters

+3
Original file line numberDiff line numberDiff line change
@@ -136,5 +136,8 @@
136136
<ClInclude Include="..\src\lobster\natreg.h">
137137
<Filter>common</Filter>
138138
</ClInclude>
139+
<ClInclude Include="..\src\lobster\wfc.h">
140+
<Filter>common</Filter>
141+
</ClInclude>
139142
</ItemGroup>
140143
</Project>

dev/src/builtins.cpp

+31
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#include "lobster/natreg.h"
1818

1919
#include "lobster/unicode.h"
20+
#include "lobster/wfc.h"
2021

2122
namespace lobster {
2223

@@ -1007,6 +1008,36 @@ void AddBuiltins(NativeRegistry &natreg) {
10071008
" a vector of vectors of indices of the circles that are within dist of eachothers radius."
10081009
" pre-filter indicates objects that should appear in the inner vectors.");
10091010

1011+
STARTDECL(wave_function_collapse) (VM &vm, Value &tilemap, Value &size) {
1012+
auto sz = ValueDecToINT<2>(vm, size);
1013+
auto rows = tilemap.vval()->len;
1014+
vector<const char *> inmap(rows);
1015+
intp cols = 0;
1016+
for (intp i = 0; i < rows; i++) {
1017+
auto sv = tilemap.vval()->At(i).sval()->strv();
1018+
if (i) { if ((intp)sv.size() != cols) vm.Error("all columns must be equal length"); }
1019+
else cols = sv.size();
1020+
inmap[i] = sv.data();
1021+
}
1022+
tilemap.DECRT(vm);
1023+
auto outstrings = ToValueOfVectorOfStringsEmpty(vm, sz, 0);
1024+
vector<char *> outmap(sz.y, nullptr);
1025+
for (int i = 0; i < sz.y; i++) outmap[i] = (char *)outstrings.vval()->At(i).sval()->data();
1026+
int num_contradictions = 0;
1027+
auto ok = WaveFunctionCollapse(int2(cols, inmap.size()), inmap.data(), sz, outmap.data(),
1028+
rnd, num_contradictions);
1029+
if (!ok)
1030+
vm.Error("tilemap contained too many tile ids");
1031+
vm.Push(outstrings);
1032+
return num_contradictions;
1033+
}
1034+
ENDDECL2(wave_function_collapse, "tilemap,size", "S]I}:2", "S]I",
1035+
"returns a tilemap of given size modelled after the possible shapes in the input"
1036+
" tilemap. Tilemap should consist of chars in the 0..127 range. Second return value"
1037+
" the number of failed neighbor matches, this should"
1038+
" ideally be 0, but can be non-0 for larger maps. Simply call this function"
1039+
" repeatedly until it is 0");
1040+
10101041
STARTDECL(resume) (VM &vm, Value &co, Value &ret) {
10111042
vm.CoResume(co.cval());
10121043
// By the time CoResume returns, we're now back in the context of co, meaning that the

dev/src/lobster/wfc.h

+211
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
// Copyright 2018 Wouter van Oortmerssen. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
16+
// Very simple tile based Wave Function Collapse ("Simple Tiled Model") implementation.
17+
// See: https://github.com/mxgmn/WaveFunctionCollapse
18+
// Derives adjacencies from an example rather than explicitly specified neighbors.
19+
// Does not do any symmetries/rotations unless they're in the example.
20+
21+
// Algorithm has a lot of similarities to A* in how its implemented.
22+
// Uses bitmasks to store the set of possible tiles, which currently limits the number of
23+
// unique tiles to 64. This restriction cool be lifted by using std::bitset instead.
24+
25+
// In my testing, generates a 50x50 tile map in <1 msec. 58% of such maps are conflict free.
26+
// At 100x100 that is 3 msec and 34%.
27+
// At 200x200 that is 24 msec and 13%
28+
// At 400x400 that is 205 msec and ~1%
29+
// Algorithm may need to extended to flood more than 2 neighbor levels to make it suitable
30+
// for really gigantic maps.
31+
32+
// inmap & outmap must point to row-major 2D arrays of the given size.
33+
// each in tile char must be in range 0..127, of which max 64 may actually be in use (may be
34+
// sparse).
35+
// Returns false if too many unique tiles in input.
36+
template<typename T> bool WaveFunctionCollapse(const int2 &insize, const char **inmap,
37+
const int2 &outsize, char **outmap,
38+
RandomNumberGenerator<T> &rnd,
39+
int &num_contradictions) {
40+
num_contradictions = 0;
41+
typedef uint64_t bitmask_t;
42+
const auto nbits = sizeof(bitmask_t) * 8;
43+
array<int, 256> tile_lookup;
44+
tile_lookup.fill(-1);
45+
struct Tile { bitmask_t sides[4] = {}; size_t freq = 0; char tidx = 0; };
46+
vector<Tile> tiles;
47+
int2 neighbors[] = { { 0, 1 }, { 1, 0 }, { 0, -1 }, { -1, 0 } };
48+
// Collect unique tiles and their frequency of occurrence.
49+
for (int iny = 0; iny < insize.y; iny++) {
50+
for (int inx = 0; inx < insize.x; inx++) {
51+
auto t = inmap[iny][inx];
52+
if (tile_lookup[t] < 0) {
53+
// We use a bitmask_t mask for valid neighbors.
54+
if (tiles.size() == nbits - 1) return false;
55+
tile_lookup[t] = (int)tiles.size();
56+
tiles.push_back(Tile());
57+
}
58+
auto &tile = tiles[tile_lookup[t]];
59+
tile.freq++;
60+
tile.tidx = t;
61+
}
62+
}
63+
// Construct valid neighbor bitmasks.
64+
for (int iny = 0; iny < insize.y; iny++) {
65+
for (int inx = 0; inx < insize.x; inx++) {
66+
auto t = inmap[iny][inx];
67+
auto &tile = tiles[tile_lookup[t]];
68+
int ni = 0;
69+
for (auto n : neighbors) {
70+
auto p = (n + int2(inx, iny) + insize) % insize;
71+
auto tn = inmap[p.y][p.x];
72+
assert(tile_lookup[tn] >= 0);
73+
tile.sides[ni] |= 1 << tile_lookup[tn];
74+
ni++;
75+
}
76+
}
77+
}
78+
size_t most_common_tile_id = 0;
79+
size_t most_common_tile_freq = 0;
80+
for (auto &tile : tiles) if (tile.freq > most_common_tile_freq) {
81+
most_common_tile_freq = tile.freq;
82+
most_common_tile_id = &tile - &tiles[0];
83+
}
84+
// Track an open list (much like A*) of next options, sorted by best candidate at the end.
85+
list<pair<int2, int>> open, temp;
86+
// Store a bitmask per output cell of remaining possible choices.
87+
auto max_bitmask = (1 << tiles.size()) - 1;
88+
enum class State : uchar { NEW, OPEN, CLOSED };
89+
struct Cell {
90+
bitmask_t wf;
91+
uchar popcnt = 0;
92+
State state = State::NEW;
93+
decltype(open)::iterator it;
94+
Cell(bitmask_t wf, uchar popcnt) : wf(wf), popcnt(popcnt) {}
95+
};
96+
vector<vector<Cell>> cells(outsize.y, vector<Cell>(outsize.x, Cell(max_bitmask, tiles.size())));
97+
auto start = rndivec<int, 2>(rnd, outsize);
98+
open.push_back({ start, 0 }); // Start.
99+
auto &scell = cells[start.y][start.x];
100+
scell.state = State::OPEN;
101+
scell.it = open.begin();
102+
// Pick tiles until no more possible.
103+
while (!open.empty()) {
104+
// Simply picking the first list item results in the same chance of conflicts as
105+
// random picks over equal options, but it is assumed the latter could generate more
106+
// interesting maps.
107+
size_t num_candidates = 1;
108+
auto numopts_0 = cells[open.back().first.y][open.back().first.x].popcnt;
109+
for (auto it = ++open.rbegin(); it != open.rend(); ++it)
110+
if (numopts_0 == cells[it->first.y][it->first.x].popcnt &&
111+
open.back().second == it->second)
112+
num_candidates++;
113+
else
114+
break;
115+
auto candidate_i = rnd(num_candidates);
116+
auto candidate_it = --open.end();
117+
for (int i = 0; i < candidate_i; i++) --candidate_it;
118+
auto cur = candidate_it->first;
119+
temp.splice(temp.end(), open, candidate_it);
120+
auto &cell = cells[cur.y][cur.x];
121+
assert(cell.state == State::OPEN);
122+
cell.state = State::CLOSED;
123+
bool contradiction = !cell.popcnt;
124+
if (contradiction) {
125+
num_contradictions++;
126+
// Rather than failing right here, fill in the whole map as best as possible just in
127+
// case a map with bad tile neighbors is still useful to the caller.
128+
// As a heuristic lets just use the most common tile, as that will likely have the
129+
// most neighbor options.
130+
cell.wf = 1 << most_common_tile_id;
131+
cell.popcnt = 1;
132+
}
133+
// From our options, pick one randomly, weighted by frequency of tile occurrence.
134+
// First find total frequency.
135+
size_t total_freq = 0;
136+
for (size_t i = 0; i < tiles.size(); i++) if (cell.wf & (1 << i)) total_freq += tiles[i].freq;
137+
auto freqpick = rnd(total_freq);
138+
// Now pick.
139+
size_t picked = 0;
140+
for (size_t i = 0; i < tiles.size(); i++) if (cell.wf & (1 << i)) {
141+
picked = i;
142+
if ((freqpick -= tiles[i].freq) <= 0) break;
143+
}
144+
assert(freqpick <= 0);
145+
// Modify the picked tile.
146+
auto &tile = tiles[picked];
147+
outmap[cur.y][cur.x] = tile.tidx;
148+
cell.wf = 1 << picked; // Exactly one option remains.
149+
cell.popcnt = 1;
150+
// Now lets cycle thru neighbors, reduce their options (and maybe their neighbors options),
151+
// and add them to the open list for next pick.
152+
int ni = 0;
153+
for (auto n : neighbors) {
154+
auto p = (cur + n + outsize) % outsize;
155+
auto &ncell = cells[p.y][p.x];
156+
if (ncell.state != State::CLOSED) {
157+
ncell.wf &= tile.sides[ni]; // Reduce options.
158+
ncell.popcnt = PopCount(ncell.wf);
159+
int totalnnumopts = 0;
160+
if (!contradiction) {
161+
// Hardcoded second level of neighbors of neighbors, to reduce chance of
162+
// contradiction.
163+
// Only do this when our current tile isn't a contradiction, to avoid
164+
// artificially shrinking options.
165+
int nni = 0;
166+
for (auto nn : neighbors) {
167+
auto pnn = (p + nn + outsize) % outsize;
168+
auto &nncell = cells[pnn.y][pnn.x];
169+
if (nncell.state != State::CLOSED) {
170+
// Collect the superset of possible options. If we remove anything but
171+
// these, we are guaranteed the direct neigbor always has a possible
172+
//pick.
173+
bitmask_t superopts = 0;
174+
for (size_t i = 0; i < tiles.size(); i++)
175+
if (ncell.wf & (1 << i))
176+
superopts |= tiles[i].sides[nni];
177+
nncell.wf &= superopts;
178+
nncell.popcnt = PopCount(nncell.wf);
179+
}
180+
totalnnumopts += nncell.popcnt;
181+
nni++;
182+
}
183+
}
184+
if (ncell.state == State::OPEN) {
185+
// Already in the open list, remove it for it to be re-added just in case
186+
// its location is not optimal anymore.
187+
totalnnumopts = min(totalnnumopts, ncell.it->second);
188+
temp.splice(temp.end(), open, ncell.it); // Avoid alloc.
189+
}
190+
// Insert this neighbor, sorted by lowest possibilities.
191+
// Use total possibilities of neighbors as a tie-breaker to avoid causing
192+
// contradictions by needless surrounding of tiles.
193+
decltype(open)::iterator dit = open.begin();
194+
for (auto it = open.rbegin(); it != open.rend(); ++it) {
195+
auto onumopts = cells[it->first.y][it->first.x].popcnt;
196+
if (onumopts > ncell.popcnt ||
197+
(onumopts == ncell.popcnt && it->second >= totalnnumopts)) {
198+
dit = it.base();
199+
break;
200+
}
201+
}
202+
if (temp.empty()) temp.push_back({});
203+
open.splice(dit, temp, ncell.it = temp.begin());
204+
*ncell.it = { p, totalnnumopts };
205+
ncell.state = State::OPEN;
206+
}
207+
ni++;
208+
}
209+
}
210+
return true;
211+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
// Example of using Wave Function Collapse to generate gameworlds based on tiles.
2+
// Using ascii chars here for simplicity.
3+
4+
// Using """ string literals so we don't have to escape \ :(
5+
6+
tilemap :== [
7+
""" /--\ """,
8+
""" | | """,
9+
""" | | """,
10+
"""/--J L--\ """,
11+
"""| | """,
12+
"""| | """,
13+
"""L--\ /--J """,
14+
""" | | """,
15+
""" | | """,
16+
""" L--J """,
17+
""" """,
18+
]
19+
20+
benchmark :== false
21+
22+
if benchmark:
23+
no_conflicts := 0
24+
for(1000) i:
25+
outmap, conflicts := wave_function_collapse(tilemap, xy { 100, 100 })
26+
print i + ": " + conflicts
27+
if not conflicts: no_conflicts++
28+
print no_conflicts
29+
print seconds_elapsed()
30+
31+
else:
32+
// Just print a single no-conflict example.
33+
for(100) i:
34+
outmap, conflicts := wave_function_collapse(tilemap, xy { 100, 50 })
35+
if not conflicts:
36+
print "iteration: " + i
37+
for(outmap) s: print s
38+
return from program
39+
40+
/*
41+
42+
prints:
43+
44+
| | /-\ /-------J | | L-J /--J
45+
| /--J | | | | | |
46+
| | | L-J | | /-\ |
47+
-----------J /-J | | | /------------\ | | | /
48+
| L-\ /----J | | | | | | |
49+
| | | /--J | | | L----J |
50+
| | L-\ | | | | |
51+
----\ | L-\ | | /-\ /-J | | L
52+
| | | | | | | | L-------------\ | /---\
53+
| | | /-----J L-J L--J | L-\ | |
54+
| | | | | | | |
55+
| | | | /-------\ | | | |
56+
| | | | | | | L----J |
57+
| | /---------J L-----\ | /---J | |
58+
/-J | | | /-----J | /-J /----J
59+
| | | | /----\ | | | |
60+
| | | /-\ /----J | | | | | |
61+
| | | | | | | | | | | /------J
62+
| | | | | /-J /--\ /-----J | | | | |
63+
| L-J | | | | | | | | | /--J |
64+
L-----\ | | | | L--J | L---\ | | | /--\ /----\
65+
| /---\ | | | | | | L------J | | | | |
66+
| | | | | | | | | | | | | |
67+
--\ | | L-----J | | | | | | | | | L--
68+
| | | | L--\ | /--------J | | | | /--J
69+
--J /--J | /-\ | | | | | /--J | | | /-----
70+
| L---\ | | | | | | | | | L---J |
71+
| | | | | | | | | /----\ | | |
72+
| L--J | | | | | /-\ | | | | | /----\ L----\
73+
| | | L-J | | | | | | | | | | |
74+
L------------\ | | L-------J | | | | | | | | |
75+
| | | | | | | | | | L--\ |
76+
/-------------J | | /---\ | L------J | | | | | |
77+
| /-J | | | /---J | | | | | |
78+
| | L------\ | L---\ | | | | L--\ /--J |
79+
| /-\ L-\ | | | | | L-----\ | | | |
80+
| | | | | | | | /----\ | /-\ | L--\ | | |
81+
--\ | | | | L---J | L--\ | | | | | | | | | L
82+
| | | | | | | | /-J | L-J | | | |
83+
L-J L-J L---------\ | | | | | /--J | | |
84+
| L-\ | | | | | | | L---\
85+
----\ /---\ | | L------J | | | | | | /--
86+
| | | | | | | L--\ | | | |
87+
| L---J | /---J | | | L--J | |
88+
| | /--\ | /--\ | | | | |
89+
L------\ /-----J | | | | | | | /--J | |
90+
| /--\ | | | | L--J | | | | |
91+
| | | | | | | /-\ L-------\ | L----------\ | |
92+
| | L---J | | | | | | | | L--J
93+
| | | | | | | L-J |
94+
95+
*/
96+

0 commit comments

Comments
 (0)