-
-
Save HauntedHorse/0e3c41285cfc99349d1b2cb4b6c7d281 to your computer and use it in GitHub Desktop.
Find Your Hat challenge project from Codeacademy Pro
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// import prompt-sync module and set signal interuption to true, | |
// allowing users to interact with prompts in the terminal | |
// and exit at will by pressing Ctrl+C | |
const prompt = require('prompt-sync')({sigint: true}); | |
// global variables representing field tiles | |
const hat = '^'; | |
const hole = 'O'; | |
const fieldCharacter = '░'; | |
const pathCharacter = '*'; | |
const gameState = { | |
isActive: true, | |
numOfMoves: 0, | |
hardMode: false, | |
} | |
// field class accepts a two dimensional array containing ASCII tiles representing the field | |
class Field { | |
constructor(field) { | |
this._field = field | |
this._playerPath = [[0, 0]] // stores player path as elements in a multidimensional array with format [y, x] | |
this._playerPos = { // default player position is (0, 0) | |
y: 0, | |
x: 0 | |
} | |
} | |
get field() { | |
return this._field; | |
} | |
get playerPosX() { | |
return this._playerPos.x; | |
} | |
get playerPosY() { | |
return this._playerPos.y; | |
} | |
get playerPosition() { // playerPosition is returned as an Object | |
return this._playerPos; | |
} | |
get playerPath() { // path is returned as a multidimensional array | |
return this._playerPath; | |
} | |
// return most recent player position as an array with format [y, x] | |
get lastPosition() { | |
if (this._playerPath.length - 2 > 0) { | |
return this._playerPath[this._playerPath.length - 2]; | |
} else { | |
return this._playerPath[0]; | |
} | |
} | |
// prints the field array to the console line by line | |
print() { | |
if (this.field) { | |
this.field.forEach(element => console.log(element.join(''))); | |
} | |
} | |
// helper methods to determine whether a player | |
// is on a hat, hole, or path tile, or out of bounds | |
isHole(y, x) { | |
return this.field[y][x] === hole | |
} | |
isHat(y, x) { | |
return this.field[y][x] === hat | |
} | |
isPath(y, x) { | |
return this.field[y][x] === pathCharacter | |
} | |
isOutOfBounds(y, x) { | |
return (y < 0 || x < 0 || y >= this.field.length || x >= this.field[0].length) | |
} | |
// helper method to replace a field tile with a path character | |
proceedPath(y, x) { | |
this._field[y].splice(x, 1, pathCharacter); | |
} | |
// helper method to detect if field characters remain on the field | |
// to prevent endless looping of tile placing method(s) | |
fieldCharRemains(field) { | |
return this.field.some(element => element.includes(fieldCharacter)); | |
} | |
// place a specified tile in a random space unoccupied by a hole or hat | |
randomTile(char) { | |
let x; | |
let y; | |
let isBlank = false; | |
let isFull = !this.fieldCharRemains(this.field); | |
while (!isBlank && !isFull) { | |
x = Math.floor(Math.random() * this.field[0].length); | |
y = Math.floor(Math.random() * this.field.length); | |
if ((!this.arrayEquals(this.field[y], this.playerPath[this.playerPath.length - 1])) && | |
(this.field[y][x] === fieldCharacter || this.field[y][x] === pathCharacter)) { | |
isBlank = true; | |
this._field[y].splice(x, 1, char); | |
} | |
isFull = !this.fieldCharRemains(this.field); // break the loop if field is | |
} // completely filled | |
} | |
// helper method to detect if two arrays are strictly equal to each other | |
arrayEquals(arr1, arr2) { | |
return arr1.length === arr2.length && | |
arr1.every((element, index) => element === arr2[index]); | |
} | |
// remove looped path tiles from the display to prevent cluttering the field | |
// NOTE: does not alter record of player path | |
destroyPath() { | |
let index = 0; | |
if (this.playerPath.length > 0) { | |
const testArr = this.playerPath.slice(0, this.playerPath.length - 2); | |
for (let i = 0; i < testArr.length; i++) { | |
if (this.arrayEquals(testArr[i], this.playerPath[this.playerPath.length - 1])) { | |
index = i; | |
} | |
} | |
const toReplace = this.playerPath.slice(index); | |
toReplace.forEach(element => { | |
this._field[element[0]].splice(element[1], 1, fieldCharacter) | |
}); | |
} | |
} | |
// update player position based on user input | |
updatePlayerPos(dir) { | |
if (dir === 'right') { | |
this._playerPos.x++; | |
} else if (dir === 'down') { | |
this._playerPos.y++; | |
} else if (dir === 'left') { | |
this._playerPos.x--; | |
} else if (dir === 'up') { | |
this._playerPos.y--; | |
} | |
this._playerPath.push([this.playerPosY, this.playerPosX]); | |
} | |
// overwrite default field with a randomized starting location | |
randomStart() { | |
this._field[0].splice(0, 1, fieldCharacter); | |
let x; | |
let y; | |
let isOpen = false; | |
while(!isOpen) { | |
x = Math.round(Math.random() * this.field[0].length); | |
y = Math.round(Math.random() * this.field.length); | |
if (this.field[y][x] !== hat) { | |
isOpen = true; | |
this._field[y].splice(x, 1, pathCharacter); | |
this._playerPos.y = y; | |
this._playerPos.x = x; | |
this._playerPath.pop(); | |
this._playerPath.push([y, x]); | |
} | |
} | |
} | |
// static method to generate a random field containing one hat and a variety of holes | |
static generateValidField(height, width, percentHoles) { | |
const generateField = (height, width, percentHoles) => { | |
if (percentHoles < 0 || percentHoles > 100) { | |
throw new RangeError (`Percentage of holes must be a value between 0 and 100`); | |
} | |
if (height <= 0 || width <= 0 || !Number.isInteger(height) || !Number.isInteger(width)) { | |
throw new RangeError (`Height and width must be positive integer values`); | |
} | |
const numHoles = Math.round((height * width) * (percentHoles / 100)); | |
const field = []; | |
let x; | |
let y; | |
// helper method to detect if field characters remain on the field | |
// to prevent endless looping of tile placing method(s) | |
const fieldCharRemains = (field) => { | |
return field.some(element => element.includes(fieldCharacter)); | |
} | |
// initialize a multidimensional array of blank field characters | |
for (let h = 0; h < height; h++) { | |
const subField = []; | |
for (let w = 0; w < width; w++) { | |
subField.push(fieldCharacter); | |
} | |
field.push(subField); | |
} | |
//the starting point, (0, 0), is indicated by a pathCharacter (*) | |
field[0].splice(0, 1, pathCharacter); | |
// helper method to place tiles on the field without repeats | |
// NOTE: this method operates by detecting whether fieldCharacters remain, | |
// if any field characters are permanently embedded in the field | |
// the loop will not be broken | |
const placeTile = (type) => { | |
let isBlank = false; | |
let isFull = !fieldCharRemains(field); | |
while (!isBlank && !isFull) { | |
x = Math.floor(Math.random() * width); | |
y = Math.floor(Math.random() * height); | |
if (field[y][x] === fieldCharacter) { | |
isBlank = true; | |
field[y].splice(x, 1, type); | |
} | |
isFull = !fieldCharRemains(field); // break the loop if field is | |
} // completely filled | |
} | |
// place a hat | |
placeTile(hat); | |
// then fill the remaining field tiles with holes | |
for (let i = 0; i < numHoles; i++) { | |
placeTile(hole); | |
} | |
return field | |
} | |
// returns true if called on a field with a valid possible path to the hat | |
const validateField = (field) => { | |
// instantiates a blank boolean array of the same size as the field, to be updated | |
// with "true" values representing spots the crawler has already checked | |
const wasHere = []; | |
for (let i = 0; i < field.length; i++) { | |
let subField = []; | |
for (let j = 0; j < field[0].length; j++) { | |
subField.push(false); | |
} | |
wasHere.push(subField); | |
} | |
// recursive traversal method, returns true when the hat is found | |
// or false if it cannot locate a hat | |
const traverse = (y, x) => { | |
if (field[y][x] === hat) { | |
return true; | |
} else if (wasHere[y][x] === true || field[y][x] === hole) { | |
return false; | |
} else { | |
wasHere[y][x] = true; | |
if (y < field.length - 1) { | |
return traverse(y + 1, x) ? true : false; | |
} | |
if (x < field[y].length - 1) { | |
return traverse(y, x + 1) ? true : false; | |
} | |
if (y > 0) { | |
return traverse(y - 1, x) ? true : false | |
} | |
if (x > 0) { | |
return traverse(y, x - 1) ? true : false; | |
} | |
} | |
} | |
return traverse(0, 0); | |
} | |
let isValid = false; | |
let attempts = 0; | |
while (!isValid) { | |
let newField = generateField(height, width, percentHoles); | |
if (validateField(newField)) { | |
isValid = true; | |
return newField; | |
} else if (attempts >= 100) { | |
console.log('A valid field could not be generated within 100 attempts.'); | |
console.log('Please check your parameters and try again.\n'); | |
throw new Error(); | |
} else { | |
attempts++; | |
} | |
} | |
} | |
} | |
// called at the end of each round to update the game state depending on new conditions | |
const update = (field, y, x) => { | |
gameState.numOfMoves++; | |
if (field.isOutOfBounds(y, x)) { | |
console.log('\nYou fell off the map! \nNow you can never find your hat.\nGame Over. \n'); | |
gameState.isActive = false; | |
} else if (field.isHole(y, x)) { | |
console.log('\nYou fell into a hole!\nThere is no hat here.\nGame Over. \n'); | |
gameState.isActive = false; | |
} else if (field.isHat(y, x)) { | |
console.log('\nYou have reunited with your hat!\nPut on that beautiful hat!'); | |
console.log(' ^ \n O \n --|-- \n / \\ \n'); | |
/* expected output: a guy with a hat! | |
^ | |
O | |
--|-- | |
/ \ | |
*/ | |
gameState.isActive = false; | |
} else { | |
if (field.isPath(y, x)) { | |
field.destroyPath(y, x); | |
console.log('\nYou have intersected your own path!'); | |
console.log('Excess path tiles have been removed.\nYour score remains the same'); | |
} | |
field.proceedPath(y, x); | |
} | |
if (gameState.hardMode) { | |
if (gameState.numOfMoves % 5 === 0) { | |
field.randomTile(hole); | |
} | |
} | |
} | |
// function to execute gameplay script based on an accepted field parameter | |
const play = (field) => { | |
let hardMode = prompt('Activate hard mode? (Y/N)').toLowerCase().trim(); | |
if (hardMode === 'y') { | |
gameState.hardMode = true; | |
} | |
console.log('Random starting location?'); | |
console.log('Note: This may result in an unwinnable field.'); | |
let randomStart = prompt('(Y/N)').toLowerCase().trim(); | |
if (randomStart === 'y') { | |
field.randomStart(); | |
} | |
// debugging log | |
// console.log(`Winnable field: ${newField.validateField()}`); | |
while(gameState.isActive) { | |
console.log(''); // empty line for formatting | |
field.print(); | |
console.log(''); | |
let isValid = false; // check for valid input | |
let input; | |
while (!isValid) { | |
input = prompt('Which direction would you like to go?').toLowerCase().trim(); | |
if (input === 'right' || input === 'left' || input === 'up' || input === 'down') { | |
isValid = true; | |
} else { | |
console.log('\nPlease specify a valid direction:\nright, left, up, or down.\n'); | |
} | |
} | |
field.updatePlayerPos(input); | |
update(field, field.playerPosY, field.playerPosX); | |
console.log(`Number of Moves: ${gameState.numOfMoves}`) | |
/* debugging logs | |
console.log(field.playerPosition) | |
console.log(`Last Position: { y: ${field.lastPosition[0]} x: ${field.lastPosition[1]} }`); | |
console.log(field.playerPath); | |
*/ | |
} | |
} | |
const newField = new Field(Field.generateValidField(10, 10, 20)); | |
play(newField); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment