-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathwfc.lua
More file actions
280 lines (246 loc) · 7.19 KB
/
wfc.lua
File metadata and controls
280 lines (246 loc) · 7.19 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
--!strict
---@class WaveFunctionCollapse
---@field WIDTH number
---@field HEIGHT number
---@field START_STATE STATE | nil
---@field CURRENT_STATE STATE
---@field RULES RULE_SET
---@field SEED number
local WaveFunctionCollapse = {}
---@class STATE
---@field state number[][]
---@field possibilities number[][][]
---@field entropies number[][]
---@field affectedPoints {i : number, j : number}[]
local STATE = {}
STATE.__index = STATE
---@class RULE
---@field WEIGHTS number[]
local RULE = {}
RULE.__index = RULE
---@class RULE_SET
---@field RULES RULE[]
local RULE_SET = {}
RULE_SET.__index = RULE_SET
---@param WIDTH number
---@param HEIGHT number
---@param START_STATE STATE | nil
---@param RULES RULE_SET
---@param SEED number
function WaveFunctionCollapse.new(WIDTH, HEIGHT, START_STATE, RULES, SEED)
local self = setmetatable({}, { __index = WaveFunctionCollapse })
self.WIDTH = WIDTH
self.HEIGHT = HEIGHT
self.START_STATE = START_STATE
self.CURRENT_STATE = {}
if self.START_STATE == nil then
self.CURRENT_STATE.state = WaveFunctionCollapse.__createEmptyGrid(WIDTH, HEIGHT)
else
assert(START_STATE)
self.CURRENT_STATE.state = START_STATE
end
self.RULES = RULES
self.SEED = SEED
math.randomseed(SEED)
return self
end
---creates an empty grid for the current state on initiliaze
---@param WIDTH number
---@param HEIGHT number
function WaveFunctionCollapse.__createEmptyGrid(WIDTH, HEIGHT)
local grid = {}
for i = 1, HEIGHT, 1 do
grid[i] = {}
for j = 1, WIDTH, 1 do
-- -1 is the symbol for an unfilled cell
grid[i][j] = -1
end
end
return grid
end
---Creates a border in the grid
---@param grid number[][]
---@param fill number
---@return number[][]
function WaveFunctionCollapse.__createBorder(grid, fill)
local height = #grid
local width = #grid[1]
-- Top and bottom borders
for x = 1, width do
grid[1][x] = fill
grid[height][x] = fill
end
-- Left and right borders
for y = 1, height do
grid[y][1] = fill
grid[y][width] = fill
end
return grid
end
---@param grid STATE
---@param index1 number
---@param index2 number
---@return {i : number, j : number}[]
local function GetNeigbors(grid, index1, index2)
local HEIGHT = #grid
local WIDTH = #grid[1]
-- check up valid
local up = index1 > 1
local down = index1 < HEIGHT
local right = index2 > 1
local left = index2 < WIDTH
local neighbors = {}
if up then table.insert(neighbors, {i = index1-1, j = index2}) end
if down then table.insert(neighbors, {i = index1+1, j = index2}) end
if left then table.insert(neighbors, {i = index1, j = index2+ 1}) end
if right then table.insert(neighbors, {i = index1, j = index2- 1}) end
return neighbors
end
-- returns a table that is filled with ones to the length
---@param length number
---@return number[]
local function oneFilledTable(length)
local tabl = {}
for i = 1, length, 1 do
table.insert(tabl, 1)
end
return tabl
end
---uses taylor series for a fast approximation of ln between 0 and 1
---@param a number
---@return number
local function fastLn(a)
local N = 5
local res = 0
for i = 1, N, 1 do
res = res + (-1)^(N%2-1)*((a-1)^N)/N
end
return res
end
---determine entropy
---@param probabilities number[]
---@returns number
local function entropyDeterminer(probabilities)
--if #probabilities == 0 then return -math.huge end
local entropy = 0
for _, p in pairs(probabilities) do
entropy = entropy - p*fastLn(p)
end
return entropy
end
--- run the update method
function WaveFunctionCollapse:init()
self.CURRENT_STATE.affectedPoints = {}
for i = 1, self.HEIGHT, 1 do
for j = 1, self.WIDTH, 1 do
table.insert(self.CURRENT_STATE.affectedPoints, {i = i, j = j})
end
end
local possibilities = {}
local entropies = {}
for i = 1, self.HEIGHT, 1 do
entropies[i] = {}
possibilities[i] = {}
for j = 1, self.WIDTH, 1 do
possibilities[j] = {}
end
end
self.CURRENT_STATE.possibilities = possibilities
self.CURRENT_STATE.entropies = entropies
end
---updates the possibilities of what entropies can be and probabilities
function WaveFunctionCollapse:updatePossibilities()
for _, point in pairs(self.CURRENT_STATE.affectedPoints) do
local i = point.i
local j = point.j
local entropy = math.huge
if self.CURRENT_STATE.state[i][j] == -1 then
local possible = oneFilledTable(#self.RULES.RULES)
local probabilities = {}
-- get neighbors
local neighbors = GetNeigbors(self.CURRENT_STATE.state, i, j)
local weights = {}
local weightTotal = 0
for _, v in pairs(neighbors) do
local value = self.CURRENT_STATE.state[v.i][v.j]
if value ~= -1 then
-- multiply each weight
for rule, weight in pairs(self.RULES.RULES[value]) do
possible[rule] = possible[rule] * weight
end
end
end
-- remove zero weights because they mess with entropy calculation and are unimportant
for index, v in pairs(possible) do
if v ~= 0 then
weights[index] = v
weightTotal = weightTotal + v
end
end
for index, v in pairs(weights) do
probabilities[index] = v / weightTotal
end
self.CURRENT_STATE.possibilities[i][j] = probabilities
entropy = entropyDeterminer(probabilities)
end
self.CURRENT_STATE.entropies[i][j] = entropy
end
end
---Finds Lowest Entropy
---@return {i : number, j : number} | nil
function WaveFunctionCollapse:findLowestEntropy()
local lowestValue = math.huge
local iI = -1
local jI = -1
for i = 1, self.HEIGHT, 1 do
for j = 1, self.WIDTH, 1 do
if self.CURRENT_STATE.entropies[i][j] < lowestValue and self.CURRENT_STATE.state[i][j] < 0 then lowestValue = self.CURRENT_STATE.entropies[i][j]; iI = i; jI = j; end
end
end
if lowestValue == -math.huge or iI < 0 then
return nil
end
return {i = iI, j = jI}
end
---Make Update
---@param point {i : number, j : number}
function WaveFunctionCollapse:MakeUpdate(point)
local randomNumber = math.random()
local possible = self.CURRENT_STATE.possibilities[point.i][point.j]
local su = 0
local index = 0
for i, v in pairs(possible) do
su = su + v
index = i
if su > randomNumber then break end
end
if index == 0 then return "FAILED" end
self.CURRENT_STATE.state[point.i][point.j] = index
self.CURRENT_STATE.affectedPoints = GetNeigbors(self.CURRENT_STATE.state, point.i, point.j)
end
---check state
---@return "FAILED" | "FILLED" | "NOTFILLED"
function WaveFunctionCollapse:checkState()
local filled = true
for _, v in pairs(self.CURRENT_STATE.state) do
for _, d in pairs(v) do
if d == -1 then filled = false; break end
end
end
if filled then return "FILLED" end
return "NOTFILLED"
end
---Performs one step of the algrothirim
function WaveFunctionCollapse:performStep()
-- update possibilities
self:updatePossibilities()
-- find lowest entropy
local point = self:findLowestEntropy()
if point == nil then return "FAILED" end
-- make choice based on random variable by seed
local state = self:MakeUpdate(point)
if state == "FAILED" then return "FAILED" end
-- return if the map is filled, not filled, or failed
return self:checkState()
end
return WaveFunctionCollapse