Compare commits

..

12 Commits

Author SHA1 Message Date
8273243db9 Move vault tiles to a separate file 2023-09-22 17:56:37 -07:00
1b4240e430 Exit vaults, level type 2023-09-22 17:55:29 -07:00
d489810154 Just screwing around 2023-09-21 20:09:03 -07:00
354a114e1c Door placement 2023-09-21 19:29:34 -07:00
01e316d979 Add arches, which demand to be collapsed 2023-09-21 17:33:48 -07:00
9ad13f7a5a Metaregions 1 2023-09-21 16:11:34 -07:00
ae0c62b010 Add and pervasively use grid type (Bitmap) 2023-09-21 15:37:12 -07:00
5de409515e Holy shit! It works 2023-09-20 22:00:45 -07:00
757cca1392 BSP: include walls in room boundary 2023-09-20 21:08:46 -07:00
c4c2b26653 Well, it's sort of working 2023-09-20 20:56:30 -07:00
b5466919e6 Use vaults if we have them 2023-09-20 19:30:03 -07:00
0a600fd930 Break up the vaults code into multiple files 2023-09-20 18:09:08 -07:00
26 changed files with 1281 additions and 319 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: 4.9 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 915 B

View File

@ -0,0 +1,66 @@
// 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
import 'package:dartterm/skreek.dart';
class Size {
final int dx;
final int dy;
Size(this.dx, this.dy) {
assert(dx >= 0);
assert(dy >= 0);
}
@override
String toString() {
return "$dx x $dy";
}
}
class Offset {
final int x;
final int y;
const Offset(this.x, this.y);
@override
String toString() {
return "@($x, $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);
}
Size get size => Size(dx, dy);
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;
}
@override
String toString() {
return "@($x0, $y0) $size";
}
}

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

@ -0,0 +1,76 @@
import 'dart:math' as math;
import 'package:dartterm/algorithms/geometry.dart' as geo;
class Region {
final geo.Rect rect;
final Set<(int, int)> points;
bool get isRectangle => points.length == rect.dx * rect.dy;
Region(this.rect, this.points);
static Region fromNonEmptySet(Set<(int, int)> s) {
assert(s.isNotEmpty);
int xMin = s.map<int>((xy) => xy.$1).reduce(math.min);
int yMin = s.map<int>((xy) => xy.$2).reduce(math.min);
int xMax = s.map<int>((xy) => xy.$1).reduce(math.max);
int yMax = s.map<int>((xy) => xy.$2).reduce(math.max);
var rect = geo.Rect(xMin, yMin, xMax - xMin + 1, yMax - yMin + 1);
return Region(rect, s);
}
@override
String toString() {
return "Region($rect, $points)";
}
}
(List<Region>, Map<(int, int), int>) 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(geo.Offset(x, y))) {
return workDone;
}
if (regions[(x, y)] != null) {
return workDone;
}
if (!isAccessible(x, y)) {
return workDone;
}
regions[(x, y)] = nextRegion;
workDone += 1;
workDone += floodfill(x - 1, y);
workDone += floodfill(x + 1, y);
workDone += floodfill(x, y - 1);
workDone += floodfill(x, y + 1);
return workDone;
}
// 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.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;
}
}
}
}
return (_toExplicit(regions, nextRegion), regions);
}
List<Region> _toExplicit(Map<(int, int), int> pointRegions, int nRegions) {
List<Set<(int, int)>> regionPoints = [for (var i = 0; i < nRegions; i++) {}];
for (var MapEntry(key: (x, y), value: id_) in pointRegions.entries) {
regionPoints[id_].add((x, y));
}
return [for (var s in regionPoints) Region.fromNonEmptySet(s)];
}

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
}
}
}

152
lib/bitmap.dart Normal file
View File

@ -0,0 +1,152 @@
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:dartterm/algorithms/geometry.dart' as geo;
import 'package:flutter/services.dart';
class Bitmap<T> {
// This idiosyncratic usage of "bitmap" comes from some other technology.
// What I'm saying is "don't blame me"
final geo.Size size;
final List<T> data;
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<T> blankWith<T>(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<T> blank<T>(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<Bitmap<T>> load<T>(String name, T Function(int) cb) async {
final assetImageByteData = await rootBundle.load(name);
final codec =
await ui.instantiateImageCodec(assetImageByteData.buffer.asUint8List());
final image = (await codec.getNextFrame()).image;
final bytedata =
(await image.toByteData(format: ui.ImageByteFormat.rawStraightRgba))!;
final sx = image.width;
final sy = image.height;
final List<T> data = [];
for (var i = 0; i < sx * sy; i++) {
var pixel = bytedata.getUint32(i * 4, Endian.big);
data.add(cb(pixel));
}
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) {
if (x < 0 || y < 0 || x >= size.dx || y >= size.dy) {
return null;
}
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<T> 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];
}
}
}
void blitFromWith(Bitmap<T> other, int dx, int dy, T Function(T, T) merge) {
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)] =
merge(data[(y + dy) * myW + (x + dx)], other.data[y * otW + x]);
}
}
}
Bitmap<T> flip() {
var geo.Size(:dx, :dy) = size;
List<T> 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<T> rotateRight() {
var geo.Size(:dx, :dy) = size;
List<T> 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<T> rotateLeft() {
var geo.Size(:dx, :dy) = size;
List<T> 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);
}
}

View File

@ -5,4 +5,7 @@ class Palette {
static const defaultFg = Colors.white; static const defaultFg = Colors.white;
static const subtitle = Colors.red; static const subtitle = Colors.red;
static const demoDoor = Colors.red;
static const demoExit = Colors.red;
} }

View File

