cae

package module
v0.4.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 3, 2026 License: CC0-1.0 Imports: 6 Imported by: 0

README

CAE-core

The simulation core of CellEngine, extracted as a standalone library: cellular automata on a grid, with rules you can write in Go or assemble as data.

Zero dependencies. The whole engine builds on the Go standard library — no graphics stack, no cgo, no transitive tree to audit.

Русская версия · Reference · Changelog · Examples · Shared library · WebAssembly

import cae "github.com/TrewCar/CAE-core"

sim := cae.Life(64, 48)
for _, p := range [][2]int{{2, 3}, {3, 3}, {4, 3}, {4, 4}, {3, 5}} {
    sim.SetKind(p[0], p[1], 1)   // a glider
}
sim.StepN(4)                     // it has moved one cell diagonally

Install

go get github.com/TrewCar/CAE-core

Requires Go 1.23 or newer.

What's in it

Package What it does
cae Facade: one Sim that holds the world, grid and rule together. Start here.
automaton The grid and the step engine. Cell, Grid, Boundary, Rule, Engine.
world What the user edits: cell kinds, named constants, world variables, property slots.
script A small imperative language for cell behaviour — expressions, if, bounded loops, methods with parameters, deferred calls. Plus ready-made programs (Langton's Ant, elementary CA, sand, explosions).
visualrule Rules as data: an ordered list of IF <conditions> THEN <commands> branches, first match wins.
goal Declarative conditions on grid state, for "run until it settles" and for assertions in tests.

Nothing above imports anything below it in a cycle, and none of them imports a renderer, a file format or the filesystem. The engine tells you what colour a cell is (Sim.ColorAt) and what a rule is; what you do with either is yours.

Three ways to write a rule

In Go, by implementing automaton.Rule — the fastest and least flexible:

type Life struct{ alive automaton.Kind }

func (r Life) Name() string { return "Life" }
func (r Life) Color(k automaton.Kind) color.Color { /* ... */ }

func (r Life) Next(env automaton.Env) automaton.Cell {
    n := env.CountMoore(func(c automaton.Cell) bool { return c.Kind == r.alive })
    if n == 3 || (n == 2 && env.Self().Kind == r.alive) {
        return automaton.Cell{Kind: r.alive}
    }
    return automaton.Cell{}
}

sim := cae.NewRule(w, Life{alive: 1}, automaton.NewGrid(64, 48, automaton.BoundaryWrap))

As a program, in the engine's own language — this is what the editor builds when a user drags nodes around, and it serialises to JSON:

w := world.New()
prog := script.DefaultProgram(w)        // Conway's Life, as data
sim := cae.NewScript(w, prog, automaton.NewGrid(64, 48, automaton.BoundaryWrap))

As branches, for rules that are just condition/command pairs:

w := world.New()
sim := cae.NewVisualRule(w, visualrule.DefaultRule(w), grid)

The last two are ordinary Go values, so you can build them, save them, ship them and load them back. The first cannot be serialised — it's code.

Rules are data

There is no file format here, and not one function that touches a disk. The world, the program, the visual rule and the goal are ordinary structs with named fields — encoding/json serialises them with no help from the engine:

data, err := json.Marshal(sim.Program())

What to wrap them in, where to put it, how to version it and what to compress it with is the application's decision, and the engine has no business taking part in it. Whatever it decided would have to ship in every build, WASM included.

Three things are deliberately not recomputed for you on the way back in, because encoding/json cannot know about them:

world.RecomputeCounters()   // next-ID counters are unexported and don't survive
prog.RecomputeSeq()         // the method-name index
rule.World = world          // json:"-" back-reference; cae.NewVisualRule does this

The one thing you cannot hand straight to json.Marshal is the grid: its cells live in a private slice so nobody can route around the boundary rules and quietly corrupt the state. They travel through Grid.Cells() and Grid.LoadCells().

See examples/rulejson for the whole round trip in about forty lines.

Determinism

Given the same starting grid, the same world and the same rule, a run is reproducible bit for bit — on any machine, at any GOMAXPROCS. This is a deliberate property, not an accident:

  • There is no random node in the language. The falling-sand rule that used to flip a coin picks the left diagonal instead.
  • Parallelism does not change results. Classic rules are stepped across several goroutines, but each worker reads only the frozen previous grid and writes only its own rows.
  • NaN and infinity cannot leak in. Division by zero yields 0, and the square root of a negative yields 0, rather than poisoning every later comparison.

Three things you control, and must control if you want an exact round trip:

  1. The tick budget. A program that overruns leaves the grid half-updated. Check sim.Truncated() — StepN already stops early when it trips.
  2. FPS, CellSize, Mouse. A program can read these through its SYSTEM node. Headless they are zero, which is well defined; just keep them the same across runs.
  3. World variables are state, not config. SET VAR writes into World.Variables, so re-running with a used world is not the same as a fresh run. Rebuild the world, or decode a fresh one.

Beyond Go

The engine is a Go module first, but it does not have to be consumed from Go:

  • Shared library — go build -buildmode=c-shared produces a .dll / .so / .dylib with a flat C ABI, drivable from C, C++, C#, Python, Rust. The Go module itself stays cgo-free; only that package needs a C toolchain.
  • WebAssembly — GOOS=js GOARCH=wasm gives a CAE global in the browser, with a pixels() call that fills an ImageData buffer directly.

Examples

go run ./examples/life          # Conway's Life in the terminal
go run ./examples/visual        # PNG and animated GIF
go run ./examples/crypt         # a stream cipher whose keystream is an automaton
go run ./examples/rulejson      # serialise a rule with encoding/json
go run ./examples/goalrun       # headless run with pass/fail checks

See examples/README.md for what each one demonstrates.

Versioning

cae.Version is the version of the engine — what simulates. It is deliberately not the version of the application embedding it, nor of the file format that application saves projects in. Three things that change for three different reasons; one number for all three would misreport each of them.

The C and JavaScript wrappers carry a second number of their own (cae_abi_version(), CAE.abi): the engine can move forward without touching a single signature there, and a wrapper can change under the same engine.

Numbering starts at 0.4.0 rather than zero because the engine is not new — it reached 0.3.x while it still lived inside CellEngine, and this picks up where it left off. See CHANGELOG.md.

Documentation

docs/ has the full reference — the world, every field of a Visual Rule condition and command, every node and statement of Script, and how goals work — plus walk-throughs of three programs written in the language itself: a scannable QR code, a rotating cube with perspective and hidden-face culling, and a radar sweep.

Tests

go test ./...

The suite includes a check that the Rule 30 program in examples/crypt reproduces the published generations of that automaton — if the interpreter ever drifts, that test goes red first.

License

See LICENSE.

Documentation

Overview

Package cae — фасад над остальными пакетами движка: одна структура Sim, которая держит вместе мир, сетку и правило и умеет всё то, ради чего движок обычно и подключают — прогнать N тиков, прочитать клетку, узнать её цвет.

Пакеты под ним (automaton, world, script, visualrule, goal) остаются полностью самостоятельными: фасад ничего не прячет и ничем не владеет монопольно — все его поля публичные, и в любой момент можно спуститься на уровень ниже и работать с ними напрямую. Он нужен ровно затем, чтобы «просто запустить симуляцию» не требовало знать про разницу между Rule и SequentialRule и про то, что для первого Engine.Step подменяет сетку (см. Sim.Step).

Формата файла проекта здесь нет сознательно. Правило, мир и цель — это обычные структуры с именованными полями, которые сериализуются стандартным encoding/json без чьей-либо помощи; во что их завернуть, куда положить, как версионировать и чем сжать — решает приложение, и движку в этом решении участвовать незачем. Единственное, что ему пришлось предусмотреть, — шов для сетки: её клетки лежат приватно, и наружу они ходят через Grid.Cells/LoadCells (см. examples/rulejson).

Ноль внешних зависимостей: весь движок собирается на одной стандартной библиотеке.

Index

Constants

View Source
const Version = "0.4.1"

Version — версия движка: того, что считает симуляцию.

Она сознательно НЕ совпадает ни с версией приложения, которое движок подключает, ни с версией формата файла, в котором это приложение сохраняет проекты. Три разные вещи меняются по трём разным поводам, и одно число на всех врало бы про каждую из них:

версия движка      меняется, когда меняется поведение симуляции —
                   новый узел языка, другая семантика тика, новый
                   контракт правила. Это она;

версия приложения  меняется, когда меняется то, что видит человек —
                   экраны, редактор, уровни. Живёт в приложении, и
                   движок про неё ничего не знает;

версия формата     меняется, когда файл проекта перестаёт читаться
                   старой сборкой. Живёт там же, где сам формат, то
                   есть в приложении: своего формата у движка нет.

Нумерация начинается с 0.4.0, а не с нуля: движок не новый, он просто впервые выехал отдельным модулем — до этого он ехал внутри CellEngine и дошёл там до 0.3.x. Начать заново с 0.1.0 значило бы сделать вид, что всей этой истории не было.

Приложению, которое хочет показать обе версии сразу, ничего изобретать не нужно: cae.Version — вот она, а свою оно и так знает.

Variables

This section is empty.

Functions

This section is empty.

Types

type Sim

type Sim struct {
	World  *world.World
	Grid   *automaton.Grid
	Engine *automaton.Engine

	// Interp — интерпретатор, если симуляция крутит программу (script);
	// nil, если правило визуальное. Через него читается LastTick (уложился
	// ли тик в бюджет) и выставляются Budget/FPS/CellSize/Mouse.
	Interp *script.Interpreter
}

Sim — симуляция целиком: мир (типы клеток, константы, переменные), сетка и правило, которое их связывает.

Поля публичные сознательно: подкрутить константу, добавить тип клетки или дорисовать что-нибудь в сетку между тиками — обычное дело, и заводить на каждое такое действие метод-обёртку не за чем.

ВАЖНО про Grid: у классического правила (visualrule и любой другой automaton.Rule) каждый тик рождает НОВУЮ сетку, поэтому запоминать s.Grid в своей переменной надолго нельзя — после Step это уже прошлый кадр. Сам Sim за этим следит и обновляет своё поле; читать всегда через s.Grid или s.At.

func Life

func Life(w, h int) *Sim

Life — самый короткий путь к работающей симуляции: Conway's Life на сетке w×h с зацикленными краями. Пригодится, чтобы за один вызов проверить, что библиотека вообще подключилась.

func NewRule

func NewRule(w *world.World, rule any, g *automaton.Grid) *Sim

NewRule собирает симуляцию из правила, написанного на Go, — любого типа, реализующего automaton.Rule или automaton.SequentialRule. Мир нужен только для цветов и может быть nil, если правило красит клетки само.

func NewScript

func NewScript(w *world.World, prog *script.Program, g *automaton.Grid) *Sim

NewScript собирает симуляцию из программы на языке script.

func NewVisualRule

func NewVisualRule(w *world.World, rule *visualrule.VisualRule, g *automaton.Grid) *Sim

NewVisualRule собирает симуляцию из визуального правила.

func (*Sim) At

func (s *Sim) At(x, y int) automaton.Cell

At — клетка в (x,y). Координаты за краем сетки не паникуют, а разрешаются политикой границы (см. automaton.Grid.At).

func (*Sim) ColorAt

func (s *Sim) ColorAt(x, y int) color.RGBA

ColorAt — каким цветом клетка (x,y) должна быть нарисована: её собственный цвет, если программа его выставила (SET COLOR), иначе цвет её типа. Ровно то, что нужно, чтобы сложить из сетки картинку, не зная ничего про устройство мира и правила.

func (*Sim) KindAt

func (s *Sim) KindAt(x, y int) int

KindAt — тип клетки в (x,y) как обычный int (тот же ID, что у world.Kind.ID) — чтобы не приводить типы на каждой строчке разбора результата.

func (*Sim) Program

func (s *Sim) Program() *script.Program

Program возвращает программу симуляции, если она собрана из script, и nil иначе. Вместе с VisualRule это шов для сохранения: обе структуры — обычные значения с именованными полями, и приложение сериализует их тем, чем сочтёт нужным, безо всякого участия движка.

func (*Sim) RunUntil

func (s *Sim) RunUntil(g goal.Goal, maxTicks int) (bool, int)

RunUntil крутит симуляцию, пока не выполнится условие g, но не дольше maxTicks тиков. Возвращает, выполнилось ли условие, и сколько тиков на это ушло. Условие проверяется и на стартовом кадре тоже (до первого Step) — если оно уже выполнено, ответ (true, 0).

func (*Sim) Set

func (s *Sim) Set(x, y int, c automaton.Cell)

Set пишет клетку. Координаты за краем сетки игнорируются.

func (*Sim) SetKind

func (s *Sim) SetKind(x, y int, k int)

SetKind — короткая запись для Set(x, y, Cell{Kind: k}).

func (*Sim) Size

func (s *Sim) Size() (w, h int)

Size — размеры сетки.

func (*Sim) Step

func (s *Sim) Step()

Step продвигает симуляцию на один тик и подхватывает новую сетку, если правило классическое (см. комментарий к Sim.Grid).

func (*Sim) StepN

func (s *Sim) StepN(n int) int

StepN прогоняет n тиков. Возвращает, сколько успело пройти: если программа не уложилась в бюджет (см. Truncated), считать дальше бессмысленно — сетка уже в полуобновлённом виде, — и прогон останавливается досрочно.

func (*Sim) Tick

func (s *Sim) Tick() uint64

Tick — сколько тиков уже прошло.

func (*Sim) Truncated

func (s *Sim) Truncated() bool

Truncated — оборвался ли последний тик по исчерпанию бюджета (только для программ; у визуального правила бюджета нет и ответ всегда false). Если true, сетка обновилась не целиком: подробности — в s.Interp.LastTick.

func (*Sim) VisualRule

func (s *Sim) VisualRule() *visualrule.VisualRule

VisualRule возвращает визуальное правило симуляции, если она собрана им, и nil иначе (программа или правило, написанное на Go).

Directories

Path Synopsis
Пакет capi — тонкая обёртка над cae.Sim с плоским C-интерфейсом, чтобы движок можно было собрать в разделяемую библиотеку (.dll / .so / .dylib) и дёргать из C, C++, C#, Python, Rust — откуда угодно, где есть FFI.
Пакет capi — тонкая обёртка над cae.Sim с плоским C-интерфейсом, чтобы движок можно было собрать в разделяемую библиотеку (.dll / .so / .dylib) и дёргать из C, C++, C#, Python, Rust — откуда угодно, где есть FFI.
examples
crypt command
Пример: поточный шифр, гаммой которого работает сам клеточный автомат.
Пример: поточный шифр, гаммой которого работает сам клеточный автомат.
goalrun command
Пример: движок как счётная машина, а не как игрушка на экране.
Пример: движок как счётная машина, а не как игрушка на экране.
internal/gridimg
Package gridimg складывает из сетки картинку — PNG или анимированный GIF.
Package gridimg складывает из сетки картинку — PNG или анимированный GIF.
life command
Пример: Conway's Life прямо в терминале, без единой картинки.
Пример: Conway's Life прямо в терминале, без единой картинки.
rulejson command
Пример: правило — это данные.
Пример: правило — это данные.
visual command
Пример: симуляция в картинках — PNG отдельных кадров и анимированный GIF.
Пример: симуляция в картинках — PNG отдельных кадров и анимированный GIF.
Package goal — декларативные условия на состояние сетки: "клеток типа K не меньше 20", "клетка (5,7) стала стеной", "картинка не менялась 30 тиков", и любые И/ИЛИ из них.
Package goal — декларативные условия на состояние сетки: "клеток типа K не меньше 20", "клетка (5,7) стала стеной", "картинка не менялась 30 тиков", и любые И/ИЛИ из них.
Package script реализует небольшой императивный язык для описания поведения клетки: выражения (Expr) произвольной вложенности и последовательности операторов (Stmt) — if/else, ограниченный repeat, return, запись в любую клетку (свою/соседнюю/произвольную по (x,y)), вызов пользовательских методов с параметрами и возвратом значения.
Package script реализует небольшой императивный язык для описания поведения клетки: выражения (Expr) произвольной вложенности и последовательности операторов (Stmt) — if/else, ограниченный repeat, return, запись в любую клетку (свою/соседнюю/произвольную по (x,y)), вызов пользовательских методов с параметрами и возвратом значения.
Package visualrule реализует automaton.Rule, чьи правила собираются не в коде, а интерактивно — как последовательность блоков "ЕСЛИ <условия> ТО <команды>", по порядку сверху вниз, первая совпавшая ветка побеждает (как if/elseif/else в Scratch).
Package visualrule реализует automaton.Rule, чьи правила собираются не в коде, а интерактивно — как последовательность блоков "ЕСЛИ <условия> ТО <команды>", по порядку сверху вниз, первая совпавшая ветка побеждает (как if/elseif/else в Scratch).
Заглушка для всех платформ, кроме js/wasm: без неё `go build ./...` на обычной машине падал бы с "build constraints exclude all Go files".
Заглушка для всех платформ, кроме js/wasm: без неё `go build ./...` на обычной машине падал бы с "build constraints exclude all Go files".
Package world хранит словарь симуляции: список типов клеток (Kind), именованных числовых констант (Constant) и переменных мира (Variable), плюс подписи слотов свойств клетки.
Package world хранит словарь симуляции: список типов клеток (Kind), именованных числовых констант (Constant) и переменных мира (Variable), плюс подписи слотов свойств клетки.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL