This commit is contained in:
commit
4d368074eb
23
.github/workflows/test.yml
vendored
Normal file
23
.github/workflows/test.yml
vendored
Normal 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
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
*.beam
|
||||
*.ez
|
||||
/build
|
||||
erl_crash.dump
|
||||
0
.zed/settings.json
Normal file
0
.zed/settings.json
Normal file
24
README.md
Normal file
24
README.md
Normal file
@ -0,0 +1,24 @@
|
||||
# spell
|
||||
|
||||
[](https://hex.pm/packages/spell)
|
||||
[](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
20
gleam.toml
Normal 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
13
manifest.toml
Normal 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
18
src/effect.gleam
Normal 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
2
src/id.gleam
Normal file
@ -0,0 +1,2 @@
|
||||
pub type Id =
|
||||
Int
|
||||
17
src/selector.gleam
Normal file
17
src/selector.gleam
Normal 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
139
src/spell.gleam
Normal 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
31
src/thoughts.txt
Normal 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
37
src/world.gleam
Normal 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
13
test/spell_test.gleam
Normal 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!"
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user