├── canvas-game ├── mazebot.png ├── mazebot-screenshot.png ├── js │ ├── display │ │ ├── status-bar.js │ │ ├── styles.js │ │ ├── avatar-path-renderer.js │ │ ├── canvas-utils.js │ │ ├── game-renderer.js │ │ ├── overlay-renderer.js │ │ └── map-renderer.js │ ├── utils │ │ ├── api-client.js │ │ ├── pathfinder.js │ │ ├── coordinates.js │ │ └── polyfills.js │ ├── index.js │ └── game │ │ ├── maze-input.js │ │ └── maze-game.js ├── index.css ├── README.md └── index.html ├── entries └── chapmanj │ ├── cert.txt │ ├── README.md │ └── mazebot-chapmanj-secret.ipynb ├── mazebot.rb ├── README.md └── API.md /canvas-game/mazebot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noops-challenge/mazebot/HEAD/canvas-game/mazebot.png -------------------------------------------------------------------------------- /canvas-game/mazebot-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noops-challenge/mazebot/HEAD/canvas-game/mazebot-screenshot.png -------------------------------------------------------------------------------- /entries/chapmanj/cert.txt: -------------------------------------------------------------------------------- 1 | {'result': 'finished', 'message': 'Congratulations chapmanj! You completed the challenge in 579.352 seconds. I am happy to present you with the attached certificate.', 'certificate': '/mazebot/race/certificate/UMmvQ9yLbiPgPy8-4ameLEETARoqlEA-PIXlKiGBNan8PGqZ1PVizccBhktdMQ2g'} -------------------------------------------------------------------------------- /canvas-game/js/display/status-bar.js: -------------------------------------------------------------------------------- 1 | // bottom of the game -- status bar 2 | function StatusBar() { 3 | 4 | return { 5 | setMaze: setMaze, 6 | setPosition: setPosition, 7 | setMoveCount: setMoveCount 8 | } 9 | 10 | function setMaze(maze) { 11 | document.getElementById('current-maze').innerText = maze.name; 12 | setMoveCount(0); 13 | setPosition(maze.startingPosition); 14 | } 15 | 16 | function setMoveCount(c) { 17 | document.getElementById('move-count').innerText = c; 18 | } 19 | 20 | function setPosition(position) { 21 | document.getElementById('current-position').innerText = position[0] + ', ' + position[1]; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /canvas-game/js/display/styles.js: -------------------------------------------------------------------------------- 1 | // fills used throughout the renderer 2 | // if only there was an api that generated random colors, 3 | // this would be a good place to use it 4 | var Styles = { 5 | mapBackground: '#222222', 6 | wallLayer1: "#111111", 7 | wallLayer2: "white", 8 | corner: "white", 9 | highlight: "#BBAA44", 10 | shadow: "#445566", 11 | avatarPath: "#FFFF00", 12 | tileOutline: [ 13 | '#555555', 14 | '#333333', 15 | ], 16 | endcapFill: [ 17 | 'rgb(36,154,53)', 18 | 'rgb(251, 12, 43)', 19 | 'rgb(56, 109, 188)', 20 | ], 21 | goalDirection: [ 22 | '#13F6D2', 23 | '#43C610', 24 | '#54D632', 25 | '#23CF22' 26 | ] 27 | }; 28 | -------------------------------------------------------------------------------- /canvas-game/js/utils/api-client.js: -------------------------------------------------------------------------------- 1 | var API_ROOT = 'https://api.noopschallenge.com'; 2 | 3 | function getJson(path) { 4 | return fetch(API_ROOT + path, { 5 | method: 'GET', 6 | headers: { 7 | 'Cache-Control': 'no-cache', 8 | 'Pragma': 'no-cache', 9 | 'Expires': 'Sat, 01 Jan 2000 00:00:00 GMT' 10 | } 11 | }).then( 12 | function(r) { 13 | if (!r.ok) throw new Error('Error fetching maze from' + path + '.'); 14 | return r.json(); 15 | } 16 | ); 17 | } 18 | 19 | function postJson(path, body) { 20 | return fetch(API_ROOT + path, { 21 | headers: { 22 | 'content-type': 'application/json' 23 | }, 24 | method: 'POST', 25 | body: JSON.stringify(body) 26 | }).then( 27 | function(r) { return r.json(); } 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /canvas-game/js/display/avatar-path-renderer.js: -------------------------------------------------------------------------------- 1 | // Renders the avatar's path through the maze 2 | function AvatarPathRenderer( 3 | avatarPathContext, 4 | coordinates 5 | ) { 6 | 7 | var positions = []; 8 | 9 | return { 10 | clearAvatarPath: clearAvatarPath, 11 | addToAvatarPath: addToAvatarPath, 12 | redraw: redraw 13 | }; 14 | 15 | // redraw the whole path. 16 | // this happens when display size changes or viewport scrolls 17 | function redraw() { 18 | CanvasUtils.resetCanvas(avatarPathContext, "rgba(0,0,0,0)"); 19 | for(var i = 0; i < positions.length - 1; i++) { 20 | renderSegment(positions[i], positions[i + 1]); 21 | } 22 | } 23 | 24 | function clearAvatarPath() { 25 | positions = []; 26 | CanvasUtils.resetCanvas(avatarPathContext, "rgba(0,0,0,0)"); 27 | } 28 | 29 | // add a new segment. We don't redraw the whole path, just add to the avatar path canvas. 30 | function addToAvatarPath(from, to) { 31 | if (positions.length === 0) positions = [from]; 32 | positions.push(to); 33 | 34 | renderSegment(from, to); 35 | } 36 | 37 | function renderSegment(from, to) { 38 | var f = coordinates.canvasPosition(from); 39 | var t = coordinates.canvasPosition(to); 40 | // only if we can see it! 41 | if (f.offscreen && t.offscreen) return; 42 | 43 | var dashBasis = Math.ceil(f.size / 20); 44 | 45 | CanvasUtils.drawPath(avatarPathContext, f.size / 8, Styles.avatarPath, [f.center, t.center], { dash: [dashBasis * 5, dashBasis * 5] }); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /canvas-game/js/display/canvas-utils.js: -------------------------------------------------------------------------------- 1 | var CanvasUtils = { 2 | resetCanvas: function (ctx, color) { 3 | ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); 4 | ctx.fillStyle = color; 5 | ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height); 6 | }, 7 | drawPath: function (ctx, width, style, coords, opts) { 8 | opts = opts || {}; 9 | 10 | ctx.strokeStyle = style; 11 | ctx.lineWidth = Math.max(Math.round(width), 1); 12 | ctx.lineCap = 'round'; 13 | 14 | ctx.setLineDash(opts.dash || []); 15 | ctx.beginPath(); 16 | ctx.moveTo(Math.round(coords[0][0]), Math.round(coords[0][1])); 17 | 18 | for (var i = 1; i < coords.length; i++) { 19 | ctx.lineTo(Math.round(coords[i][0]), Math.round(coords[i][1])); 20 | } 21 | ctx.stroke(); 22 | if (opts.fill) { 23 | ctx.fillStyle = opts.fill; 24 | ctx.fill(); 25 | } 26 | }, 27 | drawFilledCircle: function (ctx, center, radius, lineWidth, stroke, fill) { 28 | ctx.lineWidth = lineWidth; 29 | ctx.fillStyle = fill; 30 | ctx.strokeStyle = stroke; 31 | ctx.beginPath(); 32 | ctx.arc(Math.round(center[0]), Math.round(center[1]), radius, 0, 2 * Math.PI, false); 33 | ctx.stroke(); 34 | ctx.fill(); 35 | }, 36 | drawArc: function (ctx, center, radius, from, to, lineWidth, stroke) { 37 | ctx.lineWidth = lineWidth; 38 | ctx.strokeStyle = stroke; 39 | ctx.beginPath(); 40 | ctx.arc(Math.round(center[0]), Math.round(center[1]), radius, from, to, false); 41 | ctx.stroke(); 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /canvas-game/js/utils/pathfinder.js: -------------------------------------------------------------------------------- 1 | // Pathfinder takes a map and finds a path from one point to another, up to 2 | // the provided max length. 3 | // This lets us support navigating by clicking on map squares. 4 | 5 | function Pathfinder() { 6 | var map, maxLength = 1; 7 | 8 | return { 9 | setMap: setMap, 10 | setMaxLength: setMaxLength, 11 | findPath: findPath 12 | } 13 | 14 | function setMap(m) { 15 | map = m; 16 | } 17 | 18 | function setMaxLength(value) { 19 | maxLength = value; 20 | } 21 | 22 | function findPath(from, to) { 23 | return _findPath(from, to, []) 24 | } 25 | 26 | // recursively search all cardinal directions until we hit a wall or the target 27 | // This is not a very smart pathfinder 28 | function _findPath(c, to, soFar) { 29 | var x = c[0]; 30 | var y = c[1]; 31 | if (((map[y] || [])[x] || 'X') === 'X') return; 32 | if (distance(c, to) === 0) return soFar; 33 | 34 | if (soFar.length < maxLength) { 35 | var paths = [ 36 | _findPath([x, y - 1], to, soFar.concat(['N'])), 37 | _findPath([x, y + 1], to, soFar.concat(['S'])), 38 | _findPath([x + 1, y], to, soFar.concat(['E'])), 39 | _findPath([x - 1, y], to, soFar.concat(['W'])) 40 | ].filter(function(p) { return !!p; }); 41 | 42 | if (paths.length) { 43 | paths.sort(shortest); 44 | return paths[0]; 45 | } 46 | } 47 | } 48 | 49 | // comparator for finding shortest path among many 50 | function shortest(a, b) { 51 | return a.length - b.length; 52 | } 53 | 54 | // minimum number of moves from a to b 55 | // (x distance + y distance) 56 | function distance(a, b) { 57 | return Math.abs(a[0] - b[0]) + Math.abs(a[1] - b[1]); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /canvas-game/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Roboto', sans-serif; 3 | font-size: 18px; 4 | } 5 | 6 | h1, h2, h3, h4 { 7 | font-family: 'Roboto Medium', sans-serif; 8 | } 9 | 10 | p { 11 | margin: 20px auto; 12 | max-width: 700px; 13 | } 14 | 15 | .mazebot-image { 16 | max-width: 40vw; 17 | max-height: 40vh; 18 | margin: 20px; 19 | } 20 | 21 | button#start, button#next-maze { 22 | font-family: 'Roboto', sans-serif; 23 | font-size: 30px; 24 | border-radius: 7px; 25 | margin-top: 20px; 26 | padding: 10px 20px; 27 | } 28 | 29 | #game { 30 | position: fixed; 31 | top: 0; 32 | right: 0; 33 | bottom: 0; 34 | left: 0; 35 | display: none; 36 | } 37 | 38 | #get-started, #results, #error { 39 | display: none; 40 | position: absolute; 41 | top: 0; 42 | left: 0; 43 | bottom: 0; 44 | right: 0; 45 | text-align: center; 46 | } 47 | 48 | .phase-starting #get-started{ 49 | display: block; 50 | } 51 | 52 | .phase-results #results { 53 | display: block; 54 | } 55 | 56 | .phase-playing #game { 57 | display: block; 58 | } 59 | 60 | .phase-error #error { 61 | display: block; 62 | } 63 | #board, #overlay, #avatar-path { 64 | position: absolute; 65 | top: 0; 66 | left: 0; 67 | } 68 | 69 | #board { 70 | z-index: 1; 71 | } 72 | #avatar-path { 73 | z-index: 1; 74 | } 75 | #overlay { 76 | z-index: 3; 77 | cursor: crosshair; 78 | } 79 | 80 | #status-bar { 81 | padding: 5px 10px 10px; 82 | z-index: 4; 83 | display: fixed; 84 | height: 30px; 85 | bottom: 0; 86 | width: 100%; 87 | 88 | background:white; 89 | display: flex; 90 | border-top: 2px solid #222222; 91 | } 92 | 93 | #status-bar div { 94 | flex: 1 1 33%; 95 | } 96 | 97 | .status-label { 98 | text-align: center; 99 | font-size: 8px; 100 | color: #666666; 101 | } 102 | 103 | .status-value { 104 | text-align: center; 105 | } 106 | -------------------------------------------------------------------------------- /mazebot.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Ruby automated mazebot racer example 3 | # 4 | # Can you write a program that finishes the race? 5 | # 6 | require "net/http" 7 | require "json" 8 | 9 | def main 10 | # get started — replace with your login 11 | start = post_json('/mazebot/race/start', { :login => 'mazebot' }) 12 | 13 | maze_path = start['nextMaze'] 14 | # get the first maze 15 | next_maze = get_json(maze_path) 16 | # Answer each question, as long as we are correct 17 | loop do 18 | 19 | # print out the map 20 | puts 21 | puts "Here's the map:" 22 | puts 23 | next_maze['map'].each { |row| puts(row.join('')) } 24 | puts 25 | 26 | puts "What are the directions from point A to point B?" 27 | 28 | # your code to figure out the answer could also go here 29 | directions = gets.delete("\n") 30 | 31 | # send to mazebot 32 | solution_result = send_solution(maze_path, directions) 33 | if solution_result['result'] == 'success' 34 | maze_path = solution_result['nextMaze'] 35 | next_maze = get_json(maze_path) 36 | end 37 | end 38 | end 39 | 40 | def send_solution(path, directions) 41 | post_json(path, { :directions => directions }) 42 | end 43 | 44 | # get data from the api and parse it into a ruby hash 45 | def get_json(path) 46 | puts "*** GET #{path}" 47 | 48 | response = Net::HTTP.get_response(build_uri(path)) 49 | result = JSON.parse(response.body) 50 | puts "HTTP #{response.code}" 51 | 52 | #puts JSON.pretty_generate(result) 53 | result 54 | end 55 | 56 | # post an answer to the noops api 57 | def post_json(path, body) 58 | uri = build_uri(path) 59 | puts "*** POST #{path}" 60 | puts JSON.pretty_generate(body) 61 | 62 | post_request = Net::HTTP::Post.new(uri, 'Content-Type' => 'application/json') 63 | post_request.body = JSON.generate(body) 64 | 65 | response = Net::HTTP.start(uri.hostname, uri.port, :use_ssl => true) do |http| 66 | http.request(post_request) 67 | end 68 | 69 | puts "HTTP #{response.code}" 70 | result = JSON.parse(response.body) 71 | puts result[:result] 72 | result 73 | end 74 | 75 | def build_uri(path) 76 | URI.parse("https://api.noopschallenge.com" + path) 77 | end 78 | 79 | main() 80 | -------------------------------------------------------------------------------- /canvas-game/js/display/game-renderer.js: -------------------------------------------------------------------------------- 1 | // Top-level renderer 2 | // Sets up the renderers for each layer 3 | var Renderer = function ( 4 | coordinates, 5 | mapCanvas, 6 | avatarPathCanvas, 7 | overlayCanvas 8 | ) { 9 | 10 | // There are 3 layers, listed here from bottom to top. 11 | // Each layer is sized to the entire viewport size 12 | 13 | // The map: floor and walls 14 | // Only updated when a maze starts or the viewport changes 15 | // (scroll, or zoom) 16 | var mapContext = mapCanvas.getContext('2d', {alpha: false}); 17 | 18 | var mapRenderer = MapRenderer( 19 | mapContext, 20 | coordinates 21 | ); 22 | 23 | // Avatar path: transparent and only updated when the path changes 24 | // or the viewport changes 25 | var avatarPathContext = avatarPathCanvas.getContext('2d'); 26 | 27 | var avatarPathRenderer = AvatarPathRenderer( 28 | avatarPathContext, 29 | coordinates 30 | ); 31 | 32 | 33 | // Overlay with the avatar and goal 34 | // This is continuously animated content, and is updated every frame 35 | var overlayContext = overlayCanvas.getContext('2d'); 36 | var overlayRenderer = OverlayRenderer( 37 | overlayContext, 38 | coordinates 39 | ); 40 | 41 | // New map: configure all the renderers 42 | function setMap(map) { 43 | CanvasUtils.resetCanvas(mapContext); 44 | mapRenderer.setMap(map); 45 | avatarPathRenderer.clearAvatarPath(); 46 | displaySizeChanged(); 47 | } 48 | 49 | // Resize all the canvases to the new size and tell the renderers to do their thing 50 | function displaySizeChanged() { 51 | var displaySize = coordinates.displaySize(); 52 | 53 | mapCanvas.width = displaySize.width; 54 | mapCanvas.height = displaySize.height; 55 | 56 | avatarPathCanvas.width = displaySize.width; 57 | avatarPathCanvas.height = displaySize.height; 58 | 59 | overlayCanvas.width = displaySize.width; 60 | overlayCanvas.height = displaySize.height; 61 | 62 | // overlay is updated continuously so doesn't need to be here 63 | mapRenderer.render(); 64 | avatarPathRenderer.redraw(); 65 | } 66 | 67 | return { 68 | // called when a maze starts 69 | setMap: setMap, 70 | // called in render loop 71 | updateOverlay: overlayRenderer.updateOverlay, 72 | displaySizeChanged: displaySizeChanged, 73 | // player moved 74 | addToAvatarPath: avatarPathRenderer.addToAvatarPath 75 | }; 76 | } 77 | -------------------------------------------------------------------------------- /canvas-game/README.md: -------------------------------------------------------------------------------- 1 | # 🎮 Mazebot game starter 2 | 3 | Here's an example HTML game that uses the Mazebot API. that renders a random maze and allows the player to navigate around it to find the solution. 4 | 5 | 6 | Check out the [live demo](https://noops-challenge.github.io/mazebot/canvas-game) or clone this repository to your computer and open `index.html` in your browser to play locally. 7 | 8 | This game is written without libraries and no build tools -- just JavaScript, HTML, and CSS. If you want to change it, just edit the files and refresh your browser. 9 | 10 | It uses the [HTML canvas API](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API) to render the game. 11 | 12 | #### Controls: 13 | 14 | Use the arrow keys, mouse or touch to navigate the maze. 15 | Plus key (+) or double-click/tap to zoom in/out. 16 | 17 | # ✨ A few ideas 18 | 19 | - **Change the difficulty:** Right now, the size of the maze can be controlled by setting the `minWidth` and `maxWidth` query parameters, which are [passed along to the Mazebot API](./js/index.js#L37). You could make the difficulty increase as the game progresses, or let the player select their difficulty when the game starts. 20 | 21 | - **Change the graphics:** Check out the [display rendering code](./js/display) to get some ideas. You could start by changing [the colors](./js/display/styles.js)... or maybe you'd like a different [avatar](./js/display/overlay-renderer.js) -- try making a vehicle that drives or a creature that walks! You could change the [map](./js/display/map-renderer.js): try replacing the graphics with a [tileset](). You could even completely replace the renderer with your own 3D or isometeric renderer! 22 | 23 | - **Add a solver:** Perhaps you'd like to add a solver to the game that can be [triggered somehow](./js/game/maze-input#L141) by the player. 24 | 25 | - **Add a soundtrack:** Add some sound effects using the WebAudio API, or background music while you play. 26 | 27 | - **Add enemies:** Make the maze harder by adding enemies that chase the player around the maze. 28 | 29 | - **Add different controls:** You could add support for vim keys to the [keyboard handler](./js/game/maze-input), or try to add [tilt controls](https://developer.mozilla.org/en-US/docs/Web/API/Detecting_device_orientation#Processing_motion_events) for players on mobile devices. 30 | 31 | - **Animate avatar movement:** The avatar moves instantly, and can move as fast as the player can move their mouse or finger. You could try to animate the movement in the [game loop](./js/game/maze-game.js#L61) to make the game smoother. 32 | 33 | Mazebot looks forward to seeing what you can do! 34 | -------------------------------------------------------------------------------- /canvas-game/js/index.js: -------------------------------------------------------------------------------- 1 | // 2 | // Entrypoint for Mazebot game example 3 | // 4 | // - Sets up the game on window load 5 | // - Manages the "phase" ("starting", "playing", or "results") of the game. 6 | // - Connects data from the api to the game (fetches new mazes and sends solutions for checking) 7 | // - Listens for window resize and tells the game to resize itself. 8 | // 9 | 10 | var statusBarContainer; 11 | var game; 12 | 13 | window.onload = function() { 14 | 15 | // Set up the maze game 16 | statusBarContainer = document.getElementById('status-bar'); 17 | 18 | // see game/maze-game.js 19 | game = MazeGame( 20 | document.getElementById('map'), 21 | document.getElementById('avatar-path'), 22 | document.getElementById('overlay'), 23 | statusBarContainer, 24 | window.innerWidth, 25 | window.innerHeight - statusBarContainer.clientHeight 26 | ); 27 | 28 | // listen for resize 29 | window.onresize = handleResize; 30 | 31 | // hook up our two buttons that start a maze (on start and after a maze is completed) 32 | document.getElementById('start').addEventListener('click', startNewMaze); 33 | document.getElementById('next-maze').addEventListener('click', startNewMaze); 34 | } 35 | 36 | function startNewMaze() { 37 | // 38 | // one fun thing to do would be to use the minSize and maxSize query parameters 39 | // here to make the game get harder over time. 40 | // 41 | // in this case, we just pass along the search query 42 | getJson('/mazebot/random' + window.location.search).then( 43 | function(r) { 44 | setPhase('playing'); 45 | game.startMaze(r, onSolution); 46 | handleResize(); 47 | }, 48 | function(err) { 49 | document.getElementById('error-message').innerText = err.message; 50 | setPhase('error'); 51 | } 52 | ); 53 | } 54 | 55 | // Setting the phase shows/hides the dom elements that are 56 | // relevant 57 | function setPhase(phase) { 58 | var el = document.getElementById('maze-browser'); 59 | el.classList.remove('phase-starting'); 60 | el.classList.remove('phase-playing'); 61 | el.classList.remove('phase-results'); 62 | el.classList.remove('phase-error'); 63 | el.classList.add('phase-' + phase); 64 | } 65 | 66 | function handleResize() { 67 | game.setDisplaySize(window.innerWidth, window.innerHeight - statusBarContainer.clientHeight); 68 | } 69 | 70 | // when a maze is solved, show the results div and await a click to continue 71 | function onSolution(result) { 72 | document.getElementById('results-message').innerText = result.message; 73 | setPhase('results'); 74 | document.getElementById('next-maze').focus(); 75 | } 76 | -------------------------------------------------------------------------------- /entries/chapmanj/README.md: -------------------------------------------------------------------------------- 1 | 2 | # 1. Maze Traveler Code 3 | 4 | ## 1.1 NOOP Challenge GET and POST functions 5 | These were based on the examples given in the API README 6 | 7 | ## 1.2 Walker Class 8 | A representation of a being in a maze with properties: 9 | 10 | ### 1.2.1 Parameters 11 | - `x`, `y `: location 12 | - `directions`: list of moves it has made (ex. `'NEWS'`) 13 | - `locations`: list of visited locations related to .directions list 14 | - `visited`: list of all visited nodes regardless of .directions or .locations 15 | 16 | ### 1.2.1 Methods 17 | A Walker has the methods: `N`, `E`, `W`, and `S` used to move the walker, changing its coordinates and storing the direction and location to their appropriate lists: 18 | 19 | A walker has a method to set its location (used when backtracking). 20 | - `set_at`: set the Walkers `x`,`y` to a node, `locations[index]`'s `x`,`y` 21 | - `clone`: generates unique instance of the walker (I used it to evaluate neighboring solutions) 22 | 23 | ## 1.3 Dungeon_master Class 24 | The solver that manipulates a Walker to determine a solution to the passed in maze/map 25 | 26 | ### 1.3.1 Parameters 27 | - `maze` The Dungeon_master stores a passed in instance of a maze 28 | 29 | ### 1.3.2 Methods 30 | - `solve` The main logic of the solution process. 31 | 32 | #### ✨Suggest Methods 33 | 34 | - `LOOK_AROUND` 35 | Determine the state of each possible move (NEWS) from current location 36 | 37 | - `MOVE_FORWARD` 38 | Use the information gained in step one, along with the Walker's `locations` and `visited` lists to determine the next move 39 | 40 | - `BACKTRACK` 41 | If the Walker has reached a terminal non-goal location, determine what point in `locations` it should return to and continue searching in a differnt direction. Ensure that the backtrack process removes unwanted directions and locations leading to terminal location. 42 | 43 | - `FEEDBACK` 44 | Provide information about the Walkers progress as it searches. 45 | 46 | ## 1.4 Helper Functions 47 | Because long lines of display are sometimes hard to manage the following functions attempt to compactly display (smaller) mazes. 48 | 49 | - `draw_maze`: displays the maze, optionally, with the walkers tour indicated by '=' 50 | 51 | #### ✨Suggest Functions 52 | I would also suggest creating functions to concisely display data generated during the solution process. (ex. string of neighboring moves, string of Walker.locations) 53 | 54 | # 2. Solve a maze 55 | 1. Get a new maze from the Mazebot or navigate to a maze's url and save the JSON to work on a single instance (like the one provided). 56 | 57 | 2. Define a Walker 58 | 59 | 3. Define a Dungeon_master with a maze 60 | 61 | 4. Solve the maze with the Dungeon_master and Walker 62 | 63 | # 3. RaceMode 64 | 1. Initiate the race 65 | 2. While `nextMaze` is returned, solve that maze 66 | 2. Win! 67 | 68 | 69 | ```python 70 | 71 | ``` 72 | -------------------------------------------------------------------------------- /canvas-game/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Mazebot 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 38 | 39 | 40 |
41 | 42 |
43 |

