initial commit
Some checks failed
test / test (push) Has been cancelled

This commit is contained in:
Ivan Yuriev 2025-06-30 10:55:32 +03:00
commit 4d368074eb
13 changed files with 341 additions and 0 deletions

23
.github/workflows/test.yml vendored Normal file
View File

@ -0,0 +1,23 @@
name: test
on:
push:
branches:
- master
- main
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: erlef/setup-beam@v1
with:
otp-version: "27.1.2"
gleam-version: "1.11.1"
rebar3-version: "3"
# elixir-version: "1"
- run: gleam deps download
- run: gleam test
- run: gleam format --check src test

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
*.beam
*.ez
/build
erl_crash.dump

0
.zed/settings.json Normal file
View File

24
README.md Normal file
View File

@ -0,0 +1,24 @@
# spell
[![Package Version](https://img.shields.io/hexpm/v/spell)](https://hex.pm/packages/spell)
[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/spell/)
```sh
gleam add spell@1
```
```gleam
import spell
pub fn main() -> Nil {
// TODO: An example of the project in use
}
```
Further documentation can be found at <https://hexdocs.pm/spell>.
## Development
```sh
gleam run # Run the project
gleam test # Run the tests
```

20
gleam.toml Normal file
View File

@ -0,0 +1,20 @@
name = "spell"
version = "1.0.0"
# Fill out these fields if you intend to generate HTML documentation or publish
# your project to the Hex package manager.
#
# description = ""
# licences = ["Apache-2.0"]
# repository = { type = "github", user = "", repo = "" }
# links = [{ title = "Website", href = "" }]
#
# For a full reference of all the available options, you can have a look at
# https://gleam.run/writing-gleam/gleam-toml/.
[dependencies]
gleam_stdlib = ">= 0.44.0 and < 2.0.0"
gleam_erlang = ">= 1.1.0 and < 2.0.0"
[dev-dependencies]
gleeunit = ">= 1.0.0 and < 2.0.0"

13
manifest.toml Normal file
View File

@ -0,0 +1,13 @@
# This file was generated by Gleam
# You typically do not need to edit this file
packages = [
{ name = "gleam_erlang", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "D7A2E71CE7F6B513E62F9A9EF6DFDE640D9607598C477FCCADEF751C45FD82E7" },
{ name = "gleam_stdlib", version = "0.60.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "621D600BB134BC239CB2537630899817B1A42E60A1D46C5E9F3FAE39F88C800B" },
{ name = "gleeunit", version = "1.5.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "D33B7736CF0766ED3065F64A1EBB351E72B2E8DE39BAFC8ADA0E35E92A6A934F" },
]
[requirements]
gleam_erlang = { version = ">= 1.1.0 and < 2.0.0" }
gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" }
gleeunit = { version = ">= 1.0.0 and < 2.0.0" }

18
src/effect.gleam Normal file
View File

@ -0,0 +1,18 @@
import gleam/dict
import id.{type Id}
import world.{type World, World}
pub fn switch_cell_state(id: Id, world: World) {
case world.cells |> dict.get(id) {
Error(_) -> world
Ok(state) ->
World(
..world,
cells: world.cells
|> dict.insert(id, case state {
world.Alive -> world.Dead
world.Dead -> world.Alive
}),
)
}
}

2
src/id.gleam Normal file
View File

@ -0,0 +1,2 @@
pub type Id =
Int

17
src/selector.gleam Normal file
View File

@ -0,0 +1,17 @@
import gleam/dict
import world.{type World}
pub fn all(_id: Int, world: World) {
world.cells |> dict.keys
}
pub fn collision(id: Int, world: World) {
let selector_pos = world.positions |> dict.get(id)
case selector_pos {
Error(_) -> []
Ok(pos) ->
world.positions
|> dict.filter(fn(k, v) { k != id && v == pos })
|> dict.keys
}
}

139
src/spell.gleam Normal file
View File

@ -0,0 +1,139 @@
import effect
import gleam/dict
import gleam/erlang/process
import gleam/int
import gleam/io
import gleam/list
import gleam/option
import gleam/pair
import gleam/string
import id.{type Id}
import selector
import world.{type Trigger, type World, Alive, Dead, Once, World}
pub fn main() {
let trigger = Once(on: selector.collision, apply: effect.switch_cell_state)
let world =
World(
triggers: dict.from_list([#(0, trigger)]),
cells: dict.from_list([#(1, Alive)]),
effects: dict.new(),
positions: dict.from_list([#(0, 0), #(1, 10)]),
cleanup_queue: [],
)
world |> tick |> pretty_print
io.println("")
}
pub fn tick(world world: World) {
{
world |> pretty_print
let #(effects, triggers_cleanup) = world |> do_triggers
let #(world, effects_cleanup) = world |> do_effects
// hardcoded
let trigger = world.positions |> dict.get(0)
case trigger {
Error(_) -> world
Ok(pos) -> {
let positions = world.positions |> dict.insert(0, pos + 1)
let world = World(..world, positions: positions)
// end of hardcoded
let world =
World(
..world,
effects: effects,
cleanup_queue: triggers_cleanup |> list.append(effects_cleanup),
)
process.sleep(100)
tick(world |> world.cleanup)
}
}
}
}
/// The worst implementation ever
pub fn do_triggers(world: World) {
checker(world, world.triggers |> dict.to_list, dict.new(), [])
}
fn checker(world, triggers: List(#(Id, Trigger)), effects, cleanup) {
case triggers {
[] -> #(effects, cleanup)
[x, ..xs] ->
case x {
#(id, Once(on, apply)) -> {
case id |> on(world) {
[] -> checker(world, xs, effects, cleanup)
selection -> {
let effects =
selection
|> list.fold(effects, fn(effects, id) {
effects
|> dict.upsert(id, fn(x) {
case x {
option.None -> [apply]
option.Some(xs) -> [apply, ..xs]
}
})
})
let cleanup = [id, ..cleanup]
checker(world, xs, effects, cleanup)
}
}
}
}
}
}
pub fn do_effects(world: World) {
let new_state =
world.effects
|> dict.fold(world, fn(w, id, effects) {
effects
|> list.fold(w, fn(w_, ef) { ef(id, w_) })
})
#(new_state, world.effects |> dict.keys)
}
fn pretty_print(world: World) {
{ "\r" <> list.range(0, 39) |> list.map(fn(_) { " " }) |> string.join("") }
|> io.print
let space =
list.range(0, 9)
|> list.map(fn(i) { #(i, " ") })
|> dict.from_list
let entities =
world.positions
|> dict.map_values(fn(id, pos) {
case world.cells |> dict.get(id), world.triggers |> dict.get(id) {
Error(_), Ok(_) -> #(pos, "*")
Ok(state), _ ->
case state {
Alive -> #(pos, "O")
Dead -> #(pos, "#")
}
Error(_), Error(_) -> #(pos, " ")
}
})
|> dict.values
|> dict.from_list
let str =
space
|> dict.merge(entities)
|> dict.to_list
|> list.sort(fn(a, b) { a.0 |> int.compare(b.0) })
|> list.map(pair.second)
|> string.join("")
{ "\r" <> str } |> io.print
str
}

31
src/thoughts.txt Normal file
View File

@ -0,0 +1,31 @@
/// /// /// /// /// ///
// Spell: Selectors + Effects
// Effect: Element -> Element // Side effects here like altering the world? (Element, World) -> (Element, World)
// In any case, effects should be delayed. Do not call eagerly!
//
// Lifecycle:
// 1) Activate selectors
// 2) Schedule effects
// 3) Apply effects
// 4) Something that actually change the world
// 5) Repeat until the Universe dies
//
// When do we alter the global state? I don't know. Nobody knows. Perhaps, Kitsune-Jesus is the one who does know.
// Ok it should somehow compile all the scheduled effects into one GIGA-effect which is able to "mutate" the global state through re-creation.
// list.fold(Element -> World)? That's potentially incredibly difficult to implement. Like, update the whole World for a mere Element mutation?
// Make a bunch of World -> World functions for convenience? Maybe.
//
//
// Selector has states. As always, it's just a state machine with a transition function ft: (SelectorState) -> (SelectorState).
// That allows us to implement lazy evaluation (like promises/futures). We're doing so calling the transition function every 'tick'.
// Once the Selector's condition is satisfied, we may process the following logic.
// So it works like:
// selector.run(trigger_when?)
// # doing other stuff here
// .then(kys)
//
// On implementation (naive, must be hardcoded in c++ instead):
// - every tick check every selector's trigger (trigger just somehow checks the state of the world, so it's O(n^2) and generally too bad)
// - if some condition is satisfied, do stuff according to it
// - the stuff is actually to apply an Effect to some Element
// - Also we should allow high-order Selectors (i.e. Selectors on Selectors) to get more slow spaghetti code and cooler spells.

37
src/world.gleam Normal file
View File

@ -0,0 +1,37 @@
import gleam/dict.{type Dict}
import id.{type Id}
pub type CellState {
Dead
Alive
}
pub type Effect =
fn(Id, World) -> World
pub type Selector =
fn(Id, World) -> List(Id)
pub type Trigger {
Once(on: Selector, apply: Effect)
}
pub type World {
World(
triggers: Dict(Id, Trigger),
cells: Dict(Id, CellState),
effects: Dict(Id, List(Effect)),
positions: Dict(Id, Int),
cleanup_queue: List(Id),
)
}
pub fn cleanup(world: World) -> World {
let queue = world.cleanup_queue
let selectors = world.triggers |> dict.drop(queue)
let cells = world.cells |> dict.drop(queue)
let effects = world.effects |> dict.drop(queue)
let positions = world.positions |> dict.drop(queue)
World(selectors, cells, effects, positions, cleanup_queue: [])
}

13
test/spell_test.gleam Normal file
View File

@ -0,0 +1,13 @@
import gleeunit
pub fn main() -> Nil {
gleeunit.main()
}
// gleeunit test functions end in `_test`
pub fn hello_world_test() {
let name = "Joe"
let greeting = "Hello, " <> name <> "!"
assert greeting == "Hello, Joe!"
}