Door placement
This commit is contained in:
33
lib/algorithms/kruskal.dart
Normal file
33
lib/algorithms/kruskal.dart
Normal 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;
|
||||
}
|
@ -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;
|
||||
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) {
|
||||
|
78
lib/algorithms/union_find.dart
Normal file
78
lib/algorithms/union_find.dart
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import 'dart:math' as math;
|
||||
import 'package:dartterm/assets.dart';
|
||||
import 'package:dartterm/algorithms/geometry.dart' as geo;
|
||||
import 'package:dartterm/colors.dart';
|
||||
import 'package:dartterm/gen/generator.dart';
|
||||
import 'package:dartterm/input.dart';
|
||||
import 'package:dartterm/skreek.dart';
|
||||
@ -38,11 +39,12 @@ void main() async {
|
||||
var cursor = at(x * 2, y * 2).big();
|
||||
switch (output.tiles.get(x, y)) {
|
||||
case VaultTile.bspfloor:
|
||||
cursor.puts(" ");
|
||||
case VaultTile.floor:
|
||||
cursor.puts(".");
|
||||
cursor.puts(" ");
|
||||
case VaultTile.doorpronefloor:
|
||||
cursor.puts("-");
|
||||
case VaultTile.door:
|
||||
cursor.puts("d");
|
||||
cursor.fg(Palette.subtitle).puts("+");
|
||||
case VaultTile.wall:
|
||||
case VaultTile.defaultwall:
|
||||
cursor.puts("#");
|
||||
@ -60,7 +62,7 @@ void main() async {
|
||||
}
|
||||
inpLoop:
|
||||
await for (var inp in rawInput()) {
|
||||
skreek("$inp");
|
||||
skreek("$inp $seed");
|
||||
switch (inp) {
|
||||
case Keystroke(text: "a"):
|
||||
seed -= 1;
|
||||
|
@ -2,6 +2,7 @@ import 'dart:math' as math;
|
||||
|
||||
import 'package:dartterm/algorithms/geometry.dart' as geo;
|
||||
import 'package:dartterm/algorithms/regionalize.dart';
|
||||
import 'package:dartterm/algorithms/kruskal.dart';
|
||||
import 'package:dartterm/bitmap.dart';
|
||||
import 'package:dartterm/skreek.dart';
|
||||
|
||||
@ -222,7 +223,7 @@ class Generator {
|
||||
|
||||
Vault _fillMetaRegions(Requirement requirement, Vault vault) {
|
||||
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);
|
||||
|
||||
for (var i in metaregions) {
|
||||
@ -249,22 +250,25 @@ class Generator {
|
||||
|
||||
Vault _finalize(Vault subj) {
|
||||
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 ||
|
||||
tile == VaultTile.floor ||
|
||||
tile == VaultTile.doorpronefloor ||
|
||||
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 = [];
|
||||
for (int x = 0; x < vx; x++) {
|
||||
for (int y = 0; y < vy; y++) {
|
||||
@ -273,7 +277,7 @@ class Generator {
|
||||
var supporters = 0;
|
||||
for (var (dx, dy) in orthoOffsets) {
|
||||
VaultTile? neighbor = subj.tiles.get(x + dx, y + dy);
|
||||
if (canSupportArch(neighbor)) {
|
||||
if (floorlike(neighbor)) {
|
||||
supporters++;
|
||||
}
|
||||
}
|
||||
@ -292,6 +296,88 @@ class Generator {
|
||||
for (var (ax, ay) in newArches) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
@ -14,7 +14,8 @@ class Vaults {
|
||||
static Future<Vaults> load(String name) async {
|
||||
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();
|
||||
|
||||
@ -73,6 +74,8 @@ enum VaultTile {
|
||||
door,
|
||||
bspfloor,
|
||||
floor,
|
||||
doorpronefloor,
|
||||
|
||||
defaultwall, // defaultwall is in generated rooms and is overwritten by anything
|
||||
archpronewall, // archpronewall cannot overwrite wall or archwall
|
||||
archwall, // archwall cannot overwrite wall.
|
||||
@ -98,6 +101,8 @@ VaultTile? colorToVaultTile(int c) {
|
||||
return VaultTile.archwall;
|
||||
case 0x7F4000:
|
||||
return VaultTile.archpronewall;
|
||||
case 0xBCCFFF:
|
||||
return VaultTile.doorpronefloor;
|
||||
case 0xFFFFFF:
|
||||
case 0xFFFF00:
|
||||
case 0xFF00FF:
|
||||
|
Reference in New Issue
Block a user