Compare commits

...

10 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
22 changed files with 1034 additions and 269 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.

Before

Width:  |  Height:  |  Size: 844 B

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

@ -1,66 +1,76 @@
import 'dart:math' as math; import 'dart:math' as math;
import 'package:dartterm/algorithms/geometry.dart' as geo;
class Region { class Region {
final math.Rectangle<int> rect; final geo.Rect rect;
final Set<(int, int)> points; final Set<(int, int)> points;
bool get isRectangle => points.length == rect.dx * rect.dy;
Region(this.rect, this.points); Region(this.rect, this.points);
static fromNonEmptySet(Set<(int, int)> s) { static Region fromNonEmptySet(Set<(int, int)> s) {
assert(s.isNotEmpty); assert(s.isNotEmpty);
int xMin = s.map<int>((xy) => xy.$1).reduce(math.min); int xMin = s.map<int>((xy) => xy.$1).reduce(math.min);
int yMin = s.map<int>((xy) => xy.$2).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 xMax = s.map<int>((xy) => xy.$1).reduce(math.max);
int yMax = s.map<int>((xy) => xy.$2).reduce(math.max); int yMax = s.map<int>((xy) => xy.$2).reduce(math.max);
var rect = math.Rectangle.fromPoints( var rect = geo.Rect(xMin, yMin, xMax - xMin + 1, yMax - yMin + 1);
math.Point(xMin, yMin), math.Point(xMax, yMax)); return Region(rect, s);
Region(rect, s); }
@override
String toString() {
return "Region($rect, $points)";
} }
} }
List<Region> regionalize( (List<Region>, Map<(int, int), int>) regionalize(
math.Rectangle<int> rect, bool Function(int, int) isAccessible) { geo.Rect rect, bool Function(int, int) isAccessible) {
int nextRegion = 0; int nextRegion = 0;
Map<(int, int), int> regions = {}; Map<(int, int), int> regions = {};
void floodfill(int x, int y, region) { int floodfill(int x, int y) {
if (!rect.containsPoint(math.Point(x, y))) { int workDone = 0;
return; if (!rect.containsPoint(geo.Offset(x, y))) {
return workDone;
} }
if (regions[(x, y)] != null) { if (regions[(x, y)] != null) {
return; return workDone;
} }
if (!isAccessible(x, y)) { if (!isAccessible(x, y)) {
return; return workDone;
} }
regions[(x, y)] = region; regions[(x, y)] = nextRegion;
floodfill(x - 1, y, region); workDone += 1;
floodfill(x + 1, y, region); workDone += floodfill(x - 1, y);
floodfill(x, y - 1, region); workDone += floodfill(x + 1, y);
floodfill(x, y + 1, region); 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 // TODO: This can be done more efficiently with a union/find data structure
// But this is an easy implementation to understand // But this is an easy implementation to understand
for (var y = rect.top; y < rect.bottom; y++) { for (var y = rect.y0; y < rect.y1; y++) {
for (var x = rect.left; x < rect.right; x++) { for (var x = rect.x0; x < rect.x1; x++) {
if (regions[(x, y)] == null) { if (regions[(x, y)] == null) {
floodfill(x, y, nextRegion); if (floodfill(x, y) > 0) {
nextRegion += 1; nextRegion += 1;
} }
} }
} }
return _toExplicit(regions); }
return (_toExplicit(regions, nextRegion), regions);
} }
List<Region> _toExplicit(Map<(int, int), int> regions) { List<Region> _toExplicit(Map<(int, int), int> pointRegions, int nRegions) {
List<Set<(int, int)>> pointsOut = [ List<Set<(int, int)>> regionPoints = [for (var i = 0; i < nRegions; i++) {}];
for (var i = 0; i < regions.length; i++) Set() for (var MapEntry(key: (x, y), value: id_) in pointRegions.entries) {
]; regionPoints[id_].add((x, y));
for (var MapEntry(key: (x, y), value: id_) in regions.entries) {
pointsOut[id_].add((x, y));
} }
return [for (var s in pointsOut) Region.fromNonEmptySet(s)]; 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,35 +31,51 @@ void main() async {
int seed = 0; int seed = 0;
while (true) { while (true) {
Vault output = Generator(math.Random(seed), vaults).generateBoxed( clear();
Requirement( Level output =
Generator(math.Random(seed), vaults).generateLevel(Requirement(
16,
32, 32,
24, 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 < h; y++) {
for (var x = 0; x < w; x++) { for (var x = 0; x < w; 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:
}
}
} }
} }
/* /*

View File

@ -1,6 +1,10 @@
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';
part 'direction.dart'; part 'direction.dart';
@ -10,7 +14,7 @@ part 'requirement.dart';
part 'vault.dart'; part 'vault.dart';
part 'vaults.dart'; part 'vaults.dart';
const vaultTries = 10; const vaultTries = 30;
class Generator { class Generator {
final math.Random _random; final math.Random _random;
@ -19,25 +23,25 @@ class Generator {
Generator(this._random, this._vaults); Generator(this._random, this._vaults);
Vault generateBoxed(Requirement req) { Level generateLevel(Requirement requirement) {
var vx = req.vx; var out = _generateOriented(requirement, false);
var vy = req.vy; return _finalize(out);
var v = Vault.blank(vx, vy, req.smooth, LevelTile.wall);
if (req.vx < 2 || req.vy < 2) {
return v;
}
var req2 = Requirement(vx - 2, vy - 2, req.smooth);
var inner = generate(req2);
v.blitFrom(inner, 1, 1);
return v;
} }
Vault generate(Requirement requirement) { /*
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); Vault? suggested = _suggest(vaultTries, requirement);
if (suggested != null) { if (suggested != null) {
return suggested; return _fillMetaRegions(requirement, suggested);
}
} }
// First of all: randomize orientation // First of all: randomize orientation
@ -46,44 +50,109 @@ class Generator {
// Try to make vx the long axis if possible // Try to make vx the long axis if possible
var req2 = unReorientRequirement(requirement, 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
} }
// if only one of "left" and "right" needs to be smooth, prioritize right
// as left is generated first
req2 = unReorientRequirement(requirement, orientation); req2 = unReorientRequirement(requirement, orientation);
if (req2.smooth.directions.contains(Direction.left) &&
req2.smooth.directions.contains(Direction.right)) {
orientation = (orientation + 4) % 8;
}
var out2 = _generate(req2); req2 = unReorientRequirement(requirement, orientation);
var out2 = _generateBsp(req2);
var out1 = reorientVault(out2, orientation); 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(Requirement req) { Vault _generateBsp(Requirement req) {
var vx = req.vx; var vxMin = req.vxMin;
var vy = req.vy; var vyMin = req.vyMin;
var v = Vault.blank(vx, vy, req.smooth, LevelTile.wall); var vxMax = req.vxMax;
var vyMax = req.vyMax;
if (vx < 5 || vx * vy < 10) { var smoothUp = req.smooth.directions.contains(Direction.up);
v.clear(LevelTile.floor); 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 { } else {
// pick a split point var leftReq = Requirement(
var splitVx = _random.nextInt(vx - 4) + 2; 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(reqLeft); if (smoothUpDown) {
var vaultRight = generate(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(
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) { Vault? _suggest(int tries, Requirement req) {
for (var i = 0; i < tries; i++) { for (var i = 0; i < tries; i++) {
@ -112,46 +181,286 @@ class Generator {
} }
Vault? _tidy(Vault vault, Requirement req) { Vault? _tidy(Vault vault, Requirement req) {
if (vault.vx > req.vx || vault.vy > req.vy) { if (vault.vx > req.vxMax || vault.vy > req.vyMax) {
return null; return null;
} }
if (vault.vx < req.vx / 2 || vault.vy < req.vy / 2) { if (vault.vx < req.vxMin || vault.vy < req.vyMin) {
return null; 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;
}
var rsd = req.smooth.directions; Vault _fillMetaRegions(Requirement requirement, Vault vault) {
bool mustFillX = var geo.Size(:dx, :dy) = vault.size;
rsd.contains(Direction.left) && rsd.contains(Direction.right); var (metaregions, _) = regionalize(geo.Rect(0, 0, dx, dy),
if (vault.vx != req.vx && mustFillX) { (x, y) => vault.tiles.get(x, y) == VaultTile.meta0);
return null;
} for (var i in metaregions) {
bool mustFillY = assert(i.isRectangle);
rsd.contains(Direction.left) && rsd.contains(Direction.right); var sz = i.rect.size;
if (vault.vy != req.vy && mustFillY) { // 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
return null; 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;
} }
Vault full = Vault.blank(req.vx, req.vy, req.smooth, LevelTile.wall); return vault;
int vx = vault.vx; }
int dx;
if (rsd.contains(Direction.left)) { Level _finalize(Vault subj) {
dx = 0; var vx = subj.vx, vy = subj.vy;
} else if (rsd.contains(Direction.right)) {
dx = req.vx - vx; 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<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 { } else {
dx = _random.nextInt(req.vx - vx); continue;
} }
int vy = vault.vy; if (region0 == region1) {
int dy; continue;
if (rsd.contains(Direction.up)) { }
dy = 0;
} else if (rsd.contains(Direction.down)) { int roomSize = math.min(
dy = req.vy - vy; 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);
}
}
}
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 { } else {
dy = _random.nextInt(req.vx - vx); throw Exception("exit door in invalid position $exitX $exitY $vx $vy");
} }
full.blitFrom(vault, dx, dy);
return full; 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();
}

View File

@ -1,34 +1,25 @@
part of 'generator.dart'; part of 'generator.dart';
class Requirement { class Requirement {
final int vx, vy; final int vxMin, vxMax, vyMin, vyMax;
final DirectionSet smooth; final DirectionSet smooth;
Requirement(this.vx, this.vy, this.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() { Requirement flip() {
return Requirement(vx, vy, smooth.flip()); return Requirement(vxMin, vxMax, vyMin, vyMax, smooth.flip());
} }
Requirement rotateLeft() { Requirement rotateLeft() {
return Requirement(vy, vx, smooth.rotateLeft()); return Requirement(vyMin, vyMax, vxMin, vxMax, smooth.rotateLeft());
} }
Requirement rotateRight() { Requirement rotateRight() {
return Requirement(vy, vx, smooth.rotateRight()); return Requirement(vyMin, vyMax, vxMin, vxMax, smooth.rotateRight());
}
Requirement unReorient(int r) {
assert(r >= 0 && r < 8);
Requirement o = this;
if (r % 2 == 1) {
o = o.flip();
r -= 1;
}
while (r >= 2) {
o = o.rotateLeft();
r -= 2;
}
return o;
} }
} }

View File

@ -1,124 +1,75 @@
part of 'generator.dart'; part of 'generator.dart';
class Vault { class Vault {
final int vx, vy; final Bitmap<VaultTile> tiles;
final DirectionSet smooth; final DirectionSet smooth;
final List<LevelTile> tiles;
Vault(this.vx, this.vy, this.smooth, this.tiles) { geo.Size get size => tiles.size;
assert(tiles.length == vx * vy); 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);
} }
// TODO: We should be assessing this based on whether the pattern in the input static Vault blankWith(
// PNG had right-angled borders on this side, not based on the literal int vx, int vy, VaultTile Function(int, int) lt, DirectionSet smooth) {
// presence or absence of walls return Vault(Bitmap.blankWith(vx, vy, lt), smooth);
//
// In other words, this is wrong.
static Vault fromVaultData(int vx, int vy, List<LevelTile> tiles) {
assert(tiles.length == vx * vy);
var smooth = {
Direction.up,
Direction.left,
Direction.down,
Direction.right
};
for (var x = 0; x < vx; x++) {
if (tiles[x + 0 * vx] == LevelTile.wall) {
smooth.remove(Direction.up);
break;
}
}
for (var x = 0; x < vx; x++) {
if (tiles[x + (vy - 1) * vx] == LevelTile.wall) {
smooth.remove(Direction.down);
break;
}
}
for (var y = 0; y < vy; y++) {
if (tiles[0 + y * vx] == LevelTile.wall) {
smooth.remove(Direction.left);
break;
}
}
for (var y = 0; y < vy; y++) {
if (tiles[vx - 1 + y * vx] == LevelTile.wall) {
smooth.remove(Direction.right);
break;
}
} }
return Vault(vx, vy, DirectionSet(smooth), tiles); void clear(VaultTile lt) {
} tiles.clear(lt);
void clear(LevelTile lt) {
for (var y = 0; y < vy; y++) {
for (var x = 0; x < vx; x++) {
tiles[y * vx + x] = lt;
}
}
} }
void blitFrom(Vault other, int dx, int dy) { void blitFrom(Vault other, int dx, int dy) {
assert(dx >= 0); tiles.blitFromWith(other.tiles, dx, dy, mergeVaultTile);
assert(dy >= 0);
assert(dx + other.vx <= vx);
assert(dy + other.vy <= vy);
for (var x = 0; x < other.vx; x++) {
for (var y = 0; y < other.vy; y++) {
tiles[(y + dy) * vx + x + dx] = other.tiles[y * other.vx + x];
}
}
} }
Vault flip() { Vault flip() {
List<LevelTile> tiles2 = [ return Vault(tiles.flip(), smooth.flip());
for (var y = 0; y < vy; y++)
for (var x = vx - 1; x >= 0; x--) tiles[y * vx + x]
];
return Vault(vx, vy, smooth.flip(), tiles2);
} }
// TODO: Actually test this logic.
// This worked in Python, so it might even be right!
Vault rotateRight() { Vault rotateRight() {
List<LevelTile> tiles2 = [ return Vault(tiles.rotateRight(), smooth.rotateRight());
for (var x = 0; x < vx; x++)
for (var y = 0; y < vy; y++) tiles[(vy - 1 - y) * vx + x]
];
return Vault(vy, vx, smooth.rotateRight(), tiles2);
} }
Vault rotateLeft() { Vault rotateLeft() {
List<LevelTile> tiles2 = [ return Vault(tiles.rotateLeft(), smooth.rotateLeft());
for (var x = vx - 1; x >= 0; x++) }
for (var y = vy - 1; y >= 0; y++) tiles[y * vx + (vx - 1 - x)]
];
return Vault(vy, vx, smooth.rotateLeft(), tiles2);
} }
Vault reorient(int r) { VaultTile mergeVaultTile(VaultTile bottom, VaultTile top) {
assert(r >= 0 && r < 8); if (bottom == VaultTile.wall && top == VaultTile.archpronewall) {
Vault o = this; return VaultTile.wall;
while (r >= 2) {
o = o.rotateRight();
r -= 2;
} }
if (r == 1) { if (bottom == VaultTile.wall && top == VaultTile.archwall) {
o = o.flip(); return VaultTile.wall;
r -= 1;
} }
return o; if (bottom == VaultTile.archwall && top == VaultTile.archpronewall) {
return VaultTile.archwall;
}
return top;
} }
static Vault blank(int vx, int vy, DirectionSet smooth, LevelTile lt) { LevelTile flattenVaultTile(VaultTile vt) {
var tiles = [ switch (vt) {
for (var y = 0; y < vy; y++) case VaultTile.meta0:
for (var x = 0; x < vx; x++) lt case VaultTile.defaultwall:
]; case VaultTile.archpronewall:
case VaultTile.archwall:
case VaultTile.wall:
return LevelTile.wall;
return Vault(vx, vy, smooth, tiles); 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;
}
}

View File

@ -3,11 +3,6 @@ part of 'generator.dart';
class Vaults { class Vaults {
final List<Vault> _primitive = []; final List<Vault> _primitive = [];
static Future<Vaults> load(String filename) async {
// TODO
return Vaults();
}
List<Vault> randomFlight(math.Random rng) { List<Vault> randomFlight(math.Random rng) {
// TODO: There are many more efficient ways to do this! // TODO: There are many more efficient ways to do this!
List<Vault> list2 = []; List<Vault> list2 = [];
@ -15,4 +10,108 @@ class Vaults {
list2.shuffle(rng); list2.shuffle(rng);
return list2; 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;
geo.Size get size => tiles.size;
Level(this.tiles, this.spawn) {
assert(tiles.rect.containsPoint(spawn));
}
} }
LevelTile colorToTile(int c) { enum LevelTile {
switch (c) { exit,
// ABGR
case 0xFF000000: floor,
case 0xFF707070: wall,
return LevelTile.wall;
case 0xFFFFFFFF: closedDoor,
case 0xFF00FFFF: openDoor,
case 0xFFFF00FF:
return LevelTile.floor;
case 0xFFFF8700:
return LevelTile.door;
case 0xFF0000FF:
return LevelTile.exit;
default:
throw Exception("unrecognized pixel: $c");
}
} }