142 lines
3.3 KiB
Dart
142 lines
3.3 KiB
Dart
|
// 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();
|
||
|
}
|