From 5bedb919d74f9752b1c960b7102ed1c3e9256ec3 Mon Sep 17 00:00:00 2001 From: maxwellpalance Date: Tue, 16 Jun 2026 07:44:02 -0700 Subject: [PATCH 1/8] MazeChallenge V1 --- .../maze-challenge/maze-challenge.js | 347 ++++++++++++++++++ 1 file changed, 347 insertions(+) create mode 100644 src/procedures/maze-challenge/maze-challenge.js diff --git a/src/procedures/maze-challenge/maze-challenge.js b/src/procedures/maze-challenge/maze-challenge.js new file mode 100644 index 00000000..2d782025 --- /dev/null +++ b/src/procedures/maze-challenge/maze-challenge.js @@ -0,0 +1,347 @@ +/** + * The MazeChallenge Service provides the gameplay and evaluation of randomized maze puzzles. + * + * @service + * @category Games + */ + +"use strict"; + +const GENERATED_MAZES = Object.create(null); +let NEXT_MAZE_ID = 1; +const LEVEL_SIZES = { + easy: 9, + medium: 13, + hard: 17, +}; +const DELTAS = { + U: { row: -1, col: 0 }, + D: { row: 1, col: 0 }, + L: { row: 0, col: -1 }, + R: { row: 0, col: 1 }, +}; + +const MazeChallenge = {}; + +/** + * Generate and return a fresh maze for the given difficulty level. + * @param {String=} level Level to load: easy, medium, or hard + * @returns {Object} maze data for drawing + */ +MazeChallenge.getMaze = function (level) { + const maze = generateMaze(level); + + GENERATED_MAZES[maze.mazeId] = maze; + + return { + mazeId: maze.mazeId, + level: maze.level, + rows: maze.rows, + cols: maze.cols, + grid: maze.grid, + startRow: maze.startRow, + startCol: maze.startCol, + goalRow: maze.goalRow, + goalCol: maze.goalCol, + optimalLength: maze.optimalPath.length, + message: "Loaded " + maze.level + " maze.", + }; +}; + +/** + * Evaluate a submitted path for a maze. + * @param {String} mazeId ID returned by getMaze + * @param {String} path Path using U, D, L, and R + * @returns {Object} score and feedback for the submitted path + */ +MazeChallenge.evaluatePath = function (mazeId, path) { + const maze = GENERATED_MAZES[mazeId]; + + if (typeof path !== "string") { + return { + score: 0, + message: "Path can only contain U, D, L, and R.", + }; + } + + path = path.toUpperCase().replace(/[ ,]/g, ""); + + if (!maze) { + return { + score: 0, + message: "Unknown maze ID.", + }; + } + + if (/[^UDLR]/.test(path)) { + return { + score: 0, + message: "Path can only contain U, D, L, and R.", + }; + } + + if (path.length === 0) { + return { + score: 0, + message: "Enter a path using U, D, L, and R.", + }; + } + + const result = simulatePath(maze, path); + + if (!result.valid) { + return { + score: Math.min(250, result.validSteps * 15), + message: result.message, + }; + } + + if (!result.reachedGoal) { + return { + score: Math.min(400, result.validSteps * 20), + message: "Your path is valid so far, but it does not reach the goal.", + }; + } + + if (path.length === maze.optimalPath.length) { + return { + score: 1000, + message: "Correct. You reached the goal with an optimal path.", + }; + } + + return { + score: Math.max(500, 800 - (path.length - maze.optimalPath.length) * 20), + message: "You reached the goal, but your path was longer than optimal.", + }; +}; + +/** + * Get a suggested next move for the current path. + * @param {String} mazeId ID returned by getMaze + * @param {String} currentPath Current path using U, D, L, and R + * @returns {Object} simple hint data + */ +MazeChallenge.getHint = function (mazeId, currentPath) { + const maze = GENERATED_MAZES[mazeId]; + + if (!maze) { + return { + nextMove: "", + message: "Unknown maze ID.", + }; + } + + if (typeof currentPath !== "string") { + return { + nextMove: "", + message: "Path can only contain U, D, L, and R.", + }; + } + + currentPath = currentPath.toUpperCase().replace(/[ ,]/g, ""); + + if (/[^UDLR]/.test(currentPath)) { + return { + nextMove: "", + message: "Path can only contain U, D, L, and R.", + }; + } + + const result = simulatePath(maze, currentPath); + + if (!result.valid) { + return { + nextMove: "", + message: result.message, + }; + } + + if (result.row === maze.goalRow && result.col === maze.goalCol) { + return { + nextMove: "", + message: "You are already at the goal.", + }; + } + + const pathToGoal = shortestPath( + maze, + result.row, + result.col, + maze.goalRow, + maze.goalCol, + ); + const nextMove = pathToGoal[0] || ""; + const names = { U: "up", D: "down", L: "left", R: "right" }; + + return { + nextMove: nextMove, + message: nextMove + ? "Try moving " + names[nextMove] + " next." + : "No hint is available.", + }; +}; + +function generateMaze(level) { + level = typeof level === "string" ? level.toLowerCase() : "easy"; + level = LEVEL_SIZES[level] ? level : "easy"; + + const size = LEVEL_SIZES[level]; + const grid = Array.from({ length: size }, function () { + return Array(size).fill("#"); + }); + const stack = [{ row: 0, col: 0 }]; + const visited = { "0,0": true }; + + grid[0][0] = "."; + + while (stack.length > 0) { + const current = stack[stack.length - 1]; + const neighbors = [ + { row: current.row - 2, col: current.col }, + { row: current.row + 2, col: current.col }, + { row: current.row, col: current.col - 2 }, + { row: current.row, col: current.col + 2 }, + ].filter(function (next) { + return next.row >= 0 && + next.row < size && + next.col >= 0 && + next.col < size && + !visited[next.row + "," + next.col]; + }); + + if (neighbors.length === 0) { + stack.pop(); + continue; + } + + const next = neighbors[Math.floor(Math.random() * neighbors.length)]; + const middleRow = current.row + (next.row - current.row) / 2; + const middleCol = current.col + (next.col - current.col) / 2; + + grid[middleRow][middleCol] = "."; + grid[next.row][next.col] = "."; + visited[next.row + "," + next.col] = true; + stack.push(next); + } + + const goal = size % 2 === 0 ? size - 2 : size - 1; + + grid[0][0] = "S"; + grid[goal][goal] = "G"; + + const maze = { + mazeId: "maze-" + NEXT_MAZE_ID++, + level: level, + rows: size, + cols: size, + grid: grid.map(function (row) { + return row.join(""); + }), + startRow: 0, + startCol: 0, + goalRow: goal, + goalCol: goal, + optimalPath: "", + }; + + maze.optimalPath = shortestPath( + maze, + maze.startRow, + maze.startCol, + maze.goalRow, + maze.goalCol, + ); + + return maze; +} + +function simulatePath(maze, path) { + let row = maze.startRow; + let col = maze.startCol; + + for (let i = 0; i < path.length; i++) { + const delta = DELTAS[path[i]]; + const nextRow = row + delta.row; + const nextCol = col + delta.col; + const step = i + 1; + + if ( + nextRow < 0 || + nextRow >= maze.rows || + nextCol < 0 || + nextCol >= maze.cols + ) { + return { + valid: false, + row: row, + col: col, + validSteps: i, + message: "Your path leaves the maze at step " + step + ".", + }; + } + + if (maze.grid[nextRow][nextCol] === "#") { + return { + valid: false, + row: row, + col: col, + validSteps: i, + message: "Your path hits a wall at step " + step + ".", + }; + } + + row = nextRow; + col = nextCol; + } + + return { + valid: true, + reachedGoal: row === maze.goalRow && col === maze.goalCol, + row: row, + col: col, + validSteps: path.length, + }; +} + +function shortestPath(maze, startRow, startCol, goalRow, goalCol) { + const queue = [{ row: startRow, col: startCol, path: "" }]; + const visited = {}; + + visited[startRow + "," + startCol] = true; + + while (queue.length > 0) { + const current = queue.shift(); + + if (current.row === goalRow && current.col === goalCol) { + return current.path; + } + + for (const move in DELTAS) { + const delta = DELTAS[move]; + const row = current.row + delta.row; + const col = current.col + delta.col; + const key = row + "," + col; + + if ( + !visited[key] && + row >= 0 && + row < maze.rows && + col >= 0 && + col < maze.cols && + maze.grid[row][col] !== "#" + ) { + visited[key] = true; + queue.push({ + row: row, + col: col, + path: current.path + move, + }); + } + } + } + + return ""; +} + +module.exports = MazeChallenge; From cc2235ca7ae266249ed5155d45dd8ace0c4db56d Mon Sep 17 00:00:00 2001 From: maxwellpalance Date: Wed, 24 Jun 2026 10:31:16 -0700 Subject: [PATCH 2/8] Finalized V1 MazeChallenge --- .../maze-challenge/maze-challenge.js | 181 +++++++++++------- 1 file changed, 109 insertions(+), 72 deletions(-) diff --git a/src/procedures/maze-challenge/maze-challenge.js b/src/procedures/maze-challenge/maze-challenge.js index 2d782025..2cffa839 100644 --- a/src/procedures/maze-challenge/maze-challenge.js +++ b/src/procedures/maze-challenge/maze-challenge.js @@ -11,8 +11,7 @@ const GENERATED_MAZES = Object.create(null); let NEXT_MAZE_ID = 1; const LEVEL_SIZES = { easy: 9, - medium: 13, - hard: 17, + hard: 13, }; const DELTAS = { U: { row: -1, col: 0 }, @@ -25,7 +24,7 @@ const MazeChallenge = {}; /** * Generate and return a fresh maze for the given difficulty level. - * @param {String=} level Level to load: easy, medium, or hard + * @param {String=} level Level to load: easy or hard * @returns {Object} maze data for drawing */ MazeChallenge.getMaze = function (level) { @@ -33,19 +32,19 @@ MazeChallenge.getMaze = function (level) { GENERATED_MAZES[maze.mazeId] = maze; - return { - mazeId: maze.mazeId, - level: maze.level, - rows: maze.rows, - cols: maze.cols, - grid: maze.grid, - startRow: maze.startRow, - startCol: maze.startCol, - goalRow: maze.goalRow, - goalCol: maze.goalCol, - optimalLength: maze.optimalPath.length, - message: "Loaded " + maze.level + " maze.", - }; + return [ + maze.mazeId, + maze.level, + maze.rows, + maze.cols, + maze.grid, + maze.startRow, + maze.startCol, + maze.goalRow, + maze.goalCol, + maze.optimalPath.length, + "Loaded " + maze.level + " maze.", + ]; }; /** @@ -58,62 +57,62 @@ MazeChallenge.evaluatePath = function (mazeId, path) { const maze = GENERATED_MAZES[mazeId]; if (typeof path !== "string") { - return { - score: 0, - message: "Path can only contain U, D, L, and R.", - }; + return [ + 0, + "Path can only contain U, D, L, and R.", + ]; } path = path.toUpperCase().replace(/[ ,]/g, ""); if (!maze) { - return { - score: 0, - message: "Unknown maze ID.", - }; + return [ + 0, + "Unknown maze ID.", + ]; } if (/[^UDLR]/.test(path)) { - return { - score: 0, - message: "Path can only contain U, D, L, and R.", - }; + return [ + 0, + "Path can only contain U, D, L, and R.", + ]; } if (path.length === 0) { - return { - score: 0, - message: "Enter a path using U, D, L, and R.", - }; + return [ + 0, + "Enter a path using U, D, L, and R.", + ]; } const result = simulatePath(maze, path); if (!result.valid) { - return { - score: Math.min(250, result.validSteps * 15), - message: result.message, - }; + return [ + Math.min(250, result.validSteps * 15), + result.message, + ]; } if (!result.reachedGoal) { - return { - score: Math.min(400, result.validSteps * 20), - message: "Your path is valid so far, but it does not reach the goal.", - }; + return [ + Math.min(400, result.validSteps * 20), + "Your path is valid so far, but it does not reach the goal.", + ]; } if (path.length === maze.optimalPath.length) { - return { - score: 1000, - message: "Correct. You reached the goal with an optimal path.", - }; + return [ + 1000, + "Correct. You reached the goal with an optimal path.", + ]; } - return { - score: Math.max(500, 800 - (path.length - maze.optimalPath.length) * 20), - message: "You reached the goal, but your path was longer than optimal.", - }; + return [ + Math.max(500, 800 - (path.length - maze.optimalPath.length) * 20), + "You reached the goal, but your path was longer than optimal.", + ]; }; /** @@ -126,42 +125,42 @@ MazeChallenge.getHint = function (mazeId, currentPath) { const maze = GENERATED_MAZES[mazeId]; if (!maze) { - return { - nextMove: "", - message: "Unknown maze ID.", - }; + return [ + "", + "Unknown maze ID.", + ]; } if (typeof currentPath !== "string") { - return { - nextMove: "", - message: "Path can only contain U, D, L, and R.", - }; + return [ + "", + "Path can only contain U, D, L, and R.", + ]; } currentPath = currentPath.toUpperCase().replace(/[ ,]/g, ""); if (/[^UDLR]/.test(currentPath)) { - return { - nextMove: "", - message: "Path can only contain U, D, L, and R.", - }; + return [ + "", + "Path can only contain U, D, L, and R.", + ]; } const result = simulatePath(maze, currentPath); if (!result.valid) { - return { - nextMove: "", - message: result.message, - }; + return [ + "", + result.message, + ]; } if (result.row === maze.goalRow && result.col === maze.goalCol) { - return { - nextMove: "", - message: "You are already at the goal.", - }; + return [ + "", + "You are already at the goal.", + ]; } const pathToGoal = shortestPath( @@ -174,12 +173,12 @@ MazeChallenge.getHint = function (mazeId, currentPath) { const nextMove = pathToGoal[0] || ""; const names = { U: "up", D: "down", L: "left", R: "right" }; - return { - nextMove: nextMove, - message: nextMove + return [ + nextMove, + nextMove ? "Try moving " + names[nextMove] + " next." : "No hint is available.", - }; + ]; }; function generateMaze(level) { @@ -225,6 +224,8 @@ function generateMaze(level) { stack.push(next); } + addLoopsToGrid(grid, size, level); + const goal = size % 2 === 0 ? size - 2 : size - 1; grid[0][0] = "S"; @@ -256,6 +257,42 @@ function generateMaze(level) { return maze; } +function addLoopsToGrid(grid, size, level) { + const loopCounts = { easy: 3, hard: 8 }; + const target = loopCounts[level] || loopCounts.easy; + const maxAttempts = target * 50; + let opened = 0; + let attempts = 0; + + while (opened < target && attempts < maxAttempts) { + attempts++; + + const row = 1 + Math.floor(Math.random() * (size - 2)); + const col = 1 + Math.floor(Math.random() * (size - 2)); + let openNeighbors = 0; + + if (grid[row][col] !== "#") { + continue; + } + + [ + grid[row - 1][col], + grid[row + 1][col], + grid[row][col - 1], + grid[row][col + 1], + ].forEach(function (value) { + if (value === "." || value === "S" || value === "G") { + openNeighbors++; + } + }); + + if (openNeighbors >= 2) { + grid[row][col] = "."; + opened++; + } + } +} + function simulatePath(maze, path) { let row = maze.startRow; let col = maze.startCol; From 4442502284c3d5175c13aa1a92db1284d3c6d504 Mon Sep 17 00:00:00 2001 From: maxwellpalance Date: Tue, 30 Jun 2026 07:55:35 -0700 Subject: [PATCH 3/8] Doc fix --- src/procedures/maze-challenge/maze-challenge.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/procedures/maze-challenge/maze-challenge.js b/src/procedures/maze-challenge/maze-challenge.js index 2cffa839..1d801fb0 100644 --- a/src/procedures/maze-challenge/maze-challenge.js +++ b/src/procedures/maze-challenge/maze-challenge.js @@ -25,7 +25,7 @@ const MazeChallenge = {}; /** * Generate and return a fresh maze for the given difficulty level. * @param {String=} level Level to load: easy or hard - * @returns {Object} maze data for drawing + * @returns {List} maze data for drawing */ MazeChallenge.getMaze = function (level) { const maze = generateMaze(level); @@ -51,7 +51,7 @@ MazeChallenge.getMaze = function (level) { * Evaluate a submitted path for a maze. * @param {String} mazeId ID returned by getMaze * @param {String} path Path using U, D, L, and R - * @returns {Object} score and feedback for the submitted path + * @returns {List} score and feedback for the submitted path */ MazeChallenge.evaluatePath = function (mazeId, path) { const maze = GENERATED_MAZES[mazeId]; @@ -119,7 +119,7 @@ MazeChallenge.evaluatePath = function (mazeId, path) { * Get a suggested next move for the current path. * @param {String} mazeId ID returned by getMaze * @param {String} currentPath Current path using U, D, L, and R - * @returns {Object} simple hint data + * @returns {List} simple hint data */ MazeChallenge.getHint = function (mazeId, currentPath) { const maze = GENERATED_MAZES[mazeId]; From 9a5ab522b115464f35f711713edfd8819dc049e4 Mon Sep 17 00:00:00 2001 From: maxwellpalance Date: Tue, 30 Jun 2026 08:37:43 -0700 Subject: [PATCH 4/8] Correction --- src/procedures/maze-challenge/maze-challenge.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/procedures/maze-challenge/maze-challenge.js b/src/procedures/maze-challenge/maze-challenge.js index 1d801fb0..b00714fa 100644 --- a/src/procedures/maze-challenge/maze-challenge.js +++ b/src/procedures/maze-challenge/maze-challenge.js @@ -25,7 +25,7 @@ const MazeChallenge = {}; /** * Generate and return a fresh maze for the given difficulty level. * @param {String=} level Level to load: easy or hard - * @returns {List} maze data for drawing + * @returns {Array} maze data for drawing */ MazeChallenge.getMaze = function (level) { const maze = generateMaze(level); @@ -51,7 +51,7 @@ MazeChallenge.getMaze = function (level) { * Evaluate a submitted path for a maze. * @param {String} mazeId ID returned by getMaze * @param {String} path Path using U, D, L, and R - * @returns {List} score and feedback for the submitted path + * @returns {Array} score and feedback for the submitted path */ MazeChallenge.evaluatePath = function (mazeId, path) { const maze = GENERATED_MAZES[mazeId]; @@ -119,7 +119,7 @@ MazeChallenge.evaluatePath = function (mazeId, path) { * Get a suggested next move for the current path. * @param {String} mazeId ID returned by getMaze * @param {String} currentPath Current path using U, D, L, and R - * @returns {List} simple hint data + * @returns {Array} simple hint data */ MazeChallenge.getHint = function (mazeId, currentPath) { const maze = GENERATED_MAZES[mazeId]; From 86bb0e98d7bd8d58a3356bf624f8425e26e7f652 Mon Sep 17 00:00:00 2001 From: maxwellpalance Date: Tue, 30 Jun 2026 09:27:55 -0700 Subject: [PATCH 5/8] Simplification after testing --- .../maze-challenge/maze-challenge.js | 45 ++++++------------- 1 file changed, 14 insertions(+), 31 deletions(-) diff --git a/src/procedures/maze-challenge/maze-challenge.js b/src/procedures/maze-challenge/maze-challenge.js index b00714fa..53e78b89 100644 --- a/src/procedures/maze-challenge/maze-challenge.js +++ b/src/procedures/maze-challenge/maze-challenge.js @@ -118,49 +118,38 @@ MazeChallenge.evaluatePath = function (mazeId, path) { /** * Get a suggested next move for the current path. * @param {String} mazeId ID returned by getMaze - * @param {String} currentPath Current path using U, D, L, and R - * @returns {Array} simple hint data + * @param {String=} currentPath Optional current path using U, D, L, and R + * @returns {String} next move, one of U, D, L, R, or an empty string if no move is available */ MazeChallenge.getHint = function (mazeId, currentPath) { const maze = GENERATED_MAZES[mazeId]; if (!maze) { - return [ - "", - "Unknown maze ID.", - ]; + return ""; + } + + if (currentPath === undefined || currentPath === null) { + currentPath = ""; } if (typeof currentPath !== "string") { - return [ - "", - "Path can only contain U, D, L, and R.", - ]; + return ""; } currentPath = currentPath.toUpperCase().replace(/[ ,]/g, ""); if (/[^UDLR]/.test(currentPath)) { - return [ - "", - "Path can only contain U, D, L, and R.", - ]; + return ""; } const result = simulatePath(maze, currentPath); if (!result.valid) { - return [ - "", - result.message, - ]; + return ""; } if (result.row === maze.goalRow && result.col === maze.goalCol) { - return [ - "", - "You are already at the goal.", - ]; + return ""; } const pathToGoal = shortestPath( @@ -171,14 +160,8 @@ MazeChallenge.getHint = function (mazeId, currentPath) { maze.goalCol, ); const nextMove = pathToGoal[0] || ""; - const names = { U: "up", D: "down", L: "left", R: "right" }; - return [ - nextMove, - nextMove - ? "Try moving " + names[nextMove] + " next." - : "No hint is available.", - ]; + return nextMove; }; function generateMaze(level) { @@ -226,7 +209,7 @@ function generateMaze(level) { addLoopsToGrid(grid, size, level); - const goal = size % 2 === 0 ? size - 2 : size - 1; + const goal = size - 1; grid[0][0] = "S"; grid[goal][goal] = "G"; @@ -281,7 +264,7 @@ function addLoopsToGrid(grid, size, level) { grid[row][col - 1], grid[row][col + 1], ].forEach(function (value) { - if (value === "." || value === "S" || value === "G") { + if (value === ".") { openNeighbors++; } }); From 5c0e94448b2a0aa327dc3c2becdf46dc942802ec Mon Sep 17 00:00:00 2001 From: maxwellpalance Date: Thu, 23 Jul 2026 08:53:42 -0700 Subject: [PATCH 6/8] Updated backend --- .../maze-challenge/maze-challenge.js | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) diff --git a/src/procedures/maze-challenge/maze-challenge.js b/src/procedures/maze-challenge/maze-challenge.js index 53e78b89..8a601aca 100644 --- a/src/procedures/maze-challenge/maze-challenge.js +++ b/src/procedures/maze-challenge/maze-challenge.js @@ -20,6 +20,16 @@ const DELTAS = { R: { row: 0, col: 1 }, }; +const GAME_STATE = { + roundNumber: 0, + currentMazeId: null, + roundStartedAt: 0, + roundEndsAt: 0, + durationSeconds: 0, + totals: Object.create(null), + submittedThisRound: Object.create(null), +}; + const MazeChallenge = {}; /** @@ -164,6 +174,120 @@ MazeChallenge.getHint = function (mazeId, currentPath) { return nextMove; }; +/** + * Start a timed multiplayer round using an already generated maze. + * @param {String} mazeId ID returned by getMaze + * @param {Number=} durationSeconds Number of seconds for the round + * @returns {Array} round number, duration, and leaderboard text + */ +MazeChallenge.startRoundForMaze = function (mazeId, durationSeconds) { + durationSeconds = Number(durationSeconds); + + if (!Number.isFinite(durationSeconds) || durationSeconds <= 0) { + durationSeconds = 60; + } + + if (!GENERATED_MAZES[mazeId]) { + return [ + 0, + durationSeconds, + "Unknown maze ID.", + ]; + } + + GAME_STATE.roundNumber++; + GAME_STATE.currentMazeId = mazeId; + GAME_STATE.roundStartedAt = Date.now(); + GAME_STATE.roundEndsAt = GAME_STATE.roundStartedAt + durationSeconds * 1000; + GAME_STATE.durationSeconds = durationSeconds; + GAME_STATE.submittedThisRound = Object.create(null); + + return [ + GAME_STATE.roundNumber, + durationSeconds, + formatLeaderboard(), + ]; +}; + +/** + * Submit a player's path for the current timed round. + * @param {Number} roundNumber Round number being submitted + * @param {String} playerName Player name + * @param {String} path Path using U, D, L, and R + * @returns {Array} round score, total score, feedback message, and leaderboard text + */ +MazeChallenge.submitScore = function ( + roundNumber, + playerName, + path +) { + if (typeof playerName !== "string" || playerName.trim().length === 0) { + playerName = "Anonymous"; + } else { + playerName = playerName.trim(); + } + + roundNumber = Number(roundNumber); + + if (!(playerName in GAME_STATE.totals)) { + GAME_STATE.totals[playerName] = 0; + } + + if (roundNumber !== GAME_STATE.roundNumber) { + return [ + 0, + GAME_STATE.totals[playerName], + "This round is no longer active.", + formatLeaderboard(), + ]; + } + + if (Date.now() > GAME_STATE.roundEndsAt) { + return [ + 0, + GAME_STATE.totals[playerName], + "Round is over.", + formatLeaderboard(), + ]; + } + + if (GAME_STATE.submittedThisRound[playerName]) { + return [ + 0, + GAME_STATE.totals[playerName], + "Already submitted for this round.", + formatLeaderboard(), + ]; + } + + const result = MazeChallenge.evaluatePath(GAME_STATE.currentMazeId, path); + const baseScore = result[0]; + const message = result[1]; + const elapsedTime = Math.floor((Date.now() - GAME_STATE.roundStartedAt) / 1000); + + let roundScore = baseScore - elapsedTime; + + roundScore = Math.max(0, roundScore); + + GAME_STATE.totals[playerName] += roundScore; + GAME_STATE.submittedThisRound[playerName] = true; + + return [ + roundScore, + GAME_STATE.totals[playerName], + message, + formatLeaderboard(), + ]; +}; + +/** + * Get the current cumulative leaderboard. + * @returns {String} leaderboard text + */ +MazeChallenge.getLeaderboard = function () { + return formatLeaderboard(); +}; + function generateMaze(level) { level = typeof level === "string" ? level.toLowerCase() : "easy"; level = LEVEL_SIZES[level] ? level : "easy"; @@ -364,4 +488,28 @@ function shortestPath(maze, startRow, startCol, goalRow, goalCol) { return ""; } +function formatLeaderboard() { + const playerNames = Object.keys(GAME_STATE.totals); + + if (playerNames.length === 0) { + return "Round " + GAME_STATE.roundNumber + " | No scores yet."; + } + + playerNames.sort(function (a, b) { + if (GAME_STATE.totals[b] !== GAME_STATE.totals[a]) { + return GAME_STATE.totals[b] - GAME_STATE.totals[a]; + } + + return a.localeCompare(b); + }); + + let text = "Round " + GAME_STATE.roundNumber; + + playerNames.forEach(function (name) { + text += " | " + name + ": " + GAME_STATE.totals[name]; + }); + + return text; +} + module.exports = MazeChallenge; From 9a932edda5da00037ad4dc4c044d67f9fd318752 Mon Sep 17 00:00:00 2001 From: maxwellpalance Date: Fri, 31 Jul 2026 05:22:24 -0500 Subject: [PATCH 7/8] mazeId fix --- .../maze-challenge/maze-challenge.js | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/procedures/maze-challenge/maze-challenge.js b/src/procedures/maze-challenge/maze-challenge.js index 8a601aca..d424ff6a 100644 --- a/src/procedures/maze-challenge/maze-challenge.js +++ b/src/procedures/maze-challenge/maze-challenge.js @@ -57,6 +57,45 @@ MazeChallenge.getMaze = function (level) { ]; }; +/** + * Get maze data for an already generated maze. + * @param {String} mazeId ID returned by getMaze + * @returns {Array} maze data for drawing + */ +MazeChallenge.getMazeById = function (mazeId) { + const maze = GENERATED_MAZES[mazeId]; + + if (!maze) { + return [ + "", + "unknown", + 0, + 0, + [], + 0, + 0, + 0, + 0, + 0, + "Unknown maze ID.", + ]; + } + + return [ + maze.mazeId, + maze.level, + maze.rows, + maze.cols, + maze.grid, + maze.startRow, + maze.startCol, + maze.goalRow, + maze.goalCol, + maze.optimalPath.length, + "Loaded " + maze.level + " maze.", + ]; +}; + /** * Evaluate a submitted path for a maze. * @param {String} mazeId ID returned by getMaze From 058764143ab3c258147d92022f08f9fb20508cf0 Mon Sep 17 00:00:00 2001 From: Format Bot Date: Tue, 22 Sep 2026 23:11:21 +0000 Subject: [PATCH 8/8] Fix code formatting --- src/procedures/maze-challenge/maze-challenge.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/procedures/maze-challenge/maze-challenge.js b/src/procedures/maze-challenge/maze-challenge.js index d424ff6a..25333bfd 100644 --- a/src/procedures/maze-challenge/maze-challenge.js +++ b/src/procedures/maze-challenge/maze-challenge.js @@ -258,7 +258,7 @@ MazeChallenge.startRoundForMaze = function (mazeId, durationSeconds) { MazeChallenge.submitScore = function ( roundNumber, playerName, - path + path, ) { if (typeof playerName !== "string" || playerName.trim().length === 0) { playerName = "Anonymous"; @@ -302,7 +302,9 @@ MazeChallenge.submitScore = function ( const result = MazeChallenge.evaluatePath(GAME_STATE.currentMazeId, path); const baseScore = result[0]; const message = result[1]; - const elapsedTime = Math.floor((Date.now() - GAME_STATE.roundStartedAt) / 1000); + const elapsedTime = Math.floor( + (Date.now() - GAME_STATE.roundStartedAt) / 1000, + ); let roundScore = baseScore - elapsedTime;