diff --git a/lib/assets.dart b/lib/assets.dart index bc2399b..cdaa62d 100644 --- a/lib/assets.dart +++ b/lib/assets.dart @@ -1,7 +1,6 @@ import 'dart:ui' as ui; -import 'package:dartterm/wfc/template.dart'; -import 'package:dartterm/world/level.dart'; +import 'package:dartterm/gen/generator.dart'; import 'package:flutter/services.dart'; class Assets { @@ -14,15 +13,14 @@ class Assets { return image; }); - final _Table> _wfcLevelTemplates = - _Table(loadLevelWfcAsync); + final _Table _vaults = _Table(Vaults.load); ui.Image? getImageIfAvailable(String name) { return _images.getIfAvailable(name); } - WfcTemplate? getWfcLevelTemplateIfAvailable(String name) { - return _wfcLevelTemplates.getIfAvailable(name); + Vaults? getVaultsIfAvailable(String name) { + return _vaults.getIfAvailable(name); } } @@ -53,6 +51,6 @@ ui.Image? getImageIfAvailable(String name) { return assets.getImageIfAvailable(name); } -WfcTemplate? getWfcLevelTemplateIfAvailable(String name) { - return assets.getWfcLevelTemplateIfAvailable(name); +Vaults? getVaultsIfAvailable(String name) { + return assets.getVaultsIfAvailable(name); } diff --git a/lib/game.dart b/lib/game.dart index 8bc0b0b..0d7a901 100644 --- a/lib/game.dart +++ b/lib/game.dart @@ -1,41 +1,48 @@ import 'dart:developer'; - +import 'dart:math' as math; import 'package:dartterm/assets.dart'; import 'package:dartterm/colors.dart'; +import 'package:dartterm/gen/generator.dart'; import 'package:dartterm/terminal.dart'; -import 'package:dartterm/wfc/template.dart'; import 'package:dartterm/world/level.dart'; void main() async { - WfcTemplate template; + Vaults vaults; while (true) { log("about to load template"); at(0, 0).clear(); at(0, 0).puts("Loading template!"); - WfcTemplate? maybeTemplate = - getWfcLevelTemplateIfAvailable("assets/images/wfc/bighouse2.png"); + Vaults? maybeVaults = + getVaultsIfAvailable("assets/images/wfc/bighouse2.png"); - if (maybeTemplate != null) { + if (maybeVaults != null) { log("wasn't null!"); - template = maybeTemplate; + vaults = maybeVaults; break; } await zzz(0.1); } at(0, 0).clear(); - at(0, 0).puts("Loaded! $template"); + at(0, 0).puts("Loaded! $vaults"); - var W = 16; - var H = 16; - - var wfc = Wfc(template, W, H); - wfc.run(1, -1); - var output = wfc.extractPartial(); - for (var y = 0; y < W; y++) { - for (var x = 0; x < H; x++) { + Vault output = vaults.generateBoxed( + math.Random(2), + Requirement( + 16, + 16, + DirectionSet({ + Direction.up, + Direction.left, + Direction.right, + Direction.down + }))); + var w = output.vx; + var h = output.vy; + for (var y = 0; y < w; y++) { + for (var x = 0; x < h; x++) { var cursor = at(x * 2, y * 2).big(); - switch (output[x + y * W]) { + switch (output.tiles[x + y * w]) { case LevelTile.floor: cursor.puts(" "); case LevelTile.door: @@ -44,8 +51,6 @@ void main() async { cursor.puts("#"); case LevelTile.exit: cursor.puts("X"); - case null: - cursor.puts("?"); } } } diff --git a/lib/gen/generator.dart b/lib/gen/generator.dart new file mode 100644 index 0000000..c3ffc7b --- /dev/null +++ b/lib/gen/generator.dart @@ -0,0 +1,301 @@ +import 'dart:developer'; +import 'dart:math' as math; + +import 'package:dartterm/world/level.dart'; + +enum Direction { + up, + left, + down, + right, +} + +class Vaults { + List _primitive = []; + + static Future load(String filename) async { + // TODO + return Vaults(); + } + + Vault generateBoxed(math.Random random, Requirement req) { + var vx = req.vx; + var vy = req.vy; + + var tiles = [ + for (var y = 0; y < vy; y++) + for (var x = 0; x < vx; x++) LevelTile.wall + ]; + var v = Vault(tiles, vx, vy, req.smooth); + + if (req.vx < 2 || req.vy < 2) { + return v; + } + var req2 = Requirement(vx - 2, vy - 2, req.smooth); + var inner = generate(random, req2); + v.blitFrom(inner, 1, 1); + return v; + } + + Vault generate(math.Random random, Requirement requirement) { + // TODO: Pick a relevant vault from the vaults file if possible + // + + // First of all: randomize orientation. + // This way we only have to consider one kind of spilt + var orientation = randomOrientation(random); + + // Try to make vx the long axis if possible + var req2 = requirement.unReorient(orientation); + if (req2.vy > (req2.vx - 2) * 3 / 2) { + orientation = (orientation + 2) % 8; // rotate once more + } + req2 = requirement.unReorient(orientation); + + var out2 = _generate(random, req2); + var out1 = out2.reorient(orientation); + + log("$orientation ${requirement.vx} ${requirement.vy} ${req2.vx} ${req2.vy} ${out2.vx} ${out2.vy} ${out1.vx} ${out1.vy}"); + assert(out1.vx == requirement.vx); + assert(out1.vy == requirement.vy); + return out1; + } + + Vault _generate(math.Random random, Requirement req) { + var vx = req.vx; + var vy = req.vy; + + var tiles = [ + for (var y = 0; y < vy; y++) + for (var x = 0; x < vx; x++) LevelTile.wall + ]; + var v = Vault(tiles, vx, vy, req.smooth); + + if (vx < 3 || vx * vy < 10) { + v.clear(LevelTile.floor); + } else { + // pick a split point + var splitVx = random.nextInt(vx - 2) + 1; + + var reqLeft = Requirement(splitVx, vy, req.smooth.clone()); + reqLeft.smooth.directions.add(Direction.right); + var reqRight = Requirement((vx - splitVx - 1), vy, req.smooth.clone()); + reqRight.smooth.directions.add(Direction.left); + + var vaultLeft = generate(random, reqLeft); + var vaultRight = generate(random, reqRight); + v.blitFrom(vaultLeft, 0, 0); + v.blitFrom(vaultRight, splitVx + 1, 0); + } + + return v; + } +} + +// TODO: There are many more efficient ways to do this +class DirectionSet { + final Set directions; + + DirectionSet(this.directions); + + DirectionSet flip() { + var ds2 = DirectionSet({}); + for (var i in directions) { + switch (i) { + case Direction.up: + ds2.directions.add(Direction.up); + case Direction.left: + ds2.directions.add(Direction.right); + case Direction.down: + ds2.directions.add(Direction.down); + case Direction.right: + ds2.directions.add(Direction.left); + } + } + return ds2; + } + + DirectionSet rotateLeft() { + var ds2 = DirectionSet({}); + for (var i in directions) { + switch (i) { + case Direction.up: + ds2.directions.add(Direction.left); + case Direction.left: + ds2.directions.add(Direction.down); + case Direction.down: + ds2.directions.add(Direction.right); + case Direction.right: + ds2.directions.add(Direction.up); + } + } + return ds2; + } + + DirectionSet rotateRight() { + var ds2 = DirectionSet({}); + for (var i in directions) { + switch (i) { + case Direction.up: + ds2.directions.add(Direction.right); + case Direction.right: + ds2.directions.add(Direction.down); + case Direction.down: + ds2.directions.add(Direction.left); + case Direction.left: + ds2.directions.add(Direction.up); + } + } + return ds2; + } + + DirectionSet clone() { + var ds2 = DirectionSet({}); + ds2.directions.addAll(directions); + return ds2; + } +} + +class Requirement { + final int vx, vy; + final DirectionSet smooth; + + Requirement(this.vx, this.vy, this.smooth); + + Requirement flip() { + return Requirement(vx, vy, smooth.flip()); + } + + Requirement rotateLeft() { + return Requirement(vy, vx, smooth.rotateLeft()); + } + + Requirement rotateRight() { + return Requirement(vy, vx, smooth.rotateRight()); + } + + Requirement unReorient(int r) { + assert(r >= 0 && r < 8); + Requirement o = this; + if (r % 2 == 1) { + o = o.flip(); + r -= 1; + } + while (r >= 2) { + o = o.rotateLeft(); + r -= 2; + } + return o; + } +} + +class Vault { + final List tiles; + final int vx, vy; + final DirectionSet smooth; + + Vault(this.tiles, this.vx, this.vy, this.smooth) { + assert(tiles.length == vx * vy); + } + + static Vault fromVaultData(List tiles, int vx, int vy) { + assert(tiles.length == vx * vy); + var smooth = { + Direction.up, + Direction.left, + Direction.down, + Direction.right + }; + for (var x = 0; x < vx; x++) { + if (tiles[x + 0 * vx] == LevelTile.wall) { + smooth.remove(Direction.up); + break; + } + } + for (var x = 0; x < vx; x++) { + if (tiles[x + (vy - 1) * vx] == LevelTile.wall) { + smooth.remove(Direction.down); + break; + } + } + for (var y = 0; y < vy; y++) { + if (tiles[0 + y * vx] == LevelTile.wall) { + smooth.remove(Direction.left); + break; + } + } + for (var y = 0; y < vy; y++) { + if (tiles[vx - 1 + y * vx] == LevelTile.wall) { + smooth.remove(Direction.right); + break; + } + } + + return Vault(tiles, vx, vy, DirectionSet(smooth)); + } + + void clear(LevelTile lt) { + for (var y = 0; y < vy; y++) { + for (var x = 0; x < vx; x++) { + tiles[y * vx + x] = lt; + } + } + } + + void blitFrom(Vault other, int dx, int dy) { + assert(dx >= 0); + assert(dy >= 0); + assert(dx + other.vx <= vx); + assert(dy + other.vy <= vy); + for (var x = 0; x < other.vx; x++) { + for (var y = 0; y < other.vy; y++) { + tiles[(y + dy) * vx + x + dx] = other.tiles[y * other.vx + x]; + } + } + } + + Vault flip() { + List tiles2 = [ + for (var y = 0; y < vy; y++) + for (var x = vx - 1; x >= 0; x--) tiles[y * vx + x] + ]; + + return Vault(tiles2, vx, vy, smooth.flip()); + } + + Vault rotateRight() { + List tiles2 = [ + for (var x = 0; x < vx; x++) + for (var y = 0; y < vy; y++) tiles[y * vx + x] + ]; + + return Vault(tiles2, vy, vx, smooth.rotateRight()); + } + + Vault rotateLeft() { + List tiles2 = [ + for (var x = vx - 1; x >= 0; x++) + for (var y = vy - 1; y >= 0; y++) tiles[y * vx + x] + ]; + + return Vault(tiles2, vy, vx, smooth.rotateLeft()); + } + + Vault reorient(int r) { + assert(r >= 0 && r < 8); + Vault o = this; + while (r >= 2) { + o = o.rotateRight(); + r -= 2; + } + if (r == 1) { + o = o.flip(); + r -= 1; + } + return o; + } +} + +int randomOrientation(math.Random random) { + return random.nextInt(8); +} diff --git a/lib/wfc/model.dart.old b/lib/wfc/model.dart.old deleted file mode 100644 index 557b0ad..0000000 --- a/lib/wfc/model.dart.old +++ /dev/null @@ -1,281 +0,0 @@ -import 'dart:math'; - -abstract class Model { - static var dx = [-1, 0, 1, 0]; - static var dy = [0, 1, 0, -1]; - static var opposite = [2, 3, 0, 1]; - - bool _initialized = false; - List> _wave = []; - List>> propagator = []; - List>> _compatible = []; - List _observed = []; - - List<(int, int)> _stack = []; - int _stacksize = 0, _observedSoFar = 0; - - int cMx = 0, cMy = 0, cT = 0, cN = 0; - bool _periodic = false; - bool ground = false; - - List weights = []; - List _weightLogWeights = [], _distribution = []; - - List _sumsOfOnes = []; - double _sumOfWeights = 0.0, - _sumOfWeightLogWeights = 0.0, - _startingEntropy = 0.0; - List _sumsOfWeights = [], - _sumsOfWeightLogWeights = [], - _entropies = []; - - Heuristic _heuristic = Heuristic.Entropy; - - Model(int width, int height, int n, bool periodic, Heuristic heuristic) { - cMx = width; - cMy = height; - cN = n; - _periodic = periodic; - _heuristic = heuristic; - } - - void _init() { - _initialized = true; - _wave = [ - for (var r = 0; r < cMx * cMy; r++) [for (var t = 0; t < cT; t++) false] - ]; - _compatible = [ - for (var r = 0; r < cMx * cMy; r++) - [ - for (var t = 0; t < cT; t++) [0, 0, 0, 0] - ] - ]; - _distribution = [for (var t = 0; t < cT; t++) 0.0]; - _observed = [for (var r = 0; r < cMx * cMy; r++) null]; - - _weightLogWeights = [ - for (var t = 0; t < cT; t++) weights[t] * log(weights[t]) - ]; - _sumOfWeights = 0; - _sumOfWeightLogWeights = 0.0; - for (var t = 0; t < cT; t++) { - _sumOfWeights += weights[t]; - _sumOfWeightLogWeights += _weightLogWeights[t]; - } - - _startingEntropy = - log(_sumOfWeights) - _sumOfWeightLogWeights / _sumOfWeights; - - _sumsOfOnes = [for (var r = 0; r < cMx * cMy; r++) 0]; - _sumsOfWeights = [for (var r = 0; r < cMx * cMy; r++) 0.0]; - _sumsOfWeightLogWeights = [for (var r = 0; r < cMx * cMy; r++) 0.0]; - _entropies = [for (var r = 0; r < cMx * cMy; r++) 0.0]; - - _stack = [for (var r = 0; r < _wave.length * cT; r++) (0, 0)]; - _stacksize = 0; - } - - bool run(int? seed, int limit) { - if (!_initialized) { - _init(); - } - clear(); - - var random = Random(seed); - - for (var l = 0; l < limit || limit < 0; l++) { - var node = _nextUnobservedNode(random); - if (node >= 0) { - _observe(node, random); - var success = _propagate(); - if (!success) { - return false; - } - } else { - for (var i = 0; i < _wave.length; i++) { - for (var t = 0; t < cT; t++) { - if (_wave[i][t]) { - _observed[i] = t; - break; - } - } - } - return true; - } - } - - return true; - } - - int _nextUnobservedNode(Random random) { - if (_heuristic == Heuristic.Scanline) { - for (var i = _observedSoFar; i < _wave.length; i++) { - if (!_periodic && (i % cMx + cN > cMx || i ~/ cMx + cN > cMy)) { - continue; - } - if (_sumsOfOnes[i] > 1) { - _observedSoFar = i + 1; - return i; - } - } - return -1; - } - - double min = 1E+4; - int argmin = -1; - for (var i = 0; i < _wave.length; i++) { - if (!_periodic && (i % cMx + cN > cMx || i ~/ cMx + cN > cMy)) { - continue; - } - var remainingValues = _sumsOfOnes[i]; - double entropy = _heuristic == Heuristic.Entropy - ? _entropies[i] - : remainingValues.toDouble(); - if (remainingValues > 1 && entropy <= min) { - double noise = 1E-6 * random.nextDouble(); - if (entropy + noise < min) { - min = entropy + noise; - argmin = i; - } - } - } - return argmin; - } - - void _observe(int node, Random random) { - var w = _wave[node]; - for (var t = 0; t < cT; t++) { - _distribution[t] = w[t] ? weights[t] : 0.0; - } - int r = _chooseRandom(random, _distribution); - for (var t = 0; t < cT; t++) { - if (w[t] != (t == r)) { - _ban(node, t); - } - } - } - - bool _propagate() { - while (_stacksize > 0) { - int i1, t1; - (i1, t1) = _stack[_stacksize - 1]; - _stacksize--; - - int x1 = i1 % cMx; - int y1 = i1 % cMy; - - for (int d = 0; d < 4; d++) { - int x2 = x1 + dx[d]; - int y2 = y1 + dy[d]; - - if (!_periodic && - (x2 < 0 || y2 < 0 || x2 + cN > cMx || y2 + cN > cMy)) { - continue; - } - - if (x2 < 0) { - x2 += cMx; - } else if (x2 >= cMx) { - x2 -= cMx; - } - - if (y2 < 0) { - y2 += cMy; - } else if (y2 >= cMy) { - y2 -= cMy; - } - - int i2 = x2 + y2 * cMx; - var p = propagator[d][t1]; - var compat = _compatible[i2]; - - for (var l = 0; l < p.length; l++) { - var t2 = p[l]; - var comp = compat[t2]; - comp[d]--; - if (comp[d] == 0) { - _ban(i2, t2); - } - } - } - } - - return _sumsOfOnes[0] > 0; - } - - void _ban(int i, int t) { - _wave[i][t] = false; - - var comp = _compatible[i][t]; - for (var d = 0; d < 4; d++) { - comp[d] = 0; - } - _stack[_stacksize] = (i, t); - _stacksize++; - - _sumsOfOnes[i] -= 1; - _sumsOfWeights[i] -= weights[t]; - _sumsOfWeightLogWeights[i] -= _weightLogWeights[t]; - - var sum = _sumsOfWeights[i]; - _entropies[i] = log(sum) - _sumsOfWeightLogWeights[i] / sum; - } - - void clear() { - for (var i = 0; i < _wave.length; i++) { - for (var t = 0; t < cT; t++) { - _wave[i][t] = true; - for (var d = 0; d < 4; d++) { - _compatible[i][t][d] = propagator[opposite[d]][t].length; - } - } - - _sumsOfOnes[i] = weights.length; - _sumsOfWeights[i] = _sumOfWeights; - _sumsOfWeightLogWeights[i] = _sumOfWeightLogWeights; - _entropies[i] = _startingEntropy; - _observed[i] = -1; - } - _observedSoFar = 0; - - if (ground) { - for (var x = 0; x < cMx; x++) { - for (var t = 0; t < cT - 1; t++) { - _ban(x + (cMy - 1) * cMx, t); - } - for (var y = 0; y < cMy - 1; y++) { - _ban(x + y * cMx, cT - 1); - } - } - _propagate(); - } - } -} - -int _chooseRandom(Random rand, List distribution) { - if (distribution.isEmpty) { - throw Exception("can't sample empty distribution"); - } - var sum = 0.0; - for (var i = 0; i < distribution.length; i++) { - sum += distribution[i]; - } - - if (sum == 0.0) { - return rand.nextInt(distribution.length); - } - - var rnd = rand.nextDouble() * sum; - - var i = 0; - while (rnd > 0) { - rnd -= distribution[i]; - if (rnd < 0) { - return i; - } - i += 1; - } - return distribution.length - 1; -} - -enum Heuristic { Entropy, MRV, Scanline } diff --git a/lib/wfc/template.dart b/lib/wfc/template.dart deleted file mode 100644 index 95dfffe..0000000 --- a/lib/wfc/template.dart +++ /dev/null @@ -1,467 +0,0 @@ -import 'dart:developer'; -import 'dart:math' as math; -import 'dart:typed_data'; -import 'dart:ui' as ui; -import 'dart:ui'; - -import 'package:flutter/services.dart'; - -class Wfc { - // parameters - final WfcTemplate _template; - final int _mx, _my; - - // constants - int get _n => _mx * _my; - int get _nShingles => _template._shingles.n; - int get _order => _template._order; - double _weight(int shingleIx) => - _template._shingles._shingleWeights[shingleIx]; - - // overall algo state - List> _wave = []; - List>> _compatible = []; - List _observed = []; - - // computationally expensive stuff that we keep in an incremental way - List _sumsOfOnes = []; - - // temporaries - List _distribution = []; - List<(int, int)> _stack = []; - int _stacksize = 0; - - Wfc(this._template, this._mx, this._my) { - _wave = [ - for (var r = 0; r < _n; r++) [for (var t = 0; t < _nShingles; t++) false] - ]; - - _compatible = [ - for (var r = 0; r < _n; r++) - [ - for (var t = 0; t < _nShingles; t++) [0, 0, 0, 0] - ] - ]; - - _distribution = [for (var t = 0; t < _nShingles; t++) 0.0]; - _observed = [for (var r = 0; r < _n; r++) null]; - - _sumsOfOnes = [for (var r = 0; r < _n; r++) 0]; - - _stack = [for (var r = 0; r < _n * _nShingles; r++) (0, 0)]; - _stacksize = 0; - } - - void clear() { - for (var i = 0; i < _wave.length; i++) { - for (var t = 0; t < _nShingles; t++) { - _wave[i][t] = true; - for (var d = 0; d < 4; d++) { - _compatible[i][t][d] = _template - ._shingles._metadata._propagators[_opposite[d]][t].length; - } - } - - _sumsOfOnes[i] = _nShingles; - _observed[i] = null; - } - } - - bool run(int? seed, int limit) { - clear(); - - var random = math.Random(seed); - for (var l = 0; l < limit || limit < 0; l++) { - var node = _nextUnobservedNode(random); - if (node != null) { - _observe(node, random); - var success = _propagate(); - if (!success) { - return false; - } - } else { - for (var i = 0; i < _n; i++) { - for (var t = 0; t < _nShingles; t++) { - if (_wave[i][t]) { - _observed[i] = t; - break; - } - } - } - return true; - } - } - - return true; - } - - List? extract() { - var partial = extractPartial(); - List out = []; - for (var i in partial) { - if (i == null) { - return null; - } - out.add(i); - } - return out; - } - - List extractPartial() { - List result = []; - for (int i = 0; i < _n; i++) { - result.add(null); - } - - for (int y = 0; y < _my; y++) { - var dy = y < _my - _order + 1 ? 0 : _order - 1; - for (int x = 0; x < _mx; x++) { - var dx = x < _mx - _order + 1 ? 0 : _order - 1; - var shingleIx = _observed[x - dx + (y - dy) * _mx]; - if (shingleIx != null) { - var shingle = _template._shingles._shingleValues[shingleIx]; - var content = shingle.content[dx + dy * _order]; - var real = _template._embedding.decode(content); - - result[x + y * _mx] = real; - } - } - } - - return result; - } - - int? _nextUnobservedNode(math.Random random) { - double min = 1E+10; - int? argmin; - for (var i = 0; i < _n; i++) { - if (i % _mx + _order > _mx || i ~/ _mx + _order > _my) { - continue; - } - var remainingValues = _sumsOfOnes[i]; - double entropy = remainingValues.toDouble(); - if (remainingValues > 1 && entropy <= min) { - double noise = 1E-6 * random.nextDouble(); - if (entropy + noise < min) { - min = entropy + noise; - argmin = i; - } - } - } - return argmin; - } - - void _observe(int node, math.Random random) { - var w = _wave[node]; - for (var t = 0; t < _nShingles; t++) { - _distribution[t] = w[t] ? _weight(t) : 0.0; - } - - int r = _chooseRandom(random, _distribution); - for (var t = 0; t < _nShingles; t++) { - if (w[t] != (t == r)) { - _ban(node, t); - } - } - } - - bool _propagate() { - while (_stacksize > 0) { - int i1, t1; - (i1, t1) = _stack[_stacksize - 1]; - _stacksize--; - - int x1 = i1 % _mx; - int y1 = i1 ~/ _mx; - - for (int d = 0; d < 4; d++) { - var x2 = x1 + _dx[d]; - var y2 = y1 + _dy[d]; - - if (x2 < 0 || y2 < 0 || x2 + _order > _mx || y2 + _order > _my) { - continue; - } - - int i2 = x2 + y2 * _mx; - var p = _template._shingles._metadata._propagators[d][t1]; - var compat = _compatible[i2]; - - for (var t2 in p) { - var comp = compat[t2]; - comp[d]--; - if (comp[d] == 0) { - _ban(i2, t2); - } - } - } - } - - return _sumsOfOnes[0] > 0; - } - - void _ban(int i, int t) { - _wave[i][t] = false; - - var comp = _compatible[i][t]; - for (var d = 0; d < 4; d++) { - comp[d] = 0; - } - _stack[_stacksize] = (i, t); - _stacksize++; - - _sumsOfOnes[i] -= 1; - } -} - -class WfcTemplate { - final Shingles _shingles; - final Embedding _embedding; - final int _order; - - WfcTemplate(this._shingles, this._embedding, this._order); - - static Future> loadAsync( - String name, int order, T Function(int) cb) async { - final assetImageByteData = await rootBundle.load(name); - final codec = - await ui.instantiateImageCodec(assetImageByteData.buffer.asUint8List()); - final image = (await codec.getNextFrame()).image; - - final bytedata = - (await image.toByteData(format: ImageByteFormat.rawStraightRgba))!; - - final sx = image.width; - final sy = image.height; - - final List bitmap = []; - for (var i = 0; i < sx * sy; i++) { - var pixel = bytedata.getUint32(i * 4, Endian.little); - bitmap.add(cb(pixel)); - } - - return loadBitmap(bitmap, sx, sy, order); - } - - static WfcTemplate loadBitmap( - List bitmap, int sx, int sy, int order) { - if (bitmap.length != sx * sy) { - throw Exception("malformed bitmap"); - } - var embedding = Embedding(); - List sample = [ - for (var i = 0; i < bitmap.length; i++) embedding.encode(bitmap[i]) - ]; - embedding.freeze(); - - var shingles = Shingles(); - var xmax = sx - order + 1; - var ymax = sy - order + 1; - for (var y = 0; y < ymax; y++) { - for (var x = 0; x < xmax; x++) { - var ps = [ - Shingle(order, embedding.c, - (dx, dy) => sample[(x + dx) % sx + (y + dy) % sy * sx]) - ]; - ps.add(ps[0].reflect()); - ps.add(ps[0].rotate()); - ps.add(ps[2].reflect()); - ps.add(ps[2].rotate()); - ps.add(ps[4].reflect()); - ps.add(ps[4].rotate()); - ps.add(ps[6].reflect()); - for (var p in ps) { - shingles.observe(p, 1.0); - } - } - } - shingles.freeze(); - - return WfcTemplate(shingles, embedding, order); - } -} - -class Shingles { - bool _frozen = false; - final Map _shingleIndices = {}; - final List _shingleValues = []; - final List _shingleWeights = []; - - late ShingleMetadata _metadata; - - int get n { - if (!_frozen) { - throw StateError("can't use Shingles#get n until frozen"); - } - return _shingleValues.length; - } - - void freeze() { - if (_frozen) { - throw StateError("can't freeze when already frozen"); - } - _frozen = true; - - _metadata = ShingleMetadata(this); - } - - void observe(Shingle s, double n) { - if (_frozen) { - throw StateError("can't observe when already frozen"); - } - - // double n: weights can be fractional - var index = _shingleIndices[s.hashCode]; - if (index == null) { - index = _shingleValues.length; - _shingleValues.add(s); - _shingleWeights.add(n); - } else { - _shingleWeights[index] += n; - } - } -} - -class ShingleMetadata { - // [direction][source] => list of agreeing items - List>> _propagators = []; - - ShingleMetadata(Shingles s) { - _propagators = [ - for (var d = 0; d < 4; d++) - [ - for (var t = 0; t < s.n; t++) - [ - for (var t2 = 0; t2 < s.n; t2++) - if (s._shingleValues[t] - .agrees(s._shingleValues[t2], _dx[d], _dy[d])) - t2 - ] - ] - ]; - } -} - -class Shingle { - int order; - int c; - List content = []; - - @override - int hashCode = 0; - - Shingle(this.order, this.c, int Function(int, int) f) { - content = [ - for (var y = 0; y < order; y++) - for (var x = 0; x < order; x++) f(x, y) - ]; - - int result = 0, power = 1; - for (var i = 0; i < content.length; i++) { - result += content[content.length - 1 - i] * power; - power *= c; - } - hashCode = result; - } - - Shingle rotate() { - return Shingle(order, c, (x, y) => content[order - 1 - y + x * order]); - } - - Shingle reflect() { - return Shingle(order, c, (x, y) => content[order - 1 - x + y * order]); - } - - bool agrees(Shingle other, int dx, int dy) { - var p1 = content; - var p2 = other.content; - var n = order; - - int xmin = dx < 0 ? 0 : dx; - int xmax = dx < 0 ? dx + n : n; - int ymin = dy < 0 ? 0 : dy; - int ymax = dy < 0 ? dy + n : n; - for (var y = ymin; y < ymax; y++) { - for (var x = xmin; x < xmax; x++) { - if (p1[x + n * y] != p2[x - dx + n * (y - dy)]) { - return false; - } - } - } - return true; - } - - @override - bool operator ==(Object other) { - return (other is Shingle) && - other.hashCode == hashCode && - other.order == order && - other.c == c; - } -} - -class Embedding { - bool _frozen = false; - final List _colorOf = []; - final Map _codeOf = {}; - - void freeze() { - if (_frozen) { - throw StateError("can't freeze when already frozen"); - } - _frozen = true; - } - - int get c { - if (!_frozen) { - throw StateError("can't use Embedding#get c until frozen"); - } - return _colorOf.length; - } - - int encode(T t) { - var code = _codeOf[t]; - if (code == null) { - if (_frozen) { - throw StateError("can't create new code when frozen"); - } - code = _colorOf.length; - _codeOf[t] = code; - _colorOf.add(t); - } - return code; - } - - T decode(int i) { - return _colorOf[i]!; - } -} - -int _chooseRandom(math.Random rand, List distribution) { - if (distribution.isEmpty) { - throw Exception("can't sample empty distribution"); - } - var sum = 0.0; - for (var i = 0; i < distribution.length; i++) { - sum += distribution[i]; - } - - if (sum == 0.0) { - return rand.nextInt(distribution.length); - } - - var rnd = rand.nextDouble() * sum; - - var i = 0; - while (rnd > 0) { - rnd -= distribution[i]; - if (rnd < 0) { - return i; - } - i += 1; - } - return distribution.length - 1; -} - -final List _dx = [-1, 0, 1, 0]; -final List _dy = [0, 1, 0, -1]; -final List _opposite = [2, 3, 0, 1]; diff --git a/lib/world/level.dart b/lib/world/level.dart index 93135b2..b128ee9 100644 --- a/lib/world/level.dart +++ b/lib/world/level.dart @@ -1,5 +1,3 @@ -import 'package:dartterm/wfc/template.dart'; - enum LevelTile { exit, door, @@ -11,23 +9,21 @@ class Level { Set<(int, int)> openCells = {}; } -Future> loadLevelWfcAsync(String name) async { - return WfcTemplate.loadAsync(name, 3, (c) { - switch (c) { - // ABGR - case 0xFF000000: - case 0xFF707070: - return LevelTile.wall; - case 0xFFFFFFFF: - case 0xFF00FFFF: - case 0xFFFF00FF: - return LevelTile.floor; - case 0xFFFF8700: - return LevelTile.door; - case 0xFF0000FF: - return LevelTile.exit; - default: - throw Exception("unrecognized pixel: $c"); - } - }); +LevelTile colorToTile(int c) { + switch (c) { + // ABGR + case 0xFF000000: + case 0xFF707070: + return LevelTile.wall; + case 0xFFFFFFFF: + case 0xFF00FFFF: + case 0xFFFF00FF: + return LevelTile.floor; + case 0xFFFF8700: + return LevelTile.door; + case 0xFF0000FF: + return LevelTile.exit; + default: + throw Exception("unrecognized pixel: $c"); + } }