Skip to content

Commit

Permalink
initial. board is grid. pieces can move forward.
Browse files Browse the repository at this point in the history
  • Loading branch information
nbogie committed Sep 8, 2023
1 parent ec74066 commit 494d2b8
Show file tree
Hide file tree
Showing 12 changed files with 346 additions and 3 deletions.
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@
"check-all": "yarn type-check && yarn lint && yarn format && yarn test"
},
"dependencies": {
"clsx": "^2.0.0",
"immer": "^10.0.2",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react-dom": "^18.2.0",
"use-immer": "^0.9.0"
},
"devDependencies": {
"@testing-library/jest-dom": "^5.16.4",
Expand Down
81 changes: 81 additions & 0 deletions src/GameState.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { NumPlayers, Player, PlayerId, createPlayers } from "./Player";
import { DiceRoll } from "./reducer/reducerFunction";

export type DiveRoute = Piece[];
export interface GameState {
remainingAir: number;
diveRoute: DiveRoute;
players: Player[];
whoseTurn: PlayerId;
lastRoll: DiceRoll | null;
}

export type Piece = TreasurePiece | BlankPiece;
export type PieceId = string & { readonly __brand: "pieceId" };
export type TreasurePiece = {
id: PieceId;
tag: "treasure";
state: "faceUp" | "faceDown";
shape: TreasureShape;
value: TreasureValue;
};
export type BlankPiece = { tag: "blank"; id: string };
export type TreasureShape = "triangle" | "square" | "pentagon" | "hexagon";
export type TreasureValue = number;
export type TreasureLevel = 1 | 2 | 3 | 4;

export function createInitialState(numPlayers: NumPlayers): GameState {
const treasurePieces: TreasurePiece[] = shuffle(createTreasurePieces());
const players: Player[] = createPlayers(numPlayers);
return {
players,
remainingAir: 25,
diveRoute: treasurePieces,
whoseTurn: players[0].id,
lastRoll: null,
};
}
function shuffle<T>(arr: T[]): T[] {
return [...arr].sort(() => (Math.random() < 0.5 ? -1 : 1));
}

function createTreasurePieces(): TreasurePiece[] {
const pieces: TreasurePiece[] = [];
let pieceId = 1;
for (let value = 0; value < 16; value++) {
const level = (1 + Math.floor(value / 4)) as TreasureLevel;
if (!isTreasureLevel(level)) {
throw new Error(
"illegal treasure level: " + level + " from value: " + value
);
}

for (let i = 0; i < 2; i++) {
const piece: TreasurePiece = {
id: ("" + pieceId++) as PieceId,
tag: "treasure",
state: "faceDown",
shape: shapeForTreasureLevel(level),
value,
};
pieces.push(piece);
}
}
return pieces;
}
function shapeForTreasureLevel(level: TreasureLevel): TreasureShape {
const lookup: Record<TreasureLevel, TreasureShape> = {
1: "triangle",
2: "square",
3: "pentagon",
4: "hexagon",
};
const s = lookup[level];
if (s === undefined) {
throw new Error("missing shape for level: " + level);
}
return s;
}
function isTreasureLevel(level: number): level is TreasureLevel {
return [1, 2, 3, 4].includes(level);
}
33 changes: 33 additions & 0 deletions src/Player.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { TreasurePiece } from "./GameState";

export type NumPlayers = 2 | 3 | 4 | 5 | 6;
export type DiveRoutePosition = number & { __brand: "diveRoutePosition" };
export type PlayerId = string & { readonly __brand: "playerId" };
export interface Player {
id: PlayerId;
isDescending: boolean;
num: NumPlayers;
score: number;
position: DiveRoutePosition | "on-boat";
carried: TreasurePiece[];
}
export function createPlayers(numPlayers: NumPlayers): Player[] {
const players: Player[] = [];

for (let i = 0; i < numPlayers; i++) {
const id = ("player_" + (players.length + 1)) as PlayerId;
const p: Player = {
id,
num: (i + 1) as NumPlayers,
score: 10,
carried: [],
isDescending: true,
position:
i === 0
? "on-boat"
: (Math.floor(Math.random() * 10) as DiveRoutePosition), //"on-boat",
};
players.push(p);
}
return players;
}
62 changes: 62 additions & 0 deletions src/components/App.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,65 @@
html {
background: #f8f8f8;
font-size: 30px;
}

.diveRoute {
display: flex;
flex-direction: row;
gap: 0.5rem;
flex-wrap: wrap;
}

.diveRoutePiece {
background: lightblue;
padding: 0.5rem;
aspect-ratio: 1;
display: grid;
place-items: center;
width: 100px;
}

.diveRoutePiece.blank {
border-radius: 50%;
}

.diveRoutePiece.pentagon {
clip-path: polygon(50% 0%, 100% 38%, 82% 100%, 18% 100%, 0% 38%);
}
.diveRoutePiece.hexagon {
clip-path: polygon(50% 0%, 95% 25%, 95% 75%, 50% 100%, 5% 75%, 5% 25%);
}
.diveRoutePiece.triangle {
clip-path: polygon(50% 0%, 100% 100%, 0% 100%);
}

.player {
border-radius: 50%;
aspect-ratio: 1;
width: 1.5rem;
display: grid;
place-items: center;
}

.player.p1 {
background: lime;
}
.player.p2 {
background: tomato;
}
.player.p3 {
background: yellow;
}
.player.p4 {
background: purple;
}
.player.p5 {
background: black;
}
.player.p6 {
background: white;
}

button {
font-size: 2rem;
}
52 changes: 50 additions & 2 deletions src/components/App.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,58 @@
import { MyComponent } from "./MyComponent";
import clsx from "clsx";
import { useImmerReducer } from "use-immer";
import "./App.css";
import { DiveRoutePieceView } from "./DiveRoutePieceView";
import { createInitialState } from "../GameState";
import { PlayerSummary } from "./PlayerSummary";
import { reducerFunction } from "../reducer/reducerFunction";
import { Player } from "../Player";

function App() {
const initialState = createInitialState(3);
const [gs, dispatch] = useImmerReducer(reducerFunction, initialState);
return (
<div className="App">
<MyComponent />
<button onClick={() => dispatch({ tag: "roll-to-swim" })}>
Roll
</button>
Pieces:{" "}
<div className="diveRoute">
{gs.diveRoute.map((p, slotIx) => {
const playersAtSlot: Player[] = gs.players.filter(
(p) => p.position === slotIx
);

return (
<DiveRoutePieceView key={p.id} piece={p}>
{playersAtSlot.map((player) => (
<div
className={clsx("player", "p" + player.num)}
key={player.id}
>
{player.num}
</div>
))}
</DiveRoutePieceView>
);
})}
</div>
Remaining Air: {gs.remainingAir}
<br />
Last Roll:{" "}
{gs.lastRoll ? (
<>
<span>{gs.lastRoll.sum}</span>{" "}
<span>( {gs.lastRoll.faces.join(", ")} ) </span>
</>
) : (
"none"
)}
<br />
Players:{" "}
{gs.players.map((p) => (
<PlayerSummary key={p.id} player={p} />
))}
<button onClick={() => dispatch({ tag: "no-op" })}>One</button>
</div>
);
}
Expand Down
24 changes: 24 additions & 0 deletions src/components/DiveRoutePieceView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import clsx from "clsx";
import { Piece } from "../GameState";
import { ReactNode } from "react";

export function DiveRoutePieceView({
piece,
children,
}: {
piece: Piece;
children: ReactNode;
}): JSX.Element {
return (
<div
className={clsx(
"diveRoutePiece",
piece.tag,
piece.tag === "treasure" && piece.shape
)}
>
{piece.tag === "blank" ? "C" : piece.id}
{children}
</div>
);
}
12 changes: 12 additions & 0 deletions src/components/PlayerSummary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Player } from "../Player";

