diff --git a/assets/images/wfc/bighouse2.png b/assets/images/wfc/bighouse2.png new file mode 100644 index 0000000..d29ec61 Binary files /dev/null and b/assets/images/wfc/bighouse2.png differ diff --git a/lib/assets.dart b/lib/assets.dart index 25c3c34..bc2399b 100644 --- a/lib/assets.dart +++ b/lib/assets.dart @@ -1,5 +1,7 @@ import 'dart:ui' as ui; +import 'package:dartterm/wfc/template.dart'; +import 'package:dartterm/world/level.dart'; import 'package:flutter/services.dart'; class Assets { @@ -12,28 +14,35 @@ class Assets { return image; }); + final _Table> _wfcLevelTemplates = + _Table(loadLevelWfcAsync); + ui.Image? getImageIfAvailable(String name) { return _images.getIfAvailable(name); } + + WfcTemplate? getWfcLevelTemplateIfAvailable(String name) { + return _wfcLevelTemplates.getIfAvailable(name); + } } class _Table { final Map _loaded = {}; final Set _waiting = {}; - final Future Function(String) _getAsync; + final Future Function(String) _loadAsync; - _Table(this._getAsync); + _Table(this._loadAsync); T? getIfAvailable(String name) { if (!_waiting.contains(name)) { _waiting.add(name); - _spawnGetAsync(name); + _spawnLoadAsync(name); } return _loaded[name]; } - Future _spawnGetAsync(String name) async { - final asset = await _getAsync(name); + Future _spawnLoadAsync(String name) async { + final asset = await _loadAsync(name); _loaded[name] = asset; } } @@ -43,3 +52,7 @@ final assets = Assets(); ui.Image? getImageIfAvailable(String name) { return assets.getImageIfAvailable(name); } + +WfcTemplate? getWfcLevelTemplateIfAvailable(String name) { + return assets.getWfcLevelTemplateIfAvailable(name); +} diff --git a/lib/game.dart b/lib/game.dart index 1ede123..8b1186a 100644 --- a/lib/game.dart +++ b/lib/game.dart @@ -1,8 +1,56 @@ import 'dart:developer'; +import 'package:dartterm/assets.dart'; import 'package:dartterm/colors.dart'; import 'package:dartterm/terminal.dart'; +import 'package:dartterm/wfc/template.dart'; +import 'package:dartterm/world/level.dart'; +void main() async { + WfcTemplate template; + 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"); + + if (maybeTemplate != null) { + log("wasn't null!"); + template = maybeTemplate; + break; + } + await zzz(0.1); + } + + at(0, 0).clear(); + at(0, 0).puts("Loaded! $template"); + + var W = 16; + var H = 16; + + var wfc = Wfc(template, W, H); + wfc.run(0, -1); + var output = wfc.extractPartial(); + 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]) { + case LevelTile.floor: + cursor.puts(" "); + case LevelTile.door: + cursor.puts("d"); + case LevelTile.wall: + cursor.puts("#"); + case LevelTile.exit: + cursor.puts("X"); + case null: + cursor.puts("?"); + } + } + } +} +/* void main() async { var descriptor = "generic"; while (true) { @@ -35,3 +83,4 @@ void main() async { await waitMenu(); } } +*/ \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 3156ad4..e62ed96 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; void main() { + WidgetsFlutterBinding.ensureInitialized(); game.main(); runApp(const App()); } diff --git a/lib/wfc/model.dart b/lib/wfc/model.dart.old similarity index 96% rename from lib/wfc/model.dart rename to lib/wfc/model.dart.old index cb46ffc..557b0ad 100644 --- a/lib/wfc/model.dart +++ b/lib/wfc/model.dart.old @@ -3,13 +3,13 @@ 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]; + static var opposite = [2, 3, 0, 1]; bool _initialized = false; List> _wave = []; List>> propagator = []; List>> _compatible = []; - List _observed = []; + List _observed = []; List<(int, int)> _stack = []; int _stacksize = 0, _observedSoFar = 0; @@ -51,7 +51,7 @@ abstract class Model { ] ]; _distribution = [for (var t = 0; t < cT; t++) 0.0]; - _observed = [for (var r = 0; r < cMx * cMy; r++) 0]; + _observed = [for (var r = 0; r < cMx * cMy; r++) null]; _weightLogWeights = [ for (var t = 0; t < cT; t++) weights[t] * log(weights[t]) @@ -175,7 +175,7 @@ abstract class Model { if (x2 < 0) { x2 += cMx; - } else if (x2 >= 0) { + } else if (x2 >= cMx) { x2 -= cMx; } @@ -226,7 +226,7 @@ abstract class Model { 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; + _compatible[i][t][d] = propagator[opposite[d]][t].length; } } diff --git a/lib/wfc/overlapping.dart b/lib/wfc/overlapping.dart deleted file mode 100644 index 84e7437..0000000 --- a/lib/wfc/overlapping.dart +++ /dev/null @@ -1,133 +0,0 @@ -import 'dart:ui' as ui; - -import 'package:dartterm/wfc/model.dart'; - -class Bitmap { - final int sx; - final int sy; - final List data; - - const Bitmap(this.sx, this.sy, this.data); -} - -class OverlappingModel extends Model { - List> _patterns = []; - List _colors = []; - Map _colorOf = {}; - - OverlappingModel( - Bitmap bitmap, - int n, - int width, - int height, - bool periodicInput, - bool periodic, - int symmetry, - bool ground, - Heuristic heuristic) - : super(width, height, n, periodic, heuristic) { - List sample = []; - for (var i = 0; i < bitmap.data.length; i++) { - var existing = bitmap.data[i]; - var color = _colorOf[existing]; - if (color == null) { - color = _colors.length; - _colorOf[existing] = color; - _colors.add(existing); - } - sample[i] = color; - } - - var c = _colors.length; - - List pattern(int Function(int, int) f) { - return [ - for (var y = 0; y < n; y++) - for (var x = 0; x < n; x++) f(x, y) - ]; - } - - List rotate(List p) { - return pattern((x, y) => p[n - 1 - y + x * n]); - } - - List reflect(List p) { - return pattern((x, y) => p[n - 1 - x + y * n]); - } - - int hash(List p) { - var result = 0, power = 1; - for (var i = 0; i < p.length; i++) { - result += p[p.length - 1 - i] * power; - power *= c; - } - return result; - } - - var sx = bitmap.sx; - var sy = bitmap.sy; - Map patternIndices = {}; - List weightList = []; - var xmax = periodicInput ? sx : sx - n + 1; - var ymax = periodicInput ? sy : sy - n + 1; - for (var y = 0; y < ymax; y++) { - for (var x = 0; x < xmax; x++) { - var ps = [ - pattern((dx, dy) => sample[(x + dx) % sx + (y + dy) % sy * sx]) - ]; - ps.add(reflect(ps[0])); - ps.add(rotate(ps[0])); - ps.add(reflect(ps[2])); - ps.add(rotate(ps[2])); - ps.add(reflect(ps[4])); - ps.add(rotate(ps[4])); - ps.add(reflect(ps[6])); - - for (var k = 0; k < symmetry; k++) { - var p = ps[k]; - var h = hash(p); - var ix = patternIndices[h]; - if (ix != null) { - weightList[ix] = weightList[ix] + 1; - } else { - patternIndices[h] = weightList.length; - weightList.add(1.0); - _patterns.add(p); - } - } - } - } - - weights = weightList; - cT = weights.length; - this.ground = ground; - - bool agrees(List p1, List p2, int dx, int dy) { - 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; - } - - propagator = [ - for (var d = 0; d < 4; d++) - [ - for (var t = 0; t < cT; t++) - [ - for (var t2 = 0; t2 < cT; t2++) - if (agrees( - _patterns[t], _patterns[t2], Model.dx[d], Model.dy[d])) - t2 - ] - ] - ]; - } -} diff --git a/lib/wfc/template.dart b/lib/wfc/template.dart new file mode 100644 index 0000000..d054b60 --- /dev/null +++ b/lib/wfc/template.dart @@ -0,0 +1,498 @@ +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 _weightLogWeights = []; + double _sumOfWeights = 0.0, _sumOfWeightLogWeights = 0.0; + double _startingEntropy = 0.0; + + List _sumsOfOnes = []; + List _sumsOfWeights = []; + List _sumsOfWeightLogWeights = []; + List _entropies = []; + + // 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]; + + _weightLogWeights = [ + for (var t = 0; t < _nShingles; t++) _weight(t) * math.log(_weight(t)) + ]; + _sumOfWeights = 0.0; + _sumOfWeightLogWeights = 0.0; + for (var t = 0; t < _nShingles; t++) { + _sumOfWeights += _weight(t); + _sumOfWeightLogWeights += _weightLogWeights[t]; + } + _startingEntropy = + math.log(_sumOfWeights) - _sumOfWeightLogWeights / _sumOfWeights; + + _sumsOfOnes = [for (var r = 0; r < _n; r++) 0]; + _sumsOfWeights = [for (var r = 0; r < _n; r++) 0.0]; + _sumsOfWeightLogWeights = [for (var r = 0; r < _n; r++) 0.0]; + _entropies = [for (var r = 0; r < _n; r++) 0.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; + _sumsOfWeights[i] = _sumOfWeights; + _sumsOfWeightLogWeights[i] = _sumOfWeightLogWeights; + _entropies[i] = _startingEntropy; + _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(); // _entropies[i]; + 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; + _sumsOfWeights[i] -= _weight(t); + _sumsOfWeightLogWeights[i] -= _weightLogWeights[t]; + + var sum = _sumsOfWeights[i]; + _entropies[i] = math.log(sum) - _sumsOfWeightLogWeights[i] / sum; + } +} + +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); + log("pixel: $pixel"); + 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 42cf737..261fce6 100644 --- a/lib/world/level.dart +++ b/lib/world/level.dart @@ -1,5 +1,33 @@ -import 'dart:ui'; +import 'package:dartterm/wfc/template.dart'; + +enum LevelTile { + exit, + door, + floor, + wall, +} class Level { Set<(int, int)> openCells = {}; } + +Future> loadLevelWfcAsync(String name) async { + return WfcTemplate.loadAsync(name, 2, (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"); + } + }); +}