commit 4d368074ebd3316f19afb817f5981ee784509d9d Author: Ivan Yuriev Date: Mon Jun 30 10:55:32 2025 +0300 initial commit diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..7c92c48 --- /dev/null +++ b/.github/workflows/test.yml @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..599be4e --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.beam +*.ez +/build +erl_crash.dump diff --git a/.zed/settings.json b/.zed/settings.json new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md new file mode 100644 index 0000000..f215ee7 --- /dev/null +++ b/README.md @@ -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 . + +## Development + +```sh +gleam run # Run the project +gleam test # Run the tests +``` diff --git a/gleam.toml b/gleam.toml new file mode 100644 index 0000000..f18c9ef --- /dev/null +++ b/gleam.toml @@ -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" diff --git a/manifest.toml b/manifest.toml new file mode 100644 index 0000000..ba78a99 --- /dev/null +++ b/manifest.toml @@ -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" } diff --git a/src/effect.gleam b/src/effect.gleam new file mode 100644 index 0000000..36082a3 --- /dev/null +++ b/src/effect.gleam @@ -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 + }), + ) + } +} diff --git a/src/id.gleam b/src/id.gleam new file mode 100644 index 0000000..8c927ca --- /dev/null +++ b/src/id.gleam @@ -0,0 +1,2 @@ +pub type Id = + Int diff --git a/src/selector.gleam b/src/selector.gleam new file mode 100644 index 0000000..7b3d56d --- /dev/null +++ b/src/selector.gleam @@ -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 + } +} diff --git a/src/spell.gleam b/src/spell.gleam new file mode 100644 index 0000000..02ae75c --- /dev/null +++ b/src/spell.gleam @@ -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 +} diff --git a/src/thoughts.txt b/src/thoughts.txt new file mode 100644 index 0000000..ec8e418 --- /dev/null +++ b/src/thoughts.txt @@ -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. diff --git a/src/world.gleam b/src/world.gleam new file mode 100644 index 0000000..6901f46 --- /dev/null +++ b/src/world.gleam @@ -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: []) +} diff --git a/test/spell_test.gleam b/test/spell_test.gleam new file mode 100644 index 0000000..fba3c88 --- /dev/null +++ b/test/spell_test.gleam @@ -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!" +}