Compare commits

...

3 Commits

Author SHA1 Message Date
ce29ab72ae scaling 2025-09-03 21:14:54 -04:00
d203fe4141 better fanned hands 2025-09-02 23:21:48 -04:00
67fdf66cd4 depth for card piles 2025-09-02 22:38:04 -04:00
14 changed files with 120 additions and 38 deletions

2
.gitignore vendored
View File

@@ -2,6 +2,8 @@
.vinxi .vinxi
*.db *.db
.DS_STORE
# ---> Node # ---> Node
# Logs # Logs
logs logs

7
deploy Executable file
View File

@@ -0,0 +1,7 @@
#!/bin/bash
branch=$(git branch --show-current)
git switch prod
git merge $branch
git push
git switch $brnach

View File

@@ -1,7 +1,7 @@
{ {
"name": "games", "name": "games",
"type": "module", "type": "module",
"version": "0.0.8", "version": "0.0.9",
"scripts": { "scripts": {
"dev": "pnpm --parallel dev", "dev": "pnpm --parallel dev",
"build": "pnpm run -F client build", "build": "pnpm run -F client build",

View File

@@ -11,6 +11,7 @@
"@solid-primitives/scheduled": "^1.5.2", "@solid-primitives/scheduled": "^1.5.2",
"@solid-primitives/storage": "^4.3.3", "@solid-primitives/storage": "^4.3.3",
"@solidjs/router": "^0.15.3", "@solidjs/router": "^0.15.3",
"color2k": "^2.0.3",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"kefir": "^3.8.8", "kefir": "^3.8.8",
"kefir-bus": "^2.3.1", "kefir-bus": "^2.3.1",

View File

@@ -1,7 +1,7 @@
import { Component, Suspense } from "solid-js"; import { Component, Suspense } from "solid-js";
import type { Card } from "@games/shared/cards"; import type { Card } from "@games/shared/cards";
import { Clickable, Sizable, Stylable } from "./toolbox"; import { Clickable, Scalable, Stylable } from "./toolbox";
const cardToSvgFilename = (card: Card) => { const cardToSvgFilename = (card: Card) => {
if (card.kind == "joker") { if (card.kind == "joker") {
@@ -17,7 +17,12 @@ const cardToSvgFilename = (card: Card) => {
}`; }`;
}; };
export const CARD_RATIO = 1.456730769;
export const BASE_CARD_WIDTH = 100;
export default ((props) => { export default ((props) => {
const width = () => BASE_CARD_WIDTH * (props.scale ?? 1);
const height = () => width() * CARD_RATIO;
return ( return (
<Suspense> <Suspense>
<img <img
@@ -25,8 +30,8 @@ export default ((props) => {
draggable={false} draggable={false}
class={props.class} class={props.class}
style={props.style} style={props.style}
width={props.width ?? "100px"} width={`${width()}px`}
height={props.height} height={`${height()}px`}
src={ src={
props.face == "down" props.face == "down"
? "/views/back.svg" ? "/views/back.svg"
@@ -45,5 +50,5 @@ export default ((props) => {
) & ) &
Stylable & Stylable &
Clickable & Clickable &
Sizable Scalable
>; >;

View File

@@ -10,16 +10,16 @@ export default (props: { handCount: number }) => {
return ( return (
<Card <Card
face="down" face="down"
width="40px" scale={0.4}
style={{ style={{
"margin-left": "-10px", "margin-left": "-12px",
"margin-right": "-10px", "margin-right": "-12px",
transform: `rotate(${ transform: `translate(0px, ${Math.pow(
midOffset * 0.2 Math.abs(midOffset),
}rad) translate(0px, ${ 2
2 ** Math.abs(midOffset) * 2 )}px) rotate(${midOffset * 0.12}rad)`,
}px)`, "min-width": "40px",
"box-shadow": "-4px 4px 4px rgba(0, 0, 0, 0.7)", "box-shadow": "-4px 4px 6px rgba(0, 0, 0, 0.6)",
}} }}
/> />
); );

View File

@@ -25,6 +25,7 @@ export default () => {
<GameContext.Provider value={{ view, submitAction }}> <GameContext.Provider value={{ view, submitAction }}>
<Pile <Pile
count={view().deckCount} count={view().deckCount}
scale={0.8}
class="cursor-pointer fixed center" class="cursor-pointer fixed center"
onClick={() => submitAction({ type: "draw" })} onClick={() => submitAction({ type: "draw" })}
/> />
@@ -34,7 +35,14 @@ export default () => {
hand={view().myHand} hand={view().myHand}
onClickCard={(card) => submitAction({ type: "discard", card })} onClickCard={(card) => submitAction({ type: "discard", card })}
/> />
<div class="absolute tc text-align-center"> <div
class="absolute tc text-align-center"
style={{
"background-color":
view().playerTurn == me() ? "var(--yellow)" : "transparent",
color: view().playerTurn == me() ? "var(--dark)" : "var(--light)",
}}
>
It's{" "} It's{" "}
<span class="font-bold"> <span class="font-bold">
{view().playerTurn == me() {view().playerTurn == me()
@@ -51,17 +59,20 @@ export default () => {
> >
Quit Quit
</button> </button>
<For each={Object.entries(view().playerHandCounts)}> <For
each={Object.entries(view().playerHandCounts).filter(([key, _]) =>
table.players().includes(key)
)}
>
{([playerKey, handCount], i) => ( {([playerKey, handCount], i) => (
<Portal <Portal
mount={document.getElementById(`player-${playerKey}`)!} mount={document.getElementById(`player-${playerKey}`)!}
ref={(ref) => { ref={(ref) => {
console.log("Setting hand ref");
const midOffset = const midOffset =
i() + 0.5 - Object.values(view().playerHandCounts).length / 2; i() + 0.5 - Object.values(view().playerHandCounts).length / 2;
ref.style = `position: absolute; display: flex; justify-content: center; top: 65%; transform: translate(${Math.abs( ref.style = `position: absolute; display: flex; justify-content: center; top: 65%;`;
midOffset * 0
)}px, 0px) rotate(${midOffset * 1}rad)`;
}} }}
> >
<FannedHand handCount={handCount} /> <FannedHand handCount={handCount} />

View File

@@ -1,22 +1,63 @@
import { Component, For, JSX, Show } from "solid-js"; import { Component, createMemo, For, JSX, Show } from "solid-js";
import Card from "./Card"; import Card, { BASE_CARD_WIDTH, CARD_RATIO } from "./Card";
import { desaturate } from "color2k";
import { Clickable, Stylable } from "./toolbox"; import { Clickable, hashColor, Scalable, Stylable } from "./toolbox";
const cardOffset = 0.35; // Small offset for the stack effect
export default ((props) => { export default ((props) => {
const cards = createMemo(() => {
const numCards = Math.max(0, props.count - 1); // Subtract 1 for the top card
return Array.from({ length: numCards }, (_, i) => i).toReversed();
});
const width = () => BASE_CARD_WIDTH * (props.scale ?? 1);
const height = () => width() * CARD_RATIO;
const offset = () => cardOffset * (props.scale ?? 1);
return ( return (
<Show when={props.count > 0}> <Show when={props.count > 0}>
<Card <div
onClick={props.onClick} style={{
style={props.style} ...props.style,
class={props.class + " shadow-lg shadow-black"} }}
face="down" class={props.class}
/> >
<svg
class="absolute z-[-1]"
width={width() + cards().length * offset()}
height={height() + cards().length * offset()}
viewBox={`0 0 ${width() + cards().length * offset()} ${
height() + cards().length * offset()
}`}
xmlns="http://www.w3.org/2000/svg"
>
<For each={cards()}>
{(i) => {
const xOffset = (i * offset()) / 2;
const yOffset = i * offset();
const color = desaturate(hashColor(i), 0.9);
return (
<rect
x={xOffset}
y={yOffset}
width={width()}
height={height()}
rx="5" // Rounded corners
ry="5"
fill={color}
/>
);
}}
</For>
</svg>
<Card onClick={props.onClick} face="down" scale={props.scale} />
</div>
</Show> </Show>
); );
}) satisfies Component< }) satisfies Component<
{ {
count: number; count: number;
} & Stylable & } & Stylable &
Clickable Clickable &
Scalable
>; >;

View File

@@ -1,4 +1,4 @@
import { createSignal, useContext } from "solid-js"; import { createSignal, onMount, useContext } from "solid-js";
import { playerColor } from "~/profile"; import { playerColor } from "~/profile";
import { TableContext } from "./Table"; import { TableContext } from "./Table";
import { Stylable } from "./toolbox"; import { Stylable } from "./toolbox";
@@ -16,6 +16,8 @@ export default (props: { playerKey: string } & Stylable) => {
const game = useContext(GameContext); const game = useContext(GameContext);
onMount(() => console.log("Player mounted"));
return ( return (
<div <div
id={`player-${props.playerKey}`} id={`player-${props.playerKey}`}

View File

@@ -28,6 +28,7 @@ export const TableContext = createContext<{
sendWs: (msg: TWsIn) => void; sendWs: (msg: TWsIn) => void;
wsEvents: Stream<TWsOut, any>; wsEvents: Stream<TWsOut, any>;
playerNames: Store<{ [key: string]: string }>; playerNames: Store<{ [key: string]: string }>;
players: Accessor<string[]>;
}>(); }>();
export default (props: { tableKey: string }) => { export default (props: { tableKey: string }) => {
@@ -75,7 +76,9 @@ export default (props: { tableKey: string }) => {
createEffect(() => sendWs({ ready: ready() })); createEffect(() => sendWs({ ready: ready() }));
createEffect(() => sendWs({ name: name() })); createEffect(() => sendWs({ name: name() }));
const view = createObservable(gameEvents.map((evt) => evt.view)); const viewProp = gameEvents.map((evt) => evt.view).toProperty();
viewProp.log();
const view = createObservable(viewProp);
const results = createObservable<string>( const results = createObservable<string>(
merge([ merge([
gameEvents gameEvents
@@ -92,6 +95,7 @@ export default (props: { tableKey: string }) => {
wsEvents, wsEvents,
view, view,
playerNames, playerNames,
players,
}} }}
> >
<div class="flex justify-around p-t-14"> <div class="flex justify-around p-t-14">
@@ -129,8 +133,8 @@ export default (props: { tableKey: string }) => {
"top-40", "top-40",
"bottom-20", "bottom-20",
"left-10", "left-[2%]",
"right-10" "right-[2%]"
)} )}
style={{ style={{
"border-radius": "50%", "border-radius": "50%",

View File

@@ -1,3 +1,4 @@
import hash, { NotUndefined } from "object-hash";
import { JSX } from "solid-js"; import { JSX } from "solid-js";
export type Stylable = { export type Stylable = {
@@ -15,7 +16,8 @@ export type Clickable = {
| undefined; | undefined;
}; };
export type Sizable = { export type Scalable = {
width?: string; scale?: number;
height?: string;
}; };
export const hashColor = (obj: NotUndefined) => `#${hash(obj).substring(0, 6)}`;

View File

@@ -19,7 +19,6 @@ export const WS = Bus<
const api = new Elysia({ prefix: "/api" }) const api = new Elysia({ prefix: "/api" })
.post("/whoami", async ({ cookie: { token } }) => { .post("/whoami", async ({ cookie: { token } }) => {
console.log("WHOAMI");
let key: string | undefined; let key: string | undefined;
if (token.value == null || (key = resolveToken(token.value)) == null) { if (token.value == null || (key = resolveToken(token.value)) == null) {
const [newToken, newKey] = generateTokenAndKey(); const [newToken, newKey] = generateTokenAndKey();

View File

@@ -116,7 +116,7 @@ export default (config: SimpleConfiguration) =>
resolveQuit: () => null, resolveQuit: () => null,
getResult: (state) => getResult: (state) =>
Object.entries(state.playerHands).find( Object.entries(state.playerHands).find(
([_, hand]) => hand.length === 2 ([_, hand]) => hand.length === 52
)?.[0], )?.[0],
} satisfies Game< } satisfies Game<
SimpleGameState, SimpleGameState,

8
pnpm-lock.yaml generated
View File

@@ -29,6 +29,9 @@ importers:
'@solidjs/router': '@solidjs/router':
specifier: ^0.15.3 specifier: ^0.15.3
version: 0.15.3(solid-js@1.9.9) version: 0.15.3(solid-js@1.9.9)
color2k:
specifier: ^2.0.3
version: 2.0.3
js-cookie: js-cookie:
specifier: ^3.0.5 specifier: ^3.0.5
version: 3.0.5 version: 3.0.5
@@ -927,6 +930,9 @@ packages:
color-name@1.1.4: color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
color2k@2.0.3:
resolution: {integrity: sha512-zW190nQTIoXcGCaU08DvVNFTmQhUpnJfVuAKfWqUQkflXKpaDdpaYoM0iluLS9lgJNHyBF58KKA2FBEwkD7wog==}
colorette@2.0.20: colorette@2.0.20:
resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
@@ -3238,6 +3244,8 @@ snapshots:
color-name@1.1.4: {} color-name@1.1.4: {}
color2k@2.0.3: {}
colorette@2.0.20: {} colorette@2.0.20: {}
compare-func@2.0.0: compare-func@2.0.0: