The high score table moves out of the browser and into a service, so every player is on one board. It is a Node process with SQLite, no build step and no native modules - node:sqlite ships with the runtime and Node runs the TypeScript directly. Everything in a request is treated as hostile: names forced to a printable subset, every number range-checked, and scores refused if they could not have happened in the days claimed. Bodies capped, submissions rate limited per address. It still cannot prove a score is real, and server/README.md says so plainly rather than implying otherwise. The game degrades properly without it: the board says it cannot reach town, posting happens behind the closing standings, and play is untouched. Deployment is three containers behind the host's nginx - the hub at /, the game at /lemonade/, the scores service at /api/ - so the game keeps its own container and a second game is just another service. Licensing: AGPL-3.0-or-later, Affero because the leaderboard is a network service. NOTICE.md credits Bob Jamison and Charlie Kellner, records that this is clean-room work, and is honest about the one thing it is not: some on-screen wording is quoted from the original and cannot be licensed by us. The street can now get better as well as worse. The summer fair brings the town out and lifts what they will pay; a rival on the next corner takes a share and lingers. Both show in the art and in the crowd. Over 500 seasons a player who reads the briefing survives every time and finishes around $25.80; one who ignores it goes broke 70% of the time.
164 lines
4.6 KiB
HTML
164 lines
4.6 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<meta name="theme-color" content="#1d1136" />
|
|
<title>Games · jcoffey.dev</title>
|
|
<meta
|
|
name="description"
|
|
content="Small browser games. No accounts, no tracking, no installs — just open one and play."
|
|
/>
|
|
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
|
<style>
|
|
:root {
|
|
--ink: #241640;
|
|
--cream: #fff6e6;
|
|
--lemon: #ffd93d;
|
|
--pink: #ff5d8f;
|
|
--teal: #2ec4b6;
|
|
--dim: #b9a9e0;
|
|
color-scheme: dark;
|
|
}
|
|
|
|
* { box-sizing: border-box; }
|
|
|
|
body {
|
|
margin: 0;
|
|
min-height: 100dvh;
|
|
padding: clamp(24px, 6vw, 72px) clamp(16px, 5vw, 40px);
|
|
background:
|
|
radial-gradient(90% 60% at 15% 0%, rgba(255, 93, 143, 0.28) 0%, transparent 60%),
|
|
radial-gradient(80% 60% at 90% 15%, rgba(46, 196, 182, 0.24) 0%, transparent 55%),
|
|
radial-gradient(120% 90% at 50% 100%, #3a2168 0%, #1d1136 55%, #0e0820 100%);
|
|
background-attachment: fixed;
|
|
color: var(--cream);
|
|
font-family: ui-rounded, "Nunito", "Avenir Next", "Segoe UI", system-ui, sans-serif;
|
|
display: flex;
|
|
justify-content: center;
|
|
}
|
|
|
|
main { width: min(760px, 100%); }
|
|
|
|
header { margin-bottom: clamp(28px, 6vw, 48px); }
|
|
|
|
h1 {
|
|
margin: 0;
|
|
font-size: clamp(2.4rem, 9vw, 4rem);
|
|
font-weight: 900;
|
|
letter-spacing: -0.03em;
|
|
line-height: 0.95;
|
|
color: var(--lemon);
|
|
-webkit-text-stroke: 3px var(--ink);
|
|
paint-order: stroke fill;
|
|
text-shadow: 6px 6px 0 var(--ink);
|
|
}
|
|
|
|
header p {
|
|
margin: 1.1rem 0 0;
|
|
max-width: 44ch;
|
|
color: var(--dim);
|
|
font-size: clamp(1rem, 2.6vw, 1.15rem);
|
|
line-height: 1.55;
|
|
}
|
|
|
|
ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 20px; }
|
|
|
|
.game {
|
|
display: block;
|
|
padding: clamp(18px, 4vw, 28px);
|
|
border: 3px solid var(--ink);
|
|
border-radius: 22px;
|
|
background: linear-gradient(165deg, #5a3a9e 0%, #3c2470 100%);
|
|
box-shadow: 7px 7px 0 var(--ink);
|
|
color: inherit;
|
|
text-decoration: none;
|
|
transition: transform 120ms ease, box-shadow 120ms ease;
|
|
}
|
|
|
|
.game:hover, .game:focus-visible {
|
|
transform: translate(-3px, -3px);
|
|
box-shadow: 10px 10px 0 var(--ink);
|
|
outline: none;
|
|
}
|
|
|
|
.game:active { transform: translate(3px, 3px); box-shadow: 3px 3px 0 var(--ink); }
|
|
|
|
.game h2 {
|
|
margin: 0;
|
|
font-size: clamp(1.5rem, 4.5vw, 2rem);
|
|
font-weight: 900;
|
|
letter-spacing: -0.02em;
|
|
}
|
|
|
|
.game p { margin: 0.6rem 0 0; color: var(--dim); line-height: 1.55; max-width: 52ch; }
|
|
|
|
.tags { margin-top: 1rem; display: flex; flex-wrap: wrap; gap: 8px; }
|
|
|
|
.tags span {
|
|
font-size: 0.78rem;
|
|
font-weight: 800;
|
|
letter-spacing: 0.06em;
|
|
text-transform: uppercase;
|
|
padding: 0.28em 0.8em;
|
|
border-radius: 999px;
|
|
border: 2px solid var(--ink);
|
|
background: var(--lemon);
|
|
color: var(--ink);
|
|
}
|
|
|
|
.tags span.alt { background: var(--teal); }
|
|
.tags span.alt2 { background: var(--pink); color: var(--cream); }
|
|
|
|
footer {
|
|
margin-top: clamp(36px, 8vw, 64px);
|
|
color: var(--dim);
|
|
font-size: 0.9rem;
|
|
line-height: 1.7;
|
|
}
|
|
|
|
footer a { color: var(--lemon); }
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.game { transition: none; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<main>
|
|
<header>
|
|
<h1>Games</h1>
|
|
<p>
|
|
Small browser games. Nothing to install, no account to make, and no
|
|
tracking — open one and play.
|
|
</p>
|
|
</header>
|
|
|
|
<ul>
|
|
<li>
|
|
<a class="game" href="/lemonade/">
|
|
<h2>Lemonade Stand</h2>
|
|
<p>
|
|
The 1979 business sim, rebuilt. Read the weather, buy your glasses,
|
|
price the jug and hope it doesn't rain. Play it as the Atari left
|
|
it, or in a cel-shaded remaster with a funk band — same rules
|
|
either way.
|
|
</p>
|
|
<div class="tags">
|
|
<span>1–4 players</span>
|
|
<span class="alt">Leaderboard</span>
|
|
<span class="alt2">Remastered</span>
|
|
</div>
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
|
|
<footer>
|
|
<p>
|
|
Free software, AGPL-3.0-or-later. Built by John Coffey.
|
|
</p>
|
|
</footer>
|
|
</main>
|
|
</body>
|
|
</html>
|