Skip to content

Instantly share code, notes, and snippets.

@HauntedHorse
Last active October 6, 2020 04:11
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save HauntedHorse/0e3c41285cfc99349d1b2cb4b6c7d281 to your computer and use it in GitHub Desktop.
Save HauntedHorse/0e3c41285cfc99349d1b2cb4b6c7d281 to your computer and use it in GitHub Desktop.
Find Your Hat challenge project from Codeacademy Pro
// 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