Door placement

This commit is contained in:
Pyrex 2023-09-21 19:29:34 -07:00
parent 01e316d979
commit 354a114e1c
10 changed files with 239 additions and 19 deletions

15
LICENSE.md Normal file
View File

@ -0,0 +1,15 @@
Incorporates GraphLab's implementation of Union/Find, which is by Richard Griffith. Here are the licensing terms of that:
> The BSD 2-Clause License
> http://www.opensource.org/licenses/bsd-license.php
>
> Copyright (c) 2013, Richard Griffith
> All rights reserved.
>
> Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
>
> Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
> Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
>
> The views and conclusions contained in the software and documentation are those of the author and should not be interpreted as representing official policies, either expressed or implied, of the FreeBSD Project.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 867 B

After

Width:  |  Height:  |  Size: 915 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 820 B

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 792 B

After

Width:  |  Height:  |  Size: 915 B

View File

@ -0,0 +1,33 @@
import 'package:dartterm/algorithms/union_find.dart';
class Edge<T> {
final int src, dst;
final T value;
final double score; // higher score is better
const Edge(this.src, this.dst, this.value, this.score);
}
List<Edge<T>> kruskal<T>(int nRegions, List<Edge<T>> srcEdges) {
var edges = List.from(srcEdges); // copy so we can mutate it
edges.sort((e0, e1) => -e0.score.compareTo(e1.score));
List<Edge<T>> spanningEdges = [];
var connected = UnionFind(nRegions);
for (var i = 0; i < edges.length; i++) {
var edge = edges[i];
if (connected.find(edge.src) == connected.find(edge.dst)) {
continue;
}
spanningEdges.add(edge);
connected.union(edge.src, edge.dst);
// break early if we run out of regions
nRegions -= 1;
if (nRegions == 1) {
break;
}
}
return spanningEdges;
}

View File

@ -26,7 +26,8 @@ class Region {
} }
} }
List<Region> regionalize(geo.Rect rect, bool Function(int, int) isAccessible) { (List<Region>, Map<(int, int), int>) regionalize(
geo.Rect rect, bool Function(int, int) isAccessible) {
int nextRegion = 0; int nextRegion = 0;
Map<(int, int), int> regions = {}; Map<(int, int), int> regions = {};
@ -62,7 +63,7 @@ List<Region> regionalize(geo.Rect rect, bool Function(int, int) isAccessible) {
} }
} }
} }
return _toExplicit(regions, nextRegion); return (_toExplicit(regions, nextRegion), regions);
} }
List<Region> _toExplicit(Map<(int, int), int> pointRegions, int nRegions) { List<Region> _toExplicit(Map<(int, int), int> pointRegions, int nRegions) {

View File

@ -0,0 +1,78 @@
// Copyright (c) 2012, scribeGriff (Richard Griffith)
// https://github.com/scribeGriff/graphlab
// All rights reserved. Please see the LICENSE.md file.
//
// Converted to modern Dart by Pyrex Panjakar.
/// A disjoint sets ADT implemented with a Union-Find data structure.
///
/// Performs union-by-rank and path compression. Elements are represented
/// by ints, numbered from zero.
///
/// Each disjoint set has one element designated as its root.
/// Negative values indicate the element is the root of a set. The absolute
/// value of a negative value is the number of elements in the set.
/// Positive values are an index to where the root was last known to be.
/// If the set has been unioned with another, the last known root will point
/// to a more recent root.
///
/// var a = new UnionFind(myGraph.length);
///
/// var u = a.find(myGraph[i][0]);
/// var v = a.find(myGraph[i][1]);
/// a.union(u, v);
///
/// Reference: Direct port of Mark Allen Weiss' UnionFind.java
class UnionFind {
late final List<int> array;
/// Construct a disjoint sets object.
///
/// numElements is the initial number of elements--also the initial
/// number of disjoint sets, since every element is initially in its
/// own set.
UnionFind(int numElements) {
// The array is zero based but the vertices are 1 based,
// so we extend the array by 1 element to account for this.
array = [for (var i = 0; i < numElements; i++) -1];
}
/// union() unites two disjoint sets into a single set. A union-by-rank
/// heuristic is used to choose the new root.
///
/// a is an element in the first set.
/// b is an element in the first set.
void union(int a, int b) {
int rootA = find(a);
int rootB = find(b);
if (rootA == rootB) return;
if (array[rootB] < array[rootA]) {
// root_b has more elements, so leave it as the root.
// first, indicate that the set represented by root_b has grown.
array[rootB] += array[rootA];
// Then, point the root of set a at set b.
array[rootA] = rootB;
} else {
array[rootA] += array[rootB];
array[rootB] = rootA;
}
}
/// find() finds the (int) name of the set containing a given element.
/// Performs path compression along the way.
///
/// x is the element sought.
/// returns the set containing x.
int find(int x) {
if (array[x] < 0) {
return x; // x is the root of the tree; return it
} else {
// Find out who the root is; compress path by making the root
// x's parent.
array[x] = find(array[x]);
return array[x]; // Return the root
}
}
}

View File

@ -1,6 +1,7 @@
import 'dart:math' as math; import 'dart:math' as math;
import 'package:dartterm/assets.dart'; import 'package:dartterm/assets.dart';
import 'package:dartterm/algorithms/geometry.dart' as geo; import 'package:dartterm/algorithms/geometry.dart' as geo;
import 'package:dartterm/colors.dart';
import 'package:dartterm/gen/generator.dart'; import 'package:dartterm/gen/generator.dart';
import 'package:dartterm/input.dart'; import 'package:dartterm/input.dart';
import 'package:dartterm/skreek.dart'; import 'package:dartterm/skreek.dart';
@ -38,11 +39,12 @@ void main() async {
var cursor = at(x * 2, y * 2).big(); var cursor = at(x * 2, y * 2).big();
switch (output.tiles.get(x, y)) { switch (output.tiles.get(x, y)) {
case VaultTile.bspfloor: case VaultTile.bspfloor:
cursor.puts(" ");
case VaultTile.floor: case VaultTile.floor:
cursor.puts("."); cursor.puts(" ");
case VaultTile.doorpronefloor:
cursor.puts("-");
case VaultTile.door: case VaultTile.door:
cursor.puts("d"); cursor.fg(Palette.subtitle).puts("+");
case VaultTile.wall: case VaultTile.wall:
case VaultTile.defaultwall: case VaultTile.defaultwall:
cursor.puts("#"); cursor.puts("#");
@ -60,7 +62,7 @@ void main() async {
} }
inpLoop: inpLoop:
await for (var inp in rawInput()) { await for (var inp in rawInput()) {
skreek("$inp"); skreek("$inp $seed");
switch (inp) { switch (inp) {
case Keystroke(text: "a"): case Keystroke(text: "a"):
seed -= 1; seed -= 1;

View File

@ -2,6 +2,7 @@ import 'dart:math' as math;
import 'package:dartterm/algorithms/geometry.dart' as geo; import 'package:dartterm/algorithms/geometry.dart' as geo;
import 'package:dartterm/algorithms/regionalize.dart'; import 'package:dartterm/algorithms/regionalize.dart';
import 'package:dartterm/algorithms/kruskal.dart';
import 'package:dartterm/bitmap.dart'; import 'package:dartterm/bitmap.dart';
import 'package:dartterm/skreek.dart'; import 'package:dartterm/skreek.dart';
@ -222,7 +223,7 @@ class Generator {
Vault _fillMetaRegions(Requirement requirement, Vault vault) { Vault _fillMetaRegions(Requirement requirement, Vault vault) {
var geo.Size(:dx, :dy) = vault.size; var geo.Size(:dx, :dy) = vault.size;
var metaregions = regionalize(geo.Rect(0, 0, dx, dy), var (metaregions, _) = regionalize(geo.Rect(0, 0, dx, dy),
(x, y) => vault.tiles.get(x, y) == VaultTile.meta0); (x, y) => vault.tiles.get(x, y) == VaultTile.meta0);
for (var i in metaregions) { for (var i in metaregions) {
@ -249,22 +250,25 @@ class Generator {
Vault _finalize(Vault subj) { Vault _finalize(Vault subj) {
var vx = subj.vx, vy = subj.vy; var vx = subj.vx, vy = subj.vy;
subj = Vault.blankWith(vx, vy, (x, y) {
var bed = VaultTile.defaultwall;
if (x == 0 || x == vx - 1 || y == 0 || y == vy - 1) {
bed = VaultTile.wall;
}
var tile = mergeVaultTile(bed, subj.tiles.get(x, y)!);
return tile;
}, subj.smooth.clone());
bool canSupportArch(VaultTile? tile) { var orthoOffsets = [(0, -1), (0, 1), (-1, 0), (1, 0)];
// == build arches ==
bool floorlike(VaultTile? tile) {
return tile == VaultTile.bspfloor || return tile == VaultTile.bspfloor ||
tile == VaultTile.floor || tile == VaultTile.floor ||
tile == VaultTile.doorpronefloor ||
tile == VaultTile.exit; tile == VaultTile.exit;
} }
var orthoOffsets = [(0, -1), (0, 1), (-1, 0), (1, 0)]; bool walkable(VaultTile? tile) {
return tile == VaultTile.bspfloor ||
tile == VaultTile.floor ||
tile == VaultTile.doorpronefloor ||
tile == VaultTile.exit ||
tile == VaultTile.door;
}
List<(int, int)> newArches = []; List<(int, int)> newArches = [];
for (int x = 0; x < vx; x++) { for (int x = 0; x < vx; x++) {
for (int y = 0; y < vy; y++) { for (int y = 0; y < vy; y++) {
@ -273,7 +277,7 @@ class Generator {
var supporters = 0; var supporters = 0;
for (var (dx, dy) in orthoOffsets) { for (var (dx, dy) in orthoOffsets) {
VaultTile? neighbor = subj.tiles.get(x + dx, y + dy); VaultTile? neighbor = subj.tiles.get(x + dx, y + dy);
if (canSupportArch(neighbor)) { if (floorlike(neighbor)) {
supporters++; supporters++;
} }
} }
@ -292,6 +296,88 @@ class Generator {
for (var (ax, ay) in newArches) { for (var (ax, ay) in newArches) {
subj.tiles.set(ax, ay, VaultTile.floor); subj.tiles.set(ax, ay, VaultTile.floor);
} }
// == build doors ==
var (regions, toRegion) =
regionalize(geo.Rect(0, 0, subj.vx, subj.vy), (x, y) {
return walkable(subj.tiles.get(x, y));
});
double doorPoints(int x, int y) {
return subj.tiles.get(x, y) == VaultTile.doorpronefloor ? 0.5 : 0.0;
}
//
List<Edge<(int, int)>> possibleDoors = [];
for (var x = 0; x < subj.vx; x++) {
for (var y = 0; y < subj.vy; y++) {
double points;
int region0, region1;
if (subj.tiles.get(x, y) != VaultTile.wall) {
continue;
}
var regionL = toRegion[(x - 1, y)];
var regionR = toRegion[(x + 1, y)];
var regionU = toRegion[(x, y - 1)];
var regionD = toRegion[(x, y + 1)];
if (regionL != null &&
regionR != null &&
regionU == null &&
regionD == null) {
(region0, region1) = (regionL, regionR);
points = doorPoints(x - 1, y) + doorPoints(x + 1, y);
} else if (regionL == null &&
regionR == null &&
regionU != null &&
regionD != null) {
(region0, region1) = (regionU, regionD);
points = doorPoints(x, y - 1) + doorPoints(x, y + 1);
} else {
continue;
}
if (region0 == region1) {
continue;
}
int roomSize = math.min(
regions[region0].points.length,
regions[region1].points.length,
);
possibleDoors.add(Edge(region0, region1, (x, y),
doorScore(points, roomSize, _random.nextDouble())));
}
}
var minimalDoors = kruskal(regions.length, possibleDoors);
for (var d in minimalDoors) {
var (x, y) = d.value;
subj.tiles.set(x, y, VaultTile.door);
}
for (var x = 0; x < subj.vx; x++) {
for (var y = 0; y < subj.vy; y++) {
if (subj.tiles.get(x, y) == VaultTile.doorpronefloor) {
subj.tiles.set(x, y, VaultTile.floor);
}
}
}
return subj; return subj;
} }
} }
// components:
// - points for placement
// - size of the underlying room
// - random factor
double doorScore(double pointsForPlacement, int roomSize, double randomFactor) {
assert(pointsForPlacement >= 0.0 && pointsForPlacement <= 1.0);
assert(roomSize >= 0 && roomSize < 100000);
assert(randomFactor >= 0.0 && randomFactor < 1.0);
return pointsForPlacement * 100000 +
(100000 - roomSize).toDouble() +
randomFactor;
}

View File

@ -14,7 +14,8 @@ class Vaults {
static Future<Vaults> load(String name) async { static Future<Vaults> load(String name) async {
var basis = await Bitmap.load(name, colorToVaultTile); var basis = await Bitmap.load(name, colorToVaultTile);
var regions = regionalize(basis.rect, (x, y) => basis.get(x, y) != null); var (regions, _) =
regionalize(basis.rect, (x, y) => basis.get(x, y) != null);
var vs = Vaults(); var vs = Vaults();
@ -73,6 +74,8 @@ enum VaultTile {
door, door,
bspfloor, bspfloor,
floor, floor,
doorpronefloor,
defaultwall, // defaultwall is in generated rooms and is overwritten by anything defaultwall, // defaultwall is in generated rooms and is overwritten by anything
archpronewall, // archpronewall cannot overwrite wall or archwall archpronewall, // archpronewall cannot overwrite wall or archwall
archwall, // archwall cannot overwrite wall. archwall, // archwall cannot overwrite wall.
@ -98,6 +101,8 @@ VaultTile? colorToVaultTile(int c) {
return VaultTile.archwall; return VaultTile.archwall;
case 0x7F4000: case 0x7F4000:
return VaultTile.archpronewall; return VaultTile.archpronewall;
case 0xBCCFFF:
return VaultTile.doorpronefloor;
case 0xFFFFFF: case 0xFFFFFF:
case 0xFFFF00: case 0xFFFF00:
case 0xFF00FF: case 0xFF00FF: