commit 2d3f151fe1bae1c9d4b55870b651e66117bf1cfd
Author: Kyle Barlow <kb@kylebarlow.com>
Date: Sun, 19 Apr 2026 12:21:08 -0700
Initial KGames scaffold
Landing page with responsive game grid, GPL-3.0 license, and a
working Phaser 3 typing game that speaks each letter via the
Web Speech API. No build tooling — opens directly from file://.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat:
10 files changed, 382 insertions(+), 0 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,7 @@
+.DS_Store
+Thumbs.db
+*.swp
+*.swo
+.idea/
+.vscode/
+node_modules/
diff --git a/LICENSE b/LICENSE
@@ -0,0 +1,14 @@
+GNU General Public License v3.0
+
+Copyright (C) 2024 KGames contributors
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+Full license text: https://www.gnu.org/licenses/gpl-3.0.txt
diff --git a/README.md b/README.md
@@ -0,0 +1,42 @@
+# KGames
+
+Free games for kids. No ads. No signup. No backend.
+
+## Philosophy
+
+- Everything runs in the browser — HTML5, CSS, and JavaScript only
+- No build tools, no package managers, no server required
+- Games use [Phaser 3](https://phaser.io/) loaded from CDN
+- Open source under GPL-3.0
+
+## Run locally
+
+Just open `index.html` in any browser — double-click it or run:
+
+```
+xdg-open index.html # Linux
+open index.html # macOS
+```
+
+An internet connection is required on first load so the browser can fetch Phaser and Google Fonts from their CDNs. After that, the games themselves work offline.
+
+## Deploy
+
+```
+rsync -av --delete . user@server:/path/to/docroot/
+```
+
+Standard static file hosting. No `.htaccess` tricks needed.
+
+## Add a game
+
+1. Create `games/<slug>/index.html` and `games/<slug>/game.js`
+2. Add an entry to the `GAMES` array in `js/main.js`:
+ ```js
+ { slug: '<slug>', title: 'My Game', thumb: 'assets/thumbnails/<slug>.svg', status: 'live' }
+ ```
+3. Add a thumbnail SVG at `assets/thumbnails/<slug>.svg`
+
+## License
+
+GPL-3.0 — see [LICENSE](LICENSE)
diff --git a/assets/thumbnails/placeholder.svg b/assets/thumbnails/placeholder.svg
@@ -0,0 +1,9 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 160 130" width="160" height="130">
+ <!-- background -->
+ <rect x="8" y="8" width="144" height="114" rx="18" fill="#F5F5F5" stroke="#CCCCCC" stroke-width="4"/>
+
+ <!-- question mark circle -->
+ <circle cx="80" cy="60" r="32" fill="#DDDDDD"/>
+ <text x="80" y="75" font-family="Fredoka, sans-serif" font-size="44" font-weight="600"
+ fill="#888888" text-anchor="middle" dominant-baseline="auto">?</text>
+</svg>
diff --git a/assets/thumbnails/typing.svg b/assets/thumbnails/typing.svg
@@ -0,0 +1,13 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 160 130" width="160" height="130">
+ <!-- background rounded rect -->
+ <rect x="8" y="8" width="144" height="114" rx="18" fill="#FFF5F5" stroke="#E63946" stroke-width="4"/>
+
+ <!-- big letter A -->
+ <text x="80" y="76" font-family="Fredoka, sans-serif" font-size="72" font-weight="600"
+ fill="#E63946" text-anchor="middle" dominant-baseline="auto">A</text>
+
+ <!-- three tiny key bumps at the bottom -->
+ <rect x="30" y="96" width="26" height="16" rx="5" fill="#E63946" opacity="0.25"/>
+ <rect x="67" y="96" width="26" height="16" rx="5" fill="#E63946" opacity="0.25"/>
+ <rect x="104" y="96" width="26" height="16" rx="5" fill="#E63946" opacity="0.25"/>
+</svg>
diff --git a/css/style.css b/css/style.css
@@ -0,0 +1,114 @@
+:root {
+ --red: #E63946;
+ --blue: #1D7CF2;
+ --yellow:#FFD23F;
+ --green: #2EC4B6;
+ --navy: #1A1A2E;
+ --bg: #FFFFFF;
+}
+
+*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
+
+body {
+ font-family: 'Baloo 2', cursive, sans-serif;
+ background: var(--bg);
+ color: var(--navy);
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+}
+
+/* ── Header ── */
+header {
+ text-align: center;
+ padding: 2.5rem 1rem 1rem;
+}
+
+h1 {
+ font-family: 'Fredoka', sans-serif;
+ font-size: clamp(3rem, 10vw, 6rem);
+ font-weight: 600;
+ line-height: 1;
+ letter-spacing: 0.02em;
+}
+
+.tagline {
+ font-size: 1.2rem;
+ color: #555;
+ margin-top: 0.5rem;
+}
+
+/* ── Grid ── */
+main {
+ flex: 1;
+ padding: 2rem 1.5rem;
+ max-width: 1100px;
+ width: 100%;
+ margin: 0 auto;
+}
+
+#game-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
+ gap: 1.25rem;
+}
+
+/* ── Tiles ── */
+.game-tile {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 1.25rem 1rem 1rem;
+ border: 4px solid currentColor;
+ border-radius: 16px;
+ text-decoration: none;
+ color: var(--navy);
+ background: #fff;
+ transition: transform 0.15s ease, box-shadow 0.15s ease;
+ cursor: pointer;
+}
+
+a.game-tile:hover,
+a.game-tile:focus-visible {
+ transform: translateY(-4px);
+ box-shadow: 0 6px 18px rgba(0,0,0,0.12);
+ outline: none;
+}
+
+a.game-tile:focus-visible {
+ outline: 3px dashed currentColor;
+ outline-offset: 3px;
+}
+
+.tile-thumb {
+ width: 100%;
+ max-width: 160px;
+ height: 130px;
+ object-fit: contain;
+}
+
+.tile-title {
+ font-family: 'Fredoka', sans-serif;
+ font-size: 1.15rem;
+ font-weight: 600;
+ margin-top: 0.6rem;
+ text-align: center;
+}
+
+/* ── Coming-soon state ── */
+.game-tile.coming-soon {
+ filter: grayscale(1);
+ opacity: 0.45;
+ pointer-events: none;
+ cursor: default;
+}
+
+/* ── Footer ── */
+footer {
+ text-align: center;
+ padding: 1.25rem;
+ font-size: 0.85rem;
+ color: #888;
+}
+
+footer a { color: #888; }
diff --git a/games/typing/game.js b/games/typing/game.js
@@ -0,0 +1,96 @@
+const COLORS = ['#E63946', '#1D7CF2', '#FFD23F', '#2EC4B6'];
+const MAX_LETTERS = 11;
+const LETTER_SPACING = 68;
+
+class TypingScene extends Phaser.Scene {
+ constructor() {
+ super({ key: 'TypingScene' });
+ this.letters = [];
+ }
+
+ create() {
+ this.add.text(400, 50, 'Type any key!', {
+ fontFamily: 'Fredoka, sans-serif',
+ fontSize: '26px',
+ color: '#aaaaaa',
+ }).setOrigin(0.5);
+
+ this.add.text(400, 570, 'Backspace or Esc to clear', {
+ fontFamily: 'Fredoka, sans-serif',
+ fontSize: '18px',
+ color: '#555577',
+ }).setOrigin(0.5);
+
+ this.input.keyboard.on('keydown', (event) => {
+ if (event.key === 'Escape' || event.key === 'Backspace') {
+ this.clearLetters();
+ return;
+ }
+ if (event.key.length !== 1) return;
+ this.addLetter(event.key.toUpperCase());
+ this.speak(event.key);
+ });
+ }
+
+ addLetter(char) {
+ if (this.letters.length >= MAX_LETTERS) this.clearLetters();
+
+ const idx = this.letters.length;
+ const color = COLORS[idx % COLORS.length];
+
+ const text = this.add.text(0, 300, char, {
+ fontFamily: 'Fredoka, sans-serif',
+ fontSize: '80px',
+ fontStyle: 'bold',
+ color,
+ }).setOrigin(0.5);
+
+ text.setScale(0.3);
+ this.letters.push(text);
+ this.reflow();
+
+ this.tweens.add({
+ targets: text,
+ scaleX: 1,
+ scaleY: 1,
+ ease: 'Back.Out',
+ duration: 180,
+ });
+ }
+
+ reflow() {
+ const n = this.letters.length;
+ const totalW = (n - 1) * LETTER_SPACING;
+ const startX = 400 - totalW / 2;
+ this.letters.forEach((obj, i) => { obj.x = startX + i * LETTER_SPACING; });
+ }
+
+ clearLetters() {
+ this.letters.forEach(obj => {
+ this.tweens.add({
+ targets: obj,
+ alpha: 0,
+ scaleX: 0.2,
+ scaleY: 0.2,
+ duration: 150,
+ onComplete: () => obj.destroy(),
+ });
+ });
+ this.letters = [];
+ }
+
+ speak(letter) {
+ if (!window.speechSynthesis) return;
+ window.speechSynthesis.cancel();
+ window.speechSynthesis.speak(new SpeechSynthesisUtterance(letter));
+ }
+}
+
+new Phaser.Game({
+ type: Phaser.AUTO,
+ width: 800,
+ height: 600,
+ parent: 'game-container',
+ backgroundColor: '#1A1A2E',
+ scene: TypingScene,
+});
diff --git a/games/typing/index.html b/games/typing/index.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Typing for Now — KGames</title>
+ <link rel="preconnect" href="https://fonts.googleapis.com">
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+ <link href="https://fonts.googleapis.com/css2?family=Fredoka:wght@400;600&display=swap" rel="stylesheet">
+ <style>
+ * { margin: 0; padding: 0; box-sizing: border-box; }
+ body { background: #1A1A2E; font-family: 'Fredoka', sans-serif; display: flex; flex-direction: column; align-items: center; min-height: 100vh; }
+ nav { width: 100%; padding: 0.75rem 1.25rem; }
+ nav a { color: #aaa; text-decoration: none; font-size: 1rem; }
+ nav a:hover { color: #fff; }
+ #game-container { display: flex; justify-content: center; }
+ </style>
+</head>
+<body>
+ <nav><a href="../../index.html">← Back to KGames</a></nav>
+ <div id="game-container"></div>
+ <script src="https://cdn.jsdelivr.net/npm/phaser@3.80.1/dist/phaser.min.js"></script>
+ <script src="game.js"></script>
+</body>
+</html>
diff --git a/index.html b/index.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>KGames</title>
+ <link rel="preconnect" href="https://fonts.googleapis.com">
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+ <link href="https://fonts.googleapis.com/css2?family=Fredoka:wght@400;600&family=Baloo+2:wght@400;600&display=swap" rel="stylesheet">
+ <link rel="stylesheet" href="css/style.css">
+</head>
+<body>
+ <header>
+ <h1>
+ <span style="color:#E63946">K</span><span style="color:#1D7CF2">G</span><span style="color:#FFD23F">a</span><span style="color:#2EC4B6">m</span><span style="color:#E63946">e</span><span style="color:#1D7CF2">s</span>
+ </h1>
+ <p class="tagline">Free games for kids — no ads, no signup.</p>
+ </header>
+
+ <main>
+ <div id="game-grid"></div>
+ </main>
+
+ <footer>
+ <p>Open source (<a href="LICENSE">GPL-3.0</a>)</p>
+ </footer>
+
+ <script src="js/main.js"></script>
+</body>
+</html>
diff --git a/js/main.js b/js/main.js
@@ -0,0 +1,32 @@
+const GAMES = [
+ { slug: 'typing', title: 'Typing for Now', thumb: 'assets/thumbnails/typing.svg', status: 'live' },
+ { slug: null, title: 'Coming Soon', thumb: 'assets/thumbnails/placeholder.svg', status: 'placeholder' },
+ { slug: null, title: 'Coming Soon', thumb: 'assets/thumbnails/placeholder.svg', status: 'placeholder' },
+ { slug: null, title: 'Coming Soon', thumb: 'assets/thumbnails/placeholder.svg', status: 'placeholder' },
+ { slug: null, title: 'Coming Soon', thumb: 'assets/thumbnails/placeholder.svg', status: 'placeholder' },
+ { slug: null, title: 'Coming Soon', thumb: 'assets/thumbnails/placeholder.svg', status: 'placeholder' },
+];
+
+const PALETTE = ['#E63946', '#1D7CF2', '#FFD23F', '#2EC4B6'];
+
+document.addEventListener('DOMContentLoaded', () => {
+ const grid = document.getElementById('game-grid');
+
+ GAMES.forEach((game, i) => {
+ const color = PALETTE[i % PALETTE.length];
+ const isLive = game.status === 'live';
+
+ const el = document.createElement(isLive ? 'a' : 'div');
+ el.className = 'game-tile' + (isLive ? '' : ' coming-soon');
+ el.style.borderColor = color;
+
+ if (isLive) el.href = `games/${game.slug}/index.html`;
+
+ el.innerHTML = `
+ <img src="${game.thumb}" alt="${game.title}" class="tile-thumb">
+ <span class="tile-title">${game.title}</span>
+ `;
+
+ grid.appendChild(el);
+ });
+});