export function PlayerSummary({ player }: { player: Player }): JSX.Element {
return (
<div className="playerSummary">
id: {player.id}
score: {player.score}
carried: {player.carried.length}
position: {player.position}
</div>
);
}
Empty file added src/gameCore
Empty file.
7 changes: 7 additions & 0 deletions src/reducer/Action.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export type Action = NoOpAction | RollToSwimAction;
interface NoOpAction {
tag: "no-op";
}
interface RollToSwimAction {
tag: "roll-to-swim";
}
27 changes: 27 additions & 0 deletions src/reducer/doRollToSwim.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { GameState } from "../GameState";
import { DiveRoutePosition } from "../Player";
import { randomRoll } from "./reducerFunction";

export function doRollToSwim(draft: GameState) {
const roll = randomRoll();
const p = draft.players[0];

if (p.position === "on-boat") {
p.position = (roll.sum - 1) as DiveRoutePosition;
} else {
if (p.isDescending) {
p.position = Math.min(
p.position + roll.sum,
draft.diveRoute.length - 1
) as DiveRoutePosition;
if (p.position === draft.diveRoute.length - 1) {
p.isDescending = false;
}
} else {
const result = Math.max(p.position + roll.sum, -1);
p.position =
result === -1 ? "on-boat" : (result as DiveRoutePosition);
}
}
draft.lastRoll = roll;
}
31 changes: 31 additions & 0 deletions src/reducer/reducerFunction.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { original } from "immer";
import { Action } from "./Action";
import { GameState } from "../GameState";
import { doRollToSwim } from "./doRollToSwim";

