This commit is contained in:
2025-09-03 22:50:32 -04:00
parent 46e7b60ade
commit b3e040f03f
8 changed files with 88 additions and 33 deletions

View File

@@ -8,3 +8,8 @@ build:
start: start:
PORT=$(PORT) pnpm start PORT=$(PORT) pnpm start
note:
./notes/newfile
# touch ./notes/$$file.md
# code -r ./notes/$$file.md

View File

@@ -0,0 +1,5 @@
# 2025-09-03-224857
The backend has a pretty good abstraction of a `Game`, that it can swap in implemenations for.
The frontend is still mostly hardcoded to the test `simple` game.
Will need to refine the concept of `Game` to include the necessary components of displaying and interacting with a game.

7
notes/newfile Executable file
View File

@@ -0,0 +1,7 @@
#!/bin/bash
ts=$(date +"%Y-%m-%d-%H%M%S")
file=./notes/$ts.md
touch $file
echo -e "# $ts\n" > $file
echo "$file:end"
code --goto "$file:2"

View File

@@ -17,7 +17,7 @@ import {
cx, cx,
} from "~/fn"; } from "~/fn";
import { me, mePromise } from "~/profile"; import { me, mePromise } from "~/profile";
import Game from "./Game"; import Simple from "./games/simple";
import Player from "./Player"; import Player from "./Player";
import games from "@games/shared/games/index"; import games from "@games/shared/games/index";
import { createStore, Store } from "solid-js/store"; import { createStore, Store } from "solid-js/store";
@@ -157,7 +157,7 @@ export default (props: { tableKey: string }) => {
</Show> </Show>
</div> </div>
<Show when={view() != null}> <Show when={view() != null}>
<Game /> <Simple />
</Show> </Show>
<Show when={results() != null}> <Show when={results() != null}>
<span class="bg-[var(--light)] text-[var(--dark)] rounded-[24px] border-2 border-[var(--dark)] absolute center p-4 shadow-lg text-[4em] text-center"> <span class="bg-[var(--light)] text-[var(--dark)] rounded-[24px] border-2 border-[var(--dark)] absolute center p-4 shadow-lg text-[4em] text-center">

View File

@@ -5,11 +5,11 @@ import type {
SimpleResult, SimpleResult,
} from "@games/shared/games/simple"; } from "@games/shared/games/simple";
import { me } from "~/profile"; import { me } from "~/profile";
import Hand from "./Hand"; import Hand from "../Hand";
import Pile from "./Pile"; import Pile from "../Pile";
import { TableContext } from "./Table"; import { TableContext } from "../Table";
import { Portal } from "solid-js/web"; import { Portal } from "solid-js/web";
import FannedHand from "./FannedHand"; import FannedHand from "../FannedHand";
export const GameContext = createContext<{ export const GameContext = createContext<{
view: Accessor<SimplePlayerView>; view: Accessor<SimplePlayerView>;

View File

@@ -202,7 +202,7 @@ export const liveTable = <
const gameImpl = gameConfig const gameImpl = gameConfig
.filter((cfg) => cfg.game in GAMES) .filter((cfg) => cfg.game in GAMES)
.map((config) => GAMES[config.game as GameKey](config)) .map((config) => GAMES[config.game as GameKey].impl(config))
.toProperty(); .toProperty();
const withGame = <T>(obs: Observable<T, any>) => const withGame = <T>(obs: Observable<T, any>) =>
@@ -226,7 +226,7 @@ export const liveTable = <
prev, prev,
[{ action, humanKey }, game]: [ [{ action, humanKey }, game]: [
Attributed & { action: GameAction }, Attributed & { action: GameAction },
Game ReturnType<Game["impl"]>
] ]
) => ) =>
prev && prev &&

View File

