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/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}
+
+
+
+
+
+
+
+