// 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 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(); }