@ -1,22 +1,24 @@
import 'dart:developer';
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/colors.dart'; 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/skreek.dart';
import 'package:dartterm/terminal.dart'; import 'package:dartterm/terminal.dart';
import 'package:dartterm/world/level.dart'; import 'package:dartterm/world/level.dart';
void main() async { void main() async {
Vaults vaults; Vaults vaults;
while (true) { while (true) {
log("about to load template"); skreek("about to load template");
at(0, 0).clear(); at(0, 0).clear();
at(0, 0).puts("Loading template!"); at(0, 0).puts("Loading template!");
Vaults? maybeVaults = Vaults? maybeVaults =
getVaultsIfAvailable("assets/images/wfc/bighouse2.png"); getVaultsIfAvailable("assets/images/vaults/house1.png");
if (maybeVaults != null) { if (maybeVaults != null) {
log("wasn't null!"); skreek("wasn't null!");
vaults = maybeVaults; vaults = maybeVaults;
break; break;
} }
@ -29,36 +31,51 @@ void main() async {
int seed = 0; int seed = 0;
while (true) { while (true) {
Vault output = vaults.generateBoxed( clear();
math.Random(seed), Level output =
Requirement( Generator(math.Random(seed), vaults).generateLevel(Requirement(
16, 16,
32,
16, 16,
18,
DirectionSet({ DirectionSet({
Direction.up, Direction.up,
Direction.down,
Direction.left, Direction.left,
Direction.right, Direction.right,
Direction.down
}))); })));
var w = output.vx; var geo.Size(dx: w, dy: h) = output.size;
var h = output.vy; for (var y = 0; y < h; y++) {
for (var y = 0; y < w; y++) { for (var x = 0; x < w; x++) {
for (var x = 0; x < h; x++) {
var cursor = at(x * 2, y * 2).big(); var cursor = at(x * 2, y * 2).big();
switch (output.tiles[x + y * w]) { switch (output.tiles.get(x, y)) {
case LevelTile.floor: case LevelTile.floor:
case LevelTile.openDoor:
cursor.puts(" "); cursor.puts(" ");
case LevelTile.door: case LevelTile.closedDoor:
cursor.puts("d"); cursor.fg(Palette.demoDoor).puts("+");
case LevelTile.exit:
cursor.fg(Palette.demoExit).puts("X");
case LevelTile.wall: case LevelTile.wall:
cursor.puts("#"); cursor.puts("#");
case LevelTile.exit: case null:
cursor.puts("X"); cursor.puts("?");
} }
} }
} }
inpLoop:
await for (var inp in rawInput()) {
skreek("$inp $seed");
switch (inp) {
case Keystroke(text: "a"):
seed -= 1;
break inpLoop;
case Keystroke(text: "d"):
seed += 1; seed += 1;
await zzz(0.1); break inpLoop;
default:
}
}
} }
} }
/* /*

8
lib/gen/direction.dart Normal file
View File

@ -0,0 +1,8 @@
part of 'generator.dart';
enum Direction {
up,
left,
down,
right,
}

View File

@ -0,0 +1,65 @@
part of "generator.dart";
// TODO: There are many more efficient ways to do this
class DirectionSet {
final Set<Direction> directions;
DirectionSet(this.directions);
DirectionSet flip() {
var ds2 = DirectionSet({});
for (var i in directions) {
switch (i) {
case Direction.up:
ds2.directions.add(Direction.up);
case Direction.left:
ds2.directions.add(Direction.right);
case Direction.down:
ds2.directions.add(Direction.down);
case Direction.right:
ds2.directions.add(Direction.left);
}
}
return ds2;
}
DirectionSet rotateLeft() {
var ds2 = DirectionSet({});
for (var i in directions) {
switch (i) {
case Direction.up:
ds2.directions.add(Direction.left);
case Direction.left:
ds2.directions.add(Direction.down);
case Direction.down:
ds2.directions.add(Direction.right);
case Direction.right:
ds2.directions.add(Direction.up);
}
}
return ds2;
}
DirectionSet rotateRight() {
var ds2 = DirectionSet({});
for (var i in directions) {
switch (i) {
case Direction.up:
ds2.directions.add(Direction.right);
case Direction.right:
ds2.directions.add(Direction.down);
case Direction.down:
ds2.directions.add(Direction.left);
case Direction.left:
ds2.directions.add(Direction.up);
}
}
return ds2;
}
DirectionSet clone() {
var ds2 = DirectionSet({});
ds2.directions.addAll(directions);
return ds2;
}
}

View File

@ -1,308 +1,466 @@
import 'dart:developer';
import 'dart:math' as math; 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'; import 'package:dartterm/world/level.dart';
enum Direction { part 'direction.dart';
up, part 'direction_set.dart';
left, part 'orientation.dart';
down, part 'requirement.dart';
right, part 'vault.dart';
} part 'vaults.dart';
class Vaults { const vaultTries = 30;
List<Vault> _primitive = [];
static Future<Vaults> load(String filename) async { class Generator {
// TODO final math.Random _random;
return Vaults(); final Vaults _vaults;
List<Vault> _queue = [];
Generator(this._random, this._vaults);
Level generateLevel(Requirement requirement) {
var out = _generateOriented(requirement, false);
return _finalize(out);
} }
Vault generateBoxed(math.Random random, Requirement req) { /*
var vx = req.vx; Vault generateVault(Requirement requirement) {
var vy = req.vy; var out = _generateOriented(requirement, false);
var (vault, (_, _)) = _finalize(out);
var tiles = [ return vault;
for (var y = 0; y < vy; y++) }
for (var x = 0; x < vx; x++) LevelTile.wall */
];
var v = Vault(tiles, vx, vy, req.smooth); Vault _generateOriented(Requirement requirement, bool canBeVault) {
if (canBeVault) {
if (req.vx < 2 || req.vy < 2) { Vault? suggested = _suggest(vaultTries, requirement);
return v; if (suggested != null) {
return _fillMetaRegions(requirement, suggested);
} }
var req2 = Requirement(vx - 2, vy - 2, req.smooth);
var inner = generate(random, req2);
v.blitFrom(inner, 1, 1);
return v;
} }
Vault generate(math.Random random, Requirement requirement) { // First of all: randomize orientation
// TODO: Pick a relevant vault from the vaults file if possible
//
// First of all: randomize orientation.
// This way we only have to consider one kind of spilt // This way we only have to consider one kind of spilt
var orientation = randomOrientation(random); var orientation = randomOrientation(_random);
// Try to make vx the long axis if possible // Try to make vx the long axis if possible
var req2 = requirement.unReorient(orientation); var req2 = unReorientRequirement(requirement, orientation);
if (req2.vy > (req2.vx - 2) * 3 / 2) { if (req2.vyMax > (req2.vxMax - 2) * 3 / 2) {
orientation = (orientation + 2) % 8; // rotate once more orientation = (orientation + 2) % 8; // rotate once more
} }
req2 = requirement.unReorient(orientation); // 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;
}
var out2 = _generate(random, req2); req2 = unReorientRequirement(requirement, orientation);
var out1 = out2.reorient(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}"); // log("$orientation ${requirement.vx} ${requirement.vy} ${req2.vx} ${req2.vy} ${out2.vx} ${out2.vy} ${out1.vx} ${out1.vy}");
assert(out1.vx == requirement.vx); var geo.Size(:dx, :dy) = out1.size;
assert(out1.vy == requirement.vy); assert(dx >= requirement.vxMin && dx <= requirement.vxMax);
assert(dy >= requirement.vyMin && dy <= requirement.vyMax);
assert(out1.smooth.directions.containsAll(requirement.smooth.directions));
return out1; return out1;
} }
Vault _generate(math.Random random, Requirement req) { Vault _generateBsp(Requirement req) {
var vx = req.vx; var vxMin = req.vxMin;
var vy = req.vy; var vyMin = req.vyMin;
var vxMax = req.vxMax;
var vyMax = req.vyMax;
var tiles = [ var smoothUp = req.smooth.directions.contains(Direction.up);
for (var y = 0; y < vy; y++) var smoothDown = req.smooth.directions.contains(Direction.down);
for (var x = 0; x < vx; x++) LevelTile.wall var smoothUpDown = smoothUp && smoothDown;
];
var v = Vault(tiles, vx, vy, req.smooth);
if (vx < 3 || vx * vy < 10) { // var vxRand = _random.nextInt(vxMax - vxMin) + vxMin;
v.clear(LevelTile.floor); 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 { } else {
// pick a split point var leftReq = Requirement(
var splitVx = random.nextInt(vx - 2) + 1; math.max(vxMin - 4, 2), vxMax - 4, vyMin, vyMax, req.smooth.clone());
leftReq.smooth.directions.add(Direction.right);
var leftChild = _generateOriented(leftReq, true);
var reqLeft = Requirement(splitVx, vy, req.smooth.clone()); var vyMinRight = vyMin;
reqLeft.smooth.directions.add(Direction.right); var vyMaxRight = vyMax;
var reqRight = Requirement((vx - splitVx - 1), vy, req.smooth.clone());
reqRight.smooth.directions.add(Direction.left);
var vaultLeft = generate(random, reqLeft); if (smoothUpDown) {
var vaultRight = generate(random, reqRight); vyMaxRight = vyMinRight = leftChild.size.dy;
v.blitFrom(vaultLeft, 0, 0);
v.blitFrom(vaultRight, splitVx + 1, 0);
} }
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; return v;
} }
} if (smoothDown) {
var v = Vault.blank(
// TODO: There are many more efficient ways to do this vxTotal, vyTotal, VaultTile.defaultwall, req.smooth.clone());
class DirectionSet { v.blitFrom(leftChild, 0, vyTotal - leftChild.vy);
final Set<Direction> directions; v.blitFrom(rightChild, leftChild.vx - 1, vyTotal - rightChild.vy);
return v;
DirectionSet(this.directions);
DirectionSet flip() {
var ds2 = DirectionSet({});
for (var i in directions) {
switch (i) {
case Direction.up:
ds2.directions.add(Direction.up);
case Direction.left:
ds2.directions.add(Direction.right);
case Direction.down:
ds2.directions.add(Direction.down);
case Direction.right:
ds2.directions.add(Direction.left);
}
}
return ds2;
} }
DirectionSet rotateLeft() { // no smoothing reqs
var ds2 = DirectionSet({}); // min: ensure some overlap
for (var i in directions) { var vyTMax = math.min(vyMax, leftChild.vy + rightChild.vy - 3);
switch (i) { if (vyTMax > vyTotal) {
case Direction.up: vyTotal += _random.nextInt(vyTMax - vyTotal);
ds2.directions.add(Direction.left);
case Direction.left:
ds2.directions.add(Direction.down);
case Direction.down:
ds2.directions.add(Direction.right);
case Direction.right:
ds2.directions.add(Direction.up);
} }
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;
} }
return ds2;
} }
DirectionSet rotateRight() { Vault? _suggest(int tries, Requirement req) {
var ds2 = DirectionSet({}); for (var i = 0; i < tries; i++) {
for (var i in directions) { var sugg = _popSuggestion();
switch (i) { if (sugg == null) {
case Direction.up: return null;
ds2.directions.add(Direction.right);
case Direction.right:
ds2.directions.add(Direction.down);
case Direction.down:
ds2.directions.add(Direction.left);
case Direction.left:
ds2.directions.add(Direction.up);
}
}
return ds2;
} }
DirectionSet clone() { sugg = reorientVault(sugg, randomOrientation(_random));
var ds2 = DirectionSet({}); sugg = _tidy(sugg, req);
ds2.directions.addAll(directions); if (sugg != null) {
return ds2; return sugg;
} }
} }
return null;
class Requirement {
final int vx, vy;
final DirectionSet smooth;
Requirement(this.vx, this.vy, this.smooth);
Requirement flip() {
return Requirement(vx, vy, smooth.flip());
} }
Requirement rotateLeft() { Vault? _popSuggestion() {
return Requirement(vy, vx, smooth.rotateLeft()); if (_queue.isEmpty) {
_queue = _vaults.randomFlight(_random);
}
if (_queue.isEmpty) {
return null;
}
return _queue.removeLast();
} }
Requirement rotateRight() { Vault? _tidy(Vault vault, Requirement req) {
return Requirement(vy, vx, smooth.rotateRight()); if (vault.vx > req.vxMax || vault.vy > req.vyMax) {
return null;
} }
if (vault.vx < req.vxMin || vault.vy < req.vyMin) {
Requirement unReorient(int r) { return null;
assert(r >= 0 && r < 8);
Requirement o = this;
if (r % 2 == 1) {
o = o.flip();
r -= 1;
} }
while (r >= 2) { if (!vault.smooth.directions.containsAll(req.smooth.directions)) {
o = o.rotateLeft(); return null;
r -= 2;
} }
return o; // 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
class Vault { // in the set of rows or columns the metabsp region did not touch
final List<LevelTile> tiles;
final int vx, vy;
final DirectionSet smooth;
Vault(this.tiles, this.vx, this.vy, this.smooth) {
assert(tiles.length == vx * vy);
}
// TODO: We should be assessing this based on whether the pattern in the input
// PNG had right-angled borders on this side, not based on the literal
// presence or absence of walls
// //
// In other words, this is wrong. // Ex:
static Vault fromVaultData(List<LevelTile> tiles, int vx, int vy) { // XXXX##
assert(tiles.length == vx * vy); // XXXX #
var smooth = { // XXXX #
Direction.up, // # #
Direction.left, // ######
Direction.down, //
Direction.right // becomes
}; //
for (var x = 0; x < vx; x++) { // XXXZYY
if (tiles[x + 0 * vx] == LevelTile.wall) { // XXXZYY
smooth.remove(Direction.up); // XXXZYY
break; // 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;
} }
for (var x = 0; x < vx; x++) {
if (tiles[x + (vy - 1) * vx] == LevelTile.wall) { return vault;
smooth.remove(Direction.down);
break;
} }
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;
} }
for (var y = 0; y < vy; y++) {
if (tiles[0 + y * vx] == LevelTile.wall) { bool walkable(VaultTile? tile) {
smooth.remove(Direction.left); return tile == VaultTile.bspfloor ||
break; tile == VaultTile.floor ||
tile == VaultTile.doorpronefloor ||
tile == VaultTile.exit ||
tile == VaultTile.door;
} }
}
for (var y = 0; y < vy; y++) { List<(int, int)> newArches = [];
if (tiles[vx - 1 + y * vx] == LevelTile.wall) { for (int x = 0; x < vx; x++) {
smooth.remove(Direction.right); for (int y = 0; y < vy; y++) {
break; 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++;
} }
} }
return Vault(tiles, vx, vy, DirectionSet(smooth)); if (supporters == 2) {
newArches.add((x, y));
} }
subj.tiles.set(x, y, VaultTile.wall);
void clear(LevelTile lt) { }
for (var y = 0; y < vy; y++) { if (t == VaultTile.archpronewall || t == VaultTile.defaultwall) {
for (var x = 0; x < vx; x++) { subj.tiles.set(x, y, VaultTile.wall);
tiles[y * vx + x] = lt;
} }
} }
} }
void blitFrom(Vault other, int dx, int dy) { for (var (ax, ay) in newArches) {
assert(dx >= 0); subj.tiles.set(ax, ay, VaultTile.floor);
assert(dy >= 0); }
assert(dx + other.vx <= vx);
assert(dy + other.vy <= vy); // == build doors ==
for (var x = 0; x < other.vx; x++) { var (regions, toRegion) =
for (var y = 0; y < other.vy; y++) { regionalize(geo.Rect(0, 0, subj.vx, subj.vy), (x, y) {
tiles[(y + dy) * vx + x + dx] = other.tiles[y * other.vx + x]; 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<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(region0 != exitRegionId && region1 != exitRegionId,
points, roomSize, _random.nextDouble())));
}
}
List<Edge<(int, int)>> 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);
} }
} }
} }
Vault flip() { if (exitDoors.length != 1) {
List<LevelTile> tiles2 = [ throw Exception("should be exactly one exit door");
for (var y = 0; y < vy; y++)
for (var x = vx - 1; x >= 0; x--) tiles[y * vx + x]
];
return Vault(tiles2, vx, vy, smooth.flip());
} }
// TODO: Actually test this logic. // == Build the exit area ==
// This worked in Python, so it might even be right! var (exitX, exitY) = exitDoors[0].value;
Vault rotateRight() { int exitVaultX, exitVaultY;
List<LevelTile> tiles2 = [ Vault finalVault;
for (var x = 0; x < vx; x++) int vaultBlitX, vaultBlitY;
for (var y = 0; y < vy; y++) tiles[(vy - 1 - y) * vx + x] if (exitX == 0 || exitX == vx - 1) {
]; finalVault =
Vault.blank(vx + 3, vy, VaultTile.defaultwall, DirectionSet({}));
return Vault(tiles2, vy, vx, smooth.rotateRight()); 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");
} }
Vault rotateLeft() { for (var x = exitVaultX - 1; x <= exitVaultX + 1; x++) {
List<LevelTile> tiles2 = [ for (var y = exitVaultY - 1; y <= exitVaultY + 1; y++) {
for (var x = vx - 1; x >= 0; x++) finalVault.tiles.set(x, y, VaultTile.exit);
for (var y = vy - 1; y >= 0; y++) tiles[y * vx + (vx - 1 - x)] 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 Vault(tiles2, vy, vx, smooth.rotateLeft()); return Level(
} Bitmap.blankWith(finalVault.vx, finalVault.vy,
(x, y) => flattenVaultTile(finalVault.tiles.get(x, y)!)),
Vault reorient(int r) { geo.Offset(exitVaultX, exitVaultY));
assert(r >= 0 && r < 8);
Vault o = this;
while (r >= 2) {
o = o.rotateRight();
r -= 2;
}
if (r == 1) {
o = o.flip();
r -= 1;
}
return o;
} }
} }
int randomOrientation(math.Random random) { // components:
return random.nextInt(8); // - 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();
} }

