Compare commits

...

13 Commits

Author SHA1 Message Date
7fc65b49c2 Render walls at half height for now 2023-09-22 21:49:39 -07:00
e3e43f0223 Basic sitemode 2023-09-22 21:08:03 -07:00
76e92a2a50 Initial site mode stuff 2023-09-22 18:35:47 -07:00
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
35 changed files with 1699 additions and 314 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,72 @@
import 'package:collection/collection.dart';
class Edge<T> {
final double cost;
final T destination;
Edge(this.cost, this.destination);
}
class Result<T> {
final double cost;
final T? predecessor;
final T item;
Result(this.cost, this.predecessor, this.item);
}
Iterable<Result<T>> dijkstra<T>(
T source, Iterable<Edge<T>> Function(T) neighbors) sync* {
var queue = PriorityQueue<Result<T>>((i0, i1) => i0.cost.compareTo(i1.cost));
Set<T> seen = {source};
queue.add(Result(0.0, null, source));
while (queue.isNotEmpty) {
var u = queue.removeFirst();
yield u;
for (var v in neighbors(u.item)) {
if (seen.contains(v.destination)) {
continue;
}
seen.add(v.destination);
queue.add(Result(u.cost + v.cost, u.item, v.destination));
}
}
}
List<T>? dijkstraPath<T>(
T source, T destination, Iterable<Edge<T>> Function(T) neighbors,
{double? maxCost}) {
if (source == destination) {
return [];
}
Map<T, T?> predecessor = {};
for (var r in dijkstra(source, neighbors)) {
predecessor[r.item] = r.predecessor;
if (maxCost != null && r.cost >= maxCost) {
return null;
}
if (r.item == destination) {
break;
}
}
var revPath = [destination];
while (true) {
var pred = predecessor[revPath.last];
if (pred == source) {
revPath.reverseRange(0, revPath.length);
return revPath;
} else if (pred == null) {
throw Exception(
"predecessor should not be null -- that would mean we missed source");
} else {
revPath.add(pred);
}
}
}

View File

