Basic sitemode

This commit is contained in:
2023-09-22 21:08:03 -07:00
parent 76e92a2a50
commit e3e43f0223
17 changed files with 568 additions and 15 deletions

View File

@ -0,0 +1,72 @@
import 'package:collection/collection.dart';
class Edge<T> {
final double cost;
final T destination;
Edge(this.cost, this.destination);
}
class Result<T> {
final double cost;
final T? predecessor;
final T item;
Result(this.cost, this.predecessor, this.item);
}
Iterable<Result<T>> dijkstra<T>(
T source, Iterable<Edge<T>> Function(T) neighbors) sync* {
var queue = PriorityQueue<Result<T>>((i0, i1) => i0.cost.compareTo(i1.cost));
Set<T> seen = {source};
queue.add(Result(0.0, null, source));
while (queue.isNotEmpty) {
var u = queue.removeFirst();
yield u;
for (var v in neighbors(u.item)) {
if (seen.contains(v.destination)) {
continue;
}
seen.add(v.destination);
queue.add(Result(u.cost + v.cost, u.item, v.destination));
}
}
}
List<T>? dijkstraPath<T>(
T source, T destination, Iterable<Edge<T>> Function(T) neighbors,
{double? maxCost}) {
if (source == destination) {
return [];
}
Map<T, T?> predecessor = {};
for (var r in dijkstra(source, neighbors)) {
predecessor[r.item] = r.predecessor;
if (maxCost != null && r.cost >= maxCost) {
return null;
}
if (r.item == destination) {
break;
}
}
var revPath = [destination];
while (true) {
var pred = predecessor[revPath.last];
if (pred == source) {
revPath.reverseRange(0, revPath.length);
return revPath;
} else if (pred == null) {
throw Exception(
"predecessor should not be null -- that would mean we missed source");
} else {
revPath.add(pred);
}
}
}

View File

@ -17,6 +17,13 @@ class Size {
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 {
@ -29,6 +36,13 @@ class Offset {
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 {
@ -63,4 +77,15 @@ class Rect {
String toString() {
return "@($x0, $y0) $size";
}
@override
bool operator ==(Object other) =>
other is Rect &&
other.x0 == x0 &&
other.y0 == y0 &&
other.dx == dx &&
other.dy == dy;
@override
int get hashCode => (x0, y0, dx, dy).hashCode;
}

View File

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