package -> pkg
This commit is contained in:
58
pkg/shared/cards.ts
Normal file
58
pkg/shared/cards.ts
Normal 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
25
pkg/shared/games/index.ts
Normal 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;
|
||||
1
pkg/shared/games/renaissance.ts
Normal file
1
pkg/shared/games/renaissance.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default {};
|
||||
122
pkg/shared/games/simple.ts
Normal file
122
pkg/shared/games/simple.ts
Normal 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
7
pkg/shared/package.json
Normal 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
6
pkg/shared/tsconfig.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"esModuleInterop": true
|
||||
}
|
||||
}
|
||||
3
pkg/shared/utils.ts
Normal file
3
pkg/shared/utils.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import hash, { NotUndefined } from "object-hash";
|
||||
|
||||
export const heq = (a: NotUndefined) => (b: NotUndefined) => hash(a) == hash(b);
|
||||
Reference in New Issue
Block a user