Files
raycaster/lib/main.dart
T
2026-09-14 11:14:22 +02:00

1128 lines
32 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import "dart:math" as math;
import "package:flutter/services.dart";
import "package:flutter/widgets.dart";
const double gameWidth = 320;
const double gameHeight = 256;
extension GameValues on double {
/// Normalizes an angle back into [0, 2π) from (-π, π].
double get normalizeAngle =>
(this % (2 * math.pi) + 2 * math.pi) % (2 * math.pi);
/// Returns the degree angle
double get radianToDegree {
final double proposed = this * 180 / math.pi;
if (proposed > 360) {
throw Exception(
"Proposed angle is $proposed, which is larger than 360. Check input: $this",
);
}
if (proposed.isNegative) {
throw Exception(
"Proposed angle is $proposed, which is negative. Check input: $this",
);
}
return proposed;
}
// Calculate the direction we're facing using the hypotenuse of the angle.
// This means the x is positive when we're facing "east" and negative when facing "west"
// and y is positive when facting "north" while negative when facing "south".
// Flutter's coordinate system flips the y-axis, which is why we have -sin() -- to flip
// it back in the correct direction.
Offset get direction => Offset(
math.cos(normalizeAngle),
math.sin(normalizeAngle),
);
/// Get the delta from the given angle
Offset get playerDelta => Offset(
direction.dx * Player.speed,
direction.dy * Player.speed,
);
}
extension WorldValues on Offset {
// The current "tile" we're in on the map
Offset get currentTile => Offset(
(dx / GameMap.size).floorToDouble(),
(dy / GameMap.size).floorToDouble(),
);
// The index of the current "tile" we're in
// index = row * stride + column
int get index =>
(currentTile.dy.toInt() * GameMap.dimensions.width.toInt()) +
currentTile.dx.toInt();
}
void main() {
runApp(const MainApp());
}
class MainApp extends StatefulWidget {
const MainApp({super.key});
@override
State<MainApp> createState() => _MainAppState();
}
class _MainAppState extends State<MainApp> {
RayData rayData = const RayData(
playerAngle: 0,
playerPosition: Offset(1.5 * GameMap.size, 1.5 * GameMap.size),
rays: [],
);
@override
Widget build(BuildContext context) {
return Directionality(
textDirection: TextDirection.ltr,
child: LayoutBuilder(
builder: (context, constraints) => Stack(
textDirection: TextDirection.ltr,
children: [
Stack(
children: [
Background(rayData),
Walls(rayData),
],
),
Opacity(
opacity: 0.5,
child: SizedBox.square(
dimension: GameMap.size * GameMap.dimensions.width / 2,
child: Stack(
children: [
Map(rayData),
Player(
rayData: rayData,
onAngleUpdated: (radians) {
// degrees = radians × (180 / π)
setState(() {
rayData = rayData.copyWith(
playerAngle: radians,
);
});
},
onPositionUpdated: (position) {
setState(() {
rayData = rayData.copyWith(
playerPosition: position,
);
});
},
onRaysUpdated: (rays) {
setState(() {
rayData = rayData.copyWith(
rays: rays,
);
});
},
),
],
),
),
),
],
),
),
);
}
}
// * Player
class Player extends StatefulWidget {
const new({
required this.onRaysUpdated,
required this.onAngleUpdated,
required this.onPositionUpdated,
required this.rayData,
super.key,
});
final ValueChanged<List<Ray>> onRaysUpdated;
final ValueChanged<double> onAngleUpdated;
final ValueChanged<Offset> onPositionUpdated;
final RayData rayData;
/// How "fast" the player moves
static const int speed = 5;
@override
State<Player> createState() => _PlayerState();
}
class _PlayerState extends State<Player> {
@override
void initState() {
super.initState();
HardwareKeyboard.instance.addHandler(keyPressHandler);
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.onRaysUpdated(
calculateRays(
widget.rayData.playerPosition,
widget.rayData.playerAngle,
),
);
widget.onAngleUpdated(
widget.rayData.playerAngle,
);
widget.onPositionUpdated(
widget.rayData.playerPosition,
);
});
}
@override
void dispose() {
super.dispose();
HardwareKeyboard.instance.removeHandler(keyPressHandler);
}
/// Get the map tile at a given pixel coordinate
int tileAt(Offset position) {
final int col = (position.dx / GameMap.size).floor();
final int row = (position.dy / GameMap.size).floor();
if (col < 0 ||
col >= GameMap.dimensions.width ||
row < 0 ||
row >= GameMap.dimensions.height)
return 1; // solid
return GameMap.map[row * GameMap.dimensions.width.toInt() + col];
}
/// How far until we hit a wall in the movement direction?
double safeDistanceToWall(Offset position, Offset direction) {
final List<double> choices = [
RayHit.castRay(
position,
direction.dx,
).distance,
RayHit.castRay(
position,
direction.direction.normalizeAngle - 0.05,
).distance,
RayHit.castRay(
position,
direction.direction.normalizeAngle + 0.05,
).distance,
];
choices.sort();
return choices.first;
}
bool keyPressHandler(KeyEvent event) {
final Set<LogicalKeyboardKey> keys =
HardwareKeyboard.instance.logicalKeysPressed;
bool handled = false;
// Where is the player now?
Offset playerPosition = widget.rayData.playerPosition;
final Offset playerCenter =
playerPosition +
Offset(GameMap.dimensions.width / 2, GameMap.dimensions.height / 2);
double playerAngle = widget.rayData.playerAngle;
// How many pixels away from a wall to stay
const double buffer = 2;
if (keys.contains(LogicalKeyboardKey.keyA)) {
// Turn left
playerAngle -= 0.1;
handled = true;
} else if (keys.contains(LogicalKeyboardKey.keyD)) {
// Turn right
playerAngle += 0.1;
handled = true;
}
// Capture the direction *after* it's potentially mutated
final Offset direction = playerAngle.direction;
if (keys.contains(LogicalKeyboardKey.keyW)) {
if (safeDistanceToWall(playerCenter, direction) > Player.speed + buffer) {
// Move forward
playerPosition += playerAngle.playerDelta;
}
handled = true;
} else if (keys.contains(LogicalKeyboardKey.keyS)) {
if (safeDistanceToWall(playerCenter, -direction) >
Player.speed + buffer) {
// Move backwwards
playerPosition -= playerAngle.playerDelta;
}
handled = true;
}
if (handled) {
widget.onRaysUpdated(
calculateRays(playerPosition, playerAngle),
);
widget.onAngleUpdated(playerAngle);
widget.onPositionUpdated(playerPosition);
return true;
}
return false;
}
List<Ray> calculateRays(Offset offset, double playerAngle) {
const double dr = math.pi / 180;
final double baseAngle = (playerAngle - dr * 30).normalizeAngle;
final List<Ray> rays = [];
final Offset playerCenter =
offset +
Offset(
GameMap.dimensions.width / 2,
GameMap.dimensions.height / 2,
);
int r;
for (r = 0; r < 60; r++) {
final double angle = (baseAngle + r * dr).normalizeAngle;
final RayHit hit = RayHit.castRay(playerCenter, angle);
rays.add(
Ray(
hit.hitPosition,
hit.distance,
hit.colorFor(hit.tileValue, hit.axis),
),
);
}
return rays;
}
double dist(double ax, double ay, double bx, double by, double ang) {
return math.sqrt((bx - ax) * (bx - ax) + (by - ay) * (by - ay));
}
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: PlayerPainter(
offset: widget.rayData.playerPosition,
delta: widget.rayData.playerAngle.playerDelta,
),
);
}
}
class PlayerPainter extends CustomPainter {
final Offset offset;
final Offset delta;
const new({
required this.offset,
required this.delta,
});
@override
void paint(Canvas canvas, Size size) {
// Player dot
canvas.drawRect(
Rect.fromLTWH(offset.dx, offset.dy, 8, 8),
Paint()..color = const Color.fromRGBO(255, 255, 0, 1),
);
// Player direction line
canvas.drawLine(
offset + const Offset(4, 4),
offset + const Offset(4, 4) + (delta * 5),
Paint()
..color = const Color.fromRGBO(255, 255, 0, 1)
..strokeWidth = 2,
);
}
@override
bool shouldRepaint(covariant oldDelegate) => false;
@override
bool shouldRebuildSemantics(covariant oldDelegate) => false;
}
// * Walls
class Walls extends StatelessWidget {
const new(this.rayData, {super.key});
final RayData rayData;
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) => ClipRect(
child: CustomPaint(
size: constraints.biggest,
painter: WallPainter(
playerAngle: rayData.playerAngle,
rayData: rayData.rays,
),
),
),
);
}
}
class WallPainter extends CustomPainter {
final double playerAngle;
final List<Ray> rayData;
const new({
required this.playerAngle,
required this.rayData,
});
@override
void paint(Canvas canvas, Size size) {
final double scale = math.max(
size.width / gameWidth,
size.height / gameHeight,
);
final double offsetX = (size.width - gameWidth * scale) / 2;
final double offsetY = (size.height - gameHeight * scale) / 2;
canvas
..save()
..translate(offsetX, offsetY)
..scale(scale);
for (int i = 0; i < rayData.length; i++) {
final Ray ray = rayData[i];
// * --- Draw 3D Walls ---
// Correct the fish-eye effect
const double dr = math.pi / 180;
final double rayAngle = playerAngle - (dr * 30) + (i * dr);
final double correctedAngle = (playerAngle - rayAngle).normalizeAngle;
final double disT = ray.distance * math.cos(correctedAngle);
double lineHeight = (GameMap.size * gameHeight) / disT;
if (lineHeight > gameHeight) {
lineHeight = gameHeight;
}
if (lineHeight > gameWidth) {
lineHeight = gameWidth;
}
final double lineOffset = (gameHeight / 2.0) - (lineHeight / 2.0);
final double rayWidth = gameWidth / rayData.length;
final double rayX3D = i * rayWidth;
canvas.drawRect(
Rect.fromLTWH(rayX3D, lineOffset, rayWidth, lineHeight),
Paint()
..color = ray.paintColor
..isAntiAlias = false
..strokeWidth = 1.5,
);
}
canvas.restore();
}
@override
bool shouldRepaint(covariant oldDelegate) => true;
@override
bool shouldRebuildSemantics(covariant oldDelegate) => true;
}
// * Map
class Map extends StatelessWidget {
const new(
this.rayData, {
super.key,
});
final RayData rayData;
@override
Widget build(BuildContext context) {
return Stack(
children: [
CustomPaint(
size: GameMap.dimensions,
painter: MapPainter(
mapData: GameMap.map,
mapX: GameMap.dimensions.width,
mapY: GameMap.dimensions.height,
tileSize: GameMap.size / 1,
),
),
CustomPaint(
painter: MapRayPainter(
rayData: rayData,
),
),
],
);
}
}
class MapPainter extends CustomPainter {
final List<int> mapData;
final double mapX;
final double mapY;
final double tileSize;
const new({
required this.mapData,
required this.mapX,
required this.mapY,
required this.tileSize,
});
@override
void paint(Canvas canvas, Size size) {
final paintWhite = Paint()..color = const Color.fromRGBO(255, 255, 255, 1);
final paintBlack = Paint()..color = const Color.fromRGBO(0, 0, 0, 1);
final paintGrey = Paint()..color = const Color.fromRGBO(84, 84, 84, 1);
canvas.drawRect(
Rect.fromLTWH(0, 0, tileSize * mapX, tileSize * mapY),
paintGrey,
);
for (int i = 0; i < mapData.length; i++) {
final double x = (i % mapX) * tileSize;
final double y = (i ~/ mapX) * tileSize;
canvas.drawRect(
Rect.fromLTWH(x, y, tileSize - 1, tileSize - 1),
mapData[i] == 0 ? paintBlack : paintWhite,
);
}
}
@override
bool shouldRepaint(covariant oldDelegate) => true;
@override
bool shouldRebuildSemantics(covariant oldDelegate) => true;
}
class MapRayPainter extends CustomPainter {
final RayData rayData;
const new({
required this.rayData,
});
@override
void paint(Canvas canvas, Size size) {
final Offset playerCenter = rayData.playerPosition + const Offset(4, 4);
for (final Ray ray in rayData.rays) {
canvas.drawLine(
playerCenter,
ray.hitPosition - const Offset(1, 1),
Paint()
..color = const Color.fromRGBO(0, 255, 255, 1)
..strokeWidth = 1,
);
}
}
@override
bool shouldRepaint(covariant oldDelegate) => true;
@override
bool shouldRebuildSemantics(covariant oldDelegate) => true;
}
// * Background
class Background extends StatelessWidget {
const new(
this.rayData, {
super.key,
});
final RayData rayData;
@override
Widget build(BuildContext context) {
return Stack(
textDirection: TextDirection.ltr,
children: [
Ceiling(rayData),
Floor(rayData),
],
);
}
}
class Ceiling extends StatelessWidget {
const new(this.rayData, {super.key});
final RayData rayData;
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) => CustomPaint(
size: constraints.biggest,
painter: CeilingPainter(),
),
);
}
}
class CeilingPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final Rect rect = Offset.zero & size;
final double halfHeight = rect.height / 2;
final Rect topHalf = Rect.fromLTWH(
rect.left,
rect.top,
rect.width,
halfHeight,
);
const LinearGradient gradient = LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color.fromRGBO(32, 32, 32, 1.0),
Color.fromRGBO(96, 96, 96, 1.0),
],
stops: [0.0, 1.0],
);
canvas.drawRect(
topHalf,
Paint()..shader = gradient.createShader(topHalf),
);
}
@override
bool shouldRepaint(covariant oldDelegate) => false;
@override
bool shouldRebuildSemantics(covariant oldDelegate) => false;
}
class Floor extends StatelessWidget {
const new(this.rayData, {super.key});
final RayData rayData;
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) => CustomPaint(
size: constraints.biggest,
painter: FloorPainter(),
),
);
}
}
class FloorPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final Rect rect = Offset.zero & size;
final double halfHeight = rect.height / 2;
final Rect bottomHalf = Rect.fromLTWH(
rect.left,
halfHeight,
rect.width,
rect.bottom,
);
const LinearGradient gradient = LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color.fromRGBO(32, 32, 0, 1.0),
Color.fromRGBO(96, 96, 0, 1.0),
],
stops: [0.0, 1.0],
);
canvas.drawRect(
bottomHalf,
Paint()..shader = gradient.createShader(bottomHalf),
);
}
@override
bool shouldRepaint(covariant oldDelegate) => false;
@override
bool shouldRebuildSemantics(covariant oldDelegate) => false;
}
// * Rays
class Ray(
/// Where it hit on the 2D map
var Offset hitPosition,
/// Corrected 3D distance
var double distance,
/// Wall color
var Color paintColor,
);
class RayData {
/// The direction the player is facing
final double playerAngle;
/// The player's position
final Offset playerPosition;
/// The rays cast from the player
final List<Ray> rays;
const RayData({
required this.playerAngle,
required this.playerPosition,
required this.rays,
});
RayData copyWith({
double? playerAngle,
Offset? playerPosition,
List<Ray>? rays,
}) {
return RayData(
playerAngle: playerAngle ?? this.playerAngle,
playerPosition: playerPosition ?? this.playerPosition,
rays: rays ?? this.rays,
);
}
}
class RayHit(
/// Exact boundary crossing where the ray stopped
var Offset hitPosition,
/// Euclidean, from ray origin
var double distance,
/// 0 means "no hit within map"
var int tileValue,
/// [Axis] of the corresponding ray hit
var Axis axis,
) {
factory castRay(Offset origin, double angle) {
// 1. run horizontal-axis DDA → hit position + distance (or none)
// 2. run vertical-axis DDA → hit position + distance (or none)
// 3. return the nearer one
final RayHit hitHorizontal = RayHit._marchHorizontal(origin, angle);
final RayHit hitVertical = RayHit._marchVertical(origin, angle);
if (hitVertical.distance < hitHorizontal.distance) {
return hitVertical;
}
return hitHorizontal;
}
factory _marchHorizontal(Offset origin, double angle) {
// * --- Check Horizontal Lines ---
int dof = 0;
double distanceHor = double.infinity;
Offset hit = origin;
// Negative inverse of tangent
final double aTan = -1.0 / math.tan(angle);
double rayY = 0;
double rayX = 0;
double yOffset = 0;
double xOffset = 0;
// ? Looking up (ray angle > π)
if (angle > math.pi) {
rayY =
((origin.dy / GameMap.size).floorToDouble() * GameMap.size) - 0.0001;
rayX = (origin.dy - rayY) * aTan + origin.dx;
yOffset = -GameMap.size / 1;
xOffset = -yOffset * aTan;
}
// ? Looking down
if (angle < math.pi) {
rayY =
((origin.dy / GameMap.size).floorToDouble() * GameMap.size) +
GameMap.size;
rayX = (origin.dy - rayY) * aTan + origin.dx;
yOffset = GameMap.size / 1;
xOffset = -yOffset * aTan;
}
int mapPointX = 0;
int mapPointY = 0;
int mapPoint = 0;
int mapHit = 0;
while (dof < 8) {
mapPointX = rayX ~/ GameMap.size;
mapPointY = rayY ~/ GameMap.size;
mapPoint = mapPointY * GameMap.dimensions.width.toInt() + mapPointX;
if (mapPoint > 0 &&
mapPoint <
GameMap.dimensions.width.toInt() *
GameMap.dimensions.height.toInt() &&
GameMap.map[mapPoint] > 0) {
// We hit a wall!
mapHit = GameMap.map[mapPoint];
hit = Offset(rayX, rayY);
distanceHor = dist(
origin,
hit,
angle,
);
dof = 8;
} else {
rayX += xOffset;
rayY += yOffset;
dof += 1;
}
}
return RayHit(hit, distanceHor, mapHit, Axis.horizontal);
}
factory _marchVertical(Offset origin, double angle) {
// * --- Check Vertical Lines ---
int dof = 0;
double distanceVert = double.infinity;
Offset hit = origin;
// Negative of tangent
final double nTan = -math.tan(angle);
const double pi2 = math.pi / 2;
const double pi3 = 3 * math.pi / 2;
double rayX = 0;
double rayY = 0;
double xOffset = 0;
double yOffset = 0;
// ? Looking left
if (angle > pi2 && angle < pi3) {
rayX =
((origin.dx / GameMap.size).floorToDouble() * GameMap.size) - 0.0001;
rayY = (origin.dx - rayX) * nTan + origin.dy;
xOffset = -GameMap.size / 1;
yOffset = -xOffset * nTan;
}
// ? Looking right
if (angle < pi2 || angle > pi3) {
rayX =
((origin.dx / GameMap.size).floorToDouble() * GameMap.size) +
GameMap.size;
rayY = (origin.dx - rayX) * nTan + origin.dy;
xOffset = GameMap.size / 1;
yOffset = -xOffset * nTan;
}
// ? Looking straight up or down
if (angle == pi2 || angle == pi3) {
rayX = origin.dx;
rayY = origin.dy;
dof = 8;
}
int mapPointX = 0;
int mapPointY = 0;
int mapPoint = 0;
int mapHit = 0;
while (dof < 8) {
mapPointX = rayX ~/ GameMap.size;
mapPointY = rayY ~/ GameMap.size;
mapPoint = mapPointY * GameMap.dimensions.width.toInt() + mapPointX;
if (mapPoint > 0 &&
mapPoint <
GameMap.dimensions.width.toInt() *
GameMap.dimensions.height.toInt() &&
GameMap.map[mapPoint] > 0) {
// We hit a wall!
mapHit = GameMap.map[mapPoint];
// Save the values and calculate the distance from the player
hit = Offset(rayX, rayY);
distanceVert = dist(
origin,
hit,
angle,
);
// Break the loop - no need to calculate more.
dof = 8;
} else {
// If we didn't hit a wall, we check the next horizontal line by adding
// the x and y offset.
rayX += xOffset;
rayY += yOffset;
dof += 1;
}
}
return RayHit(hit, distanceVert, mapHit, Axis.vertical);
}
static double dist(Offset a, Offset b, double ang) {
return math.sqrt(
(b.dx - a.dx) * (b.dx - a.dx) + (b.dy - a.dy) * (b.dy - a.dy),
);
}
Color colorFor(int tileValue, Axis axis) {
Color wallColor = const Color.fromRGBO(0, 0, 0, 0);
if (axis == Axis.vertical) {
// Vertical walls are drawn a little brighter than horizontal walls
wallColor = const Color.fromRGBO(230, 0, 0, 1);
if (tileValue == 2) {
wallColor = const Color.fromRGBO(0, 179, 0, 1);
}
}
if (axis == Axis.horizontal) {
// Horizontal walls are drawn a little darker than vertical walls
wallColor = const Color.fromRGBO(179, 0, 0, 1);
if (tileValue == 2) {
wallColor = const Color.fromRGBO(0, 102, 0, 1);
}
}
return wallColor;
}
}
// * Map
abstract class GameMap {
// dart format off
static const List<int> map = [
1,1,1,1,1,1,1,1,
1,0,0,0,0,0,0,1,
1,0,1,0,0,0,0,1,
1,0,1,0,0,0,0,1,
1,0,0,0,0,0,0,1,
1,0,0,0,0,2,0,1,
1,0,0,0,0,0,0,1,
1,1,1,1,1,1,1,1,
];
// dart format on
// dart format off
static const List<int> floor = [
0,0,0,0,0,0,0,0,
0,0,0,0,1,1,0,0,
0,0,0,0,2,0,0,0,
0,0,0,0,0,0,0,0,
0,0,2,0,0,0,0,0,
0,0,0,0,0,0,0,0,
0,1,1,1,1,0,0,0,
0,0,0,0,0,0,0,0,
];
// dart format on
// dart format off
static const List<int> ceiling = [
0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,
0,0,0,0,0,0,1,0,
0,1,3,1,0,0,0,0,
0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,
];
// dart format on
static const Size dimensions = Size(8, 8);
static const int size = 32;
}
abstract class AllTextures {
// dart format off
static const List<int> map = [
//Checkerboard
0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1,
0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1,
0,0,0,0,0,1,1,0, 1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1,
0,0,1,1,1,1,1,0, 1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1,
0,0,1,1,1,1,1,0, 1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1,
0,0,0,0,0,1,1,0, 1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1,
0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1,
0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0,
1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0,
1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0,
1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0,
1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0,
1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0,
1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0,
1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1,
0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1,
0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1,
0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1,
0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1,
0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1,
0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1,
0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0,
1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0,
1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0,
1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0,
1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0,
1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0,
1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0,
1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0,
//Brick
0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,
1,1,1,1,1,1,1,0, 0,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,0, 0,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,0, 0,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,0, 0,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,0, 0,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,0, 0,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,0, 0,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,0, 0,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,0, 0,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,0, 0,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,0, 0,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,0, 0,1,1,1,1,1,1,1,
0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,
0,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,0, 0,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,0,
0,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,0, 0,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,0,
0,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,0, 0,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,0,
0,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,0, 0,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,0,
0,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,0, 0,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,0,
0,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,0, 0,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,0,
0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,
1,1,1,1,1,1,1,0, 0,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,0, 0,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,0, 0,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,0, 0,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,0, 0,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,0, 0,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,0, 0,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,0, 0,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,0, 0,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,0, 0,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,0, 0,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,0, 0,1,1,1,1,1,1,1,
0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,
0,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,0, 0,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,0,
0,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,0, 0,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,0,
0,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,0, 0,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,0,
0,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,0, 0,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,0,
0,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,0, 0,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,0,
0,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,0, 0,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,0,
0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,
//Window
1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,
1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1,
1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1,
1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1,
1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1,
1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1,
1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1,
1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1,
1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1,
1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1,
1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1,
1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1,
1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1,
1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1,
1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1,
1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,
1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1,
1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1,
1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1,
1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1,
1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1,
1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1,
1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1,
1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1,
1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1,
1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1,
1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1,
1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1,
1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1,
1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1,
1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,
//Door
0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,
0,0,0,1,1,1,1,1, 0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0, 1,1,1,1,1,0,0,0,
0,0,0,1,0,0,0,1, 0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0, 1,0,0,0,1,0,0,0,
0,0,0,1,0,0,0,1, 0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0, 1,0,0,0,1,0,0,0,
0,0,0,1,0,0,0,1, 0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0, 1,0,0,0,1,0,0,0,
0,0,0,1,0,0,0,1, 0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0, 1,0,0,0,1,0,0,0,
0,0,0,1,0,0,0,1, 0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0, 1,0,0,0,1,0,0,0,
0,0,0,1,0,0,0,1, 0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0, 1,0,0,0,1,0,0,0,
0,0,0,1,0,0,0,1, 0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0, 1,0,0,0,1,0,0,0,
0,0,0,1,0,0,0,1, 0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0, 1,0,0,0,1,0,0,0,
0,0,0,1,0,0,0,1, 0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0, 1,0,0,0,1,0,0,0,
0,0,0,1,0,0,0,1, 0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0, 1,0,0,0,1,0,0,0,
0,0,0,1,1,1,1,1, 0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0, 1,1,1,1,1,0,0,0,
0,0,0,0,0,0,0,0, 0,0,0,0,0,1,0,1, 1,0,1,0,0,0,0,0, 0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0, 0,0,1,1,1,1,0,1, 1,0,1,1,1,1,0,0, 0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,
0,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,0,
0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,
0,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,0,
0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,
0,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,0,
0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,
0,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,0,
0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,
0,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,0
];
// dart format on
}