package -> pkg

This commit is contained in:
2025-08-29 20:45:53 -04:00
parent 0d6d3d6d32
commit f38a5a69df
54 changed files with 5 additions and 5 deletions

58
pkg/shared/cards.ts Normal file
View File

@@ -0,0 +1,58 @@
const suits = ["heart", "diamond", "spade", "club"] as const;
export type Suit = (typeof suits)[number];
const ranks = [
2,
3,
4,
5,
6,
7,
8,
9,
10,
"jack",
"queen",
"king",
"ace",
] as const;
export type Rank = (typeof ranks)[number];
export type Card =
| {
kind: "normal";
suit: Suit;
rank: Rank;
}
| { kind: "joker"; color: "red" | "black" };
export type vCard = Card | null | number;
export type Pile<C extends vCard = Card> = C[];
export type Stack<C extends vCard = Card> = C[];
export type Hand<C extends vCard = Card> = C[];
export type Board<C extends vCard = Card> = C[];
export const newDeck = (withJokers = false): Pile =>
suits
.map((suit) =>
ranks.map((rank) => ({ kind: "normal", suit, rank } as Card))
)
.flat()
.concat(
withJokers
? [
{ kind: "joker", color: "red" },
{ kind: "joker", color: "black" },
]
: []
);
export const shuffle = (cards: Card[]) => {
let i = cards.length;
while (i > 0) {
const j = Math.floor(Math.random() * i);
i--;
[cards[i], cards[j]] = [cards[j], cards[i]];
}
return cards;
};

25
pkg/shared/games/index.ts Normal file
View File

@@ -0,0 +1,25 @@
import * as renaissance from "./renaissance";
import simple from "./simple";
export type Game<
C extends { game: string },
S,
A,
E extends { error: any },
V
> = {
title: string;
rules: string;
init: (config: C) => S;
resolveAction: (p: { config: C; state: S; action: A }) => S | E;
getView: (p: { config: C; state: S; humanKey: string }) => V;
resolveQuit: (p: { config: C; state: S; humanKey: string }) => S;
};
const games = {
// renaissance,
simple,
} satisfies { [key: string]: Game<any, any, any, any, any> };
export default games;
export type GameId = keyof typeof games;

View File

@@ -0,0 +1 @@
export default {};

122
pkg/shared/games/simple.ts Normal file
View File

@@ -0,0 +1,122 @@
import { Card, Hand, newDeck, Pile, shuffle, vCard } from "@games/shared/cards";
import { heq } from "@games/shared/utils";
import type { Game } from ".";
export type SimpleConfiguration = {
game: "simple";
players: string[];
};
// omniscient game state
export type SimpleGameState = {
deck: Pile;
turnIdx: number;
playerHands: { [humanKey: string]: Hand };
};
// a particular player's point of view in the game
export type SimplePlayerView = {
deckCount: number;
playerTurn: string;
playerHandCounts: { [humanKey: string]: number };
myHand: Hand<Card>;
};
export type SimpleAction = { type: "draw" } | { type: "discard"; card: Card };
export const newSimpleGameState = (
config: SimpleConfiguration
): SimpleGameState => {
const { players } = config;
return {
deck: shuffle(newDeck()),
turnIdx: 0,
playerHands: Object.fromEntries(
players.map((humanKey) => [humanKey, []])
),
};
};
export const getSimplePlayerView = (
config: SimpleConfiguration,
state: SimpleGameState,
humanKey: string
): SimplePlayerView => ({
deckCount: state.deck.length,
playerTurn: config.players[state.turnIdx],
myHand: state.playerHands[humanKey] as Hand,
playerHandCounts: Object.fromEntries(
Object.entries(state.playerHands)
.filter(([id]) => id != humanKey)
.map(([id, hand]) => [id, hand.length])
),
});
export const resolveSimpleAction = ({
config,
state,
humanKey,
action,
}: {
config: SimpleConfiguration;
state: SimpleGameState;
humanKey: string;
action: SimpleAction;
}): SimpleGameState => {
const playerHand = state.playerHands[humanKey];
if (playerHand == null) {
throw new Error(
`${humanKey} is not a player in this game; they cannot perform actions`
);
}
if (humanKey != config.players[state.turnIdx]) {
throw new Error(`It's not ${humanKey}'s turn!`);
}
const numPlayers = Object.keys(state.playerHands).length;
const newTurnIdx = (state.turnIdx + 1) % numPlayers;
if (action.type == "draw") {
const [drawn, ...rest] = state.deck;
return {
deck: rest,
playerHands: {
...state.playerHands,
[humanKey]: [drawn, ...playerHand],
},
turnIdx: newTurnIdx,
};
} else {
// action.type == discard
const cardIndex = playerHand.findIndex(heq(action.card));
return {
deck: [action.card, ...state.deck],
playerHands: {
...state.playerHands,
[humanKey]: playerHand
.slice(0, cardIndex)
.concat(playerHand.slice(cardIndex + 1)),
},
turnIdx: newTurnIdx,
};
}
};
type SimpleError = { error: "whoops!" };
export default {
title: "Simple",
rules: "You can draw, or you can discard. Then your turn is up.",
init: newSimpleGameState,
resolveAction: resolveSimpleAction,
getView: ({ config, state, humanKey }) =>
getSimplePlayerView(config, state, humanKey),
resolveQuit: () => null,
} satisfies Game<
SimpleConfiguration,
SimpleGameState,
SimpleAction,
SimpleError,
SimplePlayerView
>;

7
pkg/shared/package.json Normal file
View File

@@ -0,0 +1,7 @@
{
"name": "@games/shared",
"version": "1.0.0",
"dependencies": {
"object-hash": "^3.0.0"
}
}

6
pkg/shared/tsconfig.json Normal file
View File

@@ -0,0 +1,6 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"esModuleInterop": true
}
}

3
pkg/shared/utils.ts Normal file
View File

@@ -0,0 +1,3 @@
import hash, { NotUndefined } from "object-hash";
export const heq = (a: NotUndefined) => (b: NotUndefined) => hash(a) == hash(b);