Compare commits

..

20 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
b5466919e6 Use vaults if we have them 2023-09-20 19:30:03 -07:00
0a600fd930 Break up the vaults code into multiple files 2023-09-20 18:09:08 -07:00
b91f3097f2 Add correct array rotation code 2023-09-19 21:58:31 -07:00
04b580b22c Enter a demo mode, generating lots of levels 2023-09-19 21:13:31 -07:00
508fd28f04 Add another TODO comment 2023-09-19 21:04:51 -07:00
5d941afe5e Add a comment on testing rotateRight/rotateLeft 2023-09-19 21:03:07 -07:00
dd92246402 Vault/BSP based level generator, part 1 2023-09-19 21:02:04 -07:00
42 changed files with 2025 additions and 878 deletions

15
LICENSE.md Normal file
View File

@ -0,0 +1,15 @@
Incorporates GraphLab's implementation of Union/Find, which is by Richard Griffith. Here are the licensing terms of that:
> The BSD 2-Clause License
> http://www.opensource.org/licenses/bsd-license.php
>
> Copyright (c) 2013, Richard Griffith
> All rights reserved.
>
> Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
>
> Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
> Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
>
> The views and conclusions contained in the software and documentation are those of the author and should not be interpreted as representing official policies, either expressed or implied, of the FreeBSD Project.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 915 B

View File

@ -0,0 +1,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

@ -0,0 +1,76 @@
import 'dart:math' as math;
import 'package:dartterm/algorithms/geometry.dart' as geo;
class Region {
final geo.Rect rect;
final Set<(int, int)> points;
bool get isRectangle => points.length == rect.dx * rect.dy;
Region(this.rect, this.points);
static Region fromNonEmptySet(Set<(int, int)> s) {
assert(s.isNotEmpty);
int xMin = s.map<int>((xy) => xy.$1).reduce(math.min);
int yMin = s.map<int>((xy) => xy.$2).reduce(math.min);
int xMax = s.map<int>((xy) => xy.$1).reduce(math.max);
int yMax = s.map<int>((xy) => xy.$2).reduce(math.max);
var rect = geo.Rect(xMin, yMin, xMax - xMin + 1, yMax - yMin + 1);
return Region(rect, s);
}
@override
String toString() {
return "Region($rect, $points)";
}
}
(List<Region>, Map<(int, int), int>) regionalize(
geo.Rect rect, bool Function(int, int) isAccessible) {
int nextRegion = 0;
Map<(int, int), int> regions = {};
int floodfill(int x, int y) {
int workDone = 0;
if (!rect.containsPoint(geo.Offset(x, y))) {
return workDone;
}
if (regions[(x, y)] != null) {
return workDone;
}
if (!isAccessible(x, y)) {
return workDone;
}
regions[(x, y)] = nextRegion;
workDone += 1;
workDone += floodfill(x - 1, y);
workDone += floodfill(x + 1, y);
workDone += floodfill(x, y - 1);
workDone += floodfill(x, y + 1);
return workDone;
}
// TODO: This can be done more efficiently with a union/find data structure
// But this is an easy implementation to understand
for (var y = rect.y0; y < rect.y1; y++) {
for (var x = rect.x0; x < rect.x1; x++) {
if (regions[(x, y)] == null) {
if (floodfill(x, y) > 0) {
nextRegion += 1;
}
}
}
}
return (_toExplicit(regions, nextRegion), regions);
}
List<Region> _toExplicit(Map<(int, int), int> pointRegions, int nRegions) {
List<Set<(int, int)>> regionPoints = [for (var i = 0; i < nRegions; i++) {}];
for (var MapEntry(key: (x, y), value: id_) in pointRegions.entries) {
regionPoints[id_].add((x, y));
}
return [for (var s in regionPoints) Region.fromNonEmptySet(s)];
}

View File

@ -0,0 +1,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
}
}
}

View File

@ -1,7 +1,6 @@
import 'dart:ui' as ui;
import 'package:dartterm/wfc/template.dart';
import 'package:dartterm/world/level.dart';
import 'package:dartterm/gen/generator.dart';
import 'package:flutter/services.dart';
class Assets {
@ -14,15 +13,14 @@ class Assets {
return image;
});
final _Table<WfcTemplate<LevelTile>> _wfcLevelTemplates =
_Table(loadLevelWfcAsync);
final _Table<Vaults> _vaults = _Table(Vaults.load);
ui.Image? getImageIfAvailable(String name) {
return _images.getIfAvailable(name);
}
WfcTemplate<LevelTile>? getWfcLevelTemplateIfAvailable(String name) {
return _wfcLevelTemplates.getIfAvailable(name);
Vaults? getVaultsIfAvailable(String name) {
return _vaults.getIfAvailable(name);
}
}
@ -53,6 +51,6 @@ ui.Image? getImageIfAvailable(String name) {
return assets.getImageIfAvailable(name);
}
WfcTemplate<LevelTile>? getWfcLevelTemplateIfAvailable(String name) {
return assets.getWfcLevelTemplateIfAvailable(name);
Vaults? getVaultsIfAvailable(String name) {
return assets.getVaultsIfAvailable(name);
}

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,86 +0,0 @@
import 'dart:developer';
import 'package:dartterm/assets.dart';
import 'package:dartterm/colors.dart';
import 'package:dartterm/terminal.dart';
import 'package:dartterm/wfc/template.dart';
import 'package:dartterm/world/level.dart';
void main() async {
WfcTemplate<LevelTile> template;
while (true) {
log("about to load template");
at(0, 0).clear();
at(0, 0).puts("Loading template!");
WfcTemplate<LevelTile>? maybeTemplate =
getWfcLevelTemplateIfAvailable("assets/images/wfc/bighouse2.png");
if (maybeTemplate != null) {
log("wasn't null!");
template = maybeTemplate;
break;
}
await zzz(0.1);
}
at(0, 0).clear();
at(0, 0).puts("Loaded! $template");
var W = 16;
var H = 16;
var wfc = Wfc(template, W, H);
wfc.run(1, -1);
var output = wfc.extractPartial();
for (var y = 0; y < W; y++) {
for (var x = 0; x < H; x++) {
var cursor = at(x * 2, y * 2).big();
switch (output[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");
case null:
cursor.puts("?");
}
}
}
}
/*
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();
}
}
*/

72
lib/game/game.dart Normal file
View File

@ -0,0 +1,72 @@
import 'dart:math' as math;
import 'package:dartterm/assets.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 {
var level = await getLevel();
await sitemode(level);
}
/*
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();
}
}
*/
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();
}
}
*/

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

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

View File

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

467
lib/gen/generator.dart Normal file
View File

@ -0,0 +1,467 @@
import 'dart:math' as math;
import 'package:dartterm/algorithms/geometry.dart' as geo;
import 'package:dartterm/algorithms/regionalize.dart';
import 'package:dartterm/algorithms/kruskal.dart';
import 'package:dartterm/bitmap.dart';
import 'package:dartterm/skreek.dart';
import 'package:dartterm/world/level.dart';
part 'direction.dart';
part 'direction_set.dart';
part 'orientation.dart';
part 'requirement.dart';
part 'vault.dart';
part 'vault_tile.dart';
part 'vaults.dart';
const vaultTries = 30;
class Generator {
final math.Random _random;
final Vaults _vaults;
List<Vault> _queue = [];
Generator(this._random, this._vaults);
Level generateLevel(Requirement requirement) {
var out = _generateOriented(requirement, false);
return _finalize(out);
}
/*
Vault generateVault(Requirement requirement) {
var out = _generateOriented(requirement, false);
var (vault, (_, _)) = _finalize(out);
return vault;
}
*/
Vault _generateOriented(Requirement requirement, bool canBeVault) {
if (canBeVault) {
Vault? suggested = _suggest(vaultTries, requirement);
if (suggested != null) {
return _fillMetaRegions(requirement, suggested);
}
}
// First of all: randomize orientation
// This way we only have to consider one kind of spilt
var orientation = randomOrientation(_random);
// Try to make vx the long axis if possible
var req2 = unReorientRequirement(requirement, orientation);
if (req2.vyMax > (req2.vxMax - 2) * 3 / 2) {
orientation = (orientation + 2) % 8; // rotate once more
}
// if only one of "left" and "right" needs to be smooth, prioritize right
// as left is generated first
req2 = unReorientRequirement(requirement, orientation);
if (req2.smooth.directions.contains(Direction.left) &&
req2.smooth.directions.contains(Direction.right)) {
orientation = (orientation + 4) % 8;
}
req2 = unReorientRequirement(requirement, orientation);
var out2 = _generateBsp(req2);
var out1 = reorientVault(out2, orientation);
// log("$orientation ${requirement.vx} ${requirement.vy} ${req2.vx} ${req2.vy} ${out2.vx} ${out2.vy} ${out1.vx} ${out1.vy}");
var geo.Size(:dx, :dy) = out1.size;
assert(dx >= requirement.vxMin && dx <= requirement.vxMax);
assert(dy >= requirement.vyMin && dy <= requirement.vyMax);
assert(out1.smooth.directions.containsAll(requirement.smooth.directions));
return out1;
}
Vault _generateBsp(Requirement req) {
var vxMin = req.vxMin;
var vyMin = req.vyMin;
var vxMax = req.vxMax;
var vyMax = req.vyMax;
var smoothUp = req.smooth.directions.contains(Direction.up);
var smoothDown = req.smooth.directions.contains(Direction.down);
var smoothUpDown = smoothUp && smoothDown;
// var vxRand = _random.nextInt(vxMax - vxMin) + vxMin;
var vyRand = _random.nextInt(vyMax + 1 - vyMin) + vyMin;
if (vxMax < 2 || vyMax < 2) {
return Vault.blank(vxMax, vyRand, VaultTile.defaultwall, req.smooth);
} else if (vxMax < 9 || (vxMax - 2) * (vyMax - 2) < 12) {
var v2 = Vault.blank(
vxMax - 2, vyMax - 2, VaultTile.bspfloor, req.smooth.clone());
var v = Vault.blank(vxMax, vyMax, VaultTile.wall, req.smooth.clone());
v.blitFrom(v2, 1, 1);
return v;
} else {
var leftReq = Requirement(
math.max(vxMin - 4, 2), vxMax - 4, vyMin, vyMax, req.smooth.clone());
leftReq.smooth.directions.add(Direction.right);
var leftChild = _generateOriented(leftReq, true);
var vyMinRight = vyMin;
var vyMaxRight = vyMax;
if (smoothUpDown) {
vyMaxRight = vyMinRight = leftChild.size.dy;
}
var rightReq = Requirement(
vxMin - (leftChild.vx - 1),
vxMax - (leftChild.vx - 1),
vyMinRight,
vyMaxRight,
req.smooth.clone(),
);
rightReq.smooth.directions.add(Direction.left);
var rightChild = _generateOriented(rightReq, true);
var vxTotal = leftChild.vx + rightChild.vx - 1;
var vyTotal = math.max(leftChild.vy, rightChild.vy);
if (smoothUp) {
var v = Vault.blank(
vxTotal, vyTotal, VaultTile.defaultwall, req.smooth.clone());
v.blitFrom(leftChild, 0, 0);
v.blitFrom(rightChild, leftChild.vx - 1, 0);
return v;
}
if (smoothDown) {
var v = Vault.blank(
vxTotal, vyTotal, VaultTile.defaultwall, req.smooth.clone());
v.blitFrom(leftChild, 0, vyTotal - leftChild.vy);
v.blitFrom(rightChild, leftChild.vx - 1, vyTotal - rightChild.vy);
return v;
}
// no smoothing reqs
// min: ensure some overlap
var vyTMax = math.min(vyMax, leftChild.vy + rightChild.vy - 3);
if (vyTMax > vyTotal) {
vyTotal += _random.nextInt(vyTMax - vyTotal);
}
var v = Vault.blank(
vxTotal, vyTotal, VaultTile.defaultwall, req.smooth.clone());
if (_random.nextBool()) {
v.blitFrom(leftChild, 0, 0);
v.blitFrom(rightChild, leftChild.vx - 1, vyTotal - rightChild.vy);
} else {
v.blitFrom(leftChild, 0, vyTotal - leftChild.vy);
v.blitFrom(rightChild, leftChild.vx - 1, 0);
}
return v;
}
}
Vault? _suggest(int tries, Requirement req) {
for (var i = 0; i < tries; i++) {
var sugg = _popSuggestion();
if (sugg == null) {
return null;
}
sugg = reorientVault(sugg, randomOrientation(_random));
sugg = _tidy(sugg, req);
if (sugg != null) {
return sugg;
}
}
return null;
}
Vault? _popSuggestion() {
if (_queue.isEmpty) {
_queue = _vaults.randomFlight(_random);
}
if (_queue.isEmpty) {
return null;
}
return _queue.removeLast();
}
Vault? _tidy(Vault vault, Requirement req) {
if (vault.vx > req.vxMax || vault.vy > req.vyMax) {
return null;
}
if (vault.vx < req.vxMin || vault.vy < req.vyMin) {
return null;
}
if (!vault.smooth.directions.containsAll(req.smooth.directions)) {
return null;
}
// NOTE: If the vault has metaBSP regions, and they touch the outer edge, then it should be possible
// to extend those regions
// Extending a metaBSP region results in _two_ metaBSP regions:
// a big version of the original, and a second one covering all the space left over
// in the set of rows or columns the metabsp region did not touch
//
// Ex:
// XXXX##
// XXXX #
// XXXX #
// # #
// ######
//
// becomes
//
// XXXZYY
// XXXZYY
// XXXZYY
// XXXX##
// XXXX #
// XXXX #
// # #
// ######
//
// (where the Zs are spaces that Xs and Ys both touch)
//
// Extension can happen more than once, on each axis:
//
// XXXXXZYY
// XXXXXZYY
// XXXXXZYY
// XXXXXX##
// XXXXXX #
// BBXXXX #
// AA# #
// AA######
//
return vault;
}
Vault _fillMetaRegions(Requirement requirement, Vault vault) {
var geo.Size(:dx, :dy) = vault.size;
var (metaregions, _) = regionalize(geo.Rect(0, 0, dx, dy),
(x, y) => vault.tiles.get(x, y) == VaultTile.meta0);
for (var i in metaregions) {
assert(i.isRectangle);
var sz = i.rect.size;
// TODO: Relax these based on our environs -- for instance, if one of our sides doesn't need to be smooth, that metaregion doesn't either
var metaRequirement = Requirement(
sz.dx,
sz.dx,
sz.dy,
sz.dy,
DirectionSet(
{Direction.up, Direction.left, Direction.down, Direction.right}));
var inner = _generateOriented(metaRequirement, true);
var dest = Vault(Bitmap.blank(vault.vx, vault.vy, VaultTile.defaultwall),
vault.smooth.clone());
dest.blitFrom(vault, 0, 0);
dest.blitFrom(inner, i.rect.x0, i.rect.y0);
vault = dest;
}
return vault;
}
Level _finalize(Vault subj) {
var vx = subj.vx, vy = subj.vy;
var orthoOffsets = [(0, -1), (0, 1), (-1, 0), (1, 0)];
// == build arches ==
bool floorlike(VaultTile? tile) {
return tile == VaultTile.bspfloor ||
tile == VaultTile.floor ||
tile == VaultTile.doorpronefloor ||
tile == VaultTile.exit;
}
bool walkable(VaultTile? tile) {
return tile == VaultTile.bspfloor ||
tile == VaultTile.floor ||
tile == VaultTile.doorpronefloor ||
tile == VaultTile.exit ||
tile == VaultTile.door;
}
List<(int, int)> newArches = [];
for (int x = 0; x < vx; x++) {
for (int y = 0; y < vy; y++) {
var t = subj.tiles.get(x, y);
if (t == VaultTile.archwall) {
var supporters = 0;
for (var (dx, dy) in orthoOffsets) {
VaultTile? neighbor = subj.tiles.get(x + dx, y + dy);
if (floorlike(neighbor)) {
supporters++;
}
}
if (supporters == 2) {
newArches.add((x, y));
}
subj.tiles.set(x, y, VaultTile.wall);
}
if (t == VaultTile.archpronewall || t == VaultTile.defaultwall) {
subj.tiles.set(x, y, VaultTile.wall);
}
}
}
for (var (ax, ay) in newArches) {
subj.tiles.set(ax, ay, VaultTile.floor);
}
// == build doors ==
var (regions, toRegion) =
regionalize(geo.Rect(0, 0, subj.vx, subj.vy), (x, y) {
return walkable(subj.tiles.get(x, y));
});
// generate one fake region for the exit doors to be in
Set<(int, int)> exitRegion = {};
for (var x = -2; x < subj.vx + 2; x++) {
exitRegion.add((x, -1));
exitRegion.add((x, subj.vy));
}
for (var y = -2; y < subj.vy + 2; y++) {
exitRegion.add((-1, y));
exitRegion.add((subj.vx, y));
}
int exitRegionId = regions.length;
for (var (x, y) in exitRegion) {
toRegion[(x, y)] = exitRegionId;
}
regions.add(Region.fromNonEmptySet(exitRegion));
// OK: now build the doors
double doorPoints(int x, int y) {
return subj.tiles.get(x, y) == VaultTile.doorpronefloor ? 0.5 : 0.0;
}
List<Edge<(int, int)>> possibleDoors = [];
for (var x = 0; x < subj.vx; x++) {
for (var y = 0; y < subj.vy; y++) {
double points;
int region0, region1;
if (subj.tiles.get(x, y) != VaultTile.wall) {
continue;
}
var regionL = toRegion[(x - 1, y)];
var regionR = toRegion[(x + 1, y)];
var regionU = toRegion[(x, y - 1)];
var regionD = toRegion[(x, y + 1)];
if (regionL != null &&
regionR != null &&
regionU == null &&
regionD == null) {
(region0, region1) = (regionL, regionR);
points = doorPoints(x - 1, y) + doorPoints(x + 1, y);
} else if (regionL == null &&
regionR == null &&
regionU != null &&
regionD != null) {
(region0, region1) = (regionU, regionD);
points = doorPoints(x, y - 1) + doorPoints(x, y + 1);
} else {
continue;
}
if (region0 == region1) {
continue;
}
int roomSize = math.min(
regions[region0].points.length,
regions[region1].points.length,
);
possibleDoors.add(Edge(
region0,
region1,
(x, y),
doorScore(region0 != exitRegionId && region1 != exitRegionId,
points, roomSize, _random.nextDouble())));
}
}
List<Edge<(int, int)>> exitDoors = [];
var minimalDoors = kruskal(regions.length, possibleDoors);
for (var d in minimalDoors) {
var (x, y) = d.value;
subj.tiles.set(x, y, VaultTile.door);
if (d.dst == exitRegionId || d.src == exitRegionId) {
exitDoors.add(d);
}
}
for (var x = 0; x < subj.vx; x++) {
for (var y = 0; y < subj.vy; y++) {
if (subj.tiles.get(x, y) == VaultTile.doorpronefloor) {
subj.tiles.set(x, y, VaultTile.floor);
}
}
}
if (exitDoors.length != 1) {
throw Exception("should be exactly one exit door");
}
// == Build the exit area ==
var (exitX, exitY) = exitDoors[0].value;
int exitVaultX, exitVaultY;
Vault finalVault;
int vaultBlitX, vaultBlitY;
if (exitX == 0 || exitX == vx - 1) {
finalVault =
Vault.blank(vx + 3, vy, VaultTile.defaultwall, DirectionSet({}));
vaultBlitX = exitX == 0 ? 3 : 0;
vaultBlitY = 0;
exitVaultX = exitX == 0 ? 1 : vx + 1;
exitVaultY = exitY;
} else if (exitY == 0 || exitY == vy - 1) {
finalVault =
Vault.blank(vx, vy + 3, VaultTile.defaultwall, DirectionSet({}));
vaultBlitX = 0;
vaultBlitY = exitY == 0 ? 3 : 0;
exitVaultX = exitX;
exitVaultY = exitY == 0 ? 1 : vy + 1;
} else {
throw Exception("exit door in invalid position $exitX $exitY $vx $vy");
}
for (var x = exitVaultX - 1; x <= exitVaultX + 1; x++) {
for (var y = exitVaultY - 1; y <= exitVaultY + 1; y++) {
finalVault.tiles.set(x, y, VaultTile.exit);
if (x == exitVaultX && y == exitVaultY ||
_manhattan(x, y, vaultBlitX + exitX, vaultBlitY + exitY) == 1) {
finalVault.tiles.set(x, y, VaultTile.floor);
}
}
}
finalVault.blitFrom(subj, vaultBlitX, vaultBlitY);
return Level(
Bitmap.blankWith(finalVault.vx, finalVault.vy,
(x, y) => flattenVaultTile(finalVault.tiles.get(x, y)!)),
geo.Offset(exitVaultX, exitVaultY));
}
}
// components:
// - is not exit (exit should be placed last so it doesn't get more than one door)
// - points for placement
// - size of the underlying room
// - random factor
double doorScore(bool isNotExit, double pointsForPlacement, int roomSize,
double randomFactor) {
assert(pointsForPlacement >= 0.0 && pointsForPlacement <= 1.0);
assert(roomSize >= 0 && roomSize < 100000);
assert(randomFactor >= 0.0 && randomFactor < 1.0);
return (isNotExit ? 1.0 : 0.0) * 1000000 +
pointsForPlacement * 100000 +
(100000 - roomSize).toDouble() +
randomFactor;
}
int _manhattan(int x0, int y0, int x1, int y1) {
return (x1 - x0).abs() + (y1 - y0).abs();
}

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

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

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

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

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

@ -0,0 +1,41 @@
part of 'generator.dart';
class Vault {
final Bitmap<VaultTile> tiles;
final DirectionSet smooth;
geo.Size get size => tiles.size;
int get vx => size.dx;
int get vy => size.dy;
Vault(this.tiles, this.smooth);
static Vault blank(int vx, int vy, VaultTile lt, DirectionSet smooth) {
return Vault(Bitmap.blank(vx, vy, lt), smooth);
}
static Vault blankWith(
int vx, int vy, VaultTile Function(int, int) lt, DirectionSet smooth) {
return Vault(Bitmap.blankWith(vx, vy, lt), smooth);
}
void clear(VaultTile lt) {
tiles.clear(lt);
}
void blitFrom(Vault other, int dx, int dy) {
tiles.blitFromWith(other.tiles, dx, dy, mergeVaultTile);
}
Vault flip() {
return Vault(tiles.flip(), smooth.flip());
}
Vault rotateRight() {
return Vault(tiles.rotateRight(), smooth.rotateRight());
}
Vault rotateLeft() {
return Vault(tiles.rotateLeft(), smooth.rotateLeft());
}
}

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

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

@ -0,0 +1,116 @@
part of 'generator.dart';
class Vaults {
final List<Vault> _primitive = [];
List<Vault> randomFlight(math.Random rng) {
// TODO: There are many more efficient ways to do this!
List<Vault> list2 = [];
list2.addAll(_primitive);
list2.shuffle(rng);
return list2;
}
static Future<Vaults> load(String name) async {
var basis = await Bitmap.load(name, colorToVaultTile);
var (regions, _) =
regionalize(basis.rect, (x, y) => basis.get(x, y) != null);
var vs = Vaults();
for (var region in regions) {
Vault v = loadVault(region, basis);
vs._primitive.add(v);
}
return vs;
}
static Vault loadVault(Region r, Bitmap<VaultTile?> b) {
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,281 +0,0 @@
import 'dart:math';
abstract class Model {
static var dx = [-1, 0, 1, 0];
static var dy = [0, 1, 0, -1];
static var opposite = [2, 3, 0, 1];
bool _initialized = false;
List<List<bool>> _wave = [];
List<List<List<int>>> propagator = [];
List<List<List<int>>> _compatible = [];
List<int?> _observed = [];
List<(int, int)> _stack = [];
int _stacksize = 0, _observedSoFar = 0;
int cMx = 0, cMy = 0, cT = 0, cN = 0;
bool _periodic = false;
bool ground = false;
List<double> weights = [];
List<double> _weightLogWeights = [], _distribution = [];
List<int> _sumsOfOnes = [];
double _sumOfWeights = 0.0,
_sumOfWeightLogWeights = 0.0,
_startingEntropy = 0.0;
List<double> _sumsOfWeights = [],
_sumsOfWeightLogWeights = [],
_entropies = [];
Heuristic _heuristic = Heuristic.Entropy;
Model(int width, int height, int n, bool periodic, Heuristic heuristic) {
cMx = width;
cMy = height;
cN = n;
_periodic = periodic;
_heuristic = heuristic;
}
void _init() {
_initialized = true;
_wave = [
for (var r = 0; r < cMx * cMy; r++) [for (var t = 0; t < cT; t++) false]
];
_compatible = [
for (var r = 0; r < cMx * cMy; r++)
[
for (var t = 0; t < cT; t++) [0, 0, 0, 0]
]
];
_distribution = [for (var t = 0; t < cT; t++) 0.0];
_observed = [for (var r = 0; r < cMx * cMy; r++) null];
_weightLogWeights = [
for (var t = 0; t < cT; t++) weights[t] * log(weights[t])
];
_sumOfWeights = 0;
_sumOfWeightLogWeights = 0.0;
for (var t = 0; t < cT; t++) {
_sumOfWeights += weights[t];
_sumOfWeightLogWeights += _weightLogWeights[t];
}
_startingEntropy =
log(_sumOfWeights) - _sumOfWeightLogWeights / _sumOfWeights;
_sumsOfOnes = [for (var r = 0; r < cMx * cMy; r++) 0];
_sumsOfWeights = [for (var r = 0; r < cMx * cMy; r++) 0.0];
_sumsOfWeightLogWeights = [for (var r = 0; r < cMx * cMy; r++) 0.0];
_entropies = [for (var r = 0; r < cMx * cMy; r++) 0.0];
_stack = [for (var r = 0; r < _wave.length * cT; r++) (0, 0)];
_stacksize = 0;
}
bool run(int? seed, int limit) {
if (!_initialized) {
_init();
}
clear();
var random = Random(seed);
for (var l = 0; l < limit || limit < 0; l++) {
var node = _nextUnobservedNode(random);
if (node >= 0) {
_observe(node, random);
var success = _propagate();
if (!success) {
return false;
}
} else {
for (var i = 0; i < _wave.length; i++) {
for (var t = 0; t < cT; t++) {
if (_wave[i][t]) {
_observed[i] = t;
break;
}
}
}
return true;
}
}
return true;
}
int _nextUnobservedNode(Random random) {
if (_heuristic == Heuristic.Scanline) {
for (var i = _observedSoFar; i < _wave.length; i++) {
if (!_periodic && (i % cMx + cN > cMx || i ~/ cMx + cN > cMy)) {
continue;
}
if (_sumsOfOnes[i] > 1) {
_observedSoFar = i + 1;
return i;
}
}
return -1;
}
double min = 1E+4;
int argmin = -1;
for (var i = 0; i < _wave.length; i++) {
if (!_periodic && (i % cMx + cN > cMx || i ~/ cMx + cN > cMy)) {
continue;
}
var remainingValues = _sumsOfOnes[i];
double entropy = _heuristic == Heuristic.Entropy
? _entropies[i]
: remainingValues.toDouble();
if (remainingValues > 1 && entropy <= min) {
double noise = 1E-6 * random.nextDouble();
if (entropy + noise < min) {
min = entropy + noise;
argmin = i;
}
}
}
return argmin;
}
void _observe(int node, Random random) {
var w = _wave[node];
for (var t = 0; t < cT; t++) {
_distribution[t] = w[t] ? weights[t] : 0.0;
}
int r = _chooseRandom(random, _distribution);
for (var t = 0; t < cT; t++) {
if (w[t] != (t == r)) {
_ban(node, t);
}
}
}
bool _propagate() {
while (_stacksize > 0) {
int i1, t1;
(i1, t1) = _stack[_stacksize - 1];
_stacksize--;
int x1 = i1 % cMx;
int y1 = i1 % cMy;
for (int d = 0; d < 4; d++) {
int x2 = x1 + dx[d];
int y2 = y1 + dy[d];
if (!_periodic &&
(x2 < 0 || y2 < 0 || x2 + cN > cMx || y2 + cN > cMy)) {
continue;
}
if (x2 < 0) {
x2 += cMx;
} else if (x2 >= cMx) {
x2 -= cMx;
}
if (y2 < 0) {
y2 += cMy;
} else if (y2 >= cMy) {
y2 -= cMy;
}
int i2 = x2 + y2 * cMx;
var p = propagator[d][t1];
var compat = _compatible[i2];
for (var l = 0; l < p.length; l++) {
var t2 = p[l];
var comp = compat[t2];
comp[d]--;
if (comp[d] == 0) {
_ban(i2, t2);
}
}
}
}
return _sumsOfOnes[0] > 0;
}
void _ban(int i, int t) {
_wave[i][t] = false;
var comp = _compatible[i][t];
for (var d = 0; d < 4; d++) {
comp[d] = 0;
}
_stack[_stacksize] = (i, t);
_stacksize++;
_sumsOfOnes[i] -= 1;
_sumsOfWeights[i] -= weights[t];
_sumsOfWeightLogWeights[i] -= _weightLogWeights[t];
var sum = _sumsOfWeights[i];
_entropies[i] = log(sum) - _sumsOfWeightLogWeights[i] / sum;
}
void clear() {
for (var i = 0; i < _wave.length; i++) {
for (var t = 0; t < cT; t++) {
_wave[i][t] = true;
for (var d = 0; d < 4; d++) {
_compatible[i][t][d] = propagator[opposite[d]][t].length;
}
}
_sumsOfOnes[i] = weights.length;
_sumsOfWeights[i] = _sumOfWeights;
_sumsOfWeightLogWeights[i] = _sumOfWeightLogWeights;
_entropies[i] = _startingEntropy;
_observed[i] = -1;
}
_observedSoFar = 0;
if (ground) {
for (var x = 0; x < cMx; x++) {
for (var t = 0; t < cT - 1; t++) {
_ban(x + (cMy - 1) * cMx, t);
}
for (var y = 0; y < cMy - 1; y++) {
_ban(x + y * cMx, cT - 1);
}
}
_propagate();
}
}
}
int _chooseRandom(Random rand, List<double> distribution) {
if (distribution.isEmpty) {
throw Exception("can't sample empty distribution");
}
var sum = 0.0;
for (var i = 0; i < distribution.length; i++) {
sum += distribution[i];
}
if (sum == 0.0) {
return rand.nextInt(distribution.length);
}
var rnd = rand.nextDouble() * sum;
var i = 0;
while (rnd > 0) {
rnd -= distribution[i];
if (rnd < 0) {
return i;
}
i += 1;
}
return distribution.length - 1;
}
enum Heuristic { Entropy, MRV, Scanline }

View File

@ -1,467 +0,0 @@
import 'dart:developer';
import 'dart:math' as math;
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'dart:ui';
import 'package:flutter/services.dart';
class Wfc<T> {
// parameters
final WfcTemplate<T> _template;
final int _mx, _my;
// constants
int get _n => _mx * _my;
int get _nShingles => _template._shingles.n;
int get _order => _template._order;
double _weight(int shingleIx) =>
_template._shingles._shingleWeights[shingleIx];
// overall algo state
List<List<bool>> _wave = [];
List<List<List<int>>> _compatible = [];
List<int?> _observed = [];
// computationally expensive stuff that we keep in an incremental way
List<int> _sumsOfOnes = [];
// temporaries
List<double> _distribution = [];
List<(int, int)> _stack = [];
int _stacksize = 0;
Wfc(this._template, this._mx, this._my) {
_wave = [
for (var r = 0; r < _n; r++) [for (var t = 0; t < _nShingles; t++) false]
];
_compatible = [
for (var r = 0; r < _n; r++)
[
for (var t = 0; t < _nShingles; t++) [0, 0, 0, 0]
]
];
_distribution = [for (var t = 0; t < _nShingles; t++) 0.0];
_observed = [for (var r = 0; r < _n; r++) null];
_sumsOfOnes = [for (var r = 0; r < _n; r++) 0];
_stack = [for (var r = 0; r < _n * _nShingles; r++) (0, 0)];
_stacksize = 0;
}
void clear() {
for (var i = 0; i < _wave.length; i++) {
for (var t = 0; t < _nShingles; t++) {
_wave[i][t] = true;
for (var d = 0; d < 4; d++) {
_compatible[i][t][d] = _template
._shingles._metadata._propagators[_opposite[d]][t].length;
}
}
_sumsOfOnes[i] = _nShingles;
_observed[i] = null;
}
}
bool run(int? seed, int limit) {
clear();
var random = math.Random(seed);
for (var l = 0; l < limit || limit < 0; l++) {
var node = _nextUnobservedNode(random);
if (node != null) {
_observe(node, random);
var success = _propagate();
if (!success) {
return false;
}
} else {
for (var i = 0; i < _n; i++) {
for (var t = 0; t < _nShingles; t++) {
if (_wave[i][t]) {
_observed[i] = t;
break;
}
}
}
return true;
}
}
return true;
}
List<T>? extract() {
var partial = extractPartial();
List<T> out = [];
for (var i in partial) {
if (i == null) {
return null;
}
out.add(i);
}
return out;
}
List<T?> extractPartial() {
List<T?> result = [];
for (int i = 0; i < _n; i++) {
result.add(null);
}
for (int y = 0; y < _my; y++) {
var dy = y < _my - _order + 1 ? 0 : _order - 1;
for (int x = 0; x < _mx; x++) {
var dx = x < _mx - _order + 1 ? 0 : _order - 1;
var shingleIx = _observed[x - dx + (y - dy) * _mx];
if (shingleIx != null) {
var shingle = _template._shingles._shingleValues[shingleIx];
var content = shingle.content[dx + dy * _order];
var real = _template._embedding.decode(content);
result[x + y * _mx] = real;
}
}
}
return result;
}
int? _nextUnobservedNode(math.Random random) {
double min = 1E+10;
int? argmin;
for (var i = 0; i < _n; i++) {
if (i % _mx + _order > _mx || i ~/ _mx + _order > _my) {
continue;
}
var remainingValues = _sumsOfOnes[i];
double entropy = remainingValues.toDouble();
if (remainingValues > 1 && entropy <= min) {
double noise = 1E-6 * random.nextDouble();
if (entropy + noise < min) {
min = entropy + noise;
argmin = i;
}
}
}
return argmin;
}
void _observe(int node, math.Random random) {
var w = _wave[node];
for (var t = 0; t < _nShingles; t++) {
_distribution[t] = w[t] ? _weight(t) : 0.0;
}
int r = _chooseRandom(random, _distribution);
for (var t = 0; t < _nShingles; t++) {
if (w[t] != (t == r)) {
_ban(node, t);
}
}
}
bool _propagate() {
while (_stacksize > 0) {
int i1, t1;
(i1, t1) = _stack[_stacksize - 1];
_stacksize--;
int x1 = i1 % _mx;
int y1 = i1 ~/ _mx;
for (int d = 0; d < 4; d++) {
var x2 = x1 + _dx[d];
var y2 = y1 + _dy[d];
if (x2 < 0 || y2 < 0 || x2 + _order > _mx || y2 + _order > _my) {
continue;
}
int i2 = x2 + y2 * _mx;
var p = _template._shingles._metadata._propagators[d][t1];
var compat = _compatible[i2];
for (var t2 in p) {
var comp = compat[t2];
comp[d]--;
if (comp[d] == 0) {
_ban(i2, t2);
}
}
}
}
return _sumsOfOnes[0] > 0;
}
void _ban(int i, int t) {
_wave[i][t] = false;
var comp = _compatible[i][t];
for (var d = 0; d < 4; d++) {
comp[d] = 0;
}
_stack[_stacksize] = (i, t);
_stacksize++;
_sumsOfOnes[i] -= 1;
}
}
class WfcTemplate<T> {
final Shingles _shingles;
final Embedding<T> _embedding;
final int _order;
WfcTemplate(this._shingles, this._embedding, this._order);
static Future<WfcTemplate<T>> loadAsync<T>(
String name, int order, 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: ImageByteFormat.rawStraightRgba))!;
final sx = image.width;
final sy = image.height;
final List<T> bitmap = [];
for (var i = 0; i < sx * sy; i++) {
var pixel = bytedata.getUint32(i * 4, Endian.little);
bitmap.add(cb(pixel));
}
return loadBitmap(bitmap, sx, sy, order);
}
static WfcTemplate<T> loadBitmap<T>(
List<T> bitmap, int sx, int sy, int order) {
if (bitmap.length != sx * sy) {
throw Exception("malformed bitmap");
}
var embedding = Embedding<T>();
List<int> sample = [
for (var i = 0; i < bitmap.length; i++) embedding.encode(bitmap[i])
];
embedding.freeze();
var shingles = Shingles();
var xmax = sx - order + 1;
var ymax = sy - order + 1;
for (var y = 0; y < ymax; y++) {
for (var x = 0; x < xmax; x++) {
var ps = [
Shingle(order, embedding.c,
(dx, dy) => sample[(x + dx) % sx + (y + dy) % sy * sx])
];
ps.add(ps[0].reflect());
ps.add(ps[0].rotate());
ps.add(ps[2].reflect());
ps.add(ps[2].rotate());
ps.add(ps[4].reflect());
ps.add(ps[4].rotate());
ps.add(ps[6].reflect());
for (var p in ps) {
shingles.observe(p, 1.0);
}
}
}
shingles.freeze();
return WfcTemplate(shingles, embedding, order);
}
}
class Shingles {
bool _frozen = false;
final Map<int, int> _shingleIndices = {};
final List<Shingle> _shingleValues = [];
final List<double> _shingleWeights = [];
late ShingleMetadata _metadata;
int get n {
if (!_frozen) {
throw StateError("can't use Shingles#get n until frozen");
}
return _shingleValues.length;
}
void freeze() {
if (_frozen) {
throw StateError("can't freeze when already frozen");
}
_frozen = true;
_metadata = ShingleMetadata(this);
}
void observe(Shingle s, double n) {
if (_frozen) {
throw StateError("can't observe when already frozen");
}
// double n: weights can be fractional
var index = _shingleIndices[s.hashCode];
if (index == null) {
index = _shingleValues.length;
_shingleValues.add(s);
_shingleWeights.add(n);
} else {
_shingleWeights[index] += n;
}
}
}
class ShingleMetadata {
// [direction][source] => list of agreeing items
List<List<List<int>>> _propagators = [];
ShingleMetadata(Shingles s) {
_propagators = [
for (var d = 0; d < 4; d++)
[
for (var t = 0; t < s.n; t++)
[
for (var t2 = 0; t2 < s.n; t2++)
if (s._shingleValues[t]
.agrees(s._shingleValues[t2], _dx[d], _dy[d]))
t2
]
]
];
}
}
class Shingle {
int order;
int c;
List<int> content = [];
@override
int hashCode = 0;
Shingle(this.order, this.c, int Function(int, int) f) {
content = [
for (var y = 0; y < order; y++)
for (var x = 0; x < order; x++) f(x, y)
];
int result = 0, power = 1;
for (var i = 0; i < content.length; i++) {
result += content[content.length - 1 - i] * power;
power *= c;
}
hashCode = result;
}
Shingle rotate() {
return Shingle(order, c, (x, y) => content[order - 1 - y + x * order]);
}
Shingle reflect() {
return Shingle(order, c, (x, y) => content[order - 1 - x + y * order]);
}
bool agrees(Shingle other, int dx, int dy) {
var p1 = content;
var p2 = other.content;
var n = order;
int xmin = dx < 0 ? 0 : dx;
int xmax = dx < 0 ? dx + n : n;
int ymin = dy < 0 ? 0 : dy;
int ymax = dy < 0 ? dy + n : n;
for (var y = ymin; y < ymax; y++) {
for (var x = xmin; x < xmax; x++) {
if (p1[x + n * y] != p2[x - dx + n * (y - dy)]) {
return false;
}
}
}
return true;
}
@override
bool operator ==(Object other) {
return (other is Shingle) &&
other.hashCode == hashCode &&
other.order == order &&
other.c == c;
}
}
class Embedding<T> {
bool _frozen = false;
final List<T> _colorOf = [];
final Map<T, int> _codeOf = {};
void freeze() {
if (_frozen) {
throw StateError("can't freeze when already frozen");
}
_frozen = true;
}
int get c {
if (!_frozen) {
throw StateError("can't use Embedding#get c until frozen");
}
return _colorOf.length;
}
int encode(T t) {
var code = _codeOf[t];
if (code == null) {
if (_frozen) {
throw StateError("can't create new code when frozen");
}
code = _colorOf.length;
_codeOf[t] = code;
_colorOf.add(t);
}
return code;
}
T decode(int i) {
return _colorOf[i]!;
}
}
int _chooseRandom(math.Random rand, List<double> distribution) {
if (distribution.isEmpty) {
throw Exception("can't sample empty distribution");
}
var sum = 0.0;
for (var i = 0; i < distribution.length; i++) {
sum += distribution[i];
}
if (sum == 0.0) {
return rand.nextInt(distribution.length);
}
var rnd = rand.nextDouble() * sum;
var i = 0;
while (rnd > 0) {
rnd -= distribution[i];
if (rnd < 0) {
return i;
}
i += 1;
}
return distribution.length - 1;
}
final List<int> _dx = [-1, 0, 1, 0];
final List<int> _dy = [0, 1, 0, -1];
final List<int> _opposite = [2, 3, 0, 1];

View File

@ -1,33 +1,23 @@
import 'package:dartterm/wfc/template.dart';
import 'package:dartterm/bitmap.dart';
import 'package:dartterm/algorithms/geometry.dart' as geo;
class Level {
Bitmap<LevelTile> tiles;
geo.Offset spawn;
geo.Size get size => tiles.size;
Level(this.tiles, this.spawn) {
assert(tiles.rect.containsPoint(spawn));
}
}
enum LevelTile {
exit,
door,
floor,
wall,
}
class Level {
Set<(int, int)> openCells = {};
}
Future<WfcTemplate<LevelTile>> loadLevelWfcAsync(String name) async {
return WfcTemplate.loadAsync(name, 3, (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");
}
});
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
@ -62,6 +63,7 @@ flutter:
assets:
- assets/images/fonts/
- assets/images/wfc/
- assets/images/vaults/
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg