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 createState() => _MainAppState(); } class _MainAppState extends State { 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> onRaysUpdated; final ValueChanged onAngleUpdated; final ValueChanged onPositionUpdated; final RayData rayData; /// How "fast" the player moves static const int speed = 5; @override State createState() => _PlayerState(); } class _PlayerState extends State { @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 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 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 calculateRays(Offset offset, double playerAngle) { const double dr = math.pi / 180; final double baseAngle = (playerAngle - dr * 30).normalizeAngle; final List 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 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 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 rays; const RayData({ required this.playerAngle, required this.playerPosition, required this.rays, }); RayData copyWith({ double? playerAngle, Offset? playerPosition, List? 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 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 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 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 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 }