From 0c8a8cf8d861bc4ef3162e45e8b58d2f0173d2f7 Mon Sep 17 00:00:00 2001 From: equa Date: Sun, 18 Apr 2021 09:57:30 -0500 Subject: nanpa wan commit of the last two days: working cellular automata and rendering, prototype system, etc --- .gitignore | 1 + conf.lua | 6 + lib/cell.fnl | 10 + lib/cells.fnl | 67 +++++ lib/game.fnl | 109 ++++++++ lib/main.fnl | 69 +++++ lib/proto.fnl | 78 ++++++ lib/state.fnl | 19 ++ main.lua | 3 + vendor/lume.lua | 780 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 10 files changed, 1142 insertions(+) create mode 100644 .gitignore create mode 100644 conf.lua create mode 100644 lib/cell.fnl create mode 100644 lib/cells.fnl create mode 100644 lib/game.fnl create mode 100644 lib/main.fnl create mode 100644 lib/proto.fnl create mode 100644 lib/state.fnl create mode 100644 main.lua create mode 100644 vendor/lume.lua diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a941931 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +junk diff --git a/conf.lua b/conf.lua new file mode 100644 index 0000000..479acb1 --- /dev/null +++ b/conf.lua @@ -0,0 +1,6 @@ +function love.conf(t) + t.window.resizable = true + t.window.title = "GAMING" + t.window.vsync = 1 + t.window.msaa = 1 +end diff --git a/lib/cell.fnl b/lib/cell.fnl new file mode 100644 index 0000000..e9af960 --- /dev/null +++ b/lib/cell.fnl @@ -0,0 +1,10 @@ +(local proto (require :lib.proto)) + +{:init (proto.table-method :cell.init) + ;; given its 8 neighbors returns a new cell (or nil) + ;; TODO: other returns via either a clojure or coroutines + :birth (proto.meta-method :cell.birth) + :update (proto.meta-method :cell.update) + ;; returns a number from 0 to 1 + :aliveness (proto.meta-method-opt :cell.generation #0) + :color (proto.meta-method-opt :cell.color #[0 0 0])} diff --git a/lib/cells.fnl b/lib/cells.fnl new file mode 100644 index 0000000..442c7c7 --- /dev/null +++ b/lib/cells.fnl @@ -0,0 +1,67 @@ +(local cell (require :lib.cell)) + +(local neighbors [{:x -1 :y -1} + {:x -1 :y 0} + {:x -1 :y 1} + {:x 0 :y -1} + {:x 0 :y 1} + {:x 1 :y -1} + {:x 1 :y 0} + {:x 1 :y 1}]) + +(fn neighbors> [f threshold] + (var x 0) + ;; nnn this could be faster maybe + (each [k v (ipairs neighbors)] + (when (> (cell.aliveness (f v)) threshold) + (set x (+ x 1)))) + x) + +(local life + {cell.init + (fn [self] + (setmetatable {} self)) + cell.birth + (fn [self get] + (if (= (neighbors> get 0) 3) + self + nil)) + cell.update + (fn [self get] + (if (or (= (neighbors> get 0) 3) + (= (neighbors> get 0) 2)) + self + nil)) + cell.aliveness + #1 + cell.color + #[0.4 0.4 0.7] + }) + +(local brain + {cell.init + (fn [self] + (setmetatable {:stage 0} self)) + cell.birth + (fn [self get] + (if (= (neighbors> get 0.8) 2) + (do + (setmetatable {:stage 0} (getmetatable self))) + nil)) + cell.update + (fn [self get] + (if (= self.stage 0) + (setmetatable {:stage 1} (getmetatable self)) + nil)) + cell.aliveness + #(- 1 (* 0.5 $.stage)) + cell.color + #(if (= $.stage 0) + [0.7 0.4 0.3] + (= $.stage 5) + [0.5 0.4 0.3] + (= $.stage 1) + [0.2 0.2 0.3]) + }) + +{: life : brain} diff --git a/lib/game.fnl b/lib/game.fnl new file mode 100644 index 0000000..da96d9f --- /dev/null +++ b/lib/game.fnl @@ -0,0 +1,109 @@ +(local state (require :lib.state)) +(local cell (require :lib.cell)) +(local cells (require :lib.cells)) +(local fv (require :fennel.view)) + +(fn lerp* [a b c d x] + (+ c (* (/ (- x a) (- b a)) (- d c)))) + +(fn vec-lerp* [a b c d x] + {:x (lerp* a.x b.x c.x d.x x.x) + :y (lerp* a.y b.y c.y d.y x.y)}) + +(fn new-grid [w h f] + (var t {}) + (for [x 0 (- w 1)] + (tset t x {}) + (for [y 0 (- h 1)] + (tset (. t x) y (f x y)))) + t) + +(fn update [self] + (set self.ship.x (+ self.ship.x 0.02)) + (set self.ship.y (+ self.ship.y 0.005)) + (when (= self.tick 0) + (for [x 0 (- self.width 1)] + (for [y 0 (- self.height 1)] + (fn get [v] + (. self.grid + (% (+ v.x x) self.width) + (% (+ v.y y) self.height))) + (if (. self.grid x y) + ;; check if alive + (tset self.grid-alt x y (cell.update (. self.grid x y) + get)) + ;; check for neighbors and then use one at random + (do + (var neighbors []) + (for [x -1 1] + (for [y -1 1] + (table.insert neighbors (get {: x : y})))) + (if (. neighbors 1) + (tset self.grid-alt x y + (cell.birth (. neighbors (math.random (length neighbors))) get)) + (tset self.grid-alt x y nil)))))) + (set (self.grid self.grid-alt) (values self.grid-alt self.grid))) + ;; TODO + (set self.tick (% (+ self.tick 1) self.rate))) + +(fn id [x] x) + +(fn draw [self] + (local (width height) (love.graphics.getDimensions)) + (love.graphics.scale width height) + (local camera-size (math.max width height)) + (let [camera-size (math.max width height) + camera-box {:x width :y height} + camera-box {:x 1 :y 1} + radius-x (* self.radius (/ width camera-size)) + radius-y (* self.radius (/ height camera-size)) + camera-a {:x (- self.ship.x radius-x) + :y (- self.ship.y radius-y)} + camera-b {:x (+ self.ship.x radius-x) + :y (+ self.ship.y radius-y)} + cell-box (vec-lerp* {:x 0 :y 0} + {:x (* 2 radius-x) + :y (* 2 radius-y)} + {:x 0 :y 0} + camera-box + {:x 1 :y 1})] + (for [x (math.floor camera-a.x) (math.floor camera-b.x)] + (for [y (math.floor camera-a.y) (math.floor camera-b.y)] + (let [vec {:x (% x self.width) :y (% y self.height)} + render-a (vec-lerp* camera-a camera-b {:x 0 :y 0} camera-box + {: x : y}) + render-b (vec-lerp* camera-a camera-b {:x 0 :y 0} camera-box + {:x (+ x 1) :y (+ y 1)}) + the (. self.grid vec.x vec.y) + color (and the (cell.color the))] + (when color + (love.graphics.setColor (unpack color)) + (love.graphics.rectangle :fill + (id render-a.x) + (id render-a.y) + (id cell-box.x) + (id cell-box.y)))))))) + ;; (love.graphics.setLineWidth 0.1) + ;; (love.graphics.line 0 0 0.3 0.3) + ;; (love.graphics.polygon :line 0.3 0.3 0.6 0.3 0.4 0.6) + ;; (love.graphics.line 0.4 0.8 0.4 0.8) + ;; (love.graphics.print :Gaming)) + +(fn init [self] + (setmetatable + {:width 64 + :height 64 + :ship {:x 31 :y 31} + :radius 32 + :tick 0 + :rate 6 + :grid (new-grid 64 64 #(if (= (math.random 6) 1) + (if (< $1 52) + (cell.init cells.life) + (cell.init cells.brain)) + nil)) + :grid-alt (new-grid 64 64 #nil) + } + self)) + +{state.draw draw state.init init state.update update} diff --git a/lib/main.fnl b/lib/main.fnl new file mode 100644 index 0000000..4a9d26e --- /dev/null +++ b/lib/main.fnl @@ -0,0 +1,69 @@ +(local lume (require :vendor.lume)) +(local proto (require :lib.proto)) +(local state (require :lib.state)) +(local game (require :lib.game)) + +;; i am thinking we could actually do a really hacky thing (modules add themselves +;; to this list) with this later but +;; i'm not sure if it'd be worth it (it'd require those dependency loops maybe) +;; TODO: ^ +(local hotswap-modules + [:lib.cells + :lib.game + :lib.main]) + +;; the +;; oh thats why it doesnt work lmao + +(fn love.load [] + (global the-state (state.init game)) + (global messages {}) + (print "a")) + +(fn love.draw [] + (match (pcall #(state.draw the-state)) + (true x) nil + (false x) (do + (love.graphics.reset) + (print (.. "draw \n" x)) + (love.graphics.print (.. "draw: \n" x)))) + (love.graphics.reset) + (love.graphics.print (love.timer.getFPS)) + (when true ;; debug stuff + (love.graphics.print (table.concat + (lume.map messages #$.msg) + "\n") + 0 + 40) + (each [i v (lume.ripairs messages)] + (if (= v.ticks 0) + (table.remove messages i) + (set v.ticks (- v.ticks 1)))))) + +;; TODO: we need a better way to display errors at runtime for updates too +(fn love.update [] + ;; TODO: make state changes actually possible + (match (pcall #(state.update the-state)) + (true x) nil + (false x) (do + (print (.. "update: \n" x)) + (table.insert messages + {:ticks 1 + :msg (.. "update: \n" x)})))) + +(fn love.keypressed [key scancode repeat] + ;; (print key scancode repeat) + (when (= key "r") + (each [k v (lume.ripairs messages)] + (when (= v.type :reload-error) + (table.remove messages k))) + (print (.. (if (love.keyboard.isDown :lshift) :hard :soft) + " reloading...")) + (each [_ v (ipairs hotswap-modules)] + (match (lume.hotswap v) + (nil x) (table.insert messages + {:ticks -1 + :type :reload-error + :msg (.. "can't reload module " v "\n" x)}))) + (when (love.keyboard.isDown :lshift) + (love.load)))) diff --git a/lib/proto.fnl b/lib/proto.fnl new file mode 100644 index 0000000..13b65e9 --- /dev/null +++ b/lib/proto.fnl @@ -0,0 +1,78 @@ +;; function set in the prototype via its identity, i.e. +;; (local blah (meta-fn :blah) +;; (blah (setmetadata {} {blah (fn [] 4)})) ;; -> 4 +(fn meta-fn [name] + (local x {}) + (setmetatable + x + {:__call (fn [_ obj ...] ((. (getmetatable obj) x) ...)) + :__name name + :__fennelview (fn [] [name])})) + +(fn meta-method [name] + (local x {}) + (setmetatable + x + {:__call (fn [_ obj ...] ((. (getmetatable obj) x) obj ...)) + :__name name + :__fennelview (fn [] [name])})) + +;; function set in the prototype via its identity, i.e. +;; (local blah (meta-fn :blah) +;; however, these functions are optional, and nop if left out +;; (blah (setmetadata {} {})) ;; -> nil +(fn meta-fn-opt [name fallback] + (local x {}) + (setmetatable + x + {:__call (fn [_ obj ...] (if (and (getmetatable obj) + (. (getmetatable obj) x)) + ((. (getmetatable obj) x) ...) + fallback + (fallback ...) + nil)) + :__name name + :__fennelview (fn [] [name])})) + +(fn meta-method-opt [name fallback] + (local x {}) + (setmetatable + x + {:__call (fn [_ obj ...] (if (and (getmetatable obj) + (. (getmetatable obj) x)) + ((. (getmetatable obj) x) obj ...) + fallback + (fallback obj ...) + nil)) + :__name name + :__fennelview (fn [] [name])})) + +;; value set in the table via its identity, i.e. +;; (local blah (table-value :blah)) +;; (blah {blah 4}) ;; -> (. {blah 4} blah) -> 4 +(fn table-value [name] + (local x {}) + (setmetatable + x + {:__call (fn [_ obj ...] (. obj x)) + :__name name + :__fennelview (fn [] [name])})) + +(fn table-fn [name] + (local x {}) + (setmetatable + x + {:__call (fn [_ obj ...] ((. obj x) ...)) + :__name name + :__fennelview (fn [] [name])})) + +;; methods have an extra "self" param +(fn table-method [name] + (local x {}) + (setmetatable + x + {:__call (fn [_ obj ...] ((. obj x) obj ...)) + :__name name + :__fennelview (fn [] [name])})) + +{: meta-fn : meta-fn-opt : meta-method : meta-method-opt : table-value : table-fn : table-method} diff --git a/lib/state.fnl b/lib/state.fnl new file mode 100644 index 0000000..d98b94e --- /dev/null +++ b/lib/state.fnl @@ -0,0 +1,19 @@ +;; later if it becomes a hassle we can convert this into a separate +;; file that we can reload or something + +(local proto (require :lib.proto)) + +{ + :init (proto.table-method :state.init) + ;; update is a bit special; it can either return nothing (the state continues + ;; as is, and mutated somehow (sorry we're doing things non-purely; i'd like + ;; to do them purely but creating tables is slow as heck that'd be ridiculous)) + ;; or it can return a state that it transitions to automatically + ;; game pausing basically works like that: it returns a pause structure with the + ;; regular state within its object, and then the pause structure uses that state + ;; object to return back to it later. pretty cool! + :update (proto.meta-method-opt :state.update) + ;; all of the next functions are just. regular love functions, exactly the same + ;; i hope + :draw (proto.meta-method-opt :state.draw) + } diff --git a/main.lua b/main.lua new file mode 100644 index 0000000..d9b84cb --- /dev/null +++ b/main.lua @@ -0,0 +1,3 @@ +table.insert(package.loaders or package.searchers, require("fennel").makeSearcher({correlate = true})) + +require("lib/main") diff --git a/vendor/lume.lua b/vendor/lume.lua new file mode 100644 index 0000000..2157891 --- /dev/null +++ b/vendor/lume.lua @@ -0,0 +1,780 @@ +-- +-- lume +-- +-- Copyright (c) 2020 rxi +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy of +-- this software and associated documentation files (the "Software"), to deal in +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +-- of the Software, and to permit persons to whom the Software is furnished to do +-- so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. +-- + +local lume = { _version = "2.3.0" } + +local pairs, ipairs = pairs, ipairs +local type, assert, unpack = type, assert, unpack or table.unpack +local tostring, tonumber = tostring, tonumber +local math_floor = math.floor +local math_ceil = math.ceil +local math_atan2 = math.atan2 or math.atan +local math_sqrt = math.sqrt +local math_abs = math.abs + +local noop = function() +end + +local identity = function(x) + return x +end + +local patternescape = function(str) + return str:gsub("[%(%)%.%%%+%-%*%?%[%]%^%$]", "%%%1") +end + +local absindex = function(len, i) + return i < 0 and (len + i + 1) or i +end + +local iscallable = function(x) + if type(x) == "function" then return true end + local mt = getmetatable(x) + return mt and mt.__call ~= nil +end + +local getiter = function(x) + if lume.isarray(x) then + return ipairs + elseif type(x) == "table" then + return pairs + end + error("expected table", 3) +end + +local iteratee = function(x) + if x == nil then return identity end + if iscallable(x) then return x end + if type(x) == "table" then + return function(z) + for k, v in pairs(x) do + if z[k] ~= v then return false end + end + return true + end + end + return function(z) return z[x] end +end + + + +function lume.clamp(x, min, max) + return x < min and min or (x > max and max or x) +end + + +function lume.round(x, increment) + if increment then return lume.round(x / increment) * increment end + return x >= 0 and math_floor(x + .5) or math_ceil(x - .5) +end + + +function lume.sign(x) + return x < 0 and -1 or 1 +end + + +function lume.lerp(a, b, amount) + return a + (b - a) * lume.clamp(amount, 0, 1) +end + + +function lume.smooth(a, b, amount) + local t = lume.clamp(amount, 0, 1) + local m = t * t * (3 - 2 * t) + return a + (b - a) * m +end + + +function lume.pingpong(x) + return 1 - math_abs(1 - x % 2) +end + + +function lume.distance(x1, y1, x2, y2, squared) + local dx = x1 - x2 + local dy = y1 - y2 + local s = dx * dx + dy * dy + return squared and s or math_sqrt(s) +end + + +function lume.angle(x1, y1, x2, y2) + return math_atan2(y2 - y1, x2 - x1) +end + + +function lume.vector(angle, magnitude) + return math.cos(angle) * magnitude, math.sin(angle) * magnitude +end + + +function lume.random(a, b) + if not a then a, b = 0, 1 end + if not b then b = 0 end + return a + math.random() * (b - a) +end + + +function lume.randomchoice(t) + return t[math.random(#t)] +end + + +function lume.weightedchoice(t) + local sum = 0 + for _, v in pairs(t) do + assert(v >= 0, "weight value less than zero") + sum = sum + v + end + assert(sum ~= 0, "all weights are zero") + local rnd = lume.random(sum) + for k, v in pairs(t) do + if rnd < v then return k end + rnd = rnd - v + end +end + + +function lume.isarray(x) + return type(x) == "table" and x[1] ~= nil +end + + +function lume.push(t, ...) + local n = select("#", ...) + for i = 1, n do + t[#t + 1] = select(i, ...) + end + return ... +end + + +function lume.remove(t, x) + local iter = getiter(t) + for i, v in iter(t) do + if v == x then + if lume.isarray(t) then + table.remove(t, i) + break + else + t[i] = nil + break + end + end + end + return x +end + + +function lume.clear(t) + local iter = getiter(t) + for k in iter(t) do + t[k] = nil + end + return t +end + + +function lume.extend(t, ...) + for i = 1, select("#", ...) do + local x = select(i, ...) + if x then + for k, v in pairs(x) do + t[k] = v + end + end + end + return t +end + + +function lume.shuffle(t) + local rtn = {} + for i = 1, #t do + local r = math.random(i) + if r ~= i then + rtn[i] = rtn[r] + end + rtn[r] = t[i] + end + return rtn +end + + +function lume.sort(t, comp) + local rtn = lume.clone(t) + if comp then + if type(comp) == "string" then + table.sort(rtn, function(a, b) return a[comp] < b[comp] end) + else + table.sort(rtn, comp) + end + else + table.sort(rtn) + end + return rtn +end + + +function lume.array(...) + local t = {} + for x in ... do t[#t + 1] = x end + return t +end + + +function lume.each(t, fn, ...) + local iter = getiter(t) + if type(fn) == "string" then + for _, v in iter(t) do v[fn](v, ...) end + else + for _, v in iter(t) do fn(v, ...) end + end + return t +end + + +function lume.map(t, fn) + fn = iteratee(fn) + local iter = getiter(t) + local rtn = {} + for k, v in iter(t) do rtn[k] = fn(v) end + return rtn +end + + +function lume.all(t, fn) + fn = iteratee(fn) + local iter = getiter(t) + for _, v in iter(t) do + if not fn(v) then return false end + end + return true +end + + +function lume.any(t, fn) + fn = iteratee(fn) + local iter = getiter(t) + for _, v in iter(t) do + if fn(v) then return true end + end + return false +end + + +function lume.reduce(t, fn, first) + local started = first ~= nil + local acc = first + local iter = getiter(t) + for _, v in iter(t) do + if started then + acc = fn(acc, v) + else + acc = v + started = true + end + end + assert(started, "reduce of an empty table with no first value") + return acc +end + + +function lume.unique(t) + local rtn = {} + for k in pairs(lume.invert(t)) do + rtn[#rtn + 1] = k + end + return rtn +end + + +function lume.filter(t, fn, retainkeys) + fn = iteratee(fn) + local iter = getiter(t) + local rtn = {} + if retainkeys then + for k, v in iter(t) do + if fn(v) then rtn[k] = v end + end + else + for _, v in iter(t) do + if fn(v) then rtn[#rtn + 1] = v end + end + end + return rtn +end + + +function lume.reject(t, fn, retainkeys) + fn = iteratee(fn) + local iter = getiter(t) + local rtn = {} + if retainkeys then + for k, v in iter(t) do + if not fn(v) then rtn[k] = v end + end + else + for _, v in iter(t) do + if not fn(v) then rtn[#rtn + 1] = v end + end + end + return rtn +end + + +function lume.merge(...) + local rtn = {} + for i = 1, select("#", ...) do + local t = select(i, ...) + local iter = getiter(t) + for k, v in iter(t) do + rtn[k] = v + end + end + return rtn +end + + +function lume.concat(...) + local rtn = {} + for i = 1, select("#", ...) do + local t = select(i, ...) + if t ~= nil then + local iter = getiter(t) + for _, v in iter(t) do + rtn[#rtn + 1] = v + end + end + end + return rtn +end + + +function lume.find(t, value) + local iter = getiter(t) + for k, v in iter(t) do + if v == value then return k end + end + return nil +end + + +function lume.match(t, fn) + fn = iteratee(fn) + local iter = getiter(t) + for k, v in iter(t) do + if fn(v) then return v, k end + end + return nil +end + + +function lume.count(t, fn) + local count = 0 + local iter = getiter(t) + if fn then + fn = iteratee(fn) + for _, v in iter(t) do + if fn(v) then count = count + 1 end + end + else + if lume.isarray(t) then + return #t + end + for _ in iter(t) do count = count + 1 end + end + return count +end + + +function lume.slice(t, i, j) + i = i and absindex(#t, i) or 1 + j = j and absindex(#t, j) or #t + local rtn = {} + for x = i < 1 and 1 or i, j > #t and #t or j do + rtn[#rtn + 1] = t[x] + end + return rtn +end + + +function lume.first(t, n) + if not n then return t[1] end + return lume.slice(t, 1, n) +end + + +function lume.last(t, n) + if not n then return t[#t] end + return lume.slice(t, -n, -1) +end + + +function lume.invert(t) + local rtn = {} + for k, v in pairs(t) do rtn[v] = k end + return rtn +end + + +function lume.pick(t, ...) + local rtn = {} + for i = 1, select("#", ...) do + local k = select(i, ...) + rtn[k] = t[k] + end + return rtn +end + + +function lume.keys(t) + local rtn = {} + local iter = getiter(t) + for k in iter(t) do rtn[#rtn + 1] = k end + return rtn +end + + +function lume.clone(t) + local rtn = {} + for k, v in pairs(t) do rtn[k] = v end + return rtn +end + + +function lume.fn(fn, ...) + assert(iscallable(fn), "expected a function as the first argument") + local args = { ... } + return function(...) + local a = lume.concat(args, { ... }) + return fn(unpack(a)) + end +end + + +function lume.once(fn, ...) + local f = lume.fn(fn, ...) + local done = false + return function(...) + if done then return end + done = true + return f(...) + end +end + + +local memoize_fnkey = {} +local memoize_nil = {} + +function lume.memoize(fn) + local cache = {} + return function(...) + local c = cache + for i = 1, select("#", ...) do + local a = select(i, ...) or memoize_nil + c[a] = c[a] or {} + c = c[a] + end + c[memoize_fnkey] = c[memoize_fnkey] or {fn(...)} + return unpack(c[memoize_fnkey]) + end +end + + +function lume.combine(...) + local n = select('#', ...) + if n == 0 then return noop end + if n == 1 then + local fn = select(1, ...) + if not fn then return noop end + assert(iscallable(fn), "expected a function or nil") + return fn + end + local funcs = {} + for i = 1, n do + local fn = select(i, ...) + if fn ~= nil then + assert(iscallable(fn), "expected a function or nil") + funcs[#funcs + 1] = fn + end + end + return function(...) + for _, f in ipairs(funcs) do f(...) end + end +end + + +function lume.call(fn, ...) + if fn then + return fn(...) + end +end + + +function lume.time(fn, ...) + local start = os.clock() + local rtn = {fn(...)} + return (os.clock() - start), unpack(rtn) +end + + +local lambda_cache = {} + +function lume.lambda(str) + if not lambda_cache[str] then + local args, body = str:match([[^([%w,_ ]-)%->(.-)$]]) + assert(args and body, "bad string lambda") + local s = "return function(" .. args .. ")\nreturn " .. body .. "\nend" + lambda_cache[str] = lume.dostring(s) + end + return lambda_cache[str] +end + + +local serialize + +local serialize_map = { + [ "boolean" ] = tostring, + [ "nil" ] = tostring, + [ "string" ] = function(v) return string.format("%q", v) end, + [ "number" ] = function(v) + if v ~= v then return "0/0" -- nan + elseif v == 1 / 0 then return "1/0" -- inf + elseif v == -1 / 0 then return "-1/0" end -- -inf + return tostring(v) + end, + [ "table" ] = function(t, stk) + stk = stk or {} + if stk[t] then error("circular reference") end + local rtn = {} + stk[t] = true + for k, v in pairs(t) do + rtn[#rtn + 1] = "[" .. serialize(k, stk) .. "]=" .. serialize(v, stk) + end + stk[t] = nil + return "{" .. table.concat(rtn, ",") .. "}" + end +} + +setmetatable(serialize_map, { + __index = function(_, k) error("unsupported serialize type: " .. k) end +}) + +serialize = function(x, stk) + return serialize_map[type(x)](x, stk) +end + +function lume.serialize(x) + return serialize(x) +end + + +function lume.deserialize(str) + return lume.dostring("return " .. str) +end + + +function lume.split(str, sep) + if not sep then + return lume.array(str:gmatch("([%S]+)")) + else + assert(sep ~= "", "empty separator") + local psep = patternescape(sep) + return lume.array((str..sep):gmatch("(.-)("..psep..")")) + end +end + + +function lume.trim(str, chars) + if not chars then return str:match("^[%s]*(.-)[%s]*$") end + chars = patternescape(chars) + return str:match("^[" .. chars .. "]*(.-)[" .. chars .. "]*$") +end + + +function lume.wordwrap(str, limit) + limit = limit or 72 + local check + if type(limit) == "number" then + check = function(s) return #s >= limit end + else + check = limit + end + local rtn = {} + local line = "" + for word, spaces in str:gmatch("(%S+)(%s*)") do + local s = line .. word + if check(s) then + table.insert(rtn, line .. "\n") + line = word + else + line = s + end + for c in spaces:gmatch(".") do + if c == "\n" then + table.insert(rtn, line .. "\n") + line = "" + else + line = line .. c + end + end + end + table.insert(rtn, line) + return table.concat(rtn) +end + + +function lume.format(str, vars) + if not vars then return str end + local f = function(x) + return tostring(vars[x] or vars[tonumber(x)] or "{" .. x .. "}") + end + return (str:gsub("{(.-)}", f)) +end + + +function lume.trace(...) + local info = debug.getinfo(2, "Sl") + local t = { info.short_src .. ":" .. info.currentline .. ":" } + for i = 1, select("#", ...) do + local x = select(i, ...) + if type(x) == "number" then + x = string.format("%g", lume.round(x, .01)) + end + t[#t + 1] = tostring(x) + end + print(table.concat(t, " ")) +end + + +function lume.dostring(str) + return assert((loadstring or load)(str))() +end + + +function lume.uuid() + local fn = function(x) + local r = math.random(16) - 1 + r = (x == "x") and (r + 1) or (r % 4) + 9 + return ("0123456789abcdef"):sub(r, r) + end + return (("xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"):gsub("[xy]", fn)) +end + + +function lume.hotswap(modname) + local oldglobal = lume.clone(_G) + local updated = {} + local function update(old, new) + if updated[old] then return end + updated[old] = true + local oldmt, newmt = getmetatable(old), getmetatable(new) + if oldmt and newmt then update(oldmt, newmt) end + for k, v in pairs(new) do + if type(v) == "table" then update(old[k], v) else old[k] = v end + end + end + local err = nil + local function onerror(e) + for k in pairs(_G) do _G[k] = oldglobal[k] end + err = lume.trim(e) + end + local ok, oldmod = pcall(require, modname) + oldmod = ok and oldmod or nil + xpcall(function() + package.loaded[modname] = nil + local newmod = require(modname) + if type(oldmod) == "table" then update(oldmod, newmod) end + for k, v in pairs(oldglobal) do + if v ~= _G[k] and type(v) == "table" then + update(v, _G[k]) + _G[k] = v + end + end + end, onerror) + package.loaded[modname] = oldmod + if err then return nil, err end + return oldmod +end + + +local ripairs_iter = function(t, i) + i = i - 1 + local v = t[i] + if v ~= nil then + return i, v + end +end + +function lume.ripairs(t) + return ripairs_iter, t, (#t + 1) +end + + +function lume.color(str, mul) + mul = mul or 1 + local r, g, b, a + r, g, b = str:match("#(%x%x)(%x%x)(%x%x)") + if r then + r = tonumber(r, 16) / 0xff + g = tonumber(g, 16) / 0xff + b = tonumber(b, 16) / 0xff + a = 1 + elseif str:match("rgba?%s*%([%d%s%.,]+%)") then + local f = str:gmatch("[%d.]+") + r = (f() or 0) / 0xff + g = (f() or 0) / 0xff + b = (f() or 0) / 0xff + a = f() or 1 + else + error(("bad color string '%s'"):format(str)) + end + return r * mul, g * mul, b * mul, a * mul +end + + +local chain_mt = {} +chain_mt.__index = lume.map(lume.filter(lume, iscallable, true), + function(fn) + return function(self, ...) + self._value = fn(self._value, ...) + return self + end + end) +chain_mt.__index.result = function(x) return x._value end + +function lume.chain(value) + return setmetatable({ _value = value }, chain_mt) +end + +setmetatable(lume, { + __call = function(_, ...) + return lume.chain(...) + end +}) + + +return lume -- cgit 1.3.0-6-gf8a5