Compare commits

...

22 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
e6cee7c2fa 0.0.1 2025-08-02 00:01:27 -04:00
7e5bc73b0d test minor change 2025-08-01 11:01:27 -04:00
9d573b4b7d lets go back to basics 2025-08-01 01:35:12 -04:00
552c544014 try arm64v6 2025-08-01 01:26:29 -04:00
50c94b8e1e allow build scripts (really) 2025-08-01 01:01:40 -04:00
24dd8b1e99 maybe this works 2025-08-01 00:44:41 -04:00
e3710f7152 force recompilation of binaries 2025-08-01 00:27:37 -04:00
cac5ebff3a arm-optimized base images 2025-08-01 00:16:24 -04:00
5480e2921b this dockerfile rocks now 2025-08-01 00:02:24 -04:00
27d47764f8 vinxi cache in docker build 2025-07-31 23:15:51 -04:00
6edbaa7a68 actual docker fix 2025-07-31 22:34:59 -04:00
9bc6cfbf7c docker platform fix 2025-07-31 18:00:19 -04:00
600cf1c290 docker update 2025-07-31 17:55:21 -04:00
7a5ac26f45 docker explicit platform 2025-07-31 00:46:11 -04:00
44e7586baf docker fix 2025-07-31 00:36:21 -04:00
4efc60861b dont use pnpm in docker 2025-07-31 00:25:40 -04:00
58038a6e86 whoops 2025-07-31 00:20:20 -04:00
5fc503778d build with docker 2025-07-31 00:19:05 -04:00
369fbec9e5 update gitignore 2025-07-30 23:51:06 -04:00
ac944e4c87 Merge pull request 'Initial version' (#1) from dev into prod
Reviewed-on: #1
2025-07-30 23:31:40 -04:00
88 changed files with 612 additions and 50 deletions

9
.dockerignore Normal file
View File

@@ -0,0 +1,9 @@
node_modules
Dockerfile
Makefile
README.md
.output
.vinxi
.git
.gitignore
.dockerignore

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
assets/** filter=lfs diff=lfs merge=lfs -text

3
.gitignore vendored
View File

@@ -1,3 +1,6 @@
.output
.vinxi
# ---> Node
# Logs
logs

11
Dockerfile Normal file
View File

@@ -0,0 +1,11 @@
FROM node:22-alpine
WORKDIR /app
EXPOSE 3000
COPY package.json ./
RUN --mount=type=cache,target=/root/.npm npm install
COPY . .
RUN --mount=type=cache,target=/app/.vinxi npm run build
CMD ["npm", "run", "start"]

40
Dockerfile.stages Normal file
View File

@@ -0,0 +1,40 @@
FROM arm64v8/node:22 AS base
ENV SHARP_IGNORE_GLOBAL_CLI_BINARIES=1
ENV VIPS_DISABLE_DEPS=1
ENV PNPM_SCRIPT_RUNNER_ALLOW_BUILD=true
RUN corepack enable
WORKDIR /app
FROM base AS builder
COPY package.json pnpm-lock.yaml ./
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile --dangerously-allow-all-builds
COPY . .
# ^ copy source code
RUN --mount=type=cache,target=/app/.vinxi pnpm run build
# ^ produces .output (build artifact) and .vinxi (build cache)
FROM base AS production_builder
RUN apt-get update && apt-get install -y jq
COPY --from=builder /app/package.json ./
RUN jq 'del(.devDependencies)' package.json > package.prod.json
COPY --from=builder /app/pnpm-lock.yaml ./
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile --dangerously-allow-all-builds
# Prod image
FROM arm64v8/node:22-alpine
ENV SHARP_IGNORE_GLOBAL_CLI_BINARIES=1
ENV VIPS_DISABLE_DEPS=1
ENV PNPM_SCRIPT_RUNNER_ALLOW_BUILD=true
RUN corepack enable
WORKDIR /app
COPY --from=production_builder /app/package.prod.json ./package.json
COPY --from=production_builder /app/pnpm-lock.yaml ./pnpm-lock.yaml
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile
COPY --from=builder /app/.output ./.output
EXPOSE 3000
CMD ["npm", "run", "start"]

View File

@@ -1,8 +1,13 @@
SHELL := /bin/bash
build:
pnpm install
pnpm run build
sudo docker buildx build --load \
--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 .
start:
pnpm run start
sudo docker run -p $(PORT):3000 -t games

BIN
assets/sources/Card_back_01.svg LFS Normal file

Binary file not shown.

Binary file not shown.

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

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

1
assets/views/cards Symbolic link
View File

@@ -0,0 +1 @@
../sources/Vector-Cards-Version-3.2/FACES (BORDERED)/STANDARD BORDERED/Single Cards (One Per FIle)/

View File

@@ -1,12 +1,14 @@
{
"name": "games",
"type": "module",
"version": "0.0.2",
"scripts": {
"dev": "vinxi dev",
"build": "vinxi build",
"start": "vinxi start"
},
"dependencies": {
"@solidjs/router": "^0.15.3",
"@solidjs/start": "^1.1.0",
"solid-js": "^1.9.5",
"vinxi": "^0.5.7"

12
pnpm-lock.yaml generated
View File

@@ -8,6 +8,9 @@ importers:
.:
dependencies:
'@solidjs/router':
specifier: ^0.15.3
version: 0.15.3(solid-js@1.9.7)
'@solidjs/start':
specifier: ^1.1.0
version: 1.1.7(solid-js@1.9.7)(vinxi@0.5.8(@netlify/blobs@9.1.2)(@types/node@24.1.0)(db0@0.3.2)(ioredis@5.6.1)(jiti@2.5.1)(terser@5.43.1))(vite@6.3.5(@types/node@24.1.0)(jiti@2.5.1)(terser@5.43.1))
@@ -883,6 +886,11 @@ packages:
resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==}
engines: {node: '>=18'}
'@solidjs/router@0.15.3':
resolution: {integrity: sha512-iEbW8UKok2Oio7o6Y4VTzLj+KFCmQPGEpm1fS3xixwFBdclFVBvaQVeibl1jys4cujfAK5Kn6+uG2uBm3lxOMw==}
peerDependencies:
solid-js: ^1.8.6
'@solidjs/start@1.1.7':
resolution: {integrity: sha512-30nUFzCpCVH7ORtHlO4ZE+VLG3g3EP+x+ceLLJBFRXIVuFQ1p203xZvVCXWqUPydtK78O5w3nIkWA/tLtF0Ybg==}
peerDependencies:
@@ -3977,6 +3985,10 @@ snapshots:
'@sindresorhus/merge-streams@2.3.0': {}
'@solidjs/router@0.15.3(solid-js@1.9.7)':
dependencies:
solid-js: 1.9.7
'@solidjs/start@1.1.7(solid-js@1.9.7)(vinxi@0.5.8(@netlify/blobs@9.1.2)(@types/node@24.1.0)(db0@0.3.2)(ioredis@5.6.1)(jiti@2.5.1)(terser@5.43.1))(vite@6.3.5(@types/node@24.1.0)(jiti@2.5.1)(terser@5.43.1))':
dependencies:
'@tanstack/server-functions-plugin': 1.121.21(vite@6.3.5(@types/node@24.1.0)(jiti@2.5.1)(terser@5.43.1))

View File

@@ -0,0 +1,60 @@
html {
height: 100vh;
}
body {
margin: 0;
font-family: "Trebuchet MS", "Lucida Sans Unicode", "Lucida Grande",
"Lucida Sans", Arial, sans-serif;
color: white;
height: 100%;
}
body::before {
z-index: -1;
content: "";
font-size: 28px;
position: absolute;
width: 100%;
height: 100%;
}
#app {
height: 100%;
background: radial-gradient(rgb(24, 82, 65), rgb(1, 42, 16));
}
.full {
height: 100%;
width: 100%;
}
.w-full {
width: 100%;
}
.center {
display: flex;
justify-content: center;
align-items: center;
}
.column {
display: flex;
flex-direction: column;
}
.free {
position: absolute;
top: 0;
left: 0;
}
.fixed-br {
position: fixed;
bottom: 0;
right: 0;
}
.clear {
pointer-events: none;
}

View File

@@ -1,22 +1,36 @@
import { createSignal } from "solid-js";
import "./app.css";
import { Router } from "@solidjs/router";
import { FileRoutes } from "@solidjs/start/router";
import { Suspense } from "solid-js";
import pkg from "~/../package.json";
const Version = () => (
<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>
);
export default function App() {
const [count, setCount] = createSignal(0);
return (
<main>
<h1>Hello world!</h1>
<button class="increment" onClick={() => setCount(count() + 1)} type="button">
Clicks: {count()}
</button>
<p>
Visit{" "}
<a href="https://start.solidjs.com" target="_blank">
start.solidjs.com
</a>{" "}
to learn how to build SolidStart apps.
</p>
</main>
<Router
root={(props) => (
<>
<Suspense>{props.children}</Suspense>
<Version />
</>
)}
>
<FileRoutes />
</Router>
);
}

View File

@@ -1,11 +0,0 @@
export type Suit = "hearts" | "diamonds" | "spades" | "clubs";
export type Card = {
suit: Suit;
value: number;
};
export type Pile = Card[];
export type Stack = Card[];
export type Hand = Card[];
export type Board = Card[];

49
src/components/Card.tsx Normal file
View File

@@ -0,0 +1,49 @@
import { Component, createResource, JSX, Suspense } from "solid-js";
import { Card } from "../types/cards";
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
draggable={false}
class={props.class}
style={{
"border-radius": "5px",
...props.style,
}}
width="100px"
src={svgPath()?.default}
/>
</Suspense>
);
}) satisfies Component<{
class?: string;
style?: JSX.CSSProperties;
card: Card;
face?: "up" | "down";
}>;

40
src/components/Game.tsx Normal file
View File

@@ -0,0 +1,40 @@
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 (
<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>
);
};

24
src/components/Hand.tsx Normal file
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 }>;

44
src/components/Pile.tsx Normal file
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

0
src/components/Table.tsx Normal file
View File

View File

@@ -1,4 +1,4 @@
import { Board, Card, Hand, Pile, Stack, Suit } from "./cards";
import { Board, Card, Hand, Pile, Stack, Suit } from "./types/cards";
import { clone } from "./fn";
const AGG: Suit = "spades";

5
src/routes/index.tsx Normal file
View File

@@ -0,0 +1,5 @@
import Game from "../components/Game";
export default () => {
return <Game />;
};

57
src/types/cards.ts Normal file
View File

@@ -0,0 +1,57 @@
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 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;
};