From ae0c62b010367d4959ee6cb551ff7e2d1a77b462 Mon Sep 17 00:00:00 2001 From: Nyeogmi Date: Thu, 21 Sep 2023 15:37:12 -0700 Subject: [PATCH] Add and pervasively use grid type (Bitmap) --- lib/algorithms/geometry.dart | 47 ++++++++++++++ lib/algorithms/regionalize.dart | 11 ++-- lib/bitmap.dart | 108 +++++++++++++++++++++++++++++--- lib/game.dart | 7 ++- lib/gen/generator.dart | 86 ++++++++++++++----------- lib/gen/vault.dart | 61 +++++------------- lib/gen/vaults.dart | 2 +- 7 files changed, 220 insertions(+), 102 deletions(-) create mode 100644 lib/algorithms/geometry.dart diff --git a/lib/algorithms/geometry.dart b/lib/algorithms/geometry.dart new file mode 100644 index 0000000..aa61b43 --- /dev/null +++ b/lib/algorithms/geometry.dart @@ -0,0 +1,47 @@ +// Dart has a habit of using double-inclusive bounds, +// and I strongly prefer half-open bounds +// +// So: Here's a reimplementation of the geometry I need +class Size { + final int dx; + final int dy; + + Size(this.dx, this.dy) { + assert(dx >= 0); + assert(dy >= 0); + } +} + +class Offset { + final int x; + final int y; + + const Offset(this.x, this.y); +} + +class Rect { + final int x0; + final int y0; + final int dx; + final int dy; + + int get x1 => x0 + dx; + int get y1 => y0 + dy; + + Rect(this.x0, this.y0, this.dx, this.dy) { + assert(dx >= 0); + assert(dy >= 0); + } + + bool contains(int x, int y) { + return x0 <= x && x < x1 && y0 <= y && y < y1; + } + + bool containsPoint(Offset xy) { + return contains(xy.x, xy.y); + } + + bool containsRect(Rect rect) { + return x0 <= rect.x0 && y0 <= rect.y0 && rect.x1 <= x1 && rect.y1 <= y1; + } +} diff --git a/lib/algorithms/regionalize.dart b/lib/algorithms/regionalize.dart index 1e00222..922d482 100644 --- a/lib/algorithms/regionalize.dart +++ b/lib/algorithms/regionalize.dart @@ -1,5 +1,7 @@ import 'dart:math' as math; +import 'package:dartterm/algorithms/geometry.dart' as geo; + class Region { final math.Rectangle rect; final Set<(int, int)> points; @@ -25,14 +27,13 @@ class Region { } } -List regionalize( - math.Rectangle rect, bool Function(int, int) isAccessible) { +List regionalize(geo.Rect rect, bool Function(int, int) isAccessible) { int nextRegion = 0; Map<(int, int), int> regions = {}; int floodfill(int x, int y) { int workDone = 0; - if (!rect.containsPoint(math.Point(x, y))) { + if (!rect.containsPoint(geo.Offset(x, y))) { return workDone; } if (regions[(x, y)] != null) { @@ -53,8 +54,8 @@ List regionalize( // TODO: This can be done more efficiently with a union/find data structure // But this is an easy implementation to understand - for (var y = rect.top; y < rect.bottom; y++) { - for (var x = rect.left; x < rect.right; x++) { + for (var y = rect.y0; y < rect.y1; y++) { + for (var x = rect.x0; x < rect.x1; x++) { if (regions[(x, y)] == null) { if (floodfill(x, y) > 0) { nextRegion += 1; diff --git a/lib/bitmap.dart b/lib/bitmap.dart index 1f3d8fd..9ee22ad 100644 --- a/lib/bitmap.dart +++ b/lib/bitmap.dart @@ -1,17 +1,35 @@ -import 'dart:math' as math; import 'dart:typed_data'; import 'dart:ui' as ui; +import 'package:dartterm/algorithms/geometry.dart' as geo; import 'package:flutter/services.dart'; class Bitmap { // This idiosyncratic usage of "bitmap" comes from some other technology. // What I'm saying is "don't blame me" - final math.Rectangle rect; + final geo.Size size; final List data; - Bitmap(this.rect, this.data) { - assert(this.data.length == this.rect.width * this.rect.height); + geo.Rect get rect => geo.Rect(0, 0, size.dx, size.dy); + + Bitmap(this.size, this.data) { + assert(data.length == size.dx * size.dy); + } + + static Bitmap blankWith(int dx, int dy, T Function(int, int) lt) { + var data = [ + for (var y = 0; y < dy; y++) + for (var x = 0; x < dx; x++) lt(x, y) + ]; + return Bitmap(geo.Size(dx, dy), data); + } + + static Bitmap blank(int dx, int dy, T lt) { + var data = [ + for (var y = 0; y < dy; y++) + for (var x = 0; x < dx; x++) lt + ]; + return Bitmap(geo.Size(dx, dy), data); } static Future> load(String name, T Function(int) cb) async { @@ -32,15 +50,87 @@ class Bitmap { data.add(cb(pixel)); } - return Bitmap(math.Rectangle(0, 0, sx, sy), data); + return Bitmap(geo.Size(sx, sy), data); + } + + void clearWith(T Function(int, int) t) { + for (var y = 0; y < size.dy; y++) { + for (var x = 0; x < size.dx; x++) { + data[y * size.dx + x] = t(x, y); + } + } + } + + void clear(T t) { + for (var y = 0; y < size.dy; y++) { + for (var x = 0; x < size.dx; x++) { + data[y * size.dx + x] = t; + } + } } T? get(int x, int y) { - var realX = x - rect.top; - var realY = y - rect.left; - if (realX < 0 || realY < 0 || realX > rect.width || realY > rect.height) { + if (x < 0 || y < 0 || x >= size.dx || y >= size.dy) { return null; } - return data[realY * rect.width + realX]; + return data[y * size.dx + x]; + } + + T unsafeGet(int x, int y) { + assert(x < 0 || y < 0 || x >= size.dx || y >= size.dy); + return data[y * size.dx + x]; + } + + void set(int x, int y, T value) { + assert(x < 0 || y < 0 || x >= size.dx || y >= size.dy); + data[y * size.dx + x] = value; + } + + void blitFrom(Bitmap other, int dx, int dy) { + assert(rect.containsRect(geo.Rect(dx, dy, other.size.dx, other.size.dy))); + var x0 = other.rect.x0; + var y0 = other.rect.x0; + var x1 = other.rect.x1; + var y1 = other.rect.y1; + var myW = size.dx; + var otW = other.size.dx; + for (var x = x0; x < x1; x++) { + for (var y = y0; y < y1; y++) { + data[(y + dy) * myW + (x + dx)] = other.data[y * otW + x]; + } + } + } + + Bitmap flip() { + var geo.Size(:dx, :dy) = size; + + List data2 = [ + for (var y = 0; y < size.dy; y++) + for (var x = size.dx - 1; x >= 0; x--) data[y * dx + x] + ]; + + return Bitmap(geo.Size(dx, dy), data2); + } + + Bitmap rotateRight() { + var geo.Size(:dx, :dy) = size; + + List data2 = [ + for (var x = 0; x < dx; x++) + for (var y = 0; y < dy; y++) data[(dy - 1 - y) * dx + x] + ]; + + return Bitmap(geo.Size(dy, dx), data2); + } + + Bitmap rotateLeft() { + var geo.Size(:dx, :dy) = size; + + List data2 = [ + for (var x = dx - 1; x >= 0; x++) + for (var y = dy - 1; y >= 0; y++) data[y * dx + (dx - 1 - x)] + ]; + + return Bitmap(geo.Size(dy, dx), data2); } } diff --git a/lib/game.dart b/lib/game.dart index 5ec5e4d..931bdc6 100644 --- a/lib/game.dart +++ b/lib/game.dart @@ -1,5 +1,6 @@ import 'dart:math' as math; import 'package:dartterm/assets.dart'; +import 'package:dartterm/algorithms/geometry.dart' as geo; import 'package:dartterm/gen/generator.dart'; import 'package:dartterm/input.dart'; import 'package:dartterm/skreek.dart'; @@ -31,13 +32,13 @@ void main() async { clear(); Vault output = Generator(math.Random(seed), vaults) .generate(Requirement(16, 32, 16, 24, DirectionSet({})), false); - var w = output.vx; - var h = output.vy; + var geo.Size(dx: w, dy: h) = output.size; for (var y = 0; y < h; y++) { for (var x = 0; x < w; x++) { var cursor = at(x * 2, y * 2).big(); - switch (output.tiles[x + y * w]) { + switch (output.tiles.get(x, y)) { case VaultTile.bspfloor: + case null: cursor.puts(" "); case VaultTile.floor: cursor.puts("."); diff --git a/lib/gen/generator.dart b/lib/gen/generator.dart index f79ab4a..e263fe4 100644 --- a/lib/gen/generator.dart +++ b/lib/gen/generator.dart @@ -1,5 +1,6 @@ import 'dart:math' as math; +import 'package:dartterm/algorithms/geometry.dart' as geo; import 'package:dartterm/algorithms/regionalize.dart'; import 'package:dartterm/bitmap.dart'; import 'package:dartterm/skreek.dart'; @@ -50,8 +51,9 @@ class Generator { var out1 = reorientVault(out2, orientation); // log("$orientation ${requirement.vx} ${requirement.vy} ${req2.vx} ${req2.vy} ${out2.vx} ${out2.vy} ${out1.vx} ${out1.vy}"); - assert(out1.vx >= requirement.vxMin && out1.vx <= requirement.vxMax); - assert(out1.vy >= requirement.vyMin && out1.vy <= requirement.vyMax); + var geo.Size(:dx, :dy) = out1.size; + assert(dx >= requirement.vxMin && dx <= requirement.vxMax); + assert(dy >= requirement.vyMin && dy <= requirement.vyMax); assert(out1.smooth.directions.containsAll(requirement.smooth.directions)); return out1; } @@ -70,11 +72,11 @@ class Generator { var vyRand = _random.nextInt(vyMax + 1 - vyMin) + vyMin; if (vxMax < 2 || vyMax < 2) { - return Vault.blank(vxMax, vyRand, req.smooth, VaultTile.wall); + return Vault.blank(vxMax, vyRand, VaultTile.wall, req.smooth); } else if (vxMax < 9 || (vxMax - 2) * (vyMax - 2) < 12) { - var v2 = - Vault.blank(vxMax - 2, vyMax - 2, req.smooth, VaultTile.bspfloor); - var v = Vault.blank(vxMax, vyMax, req.smooth, VaultTile.wall); + var v2 = Vault.blank( + vxMax - 2, vyMax - 2, VaultTile.bspfloor, req.smooth.clone()); + var v = Vault.blank(vxMax, vyMax, VaultTile.wall, req.smooth.clone()); v.blitFrom(v2, 1, 1); return v; } else { @@ -87,8 +89,7 @@ class Generator { var vyMaxRight = vyMax; if (smoothUpDown) { - vyMinRight = leftChild.vy; - vyMaxRight = leftChild.vy; + vyMaxRight = vyMinRight = leftChild.size.dy; } var rightReq = Requirement( @@ -106,14 +107,14 @@ class Generator { if (smoothUp) { var v = - Vault.blank(vxTotal, vyTotal, req.smooth.clone(), VaultTile.wall); + Vault.blank(vxTotal, vyTotal, VaultTile.wall, req.smooth.clone()); v.blitFrom(leftChild, 0, 0); v.blitFrom(rightChild, leftChild.vx - 1, 0); return v; } if (smoothDown) { var v = - Vault.blank(vxTotal, vyTotal, req.smooth.clone(), VaultTile.wall); + Vault.blank(vxTotal, vyTotal, VaultTile.wall, req.smooth.clone()); v.blitFrom(leftChild, 0, vyTotal - leftChild.vy); v.blitFrom(rightChild, leftChild.vx - 1, vyTotal - rightChild.vy); return v; @@ -125,7 +126,7 @@ class Generator { if (vyTMax > vyTotal) { vyTotal += _random.nextInt(vyTMax - vyTotal); } - var v = Vault.blank(vxTotal, vyTotal, req.smooth.clone(), VaultTile.wall); + var v = Vault.blank(vxTotal, vyTotal, VaultTile.wall, req.smooth.clone()); if (_random.nextBool()) { v.blitFrom(leftChild, 0, 0); v.blitFrom(rightChild, leftChild.vx - 1, vyTotal - rightChild.vy); @@ -173,32 +174,43 @@ class Generator { if (!vault.smooth.directions.containsAll(req.smooth.directions)) { return null; } + // NOTE: If the vault has metaBSP regions, and they touch the outer edge, then it should be possible + // to extend those regions + // Extending a metaBSP region results in _two_ metaBSP regions: + // a big version of the original, and a second one covering all the space left over + // in the set of rows or columns the metabsp region did not touch + // + // Ex: + // XXXX## + // XXXX # + // XXXX # + // # # + // ###### + // + // becomes + // + // XXXZYY + // XXXZYY + // XXXZYY + // XXXX## + // XXXX # + // XXXX # + // # # + // ###### + // + // (where the Zs are spaces that Xs and Ys both touch) + // + // Extension can happen more than once, on each axis: + // + // XXXXXZYY + // XXXXXZYY + // XXXXXZYY + // XXXXXX## + // XXXXXX # + // BBXXXX # + // AA# # + // AA###### + // return vault; - - /* - var rsd = req.smooth.directions; - Vault full = Vault.blank(req.vx, req.vy, req.smooth, VaultTile.wall); - int vx = vault.vx; - int dx; - if (rsd.contains(Direction.left)) { - dx = 0; - } else if (rsd.contains(Direction.right)) { - dx = req.vx - vx; - } else { - dx = _random.nextInt(req.vx - vx); - } - - int vy = vault.vy; - int dy; - if (rsd.contains(Direction.up)) { - dy = 0; - } else if (rsd.contains(Direction.down)) { - dy = req.vy - vy; - } else { - dy = _random.nextInt(req.vy - vy); - } - full.blitFrom(vault, dx, dy); - return full; - */ } } diff --git a/lib/gen/vault.dart b/lib/gen/vault.dart index b1e7c0b..ed21ec8 100644 --- a/lib/gen/vault.dart +++ b/lib/gen/vault.dart @@ -1,69 +1,36 @@ part of 'generator.dart'; class Vault { - final int vx, vy; + final Bitmap tiles; final DirectionSet smooth; - final List tiles; - Vault(this.vx, this.vy, this.smooth, this.tiles) { - assert(tiles.length == vx * vy); + geo.Size get size => tiles.size; + int get vx => size.dx; + int get vy => size.dy; + + Vault(this.tiles, this.smooth); + + static Vault blank(int vx, int vy, VaultTile lt, DirectionSet smooth) { + return Vault(Bitmap.blank(vx, vy, lt), smooth); } void clear(VaultTile lt) { - for (var y = 0; y < vy; y++) { - for (var x = 0; x < vx; x++) { - tiles[y * vx + x] = lt; - } - } + tiles.clear(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]; - } - } + tiles.blitFrom(other.tiles, dx, dy); } 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(vx, vy, smooth.flip(), tiles2); + return Vault(tiles.flip(), smooth.flip()); } - // TODO: Actually test this logic. - // This worked in Python, so it might even be right! Vault rotateRight() { - List tiles2 = [ - for (var x = 0; x < vx; x++) - for (var y = 0; y < vy; y++) tiles[(vy - 1 - y) * vx + x] - ]; - - return Vault(vy, vx, smooth.rotateRight(), tiles2); + return Vault(tiles.rotateRight(), 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 + (vx - 1 - x)] - ]; - - return Vault(vy, vx, smooth.rotateLeft(), tiles2); - } - - static Vault blank(int vx, int vy, DirectionSet smooth, VaultTile lt) { - var tiles = [ - for (var y = 0; y < vy; y++) - for (var x = 0; x < vx; x++) lt - ]; - - return Vault(vx, vy, smooth, tiles); + return Vault(tiles.rotateLeft(), smooth.rotateLeft()); } } diff --git a/lib/gen/vaults.dart b/lib/gen/vaults.dart index ffd0af7..a55beda 100644 --- a/lib/gen/vaults.dart +++ b/lib/gen/vaults.dart @@ -62,7 +62,7 @@ class Vaults { } } - return Vault(r.rect.width, r.rect.height, smooth, tiles); + return Vault(Bitmap(geo.Size(r.rect.width, r.rect.height), tiles), smooth); } }