33
lib/gen/orientation.dart Normal file
View File

@ -0,0 +1,33 @@
part of 'generator.dart';
int randomOrientation(math.Random random) {
return random.nextInt(8);
}
Vault reorientVault(Vault o, int r) {
assert(r >= 0 && r < 8);
while (r >= 2) {
o = o.rotateRight();
r -= 2;
}
if (r == 1) {
o = o.flip();
r -= 1;
}
return o;
}
Requirement unReorientRequirement(Requirement o, int r) {
assert(r >= 0 && r < 8);
if (r % 2 == 1) {
o = o.flip();
r -= 1;
}
while (r >= 2) {
o = o.rotateLeft();
r -= 2;
}
return o;
}

25
lib/gen/requirement.dart Normal file
View File

@ -0,0 +1,25 @@
part of 'generator.dart';
class Requirement {
final int vxMin, vxMax, vyMin, vyMax;
final DirectionSet smooth;
Requirement(this.vxMin, this.vxMax, this.vyMin, this.vyMax, this.smooth) {
assert(vxMin <= vxMax);
assert(vyMin <= vyMax);
assert(vxMax > 2);
assert(vyMax > 2);
}
Requirement flip() {
return Requirement(vxMin, vxMax, vyMin, vyMax, smooth.flip());
}
Requirement rotateLeft() {
return Requirement(vyMin, vyMax, vxMin, vxMax, smooth.rotateLeft());
}
Requirement rotateRight() {
return Requirement(vyMin, vyMax, vxMin, vxMax, smooth.rotateRight());
}
}

