"use strict"; // It's strict! // // Helper functions // // // Class: RandomLcg // function RandomLcg() { this.iState = 0; this.iSeed = 0; this.inventSeed(); } RandomLcg.prototype.setSeed = function setSeed( iSeed ) { this.iState = this.iSeed = iSeed; } RandomLcg.prototype.getSeed = function getSeed() { return this.iSeed; } RandomLcg.prototype.inventSeed = function inventSeed() { // Based on my reading it's safest to assume that we can get 16 bits worth // of random data reliably. Let's build a 32 bit number from two 16 bit // numbers. this.setSeed( (Math.floor(Math.random()*0xffff)) | (Math.floor(Math.random()*0xffff)<<16) ); } RandomLcg.prototype.random = function random() { this.iState = ((this.iState * 1103515245) + 12345) & 0x7fffffff; return this.iState/(0x7fffffff+1); } RandomLcg.prototype.randInt = function randInt( max ) { return Math.floor(this.random() * max); } let lRand = new RandomLcg(); // Just return a random integer between 0 and max, exclusive on the upper bound. function randInt( max ) { return lRand.randInt( max ); // return Math.floor(Math.random() * max); } // Return the id for the opposite direction of the direction given. function oppositeDir( iDir ) { if( iDir%2 === 1 ) return iDir-1; return iDir+1; } // // Class: Signal // function Signal() { this.aSlot = new Array(); } Signal.prototype.connect = function connect( fSlot ) { this.aSlot.push( fSlot ); } Signal.prototype.call = function call() { for( let j = 0; j < this.aSlot.length; j++ ) { (this.aSlot[j])(); } } // // Class: Cell // function Cell() { this.iDist = 0; this.iPath = 0; this.iWalls = 0; } // // Class: Position // function Position( iDims, ...Vals ) { if( typeof iDims === 'string' ) { let sChunks = iDims.split(','); this.iDims = sChunks.length; this.aiValues = new Array(this.iDims); for( let j = 0; j < this.iDims; j++ ) { this.aiValues[j] = parseInt( sChunks[j].trim(), 10 ); } } else { // Store dimension count this.iDims = iDims; // Check to see if Vals is a non-empty array if( Array.isArray(Vals) && Vals.length > 0 ) { // Make sure Vals has the right number of elements if( Vals.length != iDims ) { throw new Error( 'Position must be initialized with no dimensional data, '+ 'or the correct number of elements.'); } // If it does have the correct number of elements, just // use it instead of creating a new array this.aiValues = Vals; } else { // We don't have values from the constructor, let's just // create a new blank one... this.aiValues = new Array( this.iDims ); // ...and set the position to zero in all dimensions. for( let j = 0; j < this.iDims; j++ ) { this.aiValues[j] = 0; } } } } Position.prototype.getDims = function getDims() { return this.iDims; } Position.prototype.get = function get( iDim ) { return this.aiValues[iDim]; } Position.prototype.set = function set( iDim, iVal ) { this.aiValues[iDim] = iVal; } Position.prototype.add = function add( iDim, iDelta ) { this.aiValues[iDim] += iDelta; return this.aiValues[iDim]; } Position.prototype.translate = function translate( iDim, iDelta ) { let tmp = new Position( this.iDims, ...this.aiValues.slice() ); tmp.add( iDim, iDelta ); return tmp; } Position.prototype.copy = function translate() { return new Position( this.iDims, ...this.aiValues.slice() ); } Position.prototype.equals = function equals( rhs ) { if( this.iDims != rhs.iDims && this.aiValues.length != rhs.aiValues.length ) return false; for( let j = 0; j < this.aiValues.length; j++ ) { if( this.aiValues[j] != rhs.aiValues[j] ) return false; } return true; } Position.prototype.toString = function toString() { let ret = this.aiValues[0].toString(); for( let j = 1; j < this.aiValues.length; j++ ) { ret += ',' + this.aiValues[j].toString(); } return ret; } // // Class: Map // function Map( Dimensions ) { // Store dimensional data this.Dimensions = Dimensions; this.aWorms = new Array(); this.pPlayer = null; this.pGoal = null; this.ePlayerMoved = new Signal(); this.eVictory = new Signal(); this.ePlayerSetup = new Signal(); // Compute the total number of cells let iTotalSize = 1; for( let j = 0; j < Dimensions.getDims(); j++ ) { iTotalSize *= Dimensions.get( j ); } // Allocate cell array, and initialize cells this.aCell = new Array( iTotalSize ); for( let j = 0; j < iTotalSize; j++ ) { this.aCell[j] = new Cell(); } } Map.prototype.getDims = function getDims() { return this.Dimensions.getDims(); } Map.prototype.getSize = function getSize( iDim ) { return this.Dimensions.get( iDim ); } Map.prototype.getPlayerPos = function getPlayerPos() { return this.pPlayer; } Map.prototype.setPlayerPos = function setPlayerPos( pNewPos ) { this.pPlayer = pNewPos; this.ePlayerMoved.call(); } Map.prototype.movePlayer = function movePlayer( iDim, iDelta ) { let cCur = this.get( this.pPlayer ); let iBit = iDim*2; if( iDelta > 0 ) iBit++; if( (cCur.iWalls&(1<= this.getSize( iDim ) ) return; this.pPlayer.add( iDim, iDelta ); this.ePlayerMoved.call(); if( this.pPlayer.equals( this.pGoal ) ) { this.eVictory.call(); } } Map.prototype.isInside = function isInside( Position ) { if( Position.getDims() != this.Dimensions.getDims() ) { throw new Error( 'Number of dimensions in map and position do not match.' ); } for( let j = 0; j < this.getDims(); j++ ) { if( Position.get( j ) < 0 ) return false; if( Position.get( j ) >= this.Dimensions.get( j ) ) return false; } return true; } Map.prototype.getIndex = function getIndex( Position ) { if( !this.isInside( Position ) ) { throw new Error('Position is outside of map.'); } let iIdx = 0; let iScale = 1; for( let j = 0; j < this.getDims(); j++ ) { iIdx += Position.get( j ) * iScale; iScale *= this.Dimensions.get( j ); } return iIdx; } Map.prototype.get = function get( Position ) { return this.aCell[this.getIndex( Position )]; } Map.prototype.addWorm = function addWorm( pStart ) { if( this.pPlayer === null ) { this.pPlayer = pStart; this.ePlayerSetup.call(); } else if( this.pGoal === null ) { this.pGoal = pStart; } let iNewId = this.aWorms.length+1; this.aWorms.push( new Worm( iNewId, pStart, this ) ); return iNewId; } Map.prototype.buildMaze = function buildMaze() { do { for( let j = 0; j < this.aWorms.length; j++ ) { if( !this.aWorms[j].timestep() ) { this.aWorms.splice( j, 1 ); j--; } } } while( this.aWorms.length > 0 ); this.connect( 1, 2 ); } Map.prototype.connect = function connect( iWormId1, iWormId2 ) { let p = new Position( this.getDims() ); let pMax1 = null; let pMax2 = null; let iDistMax = 0; let iDirMax = 0; let iDim = 0; for(;;) { let c = this.get( p ); if( c.iPath === iWormId1 || c.iPath === iWormId2 ) { // This cell is one of the two paths we want to connect, let's // see if there's a cell from the other path nearby. for( iDim = 0; iDim < this.getDims(); iDim++ ) { // Look 'down' in the current dimension let t = p.translate( iDim, -1 ); for( let iDir = 0; iDir < 2; iDir++ ) { // Is the current position inside the maze? if( t.get( iDim ) >= 0 && t.get( iDim ) < this.getSize( iDim ) ) { // Get cell here. let c2 = this.get( t ); if( c.iPath !== c2.iPath && (c2.iPath === iWormId1 || c2.iPath === iWormId2 ) ) { let iDist = c.iDist + c2.iDist; if( iDist > iDistMax ) { iDistMax = iDist; pMax1 = p.copy(); pMax2 = t.copy(); iDirMax = iDim*2+iDir; } } } // Look the other direction t.add( iDim, 2 ); } } } // This is the rediculous engine that lets us iterate through // the entire maze, one cell at a time. This basically increments our // position by one, but wraps at the edges of the maze. for( iDim = 0; iDim < this.getDims(); iDim++ ) { let iNewVal = p.add( iDim, 1 ); if( iNewVal < this.getSize( iDim ) ) break; p.set( iDim, 0 ); } // If we ran out of dimensions then it means that we hit the last // cell in the grid. if( iDim == this.getDims() ) break; } this.get( pMax1 ).iWalls |= (1< 0 ) { // We are near a wall, pick a random wall to open a hole in this.rMap.get(this.pPosition).iWalls |= iDirs[randInt(iDirs.length)]; this.rMap.get(this.pPosition).iPath = this.iId; } } Worm.prototype.timestep = function timestep() { // Handy to reference how many dimensions we have let iDims = this.rMap.getDims(); // Possible directions let pDirs = []; for(;;) { let pBack = null; for( let j = 0; j < iDims; j++ ) { let iSize = this.rMap.getSize( j ); let pPos = this.pPosition.translate( j, -1 ); if( pPos.get( j ) >= 0 ) { let xCell = this.rMap.get( pPos ); if( xCell.iPath === 0 ) { pDirs.push( new Vector( pPos, j*2 ) ); } else if( xCell.iPath === this.iId && xCell.iDist === this.iDist-1 ) { pBack = pPos; } } pPos = this.pPosition.translate( j, 1 ); if( pPos.get( j ) < iSize ) { let xCell = this.rMap.get( pPos ); if( xCell.iPath === 0 ) { pDirs.push( new Vector( pPos, j*2+1 ) ); } else if( xCell.iPath === this.iId && xCell.iDist === this.iDist-1 ) { pBack = pPos; } } } if( pDirs.length > 0 ) { break; } else { if( pBack !== null ) { this.pPosition = pBack; this.iDist--; } else { return false; } } } let iSel = randInt( pDirs.length ); let cCur = this.rMap.get( this.pPosition ); cCur.iWalls |= (1<= 3 ) { cardTd = document.createElement('td'); this.aMoveButtons[4] = cardTd.appendChild( createMoveButton( this.rMap, 2, -1, "Up (^)" ) ); cardRow.appendChild( cardTd ); } cardRow = document.createElement('tr'); cardTbl.appendChild( cardRow ); cardTd = document.createElement('td'); this.aMoveButtons[0] = cardTd.appendChild( createMoveButton( this.rMap, 0, -1, "West" ) ); cardRow.appendChild( cardTd ); cardRow.appendChild( document.createElement('td') ); cardTd = document.createElement('td'); this.aMoveButtons[1] = cardTd.appendChild( createMoveButton( this.rMap, 0, 1, "East" ) ); cardRow.appendChild( cardTd ); if( this.rMap.getDims() >= 3 ) { cardRow.appendChild( document.createElement('td') ); } cardRow = document.createElement('tr'); cardTbl.appendChild( cardRow ); cardRow.appendChild( document.createElement('td') ); cardTd = document.createElement('td'); this.aMoveButtons[3] = cardTd.appendChild( createMoveButton( this.rMap, 1, 1, "South" ) ); cardRow.appendChild( cardTd ); cardRow.appendChild( document.createElement('td') ); if( this.rMap.getDims() >= 3 ) { cardTd = document.createElement('td'); this.aMoveButtons[5] = cardTd.appendChild( createMoveButton( this.rMap, 2, 1, "Down (v)" ) ); cardRow.appendChild( cardTd ); } this.btnBox.appendChild( cardTbl ); cardTbl = document.createElement('table'); for( let j = 3; j < this.rMap.getDims(); j++ ) { cardRow = document.createElement('tr'); cardTbl.appendChild( cardRow ); cardTd = document.createElement('td'); this.aMoveButtons[j*2] = cardTd.appendChild( createMoveButton( this.rMap, j, -1, (j+1) + '-' ) ); cardRow.appendChild( cardTd ); cardTd = document.createElement('td'); this.aMoveButtons[j*2+1] = cardTd.appendChild( createMoveButton( this.rMap, j, 1, (j+1) + '+' ) ); cardRow.appendChild( cardTd ); } this.btnBox.appendChild( cardTbl ); this.updateButtons(); } RenderCanvas2D.prototype = Object.create(Render.prototype); RenderCanvas2D.prototype.constructor = RenderCanvas2D; RenderCanvas2D.prototype.render = function render() { let iSize = this.iCellSize; this.ctx.clearRect( 0, 0, this.eCanvas.width, this.eCanvas.height ); this.ctx.beginPath(); let p; if( this.rMap.pPlayer === null ) p = this.pExtPosition.copy(); else p = this.rMap.pPlayer.copy(); let iPlayerIcon = Math.floor(this.iIconSquare*0.5) + Math.floor(this.iIconSquare*0.5) * this.iIconSquare; for( let x = 0; x < this.rMap.getSize( 0 ); x++ ) { for( let y = 0; y < this.rMap.getSize( 1 ); y++ ) { p.set( 0, x ); p.set( 1, y ); let c = this.rMap.get( p ); if( p.equals( this.rMap.pGoal ) ) { let oldStyle = this.ctx.fillStyle; this.ctx.fillStyle = 'palegreen'; this.ctx.fillRect( x*iSize+2, y*iSize+2, iSize-4, iSize-4 ); this.ctx.fillStyle = oldStyle; } if( (c.iWalls&1) === 0 && x === 0) { this.ctx.moveTo( x*iSize, y*iSize ); this.ctx.lineTo( x*iSize, (y+1)*iSize ); } if( (c.iWalls&2) === 0 ) { this.ctx.moveTo( (x+1)*iSize, y*iSize ); this.ctx.lineTo( (x+1)*iSize, (y+1)*iSize ); } if( (c.iWalls&4) === 0 && y === 0) { this.ctx.moveTo( x*iSize, y*iSize ); this.ctx.lineTo( (x+1)*iSize, y*iSize ); } if( (c.iWalls&8) === 0 ) { this.ctx.moveTo( x*iSize, (y+1)*iSize ); this.ctx.lineTo( (x+1)*iSize, (y+1)*iSize ); } // Extended dimenisons (above 2 :-P) let iIcon = 0; for( let ed = 2; ed < this.rMap.getDims(); ed++ ) { if( iIcon == iPlayerIcon ) iIcon++; if( (c.iWalls&(1<<(ed*2))) !== 0 ) { this.renderDirIcon( x, y, iIcon, ed*2 ); } iIcon++; if( iIcon == iPlayerIcon ) iIcon++; if( (c.iWalls&(1<<(ed*2+1))) !== 0 ) { this.renderDirIcon( x, y, iIcon, ed*2+1 ); } iIcon++; } } } this.ctx.stroke(); // Draw the player if( this.rMap.pPlayer !== null ) { let bx = this.rMap.pPlayer.get(0)*iSize + this.iBorder + Math.floor(iPlayerIcon%this.iIconSquare)*(this.iBorder+this.iIconSize); let by = this.rMap.pPlayer.get(1)*iSize + this.iBorder + Math.floor(iPlayerIcon/this.iIconSquare)*(this.iBorder+this.iIconSize); this.rMap.pPlayer.equals( p ); this.ctx.beginPath(); this.ctx.ellipse( bx+this.iIconSize*0.5, by+this.iIconSize*0.5, this.iIconSize*0.4, this.iIconSize*0.4, 0, Math.PI*2.0, false ); this.ctx.fill(); } } RenderCanvas2D.prototype.renderDirIcon = function renderDirIcon( x, y, iIcon, iDir ) { let bx = x*this.iCellSize + this.iBorder + (iIcon%this.iIconSquare)*(this.iIconSize+this.iBorder); let by = y*this.iCellSize + this.iBorder + Math.floor(iIcon/this.iIconSquare)*(this.iIconSize+this.iBorder); //this.ctx.rect(bx, by, this.iIconSize, this.iIconSize ); switch( iDir ) { case 4: // Up this.ctx.moveTo( bx, by+this.iIconSize ); this.ctx.lineTo( bx+this.iIconSize/2, by ); this.ctx.lineTo( bx+this.iIconSize, by+this.iIconSize ); break; case 5: // Down this.ctx.moveTo( bx, by ); this.ctx.lineTo( bx+this.iIconSize/2, by+this.iIconSize ); this.ctx.lineTo( bx+this.iIconSize, by ); break; default: let label = Math.floor((iDir/2.0)+1).toString() + (((iDir%2)===0)?'-':'+'); this.ctx.fillText( label, bx, by ); break; } } RenderCanvas2D.prototype.updateButtons = function updateButtons() { let c = this.rMap.get( this.rMap.pPlayer ); for( let j = 0; j < this.rMap.getDims()*2; j++ ) { this.aMoveButtons[j].disabled = (c.iWalls&(1<= 0 ) { trgUrl = trgUrl.substring(0, query); } trgUrl += '?seed=' + lRand.getSeed() + '&dims=' + p.toString(); dLink.href = trgUrl; dLink.appendChild( document.createTextNode('Share this maze!') ); d.appendChild(dLink); document.body.appendChild( document.createElement('br') ); document.body.appendChild( d );