diff --git a/README.md b/README.md index 89effec..ac75c93 100644 --- a/README.md +++ b/README.md @@ -9,16 +9,16 @@ machine-language payload is required. | Program | Type | Controls | Description | Run locally | | --- | --- | --- | --- | --- | -| [Meteor Dodge](meteor-dodge/meteor-dodge.bas) | Arcade | `A`, `D`, `Q` | Dodge falling meteors and build your score. | [Run program](http://localhost:8134/?program=meteor-dodge%2Fmeteor-dodge.prg) | -| [Code Breaker](code-breaker/code-breaker.bas) | Logic | `0`-`9`, `Q` | Deduce a four-digit secret in ten guesses. | [Run program](http://localhost:8134/?program=code-breaker%2Fcode-breaker.prg) | -| [Submarine Hunt](submarine-hunt/submarine-hunt.bas) | Strategy | Row, Column, `Q` | Find three hidden submarines with limited sonar shots. | [Run program](http://localhost:8134/?program=submarine-hunt%2Fsubmarine-hunt.prg) | -| [Lunar Lander](lunar-lander/lunar-lander.bas) | Simulation | `0`-`9`, `Q` | Choose engine burns and touch down gently. | [Run program](http://localhost:8134/?program=lunar-lander%2Flunar-lander.prg) | -| [Memory Matrix](memory-matrix/memory-matrix.bas) | Memory | `1`, `2`, `3`, `4`, `Q` | Repeat a growing sequence of flashing quadrants. | [Run program](http://localhost:8134/?program=memory-matrix%2Fmemory-matrix.prg) | -| [Robot Maze](robot-maze/robot-maze.bas) | Puzzle | `W`, `A`, `S`, `D`, `Q` | Navigate a freshly generated maze to its exit. | [Run program](http://localhost:8134/?program=robot-maze%2Frobot-maze.prg) | -| [Music Box](music-box/music-box.bas) | Music | `A`-`K`, `1`, `2`, `3`, `Q` | Play the SID chip as a small computer keyboard. | [Run program](http://localhost:8134/?program=music-box%2Fmusic-box.prg) | -| [Pixel Painter](pixel-painter/pixel-painter.bas) | Creative | Cursor keys, Space, `C`, `Q` | Draw character-cell pictures on the screen. | [Run program](http://localhost:8134/?program=pixel-painter%2Fpixel-painter.prg) | -| [Stock Trader](stock-trader/stock-trader.bas) | Simulation | `B`, `S`, `W`, `Q` | Trade a volatile stock over twenty market days. | [Run program](http://localhost:8134/?program=stock-trader%2Fstock-trader.prg) | -| [Starfield](starfield/starfield.bas) | Demo | `1`, `2`, `3`, `Q` | Watch and control a layered animated starfield. | [Run program](http://localhost:8134/?program=starfield%2Fstarfield.prg) | +| [Meteor Dodge](meteor-dodge/meteor-dodge.bas) | Arcade | `A`, `D`, `Q` | Dodge falling meteors and build your score. | [Run program](http://localhost:8134/program/meteor-dodge) | +| [Code Breaker](code-breaker/code-breaker.bas) | Logic | `0`-`9`, `Q` | Deduce a four-digit secret in ten guesses. | [Run program](http://localhost:8134/program/code-breaker) | +| [Submarine Hunt](submarine-hunt/submarine-hunt.bas) | Strategy | Row, Column, `Q` | Find three hidden submarines with limited sonar shots. | [Run program](http://localhost:8134/program/submarine-hunt) | +| [Lunar Lander](lunar-lander/lunar-lander.bas) | Simulation | `0`-`9`, `Q` | Choose engine burns and touch down gently. | [Run program](http://localhost:8134/program/lunar-lander) | +| [Memory Matrix](memory-matrix/memory-matrix.bas) | Memory | `1`, `2`, `3`, `4`, `Q` | Repeat a growing sequence of flashing quadrants. | [Run program](http://localhost:8134/program/memory-matrix) | +| [Robot Maze](robot-maze/robot-maze.bas) | Puzzle | `W`, `A`, `S`, `D`, `Q` | Navigate a freshly generated maze to its exit. | [Run program](http://localhost:8134/program/robot-maze) | +| [Music Box](music-box/music-box.bas) | Music | `A`-`K`, `1`, `2`, `3`, `Q` | Play the SID chip as a small computer keyboard. | [Run program](http://localhost:8134/program/music-box) | +| [Pixel Painter](pixel-painter/pixel-painter.bas) | Creative | Cursor keys, Space, `C`, `Q` | Draw character-cell pictures on the screen. | [Run program](http://localhost:8134/program/pixel-painter) | +| [Stock Trader](stock-trader/stock-trader.bas) | Simulation | `B`, `S`, `W`, `Q` | Trade a volatile stock over twenty market days. | [Run program](http://localhost:8134/program/stock-trader) | +| [Starfield](starfield/starfield.bas) | Demo | `1`, `2`, `3`, `Q` | Watch and control a layered animated starfield. | [Run program](http://localhost:8134/program/starfield) | ## Running the programs diff --git a/public/app.js b/public/app.js index e55ebe7..e2c67c0 100644 --- a/public/app.js +++ b/public/app.js @@ -46,7 +46,7 @@ function renderPrograms() { for (const program of programs) { const card = document.createElement('article'); card.className = 'program-card'; - const programUrl = `/?program=${encodeURIComponent(program.path)}`; + const programUrl = `/program/${encodeURIComponent(program.slug)}`; card.addEventListener('click', event => { if (!event.target.closest('a, button')) location.href = programUrl; }); @@ -134,10 +134,11 @@ async function loadPrograms() { } } -function startEmulator(programPath) { +function startEmulator(program) { + const programPath = program.path; library.hidden = true; player.hidden = false; - const title = titleFromPath(programPath); + const title = program.title || titleFromPath(programPath); document.title = `${title} — C64 BASIC Collection`; document.querySelector('#program-title').textContent = title; @@ -164,6 +165,23 @@ function startEmulator(programPath) { document.body.append(loader); } +async function loadProgramPage(slug) { + library.hidden = true; + player.hidden = false; + try { + const response = await fetch('/api/programs', { cache: 'no-store' }); + if (!response.ok) throw new Error('The program list could not be loaded.'); + const { programs } = await response.json(); + const program = programs.find(entry => entry.slug === slug); + if (!program) throw new Error('This program does not exist.'); + startEmulator(program); + } catch (error) { + const message = document.querySelector('#emulator-error'); + message.hidden = false; + message.textContent = error.message; + } +} + refreshButton.addEventListener('click', loadPrograms); searchInput.addEventListener('input', renderPrograms); typeFilter.addEventListener('change', renderPrograms); @@ -186,7 +204,11 @@ function setView(view) { } setView(localStorage.getItem('program-view') === 'list' ? 'list' : 'grid'); -const programPath = new URLSearchParams(location.search).get('program'); -if (programPath) startEmulator(programPath); +const legacyProgramPath = new URLSearchParams(location.search).get('program'); +const programPage = location.pathname.match(/^\/program\/([a-z0-9]+(?:-[a-z0-9]+)*)\/?$/); +if (legacyProgramPath) { + const slug = legacyProgramPath.split('/')[0]; + location.replace(`/program/${encodeURIComponent(slug)}`); +} else if (programPage) loadProgramPage(decodeURIComponent(programPage[1])); else loadPrograms(); diff --git a/server.mjs b/server.mjs index 53bd8c4..d3d858c 100644 --- a/server.mjs +++ b/server.mjs @@ -31,6 +31,7 @@ function fetchPrograms() { const sourceFilePath = resolve(programDirectory, sourcePath); return { ...program, + slug: program.path.split('/')[0], source: sourceFilePath.startsWith(programDirectory + sep) && existsSync(sourceFilePath) ? sourcePath : null, size: statSync(filePath).size }; @@ -72,7 +73,8 @@ function sendSource(response, programPath, headOnly = false) { } function serveStatic(response, pathname, headOnly = false) { - const requested = pathname === '/' ? 'index.html' : pathname.slice(1); + const isProgramPage = /^\/program\/[a-z0-9]+(?:-[a-z0-9]+)*\/?$/.test(pathname); + const requested = pathname === '/' || isProgramPage ? 'index.html' : pathname.slice(1); const filePath = normalize(join(publicDirectory, requested)); if (!filePath.startsWith(publicDirectory) || !existsSync(filePath) || !statSync(filePath).isFile()) { return false;