Compare commits
20 Commits
ed57ba18ed
...
main
Author | SHA1 | Date | |
---|---|---|---|
7fc65b49c2 | |||
e3e43f0223 | |||
76e92a2a50 | |||
8273243db9 | |||
1b4240e430 | |||
d489810154 | |||
354a114e1c | |||
01e316d979 | |||
9ad13f7a5a | |||
ae0c62b010 | |||
5de409515e | |||
757cca1392 | |||
c4c2b26653 | |||
b5466919e6 | |||
0a600fd930 | |||
b91f3097f2 | |||
04b580b22c | |||
508fd28f04 | |||
5d941afe5e | |||
dd92246402 |
15
LICENSE.md
Normal file
15
LICENSE.md
Normal 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 |
BIN
assets/images/vaults/house1.png
Normal file
BIN
assets/images/vaults/house1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.4 KiB |
BIN
assets/images/vaults/warehouse_1x1.png
Normal file
BIN
assets/images/vaults/warehouse_1x1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.8 KiB |
BIN
assets/images/vaults/warehouse_2x2.png
Normal file
BIN
assets/images/vaults/warehouse_2x2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 915 B |
72
lib/algorithms/dijkstra.dart
Normal file
72
lib/algorithms/dijkstra.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
91
lib/algorithms/geometry.dart
Normal file
91
lib/algorithms/geometry.dart
Normal 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;
|
||||
}
|
33
lib/algorithms/kruskal.dart
Normal file
33
lib/algorithms/kruskal.dart
Normal 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;
|
||||
}
|
76
lib/algorithms/regionalize.dart
Normal file
76
lib/algorithms/regionalize.dart
Normal 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)];
|
||||
}
|
141
lib/algorithms/shadowcasting.dart
Normal file
141
lib/algorithms/shadowcasting.dart
Normal 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();
|
||||
}
|
78
lib/algorithms/union_find.dart
Normal file
78
lib/algorithms/union_find.dart
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
@ -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
152
lib/bitmap.dart
Normal 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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
72
lib/game/game.dart
Normal 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,
|
||||
})));
|
||||
}
|
84
lib/game/generator_test_program.dart
Normal file
84
lib/game/generator_test_program.dart
Normal 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:
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
127
lib/game/sitemode/camera.dart
Normal file
127
lib/game/sitemode/camera.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
81
lib/game/sitemode/fov.dart
Normal file
81
lib/game/sitemode/fov.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
33
lib/game/sitemode/player.dart
Normal file
33
lib/game/sitemode/player.dart
Normal 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);
|
||||
}
|
||||
}
|
65
lib/game/sitemode/sitemode.dart
Normal file
65
lib/game/sitemode/sitemode.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
34
lib/game/ui_test_program.dart
Normal file
34
lib/game/ui_test_program.dart
Normal 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
8
lib/gen/direction.dart
Normal file
@ -0,0 +1,8 @@
|
||||
part of 'generator.dart';
|
||||
|
||||
enum Direction {
|
||||
up,
|
||||
left,
|
||||
down,
|
||||
right,
|
||||
}
|
65
lib/gen/direction_set.dart
Normal file
65
lib/gen/direction_set.dart
Normal 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
467
lib/gen/generator.dart
Normal 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
33
lib/gen/orientation.dart
Normal 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
25
lib/gen/requirement.dart
Normal 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
41
lib/gen/vault.dart
Normal 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
35
lib/gen/vault_tile.dart
Normal 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
116
lib/gen/vaults.dart
Normal 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");
|
||||
}
|
||||
}
|
@ -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 }
|
||||
|
@ -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
6
lib/skreek.dart
Normal file
@ -0,0 +1,6 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
void skreek(String msg) {
|
||||
// ignore: avoid_print
|
||||
debugPrint("[skreek] $msg");
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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)) {
|
||||
|
@ -17,6 +17,10 @@ void notifyScreenDimensions(ScreenDimensions sd) {
|
||||
_terminal._notifyScreenDimensions(sd);
|
||||
}
|
||||
|
||||
Stream<Input> rawInput() {
|
||||
return _terminal.rawInput();
|
||||
}
|
||||
|
||||
void clear() {
|
||||
at(0, 0).clear();
|
||||
}
|
||||
|
@ -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 }
|
@ -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];
|
@ -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,
|
||||
}
|
||||
|
@ -34,7 +34,7 @@ packages:
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
collection:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: collection
|
||||
sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687
|
||||
|
@ -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
|
||||
|
||||
|
Reference in New Issue
Block a user