@ -0,0 +1,91 @@
// 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";
}
@override
bool operator ==(Object other) =>
other is Size && other.dx == dx && other.dy == dy;
@override
int get hashCode => (dx, dy).hashCode;
}
class Offset {
final int x;
final int y;
const Offset(this.x, this.y);
@override
String toString() {
return "@($x, $y)";
}
@override
bool operator ==(Object other) =>
other is Offset && other.x == x && other.y == y;
@override
int get hashCode => (x, y).hashCode;
}
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";
}
@override
bool operator ==(Object other) =>
other is Rect &&
other.x0 == x0 &&
other.y0 == y0 &&
other.dx == dx &&
other.dy == dy;
@override
int get hashCode => (x0, y0, dx, dy).hashCode;
}

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 'package:dartterm/algorithms/geometry.dart' as geo;
class Region {
final math.Rectangle<int> rect;
final geo.Rect rect;
final Set<(int, int)> points;
bool get isRectangle => points.length == rect.dx * rect.dy;
Region(this.rect, this.points);
static fromNonEmptySet(Set<(int, int)> s) {
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 = math.Rectangle.fromPoints(
math.Point(xMin, yMin), math.Point(xMax, yMax));
Region(rect, s);
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> regionalize(
math.Rectangle<int> rect, bool Function(int, int) isAccessible) {
(List<Region>, Map<(int, int), int>) regionalize(
geo.Rect rect, bool Function(int, int) isAccessible) {
int nextRegion = 0;
Map<(int, int), int> regions = {};
void floodfill(int x, int y, region) {
if (!rect.containsPoint(math.Point(x, y))) {
return;
int floodfill(int x, int y) {
int workDone = 0;
if (!rect.containsPoint(geo.Offset(x, y))) {
return workDone;
}
if (regions[(x, y)] != null) {
return;
return workDone;
}
if (!isAccessible(x, y)) {
return;
return workDone;
}
regions[(x, y)] = region;
floodfill(x - 1, y, region);
floodfill(x + 1, y, region);
floodfill(x, y - 1, region);
floodfill(x, y + 1, region);
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.top; y < rect.bottom; y++) {
for (var x = rect.left; x < rect.right; x++) {
for (var y = rect.y0; y < rect.y1; y++) {
for (var x = rect.x0; x < rect.x1; x++) {
if (regions[(x, y)] == null) {
floodfill(x, y, nextRegion);
if (floodfill(x, y) > 0) {
nextRegion += 1;
}
}
}
return _toExplicit(regions);
}
return (_toExplicit(regions, nextRegion), regions);
}
List<Region> _toExplicit(Map<(int, int), int> regions) {
List<Set<(int, int)>> pointsOut = [
for (var i = 0; i < regions.length; i++) Set()
];
for (var MapEntry(key: (x, y), value: id_) in regions.entries) {
pointsOut[id_].add((x, y));
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 pointsOut) Region.fromNonEmptySet(s)];
return [for (var s in regionPoints) Region.fromNonEmptySet(s)];
}

View File

@ -0,0 +1,141 @@
// Port of https://www.albertford.com/shadowcasting/
import 'package:dartterm/algorithms/geometry.dart' as geo;
void shadowcast(geo.Offset origin, bool Function(geo.Offset) isBlocking,
Function(geo.Offset) markVisible) {
markVisible(origin);
for (var i = 0; i < 4; i++) {
var quadrant = Quadrant(i, origin.x, origin.y);
void reveal(geo.Offset tile) {
markVisible(quadrant.transform(tile));
}
bool isWall(geo.Offset? tile) {
if (tile == null) {
return false;
}
return isBlocking(quadrant.transform(tile));
}
bool isFloor(geo.Offset? tile) {
if (tile == null) {
return false;
}
return !isBlocking(quadrant.transform(tile));
}
void scan(Row row) {
geo.Offset? prevTile;
for (var tile in row.tiles()) {
if (isWall(tile) || isSymmetric(row, tile)) {
reveal(tile);
}
if (isWall(prevTile) && isFloor(tile)) {
row.startSlope = slope(tile);
}
if (isFloor(prevTile) && isWall(tile)) {
Row nextRow = row.next();
nextRow.endSlope = slope(tile);
scan(nextRow);
}
prevTile = tile;
}
if (isFloor(prevTile)) {
scan(row.next());
}
}
Row firstRow = Row(1, Fraction(-1, 1), Fraction(1, 1));
scan(firstRow);
}
}
class Quadrant {
static const int north = 0;
static const int east = 1;
static const int south = 2;
static const int west = 3;
final int cardinal;
final int ox, oy;
Quadrant(this.cardinal, this.ox, this.oy);
geo.Offset transform(geo.Offset tile) {
var geo.Offset(x: row, y: col) = tile;
switch (cardinal) {
case north:
return geo.Offset(ox + col, oy - row);
case south:
return geo.Offset(ox + col, oy + row);
case east:
return geo.Offset(ox + row, oy + col);
case west:
return geo.Offset(ox - row, oy + col);
default:
throw Exception("Quadrant must be initialized with a real cardinal");
}
}
}
class Row {
int depth;
Fraction startSlope;
Fraction endSlope;
Row(this.depth, this.startSlope, this.endSlope);
Iterable<geo.Offset> tiles() sync* {
var minCol = roundTiesUp(startSlope.scale(depth));
var maxCol = roundTiesDown(endSlope.scale(depth));
for (int col = minCol; col <= maxCol; col++) {
yield geo.Offset(depth, col);
}
}
Row next() {
return Row(depth + 1, startSlope, endSlope);
}
}
class Fraction {
final int numerator;
final int denominator;
Fraction(this.numerator, this.denominator);
Fraction scale(int n) {
return Fraction(numerator * n, denominator);
}
// We're often comparing this to an int or a double, so it's OK
// to have precision loss _so long as we do all divides after all multiplies_
double toDouble() {
return numerator.toDouble() / denominator.toDouble();
}
}
Fraction slope(geo.Offset tile) {
var geo.Offset(x: rowDepth, y: col) = tile;
return Fraction(2 * col - 1, 2 * rowDepth);
}
bool isSymmetric(Row row, geo.Offset tile) {
var geo.Offset(x: rowDepth, y: col) = tile;
return (col >= row.startSlope.scale(rowDepth).toDouble() &&
col <= (row.endSlope.scale(row.depth)).toDouble());
}
int roundTiesUp(Fraction n) {
return (n.toDouble() + 0.5).floor();
}
int roundTiesDown(Fraction n) {
return (n.toDouble() - 0.5).ceil();
}

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

@ -1,8 +1,26 @@
import 'package:flutter/material.dart';
class Palette {
static const defaultBg = Colors.black;
static const defaultFg = Colors.white;
static const defaultBg = Color(0xFF272D1B);
static const uiBg = Color(0xFF232308);
static const defaultFg = Color(0xFFEEE9D1);
static const demoPlayer = Color(0xFFFEFEF2);
static const subtitle = Colors.red;
static const demoDoor = Color(0xFF847A4B);
static const demoExit = Color(0xFF847A4B);
static const sitemodePlayer = demoPlayer;
static const sitemodeSeenDoor = Color(0xFFFEFD4B);
static const sitemodeUnseenDoor = demoExit;
static const sitemodeSeenWall = defaultFg;
static const sitemodeUnseenWall = demoExit;
static const sitemodeSeenExit = defaultFg;
static const sitemodeUnseenExit = demoExit;
static const sitemodeSeenFloor = uiBg;
static const demoFloorHighlight = Color(0xFF863B6F);
}

View File

@ -260,6 +260,7 @@ final List<String> _fromCp437 = [
"\u00a0"
];
final Map<String, Cp437> _toCp437 = {};
final Map<int, Cp437> _toCp437I = {};
void _init() {
if (initialized) {
@ -268,6 +269,7 @@ void _init() {
for (final (i, c) in _fromCp437.indexed) {
_toCp437[c] = i;
_toCp437I[c.runes.first] = i;
}
initialized = true;
@ -291,6 +293,7 @@ Cp437 toCp437Char(String c) {
}
String fromCp437String(List<Cp437> s) {
_init();
var out = "";
for (final c in s) {
out += fromCp437Char(c);
@ -299,9 +302,10 @@ String fromCp437String(List<Cp437> s) {
}
List<Cp437> toCp437String(String s) {
_init();
List<Cp437> out = [];
for (final c in s.runes) {
out.add(c);
out.add(_toCp437I[c] ?? toCp437Char("?"));
}
return out;
}

View File

@ -1,64 +1,15 @@
import 'dart:developer';
import 'dart:math' as math;
import 'package:dartterm/assets.dart';
import 'package:dartterm/colors.dart';
import 'package:dartterm/game/sitemode/sitemode.dart';
import 'package:dartterm/gen/generator.dart';
import 'package:dartterm/skreek.dart';
import 'package:dartterm/terminal.dart';
import 'package:dartterm/world/level.dart';
void main() async {
Vaults vaults;
while (true) {
log("about to load template");
at(0, 0).clear();
at(0, 0).puts("Loading template!");
Vaults? maybeVaults =
getVaultsIfAvailable("assets/images/wfc/bighouse2.png");
var level = await getLevel();
if (maybeVaults != null) {
log("wasn't null!");
vaults = maybeVaults;
break;
}
await zzz(0.1);
}
at(0, 0).clear();
at(0, 0).puts("Loaded! $vaults");
int seed = 0;
while (true) {
Vault output = Generator(math.Random(seed), vaults).generateBoxed(
Requirement(
32,
24,
DirectionSet({
Direction.up,
Direction.left,
Direction.right,
Direction.down
})));
var w = output.vx;
var h = output.vy;
for (var y = 0; y < h; y++) {
for (var x = 0; x < w; x++) {
var cursor = at(x * 2, y * 2).big();
switch (output.tiles[x + y * w]) {
case LevelTile.floor:
cursor.puts(" ");
case LevelTile.door:
cursor.puts("d");
case LevelTile.wall:
cursor.puts("#");
case LevelTile.exit:
cursor.puts("X");
}
}
}
seed += 1;
await zzz(0.1);
}
await sitemode(level);
}
/*
void main() async {
@ -94,3 +45,28 @@ void main() async {
}
}
*/
Future<Level> getLevel() async {
Vaults vaults;
while (true) {
Vaults? maybeVaults =
getVaultsIfAvailable("assets/images/vaults/house1.png");
if (maybeVaults != null) {
vaults = maybeVaults;
break;
}
await zzz(0.1);
}
return Generator(math.Random(0), vaults).generateLevel(Requirement(
16,
32,
16,
18,
DirectionSet({
Direction.up,
Direction.down,
Direction.left,
Direction.right,
})));
}

View File

@ -0,0 +1,84 @@
import 'dart:math' as math;
import 'package:dartterm/assets.dart';
import 'package:dartterm/algorithms/geometry.dart' as geo;
import 'package:dartterm/colors.dart';
import 'package:dartterm/gen/generator.dart';
import 'package:dartterm/input.dart';
import 'package:dartterm/skreek.dart';
import 'package:dartterm/terminal.dart';
import 'package:dartterm/world/level.dart';
void generator_test_program() async {
Vaults vaults;
while (true) {
skreek("about to load template");
at(0, 0).clear();
at(0, 0).puts("Loading template!");
Vaults? maybeVaults =
getVaultsIfAvailable("assets/images/vaults/house1.png");
if (maybeVaults != null) {
skreek("wasn't null!");
vaults = maybeVaults;
break;
}
await zzz(0.1);
}
at(0, 0).clear();
at(0, 0).puts("Loaded! $vaults");
int seed = 0;
while (true) {
clear();
Level output =
Generator(math.Random(seed), vaults).generateLevel(Requirement(
16,
32,
16,
18,
DirectionSet({
Direction.up,
Direction.down,
Direction.left,
Direction.right,
})));
var geo.Size(dx: w, dy: h) = output.size;
for (var y = 0; y < h; y++) {
for (var x = 0; x < w; x++) {
var cursor = at(x * 2, y * 2).big();
switch (output.tiles.get(x, y)) {
case LevelTile.floor:
case LevelTile.openDoor:
cursor.puts(" ");
case LevelTile.closedDoor:
cursor.fg(Palette.demoDoor).puts("+");
case LevelTile.exit:
cursor.fg(Palette.demoExit).puts("X");
case LevelTile.wall:
cursor.puts("#");
case null:
cursor.puts("?");
}
}
}
at(output.spawn.x * 2, output.spawn.y * 2)
.fg(Palette.demoPlayer)
.big()
.puts("\u00ff");
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;
break inpLoop;
default:
}
}
}
}

View File

@ -0,0 +1,127 @@
part of 'sitemode.dart';
// We render each thing as a 2x2 block.
// We want the player's cell to be
// _actually centered_, and the terminal is an even number of cells across
// So, shifting the camera an extra cell to the north and then
// drawing one extra tile offscreen? That should accomplish it!
const cameraViewWidth = Terminal.width ~/ 2 + 1;
const cameraViewHeight = Terminal.height ~/ 2 + 1;
const cameraMargin =
4; // how far the camera is from the ideal position before it pans
const cameraTileOffset = geo.Offset(-1, -1);
extension CameraParts on SiteMode {
void cameraInit() {
camera = _cameraIdeal();
}
void cameraMaintain() {
var ideal = _cameraIdeal();
while (camera.x < ideal.x - cameraMargin) {
camera = geo.Offset(camera.x + 1, camera.y);
}
while (camera.x > ideal.x + cameraMargin) {
camera = geo.Offset(camera.x - 1, camera.y);
}
while (camera.y < ideal.y - cameraMargin) {
camera = geo.Offset(camera.x, camera.y + 1);
}
while (camera.y > ideal.y + cameraMargin) {
camera = geo.Offset(camera.x, camera.y - 1);
}
}
geo.Offset _cameraIdeal() {
return geo.Offset(playerPosition.x - cameraViewWidth ~/ 2 - 1,
playerPosition.y - cameraViewHeight ~/ 2 - 1);
}
void cameraDraw() {
// Draw the world!
// Work in columns, top to bottom, which should facilitate our fake 3D effects
for (var cx = 0; cx < cameraViewWidth; cx++) {
for (var cy = 0; cy < cameraViewHeight; cy++) {
var tx = cameraTileOffset.x + cx * 2;
var ty = cameraTileOffset.y + cy * 2;
cameraDrawFloor(tx, ty, camera.x + cx, camera.y + cy);
}
}
for (var cx = 0; cx < cameraViewWidth; cx++) {
for (var cy = 0; cy < cameraViewHeight; cy++) {
var tx = cameraTileOffset.x + cx * 2;
var ty = cameraTileOffset.y + cy * 2;
cameraDrawCell(tx, ty, camera.x + cx, camera.y + cy);
}
}
}
void cameraDrawFloor(int tx, int ty, int cx, int cy) {
var cxy = geo.Offset(cx, cy);
if (!fovVisible.contains(cxy)) {
return;
}
var tile = level.tiles.get(cx, cy);
at(tx, ty).bg(Palette.sitemodeSeenFloor).puts(" ");
}
void cameraDrawCell(int tx, int ty, int cx, int cy) {
var cursorAbove = at(tx, ty);
var cursorBelow = at(tx, ty + 2);
LevelTile? tile;
// TODO: Fade tiles when loaded from memory
// TODO: Darken live floors
bool seenLive;
var cxy = geo.Offset(cx, cy);
if (fovVisible.contains(cxy)) {
tile = level.tiles.get(cx, cy);
seenLive = true;
} else if (fovMemory.contains(cxy)) {
tile = level.tiles.get(cx, cy);
seenLive = false;
} else {
return;
}
if (fovMovable.contains(geo.Offset(cx, cy))) {
cursorAbove
.highlight()
.act(Act(label: "Move", callback: () => playerMoveTo(cx, cy)));
}
var colorWall = Palette.sitemodeSeenWall;
var colorWallInner = Palette.sitemodeSeenWall;
var colorExit = Palette.sitemodeSeenExit;
var colorDoor = Palette.sitemodeSeenDoor;
if (cy >= playerPosition.y) {
colorWallInner = Palette.sitemodeUnseenWall; // we wouldn't see it anyway
}
if (!seenLive) {
colorWallInner = colorWall = Palette.sitemodeUnseenWall;
colorExit = Palette.sitemodeUnseenExit;
colorDoor = Palette.sitemodeUnseenDoor;
}
switch (tile) {
case null:
case LevelTile.floor:
case LevelTile.openDoor:
cursorAbove.touch(2);
case LevelTile.exit:
cursorAbove.big().fg(colorExit).puts("X");
case LevelTile.wall:
cursorAbove.fg(colorWall).puts("██");
cursorBelow.small().fg(colorWallInner).puts("░░");
case LevelTile.closedDoor:
cursorAbove.big().fg(colorDoor).puts("+");
}
var cursorContent = at(tx, ty);
if (geo.Offset(cx, cy) == playerPosition) {
cursorContent.fg(Palette.sitemodePlayer).big().putc(0xff);
}
}
}

View File

@ -0,0 +1,81 @@
part of 'sitemode.dart';
const double fovMaxMovementCost = 10.0;
const double fovMaxMemoryDistance = 80.0;
extension FOVParts on SiteMode {
void fovMaintain() {
fovVisible = {};
shadowcast(playerPosition, _fovIsBlocking, (xy) => fovVisible.add(xy));
var oldFovMemory = fovMemory;
fovMemory = {};
oldFovMemory.addAll(fovVisible);
for (var xy in oldFovMemory) {
if (_fovMemorablyClose(xy)) {
fovMemory.add(xy);
}
}
fovMovable = {};
for (var r in dijkstra<geo.Offset>(playerPosition, _fovDijkstraNeighbors)) {
if (r.cost > fovMaxMovementCost) {
break;
}
fovMovable.add(r.item);
}
}
bool _fovMemorablyClose(geo.Offset other) {
var dx = (other.x - playerPosition.x).toDouble();
var dy = (other.y - playerPosition.y).toDouble();
return math.sqrt(dx * dx + dy * dy) < fovMaxMemoryDistance;
}
bool _fovKnown(geo.Offset other) {
return fovVisible.contains(other) || fovMemory.contains(other);
}
bool _fovIsBlocking(geo.Offset xy) {
switch (level.tiles.get(xy.x, xy.y)) {
case null:
return true;
case LevelTile.exit:
return true;
case LevelTile.openDoor:
case LevelTile.floor:
return false;
case LevelTile.wall:
return true;
case LevelTile.closedDoor:
return true;
}
}
Iterable<Edge<geo.Offset>> _fovDijkstraNeighbors(geo.Offset xy) sync* {
var tile = level.tiles.get(xy.x, xy.y);
if (tile == LevelTile.exit || tile == LevelTile.closedDoor) {
// can't go anywhere after this
return;
}
for (var (dx, dy) in [(-1, 0), (1, 0), (0, -1), (0, 1)]) {
var xy2 = geo.Offset(xy.x + dx, xy.y + dy);
// Only return visible or remembered tiles
if (!_fovKnown(xy2)) {
continue;
}
var tile = level.tiles.get(xy2.x, xy2.y);
if (tile == LevelTile.wall) {
continue;
}
// for now, because exiting isn't implemented
if (tile == LevelTile.exit) {
continue;
}
yield Edge(1.0, xy2);
}
}
}

View File

@ -0,0 +1,33 @@
part of 'sitemode.dart';
extension PlayerParts on SiteMode {
void playerMaintain() {
playerTookAutomatedAction = false;
var geo.Offset(:x, :y) = playerPosition;
var tile = level.tiles.get(x, y);
if (tile == LevelTile.closedDoor) {
level.tiles.set(x, y, LevelTile.openDoor);
}
if (playerIntendedPath.isNotEmpty) {
var nextPosition = playerIntendedPath.removeAt(0);
playerPosition = nextPosition;
playerTookAutomatedAction = true;
}
}
// support the UI command here
Future<void> playerMoveTo(int cx, int cy) async {
var ipath = dijkstraPath(
geo.Offset(playerPosition.x, playerPosition.y),
geo.Offset(cx, cy),
_fovDijkstraNeighbors,
);
if (ipath == null) {
return; // don't move, I guess
}
playerIntendedPath.addAll(ipath);
}
}

View File

@ -0,0 +1,65 @@
import 'package:dartterm/algorithms/dijkstra.dart';
import 'package:dartterm/algorithms/geometry.dart' as geo;
import 'package:dartterm/algorithms/shadowcasting.dart';
import 'package:dartterm/colors.dart';
import 'package:dartterm/skreek.dart';
import 'package:dartterm/terminal.dart';
import 'package:dartterm/world/level.dart';
import 'dart:math' as math;
part 'camera.dart';
part 'player.dart';
part 'fov.dart';
Future<void> sitemode(Level level) async {
await SiteMode(level).start();
}
class SiteMode {
Level level;
late geo.Offset playerPosition;
bool playerTookAutomatedAction = false;
List<geo.Offset> playerIntendedPath = [];
late geo.Offset camera;
late Set<geo.Offset> fovVisible;
late Set<geo.Offset> fovMovable;
Set<geo.Offset> fovMemory = {};
SiteMode(this.level) {
playerPosition = level.spawn;
init();
maintain();
}
void init() {
cameraInit();
}
void maintain() {
playerMaintain();
fovMaintain();
cameraMaintain();
}
void draw() {
clear();
cameraDraw();
}
Future<void> start() async {
while (true) {
maintain();
draw();
// take automated actions, otherwise receive input
if (playerTookAutomatedAction) {
await zzz(0.1);
} else {
await waitMenu();
}
}
}
}

View File

@ -0,0 +1,34 @@
/*
void main() async {
var descriptor = "generic";
while (true) {
at(0, 0).clear();
at(0, 0).puts("Hello, bats!");
at(0, 2).fg(Palette.subtitle).small().puts("Beware of the bat!");
at(4, 4)
.bg(Palette.subtitle)
.fg(Palette.defaultBg)
.big()
.highlight()
.act(Act(
isDefault: true,
label: "Strong!",
callback: () async {
log("strong!");
descriptor = "strong";
}))
.act(Act(
label: "Nocturnal!",
callback: () async {
log("nocturnal!");
descriptor = "nocturnal";
}))
.puts("ALTER BAT");
at(4, 8).normal().puts("A $descriptor bat!");
// await zzz(1.0);
await waitMenu();
}
}
*/

View File

@ -1,6 +1,10 @@
import 'dart:developer';
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';
@ -8,9 +12,10 @@ part 'direction_set.dart';
part 'orientation.dart';
part 'requirement.dart';
part 'vault.dart';
part 'vault_tile.dart';
part 'vaults.dart';
const vaultTries = 10;
const vaultTries = 30;
class Generator {
final math.Random _random;
@ -19,25 +24,25 @@ class Generator {
Generator(this._random, this._vaults);
Vault generateBoxed(Requirement req) {
var vx = req.vx;
var vy = req.vy;
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;
Level generateLevel(Requirement requirement) {
var out = _generateOriented(requirement, false);
return _finalize(out);
}
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);
if (suggested != null) {
return suggested;
return _fillMetaRegions(requirement, suggested);
}
}
// First of all: randomize orientation
@ -46,44 +51,109 @@ class Generator {
// Try to make vx the long axis if possible
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
}
// 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(req2);
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}");
assert(out1.vx == requirement.vx);
assert(out1.vy == requirement.vy);
// 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 _generate(Requirement req) {
var vx = req.vx;
var vy = req.vy;
var v = Vault.blank(vx, vy, req.smooth, LevelTile.wall);
Vault _generateBsp(Requirement req) {
var vxMin = req.vxMin;
var vyMin = req.vyMin;
var vxMax = req.vxMax;
var vyMax = req.vyMax;
if (vx < 5 || vx * vy < 10) {
v.clear(LevelTile.floor);
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 {
// pick a split point
var splitVx = _random.nextInt(vx - 4) + 2;
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 reqLeft = Requirement(splitVx, vy, req.smooth.clone());
reqLeft.smooth.directions.add(Direction.right);
var reqRight = Requirement((vx - splitVx - 1), vy, req.smooth.clone());
reqRight.smooth.directions.add(Direction.left);
var vyMinRight = vyMin;
var vyMaxRight = vyMax;
var vaultLeft = generate(reqLeft);
var vaultRight = generate(reqRight);
v.blitFrom(vaultLeft, 0, 0);
v.blitFrom(vaultRight, splitVx + 1, 0);
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++) {
@ -112,46 +182,286 @@ class Generator {
}
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;
}
if (vault.vx < req.vx / 2 || vault.vy < req.vy / 2) {
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;
}
var rsd = req.smooth.directions;
bool mustFillX =
rsd.contains(Direction.left) && rsd.contains(Direction.right);
if (vault.vx != req.vx && mustFillX) {
return null;
}
bool mustFillY =
rsd.contains(Direction.left) && rsd.contains(Direction.right);
if (vault.vy != req.vy && mustFillY) {
return null;
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;
}
Vault full = Vault.blank(req.vx, req.vy, req.smooth, LevelTile.wall);
int vx = vault.vx;
int dx;
if (rsd.contains(Direction.left)) {
dx = 0;
} else if (rsd.contains(Direction.right)) {
dx = req.vx - vx;
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<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 {
dx = _random.nextInt(req.vx - vx);
continue;
}
int vy = vault.vy;
int dy;
if (rsd.contains(Direction.up)) {
dy = 0;
} else if (rsd.contains(Direction.down)) {
dy = req.vy - vy;
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);
}
}
}
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 {
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';
class Requirement {
final int vx, vy;
final int vxMin, vxMax, vyMin, vyMax;
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() {
return Requirement(vx, vy, smooth.flip());
return Requirement(vxMin, vxMax, vyMin, vyMax, smooth.flip());
}
Requirement rotateLeft() {
return Requirement(vy, vx, smooth.rotateLeft());
return Requirement(vyMin, vyMax, vxMin, vxMax, smooth.rotateLeft());
}
Requirement rotateRight() {
return Requirement(vy, vx, 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;
return Requirement(vyMin, vyMax, vxMin, vxMax, smooth.rotateRight());
}
}

View File

@ -1,124 +1,41 @@
part of 'generator.dart';
class Vault {
final int vx, vy;
final Bitmap<VaultTile> tiles;
final DirectionSet smooth;
final List<LevelTile> tiles;
Vault(this.vx, this.vy, this.smooth, this.tiles) {
assert(tiles.length == vx * vy);
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);
}
// 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.
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;
}
static Vault blankWith(
int vx, int vy, VaultTile Function(int, int) lt, DirectionSet smooth) {
return Vault(Bitmap.blankWith(vx, vy, lt), smooth);
}
return Vault(vx, vy, DirectionSet(smooth), tiles);
}
void clear(LevelTile lt) {
for (var y = 0; y < vy; y++) {
for (var x = 0; x < vx; x++) {
tiles[y * vx + x] = lt;
}
}
void clear(VaultTile lt) {
tiles.clear(lt);
}
void blitFrom(Vault other, int dx, int dy) {
assert(dx >= 0);
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];
}
}
tiles.blitFromWith(other.tiles, dx, dy, mergeVaultTile);
}
Vault flip() {
List<LevelTile> tiles2 = [
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);
return Vault(tiles.flip(), smooth.flip());
}
// TODO: Actually test this logic.
// This worked in Python, so it might even be right!
Vault rotateRight() {
List<LevelTile> tiles2 = [
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);
return Vault(tiles.rotateRight(), smooth.rotateRight());
}
Vault rotateLeft() {
List<LevelTile> tiles2 = [
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) {
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;
}
static Vault blank(int vx, int vy, DirectionSet smooth, LevelTile lt) {
var tiles = [
for (var y = 0; y < vy; y++)
for (var x = 0; x < vx; x++) lt
];
return Vault(vx, vy, smooth, tiles);
return Vault(tiles.rotateLeft(), smooth.rotateLeft());
}
}

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 {
final List<Vault> _primitive = [];
static Future<Vaults> load(String filename) async {
// TODO
return Vaults();
}
List<Vault> randomFlight(math.Random rng) {
// TODO: There are many more efficient ways to do this!
List<Vault> list2 = [];
@ -15,4 +10,107 @@ class Vaults {
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) {
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);
String? text() => _text;
String? get text => _text;
}
enum Button { left, right }

View File

@ -1,6 +1,6 @@
import 'package:dartterm/colors.dart';
import 'package:dartterm/input.dart' as input;
import 'package:dartterm/game.dart' as game;
import 'package:dartterm/game/game.dart' as game;
import 'package:dartterm/terminal.dart' as terminal;
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
@ -17,6 +17,7 @@ class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'DARTTERM',
theme: ThemeData(
useMaterial3: true, scaffoldBackgroundColor: Palette.defaultBg),

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

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

View File

@ -18,12 +18,20 @@ class Cursor {
t._tiles = [for (var i = 0; i < nTiles; i += 1) Tile()];
}
void putc(int c) {
void putc(int? c) {
for (var dx = 0; dx < font.cellWidth; dx += 1) {
for (var dy = 0; dy < font.cellHeight; dy += 1) {
var i = t._fromXY(x + dx, y + dy);
if (i != null) {
// you can pass null and in that case a character-sized block of
// screen is automatically UI-affected, but not drawn
if (c == null) {
t._tiles[i].highlightGroup = highlightGroup;
t._tiles[i].actions.addAll(_acts);
continue;
}
final (sourceCx, sourceCy) = (
(c % font.nCellsW) * font.cellWidth + dx,
(c ~/ font.nCellsW) * font.cellHeight + dy
@ -48,6 +56,13 @@ class Cursor {
}
}
void touch(int n) {
for (var i = 0; i < n; i++) {
putc(null);
x += font.cellWidth;
}
}
void puts(String s) {
var startX = x;
for (final c in toCp437String(s)) {

View File

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

View File

@ -1,29 +1,23 @@
enum LevelTile {
exit,
door,
floor,
wall,
}
import 'package:dartterm/bitmap.dart';
import 'package:dartterm/algorithms/geometry.dart' as geo;
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) {
switch (c) {
// ABGR
case 0xFF000000:
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

@ -34,7 +34,7 @@ packages:
source: hosted
version: "1.1.1"
collection:
dependency: transitive
dependency: "direct main"
description:
name: collection
sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687

View File

@ -28,6 +28,7 @@ environment:
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
collection: ^1.17.2
flutter:
sdk: flutter