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:
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,
} from "~/fn";
import { me, mePromise } from "~/profile";
import Game from "./Game";
import Simple from "./games/simple";
import Player from "./Player";
import games from "@games/shared/games/index";
import { createStore, Store } from "solid-js/store";
@@ -157,7 +157,7 @@ export default (props: { tableKey: string }) => {
</Show>
</div>
<Show when={view() != null}>
<Game />
<Simple />
</Show>
<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">

View File

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

View File

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

View File

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

View File

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