75
lib/gen/vault.dart Normal file
View File

@ -0,0 +1,75 @@
part of 'generator.dart';
class Vault {
final Bitmap<VaultTile> tiles;
final DirectionSet smooth;
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);
}
static Vault blankWith(
int vx, int vy, VaultTile Function(int, int) lt, DirectionSet smooth) {
return Vault(Bitmap.blankWith(vx, vy, lt), smooth);
}
void clear(VaultTile lt) {
tiles.clear(lt);
}
void blitFrom(Vault other, int dx, int dy) {
tiles.blitFromWith(other.tiles, dx, dy, mergeVaultTile);
}
Vault flip() {
return Vault(tiles.flip(), smooth.flip());
}
Vault rotateRight() {
return Vault(tiles.rotateRight(), smooth.rotateRight());
}
Vault rotateLeft() {
return Vault(tiles.rotateLeft(), smooth.rotateLeft());
}
}
VaultTile mergeVaultTile(VaultTile bottom, VaultTile top) {
if (bottom == VaultTile.wall && top == VaultTile.archpronewall) {
return VaultTile.wall;
}
if (bottom == VaultTile.wall && top == VaultTile.archwall) {
return VaultTile.wall;
}
if (bottom == VaultTile.archwall && top == VaultTile.archpronewall) {
return VaultTile.archwall;
}
return top;
}
LevelTile flattenVaultTile(VaultTile vt) {
switch (vt) {
case VaultTile.meta0:
case VaultTile.defaultwall:
case VaultTile.archpronewall:
case VaultTile.archwall:
case VaultTile.wall:
return LevelTile.wall;
case VaultTile.exit:
return LevelTile.exit;
case VaultTile.door:
return LevelTile.closedDoor;
case VaultTile.doorpronefloor:
case VaultTile.bspfloor:
case VaultTile.floor:
return LevelTile.floor;
}
}

