A compiled language for 2D games.

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 part of the syntax, the maths is deterministic fixed-point, and ludicc is itself written in Ludic: no C is generated, compiled or linked in a build.

hello.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()
  }
}
.ludic→ludicc→LLVM IR→native binary

ludicc 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.

Why Ludic

A language shaped around the game.

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.

ECS in the syntax

property, model, and handler are keywords. Query entities with query [A, B, {Tag}] and iterate matches directly — no framework to wire up.

Deterministic runtime

Q16.16 fixed-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.

Drawing & named colors

The Screen API draws rectangles, text and pixels with named arguments; Color.Crimson and 220 more names read like English and cost nothing at runtime.

One self-contained binary

A game compiles to a single native executable — no engine to install, no interpreter, no runtime shipped beside it. Build, and run the file.

Snapshot save/load

save() and load() serialize the entire ECS world — every entity, property and program var — in one call.

Scenes & state machines

scene/layer/become model mutually-exclusive game states with enter/exit hooks; match/machine/state handle dispatch and per-entity FSMs.

Retained UI & assets

Declare a widget tree as data with ui — panels, labels, buttons, 9-slice skins, keyboard focus. Sprites decode from PNG at runtime; text is real TrueType.

Modules & native calls

module + @export fn builds a shared library of plain native symbols; extern fn … = "symbol" reaches out to any native library when you need the platform.

Show, don't tell

Real programs, one toolchain.

The same ludicc 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.

↳
Get started

One command to install. One to play.

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.

1

Install

The installer downloads a verified toolchain for your platform into ~/.ludic and puts it on your PATH. Nothing else is touched; uninstalling is rm -rf ~/.ludic. On a platform with no prebuilt toolchain it bootstraps from the compiler's own IR seed instead — same result, a few seconds longer.

2

Create a project

ludic new mygame writes a manifest, a program that already moves something on screen, a test, and an assets/ directory. There is no scaffolding to choose and no build file to write.

3

Run it

ludic run compiles src/main.ludic to a native binary and launches it. ludic build stops at the binary — one self-contained executable, with nothing to ship beside it.

4

Test it, headlessly

ludic test compiles and runs every test block in the project. --headless renders frames to a .ppm from piped input, so a game is deterministic enough to diff in CI.

zsh — ludic
# install the toolchain (macOS, Linux)
$ curl -fsSL https://workshopsoft.pages.workshopsoft.io/ludic/install.sh | sh
→ installed ludic 0.4.0 → ~/.ludic

# a project that builds and plays as it stands
$ ludic new mygame
$ cd mygame
$ ludic run
→ a native window, running your game

# tests, and a deterministic headless render for CI
$ ludic test
$ ludic build --headless
$ printf 'ddddwww' | ./build/mygame_headless
→ writes build/out.ppm
Editor experience

One language server, every editor.

ludic-lsp speaks LSP 3.17 over stdio: context-aware completion, diagnostics from the compiler itself, go-to-definition and rename across imported files, and comment-preserving formatting. It even understands ```ludic fences in Markdown.

◆ VS Code
◆ JetBrains IDEs
◆ Neovim
◆ Helix
◆ Emacs
◆ Sublime Text
◆ Zed

Editors spawn ludic lsp — it ships with the toolchain, so there is nothing extra to build or install. The same formatter runs as ludic fmt for pre-commit hooks and CI.