kgames

KGames — free keyboard games for kids
git clone https://www.keyboard.games/code/kgames.git
Log | Files | Refs | README | LICENSE

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:
A.gitignore | 7+++++++
ALICENSE | 14++++++++++++++
AREADME.md | 42++++++++++++++++++++++++++++++++++++++++++
Aassets/thumbnails/placeholder.svg | 9+++++++++
Aassets/thumbnails/typing.svg | 13+++++++++++++
Acss/style.css | 114+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agames/typing/game.js | 96+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agames/typing/index.html | 25+++++++++++++++++++++++++
Aindex.html | 30++++++++++++++++++++++++++++++
Ajs/main.js | 32++++++++++++++++++++++++++++++++
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">&larr; 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 &mdash; 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); + }); +});