diff --git a/src/modules/maze/gen-maze.ts b/src/modules/maze/gen-maze.ts new file mode 100644 index 0000000..e2b0ad8 --- /dev/null +++ b/src/modules/maze/gen-maze.ts @@ -0,0 +1,177 @@ +import * as gen from 'random-seed'; +import { CellType } from './maze'; + +const cellVariants = { + empty: { + digg: { left: 'left', right: 'right', top: 'top', bottom: 'bottom' }, + cross: { left: false, right: false, top: false, bottom: false }, + }, + left: { + digg: { left: null, right: 'leftRight', top: 'leftTop', bottom: 'leftBottom' }, + cross: { left: false, right: false, top: false, bottom: false }, + }, + right: { + digg: { left: 'leftRight', right: null, top: 'rightTop', bottom: 'rightBottom' }, + cross: { left: false, right: false, top: false, bottom: false }, + }, + top: { + digg: { left: 'leftTop', right: 'rightTop', top: null, bottom: 'topBottom' }, + cross: { left: false, right: false, top: false, bottom: false }, + }, + bottom: { + digg: { left: 'leftBottom', right: 'rightBottom', top: 'topBottom', bottom: null }, + cross: { left: false, right: false, top: false, bottom: false }, + }, + leftTop: { + digg: { left: null, right: 'leftRightTop', top: null, bottom: 'leftTopBottom' }, + cross: { left: false, right: false, top: false, bottom: false }, + }, + leftBottom: { + digg: { left: null, right: 'leftRightBottom', top: 'leftTopBottom', bottom: null }, + cross: { left: false, right: false, top: false, bottom: false }, + }, + rightTop: { + digg: { left: 'leftRightTop', right: null, top: null, bottom: 'rightTopBottom' }, + cross: { left: false, right: false, top: false, bottom: false }, + }, + rightBottom: { + digg: { left: 'leftRightBottom', right: null, top: 'rightTopBottom', bottom: null }, + cross: { left: false, right: false, top: false, bottom: false }, + }, + leftRightTop: { + digg: { left: null, right: null, top: null, bottom: null }, + cross: { left: false, right: false, top: false, bottom: false }, + }, + leftRightBottom: { + digg: { left: null, right: null, top: null, bottom: null }, + cross: { left: false, right: false, top: false, bottom: false }, + }, + leftTopBottom: { + digg: { left: null, right: null, top: null, bottom: null }, + cross: { left: false, right: false, top: false, bottom: false }, + }, + rightTopBottom: { + digg: { left: null, right: null, top: null, bottom: null }, + cross: { left: false, right: false, top: false, bottom: false }, + }, + leftRight: { + digg: { left: null, right: null, top: 'leftRightTop', bottom: 'leftRightBottom' }, + cross: { left: false, right: false, top: true, bottom: true }, + }, + topBottom: { + digg: { left: 'leftTopBottom', right: 'rightTopBottom', top: null, bottom: null }, + cross: { left: true, right: true, top: false, bottom: false }, + }, + cross: { + digg: { left: 'cross', right: 'cross', top: 'cross', bottom: 'cross' }, + cross: { left: false, right: false, top: false, bottom: false }, + }, +} as { [k in CellType]: { + digg: { left: CellType | null; right: CellType | null; top: CellType | null; bottom: CellType | null; }; + cross: { left: boolean; right: boolean; top: boolean; bottom: boolean; }; +} }; + +type Dir = 'left' | 'right' | 'top' | 'bottom'; + +export function genMaze(seed) { + const rand = gen.create(seed); + + const mazeSize = 11 + rand(21); + + // maze (filled by 'empty') + const maze: CellType[][] = new Array(mazeSize); + for (let i = 0; i < mazeSize; i++) { + maze[i] = new Array(mazeSize).fill('empty'); + } + + const origin = { + x: rand(mazeSize), + y: rand(mazeSize), + }; + + function checkDiggable(x: number, y: number, dir: Dir) { + if (cellVariants[maze[x][y]].digg[dir] === null) return false; + + const newPos = + dir === 'top' ? { x: x, y: y - 1 } : + dir === 'bottom' ? { x: x, y: y + 1 } : + dir === 'left' ? { x: x - 1, y: y } : + dir === 'right' ? { x: x + 1, y: y } : + { x, y }; + + if (newPos.x < 0 || newPos.y < 0 || newPos.x >= mazeSize || newPos.y >= mazeSize) return false; + + const cell = maze[newPos.x][newPos.y]; + if (cell === 'empty') return true; + if (cellVariants[cell].cross[dir] && checkDiggable(newPos.x, newPos.y, dir)) return true; + + return false; + } + + function diggFrom(x: number, y: number) { + const isUpDiggable = checkDiggable(x, y, 'top'); + const isRightDiggable = checkDiggable(x, y, 'right'); + const isDownDiggable = checkDiggable(x, y, 'bottom'); + const isLeftDiggable = checkDiggable(x, y, 'left'); + + if (!isUpDiggable && !isRightDiggable && !isDownDiggable && !isLeftDiggable) return; + + const dirs: Dir[] = []; + if (isUpDiggable) dirs.push('top'); + if (isRightDiggable) dirs.push('right'); + if (isDownDiggable) dirs.push('bottom'); + if (isLeftDiggable) dirs.push('left'); + + const dir = dirs[rand(dirs.length)]; + + maze[x][y] = cellVariants[maze[x][y]].digg[dir]; + + if (dir === 'top') { + maze[x][y - 1] = maze[x][y - 1] === 'empty' ? 'bottom' : 'cross'; + diggFrom(x, y - 1); + return; + } + if (dir === 'right') { + maze[x + 1][y] = maze[x + 1][y] === 'empty' ? 'left' : 'cross'; + diggFrom(x + 1, y); + return; + } + if (dir === 'bottom') { + maze[x][y + 1] = maze[x][y + 1] === 'empty' ? 'top' : 'cross'; + diggFrom(x, y + 1); + return; + } + if (dir === 'left') { + maze[x - 1][y] = maze[x - 1][y] === 'empty' ? 'right' : 'cross'; + diggFrom(x - 1, y); + return; + } + } + + diggFrom(origin.x, origin.y); + + let hasEmptyCell = true; + while (hasEmptyCell) { + const nonEmptyCells = []; + + for (let y = 0; y < mazeSize; y++) { + for (let x = 0; x < mazeSize; x++) { + const cell = maze[x][y]; + if (cell !== 'empty' && cell !== 'cross') nonEmptyCells.push([x, y]); + } + } + + const pos = nonEmptyCells[rand(nonEmptyCells.length)]; + + diggFrom(pos[0], pos[1]); + + hasEmptyCell = false; + for (let y = 0; y < mazeSize; y++) { + for (let x = 0; x < mazeSize; x++) { + if (maze[x][y] === 'empty') hasEmptyCell = true; + } + } + } + + return maze; +} diff --git a/src/modules/maze/index.ts b/src/modules/maze/index.ts index 1e29e25..e8df493 100644 --- a/src/modules/maze/index.ts +++ b/src/modules/maze/index.ts @@ -2,147 +2,13 @@ * Random avatar generator */ -const p = require('pureimage'); -import * as gen from 'random-seed'; import * as fs from 'fs'; import autobind from 'autobind-decorator'; import Module from '../../module'; import serifs from '../../serifs'; import * as tmp from 'tmp'; - -type CellType = 'empty' | 'left' | 'right' | 'top' | 'bottom' | 'leftTop' | 'leftBottom' | 'rightTop' | 'rightBottom' | 'leftRightTop' | 'leftRightBottom' | 'leftTopBottom' | 'rightTopBottom' | 'leftRight' | 'topBottom' | 'cross'; - -type Dir = 'left' | 'right' | 'top' | 'bottom'; - -const cellVariants = { - empty: { - digg: { left: 'left', right: 'right', top: 'top', bottom: 'bottom' }, - cross: { left: false, right: false, top: false, bottom: false }, - }, - left: { - digg: { left: null, right: 'leftRight', top: 'leftTop', bottom: 'leftBottom' }, - cross: { left: false, right: false, top: false, bottom: false }, - }, - right: { - digg: { left: 'leftRight', right: null, top: 'rightTop', bottom: 'rightBottom' }, - cross: { left: false, right: false, top: false, bottom: false }, - }, - top: { - digg: { left: 'leftTop', right: 'rightTop', top: null, bottom: 'topBottom' }, - cross: { left: false, right: false, top: false, bottom: false }, - }, - bottom: { - digg: { left: 'leftBottom', right: 'rightBottom', top: 'topBottom', bottom: null }, - cross: { left: false, right: false, top: false, bottom: false }, - }, - leftTop: { - digg: { left: null, right: 'leftRightTop', top: null, bottom: 'leftTopBottom' }, - cross: { left: false, right: false, top: false, bottom: false }, - }, - leftBottom: { - digg: { left: null, right: 'leftRightBottom', top: 'leftTopBottom', bottom: null }, - cross: { left: false, right: false, top: false, bottom: false }, - }, - rightTop: { - digg: { left: 'leftRightTop', right: null, top: null, bottom: 'rightTopBottom' }, - cross: { left: false, right: false, top: false, bottom: false }, - }, - rightBottom: { - digg: { left: 'leftRightBottom', right: null, top: 'rightTopBottom', bottom: null }, - cross: { left: false, right: false, top: false, bottom: false }, - }, - leftRightTop: { - digg: { left: null, right: null, top: null, bottom: null }, - cross: { left: false, right: false, top: false, bottom: false }, - }, - leftRightBottom: { - digg: { left: null, right: null, top: null, bottom: null }, - cross: { left: false, right: false, top: false, bottom: false }, - }, - leftTopBottom: { - digg: { left: null, right: null, top: null, bottom: null }, - cross: { left: false, right: false, top: false, bottom: false }, - }, - rightTopBottom: { - digg: { left: null, right: null, top: null, bottom: null }, - cross: { left: false, right: false, top: false, bottom: false }, - }, - leftRight: { - digg: { left: null, right: null, top: 'leftRightTop', bottom: 'leftRightBottom' }, - cross: { left: false, right: false, top: true, bottom: true }, - }, - topBottom: { - digg: { left: 'leftTopBottom', right: 'rightTopBottom', top: null, bottom: null }, - cross: { left: true, right: true, top: false, bottom: false }, - }, - cross: { - digg: { left: 'cross', right: 'cross', top: 'cross', bottom: 'cross' }, - cross: { left: false, right: false, top: false, bottom: false }, - }, -} as { [k in CellType]: { - digg: { left: CellType | null; right: CellType | null; top: CellType | null; bottom: CellType | null; }; - cross: { left: boolean; right: boolean; top: boolean; bottom: boolean; }; -} }; - -const imageSize = 2048; // px -const margin = 192; -const mazeAreaSize = imageSize - (margin * 2); - -const themes = [{ - bg1: '#C1D9CE', - bg2: '#F2EDD5', - wall: '#0F8AA6', - road: '#C1D9CE', - marker: '#84BFBF', -}, { - bg1: '#17275B', - bg2: '#1F2E67', - wall: '#17275B', - road: '#6A77A4', - marker: '#E6E5E3', -}, { - bg1: '#BFD962', - bg2: '#EAF2AC', - wall: '#1E4006', - road: '#BFD962', - marker: '#74A608', -}, { - bg1: '#C0CCB8', - bg2: '#FFE2C0', - wall: '#664A3C', - road: '#FFCB99', - marker: '#E78F72', -}, { - bg1: '#101010', - bg2: '#151515', - wall: '#909090', - road: '#202020', - marker: '#606060', -}, { - bg1: '#e0e0e0', - bg2: '#f2f2f2', - wall: '#a0a0a0', - road: '#e0e0e0', - marker: '#707070', -}, { - bg1: '#7DE395', - bg2: '#D0F3CF', - wall: '#349D9E', - road: '#7DE395', - marker: '#56C495', -}, { - bg1: '#C9EEEA', - bg2: '#DBF4F1', - wall: '#4BC6B9', - road: '#C9EEEA', - marker: '#19A89D', -}, { - bg1: '#1e231b', - bg2: '#27331e', - wall: '#67b231', - road: '#385622', - marker: '#78d337', -}]; +import { genMaze } from './gen-maze'; +import { renderMaze } from './render-maze'; export default class extends Module { public readonly name = 'maze'; @@ -168,11 +34,11 @@ export default class extends Module { const seed = date; this.log('Maze generating...'); - const maze = this.genMize(seed); + const maze = genMaze(seed); this.log('Maze rendering...'); const [temp] = await this.createTemp(); - await this.renderMaze(seed, maze, fs.createWriteStream(temp)); + await renderMaze(seed, maze, fs.createWriteStream(temp)); this.log('Image uploading...'); const file = await this.ai.upload(fs.createReadStream(temp)); @@ -193,316 +59,4 @@ export default class extends Module { }); }); } - - @autobind - private genMize(seed) { - const rand = gen.create(seed); - - const mazeSize = 11 + rand(21); - - // maze (filled by 'empty') - const maze: CellType[][] = new Array(mazeSize); - for (let i = 0; i < mazeSize; i++) { - maze[i] = new Array(mazeSize).fill('empty'); - } - - const origin = { - x: rand(mazeSize), - y: rand(mazeSize), - }; - - function checkDiggable(x: number, y: number, dir: Dir) { - if (cellVariants[maze[x][y]].digg[dir] === null) return false; - - const newPos = - dir === 'top' ? { x: x, y: y - 1 } : - dir === 'bottom' ? { x: x, y: y + 1 } : - dir === 'left' ? { x: x - 1, y: y } : - dir === 'right' ? { x: x + 1, y: y } : - { x, y }; - - if (newPos.x < 0 || newPos.y < 0 || newPos.x >= mazeSize || newPos.y >= mazeSize) return false; - - const cell = maze[newPos.x][newPos.y]; - if (cell === 'empty') return true; - if (cellVariants[cell].cross[dir] && checkDiggable(newPos.x, newPos.y, dir)) return true; - - return false; - } - - function diggFrom(x: number, y: number) { - const isUpDiggable = checkDiggable(x, y, 'top'); - const isRightDiggable = checkDiggable(x, y, 'right'); - const isDownDiggable = checkDiggable(x, y, 'bottom'); - const isLeftDiggable = checkDiggable(x, y, 'left'); - - if (!isUpDiggable && !isRightDiggable && !isDownDiggable && !isLeftDiggable) return; - - const dirs: Dir[] = []; - if (isUpDiggable) dirs.push('top'); - if (isRightDiggable) dirs.push('right'); - if (isDownDiggable) dirs.push('bottom'); - if (isLeftDiggable) dirs.push('left'); - - const dir = dirs[rand(dirs.length)]; - - maze[x][y] = cellVariants[maze[x][y]].digg[dir]; - - if (dir === 'top') { - maze[x][y - 1] = maze[x][y - 1] === 'empty' ? 'bottom' : 'cross'; - diggFrom(x, y - 1); - return; - } - if (dir === 'right') { - maze[x + 1][y] = maze[x + 1][y] === 'empty' ? 'left' : 'cross'; - diggFrom(x + 1, y); - return; - } - if (dir === 'bottom') { - maze[x][y + 1] = maze[x][y + 1] === 'empty' ? 'top' : 'cross'; - diggFrom(x, y + 1); - return; - } - if (dir === 'left') { - maze[x - 1][y] = maze[x - 1][y] === 'empty' ? 'right' : 'cross'; - diggFrom(x - 1, y); - return; - } - } - - diggFrom(origin.x, origin.y); - - let hasEmptyCell = true; - while (hasEmptyCell) { - const nonEmptyCells = []; - - for (let y = 0; y < mazeSize; y++) { - for (let x = 0; x < mazeSize; x++) { - const cell = maze[x][y]; - if (cell !== 'empty' && cell !== 'cross') nonEmptyCells.push([x, y]); - } - } - - const pos = nonEmptyCells[rand(nonEmptyCells.length)]; - - diggFrom(pos[0], pos[1]); - - hasEmptyCell = false; - for (let y = 0; y < mazeSize; y++) { - for (let x = 0; x < mazeSize; x++) { - if (maze[x][y] === 'empty') hasEmptyCell = true; - } - } - } - - return maze; - } - - @autobind - private renderMaze(seed, maze: CellType[][], stream: fs.WriteStream): Promise { - const rand = gen.create(seed); - const mazeSize = maze.length; - - const colors = themes[rand(themes.length)]; - - const canvas = p.make(imageSize, imageSize); - const ctx = canvas.getContext('2d'); - - ctx.fillStyle = colors.bg1; - ctx.beginPath(); - ctx.fillRect(0, 0, imageSize, imageSize); - - ctx.fillStyle = colors.bg2; - ctx.beginPath(); - ctx.fillRect(margin / 2, margin / 2, imageSize - ((margin / 2) * 2), imageSize - ((margin / 2) * 2)); - - // Draw - function drawCell(ctx, x, y, size, left, right, top, bottom, mark) { - const wallThickness = size / 8; - const wallMargin = size / 4; - const markerMargin = size / 3; - - ctx.fillStyle = colors.road; - if (left) { - ctx.beginPath(); - ctx.fillRect(x, y + wallMargin, size - wallMargin, size - (wallMargin * 2)); - } - if (right) { - ctx.beginPath(); - ctx.fillRect(x + wallMargin, y + wallMargin, size - wallMargin, size - (wallMargin * 2)); - } - if (top) { - ctx.beginPath(); - ctx.fillRect(x + wallMargin, y, size - (wallMargin * 2), size - wallMargin); - } - if (bottom) { - ctx.beginPath(); - ctx.fillRect(x + wallMargin, y + wallMargin, size - (wallMargin * 2), size - wallMargin); - } - - if (mark) { - ctx.fillStyle = colors.marker; - ctx.beginPath(); - ctx.fillRect(x + markerMargin, y + markerMargin, size - (markerMargin * 2), size - (markerMargin * 2)); - } - - const wallLeftTopX = x + wallMargin - (wallThickness / 2); - const wallLeftTopY = y + wallMargin - (wallThickness / 2); - const wallRightTopX = x + size - wallMargin - (wallThickness / 2); - const wallRightTopY = y + wallMargin - (wallThickness / 2); - const wallLeftBottomX = x + wallMargin - (wallThickness / 2); - const wallLeftBottomY = y + size - wallMargin - (wallThickness / 2); - const wallRightBottomX = x + size - wallMargin - (wallThickness / 2); - const wallRightBottomY = y + size - wallMargin - (wallThickness / 2); - - ctx.fillStyle = colors.wall; - if (left && right && top && bottom) { - ctx.beginPath(); - if (rand(2) === 0) { - ctx.fillRect(x + wallMargin - (wallThickness / 2), y, wallThickness, size); - ctx.fillRect(x + size - wallMargin - (wallThickness / 2), y, wallThickness, size); - ctx.fillRect(x, y + wallMargin - (wallThickness / 2), wallMargin, wallThickness); - ctx.fillRect(x + size - wallMargin, y + wallMargin - (wallThickness / 2), wallMargin, wallThickness); - ctx.fillRect(x, y + size - wallMargin - (wallThickness / 2), wallMargin, wallThickness); - ctx.fillRect(x + size - wallMargin, y + size - wallMargin - (wallThickness / 2), wallMargin, wallThickness); - } else { - ctx.fillRect(x, y + wallMargin - (wallThickness / 2), size, wallThickness); - ctx.fillRect(x, y + size - wallMargin - (wallThickness / 2), size, wallThickness); - ctx.fillRect(wallLeftTopX, y, wallThickness, wallMargin - (wallThickness / 2)); - ctx.fillRect(wallRightTopX, y, wallThickness, wallMargin - (wallThickness / 2)); - ctx.fillRect(wallLeftTopX, wallLeftBottomY, wallThickness, wallMargin + (wallThickness / 2)); - ctx.fillRect(wallRightTopX, wallRightBottomY, wallThickness, wallMargin + (wallThickness / 2)); - } - return; - } - - if (!left && right && !top && bottom) { - ctx.beginPath(); - ctx.fillRect(wallLeftTopX, wallLeftTopY, size - wallMargin + (wallThickness / 2), wallThickness); - ctx.fillRect(wallLeftTopX, wallLeftTopY, wallThickness, size - wallMargin + (wallThickness / 2)); - } - - if (right && bottom) { - ctx.fillRect(wallRightBottomX, wallRightBottomY, wallMargin + (wallThickness / 2), wallThickness); - ctx.fillRect(wallRightBottomX, wallRightBottomY, wallThickness, wallMargin + (wallThickness / 2)); - } - - if (left && !right && !top && bottom) { - ctx.beginPath(); - ctx.fillRect(x, y + wallMargin - (wallThickness / 2), size - wallMargin + (wallThickness / 2), wallThickness); - ctx.fillRect(wallRightTopX, y + wallMargin - (wallThickness / 2), wallThickness, size - wallMargin + (wallThickness / 2)); - } - - if (left && bottom) { - ctx.fillRect(x, wallLeftBottomY, wallMargin - (wallThickness / 2), wallThickness); - ctx.fillRect(wallLeftBottomX, wallLeftBottomY, wallThickness, wallMargin + (wallThickness / 2)); - } - - if (!left && right && top && !bottom) { - ctx.beginPath(); - ctx.fillRect(wallLeftBottomX, y, wallThickness, size - wallMargin + (wallThickness / 2)); - ctx.fillRect(wallLeftBottomX, wallLeftBottomY, size - wallMargin + (wallThickness / 2), wallThickness); - } - - if (right && top) { - ctx.fillRect(wallRightTopX, y, wallThickness, wallMargin - (wallThickness / 2)); - ctx.fillRect(wallRightTopX, wallRightTopY, size - wallMargin - (wallThickness * 2), wallThickness); - } - - if (left && !right && top && !bottom) { - ctx.beginPath(); - ctx.fillRect(x, wallLeftBottomY, size - wallMargin - (wallThickness / 2), wallThickness); - ctx.fillRect(wallRightTopX, y, wallThickness, size - wallMargin + (wallThickness / 2)); - } - - if (left && top) { - ctx.fillRect(wallLeftTopX, y, wallThickness, wallMargin + (wallThickness / 2)); - ctx.fillRect(x, wallLeftTopY, wallMargin - (wallThickness / 2), wallThickness); - } - - if (!left && right && !top && !bottom) { - ctx.beginPath(); - ctx.fillRect(wallLeftTopX, wallLeftTopY, size - wallMargin + (wallThickness / 2), wallThickness); - ctx.fillRect(wallLeftBottomX, wallLeftBottomY, size - wallMargin + (wallThickness / 2), wallThickness); - ctx.fillRect(wallLeftTopX, wallLeftTopY, wallThickness, size - wallMargin - (wallThickness * 2)); - return; - } - - if (left && !right && !top && !bottom) { - ctx.beginPath(); - ctx.fillRect(x, wallLeftTopY, size - wallMargin + (wallThickness / 2), wallThickness); - ctx.fillRect(x, wallLeftBottomY, size - wallMargin + (wallThickness / 2), wallThickness); - ctx.fillRect(wallRightTopX, wallRightTopY, wallThickness, size - wallMargin - (wallThickness * 2)); - return; - } - - if (!left && !right && !top && bottom) { - ctx.beginPath(); - ctx.fillRect(wallLeftTopX, wallLeftTopY, wallThickness, size - wallMargin + (wallThickness / 2)); - ctx.fillRect(wallRightTopX, wallRightTopY, wallThickness, size - wallMargin + (wallThickness / 2)); - ctx.fillRect(wallLeftTopX, wallLeftTopY, size - wallMargin - (wallThickness * 2), wallThickness); - return; - } - - if (!left && !right && top && !bottom) { - ctx.beginPath(); - ctx.fillRect(wallLeftTopX, y, wallThickness, size - wallMargin + (wallThickness / 2)); - ctx.fillRect(wallRightTopX, y, wallThickness, size - wallMargin + (wallThickness / 2)); - ctx.fillRect(wallLeftBottomX, wallLeftBottomY, size - wallMargin - (wallThickness * 2), wallThickness); - return; - } - - if (top && bottom) { - if (!left) { - ctx.beginPath(); - ctx.fillRect(x + wallMargin - (wallThickness / 2), y, wallThickness, size); - } - if (!right) { - ctx.beginPath(); - ctx.fillRect(x + size - wallMargin - (wallThickness / 2), y, wallThickness, size); - } - } - if (left && right) { - if (!top) { - ctx.beginPath(); - ctx.fillRect(x, y + wallMargin - (wallThickness / 2), size, wallThickness); - } - if (!bottom) { - ctx.beginPath(); - ctx.fillRect(x, y + size - wallMargin - (wallThickness / 2), size, wallThickness); - } - } - } - - const cellSize = mazeAreaSize / mazeSize; - - for (let x = 0; x < mazeSize; x++) { - for (let y = 0; y < mazeSize; y++) { - const actualX = margin + (cellSize * x); - const actualY = margin + (cellSize * y); - - const cell = maze[x][y]; - - const mark = (x === 0 && y === 0) || (x === mazeSize - 1 && y === mazeSize - 1); - - if (cell === 'left') drawCell(ctx, actualX, actualY, cellSize, true, false, false, false, mark); - if (cell === 'right') drawCell(ctx, actualX, actualY, cellSize, false, true, false, false, mark); - if (cell === 'top') drawCell(ctx, actualX, actualY, cellSize, false, false, true, false, mark); - if (cell === 'bottom') drawCell(ctx, actualX, actualY, cellSize, false, false, false, true, mark); - if (cell === 'leftTop') drawCell(ctx, actualX, actualY, cellSize, true, false, true, false, mark); - if (cell === 'leftBottom') drawCell(ctx, actualX, actualY, cellSize, true, false, false, true, mark); - if (cell === 'rightTop') drawCell(ctx, actualX, actualY, cellSize, false, true, true, false, mark); - if (cell === 'rightBottom') drawCell(ctx, actualX, actualY, cellSize, false, true, false, true, mark); - if (cell === 'leftRightTop') drawCell(ctx, actualX, actualY, cellSize, true, true, true, false, mark); - if (cell === 'leftRightBottom') drawCell(ctx, actualX, actualY, cellSize, true, true, false, true, mark); - if (cell === 'leftTopBottom') drawCell(ctx, actualX, actualY, cellSize, true, false, true, true, mark); - if (cell === 'rightTopBottom') drawCell(ctx, actualX, actualY, cellSize, false, true, true, true, mark); - if (cell === 'leftRight') drawCell(ctx, actualX, actualY, cellSize, true, true, false, false, mark); - if (cell === 'topBottom') drawCell(ctx, actualX, actualY, cellSize, false, false, true, true, mark); - if (cell === 'cross') drawCell(ctx, actualX, actualY, cellSize, true, true, true, true, mark); - } - } - - return p.encodePNGToStream(canvas, stream); - } } diff --git a/src/modules/maze/maze.ts b/src/modules/maze/maze.ts new file mode 100644 index 0000000..de2f6d2 --- /dev/null +++ b/src/modules/maze/maze.ts @@ -0,0 +1 @@ +export type CellType = 'empty' | 'left' | 'right' | 'top' | 'bottom' | 'leftTop' | 'leftBottom' | 'rightTop' | 'rightBottom' | 'leftRightTop' | 'leftRightBottom' | 'leftTopBottom' | 'rightTopBottom' | 'leftRight' | 'topBottom' | 'cross'; diff --git a/src/modules/maze/render-maze.ts b/src/modules/maze/render-maze.ts new file mode 100644 index 0000000..59a4c83 --- /dev/null +++ b/src/modules/maze/render-maze.ts @@ -0,0 +1,217 @@ +import * as fs from 'fs'; +import * as gen from 'random-seed'; +const p = require('pureimage'); + +import { CellType } from './maze'; +import { themes } from './themes'; + +const imageSize = 2048; // px +const margin = 192; +const mazeAreaSize = imageSize - (margin * 2); + +export function renderMaze(seed, maze: CellType[][], stream: fs.WriteStream): Promise { + const rand = gen.create(seed); + const mazeSize = maze.length; + + const colors = themes[rand(themes.length)]; + + const canvas = p.make(imageSize, imageSize); + const ctx = canvas.getContext('2d'); + + ctx.fillStyle = colors.bg1; + ctx.beginPath(); + ctx.fillRect(0, 0, imageSize, imageSize); + + ctx.fillStyle = colors.bg2; + ctx.beginPath(); + ctx.fillRect(margin / 2, margin / 2, imageSize - ((margin / 2) * 2), imageSize - ((margin / 2) * 2)); + + // Draw + function drawCell(ctx, x, y, size, left, right, top, bottom, mark) { + const wallThickness = size / 8; + const wallMargin = size / 4; + const markerMargin = size / 3; + + ctx.fillStyle = colors.road; + if (left) { + ctx.beginPath(); + ctx.fillRect(x, y + wallMargin, size - wallMargin, size - (wallMargin * 2)); + } + if (right) { + ctx.beginPath(); + ctx.fillRect(x + wallMargin, y + wallMargin, size - wallMargin, size - (wallMargin * 2)); + } + if (top) { + ctx.beginPath(); + ctx.fillRect(x + wallMargin, y, size - (wallMargin * 2), size - wallMargin); + } + if (bottom) { + ctx.beginPath(); + ctx.fillRect(x + wallMargin, y + wallMargin, size - (wallMargin * 2), size - wallMargin); + } + + if (mark) { + ctx.fillStyle = colors.marker; + ctx.beginPath(); + ctx.fillRect(x + markerMargin, y + markerMargin, size - (markerMargin * 2), size - (markerMargin * 2)); + } + + const wallLeftTopX = x + wallMargin - (wallThickness / 2); + const wallLeftTopY = y + wallMargin - (wallThickness / 2); + const wallRightTopX = x + size - wallMargin - (wallThickness / 2); + const wallRightTopY = y + wallMargin - (wallThickness / 2); + const wallLeftBottomX = x + wallMargin - (wallThickness / 2); + const wallLeftBottomY = y + size - wallMargin - (wallThickness / 2); + const wallRightBottomX = x + size - wallMargin - (wallThickness / 2); + const wallRightBottomY = y + size - wallMargin - (wallThickness / 2); + + ctx.fillStyle = colors.wall; + if (left && right && top && bottom) { + ctx.beginPath(); + if (rand(2) === 0) { + ctx.fillRect(x + wallMargin - (wallThickness / 2), y, wallThickness, size); + ctx.fillRect(x + size - wallMargin - (wallThickness / 2), y, wallThickness, size); + ctx.fillRect(x, y + wallMargin - (wallThickness / 2), wallMargin, wallThickness); + ctx.fillRect(x + size - wallMargin, y + wallMargin - (wallThickness / 2), wallMargin, wallThickness); + ctx.fillRect(x, y + size - wallMargin - (wallThickness / 2), wallMargin, wallThickness); + ctx.fillRect(x + size - wallMargin, y + size - wallMargin - (wallThickness / 2), wallMargin, wallThickness); + } else { + ctx.fillRect(x, y + wallMargin - (wallThickness / 2), size, wallThickness); + ctx.fillRect(x, y + size - wallMargin - (wallThickness / 2), size, wallThickness); + ctx.fillRect(wallLeftTopX, y, wallThickness, wallMargin - (wallThickness / 2)); + ctx.fillRect(wallRightTopX, y, wallThickness, wallMargin - (wallThickness / 2)); + ctx.fillRect(wallLeftTopX, wallLeftBottomY, wallThickness, wallMargin + (wallThickness / 2)); + ctx.fillRect(wallRightTopX, wallRightBottomY, wallThickness, wallMargin + (wallThickness / 2)); + } + return; + } + + if (!left && right && !top && bottom) { + ctx.beginPath(); + ctx.fillRect(wallLeftTopX, wallLeftTopY, size - wallMargin + (wallThickness / 2), wallThickness); + ctx.fillRect(wallLeftTopX, wallLeftTopY, wallThickness, size - wallMargin + (wallThickness / 2)); + } + + if (right && bottom) { + ctx.fillRect(wallRightBottomX, wallRightBottomY, wallMargin + (wallThickness / 2), wallThickness); + ctx.fillRect(wallRightBottomX, wallRightBottomY, wallThickness, wallMargin + (wallThickness / 2)); + } + + if (left && !right && !top && bottom) { + ctx.beginPath(); + ctx.fillRect(x, y + wallMargin - (wallThickness / 2), size - wallMargin + (wallThickness / 2), wallThickness); + ctx.fillRect(wallRightTopX, y + wallMargin - (wallThickness / 2), wallThickness, size - wallMargin + (wallThickness / 2)); + } + + if (left && bottom) { + ctx.fillRect(x, wallLeftBottomY, wallMargin - (wallThickness / 2), wallThickness); + ctx.fillRect(wallLeftBottomX, wallLeftBottomY, wallThickness, wallMargin + (wallThickness / 2)); + } + + if (!left && right && top && !bottom) { + ctx.beginPath(); + ctx.fillRect(wallLeftBottomX, y, wallThickness, size - wallMargin + (wallThickness / 2)); + ctx.fillRect(wallLeftBottomX, wallLeftBottomY, size - wallMargin + (wallThickness / 2), wallThickness); + } + + if (right && top) { + ctx.fillRect(wallRightTopX, y, wallThickness, wallMargin - (wallThickness / 2)); + ctx.fillRect(wallRightTopX, wallRightTopY, size - wallMargin - (wallThickness * 2), wallThickness); + } + + if (left && !right && top && !bottom) { + ctx.beginPath(); + ctx.fillRect(x, wallLeftBottomY, size - wallMargin - (wallThickness / 2), wallThickness); + ctx.fillRect(wallRightTopX, y, wallThickness, size - wallMargin + (wallThickness / 2)); + } + + if (left && top) { + ctx.fillRect(wallLeftTopX, y, wallThickness, wallMargin + (wallThickness / 2)); + ctx.fillRect(x, wallLeftTopY, wallMargin - (wallThickness / 2), wallThickness); + } + + if (!left && right && !top && !bottom) { + ctx.beginPath(); + ctx.fillRect(wallLeftTopX, wallLeftTopY, size - wallMargin + (wallThickness / 2), wallThickness); + ctx.fillRect(wallLeftBottomX, wallLeftBottomY, size - wallMargin + (wallThickness / 2), wallThickness); + ctx.fillRect(wallLeftTopX, wallLeftTopY, wallThickness, size - wallMargin - (wallThickness * 2)); + return; + } + + if (left && !right && !top && !bottom) { + ctx.beginPath(); + ctx.fillRect(x, wallLeftTopY, size - wallMargin + (wallThickness / 2), wallThickness); + ctx.fillRect(x, wallLeftBottomY, size - wallMargin + (wallThickness / 2), wallThickness); + ctx.fillRect(wallRightTopX, wallRightTopY, wallThickness, size - wallMargin - (wallThickness * 2)); + return; + } + + if (!left && !right && !top && bottom) { + ctx.beginPath(); + ctx.fillRect(wallLeftTopX, wallLeftTopY, wallThickness, size - wallMargin + (wallThickness / 2)); + ctx.fillRect(wallRightTopX, wallRightTopY, wallThickness, size - wallMargin + (wallThickness / 2)); + ctx.fillRect(wallLeftTopX, wallLeftTopY, size - wallMargin - (wallThickness * 2), wallThickness); + return; + } + + if (!left && !right && top && !bottom) { + ctx.beginPath(); + ctx.fillRect(wallLeftTopX, y, wallThickness, size - wallMargin + (wallThickness / 2)); + ctx.fillRect(wallRightTopX, y, wallThickness, size - wallMargin + (wallThickness / 2)); + ctx.fillRect(wallLeftBottomX, wallLeftBottomY, size - wallMargin - (wallThickness * 2), wallThickness); + return; + } + + if (top && bottom) { + if (!left) { + ctx.beginPath(); + ctx.fillRect(x + wallMargin - (wallThickness / 2), y, wallThickness, size); + } + if (!right) { + ctx.beginPath(); + ctx.fillRect(x + size - wallMargin - (wallThickness / 2), y, wallThickness, size); + } + } + if (left && right) { + if (!top) { + ctx.beginPath(); + ctx.fillRect(x, y + wallMargin - (wallThickness / 2), size, wallThickness); + } + if (!bottom) { + ctx.beginPath(); + ctx.fillRect(x, y + size - wallMargin - (wallThickness / 2), size, wallThickness); + } + } + } + + const cellSize = mazeAreaSize / mazeSize; + + for (let x = 0; x < mazeSize; x++) { + for (let y = 0; y < mazeSize; y++) { + const actualX = margin + (cellSize * x); + const actualY = margin + (cellSize * y); + + const cell = maze[x][y]; + + const mark = (x === 0 && y === 0) || (x === mazeSize - 1 && y === mazeSize - 1); + + if (cell === 'left') drawCell(ctx, actualX, actualY, cellSize, true, false, false, false, mark); + if (cell === 'right') drawCell(ctx, actualX, actualY, cellSize, false, true, false, false, mark); + if (cell === 'top') drawCell(ctx, actualX, actualY, cellSize, false, false, true, false, mark); + if (cell === 'bottom') drawCell(ctx, actualX, actualY, cellSize, false, false, false, true, mark); + if (cell === 'leftTop') drawCell(ctx, actualX, actualY, cellSize, true, false, true, false, mark); + if (cell === 'leftBottom') drawCell(ctx, actualX, actualY, cellSize, true, false, false, true, mark); + if (cell === 'rightTop') drawCell(ctx, actualX, actualY, cellSize, false, true, true, false, mark); + if (cell === 'rightBottom') drawCell(ctx, actualX, actualY, cellSize, false, true, false, true, mark); + if (cell === 'leftRightTop') drawCell(ctx, actualX, actualY, cellSize, true, true, true, false, mark); + if (cell === 'leftRightBottom') drawCell(ctx, actualX, actualY, cellSize, true, true, false, true, mark); + if (cell === 'leftTopBottom') drawCell(ctx, actualX, actualY, cellSize, true, false, true, true, mark); + if (cell === 'rightTopBottom') drawCell(ctx, actualX, actualY, cellSize, false, true, true, true, mark); + if (cell === 'leftRight') drawCell(ctx, actualX, actualY, cellSize, true, true, false, false, mark); + if (cell === 'topBottom') drawCell(ctx, actualX, actualY, cellSize, false, false, true, true, mark); + if (cell === 'cross') drawCell(ctx, actualX, actualY, cellSize, true, true, true, true, mark); + } + } + + return p.encodePNGToStream(canvas, stream); +} diff --git a/src/modules/maze/themes.ts b/src/modules/maze/themes.ts new file mode 100644 index 0000000..83115df --- /dev/null +++ b/src/modules/maze/themes.ts @@ -0,0 +1,55 @@ +export const themes = [{ + bg1: '#C1D9CE', + bg2: '#F2EDD5', + wall: '#0F8AA6', + road: '#C1D9CE', + marker: '#84BFBF', +}, { + bg1: '#17275B', + bg2: '#1F2E67', + wall: '#17275B', + road: '#6A77A4', + marker: '#E6E5E3', +}, { + bg1: '#BFD962', + bg2: '#EAF2AC', + wall: '#1E4006', + road: '#BFD962', + marker: '#74A608', +}, { + bg1: '#C0CCB8', + bg2: '#FFE2C0', + wall: '#664A3C', + road: '#FFCB99', + marker: '#E78F72', +}, { + bg1: '#101010', + bg2: '#151515', + wall: '#909090', + road: '#202020', + marker: '#606060', +}, { + bg1: '#e0e0e0', + bg2: '#f2f2f2', + wall: '#a0a0a0', + road: '#e0e0e0', + marker: '#707070', +}, { + bg1: '#7DE395', + bg2: '#D0F3CF', + wall: '#349D9E', + road: '#7DE395', + marker: '#56C495', +}, { + bg1: '#C9EEEA', + bg2: '#DBF4F1', + wall: '#4BC6B9', + road: '#C9EEEA', + marker: '#19A89D', +}, { + bg1: '#1e231b', + bg2: '#27331e', + wall: '#67b231', + road: '#385622', + marker: '#78d337', +}];