35
lib/gen/vault_tile.dart Normal file
View File

@ -0,0 +1,35 @@
part of 'generator.dart';
VaultTile mergeVaultTile(VaultTile bottom, VaultTile top) {
if (bottom == VaultTile.wall && top == VaultTile.archpronewall) {
return VaultTile.wall;
}
if (bottom == VaultTile.wall && top == VaultTile.archwall) {
return VaultTile.wall;
}
if (bottom == VaultTile.archwall && top == VaultTile.archpronewall) {
return VaultTile.archwall;
}
return top;
}
LevelTile flattenVaultTile(VaultTile vt) {
switch (vt) {
case VaultTile.meta0:
case VaultTile.defaultwall:
case VaultTile.archpronewall:
case VaultTile.archwall:
case VaultTile.wall:
return LevelTile.wall;
case VaultTile.exit:
return LevelTile.exit;
case VaultTile.door:
return LevelTile.closedDoor;
case VaultTile.doorpronefloor:
case VaultTile.bspfloor:
case VaultTile.floor:
return LevelTile.floor;
}
}

117
lib/gen/vaults.dart Normal file
View File

@ -0,0 +1,117 @@
part of 'generator.dart';
class Vaults {
final List<Vault> _primitive = [];
List<Vault> randomFlight(math.Random rng) {
// TODO: There are many more efficient ways to do this!
List<Vault> list2 = [];
list2.addAll(_primitive);
list2.shuffle(rng);
return list2;
}
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 vs = Vaults();
for (var region in regions) {
Vault v = loadVault(region, basis);
vs._primitive.add(v);
}
return vs;
}
static Vault loadVault(Region r, Bitmap<VaultTile?> b) {
skreek("Loading vault: $r");
var tiles = [
for (var y = r.rect.y0; y < r.rect.y1; y++)
for (var x = r.rect.x0; x < r.rect.x1; x++)
r.points.contains((x, y))
? (b.get(x, y) ?? VaultTile.wall)
: VaultTile.wall
];
DirectionSet smooth = DirectionSet(
{Direction.up, Direction.left, Direction.right, Direction.down});
for (var x = r.rect.x0; x < r.rect.x1; x++) {
if (b.get(x, r.rect.y0) == null) {
smooth.directions.remove(Direction.up);
break;
}
}
for (var x = r.rect.x0; x < r.rect.x1; x++) {
if (b.get(x, r.rect.y1 - 1) == null) {
smooth.directions.remove(Direction.down);
break;
}
}
for (var y = r.rect.y0; y < r.rect.y1; y++) {
if (b.get(r.rect.x0, y) == null) {
smooth.directions.remove(Direction.left);
break;
}
}
for (var y = r.rect.y0; y < r.rect.y1; y++) {
if (b.get(r.rect.x1 - 1, y) == null) {
smooth.directions.remove(Direction.right);
break;
}
}
return Vault(Bitmap(r.rect.size, tiles), smooth);
}
}
enum VaultTile {
meta0,
exit,
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.
wall,
}
VaultTile? colorToVaultTile(int c) {
switch (c ~/ 256) {
// RGB (originally rgba)
// == spacers ==
case 0x007F00: // deep green
return null; // separates vaults
// == metasyntax ==
case 0x00FF00: // green
return VaultTile.meta0; // call back into BSP
// == level elements ==
case 0x000000:
case 0x707070:
return VaultTile.wall;
case 0xFF8000:
return VaultTile.archwall;
case 0x7F4000:
return VaultTile.archpronewall;
case 0xBCCFFF:
return VaultTile.doorpronefloor;
case 0xFFFFFF:
case 0xFFFF00:
case 0xFF00FF:
return VaultTile.floor;
case 0x0087FF:
return VaultTile.door;
case 0xFF0000:
return VaultTile.exit;
default:
throw Exception("unrecognized pixel: $c");
}
}

View File

@ -18,7 +18,7 @@ class Keystroke extends Input {
const Keystroke(this.key, this._text); const Keystroke(this.key, this._text);
String? text() => _text; String? get text => _text;
} }
enum Button { left, right } enum Button { left, right }

6
lib/skreek.dart Normal file
View File

@ -0,0 +1,6 @@
import 'package:flutter/foundation.dart';
void skreek(String msg) {
// ignore: avoid_print
debugPrint("[skreek] $msg");
}

View File

@ -8,6 +8,7 @@ import 'package:dartterm/colors.dart';
import 'package:dartterm/cp437.dart'; import 'package:dartterm/cp437.dart';
import 'package:dartterm/fonts.dart'; import 'package:dartterm/fonts.dart';
import 'package:dartterm/input.dart'; import 'package:dartterm/input.dart';
import 'package:dartterm/skreek.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -71,7 +72,7 @@ class Terminal {
} }
void _notifyInput(Input i) { void _notifyInput(Input i) {
log("Input: $i $_lastSeenMouse"); skreek("Input: $i $_lastSeenMouse");
_inputSink.add(i); _inputSink.add(i);
} }

View File

@ -17,6 +17,10 @@ void notifyScreenDimensions(ScreenDimensions sd) {
_terminal._notifyScreenDimensions(sd); _terminal._notifyScreenDimensions(sd);
} }
Stream<Input> rawInput() {
return _terminal.rawInput();
}
void clear() { void clear() {
at(0, 0).clear(); at(0, 0).clear();
} }