Welcome to Mazebot

44 | 45 |

Use your arrow keys, mouse, or touch to navigate the maze

46 |

Press + or double click to zoom in or out

47 |
48 | 49 |
50 |
51 | 52 |
53 |

RESULTS

54 | 55 |

56 |
57 | 58 |
59 |
60 | 61 |
62 |

OOPS!

63 | 64 |

Something went wrong there.

65 |

66 |

Please refresh the page.

67 |
68 | 69 |
70 | 71 | 72 | 73 |
74 |
75 |
Maze
76 |
77 |
78 |
79 |
Moves
80 |
81 |
82 |
83 |
Position
84 |
85 |
86 |
87 |
88 | 89 |
90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /canvas-game/js/game/maze-input.js: -------------------------------------------------------------------------------- 1 | // Input handling for ... 2 | // 3 | // - mouse input 4 | // - keyboard inpot 5 | // - touch input 6 | // 7 | // ... that converts user actions to *maze* coordinates. 8 | // 9 | // If you want to add support for vim or emacs keys, this is the place to do it. 10 | // 11 | function MazeInput(coordinates, overlay) { 12 | function noop() { } // ;^) 13 | 14 | var mouseOrTouchActive = false; 15 | var mousePos = []; 16 | var onPosition = noop; 17 | var onDirection = noop; 18 | var onZoom = noop; 19 | var lastTap = 0; 20 | var firstTouch; 21 | 22 | var last10 = []; 23 | 24 | document.addEventListener('gesturestart', function (e) { 25 | e.preventDefault(); 26 | }); 27 | 28 | //window.addEventListener('keydown', handleKeydown); 29 | window.addEventListener('keydown', handleKeydown); 30 | 31 | overlay.addEventListener('dblclick', handleMapDoubleClick); 32 | 33 | overlay.addEventListener('mousedown', handleMousedown); 34 | overlay.addEventListener('mouseup', handleMouseup); 35 | overlay.addEventListener('mousemove', handleMousemove); 36 | 37 | overlay.addEventListener('touchstart', handleTouchstart); 38 | overlay.addEventListener('touchend', handleTouchend); 39 | overlay.addEventListener('touchmove', handleTouchmove); 40 | 41 | return { 42 | setHandlers: setHandlers 43 | }; 44 | 45 | // these are the handlers that are invoked when a position is clicked 46 | // or a direction key is pressed. 47 | // They are all set at once for convenience. 48 | // 49 | // Note that these handlers are always invoked with maze coordinates. 50 | function setHandlers(onPosition_, onDirection_, onZoom_) { 51 | onPosition = onPosition_ || noop; 52 | onDirection = onDirection_ || noop; 53 | onZoom = onZoom_ || noop; 54 | mouseOrTouchActive = false; 55 | } 56 | 57 | // 58 | // Mouse handling: double-click for zoom, mouse hold and move for moving. 59 | // 60 | function handleMapDoubleClick(e) { 61 | var pos = coordinates.mapPosition([e.clientX, e.clientY]); 62 | onZoom(pos); 63 | } 64 | 65 | function handleMousedown(e) { 66 | notifyMove(e); 67 | mouseOrTouchActive = true; 68 | } 69 | 70 | function handleMouseup(e) { 71 | mouseOrTouchActive = false; 72 | mouseCell = []; 73 | } 74 | 75 | function handleMousemove(e) { 76 | if (mouseOrTouchActive) { 77 | notifyMove(e); 78 | } 79 | } 80 | 81 | // 82 | // This touch handling is pretty basic and a bit rough. 83 | // 84 | 85 | // on start, set 86 | function handleTouchstart(e) { 87 | firstTouch = e.touches[0]; 88 | mouseOrTouchActive = true; 89 | } 90 | 91 | function handleTouchmove(e) { 92 | var touch = e.changedTouches[0]; 93 | notifyMove(touch); 94 | e.preventDefault(); 95 | } 96 | 97 | function handleTouchend(e) { 98 | mouseOrTouchActive = false; 99 | if (lastTap > Date.now() - 500) { 100 | notifyZoom(firstTouch); 101 | e.preventDefault(); 102 | } 103 | lastTap = Date.now(); 104 | } 105 | 106 | // convert from client coordinates to grid coordinates and notify 107 | // the listener of a move attempt (click on grid square) 108 | function notifyMove(e) { 109 | var pos = coordinates.mapPosition([e.clientX, e.clientY]); 110 | if (pos[0] !== mousePos[0] || pos[1] !== mousePos[1]) { 111 | onPosition(pos); 112 | } 113 | mousePos = pos; 114 | } 115 | 116 | // convert an event with client coordinates into a zoom 117 | // event to the corresponding grid position 118 | function notifyZoom(e) { 119 | var pos = coordinates.mapPosition([e.clientX, e.clientY]); 120 | onZoom(pos); 121 | } 122 | 123 | // Handle arrow keys. 124 | 125 | function handleKeydown(e) { 126 | const key = e.key.replace(/^Arrow/, ''); 127 | 128 | var directions = { 129 | Right: 'E', 130 | Left: 'W', 131 | Up: 'N', 132 | Down: 'S', 133 | } 134 | 135 | if (checkKonami(key)) console.log('KONAMI CODE ACTIVATED'); 136 | var direction = directions[key]; 137 | if (direction) onDirection(direction); 138 | if (key === '=' || key === '+') onZoom(); 139 | } 140 | 141 | // ohai what's this? 142 | // this would be a good place to invoke a maze solver if you had one. 143 | function checkKonami(code) { 144 | var k = ['Up', 'Up', 'Down', 'Down', 'Left', 'Right', 'Left', 'Right', 'b', 'a']; 145 | last10.push(code); 146 | if (last10.length > 10) last10.shift(); 147 | if (last10.length !== 10) return false; 148 | for (var i = 0; i < 10; i++) { 149 | if (last10[i] !== k[i]) return false; 150 | } 151 | return true; 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Mazebot animation](https://user-images.githubusercontent.com/212941/59631813-9ad09f80-90fd-11e9-8556-810c48531558.gif) 2 | 3 | # 👋 Meet Mazebot 4 | For many years, Mazebot spent most of its vacation days and countless lunch breaks mapping the many forgotten cavernous subterranean floors beneath the Noops factory. 5 | 6 | Spending all that time getting lost and finding its way took its toll on Mazebot. 7 | 8 | These days, Mazebot is excited about only one thing: Challenging you to solve mazes. 9 | 10 | Are you ready to solve them? 11 | 12 | Start with the smaller mazes. Once you get the basics down, enter the Mazebot 500, a race to solve a series of mazes in a row. 13 | 14 | IF you can complete them all, you will get a certificate of your achievement. 15 | 16 | # 🤖 API 17 | 18 | Each maze in Mazebot's collection has a map that is a an array of rows. 19 | 20 | Each row is an array of single-length strings representing the contents of each location in the maze. 21 | 22 | The possible values are: 23 | 24 | - `" "` empty - a passable square 25 | - `"X"` - wall - not passable 26 | - `"A"` - starting position - this is where you start in the maze 27 | - `"B"` - goal - this is where you need to navigate to to escape the maze 28 | 29 | The rows are in order from north to south, and the entries in each column are in order from west to east. 30 | 31 | In these mazes, you may travel in any of the four cardinal directions ("N", "E", "S", "W"). 32 | 33 | ## ✳️ How to play 34 | 35 | Mazebot offers two ways to play: random mode, and the great maze race. 36 | 37 | See the [API documentation](./API.md) for more information. 38 | 39 | ## 🎲 Random mode 40 | 41 | Mazebot will give you a random selection from its maze collection and see how fast you can solve it. 42 | 43 | You can optionally limit the sizes of maze you would like with the `minSize` and `maxSize` parameters. 44 | 45 | ### Get a random maze 46 | 47 | `GET /mazebot/random` 48 | 49 | ### Get a maze that is at least 20 squares wide. 50 | 51 | `GET /mazebot/random?minSize=20` 52 | 53 | ### Get a maze that is between 30 and 60 squares wide. 54 | 55 | `GET /mazebot/random?minSize=30&maxSize=60` 56 | 57 | ## 🏎️ Race mode 58 | 59 | In race mode, mazebot will give you a series of mazes and challenge you to solve them all. At the end, if you are successful, Mazebot will award you a certificate that you can use to prove your maze mettle. 60 | 61 | ### Get information about the race 62 | 63 | `GET /mazebot/race` 64 | 65 | ### Start the race 66 | 67 | `POST /mazebot/race { "login": "yourgithubnamehere" }` 68 | 69 | # Starter Kits 70 | 71 | ## Ruby command line client 72 | 73 | You can start building a command-line solver by starting with the [the included ruby script](./mazebot.rb). 74 | 75 | The script demonstrates how to access the Mazebot API and work through the Mazebot race. 76 | 77 | Can you build a program that can complete the Mazebot 500 on its own? 78 | 79 | ## HTML5 Canvas Maze Game 80 | 81 | Mazebot has also included an [HTML Game](./canvas-game) that will let you play through random mazes from the Mazebot API. 82 | 83 | Check out the [source code](./canvas-game) and show us your forks. 84 | 85 | [Play it now!](https://noopschallenge.github.io/canvas-game/index.html) 86 | 87 | ![Screenshot](canvas-game/mazebot-screenshot.png?raw=true "Mazebot screenshot") 88 | 89 | Look at [the README](./canvas-game/README.md) for more information and ideas for extending the game. 90 | 91 | # ✨ A few ideas 92 | 93 | - **Create an automated solver**: Humans can be pretty good at solving mazes, but they'll never be as fast as a well-tuned computer. You could start from the [the included ruby script](./mazebot.rb) or start from scratch in another language. If you create a solver in another language, please share it with the Noops! 94 | 95 | - **Extend the game:** Check out [the canvas game README](./canvas-game/README.md) for some ideas on how to get started. 96 | 97 | - **Create your own maze game:** Try using a tool like [Phaser](http://phaser.io/) to create your own game using the Mazebot API. 98 | 99 | - **Create your own maze API:** Try making your own API that serves random mazes and connect the canvas maze game to it. 100 | 101 | - **Create a colorful terminal client** Do you have a fondness for ASCII graphics? Create a fun client 102 | 103 | - **Generate art with the mazes**: Solving these mazes isn't the only thing you can do with them. Maybe you'd rather use these maze patterns to generate art or sound. 104 | 105 | - **Remix:** Try mixing in one of the [other Noops APIs](http://noopschallenge.com/challenges) to make something amazing. 106 | 107 | Mazebot can't wait to see what you make! 108 | 109 | More about Mazebot here: https://noopschallenge.com/challenges/mazebot 110 | -------------------------------------------------------------------------------- /canvas-game/js/display/overlay-renderer.js: -------------------------------------------------------------------------------- 1 | // Render the overlay: 2 | // - the avatar 3 | // - the goal 4 | // this is continuously animating and is called many times per second 5 | function OverlayRenderer( 6 | overlayContext, 7 | coordinates 8 | ) { 9 | return { 10 | updateOverlay: function (avatarPosition, avatarDirection, endingPosition, frame) { 11 | CanvasUtils.resetCanvas(overlayContext, "rgba(0,0,0,0)"); 12 | 13 | // goal first, so the avatar "floats" above if adjacent 14 | drawGoal(avatarPosition, endingPosition, frame); 15 | drawAvatar(avatarPosition, avatarDirection, frame); 16 | } 17 | }; 18 | 19 | function drawGoal(currentPosition, endingPosition, time) { 20 | var position = coordinates.canvasPosition(endingPosition); 21 | 22 | if (position.offscreen) { 23 | // goal is offscreen - render a directional pointer to the goal 24 | var avatarScreenPosition = coordinates.canvasPosition(currentPosition); 25 | 26 | // figure out the direction 27 | var direction = angle(currentPosition, endingPosition); 28 | var distance = Math.abs(currentPosition[0] - endingPosition[0]) + Math.abs(currentPosition[1] - endingPosition[1]); 29 | 30 | // make it farther away from the avatar the farther from goal 31 | var distanceModifier = (Math.min(40, distance - 20) / 100); 32 | var radius = position.size * (0.85 + distanceModifier); 33 | 34 | // could vary this too... 35 | var lineWidth = 3; 36 | 37 | // cycle the colors based on current time 38 | var frame = Math.floor((time / 150) % Styles.goalDirection.length); 39 | 40 | // draw the arcs 41 | for (var i = 0; i < Styles.goalDirection.length; i++) { 42 | var magnitude = 0.08 * (i + 0.5); 43 | var style = Styles.goalDirection[(frame + i) % Styles.goalDirection.length] 44 | CanvasUtils.drawArc(overlayContext, avatarScreenPosition.center, radius * (1 - (i * 0.05)), direction - magnitude, direction + magnitude, lineWidth, style) 45 | } 46 | } 47 | else { 48 | // hrender a target 49 | 50 | // how big? 51 | if (position.size < 30) factor = factor * 1.2; 52 | if (position.size < 15) factor = factor * 1.2; 53 | 54 | // pulse the size over time 55 | var factor = Math.abs(time % 2000 - 1000) / 4000; 56 | var scale = 0.4 + factor; 57 | var basis = scale * position.size; 58 | 59 | CanvasUtils.drawFilledCircle(overlayContext, position.center, basis * 1.2, position.size / 10, "black", "yellow"); 60 | if (coordinates.cellSize() > 12) { 61 | CanvasUtils.drawFilledCircle(overlayContext, position.center, basis, position.size / 9, "black", "yellow"); 62 | } 63 | CanvasUtils.drawFilledCircle(overlayContext, position.center, basis * 0.8, position.size / 8, "red", "white"); 64 | if (coordinates.cellSize() > 12) { 65 | CanvasUtils.drawFilledCircle(overlayContext, position.center, basis * 0.6, position.size / 8, "red", "yellow"); 66 | } 67 | CanvasUtils.drawFilledCircle(overlayContext, position.center, basis * 0.4, position.size / 8, "red", "white"); 68 | CanvasUtils.drawFilledCircle(overlayContext, position.center, basis * 0.2, position.size / 8, "black", "red"); 69 | } 70 | } 71 | 72 | function angle(from, to) { 73 | var dy = to[1] - from[1]; 74 | var dx = to[0] - from[0]; 75 | return Math.atan2(dy, dx); 76 | } 77 | 78 | // Draw the arrow that represensts the avatar 79 | function drawAvatar(avatarPosition, avatarDirection, time) { 80 | var cell = coordinates.canvasPosition(avatarPosition); 81 | 82 | var scale = 0.8 - (0.0003 * Math.abs(time % 1000 - 500)); 83 | 84 | // size for the display, kinda 85 | if (cell.size < 20) scale += 0.4; 86 | if (cell.size < 10) scale += 0.4; 87 | 88 | var c = cell.scale(scale); 89 | var width = Math.floor(cell.size / 15); 90 | 91 | // point the arrow 92 | if (avatarDirection === 'W') { 93 | drawArrow(overlayContext, c.w, c.ne, c.se, width); 94 | } 95 | else if (avatarDirection === 'E') { 96 | drawArrow(overlayContext, c.e, c.nw, c.sw, width); 97 | } 98 | else if (avatarDirection === 'S') { 99 | drawArrow(overlayContext, c.s, c.ne, c.nw, width); 100 | } 101 | else { 102 | drawArrow(overlayContext, c.n, c.se, c.sw, width); 103 | } 104 | } 105 | 106 | 107 | function drawArrow(overlayContext, tip, c1, c2, width) { 108 | //shadow 109 | CanvasUtils.drawPath(overlayContext, 2, 'black', shadowOffset(width, [c1, c2, tip, c1]), { fill: 'black' }); 110 | 111 | // arrow 112 | CanvasUtils.drawPath(overlayContext, width, '#00E8F6', [c1, c2, tip, c1], { fill: 'yellow' }); 113 | } 114 | 115 | function shadowOffset(width, points) { 116 | return points.map(function(p) { return [p[0] + width * 1.5, p[1] + width * 1.5] }); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /canvas-game/js/game/maze-game.js: -------------------------------------------------------------------------------- 1 | // 2 | // The top-level game manager 3 | // 4 | function MazeGame( 5 | mapCanvas, 6 | avatarPathCanvas, 7 | overlayCanvas, 8 | statusBarContainer, 9 | width, 10 | height 11 | ) { 12 | 13 | var currentMaze = null; 14 | var onSolution; 15 | var currentPosition = null; 16 | var moves = []; 17 | 18 | // Maps between display positions and map positions 19 | var coordinates = Coordinates(); 20 | coordinates.setDisplaySize(width, height); 21 | 22 | // When user taps/drags in a position, 23 | // Find a path to the position so we can move them there 24 | // This is a really basic depth-first pathfinder 25 | var pathfinder = Pathfinder(); 26 | 27 | // Handles user input on the maze and tells us about it: 28 | // click to move, keyboard movement, toggling zoom, etc. 29 | var mazeInput = MazeInput(coordinates, overlay); 30 | 31 | // Renders the maze 32 | var renderer = Renderer( 33 | coordinates, 34 | mapCanvas, 35 | avatarPathCanvas, 36 | overlayCanvas 37 | ); 38 | 39 | // The status bar at the bottom of the screen 40 | var statusBar = StatusBar(statusBarContainer, coordinates); 41 | 42 | return { 43 | // start a new maze 44 | startMaze: startMaze, 45 | 46 | // when the screen size changes, re-render and configure the coordinate system 47 | setDisplaySize: setDisplaySize 48 | }; 49 | 50 | function startMaze(maze, onSolution_) { 51 | moves = []; 52 | setMaze(maze); 53 | onSolution = onSolution_; 54 | renderFrame(); 55 | } 56 | 57 | // the render loop. 58 | function renderFrame() { 59 | if (currentMaze) { 60 | // time is used for periodic animations 61 | var time = Date.now(); 62 | 63 | // There are three stacked canvases: 64 | // the map, the avatar path, and the overlay with the avatar and goal. 65 | // Only the overlay is animated, so it is updated here 66 | renderer.updateOverlay(currentPosition, moves[moves.length - 1], currentMaze.endingPosition, time); 67 | } 68 | 69 | // do it again 70 | requestAnimationFrame(renderFrame); 71 | } 72 | 73 | // Configure the gmae with a new maze 74 | function setMaze(maze) { 75 | currentMaze = maze; 76 | currentPosition = maze.startingPosition; 77 | 78 | // pathfinder needs the map to find paths 79 | pathfinder.setMap(maze.map); 80 | pathfinder.setMaxLength(Math.min(10, maze.map.length)); 81 | 82 | // set up the coordinate system that everything else relies upon 83 | coordinates.setMapSize(maze.map[0].length, maze.map.length); 84 | coordinates.zoom(maze.startingPosition) 85 | 86 | // set the map in the renderer, which will kick off map rendering etc. 87 | renderer.setMap(maze.map); 88 | statusBar.setMaze(maze); 89 | 90 | // turn input on. We ignore input when there is no game active. 91 | mazeInput.setHandlers(handleInputCell, handleInputDirection, handleInputZoom); 92 | 93 | } 94 | 95 | // user requested zoom, optionally with a position 96 | // (double-click/tap on a square as opposed to keyboard plus) 97 | function handleInputZoom(position) { 98 | if (coordinates.zoomed()) { 99 | coordinates.unzoom(); 100 | } 101 | else { 102 | coordinates.zoom(position || currentPosition); 103 | } 104 | 105 | renderer.displaySizeChanged(); 106 | } 107 | 108 | // User click, tapped, or dragged to a cell. 109 | // if it is reachable with the current pathfinder, 110 | // we move them there 111 | function handleInputCell(pos) { 112 | var path = pathfinder.findPath(currentPosition, pos); 113 | if (path) { 114 | path.forEach(handleInputDirection); 115 | } 116 | } 117 | 118 | // user hit an arrow key 119 | // if the neighboring square is open, move there 120 | function handleInputDirection(direction) { 121 | var nextPosition = applyDirectionToCurrentPosition(direction); 122 | if (isValidPosition(nextPosition)) { 123 | // save the move 124 | moves.push(direction); 125 | 126 | // update all the display bits 127 | statusBar.setMoveCount(moves.length); 128 | statusBar.setPosition(nextPosition); 129 | renderer.addToAvatarPath(currentPosition, nextPosition); 130 | currentPosition = nextPosition; 131 | 132 | // tell the coordinate system about the move 133 | // if it returns true that means it movesd the viewport 134 | // in which case we need to tell the renderer it changed 135 | if (coordinates.scrollIfNeeded(currentPosition, direction)) { 136 | renderer.displaySizeChanged(); 137 | } 138 | 139 | // See if we have readched the goal 140 | checkForSolution(); 141 | } 142 | } 143 | 144 | // configure display; see coordinates.js for the meat of what happnens here. 145 | function setDisplaySize(width, height) { 146 | coordinates.setDisplaySize(width, height); 147 | renderer.displaySizeChanged(); 148 | } 149 | 150 | // given a direction, find the coordinates in that direction 151 | function applyDirectionToCurrentPosition(d) { 152 | var offsets = { 153 | N: [0, -1], 154 | S: [0, 1], 155 | W: [-1, 0], 156 | E: [1, 0], 157 | } 158 | 159 | return [currentPosition[0] + offsets[d][0], currentPosition[1] + offsets[d][1]]; 160 | } 161 | 162 | // Check to see if we are at the goal. 163 | // If so, submit our solution to the Mazebot api 164 | // and call back with our result so we can show it 165 | // (ending the current maze) 166 | function checkForSolution() { 167 | if ( 168 | currentPosition[0] === currentMaze.endingPosition[0] && 169 | currentPosition[1] === currentMaze.endingPosition[1] 170 | ) { 171 | mazeInput.setHandlers(null, null); 172 | postJson(currentMaze.mazePath, { 173 | directions: moves.join('') 174 | }).then(function (result) { 175 | onSolution(result); 176 | }); 177 | } 178 | } 179 | 180 | // is this a valid position to move to? 181 | // is it either the start, the end, or an open position 182 | function isValidPosition(p) { 183 | var map = currentMaze.map; 184 | 185 | // out of bounds 186 | if ( 187 | p[0] < 0 || 188 | p[1] < 0 || 189 | p[0] >= map[0].length || 190 | p[1] >= map.length 191 | ) return false; 192 | 193 | // wall 194 | if (map[p[1]][p[0]] === 'X') return false; 195 | 196 | return true; 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | ## Mazebot API 2 | 3 | The Mazebot API (https://api.noopschallenge.com/mazebot) has two different modes: *random* and *race*. 4 | 5 | ## Random mode 6 | 7 | Mazebot will serve you random mazes. You can also use the query parameter `minSize` and `maxSize` to control the size of maze that is served. 8 | 9 | The available sizes are: 10, 20, 30, 40, 60, 100, 120, 150, and 200. 10 | 11 | ### getting a random maze 12 | 13 | `GET https://api.noopschallenge.com/mazebot/random` 14 | 15 | `HTTP 200` 16 | 17 | ``` 18 | { 19 | "name": "Maze #236 (10x10)", 20 | "mazePath": "/mazebot/mazes/ikTcNQMwKhux3bWjV3SSYKfyaVHcL0FXsvbwVGk5ns8", 21 | "startingPosition": [ 4, 3 ], 22 | "endingPosition": [ 3, 6 ], 23 | "message": "When you have figured out the solution, post it back to this url. See the exampleSolution for more information.", 24 | "exampleSolution": { "directions": "ENWNNENWNNS" }, 25 | "map": [ 26 | [ " ", " ", "X", " ", " ", " ", "X", " ", "X", "X" ], 27 | [ " ", "X", " ", " ", " ", " ", " ", " ", " ", " " ], 28 | [ " ", "X", " ", "X", "X", "X", "X", "X", "X", " " ], 29 | [ " ", "X", " ", " ", "A", " ", " ", " ", "X", " " ], 30 | [ " ", "X", "X", "X", "X", "X", "X", "X", " ", " " ], 31 | [ "X", " ", " ", " ", "X", " ", " ", " ", "X", " " ], 32 | [ " ", " ", "X", "B", "X", " ", "X", " ", "X", " " ], 33 | [ " ", " ", "X", " ", "X", " ", "X", " ", " ", " " ], 34 | [ "X", " ", "X", "X", "X", "X", "X", " ", "X", "X" ], 35 | [ "X", " ", " ", " ", " ", " ", " ", " ", "X", "X" ] 36 | ] 37 | } 38 | ``` 39 | 40 | 41 | ### getting a maze with size constraints 42 | 43 | `GET https://api.noopschallenge.com/mazebot/random?minSize=10&maxSize=20` 44 | 45 | `HTTP 200` 46 | 47 | ``` 48 | { 49 | "name": "Maze #142 (10x10)", 50 | "mazePath": "/mazebot/mazes/dTXurZOonsCbWC9_PDBWpiRAvBME3VBDIf9hcwwCdNc", 51 | "startingPosition": [ 9, 3 ], 52 | "endingPosition": [ 7, 0 ], 53 | "message": "When you have figured out the solution, post it back to this url. See the exampleSolution for more information.", 54 | "exampleSolution": { "directions": "ENWNNENWNNS" }, 55 | "map": [ 56 | [ "X", " ", " ", " ", " ", " ", "X", "B", " ", " " ], 57 | [ " ", " ", " ", " ", "X", " ", " ", " ", "X", " " ], 58 | [ " ", "X", "X", "X", " ", "X", "X", "X", " ", "X" ], 59 | [ " ", " ", " ", " ", "X", " ", " ", "X", " ", "A" ], 60 | [ " ", "X", "X", "X", " ", "X", " ", "X", " ", " " ], 61 | [ " ", " ", " ", "X", " ", "X", " ", "X", " ", " " ], 62 | [ " ", "X", " ", "X", " ", "X", " ", "X", " ", "X" ], 63 | [ " ", " ", " ", "X", " ", "X", " ", "X", " ", " " ], 64 | [ "X", " ", "X", "X", " ", " ", " ", " ", " ", " " ], 65 | [ "X", " ", " ", " ", " ", "X", " ", " ", " ", "X" ] 66 | ] 67 | } 68 | ``` 69 | 70 | 71 | ### solving a random maze 72 | 73 | `POST https://api.noopschallenge.com/mazebot/mazes/RPSpGy5aqL5p30LpQwqiO9O2yQipiLQT3WGqPSHtyVg` 74 | 75 | 76 | ``` 77 | { 78 | "directions": "ENNNN...." 79 | } 80 | ``` 81 | 82 | `HTTP 200` 83 | 84 | ``` 85 | { 86 | "result": "success", 87 | "message": "You solved it in 0.029 seconds with 56 steps, the shortest possible solution.", 88 | "shortestSolutionLength": 56, 89 | "yourSolutionLength": 56, 90 | "elapsed": 29 91 | } 92 | ``` 93 | 94 | 95 | ### incorrect solution 96 | 97 | `POST https://api.noopschallenge.com/mazebot/mazes/17pSAsql1EEaCvEe28UnAQ` 98 | 99 | 100 | ``` 101 | { 102 | "directions": "ESS" 103 | } 104 | ``` 105 | 106 | `HTTP 400` 107 | 108 | ``` 109 | { "message": "Hit a wall at directions[1]", "result": "failed" } 110 | ``` 111 | 112 | 113 | ### incorrect solution - did not finish 114 | 115 | `POST https://api.noopschallenge.com/mazebot/mazes/17pSAsql1EEaCvEe28UnAQ` 116 | 117 | 118 | ``` 119 | { 120 | "directions": "" 121 | } 122 | ``` 123 | 124 | `HTTP 400` 125 | 126 | ``` 127 | { "message": "Did not make it to point B", "result": "failed" } 128 | ``` 129 | 130 | ## Race mode 131 | 132 | Once you are getting good at solving mazes, join the Mazebot 500, a timed race to solve a series of mazes of increasing difficulty. 133 | If you can solve them all, Mazebot will grant you a certificate of your achievement. 134 | 135 | Be warned -- some of these mazes are really big! Maybe a robot would be better suited to this task than a human... 136 | 137 | ### get info about the maze race 138 | 139 | `GET https://api.noopschallenge.com/mazebot/race` 140 | 141 | `HTTP 200` 142 | 143 | ``` 144 | { 145 | "message": "Welcome to the Mazebot 500.\n\nI will give you a series of mazes\nIf you can solve them all, I'll give you a certificate that proves your maze mettle.\n\nAre you ready to enter?\n\nWhen you are ready, send me your github login by posting json to /mazebot/race/start\n\nSee the attached exampleRequest.\n", 146 | "exampleRequest": { "login": "noops-challenge" } 147 | } 148 | ``` 149 | 150 | 151 | ### Start the race with a POST 152 | 153 | `POST https://api.noopschallenge.com/mazebot/race/start` 154 | 155 | 156 | ``` 157 | { 158 | "login": "yourgithubloginhere" 159 | } 160 | ``` 161 | 162 | `HTTP 200` 163 | 164 | ``` 165 | { 166 | "message": "Start your engines!", 167 | "nextMaze": "/mazebot/race/iEGpDT1I0qFzGU81yb49JY3Srj1daT70P6e-Zr6bpR0" 168 | } 169 | ``` 170 | 171 | 172 | ### Get the first maze in the race from the provided URL 173 | 174 | `GET https://api.noopschallenge.com/mazebot/race/Fh5Kt7l9gMQr41GvWkmoCg` 175 | 176 | `HTTP 200` 177 | 178 | ``` 179 | { 180 | "name": "Mazebot 500 Stage#1 (5x5)", 181 | "mazePath": "/mazebot/race/Fh5Kt7l9gMQr41GvWkmoCg", 182 | "map": [ 183 | [ "A", " ", " ", " ", " " ], 184 | [ " ", "X", "X", "X", " " ], 185 | [ " ", " ", "X", " ", " " ], 186 | [ "X", " ", "X", "B", "X" ], 187 | [ "X", " ", " ", " ", "X" ] 188 | ], 189 | "message": "When you have figured out the solution, post it back to this url in JSON format. See the exampleSolution for more information.", 190 | "startingPosition": [ 0, 0 ], 191 | "endingPosition": [ 3, 3 ], 192 | "exampleSolution": { "directions": "ENWNNENWNNS" } 193 | } 194 | ``` 195 | 196 | 197 | ### Solve the first maze in the race 198 | 199 | `POST https://api.noopschallenge.com/mazebot/race/Fh5Kt7l9gMQr41GvWkmoCg` 200 | 201 | 202 | ``` 203 | { 204 | "directions": "SSE..." 205 | } 206 | ``` 207 | 208 | `HTTP 200` 209 | 210 | ``` 211 | { 212 | "result": "success", 213 | "elapsed": 0.52, 214 | "shortestSolutionLength": 8, 215 | "yourSolutionLength": 8, 216 | "nextMaze": "/mazebot/race/2os41shHeVDhaKTAw1m9-Q" 217 | } 218 | `` 219 | -------------------------------------------------------------------------------- /canvas-game/js/utils/coordinates.js: -------------------------------------------------------------------------------- 1 | // Coordinate system that maps between display coordinates and maze coordinates 2 | function Coordinates() { 3 | var displayWidth = 1; 4 | var displayHeight = 1; 5 | var mapHeight = 1; 6 | var mapWidth = 1; 7 | var cellSize = 0; 8 | 9 | var displayOffsetX = 0; 10 | var displayOffsetY = 0; 11 | 12 | var cellOffsetX = 0; 13 | var cellOffsetY = 0; 14 | var displayCells = 1; 15 | 16 | return { 17 | // set the size of the display 18 | setDisplaySize: setDisplaySize, 19 | // set the size of the map 20 | setMapSize: setMapSize, 21 | 22 | // given a map coordinate, get the position on the canvas 23 | canvasPosition: canvasPosition, 24 | 25 | // from a point on the canvas, get the map position 26 | mapPosition: mapPosition, 27 | 28 | // zoom into a point 29 | zoom: zoom, 30 | unzoom: unzoom, 31 | 32 | // given a player move, scroll to keep the player in view 33 | // returns true if scrolled, so renderers can update 34 | scrollIfNeeded: scrollIfNeeded, 35 | 36 | // boolean: are we zoomed? 37 | zoomed: zoomed, 38 | 39 | // number: pixel size of cells 40 | cellSize: function() { return cellSize }, 41 | 42 | // size of the display in pixels 43 | displaySize: function(){ return { width: displayWidth, height: displayHeight }; } 44 | }; 45 | 46 | function zoomed() { 47 | return displayCells !== mapWidth; 48 | } 49 | 50 | // zoom the viewport in on a point; 51 | function zoom(center) { 52 | // figure out how many cells wide/high to show 53 | // note it is always an odd number so there is a center cell 54 | // "responsive" -- try to keep the viewport reasonable 55 | var cells = 17; 56 | var smallestScreenDimension = Math.min(displayHeight, displayWidth); 57 | 58 | if (smallestScreenDimension < 600) { 59 | cells = 9; 60 | } 61 | else if (smallestScreenDimension < 1200) { 62 | cells = 13; 63 | } 64 | 65 | // Only zoom if the map is bigger than the screen while zoomed 66 | if (cells > mapWidth) { 67 | unzoom(); 68 | return; 69 | } 70 | 71 | displayCells = cells; 72 | 73 | setCenter(center); 74 | setCellSize(); 75 | } 76 | 77 | // center the viewpoprt on a maze square 78 | function setCenter(center) { 79 | var offset = Math.floor(displayCells / 2); 80 | cellOffsetX = Math.min(mapWidth - displayCells, Math.max(0, center[0] - offset)); 81 | cellOffsetY = Math.min(mapHeight - displayCells, Math.max(0, center[1] - offset)); 82 | } 83 | 84 | // undo any zooming 85 | function unzoom() { 86 | displayCells = mapWidth; 87 | cellOffsetX = 0; 88 | cellOffsetY = 0; 89 | setCellSize(); 90 | } 91 | 92 | 93 | // when a move happens, decide if we need to scroll and recenter the screen 94 | function scrollIfNeeded(position, direction) { 95 | if (!zoomed()) return; 96 | 97 | if ( direction === 'W' && position[0] <= cellOffsetX && cellOffsetX > 0) { 98 | cellOffsetX = Math.max(0, cellOffsetX - 1); 99 | return true; 100 | } 101 | else if (direction === 'N' && position[1] <= cellOffsetY && cellOffsetY > 0) { 102 | cellOffsetY = Math.max(0, cellOffsetY - 1) 103 | return true; 104 | } 105 | else if (direction === 'E' && position[0] >= cellOffsetX + displayCells - 1 && cellOffsetX < mapHeight - displayCells) { 106 | cellOffsetX = Math.min(mapWidth - displayCells, cellOffsetX + 1); 107 | return true; 108 | } 109 | else if (direction === 'S' && position[1] >= cellOffsetY + displayCells - 1 && cellOffsetY < mapHeight - displayCells) { 110 | cellOffsetY = Math.min(mapHeight - displayCells, cellOffsetY + 1); 111 | return true; 112 | } 113 | 114 | // if avatar was completely offscreen, center on them 115 | if (canvasPosition(position).offscreen) { 116 | setCenter(position); 117 | return true; 118 | } 119 | } 120 | 121 | // When display size changes, recalculate our offsets 122 | function setDisplaySize(width, height) { 123 | displayWidth = width; 124 | displayHeight = height; 125 | 126 | if (width > height) { 127 | displayOffsetX = Math.round((width - height) / 2); 128 | displayOffsetY = 0; 129 | } 130 | else { 131 | displayOffsetX = 0; 132 | displayOffsetY = Math.round((height - width) / 2); 133 | } 134 | 135 | setCellSize(); 136 | } 137 | 138 | // Set the size of map. 139 | // This is immediately followed by zooming in practice, 140 | // so we dont' need to do anything else here -- but that could be kind of fragile 141 | function setMapSize(width, height) { 142 | mapWidth = width; 143 | mapHeight = height; 144 | } 145 | 146 | // cache the size of cell; we use it everywhere 147 | function setCellSize() { 148 | // note: only really supports square maps 149 | cellSize = Math.min(displayWidth, displayHeight) / displayCells; 150 | } 151 | 152 | function mapPosition(canvasCoords) { 153 | return [Math.floor((canvasCoords[0] - displayOffsetX) / cellSize) + cellOffsetX, Math.floor((canvasCoords[1] - displayOffsetY) / cellSize) + cellOffsetY]; 154 | } 155 | 156 | // canvas positions are used all over the renderers 157 | // to figure out screen coordinates from map coordinates 158 | // they have a bunch of pre-calced props with piel coordinates 159 | // for renderers to use 160 | function canvasPosition(coords) { 161 | var x = coords[0] - cellOffsetX; 162 | var y = coords[1] - cellOffsetY; 163 | var l = Math.floor(x * cellSize + displayOffsetX); 164 | var t = Math.floor(y * cellSize + displayOffsetY); 165 | 166 | return makePosition(l, t, cellSize); 167 | } 168 | 169 | function makePosition(l, t, size) { 170 | var half = Math.floor(size / 2); 171 | var hc = l + half; 172 | var vc = t + half; 173 | 174 | var r = l + size; 175 | var b = t + size; 176 | 177 | // all values are in canvas (ie. pixel) coordinates 178 | var p = { 179 | // numeric values 180 | t: t, // top 181 | l: l, // left 182 | r: r, // right 183 | b: b, // bottom 184 | hc: hc, // horizontal center 185 | vc: vc, // vertical center 186 | size: size, // width/height (it is a square) 187 | 188 | // coordinates for various points in the cell 189 | // [x, y] pairs 190 | center: [hc, vc], // the center 191 | 192 | n: [hc, t], // north: the center point of the north edge 193 | s: [hc, b], // south 194 | e: [r, vc], // east 195 | w: [l, vc], // west 196 | 197 | nw: [l, t], // northwest corner ("top left") of the box 198 | sw: [l, b], // southwest 199 | ne: [r, t], // northeast 200 | se: [r, b], // southeast 201 | 202 | // boolean: is this offscreen (out of the viewport)? 203 | // used to skip rendering 204 | offscreen: l > displayWidth || r < 0 || t > displayHeight || b < 0 205 | }; 206 | 207 | // scale method creates another position that has the same center 208 | // but is larger/smaller -- useful for rendering 209 | p.scale = scalePosition.bind(null, p); 210 | 211 | return p; 212 | } 213 | 214 | 215 | // see above - scales a canvas position bigger or smaller 216 | // scale < 1 shrinks, > 1 makes it bigger 217 | function scalePosition(position, scale) { 218 | var size = position.size * scale; 219 | var half = Math.floor(size / 2); 220 | var vc = position.vc; 221 | var hc = position.hc; 222 | 223 | var t = vc - half; 224 | var l = hc - half; 225 | return makePosition(l, t, size); 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /canvas-game/js/display/map-renderer.js: -------------------------------------------------------------------------------- 1 | // 2 | 3 | var MapRenderer = function ( 4 | mapContext, 5 | coordinates 6 | ) { 7 | var map = []; 8 | var walls = []; 9 | var corners = []; 10 | 11 | return { 12 | setMap: setMap, 13 | render: render 14 | }; 15 | 16 | function setMap(m) { 17 | map = m; 18 | render(); 19 | } 20 | 21 | // There are two phases: 22 | // 1. collect all of the elements to render 23 | // 2. render those elements in the proper order from back to front 24 | function render() { 25 | // Collect all the contents we will render 26 | clearContents(); 27 | map.forEach(collectRowContents); 28 | collectBoundaries(); 29 | 30 | // Now draw 31 | CanvasUtils.resetCanvas(mapContext, Styles.mapBackground); 32 | 33 | // Draw floor 34 | map.forEach(drawRowBackground); 35 | 36 | // Draw walls 37 | renderContents(); 38 | } 39 | 40 | function clearContents() { 41 | walls = []; 42 | corners = []; 43 | endcaps = []; 44 | } 45 | 46 | 47 | function collectBoundaries() { 48 | var tl = coordinates.canvasPosition([-1,-1]); 49 | var bl = coordinates.canvasPosition([-1,map.length]); 50 | var tr = coordinates.canvasPosition([map[0].length, -1]); 51 | var br = coordinates.canvasPosition([map[0].length, map.length]); 52 | 53 | addWallSegment(tl.center, bl.center, tl.size); 54 | addWallSegment(tr.center, br.center, tl.size); 55 | addWallSegment(bl.center, br.center, tl.size); 56 | addWallSegment(tl.center, tr.center, tl.size); 57 | } 58 | 59 | function collectRowContents(row, y) { 60 | row.forEach(function (value, x) { 61 | processCell(x, y, value, map); 62 | }); 63 | } 64 | 65 | // collect walls 66 | function processCell(x, y, value, map) { 67 | if (value === 'X') { 68 | processFilledCell(x, y, map); 69 | } 70 | } 71 | 72 | // This cell has an X -- figure out how to render the walls 73 | function processFilledCell(x, y, map) { 74 | var cell = coordinates.canvasPosition([x, y]); 75 | 76 | if (cell.offscreen) return; 77 | 78 | var neighbors = getNeighbors(x, y, map); 79 | 80 | addCorners(cell, neighbors); 81 | addWalls([x, y], cell, neighbors); 82 | } 83 | 84 | // Fill in corners with white 85 | function addCorners(cell, neighbors) { 86 | var scaled = cell.scale(1.1); 87 | var half = Math.floor(scaled.size / 2) 88 | 89 | if (neighbors.n && neighbors.w) { 90 | addCorner(scaled.nw, half, half); 91 | } 92 | if (neighbors.n && neighbors.e) { 93 | addCorner(scaled.n, half, half); 94 | } 95 | if (neighbors.s && neighbors.w) { 96 | addCorner(scaled.w, half, half); 97 | } 98 | if (neighbors.s && neighbors.e) { 99 | addCorner(scaled.center, half, half); 100 | } 101 | } 102 | 103 | function addCorner(topLeft, width, height) { 104 | corners.push({ 105 | topLeft: topLeft, 106 | width: width, 107 | height: height 108 | }); 109 | } 110 | 111 | // 112 | // get the neighbors of the cell. 113 | // 114 | // each direction is either 'X' (wall), 115 | // 'O' (out of bounds), or undefined 116 | function getNeighbors(x, y, map) { 117 | var count = 0; 118 | var edgeCount = 0; 119 | 120 | function neighbor(dx, dy) { 121 | var value = (map[dy + y] || [])[x + dx]; 122 | 123 | if (!value) { 124 | // on an edge 125 | edgeCount++; 126 | return 'O'; 127 | } 128 | if (value === 'X') { 129 | // neighbor is a wall 130 | count++; 131 | return 'X'; 132 | } 133 | } 134 | return { 135 | n: neighbor(0, -1), 136 | s: neighbor(0, 1), 137 | w: neighbor(-1, 0), 138 | e: neighbor(1, 0), 139 | nw: neighbor(-1, -1), 140 | sw: neighbor(-1, 1), 141 | ne: neighbor(1, -1), 142 | se: neighbor(1, 1), 143 | edgeCount: edgeCount, 144 | count: count 145 | }; 146 | } 147 | 148 | // add wall to each neighbor that is occupied 149 | function addWalls(position, cell, neighbors) { 150 | addCellWallSegment(cell, neighbors, "n"); 151 | addCellWallSegment(cell, neighbors, "w"); 152 | addCellWallSegment(cell, neighbors, "s"); 153 | addCellWallSegment(cell, neighbors, "e"); 154 | 155 | addCellWallSegment(cell, neighbors, "ne"); 156 | addCellWallSegment(cell, neighbors, "nw"); 157 | addCellWallSegment(cell, neighbors, "se"); 158 | addCellWallSegment(cell, neighbors, "sw"); 159 | 160 | // don't render endcaps if zoomed out and squares are tiny 161 | if (cell.size < 13) return; 162 | 163 | // add an endcap if this is a lone wall square or 164 | // if there is only one neighbor 165 | if ((neighbors.count === 1 && !neighbors.edgeCount) || neighbors.count === 0) { 166 | // pick a color 167 | var colorIndex = (position[0] + position[1]) % Styles.endcapFill.length; 168 | addEndcap(cell.center, colorIndex); 169 | } 170 | } 171 | 172 | function addEndcap(center, colorIndex) { 173 | endcaps.push({ center: center, colorIndex: colorIndex}); 174 | } 175 | 176 | // add a piece of wall if the direction specified by prop is occupied 177 | function addCellWallSegment(cell, neighbors, prop) { 178 | var contents = neighbors[prop]; 179 | 180 | if (contents === 'O') { 181 | // If we are at the edge, we don't want to treat the diagonal 182 | // edges as occupied, only the cardinal ones 183 | // so if we are drawing a "nw" wall, only do it if we have north and west neighbors 184 | if (prop.length === 2) { 185 | if (!neighbors[prop[0]] || !neighbors[prop[1]]) { 186 | return; 187 | } 188 | } 189 | // out of bounds means we are at the edge 190 | // scale up so we draw past the edge 191 | var doubled = cell.scale(2); 192 | addWallSegment(doubled[prop], doubled.center, coordinates.cellSize() / 1.7); 193 | } 194 | else if (contents === 'X') { 195 | // fudge enough to always connect adjacent lines 196 | var enlarged = cell.scale(1.1); 197 | addWallSegment(enlarged[prop], cell.center, coordinates.cellSize() / 1.7); 198 | } 199 | } 200 | 201 | function addWallSegment(from, to, thickness) { 202 | walls.push({ 203 | from: from, to: to, thickness: thickness 204 | }); 205 | } 206 | 207 | // draw the background, a tiled floor pattern 208 | function drawRowBackground(row, y) { 209 | row.forEach(function (value, x) { 210 | drawBackground(mapContext, x, y, value, map); 211 | }); 212 | } 213 | 214 | // draw tiles on the entire visible map 215 | function drawBackground(mapContext, x, y) { 216 | var cell = coordinates.canvasPosition([x, y]); 217 | if (cell.offscreen) return; 218 | 219 | var width = cell.size / 40; 220 | 221 | var shrunk = cell.scale(0.95); 222 | 223 | // highlight/shadow 224 | CanvasUtils.drawPath(mapContext, width, Styles.tileOutline[0], [shrunk.sw, shrunk.nw, shrunk.ne]); 225 | CanvasUtils.drawPath(mapContext, width, Styles.tileOutline[1], [shrunk.ne, shrunk.se, shrunk.sw]); 226 | } 227 | 228 | // render the walls, edge boundary, and endcaps 229 | function renderContents() { 230 | // calculate how far to offset our highlight and shadow layers 231 | // that create the faux 3d look based on the cell size. 232 | var dx = Math.min(8, Math.max(2, coordinates.cellSize() / 15)); 233 | var dy = dx / 2; 234 | var highlightOffset = [-dx, -dy]; 235 | var shadowOffset = [dx, dy]; 236 | 237 | // only render the shadow and highlight layers if cells are big enough 238 | if (coordinates.cellSize() > 12) { 239 | // we draw all of the the highlights and shadows on top of each other 240 | endcaps.forEach(drawEndcapBackground.bind(null, highlightOffset, shadowOffset)); 241 | walls.forEach(drawWallBackground.bind(null, highlightOffset, shadowOffset)); 242 | // border layer 243 | walls.forEach(drawWallLayer1); 244 | } 245 | 246 | // draw the tops of walls ad boundaries 247 | walls.forEach(drawWallLayer2); 248 | 249 | // fill in the corners where needed - 250 | corners.forEach(drawCorner); 251 | 252 | // draw the colored endcaps (if on a large enough screen) 253 | if (coordinates.cellSize() > 12) { 254 | endcaps.forEach(drawEndcap); 255 | } 256 | } 257 | 258 | // draw the highlight and shadow layers 259 | function drawWallBackground(highlightOffset, shadowOffset, wall) { 260 | CanvasUtils.drawPath(mapContext, wall.thickness, Styles.highlight, translateMany(highlightOffset, [wall.from, wall.to])); 261 | CanvasUtils.drawPath(mapContext, wall.thickness, Styles.shadow, translateMany(shadowOffset, [wall.from, wall.to])); 262 | } 263 | 264 | // highlight and shado for the endcap circless 265 | function drawEndcapBackground(highlightOffset, shadowOffset, endcap) { 266 | CanvasUtils.drawFilledCircle(mapContext, translate(highlightOffset, endcap.center), coordinates.cellSize() / 2.5, 2, Styles.highlight, Styles.highlight); 267 | CanvasUtils.drawFilledCircle(mapContext, translate(shadowOffset, endcap.center), coordinates.cellSize() / 2.5, 2, Styles.shadow, Styles.shadow); 268 | } 269 | 270 | // the border layer (black) 271 | function drawWallLayer1(wall) { 272 | CanvasUtils.drawPath(mapContext, wall.thickness, Styles.wallLayer1, [wall.from, wall.to]); 273 | } 274 | 275 | // The top layer (white) 276 | function drawWallLayer2(wall) { 277 | CanvasUtils.drawPath(mapContext, wall.thickness * 0.8, Styles.wallLayer2, [wall.from, wall.to]); 278 | } 279 | 280 | // Corners are just white rects that fill in the corner by a diagonal wall 281 | // so that there is no ugly hole there 282 | function drawCorner(corner) { 283 | mapContext.strokeStyle = Styles.corner; 284 | mapContext.fillStyle = Styles.corner; 285 | mapContext.fillRect( 286 | corner.topLeft[0], 287 | corner.topLeft[1], 288 | corner.width, 289 | corner.height 290 | ); 291 | } 292 | 293 | // Endcaps are circles on the end of walls 294 | function drawEndcap(endcap) { 295 | CanvasUtils.drawFilledCircle(mapContext, endcap.center, coordinates.cellSize() / 2.5, coordinates.cellSize() / 12, 'black', 'white'); 296 | CanvasUtils.drawFilledCircle(mapContext, endcap.center, coordinates.cellSize() / 3.5, coordinates.cellSize() / 12, 'black', Styles.endcapFill[endcap.colorIndex]); 297 | } 298 | 299 | // utility functions for translating coordinates for highlight and shadow 300 | function translate(offset, pair) { 301 | return [pair[0] + offset[0], pair[1] + offset[1]]; 302 | } 303 | 304 | function translateMany(offset, pairs) { 305 | return pairs.map(function(p) { return [p[0] + offset[0], p[1] + offset[1]]; }); 306 | } 307 | } 308 | -------------------------------------------------------------------------------- /entries/chapmanj/mazebot-chapmanj-secret.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# 1. Maze Traveler Code" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": null, 13 | "metadata": {}, 14 | "outputs": [], 15 | "source": [ 16 | "import urllib, json, urllib.request, numpy as np" 17 | ] 18 | }, 19 | { 20 | "cell_type": "code", 21 | "execution_count": null, 22 | "metadata": {}, 23 | "outputs": [], 24 | "source": [ 25 | "baseurl = \"https://api.noopschallenge.com\"" 26 | ] 27 | }, 28 | { 29 | "cell_type": "markdown", 30 | "metadata": {}, 31 | "source": [ 32 | "## 1.1 NOOP Challenge GET and POST functions" 33 | ] 34 | }, 35 | { 36 | "cell_type": "code", 37 | "execution_count": null, 38 | "metadata": {}, 39 | "outputs": [], 40 | "source": [ 41 | "def get_maze(qurl):\n", 42 | " with urllib.request.urlopen(qurl) as url:\n", 43 | " question = json.loads(url.read().decode())\n", 44 | " return question" 45 | ] 46 | }, 47 | { 48 | "cell_type": "code", 49 | "execution_count": null, 50 | "metadata": {}, 51 | "outputs": [], 52 | "source": [ 53 | "def post(qurl, qanswer) :\n", 54 | "\n", 55 | " head={'Content-Type': 'application/json'}\n", 56 | " \n", 57 | " answer = qanswer.encode('utf8')\n", 58 | " \n", 59 | " try :\n", 60 | " req = urllib.request.Request(qurl, data=answer, headers=head)\n", 61 | " res = urllib.request.urlopen(req)\n", 62 | " response = json.load(res)\n", 63 | "\n", 64 | " return response\n", 65 | " \n", 66 | " except urllib.error.HTTPError as e:\n", 67 | " response = json.load(e)\n", 68 | " return response" 69 | ] 70 | }, 71 | { 72 | "cell_type": "markdown", 73 | "metadata": {}, 74 | "source": [ 75 | "## 1.2 Maze Walker Class" 76 | ] 77 | }, 78 | { 79 | "cell_type": "code", 80 | "execution_count": null, 81 | "metadata": {}, 82 | "outputs": [], 83 | "source": [ 84 | "class Walker:\n", 85 | " def __init__(self, x=0, y=0):\n", 86 | " self.x = x\n", 87 | " self.y = y\n", 88 | " self.directions = []\n", 89 | " self.locations = [[x, y]]\n", 90 | " self.visited = [[x, y]]\n", 91 | " \n", 92 | " def xy(self) :\n", 93 | " return [self.x, self.y]\n", 94 | " \n", 95 | " def N(self) : \n", 96 | " self.y -= 1\n", 97 | " self.directions += 'N' \n", 98 | " self.record()\n", 99 | " \n", 100 | " def S(self) :\n", 101 | " self.y += 1\n", 102 | " self.directions += 'S'\n", 103 | " self.record()\n", 104 | "\n", 105 | " def E(self) :\n", 106 | " self.x += 1\n", 107 | " self.directions += 'E'\n", 108 | " self.record()\n", 109 | "\n", 110 | " def W(self) :\n", 111 | " self.x -= 1\n", 112 | " self.directions += 'W'\n", 113 | " self.record()\n", 114 | "\n", 115 | " def set_at(self, index) : \n", 116 | " self.x = self.locations[index][0]\n", 117 | " self.y = self.locations[index][1]\n", 118 | " self.locations = self.locations[0:index + 1]\n", 119 | " self.directions = self.directions[0:index ]\n", 120 | " \n", 121 | " def record(self) :\n", 122 | " self.locations.append([self.x, self.y])\n", 123 | " self.visited.append([self.x, self.y])\n", 124 | "\n", 125 | " def get_moves(self) :\n", 126 | " return [self.N, self.S, self.E, self.W]\n", 127 | " \n", 128 | " def clone(self) :\n", 129 | " myclone = Walker(self.x, self.y)\n", 130 | " \n", 131 | " for i in self.directions :\n", 132 | " myclone.directions.append(i)\n", 133 | " \n", 134 | " for j in self.locations :\n", 135 | " myclone.locations.append([j[0], j[1]])\n", 136 | " \n", 137 | " for k in self.visited :\n", 138 | " myclone.visited.append([k[0], k[1]])\n", 139 | " \n", 140 | " return myclone\n", 141 | " \n", 142 | " def __str__(self) :\n", 143 | " return '(' + str(self.x) + ',' + str(self.y) + ')' + self.directions[-1] #+ self.visited" 144 | ] 145 | }, 146 | { 147 | "cell_type": "markdown", 148 | "metadata": {}, 149 | "source": [ 150 | "## 1.3 Dungeon Master Class" 151 | ] 152 | }, 153 | { 154 | "cell_type": "code", 155 | "execution_count": null, 156 | "metadata": {}, 157 | "outputs": [], 158 | "source": [ 159 | "class Dungeon_master :\n", 160 | " def __init__(self, maze) :\n", 161 | " self.maze = maze #col x row (like x,y)\n", 162 | " \n", 163 | " def solve(self, walker, visualize=False, verbose=False, silent=False, iterations=-1) :\n", 164 | " \n", 165 | " while [walker.x, walker.y] != self.maze['endingPosition'] :\n", 166 | " \n", 167 | " #neighbors ^v><^v><^v><^v><^v><^v><^v><^v><^v><^v><^v><^v><^v><\n", 168 | " #\n", 169 | " \n", 170 | " #move forward >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n", 171 | " #\n", 172 | " \n", 173 | " #backtrack <<<<<<<<<<<<<<<<<<<<<<<<<\n", 174 | " #\n", 175 | " \n", 176 | " #feedback ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", 177 | " #\n", 178 | " \n", 179 | " #output ==================================>\n", 180 | " #\n", 181 | " \n", 182 | " return walker.directions\n", 183 | " " 184 | ] 185 | }, 186 | { 187 | "cell_type": "markdown", 188 | "metadata": {}, 189 | "source": [ 190 | "## 1.4 Helper Functions" 191 | ] 192 | }, 193 | { 194 | "cell_type": "code", 195 | "execution_count": null, 196 | "metadata": {}, 197 | "outputs": [], 198 | "source": [ 199 | "def draw_maze(maze_map, walker=None) :\n", 200 | " \n", 201 | " mymap = '\\n'\n", 202 | " \n", 203 | " i = 0\n", 204 | " for row in maze_map:\n", 205 | " j = 0 \n", 206 | " for cell in row :\n", 207 | " if walker != None and [j,i] in walker.locations and cell != 'A' and cell != 'B' : mymap += ' ='\n", 208 | " else: mymap += ' ' + cell \n", 209 | " j += 1\n", 210 | " mymap += '\\n'\n", 211 | " i += 1\n", 212 | " \n", 213 | " print(mymap)" 214 | ] 215 | }, 216 | { 217 | "cell_type": "markdown", 218 | "metadata": {}, 219 | "source": [ 220 | "# 2. Solve a maze" 221 | ] 222 | }, 223 | { 224 | "cell_type": "code", 225 | "execution_count": null, 226 | "metadata": {}, 227 | "outputs": [], 228 | "source": [ 229 | "#get new maze\n", 230 | "#maze = get_maze(baseurl + '/mazebot/random')\n", 231 | "\n", 232 | "#or\n", 233 | "\n", 234 | "#use a single instance\n", 235 | "maze = {'name': 'Maze #472 (20x20)', 'mazePath': '/mazebot/mazes/bVDhG-4JSk4DiK1mJww7uN_hSO2juWMDIox26QVYR8Y', 'startingPosition': [17, 13], 'endingPosition': [12, 9], 'message': 'When you have figured out the solution, post it back to this url in JSON format. See the exampleSolution for more information.', 'exampleSolution': {'directions': 'ENWNNENWNNS'}, 'map': [[' ', ' ', ' ', ' ', ' ', ' ', ' ', 'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X', ' ', ' ', ' ', ' ', ' '], [' ', 'X', 'X', ' ', 'X', ' ', 'X', ' ', 'X', ' ', 'X', 'X', 'X', 'X', 'X', ' ', 'X', ' ', 'X', ' '], [' ', ' ', 'X', ' ', 'X', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'X', ' ', ' ', ' ', 'X', ' ', 'X', ' '], [' ', ' ', 'X', ' ', 'X', ' ', 'X', 'X', 'X', 'X', ' ', ' ', ' ', ' ', 'X', ' ', 'X', ' ', 'X', ' '], [' ', 'X', 'X', ' ', 'X', ' ', ' ', ' ', ' ', 'X', 'X', 'X', 'X', ' ', 'X', ' ', 'X', ' ', 'X', ' '], [' ', ' ', ' ', ' ', 'X', ' ', 'X', 'X', ' ', 'X', ' ', ' ', 'X', ' ', 'X', ' ', 'X', ' ', 'X', ' '], [' ', 'X', 'X', 'X', 'X', ' ', 'X', ' ', ' ', 'X', 'X', 'X', 'X', ' ', 'X', ' ', 'X', ' ', 'X', ' '], [' ', 'X', ' ', ' ', 'X', ' ', ' ', 'X', 'X', 'X', ' ', ' ', ' ', ' ', ' ', ' ', 'X', ' ', 'X', ' '], [' ', 'X', 'X', 'X', ' ', 'X', ' ', 'X', ' ', ' ', 'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X', ' '], [' ', ' ', ' ', 'X', ' ', ' ', 'X', ' ', 'X', 'X', 'X', ' ', 'B', ' ', ' ', ' ', ' ', ' ', ' ', ' '], [' ', 'X', ' ', 'X', 'X', 'X', ' ', 'X', ' ', ' ', ' ', 'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X', ' '], [' ', ' ', ' ', ' ', ' ', ' ', 'X', ' ', 'X', 'X', 'X', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '], ['X', ' ', 'X', 'X', 'X', ' ', 'X', ' ', ' ', ' ', ' ', 'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X'], [' ', 'X', ' ', ' ', ' ', ' ', 'X', ' ', 'X', 'X', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'A', ' ', ' '], [' ', 'X', ' ', 'X', 'X', 'X', 'X', ' ', ' ', ' ', 'X', 'X', 'X', 'X', 'X', 'X', 'X', ' ', 'X', ' '], ['X', 'X', ' ', 'X', ' ', ' ', ' ', 'X', 'X', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'X', ' ', 'X', ' '], ['X', 'X', ' ', ' ', ' ', 'X', ' ', 'X', ' ', 'X', 'X', 'X', 'X', 'X', 'X', ' ', 'X', ' ', ' ', ' '], ['X', 'X', 'X', 'X', ' ', 'X', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'X', 'X', ' ', 'X'], ['X', 'X', 'X', 'X', ' ', ' ', 'X', ' ', 'X', ' ', 'X', ' ', ' ', 'X', ' ', 'X', ' ', ' ', ' ', 'X'], ['X', 'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X', ' ', ' ', ' ', 'X', 'X', 'X']]}" 236 | ] 237 | }, 238 | { 239 | "cell_type": "code", 240 | "execution_count": null, 241 | "metadata": { 242 | "scrolled": true 243 | }, 244 | "outputs": [], 245 | "source": [ 246 | "chapj = Walker(maze['startingPosition'][0], maze['startingPosition'][1])\n", 247 | "Yeshua = Dungeon_master(maze)\n", 248 | "maze_tour = Yeshua.solve(chapj, visualize=True, verbose=False, silent=False, iterations=-1)" 249 | ] 250 | }, 251 | { 252 | "cell_type": "code", 253 | "execution_count": null, 254 | "metadata": { 255 | "scrolled": true 256 | }, 257 | "outputs": [], 258 | "source": [ 259 | "answer = '{\"directions\" : \"' + maze_tour + '\"}'\n", 260 | "answer" 261 | ] 262 | }, 263 | { 264 | "cell_type": "code", 265 | "execution_count": null, 266 | "metadata": {}, 267 | "outputs": [], 268 | "source": [ 269 | "mazeboturl = baseurl + maze['mazePath']\n", 270 | "mazeboturl" 271 | ] 272 | }, 273 | { 274 | "cell_type": "code", 275 | "execution_count": null, 276 | "metadata": {}, 277 | "outputs": [], 278 | "source": [ 279 | "response = post(mazeboturl, answer)\n", 280 | "print(response)" 281 | ] 282 | }, 283 | { 284 | "cell_type": "markdown", 285 | "metadata": {}, 286 | "source": [ 287 | "# 3. RaceMode" 288 | ] 289 | }, 290 | { 291 | "cell_type": "code", 292 | "execution_count": null, 293 | "metadata": {}, 294 | "outputs": [], 295 | "source": [ 296 | "response = get_maze('https://api.noopschallenge.com/mazebot/race')\n", 297 | "print(response['message'], response['exampleRequest'])" 298 | ] 299 | }, 300 | { 301 | "cell_type": "code", 302 | "execution_count": null, 303 | "metadata": { 304 | "scrolled": true 305 | }, 306 | "outputs": [], 307 | "source": [ 308 | "def start_race() :\n", 309 | " response = post(baseurl + '/mazebot/race/start', '{\"login\" : \"chapmanj\"}')\n", 310 | "\n", 311 | " while response.get('nextMaze') != None :\n", 312 | "\n", 313 | " #get maze\n", 314 | " maze = get_maze(baseurl + response['nextMaze'])\n", 315 | "\n", 316 | " print('attepting: ' + maze['name'])\n", 317 | "\n", 318 | " #solve it\n", 319 | " chapj = Walker(maze['startingPosition'][0], maze['startingPosition'][1])\n", 320 | " Yeshua = Dungeon_master(maze)\n", 321 | " maze_tour = Yeshua.solve(chapj, visualize=False, verbose=False, silent=True)\n", 322 | " answer = '{\"directions\" : \"' + maze_tour + '\"}'\n", 323 | "\n", 324 | " mazeboturl = baseurl + maze['mazePath']\n", 325 | "\n", 326 | " response = post(mazeboturl, answer)\n", 327 | "\n", 328 | " print(response)\n", 329 | " print('---')" 330 | ] 331 | }, 332 | { 333 | "cell_type": "code", 334 | "execution_count": null, 335 | "metadata": {}, 336 | "outputs": [], 337 | "source": [ 338 | "start_race()" 339 | ] 340 | } 341 | ], 342 | "metadata": { 343 | "kernelspec": { 344 | "display_name": "Python 3", 345 | "language": "python", 346 | "name": "python3" 347 | }, 348 | "language_info": { 349 | "codemirror_mode": { 350 | "name": "ipython", 351 | "version": 3 352 | }, 353 | "file_extension": ".py", 354 | "mimetype": "text/x-python", 355 | "name": "python", 356 | "nbconvert_exporter": "python", 357 | "pygments_lexer": "ipython3", 358 | "version": "3.7.3" 359 | } 360 | }, 361 | "nbformat": 4, 362 | "nbformat_minor": 2 363 | } 364 | -------------------------------------------------------------------------------- /canvas-game/js/utils/polyfills.js: -------------------------------------------------------------------------------- 1 | (function(undefined) {function Call(t,l){var n=arguments.length>2?arguments[2]:[];if(!1===IsCallable(t))throw new TypeError(Object.prototype.toString.call(t)+"is not a function.");return t.apply(l,n)}function Get(n,t){return n[t]}function HasProperty(n,r){return r in n}function IsCallable(n){return"function"==typeof n}function ToInteger(n){var i=Number(n);return isNaN(i)?0:1/i===Infinity||1/i==-Infinity||i===Infinity||i===-Infinity?i:(i<0?-1:1)*Math.floor(Math.abs(i))}function ToLength(n){var t=ToInteger(n);return t<=0?0:Math.min(t,Math.pow(2,53)-1)}function ToObject(e){if(null===e||e===undefined)throw TypeError();return Object(e)}function GetV(t,e){return ToObject(t)[e]}function GetMethod(e,n){var r=GetV(e,n);if(null===r||r===undefined)return undefined;if(!1===IsCallable(r))throw new TypeError("Method not callable: "+n);return r}function Type(e){switch(typeof e){case"undefined":return"undefined";case"boolean":return"boolean";case"number":return"number";case"string":return"string";case"symbol":return"symbol";default:return null===e?"null":"Symbol"in this&&e instanceof this.Symbol?"symbol":"object"}}function OrdinaryToPrimitive(r,t){if("string"===t)var e=["toString","valueOf"];else e=["valueOf","toString"];for(var i=0;i1?arguments[1]:undefined;if("object"===Type(e)){if(arguments.length<2)var i="default";else t===String?i="string":t===Number&&(i="number");var r="function"==typeof this.Symbol&&"symbol"==typeof this.Symbol.toPrimitive?GetMethod(e,this.Symbol.toPrimitive):undefined;if(r!==undefined){var n=Call(r,e,[i]);if("object"!==Type(n))return n;throw new TypeError("Cannot convert exotic object to primitive.")}return"default"===i&&(i="number"),OrdinaryToPrimitive(e,i)}return e}function ToString(t){switch(Type(t)){case"symbol":throw new TypeError("Cannot convert a Symbol value to a string");case"object":return ToString(ToPrimitive(t,"string"));default:return String(t)}}if (!("document"in this 2 | )) {"undefined"==typeof WorkerGlobalScope&&"function"!=typeof importScripts&&(this.HTMLDocument?this.Document=this.HTMLDocument:(this.Document=this.HTMLDocument=document.constructor=new Function("return function Document() {}")(),this.Document.prototype=document));}if (!("Element"in this&&"HTMLElement"in this 3 | )) {!function(){function e(){return a--||clearTimeout(t),!(!document.body||document.body.prototype||!/(complete|interactive)/.test(document.readyState))&&(m(document,!0),t&&document.body.prototype&&clearTimeout(t),!!document.body.prototype)}if(window.Element&&!window.HTMLElement)return void(window.HTMLElement=window.Element);window.Element=window.HTMLElement=new Function("return function Element() {}")();var t,n=document.appendChild(document.createElement("body")),o=n.appendChild(document.createElement("iframe")),r=o.contentWindow.document,c=Element.prototype=r.appendChild(r.createElement("*")),d={},m=function(e,t){var n,o,r,c=e.childNodes||[],u=-1;if(1===e.nodeType&&e.constructor!==Element){e.constructor=Element;for(n in d)o=d[n],e[n]=o}for(;r=t&&c[++u];)m(r,t);return e},u=document.getElementsByTagName("*"),i=document.createElement,a=100;c.attachEvent("onpropertychange",function(e){for(var t,n=e.propertyName,o=!d.hasOwnProperty(n),r=c[n],m=d[n],i=-1;t=u[++i];)1===t.nodeType&&(o||t[n]===m)&&(t[n]=r);d[n]=r}),c.constructor=Element,c.hasAttribute||(c.hasAttribute=function l(e){return null!==this.getAttribute(e)}),e()||(document.onreadystatechange=e,t=setInterval(e,25)),document.createElement=function p(e){var t=i(String(e).toLowerCase());return m(t)},document.removeChild(n)}();}if (!("defineProperty"in Object&&function(){try{var e={} 4 | return Object.defineProperty(e,"test",{value:42}),!0}catch(t){return!1}}() 5 | )) {!function(e){var t=Object.prototype.hasOwnProperty("__defineGetter__"),r="A property cannot both have accessors and be writable or have a value";Object.defineProperty=function n(o,i,c){if(e&&(o===window||o===document||o===Element.prototype||o instanceof Element))return e(o,i,c);if(null===o||!(o instanceof Object||"object"==typeof o))throw new TypeError("Object.defineProperty called on non-object");if(!(c instanceof Object))throw new TypeError("Property description must be an object");var a=String(i),f="value"in c||"writable"in c,p="get"in c&&typeof c.get,s="set"in c&&typeof c.set;if(p){if("function"!==p)throw new TypeError("Getter must be a function");if(!t)throw new TypeError("Getters & setters cannot be defined on this javascript engine");if(f)throw new TypeError(r);Object.__defineGetter__.call(o,a,c.get)}else o[a]=c.value;if(s){if("function"!==s)throw new TypeError("Setter must be a function");if(!t)throw new TypeError("Getters & setters cannot be defined on this javascript engine");if(f)throw new TypeError(r);Object.__defineSetter__.call(o,a,c.set)}return"value"in c&&(o[a]=c.value),o}}(Object.defineProperty);}function CreateMethodProperty(e,r,t){var a={value:t,writable:!0,enumerable:!1,configurable:!0};Object.defineProperty(e,r,a)}if (!("forEach"in Array.prototype 6 | )) {CreateMethodProperty(Array.prototype,"forEach",function r(t){var e=ToObject(this),n=e instanceof String?e.split(""):e,o=ToLength(Get(e,"length"));if(!1===IsCallable(t))throw new TypeError(t+" is not a function");for(var a=arguments.length>1?arguments[1]:undefined,i=0;in&&(r.length=n)}var r=A(n),o=0;return function(n,t){r[o++]=n,r[o++]=t,2===o&&rn.nextTick(e)}}function l(n,t){var e,r,u,c,f=0;if(!n)throw h(Q);var a=n[rn[q][z]];if(o(a))r=a.call(n);else{if(!o(n.next)){if(i(n,A)){for(e=n.length;f-1?e:t}function l(t,e){e=e||{};var r=e.body;if(t instanceof l){if(t.bodyUsed)throw new TypeError("Already read");this.url=t.url,this.credentials=t.credentials,e.headers||(this.headers=new n(t.headers)),this.method=t.method,this.mode=t.mode,r||null==t._bodyInit||(r=t._bodyInit,t.bodyUsed=!0)}else this.url=String(t);if(this.credentials=e.credentials||this.credentials||"omit",!e.headers&&this.headers||(this.headers=new n(e.headers)),this.method=y(e.method||this.method||"GET"),this.mode=e.mode||this.mode||null,this.referrer=null,("GET"===this.method||"HEAD"===this.method)&&r)throw new TypeError("Body not allowed for GET or HEAD requests");this._initBody(r)}function p(t){var e=new FormData;return t.trim().split("&").forEach(function(t){if(t){var r=t.split("="),o=r.shift().replace(/\+/g," "),n=r.join("=").replace(/\+/g," ");e.append(decodeURIComponent(o),decodeURIComponent(n))}}),e}function c(t){var e=new n;return t.replace(/\r?\n[\t ]+/g," ").split(/\r?\n/).forEach(function(t){var r=t.split(":"),o=r.shift().trim();if(o){var n=r.join(":").trim();e.append(o,n)}}),e}function b(t,e){e||(e={}),this.type="default",this.status=e.status===undefined?200:e.status,this.ok=this.status>=200&&this.status<300,this.statusText="statusText"in e?e.statusText:"OK",this.headers=new n(e.headers),this.url=e.url||"",this._initBody(t)}var m={searchParams:"URLSearchParams"in t,iterable:"Symbol"in t&&"iterator"in Symbol,blob:"FileReader"in t&&"Blob"in t&&function(){try{return new Blob,!0}catch(t){return!1}}(),formData:"FormData"in t,arrayBuffer:"ArrayBuffer"in t};if(m.arrayBuffer)var w=["[object Int8Array]","[object Uint8Array]","[object Uint8ClampedArray]","[object Int16Array]","[object Uint16Array]","[object Int32Array]","[object Uint32Array]","[object Float32Array]","[object Float64Array]"],v=function(t){return t&&DataView.prototype.isPrototypeOf(t)},B=ArrayBuffer.isView||function(t){return t&&w.indexOf(Object.prototype.toString.call(t))>-1};n.prototype.append=function(t,o){t=e(t),o=r(o);var n=this.map[t];this.map[t]=n?n+","+o:o},n.prototype["delete"]=function(t){delete this.map[e(t)]},n.prototype.get=function(t){return t=e(t),this.has(t)?this.map[t]:null},n.prototype.has=function(t){return this.map.hasOwnProperty(e(t))},n.prototype.set=function(t,o){this.map[e(t)]=r(o)},n.prototype.forEach=function(t,e){for(var r in this.map)this.map.hasOwnProperty(r)&&t.call(e,this.map[r],r,this)},n.prototype.keys=function(){var t=[];return this.forEach(function(e,r){t.push(r)}),o(t)},n.prototype.values=function(){var t=[];return this.forEach(function(e){t.push(e)}),o(t)},n.prototype.entries=function(){var t=[];return this.forEach(function(e,r){t.push([r,e])}),o(t)},m.iterable&&(n.prototype[Symbol.iterator]=n.prototype.entries);var _=["DELETE","GET","HEAD","OPTIONS","POST","PUT"];l.prototype.clone=function(){return new l(this,{body:this._bodyInit})},d.call(l.prototype),d.call(b.prototype),b.prototype.clone=function(){return new b(this._bodyInit,{status:this.status,statusText:this.statusText,headers:new n(this.headers),url:this.url})},b.error=function(){var t=new b(null,{status:0,statusText:""});return t.type="error",t};var A=[301,302,303,307,308];b.redirect=function(t,e){if(-1===A.indexOf(e))throw new RangeError("Invalid status code");return new b(null,{status:e,headers:{location:t}})},t.Headers=n,t.Request=l,t.Response=b,t.fetch=function(t,e){return new Promise(function(r,o){var n=new l(t,e),i=new XMLHttpRequest;i.onload=function(){var t={status:i.status,statusText:i.statusText,headers:c(i.getAllResponseHeaders()||"")};t.url="responseURL"in i?i.responseURL:t.headers.get("X-Request-URL");var e="response"in i?i.response:i.responseText;r(new b(e,t))},i.onerror=function(){o(new TypeError("Network request failed"))},i.ontimeout=function(){o(new TypeError("Network request failed"))},i.open(n.method,n.url,!0),"include"===n.credentials?i.withCredentials=!0:"omit"===n.credentials&&(i.withCredentials=!1),"responseType"in i&&m.blob&&(i.responseType="blob"),n.headers.forEach(function(t,e){i.setRequestHeader(e,t)}),i.send("undefined"==typeof n._bodyInit?null:n._bodyInit)})},t.fetch.polyfill=!0}("undefined"!=typeof self?self:this);}}).call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {}); 14 | --------------------------------------------------------------------------------