Skip to content

Commit

Permalink
Add support for converting grid format to raw format.
Browse files Browse the repository at this point in the history
  • Loading branch information
We-Gold committed Jul 15, 2023
1 parent 1c029c1 commit 58cd823
Show file tree
Hide file tree
Showing 5 changed files with 174 additions and 41 deletions.
20 changes: 15 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,16 +207,26 @@ const supersampled = supersampleMaze(rawMaze, 2)
// Output: sample maze structure but twice the rows and columns
```

**Raw -> Filled in walls**
**Raw <-> Grid Maze Format**

```js
// `rawMaze` already generated

// Add wall cells instead of booleans (and upscale to a factor of 2)
const walledMaze = fillWallsWithCells(rawMaze, 1, 0, 2)
// Replace wall properties with open/wall cells (and upscale to a factor of 2)
const gridMaze = convertRawToGridFormat(rawMaze, 2)

// Output: same maze structure but walls are represented by 1
// and cells by 0, and twice the number of cells
// Output: same maze structure but walls are represented by true
// and cells by false, and a different number of cells

// Convert back to Raw (and tell it the upscale factor)
const rawMaze2 = convertGridToRawFormat(gridMaze, 2)

// Output: the original `rawMaze`

// Convert points from raw to grid and back
const rawPoint = [2, 2]
const gridPoint = convertRawToGridPoint(rawPoint)
const originalPoint = convertGridToRawPoint(gridPoint)
```

**Raw -> Raw (Braided)**
Expand Down
110 changes: 83 additions & 27 deletions lib/convert-maze.js
Original file line number Diff line number Diff line change
Expand Up @@ -349,32 +349,28 @@ export const supersampleMaze = (rawMaze, factor = 2) => {
* Optionally supersample the maze (cannot operate on a supersampled maze).
*
* @param {number[][]} rawMaze The raw maze stored as a matrix of binary numbers
* @param {any} [wallSymbol] The object that symbolizes a wall
* @param {any} [cellSymbol] The object that symbolizes an empty cell
* @param {number} [supersampleFactor] The factor to scale by
* @returns {any[][]} The new maze with wall cells
* @returns {boolean[][]} The new maze in grid format
*/
export const fillWallsWithCells = (
rawMaze,
wallSymbol = 1,
cellSymbol = 0,
supersampleFactor = 1
) => {
export const convertRawToGridFormat = (rawMaze, supersampleFactor = 1) => {
const cellWallSize = supersampleFactor

const wallSymbol = true
const openSymbol = false

// Insert one row/col between each cell to place the wall cells in
const [rows, cols] = [
rawMaze.length * cellWallSize + rawMaze.length - 1,
rawMaze[0].length * cellWallSize + rawMaze[0].length - 1,
]

// Create a placeholder maze to hold the new cells
const walledMaze = createFilledMatrix(rows, cols, cellSymbol)
const gridMaze = createFilledMatrix(rows, cols, openSymbol)

// Add south and east walls, and fill in any corners
for (let row = 0; row < rawMaze.length; row++) {
for (let col = 0; col < rawMaze[row].length; col++) {
const [walledRow, walledCol] = [
const [gridRow, gridCol] = [
row * cellWallSize + row,
col * cellWallSize + col,
]
Expand All @@ -389,48 +385,108 @@ export const fillWallsWithCells = (
// Add the south wall to every new south cell
if (hasSouthWall && row < rawMaze.length - 1) {
for (let i = 0; i < cellWallSize; i++) {
walledMaze[walledRow + cellWallSize][walledCol + i] =
wallSymbol
gridMaze[gridRow + cellWallSize][gridCol + i] = wallSymbol
}

// Add a south wall to the the left
if (col > 0)
walledMaze[walledRow + cellWallSize][walledCol - 1] =
wallSymbol
gridMaze[gridRow + cellWallSize][gridCol - 1] = wallSymbol

// Fill in any missing corners
if (hasWestWall && col > 0) {
walledMaze[walledRow + cellWallSize][walledCol - 1] =
wallSymbol
gridMaze[gridRow + cellWallSize][gridCol - 1] = wallSymbol
} else if (hasEastWall) {
walledMaze[walledRow + cellWallSize][
walledCol + cellWallSize
] = wallSymbol
gridMaze[gridRow + cellWallSize][gridCol + cellWallSize] =
wallSymbol
}
}

// Add the east wall to every new east cell
if (hasEastWall && col < rawMaze[row].length - 1) {
for (let i = 0; i < cellWallSize; i++) {
walledMaze[walledRow + i][walledCol + cellWallSize] =
wallSymbol
gridMaze[gridRow + i][gridCol + cellWallSize] = wallSymbol
}

// Add an east wall to the north
if (row > 0)
walledMaze[walledRow - 1][walledCol + cellWallSize] =
wallSymbol
gridMaze[gridRow - 1][gridCol + cellWallSize] = wallSymbol

// Fill in any missing corners
if (hasNorthWall && row > 0) {
walledMaze[walledRow - 1][walledCol + cellWallSize] =
wallSymbol
gridMaze[gridRow - 1][gridCol + cellWallSize] = wallSymbol
}
}
}
}

return walledMaze
return gridMaze
}

/**
* Replace the boolean representation (grid) with the raw format.
*
* Optionally handle a supersampled maze.
*
* @param {boolean[][]} gridMaze The grid maze stored as a matrix of booleans
* @param {number} [supersampleFactor] The factor to scale by
* @returns {number[][]} The new maze with wall cells
*/
export const convertGridToRawFormat = (gridMaze, supersampleFactor = 1) => {
const cellWallSize = supersampleFactor

// Account for one row/col being inserted between each cell to place the wall cells in
const [rows, cols] = [
(gridMaze.length + 1) / (cellWallSize + 1),
(gridMaze[0].length + 1) / (cellWallSize + 1),
]

// Create the raw matrix
const cells = createFilledMatrix(rows, cols, 0)

for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
const [gridRow, gridCol] = [
row * cellWallSize + row,
col * cellWallSize + col,
]

// Check top left for walls
const hasNorthWall = row - 1 < 0 || gridMaze[gridRow - 1][gridCol]
const hasWestWall = col - 1 < 0 || gridMaze[gridRow][gridCol - 1]

// Check the bottom right for walls
const hasSouthWall =
row + 1 >= rows || gridMaze[gridRow + cellWallSize + 1][gridCol]
const hasEastWall =
col + 1 >= cols || gridMaze[gridRow][gridCol + cellWallSize + 1]

cells[row][col] =
(hasNorthWall << 3) |
(hasSouthWall << 2) |
(hasEastWall << 1) |
hasWestWall
}
}

return cells
}

/**
* @param {number[]} cellIndex
* @param {number} factor
* @returns {number[]} The resulting grid point
*/
export const convertRawToGridPoint = ([row, col], factor = 1) => {
return [row * factor + row, col * factor + col]
}

/**
* @param {number[]} cellIndex
* @param {number} factor
* @returns {number[]} The resulting raw point
*/
export const convertGridToRawPoint = ([row, col], factor = 1) => {
return [row / (factor + 1), col / (factor + 1)]
}

/**
Expand Down
54 changes: 50 additions & 4 deletions lib/render-maze.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { East, North, South, West, cellAt, cellIs } from "./helpers"

/**
*
*
* @param {CanvasRenderingContext2D} ctx A canvas context
* @param {number} w The width and height of each cell in the maze grid
* @param {number[][]} rawMaze The full maze in 'binary' format
* @param {number[][]} solution
* @param {number[][]} solution
*/
export const renderMazeToCanvas = (ctx, w, rawMaze, solution = []) => {
export const renderRawMazeToCanvas = (ctx, w, rawMaze, solution = []) => {
ctx.lineJoin = "miter"
ctx.lineCap = "round"

Expand Down Expand Up @@ -60,6 +60,53 @@ export const renderMazeToCanvas = (ctx, w, rawMaze, solution = []) => {
stroke(ctx, 230, 100, 0, w / 2)
}

/**
*
* @param {CanvasRenderingContext2D} ctx A canvas context
* @param {number} w The width and height of each cell in the maze grid
* @param {number[][]} gridMaze The full maze in grid format
* @param {number[][]} solution
*/
export const renderGridMazeToCanvas = (ctx, w, gridMaze, solution = []) => {
ctx.lineJoin = "miter"
ctx.lineCap = "round"

clear(ctx, 0, 0, 0)

// Draw all walls of the maze
for (let row = 0; row < gridMaze.length; row++) {
for (let col = 0; col < gridMaze[row].length; col++) {
if (gridMaze[row][col]) {
// Determine the top left corner of the cell
const [x, y] = [col * w, row * w]

// Draw the wall
rectPath(ctx, x, y, w, w)

// Fill it with white
fill(ctx, 255, 255, 255)
}
}
}

// Draw the solution
ctx.beginPath()
const coordinates = solution.map(([row, col]) => ({
x: col * w + w / 2,
y: row * w + w / 2,
}))

for (let i = 0; i < coordinates.length - 1; i++) {
if (i === 0) {
ctx.moveTo(coordinates[i].x, coordinates[i].y)
}

ctx.lineTo(coordinates[i + 1].x, coordinates[i + 1].y)
}

stroke(ctx, 230, 100, 0, w / 2)
}

//////////// Canvas Helper Methods ////////////
const width = (ctx) => ctx.canvas.width
const height = (ctx) => ctx.canvas.height
Expand Down Expand Up @@ -91,4 +138,3 @@ const line = (ctx, x1, y1, x2, y2) => {
ctx.moveTo(x1, y1)
ctx.lineTo(x2, y2)
}

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "algernon-js",
"author": "Weaver Goldman <we.goldm@gmail.com>",
"description": "Algernon is a JS library for efficiently generating, solving, and rendering mazes.",
"version": "0.2.5",
"version": "0.2.6",
"type": "module",
"license": "MIT",
"repository": {
Expand Down
29 changes: 25 additions & 4 deletions test/convert.spec.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { expect } from "vitest"
import {
generateMazeBacktracking,
convertRawToNodeMatrix,
Expand All @@ -8,8 +9,12 @@ import {
deserializeStringToRaw,
convertRawToNodeGraph,
convertNodeGraphToRaw,
fillWallsWithCells,
convertRawToGridFormat,
convertGridToRawFormat,
convertRawToGridPoint,
convertGridToRawPoint,
} from "../lib"
import { Visited } from "../lib/helpers"

test("Converted matrix is correct", () => {
const [testRows, testCols] = [20, 30]
Expand Down Expand Up @@ -70,20 +75,36 @@ test("Serialization and Deserialization works", () => {

})

test("Fill with walls produces correct dimensions", () => {
test("Conversion to and from Grid works correctly", () => {
const [testRows, testCols] = [20, 30]

const maze = generateMazeBacktracking(testRows, testCols)

const normalSize = fillWallsWithCells(maze, 1, 0)
const normalSize = convertRawToGridFormat(maze)

expect(normalSize).toHaveLength(testRows + testRows - 1)
expect(normalSize[0]).toHaveLength(testCols + testCols - 1)

const factor = 2

const largerSize = fillWallsWithCells(maze, 1, 0, factor)
const largerSize = convertRawToGridFormat(maze, factor)

expect(largerSize).toHaveLength(testRows * factor + testRows - 1)
expect(largerSize[0]).toHaveLength(testCols * factor + testCols - 1)
})

test("Converting points works correctly", () => {
const point = [2, 2]

const gridPoint = convertRawToGridPoint(point)
const originalPoint = convertGridToRawPoint(gridPoint)

expect(point).toEqual(originalPoint)

const point2 = [2, 2]

const gridPoint2 = convertRawToGridPoint(point2, 2)
const originalPoint2 = convertGridToRawPoint(gridPoint2, 2)

expect(point2).toEqual(originalPoint2)
})

0 comments on commit 58cd823

Please sign in to comment.