View File

@ -1,29 +1,23 @@
enum LevelTile { import 'package:dartterm/bitmap.dart';
exit, import 'package:dartterm/algorithms/geometry.dart' as geo;
door,
floor,
wall,
}
class Level { class Level {
Set<(int, int)> openCells = {}; Bitmap<LevelTile> tiles;
} geo.Offset spawn;
LevelTile colorToTile(int c) { geo.Size get size => tiles.size;
switch (c) {
// ABGR Level(this.tiles, this.spawn) {
case 0xFF000000: assert(tiles.rect.containsPoint(spawn));
case 0xFF707070:
return LevelTile.wall;
case 0xFFFFFFFF:
case 0xFF00FFFF:
case 0xFFFF00FF:
return LevelTile.floor;
case 0xFFFF8700:
return LevelTile.door;
case 0xFF0000FF:
return LevelTile.exit;
default:
throw Exception("unrecognized pixel: $c");
} }
} }
enum LevelTile {
exit,
floor,
wall,
closedDoor,
openDoor,
}

View File

@ -62,6 +62,7 @@ flutter:
assets: assets:
- assets/images/fonts/ - assets/images/fonts/
- assets/images/wfc/ - assets/images/wfc/
- assets/images/vaults/
# - images/a_dot_burr.jpeg # - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg # - images/a_dot_ham.jpeg