Files
js-bowling/index.js
2023-07-06 10:19:00 -05:00

223 lines
7.3 KiB
JavaScript

/**
* 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();