@@ -3,21 +3,28 @@ import simple from "./simple";
export type Game< export type Game<
S = unknown, // state S = unknown, // state
A = unknown, // action A = unknown, // action
E extends { error: any } = { error: any }, // error E = unknown, // error
V = unknown, // view V = unknown, // view
R = unknown // results R = unknown, // results
C extends { game: string; players: string[] } = {
game: string;
players: string[];
}
> = { > = {
title: string; impl: (config: C) => {
rules: string; title: string;
init: () => S; rules: string;
resolveAction: (p: { state: S; action: A; humanKey: string }) => S | E; init: () => S;
getView: (p: { state: S; humanKey: string }) => V; resolveAction: (p: { state: S; action: A; humanKey: string }) => S | E;
resolveQuit: (p: { state: S; humanKey: string }) => S; getView: (p: { state: S; humanKey: string }) => V;
getResult: (state: S) => R | undefined; resolveQuit: (p: { state: S; humanKey: string }) => S;
getResult: (state: S) => R | undefined;
};
defaultConfig: C;
}; };
export const GAMES: { export const GAMES: {
[key: string]: (config: { game: string; players: string[] }) => Game; [key: string]: Game;
} = { } = {
// renaissance, // renaissance,
simple, simple,

View File

@@ -1,10 +1,13 @@
import { Card, Hand, newDeck, Pile, shuffle, vCard } from "@games/shared/cards"; import { Card, Hand, newDeck, Pile, shuffle, vCard } from "@games/shared/cards";
import { heq } from "@games/shared/utils"; import { heq } from "@games/shared/utils";
import type { Game } from "."; import type { Game } from ".";
import { XOR } from "ts-xor";
export type SimpleConfiguration = { export type SimpleConfiguration = {
game: "simple"; game: "simple";
players: string[]; players: string[];
"can discard": boolean;
"cards to win": number;
}; };
// omniscient game state // omniscient game state
@@ -50,6 +53,16 @@ export const getSimplePlayerView = (
), ),
}); });
// type SimpleError = XOR<
// { "go away": string },
// { chill: string },
// { "ah ah": string }
// >;
type SimpleError = {
class: "go away" | "chill" | "ah ah";
message: string;
};
export const resolveSimpleAction = ({ export const resolveSimpleAction = ({
config, config,
state, state,
@@ -62,13 +75,18 @@ export const resolveSimpleAction = ({
humanKey: string; humanKey: string;
}): SimpleGameState => { }): SimpleGameState => {
const playerHand = state.playerHands[humanKey]; const playerHand = state.playerHands[humanKey];
if (playerHand == null) { if (playerHand == null) {
throw new Error( throw {
`${humanKey} is not a player in this game; they cannot perform actions` message: "You are not a part of this game!",
); class: "go away",
} satisfies SimpleError;
} }
if (humanKey != config.players[state.turnIdx]) { if (humanKey != config.players[state.turnIdx]) {
throw new Error(`It's not ${humanKey}'s turn!`); throw {
message: "It's not your turn!",
class: "chill",
} satisfies SimpleError;
} }
const numPlayers = Object.keys(state.playerHands).length; const numPlayers = Object.keys(state.playerHands).length;
@@ -87,6 +105,13 @@ export const resolveSimpleAction = ({
}; };
} else { } else {
// action.type == discard // action.type == discard
if (config["can discard"] == false) {
throw {
message: "You're not allowed to discard!",
class: "ah ah",
} satisfies SimpleError;
}
const cardIndex = playerHand.findIndex(heq(action.card)); const cardIndex = playerHand.findIndex(heq(action.card));
return { return {
deck: [action.card, ...state.deck], deck: [action.card, ...state.deck],
@@ -103,10 +128,14 @@ export const resolveSimpleAction = ({
export type SimpleResult = string; export type SimpleResult = string;
type SimpleError = { error: "whoops!" }; export default {
defaultConfig: {
export default (config: SimpleConfiguration) => game: "simple",
({ players: [],
"can discard": true,
"cards to win": 5,
},
impl: (config: SimpleConfiguration) => ({
title: "Simple", title: "Simple",
rules: "You can draw, or you can discard. Then your turn is up.", rules: "You can draw, or you can discard. Then your turn is up.",
init: () => newSimpleGameState(config), init: () => newSimpleGameState(config),
@@ -118,10 +147,12 @@ export default (config: SimpleConfiguration) =>
Object.entries(state.playerHands).find( Object.entries(state.playerHands).find(
([_, hand]) => hand.length === 52 ([_, hand]) => hand.length === 52
)?.[0], )?.[0],
} satisfies Game< }),
SimpleGameState, } satisfies Game<
SimpleAction, SimpleGameState,
SimpleError, SimpleAction,
SimplePlayerView, SimpleError,
SimpleResult SimplePlayerView,
>); SimpleResult,
SimpleConfiguration
>;