Files
game-kit/gamehall/site/index.html
T

267 lines
14 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Kestrel's Hall</title>
<meta name="description" content="Free board games in the browser: Wiz-War, Waving Hands, Hnefatafl, and Dip. Play with friends by link or alone against the house. No accounts, no adverts, nothing sold.">
<link rel="canonical" href="https://gamehall.kestrelsnest.social/">
<meta property="og:type" content="website">
<meta property="og:title" content="Kestrel's Hall">
<meta property="og:description" content="Free board games in the browser. Play with friends by link or alone against the house. No accounts, no adverts, nothing sold.">
<meta property="og:url" content="https://gamehall.kestrelsnest.social/">
<meta property="og:image" content="https://gamehall.kestrelsnest.social/og.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta name="twitter:card" content="summary_large_image">
<link rel="icon" href="favicon.svg">
<style>
:root {
--ink: #14181a;
--panel: #1c2124;
--line: #2c3236;
--bone: #e6dfcf;
--bone-dim: #a9a494;
--bone-faint: #7c7868;
--gold: #d2a94a;
--gutter: 16px;
}
* { box-sizing: border-box; }
html { background: var(--ink); }
body {
margin: 0;
background: var(--ink);
color: var(--bone);
font-family: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif;
font-size: 18px;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
a { color: var(--gold); text-decoration: none; }
a:hover { text-decoration: underline; }
.wrap { max-width: 1080px; margin: 0 auto; padding: 0 var(--gutter); }
header.mast { padding: 56px 0 28px; text-align: center; }
header.mast h1 {
margin: 0;
font-size: clamp(2.4rem, 6vw, 4rem);
font-weight: 400;
letter-spacing: 0.01em;
line-height: 1.05;
}
header.mast p.lede {
margin: 14px auto 0;
max-width: 38em;
font-size: clamp(1.05rem, 2.2vw, 1.3rem);
color: var(--bone-dim);
}
header.mast p.lede b { color: var(--bone); font-weight: 500; }
.games { display: grid; gap: 22px; grid-template-columns: repeat(2, 1fr); margin-top: 22px; }
@media (max-width: 860px) { .games { grid-template-columns: 1fr; max-width: 520px; margin-left: auto; margin-right: auto; } }
.game {
display: flex; flex-direction: column;
background: var(--panel);
border: 1px solid var(--line);
border-radius: 12px;
overflow: hidden;
color: inherit;
text-decoration: none;
transition: transform .15s ease, border-color .15s ease;
}
.game:hover { transform: translateY(-3px); border-color: var(--gold); text-decoration: none; }
.game .art { position: relative; aspect-ratio: 5 / 3; background: #222; overflow: hidden; }
.game .art img.fill { width: 100%; height: 100%; object-fit: cover; display: block; }
.game .art img.tafl { object-position: 50% 50%; }
.game .art img.hands { object-position: 0% 50%; }
.game .art img.dip { object-position: 50% 80%; }
.game .body { padding: 16px 18px 18px; display: flex; flex-direction: column; gap: 6px; flex: 1; }
.game h2 { margin: 0; font-size: 1.45rem; font-weight: 500; }
.game .pitch { margin: 0; color: var(--bone); }
.game .facts { margin: 0; color: var(--bone-faint); font-size: 0.92rem; }
.game .play { margin-top: auto; padding-top: 10px; color: var(--gold); font-size: 1rem; }
.game:hover .play { text-decoration: underline; }
/* The maze card: a stone floor in CSS, mortar-coloured walls, and the hall's own token drawings. */
.maze {
width: 100%; height: 100%;
background: #2a2e33;
background-image:
linear-gradient(rgba(255,255,255,.06) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,.06) 1px, transparent 1px),
radial-gradient(circle at 30% 40%, rgba(255,240,200,.05), transparent 60%);
background-size: 12.5% 20%, 12.5% 20%, 100% 100%;
}
.maze .w { position: absolute; background: #b8a77d; box-shadow: 0 0 0 1px #5c5238, 0 2px 4px rgba(0,0,0,.5); border-radius: 2px; }
.maze .h { height: 3%; }
.maze .v { width: 1.8%; }
.maze .t { position: absolute; width: 12.5%; filter: drop-shadow(0 2px 3px rgba(0,0,0,.6)); }
section { margin-top: 56px; }
section h2 { font-size: 1.6rem; font-weight: 400; margin: 0 0 12px; }
.cols { display: grid; grid-template-columns: 1fr 1fr; gap: 40px; }
@media (max-width: 720px) { .cols { grid-template-columns: 1fr; gap: 28px; } }
section p { margin: 0 0 12px; color: var(--bone-dim); }
section p b { color: var(--bone); font-weight: 500; }
ul.how { margin: 0; padding-left: 1.2em; color: var(--bone-dim); }
ul.how li { margin-bottom: 8px; }
ul.how b { color: var(--bone); font-weight: 500; }
.keeper { display: flex; gap: 20px; align-items: flex-start; }
.keeper img { width: 84px; height: 84px; border-radius: 50%; object-fit: cover; flex: 0 0 auto; border: 2px solid var(--line); }
@media (max-width: 480px) { .keeper { flex-direction: column; } }
footer { margin-top: 64px; padding: 28px 0 48px; border-top: 1px solid var(--line); color: var(--bone-faint); font-size: 0.9rem; }
footer p { margin: 0 0 8px; }
/* The hall-wide notice: its own colors, the same in every game of the hall. */
.hall-announcement { position: sticky; top: 0; z-index: 1000; display: flex; align-items: center; justify-content: center; gap: .8rem; padding: .45rem 2.4rem .45rem 1rem; background: #f2c45a; color: #1d1a12; font: 500 .95rem/1.35 system-ui, -apple-system, 'Segoe UI', sans-serif; text-align: center; box-shadow: 0 1px 4px #0005; }
.hall-announcement button { position: absolute; right: .5rem; background: none; border: 0; color: inherit; font-size: 1.3rem; line-height: 1; padding: .2rem .5rem; cursor: pointer; }
</style>
</head>
<body>
<div class="wrap">
<header class="mast">
<h1>Kestrel's Hall</h1>
<p class="lede"><b>Free board games in the browser.</b> Play with friends by sending a link, or alone against the house. No accounts, no adverts, no timers, and nothing sold.</p>
</header>
<div class="games">
<a class="game" href="https://wizwar.kestrelsnest.social">
<div class="art">
<div class="maze">
<div class="w h" style="left:12.5%;top:19%;width:25%"></div>
<div class="w v" style="left:36.2%;top:19%;height:42%"></div>
<div class="w h" style="left:50%;top:39%;width:37.5%"></div>
<div class="w v" style="left:61.2%;top:59%;height:41%"></div>
<div class="w h" style="left:0;top:59%;width:25%"></div>
<div class="w v" style="left:86.2%;top:0;height:40%"></div>
<div class="w h" style="left:75%;top:80%;width:25%"></div>
<img class="t" src="wizard-0.svg" alt="" style="left:0.5%;top:21%">
<img class="t" src="wizard-1.svg" style="left:75.5%;top:42%" alt="">
<img class="t" src="wizard-2.svg" style="left:38%;top:62%" alt="">
<img class="t" src="treasure-0.svg" style="left:88%;top:1%" alt="">
<img class="t" src="skeleton.svg" style="left:25.5%;top:1%" alt="">
<img class="t" src="troll.svg" style="left:50.5%;top:21%" alt="">
<img class="t" src="rock.svg" style="left:13%;top:82%" alt="">
</div>
</div>
<div class="body">
<h2>Wiz-War</h2>
<p class="pitch">Steal treasure. Blast through walls. Outwit your friends.</p>
<p class="facts">2 to 6 players · an unofficial rebuild of Tom Jolly's 1993 board game</p>
<span class="play">Play at wizwar.kestrelsnest.social →</span>
</div>
</a>
<a class="game" href="https://hands.kestrelsnest.social">
<div class="art"><img class="fill hands" src="hands.png" alt="The Waving Hands share card: a column of gesture letters beside the title."></div>
<div class="body">
<h2>Waving Hands</h2>
<p class="pitch">Wizards duel with their hands, and the right run of gestures is a spell.</p>
<p class="facts">2 to 12 players · Richard Bartle's pencil-and-paper duel, 1977</p>
<span class="play">Play at hands.kestrelsnest.social →</span>
</div>
</a>
<a class="game" href="https://tafl.kestrelsnest.social">
<div class="art"><img class="fill tafl" src="tafl.svg" alt="A hnefatafl board at the start of a game: the king on the throne, defenders around him, attackers on the four edges."></div>
<div class="body">
<h2>Hnefatafl</h2>
<p class="pitch">The Viking board game of a king breaking for safety and a ring of attackers closing in.</p>
<p class="facts">2 players · the Norse tafl game, by the Copenhagen rules or Linnaeus's 1732 Tablut</p>
<span class="play">Play at tafl.kestrelsnest.social →</span>
</div>
</a>
<a class="game" href="https://dip.kestrelsnest.social">
<div class="art"><img class="fill dip" src="dip.jpg" alt="Dip's map of Europe at the opening: the seven powers' armies and fleets on their home centers, each power's lands in its color."></div>
<div class="body">
<h2>Dip</h2>
<p class="pitch">Seven powers share one map, and none of them has to keep its word.</p>
<p class="facts">2 to 7 players · an unofficial rebuild of Allan B. Calhamer's 1959 Diplomacy</p>
<span class="play">Play at dip.kestrelsnest.social →</span>
</div>
</a>
</div>
<section class="cols">
<div>
<h2>How a table works</h2>
<ul class="how">
<li><b>Open a table</b> and you get a four-letter code and a link. Send it to a friend, and begin once they sit down.</li>
<li><b>No one home?</b> Every game can seat the house, whether an automaton, a housecarl, a rival wizard or an envoy, so you can play right away on your own.</li>
<li><b>Nothing to sign up for.</b> Your browser holds your seat. Close the tab and come back, and the game is where you left it.</li>
<li><b>Watchers welcome.</b> Anyone with the link can sit in the Peanut Gallery and, if the host allows, join the table talk.</li>
<li><b>Something went wrong?</b> Every table has a report button that reaches the keeper with the exact moment attached.</li>
</ul>
</div>
<div>
<h2>The keeper</h2>
<div class="keeper">
<img src="face.jpg" alt="Eric Wagoner">
<div>
<p>I am Eric, and I keep these tables from Athens, Georgia. These are games I have loved at a real table, rebuilt so friends can play them from wherever they are, with the door left open for anyone who wanders in.</p>
<p>Send word to <a href="mailto:eric@ericwagoner.com">eric@ericwagoner.com</a>, <a href="https://bsky.app/profile/kestrelsnest.social">@kestrelsnest.social</a> on Bluesky, or <a href="https://toots.kestrelsnest.social/@eric">@eric@toots.kestrelsnest.social</a> on Mastodon. I roost at <a href="https://blog.kestrelsnest.social">blog.kestrelsnest.social</a>.</p>
</div>
</div>
<h2 style="margin-top:28px">The hosting</h2>
<p>The games are free and will stay free. They run on small servers that cost a few dollars a month, and anyone who wants to chip in toward the hosting can do so at <a href="https://ko-fi.com/kestrelsnest">ko-fi.com/kestrelsnest</a>. Every game stays whole either way.</p>
</div>
</section>
<footer>
<p>Wiz-War is Tom Jolly's design, and the rights rest with Steve Jackson Games. Waving Hands is Richard Bartle's. Diplomacy is Allan B. Calhamer's design, and its name is a trademark of Hasbro. Nothing here is official or endorsed by Steve Jackson Games, Richard Bartle or Hasbro. Hnefatafl belongs to everyone, and is more than a thousand years old.</p>
</footer>
</div>
<script>
// The hall-wide notice every game of Kestrel's Hall shows (hall-announce on the
// host): checked on load, every few minutes, and when the page is looked at
// again; a reader may dismiss one, and the next still shows.
(() => {
const KEY = 'hall:dismissed';
let bar = null;
let current = null;
const read = () => { try { return localStorage.getItem(KEY); } catch { return null; } };
function when(at) {
const t = at ? Date.parse(at) : NaN;
if (!Number.isFinite(t)) return '';
const minutes = Math.round((t - Date.now()) / 60000);
if (minutes > 90) return ` (${new Date(t).toLocaleString([], { weekday: 'short', hour: 'numeric', minute: '2-digit' })}, your time)`;
if (minutes > 1) return ` (in ${minutes} minutes, at ${new Date(t).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })})`;
return minutes >= -15 ? ' (now)' : '';
}
function show() {
const a = current;
const live = a && a.id !== read() && !(a.until && Date.parse(a.until) < Date.now());
if (!live) { bar?.remove(); bar = null; return; }
if (!bar) {
bar = document.createElement('div');
bar.className = 'hall-announcement';
bar.setAttribute('role', 'status');
bar.innerHTML = '<span></span><button type="button" aria-label="Dismiss this announcement">×</button>';
bar.querySelector('button').onclick = () => { try { localStorage.setItem(KEY, current.id); } catch {} show(); };
document.body.prepend(bar);
}
bar.querySelector('span').textContent = a.text + when(a.at);
}
async function check() {
try {
const res = await fetch('/hall/announcement.json', { cache: 'no-store' });
const a = res.ok ? await res.json() : null;
current = a && typeof a.text === 'string' && a.text && typeof a.id === 'string' ? a : null;
} catch {}
show();
}
check();
setInterval(check, 5 * 60000);
setInterval(show, 30000);
document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') check(); });
})();
</script>
</body>
</html>