dartterm/lib/gen/generator.dart

468 lines
14 KiB
Dart
Raw Normal View History

import 'dart:math' as math;
import 'package:dartterm/algorithms/geometry.dart' as geo;
2023-09-21 03:56:30 +00:00
import 'package:dartterm/algorithms/regionalize.dart';
2023-09-22 02:29:34 +00:00
import 'package:dartterm/algorithms/kruskal.dart';
2023-09-21 03:56:30 +00:00
import 'package:dartterm/bitmap.dart';
import 'package:dartterm/skreek.dart';
2023-09-23 00:55:29 +00:00
import 'package:dartterm/world/level.dart';
part 'direction.dart';
part 'direction_set.dart';
part 'orientation.dart';
part 'requirement.dart';
part 'vault.dart';
2023-09-23 01:35:47 +00:00
part 'vault_tile.dart';
part 'vaults.dart';
const vaultTries = 30;
2023-09-21 02:30:03 +00:00
class Generator {
final math.Random _random;
final Vaults _vaults;
2023-09-21 02:30:03 +00:00
List<Vault> _queue = [];
Generator(this._random, this._vaults);
2023-09-23 00:55:29 +00:00
Level generateLevel(Requirement requirement) {
var out = _generateOriented(requirement, false);
return _finalize(out);
}
2023-09-23 00:55:29 +00:00
/*
Vault generateVault(Requirement requirement) {
var out = _generateOriented(requirement, false);
var (vault, (_, _)) = _finalize(out);
return vault;
}
*/
Vault _generateOriented(Requirement requirement, bool canBeVault) {
2023-09-21 05:00:45 +00:00
if (canBeVault) {
Vault? suggested = _suggest(vaultTries, requirement);
if (suggested != null) {
2023-09-21 23:11:34 +00:00
return _fillMetaRegions(requirement, suggested);
2023-09-21 05:00:45 +00:00
}
2023-09-21 02:30:03 +00:00
}
// 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 = unReorientRequirement(requirement, orientation);
2023-09-21 05:00:45 +00:00
if (req2.vyMax > (req2.vxMax - 2) * 3 / 2) {
orientation = (orientation + 2) % 8; // rotate once more
}
2023-09-21 05:00:45 +00:00
// if only one of "left" and "right" needs to be smooth, prioritize right
// as left is generated first
req2 = unReorientRequirement(requirement, orientation);
2023-09-21 05:00:45 +00:00
if (req2.smooth.directions.contains(Direction.left) &&
req2.smooth.directions.contains(Direction.right)) {
orientation = (orientation + 4) % 8;
}
2023-09-21 05:00:45 +00:00
req2 = unReorientRequirement(requirement, orientation);
var out2 = _generateBsp(req2);
var out1 = reorientVault(out2, orientation);
2023-09-21 03:56:30 +00:00
// log("$orientation ${requirement.vx} ${requirement.vy} ${req2.vx} ${req2.vy} ${out2.vx} ${out2.vy} ${out1.vx} ${out1.vy}");
var geo.Size(:dx, :dy) = out1.size;
assert(dx >= requirement.vxMin && dx <= requirement.vxMax);
assert(dy >= requirement.vyMin && dy <= requirement.vyMax);
2023-09-21 05:00:45 +00:00
assert(out1.smooth.directions.containsAll(requirement.smooth.directions));
return out1;
}
Vault _generateBsp(Requirement req) {
2023-09-21 05:00:45 +00:00
var vxMin = req.vxMin;
var vyMin = req.vyMin;
var vxMax = req.vxMax;
var vyMax = req.vyMax;
var smoothUp = req.smooth.directions.contains(Direction.up);
var smoothDown = req.smooth.directions.contains(Direction.down);
var smoothUpDown = smoothUp && smoothDown;
// var vxRand = _random.nextInt(vxMax - vxMin) + vxMin;
var vyRand = _random.nextInt(vyMax + 1 - vyMin) + vyMin;
if (vxMax < 2 || vyMax < 2) {
return Vault.blank(vxMax, vyRand, VaultTile.defaultwall, req.smooth);
2023-09-21 05:00:45 +00:00
} else if (vxMax < 9 || (vxMax - 2) * (vyMax - 2) < 12) {
var v2 = Vault.blank(
vxMax - 2, vyMax - 2, VaultTile.bspfloor, req.smooth.clone());
var v = Vault.blank(vxMax, vyMax, VaultTile.wall, req.smooth.clone());
2023-09-21 04:08:46 +00:00
v.blitFrom(v2, 1, 1);
2023-09-21 05:00:45 +00:00
return v;
} else {
2023-09-21 05:00:45 +00:00
var leftReq = Requirement(
math.max(vxMin - 4, 2), vxMax - 4, vyMin, vyMax, req.smooth.clone());
leftReq.smooth.directions.add(Direction.right);
var leftChild = _generateOriented(leftReq, true);
2023-09-21 05:00:45 +00:00
var vyMinRight = vyMin;
var vyMaxRight = vyMax;
if (smoothUpDown) {
vyMaxRight = vyMinRight = leftChild.size.dy;
2023-09-21 05:00:45 +00:00
}
var rightReq = Requirement(
vxMin - (leftChild.vx - 1),
vxMax - (leftChild.vx - 1),
vyMinRight,
vyMaxRight,
req.smooth.clone(),
);
rightReq.smooth.directions.add(Direction.left);
var rightChild = _generateOriented(rightReq, true);
2023-09-21 05:00:45 +00:00
var vxTotal = leftChild.vx + rightChild.vx - 1;
var vyTotal = math.max(leftChild.vy, rightChild.vy);
if (smoothUp) {
var v = Vault.blank(
vxTotal, vyTotal, VaultTile.defaultwall, req.smooth.clone());
2023-09-21 05:00:45 +00:00
v.blitFrom(leftChild, 0, 0);
v.blitFrom(rightChild, leftChild.vx - 1, 0);
return v;
}
if (smoothDown) {
var v = Vault.blank(
vxTotal, vyTotal, VaultTile.defaultwall, req.smooth.clone());
2023-09-21 05:00:45 +00:00
v.blitFrom(leftChild, 0, vyTotal - leftChild.vy);
v.blitFrom(rightChild, leftChild.vx - 1, vyTotal - rightChild.vy);
return v;
}
// no smoothing reqs
// min: ensure some overlap
var vyTMax = math.min(vyMax, leftChild.vy + rightChild.vy - 3);
if (vyTMax > vyTotal) {
vyTotal += _random.nextInt(vyTMax - vyTotal);
}
var v = Vault.blank(
vxTotal, vyTotal, VaultTile.defaultwall, req.smooth.clone());
2023-09-21 05:00:45 +00:00
if (_random.nextBool()) {
v.blitFrom(leftChild, 0, 0);
v.blitFrom(rightChild, leftChild.vx - 1, vyTotal - rightChild.vy);
} else {
v.blitFrom(leftChild, 0, vyTotal - leftChild.vy);
v.blitFrom(rightChild, leftChild.vx - 1, 0);
}
return v;
}
}
2023-09-21 02:30:03 +00:00
Vault? _suggest(int tries, Requirement req) {
for (var i = 0; i < tries; i++) {
var sugg = _popSuggestion();
if (sugg == null) {
return null;
}
sugg = reorientVault(sugg, randomOrientation(_random));
sugg = _tidy(sugg, req);
if (sugg != null) {
return sugg;
}
}
return null;
}
Vault? _popSuggestion() {
if (_queue.isEmpty) {
_queue = _vaults.randomFlight(_random);
}
if (_queue.isEmpty) {
return null;
}
return _queue.removeLast();
}
Vault? _tidy(Vault vault, Requirement req) {
2023-09-21 05:00:45 +00:00
if (vault.vx > req.vxMax || vault.vy > req.vyMax) {
2023-09-21 02:30:03 +00:00
return null;
}
2023-09-21 05:00:45 +00:00
if (vault.vx < req.vxMin || vault.vy < req.vyMin) {
2023-09-21 02:30:03 +00:00
return null;
}
2023-09-21 03:56:30 +00:00
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######
//
2023-09-21 05:00:45 +00:00
return vault;
2023-09-21 02:30:03 +00:00
}
2023-09-21 23:11:34 +00:00
Vault _fillMetaRegions(Requirement requirement, Vault vault) {
var geo.Size(:dx, :dy) = vault.size;
2023-09-22 02:29:34 +00:00
var (metaregions, _) = regionalize(geo.Rect(0, 0, dx, dy),
2023-09-21 23:11:34 +00:00
(x, y) => vault.tiles.get(x, y) == VaultTile.meta0);
for (var i in metaregions) {
assert(i.isRectangle);
var sz = i.rect.size;
// TODO: Relax these based on our environs -- for instance, if one of our sides doesn't need to be smooth, that metaregion doesn't either
var metaRequirement = Requirement(
sz.dx,
sz.dx,
sz.dy,
sz.dy,
DirectionSet(
{Direction.up, Direction.left, Direction.down, Direction.right}));
var inner = _generateOriented(metaRequirement, true);
var dest = Vault(Bitmap.blank(vault.vx, vault.vy, VaultTile.defaultwall),
2023-09-21 23:11:34 +00:00
vault.smooth.clone());
dest.blitFrom(vault, 0, 0);
dest.blitFrom(inner, i.rect.x0, i.rect.y0);
vault = dest;
}
return vault;
}
2023-09-23 00:55:29 +00:00
Level _finalize(Vault subj) {
var vx = subj.vx, vy = subj.vy;
2023-09-22 02:29:34 +00:00
var orthoOffsets = [(0, -1), (0, 1), (-1, 0), (1, 0)];
// == build arches ==
bool floorlike(VaultTile? tile) {
return tile == VaultTile.bspfloor ||
tile == VaultTile.floor ||
2023-09-22 02:29:34 +00:00
tile == VaultTile.doorpronefloor ||
tile == VaultTile.exit;
}
2023-09-22 02:29:34 +00:00
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++) {
var t = subj.tiles.get(x, y);
if (t == VaultTile.archwall) {
var supporters = 0;
for (var (dx, dy) in orthoOffsets) {
VaultTile? neighbor = subj.tiles.get(x + dx, y + dy);
2023-09-22 02:29:34 +00:00
if (floorlike(neighbor)) {
supporters++;
}
}
if (supporters == 2) {
newArches.add((x, y));
}
subj.tiles.set(x, y, VaultTile.wall);
}
if (t == VaultTile.archpronewall || t == VaultTile.defaultwall) {
subj.tiles.set(x, y, VaultTile.wall);
}
}
}
for (var (ax, ay) in newArches) {
subj.tiles.set(ax, ay, VaultTile.floor);
}
2023-09-22 02:29:34 +00:00
// == build doors ==
var (regions, toRegion) =
regionalize(geo.Rect(0, 0, subj.vx, subj.vy), (x, y) {
return walkable(subj.tiles.get(x, y));
});
2023-09-23 00:55:29 +00:00
// generate one fake region for the exit doors to be in
Set<(int, int)> exitRegion = {};
for (var x = -2; x < subj.vx + 2; x++) {
exitRegion.add((x, -1));
exitRegion.add((x, subj.vy));
}
for (var y = -2; y < subj.vy + 2; y++) {
exitRegion.add((-1, y));
exitRegion.add((subj.vx, y));
}
int exitRegionId = regions.length;
for (var (x, y) in exitRegion) {
toRegion[(x, y)] = exitRegionId;
}
regions.add(Region.fromNonEmptySet(exitRegion));
// OK: now build the doors
2023-09-22 02:29:34 +00:00
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,
);
2023-09-23 00:55:29 +00:00
possibleDoors.add(Edge(
region0,
region1,
(x, y),
doorScore(region0 != exitRegionId && region1 != exitRegionId,
points, roomSize, _random.nextDouble())));
2023-09-22 02:29:34 +00:00
}
}
2023-09-23 00:55:29 +00:00
List<Edge<(int, int)>> exitDoors = [];
2023-09-22 02:29:34 +00:00
var minimalDoors = kruskal(regions.length, possibleDoors);
for (var d in minimalDoors) {
var (x, y) = d.value;
subj.tiles.set(x, y, VaultTile.door);
2023-09-23 00:55:29 +00:00
if (d.dst == exitRegionId || d.src == exitRegionId) {
exitDoors.add(d);
}
2023-09-22 02:29:34 +00:00
}
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);
}
}
}
2023-09-23 00:55:29 +00:00
if (exitDoors.length != 1) {
throw Exception("should be exactly one exit door");
}
// == Build the exit area ==
var (exitX, exitY) = exitDoors[0].value;
int exitVaultX, exitVaultY;
Vault finalVault;
int vaultBlitX, vaultBlitY;
if (exitX == 0 || exitX == vx - 1) {
finalVault =
Vault.blank(vx + 3, vy, VaultTile.defaultwall, DirectionSet({}));
vaultBlitX = exitX == 0 ? 3 : 0;
vaultBlitY = 0;
exitVaultX = exitX == 0 ? 1 : vx + 1;
exitVaultY = exitY;
} else if (exitY == 0 || exitY == vy - 1) {
finalVault =
Vault.blank(vx, vy + 3, VaultTile.defaultwall, DirectionSet({}));
vaultBlitX = 0;
vaultBlitY = exitY == 0 ? 3 : 0;
exitVaultX = exitX;
exitVaultY = exitY == 0 ? 1 : vy + 1;
} else {
throw Exception("exit door in invalid position $exitX $exitY $vx $vy");
}
for (var x = exitVaultX - 1; x <= exitVaultX + 1; x++) {
for (var y = exitVaultY - 1; y <= exitVaultY + 1; y++) {
finalVault.tiles.set(x, y, VaultTile.exit);
if (x == exitVaultX && y == exitVaultY ||
_manhattan(x, y, vaultBlitX + exitX, vaultBlitY + exitY) == 1) {
finalVault.tiles.set(x, y, VaultTile.floor);
}
}
}
finalVault.blitFrom(subj, vaultBlitX, vaultBlitY);
return Level(
Bitmap.blankWith(finalVault.vx, finalVault.vy,
(x, y) => flattenVaultTile(finalVault.tiles.get(x, y)!)),
geo.Offset(exitVaultX, exitVaultY));
}
}
2023-09-22 02:29:34 +00:00
// components:
2023-09-23 00:55:29 +00:00
// - is not exit (exit should be placed last so it doesn't get more than one door)
2023-09-22 02:29:34 +00:00
// - points for placement
// - size of the underlying room
// - random factor
2023-09-23 00:55:29 +00:00
double doorScore(bool isNotExit, double pointsForPlacement, int roomSize,
double randomFactor) {
2023-09-22 02:29:34 +00:00
assert(pointsForPlacement >= 0.0 && pointsForPlacement <= 1.0);
assert(roomSize >= 0 && roomSize < 100000);
assert(randomFactor >= 0.0 && randomFactor < 1.0);
2023-09-23 00:55:29 +00:00
return (isNotExit ? 1.0 : 0.0) * 1000000 +
pointsForPlacement * 100000 +
2023-09-22 02:29:34 +00:00
(100000 - roomSize).toDouble() +
randomFactor;
}
2023-09-23 00:55:29 +00:00
int _manhattan(int x0, int y0, int x1, int y1) {
return (x1 - x0).abs() + (y1 - y0).abs();
}