kgames

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

commit 1563bb67890aac942a95b10401e607cb18415a1e
parent 751a75cb7d3496d1586e0da1c01ecf877891bf22
Author: Kyle Barlow <kb@kylebarlow.com>
Date:   Mon, 17 Aug 2026 21:15:51 -0700

Lengthen music

Diffstat:
Mgames/animal-letter/game.js | 60+++++++++++++++++++++++++++++++++++++-----------------------
Mgames/animal-spell/game.js | 52++++++++++++++++++++++++++++++++++++----------------
Mgames/fire-truck/game.js | 97+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
Mgames/letter-find/game.js | 46++++++++++++++++++++++++++++++++++------------
4 files changed, 175 insertions(+), 80 deletions(-)

diff --git a/games/animal-letter/game.js b/games/animal-letter/game.js @@ -2,29 +2,43 @@ const COLORS = ['#E63946', '#1D7CF2', '#FFD23F', '#2EC4B6']; const GOAL = 10; const CDN = 'https://cdn.jsdelivr.net/gh/jdecked/twemoji@15.1.0/assets/svg/'; -// F major — circus-like leaps and animal energy (32-note loop, ~7.5s cycle at 128 BPM) -const MELODY = [ - // bar 1: fanfare — big octave leaps - 'F4','C5','F5','C5', 'F4','A4','C5','A4', - // bar 2: bounding up to Bb - 'Bb4','D5','F5','Bb5', 'F5','D5','Bb4','F4', - // bar 3: pivot to C — chase scene - 'C5','E5','G5','C6', 'G5','E5','C5','E5', - // bar 4: swinging close - 'F5','D5','Bb4','F4', 'C5','F5','A5','F5', -]; -const HARMONY = [ - // diatonic 3rds above in F major - 'A4','E5','A5','E5', 'A4','C5','E5','C5', - 'D5','F5','A5','D6', 'A5','F5','D5','A4', - 'E5','G5','Bb5','E6', 'Bb5','G5','E5','G5', - 'A5','F5','D5','A4', 'E5','A5','C6','A5', -]; -const BASS = [ - // 2-bar ostinato (loops independently of 4-bar melody) - 'F2', null, null, null, null, null, null, null, - 'Bb2',null, null, null, 'C3', null, null, null, -]; +// F major — circus-like leaps and animal energy, extended to a ~1-minute song: +// 32-step sections (eighth notes at 128 BPM) concatenated so the loop is long +// enough not to grate. 'A' is the original fanfare; B–D are new sections in +// the same character. '.' = rest. Harmony is a diatonic third above the +// melody (it plays during the success jingle). +const N = s => s.trim().split(/\s+/).map(t => (t === '.' ? null : t)); + +const MEL = { + A: N(`F4 C5 F5 C5 F4 A4 C5 A4 Bb4 D5 F5 Bb5 F5 D5 Bb4 F4 + C5 E5 G5 C6 G5 E5 C5 E5 F5 D5 Bb4 F4 C5 F5 A5 F5`), + B: N(`F5 E5 D5 C5 D5 E5 F5 A5 G5 F5 E5 D5 C5 D5 E5 C5 + A5 G5 F5 G5 A5 C6 A5 F5 G5 E5 C5 E5 F5 A5 G5 F5`), + C: N(`D5 F5 A5 D6 A5 F5 D5 F5 C5 E5 G5 C6 G5 E5 C5 E5 + Bb4 D5 F5 Bb5 F5 D5 Bb4 D5 C5 F5 A5 C6 A5 G5 F5 F5`), + D: N(`A4 Bb4 C5 D5 C5 Bb4 A4 F4 G4 A4 Bb4 C5 Bb4 A4 G4 E4 + F4 A4 C5 F5 E5 C5 A4 C5 D5 C5 Bb4 A4 G4 C5 E5 F5`), +}; +const HAR = { + A: N(`A4 E5 A5 E5 A4 C5 E5 C5 D5 F5 A5 D6 A5 F5 D5 A4 + E5 G5 Bb5 E6 Bb5 G5 E5 G5 A5 F5 D5 A4 E5 A5 C6 A5`), + B: N(`A5 G5 F5 E5 F5 G5 A5 C6 Bb5 A5 G5 F5 E5 F5 G5 E5 + C6 Bb5 A5 Bb5 C6 E6 C6 A5 Bb5 G5 E5 G5 A5 C6 Bb5 A5`), + C: N(`F5 A5 C6 F6 C6 A5 F5 A5 E5 G5 Bb5 E6 Bb5 G5 E5 G5 + D5 F5 A5 D6 A5 F5 D5 F5 E5 A5 C6 E6 C6 Bb5 A5 A5`), + D: N(`C5 D5 E5 F5 E5 D5 C5 A4 Bb4 C5 D5 E5 D5 C5 Bb4 G4 + A4 C5 E5 A5 G5 E5 C5 E5 F5 E5 D5 C5 Bb4 E5 G5 A5`), +}; +const BAS = { + A: N(`F2 . . . . . . . Bb2 . . . C3 . . . F2 . . . . . . . Bb2 . . . C3 . . .`), + B: N(`F2 . . . . . . . C3 . . . . . . . F2 . . . . . . . C3 . . . F2 . . .`), + C: N(`D3 . . . . . . . C3 . . . . . . . Bb2 . . . . . . . C3 . . . F2 . . .`), + D: N(`F2 . . . . . . . C3 . . . . . . . F2 . . . . . . . Bb2 . . . C3 . . .`), +}; +const ORDER = ['A','B','A','C','D','B','C','A']; +const MELODY = ORDER.flatMap(k => MEL[k]); +const HARMONY = ORDER.flatMap(k => HAR[k]); +const BASS = ORDER.flatMap(k => BAS[k]); class AnimalLetterScene extends Phaser.Scene { constructor() { diff --git a/games/animal-spell/game.js b/games/animal-spell/game.js @@ -2,22 +2,42 @@ const COLORS = ['#E63946', '#1D7CF2', '#FFD23F', '#2EC4B6']; const GOAL = 10; const CDN = 'https://cdn.jsdelivr.net/gh/jdecked/twemoji@15.1.0/assets/svg/'; -const MELODY = [ - 'F4','C5','F5','C5', 'F4','A4','C5','A4', - 'Bb4','D5','F5','Bb5', 'F5','D5','Bb4','F4', - 'C5','E5','G5','C6', 'G5','E5','C5','E5', - 'F5','D5','Bb4','F4', 'C5','F5','A5','F5', -]; -const HARMONY = [ - 'A4','E5','A5','E5', 'A4','C5','E5','C5', - 'D5','F5','A5','D6', 'A5','F5','D5','A4', - 'E5','G5','Bb5','E6', 'Bb5','G5','E5','G5', - 'A5','F5','D5','A4', 'E5','A5','C6','A5', -]; -const BASS = [ - 'F2', null, null, null, null, null, null, null, - 'Bb2',null, null, null, 'C3', null, null, null, -]; +// ~1-minute song: 32-step sections (eighth notes at 128 BPM) concatenated so +// the loop is long enough not to grate. 'A' is the original circus fanfare; +// B–D are new sections in the same F-major character. '.' = rest. Harmony is +// a diatonic third above the melody (it plays during the success jingle). +const N = s => s.trim().split(/\s+/).map(t => (t === '.' ? null : t)); + +const MEL = { + A: N(`F4 C5 F5 C5 F4 A4 C5 A4 Bb4 D5 F5 Bb5 F5 D5 Bb4 F4 + C5 E5 G5 C6 G5 E5 C5 E5 F5 D5 Bb4 F4 C5 F5 A5 F5`), + B: N(`F5 E5 D5 C5 D5 E5 F5 A5 G5 F5 E5 D5 C5 D5 E5 C5 + A5 G5 F5 G5 A5 C6 A5 F5 G5 E5 C5 E5 F5 A5 G5 F5`), + C: N(`D5 F5 A5 D6 A5 F5 D5 F5 C5 E5 G5 C6 G5 E5 C5 E5 + Bb4 D5 F5 Bb5 F5 D5 Bb4 D5 C5 F5 A5 C6 A5 G5 F5 F5`), + D: N(`A4 Bb4 C5 D5 C5 Bb4 A4 F4 G4 A4 Bb4 C5 Bb4 A4 G4 E4 + F4 A4 C5 F5 E5 C5 A4 C5 D5 C5 Bb4 A4 G4 C5 E5 F5`), +}; +const HAR = { + A: N(`A4 E5 A5 E5 A4 C5 E5 C5 D5 F5 A5 D6 A5 F5 D5 A4 + E5 G5 Bb5 E6 Bb5 G5 E5 G5 A5 F5 D5 A4 E5 A5 C6 A5`), + B: N(`A5 G5 F5 E5 F5 G5 A5 C6 Bb5 A5 G5 F5 E5 F5 G5 E5 + C6 Bb5 A5 Bb5 C6 E6 C6 A5 Bb5 G5 E5 G5 A5 C6 Bb5 A5`), + C: N(`F5 A5 C6 F6 C6 A5 F5 A5 E5 G5 Bb5 E6 Bb5 G5 E5 G5 + D5 F5 A5 D6 A5 F5 D5 F5 E5 A5 C6 E6 C6 Bb5 A5 A5`), + D: N(`C5 D5 E5 F5 E5 D5 C5 A4 Bb4 C5 D5 E5 D5 C5 Bb4 G4 + A4 C5 E5 A5 G5 E5 C5 E5 F5 E5 D5 C5 Bb4 E5 G5 A5`), +}; +const BAS = { + A: N(`F2 . . . . . . . Bb2 . . . C3 . . . F2 . . . . . . . Bb2 . . . C3 . . .`), + B: N(`F2 . . . . . . . C3 . . . . . . . F2 . . . . . . . C3 . . . F2 . . .`), + C: N(`D3 . . . . . . . C3 . . . . . . . Bb2 . . . . . . . C3 . . . F2 . . .`), + D: N(`F2 . . . . . . . C3 . . . . . . . F2 . . . . . . . Bb2 . . . C3 . . .`), +}; +const ORDER = ['A','B','A','C','D','B','C','A']; +const MELODY = ORDER.flatMap(k => MEL[k]); +const HARMONY = ORDER.flatMap(k => HAR[k]); +const BASS = ORDER.flatMap(k => BAS[k]); class AnimalSpellScene extends Phaser.Scene { constructor() { diff --git a/games/fire-truck/game.js b/games/fire-truck/game.js @@ -42,39 +42,78 @@ const PALM_TRUNK = 0x9a6b43; const PALM_FROND = 0x46b063; const BIKE_LANE_COLOR = 0x3e8e5a; // painted protected bike lanes -const FT_BPM = 158; -const FT_MELODY = [ - 'E5','G5','A5','B5','G5','E5','D5','E5', - 'A4','C5','D5','E5','C5','A4','G4','A4', -]; -const FT_BASS = [ - 'E2','B2','E2','B2','A2','C3','E3','A2', - 'G2','B2','G2','D3','A2','E2','A2','E2', -]; -const FT_CHORDS = [ - ['E3','G3','B3'], null, null, null, - ['A2','C3','E3'], null, null, null, - ['G2','B2','D3'], null, null, null, - ['A2','C3','E3'], null, null, null, -]; +// Both songs are ~1-minute arrangements: 16-step phrases (eighth notes) +// concatenated so the loop is long enough not to grate. 'A' is the original +// theme; the other phrases are new sections in the same E-minor character. +// '.' = rest in the string form; chord rows place a triad every 4 steps +// (main theme) or every 2 steps (end fanfare). +const ftN = s => s.trim().split(/\s+/).map(t => (t === '.' ? null : t)); +const FTC = { + Em: ['E3','G3','B3'], Am: ['A2','C3','E3'], G: ['G2','B2','D3'], + C: ['C3','E3','G3'], D: ['D3','F#3','A3'], Bm: ['B2','D3','F#3'], +}; +const ftChd4 = (a, b, c, d) => + [a, null, null, null, b, null, null, null, c, null, null, null, d, null, null, null]; +const ftChd8 = (...cs) => cs.flatMap(c => [c, null]); + +const FT_BPM = 158; +const FT_MEL = { + A: ftN('E5 G5 A5 B5 G5 E5 D5 E5 A4 C5 D5 E5 C5 A4 G4 A4'), + B: ftN('B4 D5 E5 G5 E5 D5 B4 D5 E5 G5 B5 A5 G5 E5 D5 B4'), + C: ftN('E4 G4 A4 B4 A4 G4 E4 G4 A4 B4 C5 B4 A4 G4 F#4 E4'), + D: ftN('B5 A5 G5 A5 B5 E6 D6 B5 C6 B5 A5 G5 A5 B5 A5 G5'), + E: ftN('G5 B5 D6 B5 G5 D5 B4 D5 C5 E5 G5 E5 D5 F#5 A5 F#5'), + F: ftN('E5 D5 C5 B4 A4 B4 C5 A4 B4 C5 D5 E5 F#5 D5 B4 E5'), +}; +const FT_BAS = { + A: ftN('E2 B2 E2 B2 A2 C3 E3 A2 G2 B2 G2 D3 A2 E2 A2 E2'), + B: ftN('E2 B2 E2 B2 G2 B2 G2 B2 C3 G2 C3 G2 D3 A2 D3 B2'), + C: ftN('E2 E3 E2 E3 E2 E3 E2 B2 A2 A3 A2 A3 B2 F#2 B2 E2'), + D: ftN('E2 B2 E3 B2 E2 B2 E3 B2 A2 E3 A2 E3 D3 A2 D3 A2'), + E: ftN('G2 D3 G2 D3 G2 B2 G2 B2 C3 G2 C3 G2 D3 A2 D3 C3'), + F: ftN('A2 E3 A2 E3 A2 C3 A2 C3 B2 F#2 B2 F#2 B2 D3 B2 E2'), +}; +const FT_CHD = { + A: ftChd4(FTC.Em, FTC.Am, FTC.G, FTC.Am), + B: ftChd4(FTC.Em, FTC.G, FTC.C, FTC.D), + C: ftChd4(FTC.Em, FTC.Em, FTC.Am, FTC.Bm), + D: ftChd4(FTC.Em, FTC.Em, FTC.Am, FTC.D), + E: ftChd4(FTC.G, FTC.G, FTC.C, FTC.D), + F: ftChd4(FTC.Am, FTC.Am, FTC.Bm, FTC.Bm), +}; +const FT_ORDER = ['A','B','A','C','D','B','E','F','A','C', + 'D','E','B','F','C','D','A','B','F','A']; +const FT_MELODY = FT_ORDER.flatMap(k => FT_MEL[k]); +const FT_BASS = FT_ORDER.flatMap(k => FT_BAS[k]); +const FT_CHORDS = FT_ORDER.flatMap(k => FT_CHD[k]); // Celebration song for the endgame screen — related to the main theme // (same Em/Am/G harmonic language, brighter octave, triumphant ascending phrases) const FT_END_BPM = 170; -const FT_END_MELODY = [ - 'E5','G5','B5','E6', 'D6','B5','G5','A5', - 'A5','C6','E6','A6', 'G6','E6','D6','E6', -]; -const FT_END_BASS = [ - 'E3','G3','B3','E4', 'D4','B3','G3','A3', - 'A3','C4','E4','A4', 'G4','E4','B3','E4', -]; -const FT_END_CHORDS = [ - ['E3','G3','B3'], null, ['B2','D3','G3'], null, - ['G2','B2','D3'], null, ['A2','E3','A3'], null, - ['A2','C3','E3'], null, ['E3','A3','C4'], null, - ['G2','B2','D3'], null, ['E3','G3','B3'], null, -]; +const FT_END_MEL = { + A: ftN('E5 G5 B5 E6 D6 B5 G5 A5 A5 C6 E6 A6 G6 E6 D6 E6'), + B: ftN('B5 G5 E5 G5 B5 D6 E6 D6 C6 A5 E5 A5 C6 E6 D6 B5'), + C: ftN('G5 B5 D6 G6 E6 C6 G5 C6 D6 B5 G5 B5 E6 D6 B5 A5'), + D: ftN('E6 D6 B5 G5 A5 B5 C6 A5 B5 G5 E5 G5 B5 D6 E6 E6'), +}; +const FT_END_BAS = { + A: ftN('E3 G3 B3 E4 D4 B3 G3 A3 A3 C4 E4 A4 G4 E4 B3 E4'), + B: ftN('E3 B3 G3 B3 E4 B3 G3 B3 A3 E4 C4 E4 D4 A3 D4 B3'), + C: ftN('G3 B3 D4 G4 C4 E4 G4 C4 D4 B3 G3 B3 E4 B3 G3 A3'), + D: ftN('E4 B3 G3 B3 A3 E3 A3 C4 B3 E3 G3 E3 B3 G3 E4 E3'), +}; +const FT_END_CHD = { + A: ftChd8(FTC.Em, ['B2','D3','G3'], FTC.G, ['A2','E3','A3'], + FTC.Am, ['E3','A3','C4'], FTC.G, FTC.Em), + B: ftChd8(FTC.Em, FTC.Em, FTC.G, FTC.Em, FTC.Am, FTC.Am, FTC.Am, FTC.G), + C: ftChd8(FTC.G, FTC.G, FTC.C, FTC.C, FTC.G, FTC.Em, FTC.Em, FTC.Am), + D: ftChd8(FTC.Em, FTC.Em, FTC.Am, FTC.Am, FTC.Em, FTC.Em, FTC.G, FTC.Em), +}; +const FT_END_ORDER = ['A','B','A','C','A','B','D','C','A','B', + 'C','D','A','C','B','D','A','B','D','A']; +const FT_END_MELODY = FT_END_ORDER.flatMap(k => FT_END_MEL[k]); +const FT_END_BASS = FT_END_ORDER.flatMap(k => FT_END_BAS[k]); +const FT_END_CHORDS = FT_END_ORDER.flatMap(k => FT_END_CHD[k]); function arrowLabel(dir) { const ARROWS = { left: '←', right: '→', straight: '↑' }; diff --git a/games/letter-find/game.js b/games/letter-find/game.js @@ -2,18 +2,40 @@ const COLORS = ['#E63946', '#1D7CF2', '#FFD23F', '#2EC4B6']; const CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; const GOAL = 10; -const MELODY = [ - 'C5','E5','G5','A5','G5','E5','C5','E5', - 'F5','A5','C6','A5','G5','B4','C5','G4', -]; -const HARMONY = [ - 'E5','G5','B5','C6','B5','G5','E5','G5', - 'A5','C6','E6','C6','B5','D5','E5','B4', -]; -const BASS = [ - 'C3',null,null,null,null,null,null,null, - 'F2',null,null,null,'G2',null,null,null, -]; +// ~1-minute song: 16-step phrases (eighth notes at 128 BPM) concatenated so the +// loop is long enough not to grate. 'A' is the original theme; B–F are new +// sections in the same C-major character. '.' = rest. Harmony is a diatonic +// third above the melody (it plays during the success jingle). +const N = s => s.trim().split(/\s+/).map(t => (t === '.' ? null : t)); + +const MEL = { + A: N('C5 E5 G5 A5 G5 E5 C5 E5 F5 A5 C6 A5 G5 B4 C5 G4'), + B: N('E5 D5 C5 D5 E5 G5 E5 C5 D5 F5 A5 F5 E5 D5 C5 G4'), + C: N('G5 A5 B5 C6 B5 A5 G5 E5 A5 G5 F5 E5 D5 E5 F5 D5'), + D: N('C5 D5 E5 F5 E5 D5 C5 A4 B4 C5 D5 E5 D5 C5 B4 G4'), + E: N('C6 B5 A5 G5 A5 B5 C6 E6 D6 C6 B5 A5 G5 F5 E5 D5'), + F: N('E5 G5 C6 G5 E5 C5 D5 E5 F5 E5 D5 C5 D5 B4 C5 C5'), +}; +const HAR = { + A: N('E5 G5 B5 C6 B5 G5 E5 G5 A5 C6 E6 C6 B5 D5 E5 B4'), + B: N('G5 F5 E5 F5 G5 B5 G5 E5 F5 A5 C6 A5 G5 F5 E5 B4'), + C: N('B5 C6 D6 E6 D6 C6 B5 G5 C6 B5 A5 G5 F5 G5 A5 F5'), + D: N('E5 F5 G5 A5 G5 F5 E5 C5 D5 E5 F5 G5 F5 E5 D5 B4'), + E: N('E6 D6 C6 B5 C6 D6 E6 G6 F6 E6 D6 C6 B5 A5 G5 F5'), + F: N('G5 B5 E6 B5 G5 E5 F5 G5 A5 G5 F5 E5 F5 D5 E5 E5'), +}; +const BAS = { + A: N('C3 . . . . . . . F2 . . . G2 . . .'), + B: N('C3 . . . . . . . D3 . . . G2 . . .'), + C: N('G2 . . . . . . . F2 . . . G2 . . .'), + D: N('C3 . . . A2 . . . G2 . . . G2 . . .'), + E: N('C3 . . . F2 . . . G2 . . . G2 . . .'), + F: N('C3 . . . F2 . . . G2 . C3 . C3 . . .'), +}; +const ORDER = ['A','B','A','C','D','B','E','F','A','C','D','B','E','C','F','A']; +const MELODY = ORDER.flatMap(k => MEL[k]); +const HARMONY = ORDER.flatMap(k => HAR[k]); +const BASS = ORDER.flatMap(k => BAS[k]); class LetterFindScene extends Phaser.Scene { constructor() {