201 lines
18 KiB
HTML
201 lines
18 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Ludic — a compiled game language</title>
|
|
<meta name="description" content="Ludic compiles ahead of time to a native binary. The entity-component system is part of the syntax, the runtime is deterministic fixed-point, and the compiler is written in Ludic — no C is generated, compiled or linked in a build.">
|
|
<meta property="og:title" content="Ludic — a compiled game language">
|
|
<meta property="og:description" content="Ahead-of-time compiled, with an ECS in the syntax and a deterministic fixed-point runtime. The compiler is written in Ludic; no C is generated, compiled or linked in a build.">
|
|
<meta property="og:type" content="website">
|
|
<link rel="stylesheet" href="base.css">
|
|
<link rel="stylesheet" href="site.css">
|
|
</head>
|
|
<body>
|
|
<header class="nav">
|
|
<div class="wrap nav-in">
|
|
<a class="brand" href="#top"><span class="logo">L</span> Ludic</a>
|
|
<button class="nav-toggle" aria-label="Toggle menu" aria-expanded="false">☰</button>
|
|
<nav class="nav-links"><a href="#features">Features</a><a href="#showcase">Examples</a><a href="#start">Get started</a><a class="nav-cta" href="api.html">API Reference</a>
|
|
<a class="nav-cta" href="https://git.workshopsoft.io/workshopsoft/ludic">Source ↗</a>
|
|
</nav>
|
|
</div>
|
|
</header>
|
|
|
|
<a id="top"></a>
|
|
<section class="hero">
|
|
<div class="wrap hero-grid">
|
|
<div>
|
|
<h1>A compiled language <span class="accent">for 2D games.</span></h1>
|
|
<p class="lead">Ludic compiles ahead of time to a native binary — no engine to install, no interpreter, nothing shipped beside the executable. The entity-component system is <b>part of the syntax</b>, the maths is <b>deterministic fixed-point</b>, and <code>ludicc</code> is itself written in Ludic: <b>no C is generated, compiled or linked in a build</b>.</p>
|
|
<div class="cta-row">
|
|
<a class="btn btn-primary" href="#start">Get started →</a>
|
|
<a class="btn btn-ghost" href="api.html">API Reference</a>
|
|
</div>
|
|
</div>
|
|
<div class="code-card">
|
|
<div class="code-top"><span class="code-name">hello.ludic</span></div>
|
|
<pre data-lang="ludic"># the smallest program that exercises the whole ECS pipeline
|
|
program Hello {
|
|
|
|
property Position { column: int = 0, row: int = 0 }
|
|
property Velocity { delta_x: int = 0, delta_y: int = 0 }
|
|
|
|
handler SpawnEnemies phase Start {
|
|
spawn Enemy { Position { column: 3, row: 4 }, Velocity { delta_x: 1, delta_y: 0 } }
|
|
spawn Enemy { Position { column: 10, row: 2 }, Velocity { delta_x: 0, delta_y: 1 } }
|
|
}
|
|
|
|
# a handler declares the entities it touches; the body
|
|
# runs once per match, each property bound by name.
|
|
@Queries(these: [Position, Velocity])
|
|
handler AdvancePositions phase FixedUpdate {
|
|
Position.column += Velocity.delta_x
|
|
Position.row = Position.row + Velocity.delta_y
|
|
}
|
|
|
|
handler ReportPositions phase Update {
|
|
for (moving) in query [Position] {
|
|
print(moving.column)
|
|
print(moving.row)
|
|
}
|
|
quit()
|
|
}
|
|
}</pre>
|
|
</div>
|
|
</div>
|
|
<div class="wrap">
|
|
<div class="pipeline"><span class="stage">.ludic</span><span class="arrow">→</span><span class="stage hl">ludicc</span><span class="arrow">→</span><span class="stage">LLVM IR</span><span class="arrow">→</span><span class="stage">native binary</span></div>
|
|
<p class="pipe-note"><code>ludicc</code> lowers straight to LLVM IR and links a native binary. There is no C step in between and no runtime to ship alongside — the game is the executable.</p>
|
|
</div>
|
|
</section>
|
|
|
|
<section id="features">
|
|
<div class="wrap">
|
|
<div class="sec-head">
|
|
<div class="kicker">Why Ludic</div>
|
|
<h2>A language shaped around the game.</h2>
|
|
<p>The things you normally bolt on — an entity-component system, a renderer, a deterministic clock, save/load — are primitives of the language itself. One way to do each, so there is little to decide and nothing to wire up.</p>
|
|
</div>
|
|
<div class="feat-grid"><div class="feat"><h3>ECS in the syntax</h3><p><code>property</code>, <code>model</code>, and <code>handler</code> are keywords. Query entities with <code>query [A, B, {Tag}]</code> and iterate matches directly — no framework to wire up.</p></div><div class="feat"><h3>Deterministic runtime</h3><p>Q16.16 <code>fixed</code>-point math and a seeded RNG mean the same inputs produce the same frame — byte-for-byte — every run. Ideal for replays and lockstep netcode.</p></div><div class="feat"><h3>Drawing & named colors</h3><p>The <code>Screen</code> API draws rectangles, text and pixels with named arguments; <code>Color.Crimson</code> and 220 more names read like English and cost nothing at runtime.</p></div><div class="feat"><h3>One self-contained binary</h3><p>A game compiles to a single native executable — no engine to install, no interpreter, no runtime shipped beside it. Build, and run the file.</p></div><div class="feat"><h3>Snapshot save/load</h3><p><code>save()</code> and <code>load()</code> serialize the entire ECS world — every entity, property and program <code>var</code> — in one call.</p></div><div class="feat"><h3>Scenes & state machines</h3><p><code>scene</code>/<code>layer</code>/<code>become</code> model mutually-exclusive game states with enter/exit hooks; <code>match</code>/<code>machine</code>/<code>state</code> handle dispatch and per-entity FSMs.</p></div><div class="feat"><h3>Retained UI & assets</h3><p>Declare a widget tree as data with <code>ui</code> — panels, labels, buttons, 9-slice skins, keyboard focus. Sprites decode from PNG at runtime; text is real TrueType.</p></div><div class="feat"><h3>Modules & native calls</h3><p><code>module</code> + <code>@export fn</code> builds a shared library of plain native symbols; <code>extern fn … = "symbol"</code> reaches out to any native library when you need the platform.</p></div></div>
|
|
</div>
|
|
</section>
|
|
|
|
<section id="showcase">
|
|
<div class="wrap">
|
|
<div class="sec-head">
|
|
<div class="kicker">Show, don't tell</div>
|
|
<h2>Real programs, one toolchain.</h2>
|
|
<p>The same <code>ludicc</code> that builds a JRPG builds a from-scratch Snake and a scene demo. Nothing is hardcoded to a genre — and every token below links into the reference.</p>
|
|
</div>
|
|
<div class="tabs" id="tabs" role="tablist"></div>
|
|
<div id="panels"></div>
|
|
<div class="showcase-note"><span class="b">↳</span><span id="note"></span></div>
|
|
</div>
|
|
</section>
|
|
|
|
<section id="philosophy">
|
|
<div class="wrap">
|
|
<div class="banner">
|
|
<div>
|
|
<div class="kicker">The philosophy</div>
|
|
<h2>Batteries in the language, not in a framework.</h2>
|
|
<p>Most game code is plumbing — registering systems, wiring a renderer, threading state through a framework. Ludic makes those decisions once and bakes them into the language, so the code you write is the game's actual logic.</p><p>That extends to the toolchain. <code>ludicc</code> is written in Ludic and recompiles its own source to a byte-identical binary, rebuilding from a checked-in IR seed with clang and nothing else.</p>
|
|
</div>
|
|
<div class="stat-row"><div class="stat"><div class="big">0</div><div class="lbl">lines of C in a build</div></div><div class="stat"><div class="big">1</div><div class="lbl">backend: LLVM IR</div></div><div class="stat"><div class="big">=</div><div class="lbl">byte-exact self-rebuild</div></div></div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section id="start">
|
|
<div class="wrap">
|
|
<div class="sec-head">
|
|
<div class="kicker">Get started</div>
|
|
<h2>One command to install. One to play.</h2>
|
|
<p>Install the toolchain with a single command — it brings the compiler, the <code>ludic</code> CLI, the engine runtime and the editor tooling, and needs nothing else on your machine but a C toolchain to link with. Then create a project and run it.</p>
|
|
</div>
|
|
<div class="steps">
|
|
<div><div class="step"><div class="n">1</div><div><h4>Install</h4><p>The installer downloads a verified toolchain for your platform into <code>~/.ludic</code> and puts it on your <code>PATH</code>. Nothing else is touched; uninstalling is <code>rm -rf ~/.ludic</code>. On a platform with no prebuilt toolchain it bootstraps from the compiler's own IR seed instead — same result, a few seconds longer.</p></div></div><div class="step"><div class="n">2</div><div><h4>Create a project</h4><p><code>ludic new mygame</code> writes a manifest, a program that already moves something on screen, a test, and an <code>assets/</code> directory. There is no scaffolding to choose and no build file to write.</p></div></div><div class="step"><div class="n">3</div><div><h4>Run it</h4><p><code>ludic run</code> compiles <code>src/main.ludic</code> to a native binary and launches it. <code>ludic build</code> stops at the binary — one self-contained executable, with nothing to ship beside it.</p></div></div><div class="step"><div class="n">4</div><div><h4>Test it, headlessly</h4><p><code>ludic test</code> compiles and runs every <code>test</code> block in the project. <code>--headless</code> renders frames to a <code>.ppm</code> from piped input, so a game is deterministic enough to diff in CI.</p></div></div></div>
|
|
<div class="term">
|
|
<div class="code-top"><span class="code-name">zsh — ludic</span></div>
|
|
<pre><span class="t-com"># install the toolchain (macOS, Linux)</span>
|
|
<span class="prompt">$</span> curl -fsSL https://workshopsoft.pages.workshopsoft.io/ludic/install.sh | sh
|
|
<span class="out">→ installed ludic 0.4.0 → ~/.ludic</span>
|
|
|
|
<span class="t-com"># a project that builds and plays as it stands</span>
|
|
<span class="prompt">$</span> ludic new mygame
|
|
<span class="prompt">$</span> cd mygame
|
|
<span class="prompt">$</span> ludic run
|
|
<span class="out">→ a native window, running your game</span>
|
|
|
|
<span class="t-com"># tests, and a deterministic headless render for CI</span>
|
|
<span class="prompt">$</span> ludic test
|
|
<span class="prompt">$</span> ludic build --headless
|
|
<span class="prompt">$</span> printf 'ddddwww' | ./build/mygame_headless
|
|
<span class="out">→ writes build/out.ppm</span>
|
|
</pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section id="editors">
|
|
<div class="wrap">
|
|
<div class="sec-head">
|
|
<div class="kicker">Editor experience</div>
|
|
<h2>One language server, every editor.</h2>
|
|
<p><code>ludic-lsp</code> speaks LSP 3.17 over stdio: context-aware completion, diagnostics from the compiler itself, go-to-definition and rename across <code>import</code>ed files, and comment-preserving formatting. It even understands <code>```ludic</code> fences in Markdown.</p>
|
|
</div>
|
|
<div class="editors"><div class="ed"><span class="k">◆</span> VS Code</div><div class="ed"><span class="k">◆</span> JetBrains IDEs</div><div class="ed"><span class="k">◆</span> Neovim</div><div class="ed"><span class="k">◆</span> Helix</div><div class="ed"><span class="k">◆</span> Emacs</div><div class="ed"><span class="k">◆</span> Sublime Text</div><div class="ed"><span class="k">◆</span> Zed</div></div>
|
|
<p class="ed-note">Editors spawn <code>ludic lsp</code> — it ships with the toolchain, so there is nothing extra to build or install. The same formatter runs as <code>ludic fmt</code> for pre-commit hooks and CI.</p>
|
|
</div>
|
|
</section>
|
|
|
|
<footer>
|
|
<div class="wrap foot-in">
|
|
<div>
|
|
<div class="brand" style="margin-bottom:6px"><span class="logo">L</span> Ludic</div>
|
|
<div class="muted">A compiled language for 2D games, with the entity system in the syntax.</div>
|
|
</div>
|
|
<nav class="foot-links"><a href="#features">Features</a><a href="#showcase">Examples</a><a href="#start">Get started</a><a href="api.html">API Reference</a><a href="https://git.workshopsoft.io/workshopsoft/ludic">Source ↗</a></nav>
|
|
</div>
|
|
</footer>
|
|
|
|
<script src="ludic-highlight.js"></script>
|
|
<script>
|
|
Ludic.highlightAll();
|
|
const SAMPLES = [{"name": "snake.ludic", "label": "snake", "note": "A complete Snake \u2014 grid, growth, food, game-over \u2014 from primitives. State is named vars, colors are named, and every draw call says what each argument is.", "code": "program Snake {\n property Position { column: int = 0, row: int = 0 }\n property Segment { index: int = 0 }\n\n const GRID_WIDTH: int = 20 const GRID_HEIGHT: int = 15 const TILE_SIZE: int = 16\n\n var food_column: int = 14 var food_row: int = 7 var score: int = 0\n\n handler DrawWorld phase Render {\n Screen.clear(Color.MidnightBlue)\n # checkerboard field \u2014 a fresh, immutable column/row each pass\n for row in 0 .. GRID_HEIGHT {\n for column in 0 .. GRID_WIDTH {\n var tile_color = Color.Charcoal\n if (column + row) % 2 == 0 { tile_color = Color.Gunmetal }\n Screen.fill_rectangle(x: column * TILE_SIZE, y: row * TILE_SIZE, width: TILE_SIZE, height: TILE_SIZE, color: tile_color)\n }\n }\n # the food, then every snake segment straight from the ECS\n Screen.fill_rectangle(x: food_column * TILE_SIZE + 3, y: food_row * TILE_SIZE + 3, width: TILE_SIZE - 6, height: TILE_SIZE - 6, color: Color.Crimson)\n for (position, segment) in query [Position, Segment] {\n var segment_color = Color.LimeGreen\n if segment.index == 0 { segment_color = Color.MintGreen } # brighter head\n Screen.fill_rectangle(x: position.column * TILE_SIZE + 1, y: position.row * TILE_SIZE + 1, width: TILE_SIZE - 2, height: TILE_SIZE - 2, color: segment_color)\n }\n Screen.draw_text(x: 6, y: 4, text: \"SCORE\", color: Color.White, scale: 1)\n Screen.draw_number(x: 52, y: 4, value: score, color: Color.Gold, scale: 1)\n Screen.show()\n }\n}"}, {"name": "scenes.ludic", "label": "scenes", "note": "One active scene at a time. `become` runs the old scene's on-exit and the new one's on-enter; layers draw in declaration order. State is a plain named var.", "code": "program SceneDemo {\n var counter: int = 0\n\n handler Boot phase Start {\n counter = 0\n print(1000)\n }\n\n scene Title start {\n on enter { print(1) }\n on exit { print(2) }\n layer Main {\n handler Tick phase Update {\n counter += 1\n print(100 + counter)\n if counter >= 2 { become Play } # hand off to Play\n }\n }\n }\n\n scene Play {\n on enter {\n print(3)\n counter = 0\n }\n on exit { print(4) }\n layer World {\n handler Step phase Update {\n counter += 1\n print(200 + counter)\n if counter >= 2 { quit() }\n }\n }\n layer Hud {\n handler DrawHud phase Render { print(900) }\n }\n }\n}"}, {"name": "toggle.ludic", "label": "lifecycle", "note": "Enable/disable at three scopes \u2014 entity, model, handler. Disabling never destroys data: a property's values persist, so a later enable restores them.", "code": "program Toggles {\n property Health { current: int = 0, maximum: int = 100 }\n property Shield { amount: int = 0 }\n model Player { Health, Shield }\n\n # hooks fire at the toggle point, data bound by name\n @OnDisable(Shield) handler ShieldDown { print(Shield.amount + 1) }\n @OnEnable(Shield) handler ShieldUp { print(Shield.amount + 2) }\n\n handler SpawnPlayer phase Start {\n spawn Player { Health { maximum: 50 }, Shield { amount: 5 } }\n }\n\n handler ToggleShield phase Render {\n for (player) in query [Player] { disable Shield on self() } # @OnDisable\n for (player) in query [Player] { enable Shield on self() } # @OnEnable, data intact\n disable Player # whole model off\n quit()\n }\n}"}, {"name": "hello.ludic", "label": "hello", "note": "The smallest program that exercises the whole pipeline: properties, a spawn, a queried handler, and a render/quit.", "code": "# the smallest program that exercises the whole ECS pipeline\nprogram Hello {\n\n property Position { column: int = 0, row: int = 0 }\n property Velocity { delta_x: int = 0, delta_y: int = 0 }\n\n handler SpawnEnemies phase Start {\n spawn Enemy { Position { column: 3, row: 4 }, Velocity { delta_x: 1, delta_y: 0 } }\n spawn Enemy { Position { column: 10, row: 2 }, Velocity { delta_x: 0, delta_y: 1 } }\n }\n\n # a handler declares the entities it touches; the body\n # runs once per match, each property bound by name.\n @Queries(these: [Position, Velocity])\n handler AdvancePositions phase FixedUpdate {\n Position.column += Velocity.delta_x\n Position.row = Position.row + Velocity.delta_y\n }\n\n handler ReportPositions phase Update {\n for (moving) in query [Position] {\n print(moving.column)\n print(moving.row)\n }\n quit()\n }\n}"}];
|
|
const tabsEl = document.getElementById("tabs");
|
|
const panelsEl = document.getElementById("panels");
|
|
const noteEl = document.getElementById("note");
|
|
SAMPLES.forEach((s, idx) => {
|
|
const t = document.createElement("button");
|
|
t.className = "tab" + (idx === 0 ? " active" : "");
|
|
t.type = "button";
|
|
t.setAttribute("role", "tab");
|
|
t.setAttribute("aria-selected", idx === 0 ? "true" : "false");
|
|
t.textContent = s.label;
|
|
t.onclick = () => select(idx);
|
|
tabsEl.appendChild(t);
|
|
const card = document.createElement("div");
|
|
card.className = "code-card panel-code" + (idx === 0 ? " active" : "");
|
|
card.innerHTML =
|
|
'<div class="code-top"><span class="code-name"></span></div><pre></pre>';
|
|
card.querySelector(".code-name").textContent = s.name;
|
|
card.querySelector("pre").innerHTML = Ludic.highlight(s.code);
|
|
panelsEl.appendChild(card);
|
|
});
|
|
function select(idx) {
|
|
[...tabsEl.children].forEach((t, i) => {
|
|
const on = i === idx;
|
|
t.classList.toggle("active", on);
|
|
t.setAttribute("aria-selected", on ? "true" : "false");
|
|
});
|
|
[...panelsEl.children].forEach((p, i) => p.classList.toggle("active", i === idx));
|
|
noteEl.textContent = SAMPLES[idx].note;
|
|
}
|
|
noteEl.textContent = SAMPLES[0].note;
|
|
Ludic.installCards();
|
|
</script>
|
|
</body>
|
|
</html>
|