Compare commits

...

2 Commits

Author SHA1 Message Date
0124b69440 basic drawing 2025-08-03 14:43:29 -04:00
e4f6e1899d cards render well 2025-08-03 12:30:26 -04:00
12 changed files with 236 additions and 40 deletions

View File

@@ -5,6 +5,7 @@ build:
--cache-from=type=local,src=/var/cache/docker-build \
--cache-to=type=local,dest=/var/cache/docker-build \
--platform linux/arm64 \
--progress=plain \
--tag games .

BIN
assets/sources/Card_back_01.svg LFS Normal file

Binary file not shown.

1
assets/views/back.svg Symbolic link
View File

@@ -0,0 +1 @@
../sources/Card_back_01.svg

View File

@@ -1,7 +1,7 @@
{
"name": "games",
"type": "module",
"version": "0.0.1",
"version": "0.0.2",
"scripts": {
"dev": "vinxi dev",
"build": "vinxi build",

View File

@@ -28,6 +28,10 @@ body::before {
width: 100%;
}
.w-full {
width: 100%;
}
.center {
display: flex;
justify-content: center;
@@ -50,3 +54,7 @@ body::before {
bottom: 0;
right: 0;
}
.clear {
pointer-events: none;
}

View File

@@ -5,8 +5,16 @@ import { Suspense } from "solid-js";
import pkg from "~/../package.json";
const Version = () => (
<div class="full free">
<span style={{ margin: "5px", "font-size": "0.8rem" }} class="fixed-br">
<div class="full free clear">
<span
style={{
margin: "5px",
"font-size": "0.8rem",
"font-family": "monospace",
"pointer-events": "all",
}}
class="fixed-br"
>
v{pkg.version}
</span>
</div>

View File

@@ -1,13 +1,49 @@
import { Component, createResource, Suspense } from "solid-js";
import { Component, createResource, JSX, Suspense } from "solid-js";
import { Card } from "../types/cards";
export default (() => {
const [svgPath] = createResource(
() => import(`~/../assets/views/cards/CLUB-1.svg`)
const cardToSvgFilename = (card: Card) => {
if (card.kind == "joker") {
return `JOKER-${card.color == "black" ? "2" : "3"}`;
}
const value =
{ ace: 1, jack: 11, queen: 12, king: 13 }[
card.rank as "ace" | "jack" | "queen" | "king" // fuck you typescript
] ?? (card.rank as number);
return `${card.suit.toUpperCase()}-${value}${
value >= 11 ? "-" + (card.rank as string).toUpperCase() : ""
}`;
};
export default ((props) => {
const [svgPath] = createResource(() =>
props.face == "down"
? // @ts-ignore
import("~/../assets/views/back.svg")
: import(
`~/../assets/views/cards/${cardToSvgFilename(
props.card
)}.svg`
)
);
return (
<Suspense>
<img src={svgPath()?.default} />
<img
draggable={false}
class={props.class}
style={{
"border-radius": "5px",
...props.style,
}}
width="100px"
src={svgPath()?.default}
/>
</Suspense>
);
}) satisfies Component;
}) satisfies Component<{
class?: string;
style?: JSX.CSSProperties;
card: Card;
face?: "up" | "down";
}>;

View File

@@ -1,15 +1,40 @@
import { JSX } from "solid-js";
import { createContext, JSX } from "solid-js";
import Card from "./Card";
import Hand from "./Hand";
import Pile from "./Pile";
import { newDeck, shuffle, Hand as THand } from "../types/cards";
import { createStore, produce } from "solid-js/store";
const GameContext = createContext();
export default () => {
const [gameState, setGameState] = createStore({
pile: shuffle(newDeck()),
hand: [] as THand,
});
return (
<div
class="full center column"
style={{ "row-gap": "20px", "font-size": "32px" }}
>
games
<Card />
coming soon
</div>
<GameContext.Provider value={{ gameState, setGameState }}>
<div
onClick={() => {}}
class="full column"
style={{ "row-gap": "20px", "font-size": "32px" }}
>
<div class="full center">
<Pile
pile={gameState.pile}
style={{ cursor: "pointer" }}
onClick={() =>
setGameState(
produce((state) => {
state.hand.push(state.pile.pop()!);
})
)
}
/>
</div>
<Hand hand={gameState.hand} />
</div>
</GameContext.Provider>
);
};

View File

@@ -0,0 +1,24 @@
import { Component, For } from "solid-js";
import Card from "./Card";
import { Hand } from "../types/cards";
export default ((props) => {
return (
<div
style={{
border: "2px dashed white",
"border-radius": "12px",
margin: "10px",
"margin-bottom": "25px",
padding: "10px",
height: "200px",
overflow: "scroll",
"scrollbar-width": "none",
display: "flex",
gap: "5px",
}}
>
<For each={props.hand}>{(card) => <Card card={card} />}</For>
</div>
);
}) satisfies Component<{ hand: Hand }>;

View File

@@ -0,0 +1,44 @@
import { Component, For, JSX } from "solid-js";
import Card from "./Card";
import { Pile } from "../types/cards";
export default ((props) => {
return (
<div
class="center"
style={{ width: "200px", height: "400px", ...props.style }}
onClick={props.onClick}
>
<For each={props.pile}>
{(card, i) => (
<Card
card={card}
face="down"
style={{
position: "absolute",
transform: `translate(${i() * 0.8}px, ${
-i() * 0.4
}px)`,
"z-index": 100 - i(),
border: `0.1px solid rgb(${
60 - i() + Math.random() * 10
}, ${60 - i() + Math.random() * 10}, ${
60 - i() + Math.random() * 10
});`,
}}
/>
)}
</For>
</div>
);
}) satisfies Component<{
pile: Pile;
style?: JSX.CSSProperties;
onClick?:
| JSX.EventHandlerUnion<
HTMLDivElement,
MouseEvent,
JSX.EventHandler<HTMLDivElement, MouseEvent>
>
| undefined;
}>;

View File

@@ -1,11 +1,57 @@
export type Suit = "hearts" | "diamonds" | "spades" | "clubs";
const suits = ["heart", "diamond", "spade", "club"] as const;
export type Suit = (typeof suits)[number];
export type Card = {
suit: Suit;
value: 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 Pile = Card[];
export type Stack = Card[];
export type Hand = Card[];
export type Board = Card[];
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;
};

View File

@@ -1,19 +1,19 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"jsx": "preserve",
"jsxImportSource": "solid-js",
"allowJs": true,
"strict": true,
"noEmit": true,
"types": ["vinxi/types/client"],
"isolatedModules": true,
"paths": {
"~/*": ["./src/*"]
}
}
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"jsx": "preserve",
"jsxImportSource": "solid-js",
"allowJs": true,
"strict": true,
"noEmit": true,
"types": ["vinxi/types/client"],
"isolatedModules": true,
"paths": {
"~/*": ["./src/*"]
}
}
}