diff --git a/.vscode/settings.json b/.vscode/settings.json index 11e5c7f..e032b14 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,5 +10,9 @@ "cssvar.ignore": [], // add support for autocomplete in JS or JS like files - "cssvar.extensions": ["css", "postcss", "svelte"] + "cssvar.extensions": [ + "css", + "postcss", + "svelte" + ] } diff --git a/src/lib/components/Container.svelte b/src/lib/components/Container.svelte new file mode 100644 index 0000000..427b1ee --- /dev/null +++ b/src/lib/components/Container.svelte @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/lib/components/PlayerStats.svelte b/src/lib/components/PlayerStats.svelte index 86a9757..398c162 100644 --- a/src/lib/components/PlayerStats.svelte +++ b/src/lib/components/PlayerStats.svelte @@ -2,9 +2,10 @@ import type { NineBallPlayer, NineBallGame } from '$lib'; import type { EightBallGame } from '$lib/eight-ball'; import type { Player as EightBallPlayer } from '$lib/index/player'; + import type { StraightPoolGame, StraightPoolPlayer } from '$lib/straight-pool'; - export let player: NineBallPlayer | EightBallPlayer; - export let game: NineBallGame | EightBallGame; + export let player: NineBallPlayer | EightBallPlayer | StraightPoolPlayer; + export let game: NineBallGame | EightBallGame | StraightPoolGame; export let playerNumber: number; diff --git a/src/lib/components/ProgressBar.svelte b/src/lib/components/ProgressBar.svelte index 2f0fa47..4605f76 100644 --- a/src/lib/components/ProgressBar.svelte +++ b/src/lib/components/ProgressBar.svelte @@ -3,8 +3,9 @@ import type { EightBallPlayer } from '$lib/eight-ball'; import { tweened } from 'svelte/motion'; import { cubicOut } from 'svelte/easing'; + import type { StraightPoolPlayer } from '$lib/straight-pool'; - export let player: NineBallPlayer | EightBallPlayer; + export let player: NineBallPlayer | EightBallPlayer | StraightPoolPlayer; let className = ''; export { className as class }; export let reverse = false; diff --git a/src/lib/components/Scoreboard.svelte b/src/lib/components/Scoreboard.svelte index 479d64f..bb4ef2c 100644 --- a/src/lib/components/Scoreboard.svelte +++ b/src/lib/components/Scoreboard.svelte @@ -1,8 +1,9 @@
diff --git a/src/lib/components/straight-pool/ControlPad.svelte b/src/lib/components/straight-pool/ControlPad.svelte new file mode 100644 index 0000000..ed3755a --- /dev/null +++ b/src/lib/components/straight-pool/ControlPad.svelte @@ -0,0 +1,39 @@ + + +
+ + +
+ + + +
+ +
diff --git a/src/lib/straight-pool/actions.ts b/src/lib/straight-pool/actions.ts new file mode 100644 index 0000000..3b5b9eb --- /dev/null +++ b/src/lib/straight-pool/actions.ts @@ -0,0 +1,79 @@ +interface IIncrement { + type: 'INCREMENT'; +} + +interface IDecrement { + type: 'DECREMENT'; +} + +interface ISafety { + type: 'SAFETY'; +} + +interface IMiss { + readonly type: 'MISS'; + readonly deadBallCount: number; +} + +interface IEndRack { + type: 'END_RACK'; + deadBallCount: number; +} + +interface IUndo { + type: 'UNDO'; +} + +interface IRedo { + type: 'REDO'; +} + +interface ITimeOut { + type: 'TIMEOUT'; +} + + +export class Increment implements IIncrement { + readonly type = 'INCREMENT'; +} + +export class Decrement implements IDecrement { + readonly type = 'DECREMENT'; +} + +export class Safety implements ISafety { + readonly type = 'SAFETY'; +} + +export class Miss implements IMiss { + readonly type = 'MISS'; + constructor(readonly deadBallCount: number = 0) {} +} + +export class EndRack implements IEndRack { + readonly type = 'END_RACK'; + deadBallCount = 0; +} + +export class Undo implements IUndo { + readonly type = 'UNDO'; +} + +export class Redo implements IRedo { + readonly type = 'REDO'; +} + +export class Timeout implements ITimeOut { + readonly type = 'TIMEOUT'; +} + +export type Action = + | Increment + | Decrement + | Safety + | Miss + | EndRack + | Undo + | Redo + | Timeout + diff --git a/src/lib/straight-pool/index.ts b/src/lib/straight-pool/index.ts new file mode 100644 index 0000000..17e18d4 --- /dev/null +++ b/src/lib/straight-pool/index.ts @@ -0,0 +1,241 @@ +import type { Action, EndRack } from './actions'; +import type { StraightPoolPlayer } from './player'; +import type { BallModel } from '$lib/components/Ball.svelte'; + +class AssertionError extends Error { + constructor(cause: string) { + super('Assertion Error: ' + cause); + } +} + +const BALL_COLORS: string[] = [ + 'yellow', + 'blue', + 'red', + 'purple', + 'orange', + 'green', + 'maroon', + 'black' +]; + +export class StraightPoolGame { + readonly type = 'straightPool'; + players: [StraightPoolPlayer, StraightPoolPlayer]; + winner: StraightPoolPlayer | null = null; + racks = [new StraightPoolRack(0)]; + actions: Action[] = []; + undoneActions: Action[] = []; + + constructor(player1: StraightPoolPlayer, player2: StraightPoolPlayer) { + this.players = [player1, player2]; + } + + get player1() { + return this.players[0]; + } + + get player2() { + return this.players[1]; + } + + get totalInnings() { + return this.racks.reduce((n, { innings }) => n + innings, 0); + } + + get currentRack() { + const rack = this.racks.at(-1); + if (!rack) throw new AssertionError('current rack should always be defined'); + return rack; + } + + get currentPlayer() { + return this.players[this.currentRack.turn]; + } + + get previousPlayer() { + const prevPlayer = this.currentRack.turn === 0 ? 1 : 0; + return this.players[prevPlayer]; + } + + endRack() { + const additionalDeadBalls = this.currentRack.endRack(); + this.racks.push(new StraightPoolRack(this.currentRack.turn)); + return additionalDeadBalls; + } + + unEndRack(action: EndRack) { + this.racks.pop(); + this.currentRack.unEndRack(action.deadBallCount); + } + + increment() { + this.currentPlayer.score++; + this.currentRack.increment(); + } + + decrement() { + if (this.currentPlayer.score) { + this.currentPlayer.score--; + this.currentRack.decrement(); + } + } + + + undoAction(action: Action) { + switch (action.type) { + // undo and redo should never get here because they don't get pushed to either stack + case 'DECREMENT': + this.increment(); + break; + case 'INCREMENT': + this.decrement() + break; + case 'SAFETY': + this.currentPlayer.safeties--; + break; + case 'MISS': + this.currentRack.unEndTurn(); + break; + case 'END_RACK': + // save deadBalls for use in redo + this.unEndRack(action); + break; + case 'TIMEOUT': + this.currentRack.unUseTimeout(); + break; + default: + throw new AssertionError('unexpected action'); + } + this.undoneActions.push(action); + } + + doAction(action: Action) { + switch (action.type) { + case 'UNDO': + const actionToUndo = this.actions.pop(); + if (actionToUndo) this.undoAction(actionToUndo); + return; + case 'REDO': + const actionToRedo = this.undoneActions.pop(); + if (actionToRedo) this.doAction(actionToRedo); + return; + case 'DECREMENT': + this.decrement() + break; + case 'INCREMENT': + this.increment() + break; + case 'SAFETY': + this.currentPlayer.safeties++; + break; + case 'MISS': + this.currentRack.endTurn(); + break; + case 'TIMEOUT': + this.currentRack.useTimeout(); + break; + case 'END_RACK': + // save deadBalls for use in redo + action.deadBallCount = this.endRack(); + break; + default: + throw new AssertionError('unexpected action'); + } + this.actions.push(action); + // clear undone actions because history has been overridden + this.undoneActions.length = 0; + } +} + +export class StraightPoolRack { + static RACK_POINTS = 10; + innings = 0; + deadBallCount = 0; + scores = [0, 0]; + turn = 0; + timeouts = [1, 1]; + gameBalls = this.createBalls(); + pocketedBalls: BallModel[] = []; + deadBalls: BallModel[] = []; + + constructor(turn: number) { + this.turn = turn; + } + + endTurn() { + this.changeTurn(); + if (this.turn === 0) { + this.innings++; + } + } + + unEndTurn() { + this.changeTurn(); + if (this.turn === 1) { + this.innings--; + } + } + + private changeTurn() { + this.turn = (this.turn + 1) % 2; + } + + // returns additional dead balls + endRack() { + const additional = StraightPoolRack.RACK_POINTS - this.deadBallCount - this.total; + this.deadBallCount += additional; + return additional; + } + + unEndRack(deadBallsToRestore: number) { + this.deadBallCount -= deadBallsToRestore; + } + + increment() { + this.scores[this.turn] += 1; + } + + decrement() { + this.scores[this.turn] -= 1; + } + + useTimeout() { + if (this.timeouts[this.turn]) { + this.timeouts[this.turn]--; + } + } + + unUseTimeout() { + //since you use a timeout while its a players turn, undoing actions should always lead to it being this.turn + this.timeouts[this.turn]++; + } + + private createBalls() { + const balls = []; + + for (let i = 0; i < 9; i++) { + const ball: BallModel = { + number: i + 1, + color: BALL_COLORS[i % BALL_COLORS.length], + isStripe: i >= 8, + isDead: false, + isPocketed: false, + isPostKill: false + }; + balls.push(ball); + } + return balls; + } + + get total() { + return this.scores.reduce((a, b) => a + b); + } + + get leftOverBalls() { + return this.gameBalls.filter((ball) => !ball.isPocketed); + } +} + +export * as Actions from './actions'; +export * from './player'; diff --git a/src/lib/straight-pool/player.ts b/src/lib/straight-pool/player.ts new file mode 100644 index 0000000..ce806c7 --- /dev/null +++ b/src/lib/straight-pool/player.ts @@ -0,0 +1,17 @@ +export class StraightPoolPlayer { + constructor(name: string, scoreRequired: number, color: string) { + this.name = name; + this.scoreRequired = scoreRequired; + this.color = color; + } + + name = ''; + color = ''; + score = 0; + safeties = 0; + scoreRequired = 100; + + get progressPercent() { + return this.score / this.scoreRequired; + } +} diff --git a/src/routes/setup/+page.svelte b/src/routes/setup/+page.svelte index 2cfcc06..4371c58 100644 --- a/src/routes/setup/+page.svelte +++ b/src/routes/setup/+page.svelte @@ -6,10 +6,8 @@ import { RuleForm, type PlayerFormData } from '$lib/components'; import WarningIcon from '$lib/components/WarningIcon.svelte'; import { startCase } from 'lodash'; - import type { GameType, RuleType } from '$lib/types.js'; - export let data; let { game, toast, toastTime } = data; diff --git a/src/routes/straight-pool/+page.svelte b/src/routes/straight-pool/+page.svelte new file mode 100644 index 0000000..f235679 --- /dev/null +++ b/src/routes/straight-pool/+page.svelte @@ -0,0 +1,33 @@ + + + + + {#each game.players as player, playerNumber} + + {/each} + {#each game.players as player, playerNumber} + + {/each} + + + + + + + +