1/// Seeded randomness, kept as a design system rather than as a utility.2///3/// Every varying thing in this app is a pure function of a seed: which board a4/// rung serves, what that board is printed in, which of the four piece colours5/// fills the maker's mark today. None of it is `random`. A rung looked the way6/// it looked yesterday, and will tomorrow, because the same pair of numbers7/// went in.8///9/// Two halves, always used together. `hash_string` is FNV-1a over the name of10/// the thing being decided, and `mulberry` is the generator that name seeds.11/// The old seed used the length of an id, so same-length ids shared a stream;12/// hashing the whole string is what stops `narrow` and `corner` being the same13/// board twice.14///15/// The arithmetic is 32-bit wrap-around, which the platform's integers are not:16/// a Gleam `Int` on this target is a double, so `imul` and the unsigned shift17/// live in FFI and nothing else does. The generators themselves are written18/// here, in the open, because they are the part worth being able to read.19import gleam/int20import gleam/list21import gleam/string2223/// A stream: the state it left off at, so a caller threads it rather than24/// mutating it. `mulberry` in the mockup was a closure; a closure that returns25/// a different answer each call is the one thing a Gleam function cannot be,26/// and threading the state makes the determinism structural instead of27/// conventional.28pub opaque type Rng {29 Rng(state: Int)30}3132/// 32-bit multiply with wrap-around: JavaScript's `Math.imul`.33@external(javascript, "./prng_ffi.mjs", "imul")34fn imul(a: Int, b: Int) -> Int3536/// Logical right shift, which the platform's `>>` is not.37@external(javascript, "./prng_ffi.mjs", "ushr")38fn ushr(value: Int, bits: Int) -> Int3940/// Truncate to an unsigned 32-bit word.41@external(javascript, "./prng_ffi.mjs", "u32")42fn u32(value: Int) -> Int4344/// Force to a signed 32-bit word, which is what `| 0` does.45@external(javascript, "./prng_ffi.mjs", "i32")46fn i32(value: Int) -> Int4748@external(javascript, "./prng_ffi.mjs", "bxor")49fn bxor(a: Int, b: Int) -> Int5051@external(javascript, "./prng_ffi.mjs", "bor")52fn bor(a: Int, b: Int) -> Int5354/// FNV-1a over a string. The other half of every seeded decision here.55pub fn hash_string(value: String) -> Int {56 value57 |> string.to_utf_codepoints58 |> list.fold(0x811c9dc5, fn(hash, codepoint) {59 let hash = bxor(hash, string.utf_codepoint_to_int(codepoint))60 imul(hash, 0x01000193)61 })62 |> u3263}6465/// Open a stream on a seed.66pub fn mulberry(seed: Int) -> Rng {67 Rng(i32(seed))68}6970/// Open a stream on the name of the thing being decided.71pub fn seeded(name: String) -> Rng {72 mulberry(hash_string(name))73}7475/// Open a stream on a name and a number — the pair almost every caller wants,76/// since a ladder is a name and a rung.77pub fn seeded_at(name: String, offset: Int) -> Rng {78 mulberry(hash_string(name) + offset)79}8081/// One draw in `0.0 <= n < 1.0`, and the stream that follows it.82pub fn next(rng: Rng) -> #(Float, Rng) {83 let Rng(state) = rng84 let state = i32(state + 0x6D2B79F5)85 let t = imul(bxor(state, ushr(state, 15)), bor(1, state))86 let t = bxor(t + imul(bxor(t, ushr(t, 7)), bor(61, t)), t)87 let value = ushr(bxor(t, ushr(t, 14)), 0)88 #(int.to_float(value) /. 4_294_967_296.0, Rng(state))89}9091/// One draw in `0 <= n < bound`.92pub fn next_int(rng: Rng, bound: Int) -> #(Int, Rng) {93 case bound <= 0 {94 True -> #(0, rng)95 False -> {96 let #(value, rng) = next(rng)97 #(int.min(bound - 1, float_floor(value *. int.to_float(bound))), rng)98 }99 }100}101102/// One member of a pool. Clamped rather than wrapped, exactly as the mockup's103/// `pick` is: a draw of 1.0 is not supposed to be possible, and landing off the104/// end of a theme's pool would be a crash in a picture rather than a colour.105pub fn pick(rng: Rng, items: List(a), fallback: a) -> #(a, Rng) {106 let count = list.length(items)107 case count {108 0 -> #(fallback, rng)109 _ -> {110 let #(index, rng) = next_int(rng, count)111 let item = items |> list.drop(index) |> list.first |> unwrap_or(fallback)112 #(item, rng)113 }114 }115}116117fn unwrap_or(result: Result(a, b), fallback: a) -> a {118 case result {119 Ok(value) -> value120 Error(_) -> fallback121 }122}123124@external(javascript, "./prng_ffi.mjs", "floor")125fn float_floor(value: Float) -> Int