commit de2786cf41509bb21826b52608e54e1998dc1824 Author: Mikayla Dobson Date: Thu Jul 6 10:19:00 2023 -0500 init diff --git a/index.js b/index.js new file mode 100644 index 0000000..f166ead --- /dev/null +++ b/index.js @@ -0,0 +1,222 @@ +/** + * http://www.fryes4fun.com/Bowling/scoring.htm + * + * high level overview: + * one player per traditional round + * each frame is mostly the same + * 10 frame total + * final frame may have 2-3 throws + * + * score for turn can be dependent on the next two throws + * (i.e. strike or spare) + */ + +class Frame { + // an open frame is any frame in which all ten pins are not knocked down + open + strike + spare + pinsKnockedDown + throws + + constructor(open, strike, spare, pinsKnockedDown, throws) { + this.open = open || false; + this.strike = strike || false; + this.spare = spare || false; + this.pinsKnockedDown = pinsKnockedDown || 0; + this.throws = throws || [0, 0, 0]; + } +} + +const STRIKE = 10; + +class Bowling { + /** for keeping track of score state, what kinds of throws happened throughout game, etc. */ + scorecard + currentThrow + currentFrame + /** for overall score representation, i.e. the cumulative results from data tracked on the scorecard */ + score + + constructor(score = 0, currentFrame = 0, currentThrow = 1) { + this.score = score; + this.currentFrame = currentFrame; + this.currentThrow = currentThrow; + + this.scorecard = [] + + for (let i = 0; i < 10; i++) { + this.scorecard.push(new Frame()); + } + + return this; + } + + /** semantic helpers for applying conditional logic based on proximity to endgame */ + isFinalFrame() { + return this.currentFrame === 9; + } + + isNotFinalFrame() { + return this.currentFrame !== 9; + } + + /** getters and setters for managing Frame class */ + setThrowResult(result) { + this.scorecard[this.currentFrame].throws[this.currentThrow - 1] = result; + } + + setPinsKnockedDown(pinsKnockedDown) { + this.scorecard[this.currentFrame].pinsKnockedDown = pinsKnockedDown; + } + + getPinsKnockedDown() { + return this.scorecard[this.currentFrame].pinsKnockedDown; + } + + /** + * attempt to knock down pins until the rules dictate that we can't any more + * at this point, we break from the function + * + * @returns true when the frame is over, false when the frame is not over + */ + throwTheBall() { + const remainingPins = 10 - this.scorecard[this.currentFrame].pinsKnockedDown; + + // we still base our result on 10 total possible, and use the maximum possible value in the case of + // "knocking down pins that aren't there" + // otherwise our RNG is too strict and it's highly improbable that we ever throw strikes or spares + const throwResult = Math.min(Math.floor(Math.random() * 11), remainingPins); + + // record the throw result in the corresponding throw record in the scorecard + this.setThrowResult(throwResult); + + if (throwResult === STRIKE) { + this.scorecard[this.currentFrame].strike = true; + this.setPinsKnockedDown(STRIKE); + + // unless it is the final turn of the game, rolling a strike will end this turn + if (this.isNotFinalFrame()) { + return true; + } + + // in the final turn of the game, rolling a strike on the first turn awards you two additional throws + if (this.currentThrow < 3) { + this.currentThrow += 1; + return false; + } + + return true; + /** spare case (not the first throw AND all the pins are knocked down) */ + } else if (throwResult === remainingPins) { + this.scorecard[this.currentFrame].spare = true; + this.setPinsKnockedDown(STRIKE); + + if (this.isNotFinalFrame()) { + return true; + } + + // in the final turn of the game, rolling a strike on the first turn awards you two additional throws + if (this.currentThrow < 3) { + this.currentThrow += 1; + return false; + } + + return true; + } else { + /** add the number of pins knocked down to your total pins knocked down for this roll */ + this.setPinsKnockedDown(this.getPinsKnockedDown() + throwResult); + let frameIsOver = false; + + if (this.isNotFinalFrame()) { + frameIsOver = this.currentThrow >= 2; + + if (frameIsOver) return true; + + this.currentThrow += 1; + return false; + } + + frameIsOver = this.currentThrow >= 3; + + if (frameIsOver) return true; + + this.currentThrow += 1; + return false; + } + } + + /** throw the ball until the criteria for completing a frame have been met */ + completeFrame() { + let frameComplete = this.throwTheBall(); + + while (!frameComplete) { + frameComplete = this.throwTheBall(); + } + + if (this.getPinsKnockedDown() !== STRIKE) { + this.scorecard[this.currentFrame].open = true; + } + + this.currentFrame += 1; + this.currentThrow = 1; + } + + /** + * strikes and spares have a scoring function that is relative to the throws that happen after the + * strike or spare. from the above link: + * + * - A strike earns 10 points plus the sum of your next two shots + * - A spare earns 10 points plus the sum of your next one shot + */ + calculateScore() { + let i = 0; + console.log("SCORING:") + + while (i < 9) { + let scoringExplanation = `Frame ${i + 1}: ${this.scorecard[i].pinsKnockedDown}`; + if (i === 0) { + this.score += this.scorecard[i].pinsKnockedDown; + } else if (this.scorecard[i - 1].strike) { + // add the strike value with the sum of the points for the next two throws + scoringExplanation += ` PLUS strike on frame ${i} results in additional ${this.scorecard[i].throws[0]} + ${this.scorecard[i].throws[1]} from next two throws` + this.score += this.scorecard[i].pinsKnockedDown + this.scorecard[i].throws[0] + this.scorecard[i].throws[1]; + } else if (this.scorecard[i - 1].spare) { + // add the value of the first throw to your score for this frame + scoringExplanation += ` PLUS spare on frame ${i} results in additional ${this.scorecard[i].throws[0]} from next throw` + this.score += this.scorecard[i].pinsKnockedDown + this.scorecard[i].throws[0]; + } else { + this.score += this.scorecard[i].pinsKnockedDown; + } + + console.log(scoringExplanation); + i++; + } + } + + /** "main" method that executes the component methods in the correct order */ + play() { + console.log("\n") + console.log("COMPLETELY ACCURATE BOWLING SIMULATOR") + console.log("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~") + console.log("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~") + console.log("\n") + + while (true) { + if (this.currentFrame === 10) break; + this.completeFrame(); + } + + this.calculateScore(); + + // logging for scoring details + console.log(`\nYour score was: ${this.score}`); + console.log("Scorecard:\n"); + + this.scorecard.forEach(frame => { + console.log(`${frame.throws} ${frame.strike ? "(strike)" : frame.spare ? "(spare)" : ""}`); + }) + } +} + +new Bowling().play(); diff --git a/package.json b/package.json new file mode 100644 index 0000000..aaa0981 --- /dev/null +++ b/package.json @@ -0,0 +1,12 @@ +{ + "name": "bowling-in-javascript", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC" +}