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'; import 'package:dartterm/world/level.dart'; part 'direction.dart'; part 'direction_set.dart'; part 'orientation.dart'; part 'requirement.dart'; part 'vault.dart'; part 'vaults.dart'; const vaultTries = 30; class Generator { final math.Random _random; final Vaults _vaults; List _queue = []; Generator(this._random, this._vaults); Level generateLevel(Requirement requirement) { var out = _generateOriented(requirement, false); return _finalize(out); } /* Vault generateVault(Requirement requirement) { var out = _generateOriented(requirement, false); var (vault, (_, _)) = _finalize(out); return vault; } */ Vault _generateOriented(Requirement requirement, bool canBeVault) { if (canBeVault) { Vault? suggested = _suggest(vaultTries, requirement); if (suggested != null) { return _fillMetaRegions(requirement, suggested); } } // 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); if (req2.vyMax > (req2.vxMax - 2) * 3 / 2) { orientation = (orientation + 2) % 8; // rotate once more } // if only one of "left" and "right" needs to be smooth, prioritize right // as left is generated first req2 = unReorientRequirement(requirement, orientation); if (req2.smooth.directions.contains(Direction.left) && req2.smooth.directions.contains(Direction.right)) { orientation = (orientation + 4) % 8; } req2 = unReorientRequirement(requirement, orientation); var out2 = _generateBsp(req2); var out1 = reorientVault(out2, orientation); // 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); assert(out1.smooth.directions.containsAll(requirement.smooth.directions)); return out1; } Vault _generateBsp(Requirement req) { 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); } 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()); v.blitFrom(v2, 1, 1); return v; } else { 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); var vyMinRight = vyMin; var vyMaxRight = vyMax; if (smoothUpDown) { vyMaxRight = vyMinRight = leftChild.size.dy; } 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); 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()); 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()); 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()); 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; } } 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) { if (vault.vx > req.vxMax || vault.vy > req.vyMax) { return null; } if (vault.vx < req.vxMin || vault.vy < req.vyMin) { return null; } 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; } Vault _fillMetaRegions(Requirement requirement, Vault vault) { var geo.Size(:dx, :dy) = vault.size; var (metaregions, _) = regionalize(geo.Rect(0, 0, dx, dy), (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), vault.smooth.clone()); dest.blitFrom(vault, 0, 0); dest.blitFrom(inner, i.rect.x0, i.rect.y0); vault = dest; } return vault; } Level _finalize(Vault subj) { var vx = subj.vx, vy = subj.vy; 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; } 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); 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); } // == build doors == var (regions, toRegion) = regionalize(geo.Rect(0, 0, subj.vx, subj.vy), (x, y) { return walkable(subj.tiles.get(x, y)); }); // 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 double doorPoints(int x, int y) { return subj.tiles.get(x, y) == VaultTile.doorpronefloor ? 0.5 : 0.0; } List> 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(region0 != exitRegionId && region1 != exitRegionId, points, roomSize, _random.nextDouble()))); } } List> exitDoors = []; var minimalDoors = kruskal(regions.length, possibleDoors); for (var d in minimalDoors) { var (x, y) = d.value; subj.tiles.set(x, y, VaultTile.door); if (d.dst == exitRegionId || d.src == exitRegionId) { exitDoors.add(d); } } 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); } } } 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)); } } // components: // - is not exit (exit should be placed last so it doesn't get more than one door) // - points for placement // - size of the underlying room // - random factor double doorScore(bool isNotExit, 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 (isNotExit ? 1.0 : 0.0) * 1000000 + pointsForPlacement * 100000 + (100000 - roomSize).toDouble() + randomFactor; } int _manhattan(int x0, int y0, int x1, int y1) { return (x1 - x0).abs() + (y1 - y0).abs(); }