export function reducerFunction(draft: GameState, action: Action): void {
console.log(original(draft));
switch (action.tag) {
case "no-op":
return;
case "roll-to-swim": {
doRollToSwim(draft);

return;
}
}
}

export function randomRoll(): DiceRoll {
const a = pick([1, 2, 3] as DieFace[]);
const b = pick([1, 2, 3] as DieFace[]);
const sum = (a + b) as DiceSum;
return { sum, faces: [a, b] };
}
export type DiceRoll = { sum: DiceSum; faces: [DieFace, DieFace] };
export type DiceSum = 2 | 3 | 4 | 5 | 6;
export type DieFace = 1 | 2 | 3;

function pick<T>(arr: T[]): T {
return arr[Math.floor(Math.random() * arr.length)];
}
15 changes: 15 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2048,6 +2048,11 @@ ci-info@^3.2.0:
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.8.0.tgz#81408265a5380c929f0bc665d62256628ce9ef91"
integrity sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==

clsx@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.0.0.tgz#12658f3fd98fafe62075595a5c30e43d18f3d00b"
integrity sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==

color-convert@^1.9.0:
version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
Expand Down Expand Up @@ -3009,6 +3014,11 @@ ignore@^5.2.0:
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324"
integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==

immer@^10.0.2:
version "10.0.2"
resolved "https://registry.yarnpkg.com/immer/-/immer-10.0.2.tgz#11636c5b77acf529e059582d76faf338beb56141"
integrity sha512-Rx3CqeqQ19sxUtYV9CU911Vhy8/721wRFnJv3REVGWUmoAcIwzifTsdmJte/MV+0/XpM35LZdQMBGkRIoLPwQA==

import-fresh@^3.0.0, import-fresh@^3.2.1:
version "3.3.0"
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
Expand Down Expand Up @@ -4424,6 +4434,11 @@ url-parse@^1.5.3:
querystringify "^2.1.1"
requires-port "^1.0.0"

use-immer@^0.9.0:
version "0.9.0"
resolved "https://registry.yarnpkg.com/use-immer/-/use-immer-0.9.0.tgz#66e4e8f7ab75df45e96dfd5c56337f9fd49db9fd"
integrity sha512-/L+enLi0nvuZ6j4WlyK0US9/ECUtV5v9RUbtxnn5+WbtaXYUaOBoKHDNL9I5AETdurQ4rIFIj/s+Z5X80ATyKw==

vite-node@0.32.0:
version "0.32.0"
resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-0.32.0.tgz#8ee54539fa75d1271adaa9788c8ba526480f4519"
Expand Down

0 comments on commit 494d2b8

Please sign in to comment.