├── .gitmodules ├── 2d_geom.lua ├── class.lua ├── common.lua ├── conf.lua ├── font ├── Licence.txt └── ProggyClean.ttf ├── functional.lua ├── main.lua ├── math.lua ├── pathfind.lua ├── readme.md ├── sequence.lua ├── set.lua └── table.lua /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "batteries"] 2 | path = batteries 3 | url = https://github.com/1bardesign/batteries.git 4 | -------------------------------------------------------------------------------- /2d_geom.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- interactive intersection example 3 | -- 4 | -- bouncing shapes in ~120 lines 5 | -- 6 | 7 | require("common") 8 | 9 | -- level gen 10 | 11 | --(helper) 12 | local gen_area = vec2(350, 500) 13 | local function random_point_in_area() 14 | return vec2(love.math.random(), love.math.random()) 15 | :vector_mul_inplace(gen_area) 16 | end 17 | 18 | -- generate some circle objects 19 | local circles = functional.generate(20, function() 20 | return { 21 | -- circle geometry 22 | pos = random_point_in_area(), 23 | rad = love.math.random(5, 20), 24 | -- movement 25 | vel = vec2(love.math.randomNormal() * 40, love.math.randomNormal() * 40), 26 | } 27 | end) 28 | 29 | --antigrav aabb area 30 | local antigrav_pos = gen_area:copy() 31 | :scalar_mul_inplace(0.5) 32 | :scalar_add_inplace(50, 0) 33 | local antigrav_halfsize = vec2(30, 150) 34 | 35 | -- generate some random "world" lines 36 | local lines = functional.stitch( 37 | functional.generate(5, random_point_in_area), 38 | function(start_pos) 39 | local chain = {} 40 | local current_pos = start_pos 41 | local dir = vec2:polar(1, math.tau * love.math.random()) 42 | for i = 1, love.math.random(4, 7) do 43 | table.insert(chain, current_pos) 44 | current_pos = current_pos:fused_multiply_add(dir, love.math.random(30, 50)) 45 | dir = dir:rotate_inplace(love.math.randomNormal()) 46 | end 47 | chain = functional.cycle(chain, function(a, b) 48 | return {a, b} 49 | end) 50 | --break loop randomly, or if too far from start (likely to be a bad looking poly) 51 | local last_link = table.back(chain) 52 | if 53 | love.math.random() < 0.5 54 | or last_link[1]:distance(last_link[2]) > 100 55 | then 56 | table.remove(chain) 57 | end 58 | return chain 59 | end 60 | ) 61 | 62 | -- actual logic 63 | return { 64 | update = function(self, dt) 65 | --independent update 66 | for _, v in ipairs(circles) do 67 | -- integrate position 68 | v.pos:fused_multiply_add_inplace(v.vel, dt) 69 | 70 | --inside anti-grav area, float up 71 | v.antigrav = intersect.aabb_circle_overlap( 72 | antigrav_pos, antigrav_halfsize, 73 | v.pos, v.rad 74 | ) 75 | if v.antigrav then 76 | v.vel:scalar_add_inplace(0, -dt * 100) 77 | end 78 | 79 | -- pull inside world 80 | local push = 100 * dt 81 | if OUTPUT_SIZE then 82 | if v.pos.x < 0 then v.vel:scalar_add_inplace(push, 0) end 83 | if v.pos.y < 0 then v.vel:scalar_add_inplace(0, push) end 84 | if v.pos.x > OUTPUT_SIZE.x then v.vel:scalar_add_inplace(-push, 0) end 85 | if v.pos.y > OUTPUT_SIZE.y then v.vel:scalar_add_inplace(0, -push) end 86 | end 87 | end 88 | -- inter-dependent update: collide 89 | for i = 1, #circles do 90 | local a = circles[i] 91 | -- (reuse this vector) 92 | local separating_vector = vec2.pooled() 93 | --collide against other circles 94 | for j = i+1, #circles do 95 | local b = circles[j] 96 | if intersect.circle_circle_collide( 97 | a.pos, a.rad, 98 | b.pos, b.rad, 99 | separating_vector 100 | ) then 101 | --resolve collision 102 | intersect.resolve_msv(a.pos, b.pos, separating_vector) 103 | 104 | --reuse the separating vector as the normal 105 | local normal = separating_vector:normalise_inplace() 106 | 107 | --transfer velocities 108 | intersect.mutual_bounce(a.vel, b.vel, normal, 0.8) 109 | end 110 | end 111 | 112 | --collide against lines 113 | for _, line in ipairs(lines) do 114 | if intersect.line_circle_collide( 115 | line[1], line[2], 0, 116 | a.pos, a.rad, 117 | separating_vector 118 | ) then 119 | --resolve (we don't want to modify the line) 120 | a.pos:fused_multiply_add_inplace(separating_vector, -1) 121 | intersect.bounce_off(a.vel, separating_vector:normalise_inplace()) 122 | end 123 | end 124 | --clean up 125 | separating_vector:release() 126 | end 127 | end, 128 | draw = function(self) 129 | --antigrav area 130 | love.graphics.setColor(0.5, 0.25, 0.35) 131 | love.graphics.rectangle( 132 | "fill", 133 | antigrav_pos.x - antigrav_halfsize.x, 134 | antigrav_pos.y - antigrav_halfsize.y, 135 | antigrav_halfsize.x * 2, 136 | antigrav_halfsize.y * 2 137 | ) 138 | --draw all the circles 139 | for _, v in ipairs(circles) do 140 | if v.antigrav then 141 | love.graphics.setColor(1, 0.8, 0.8) 142 | else 143 | love.graphics.setColor(1, 1, 1) 144 | end 145 | love.graphics.circle("fill", v.pos.x, v.pos.y, v.rad) 146 | end 147 | --draw all the lines, a bit darker 148 | local g = 0.7 149 | love.graphics.setColor(g, g, g) 150 | for _, v in ipairs(lines) do 151 | love.graphics.line(v[1].x, v[1].y, v[2].x, v[2].y) 152 | end 153 | end, 154 | } 155 | -------------------------------------------------------------------------------- /class.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- classes and basic oop 3 | -- 4 | 5 | require("common") 6 | 7 | ------------------------------------------------------------------------------- 8 | heading("basic class") 9 | 10 | --the `class` function declares a new class 11 | local pair = class({ 12 | name = "pair", 13 | }) 14 | 15 | --the constructor is called `new` 16 | --it can take whatever arguments you need 17 | --and should be defined as a method using dot syntax 18 | function pair:new(a, b) 19 | self.first = a 20 | self.second = b 21 | end 22 | 23 | --methods can be declared on the class 24 | function pair:sum() 25 | return self.first + self.second 26 | end 27 | 28 | --new instances are constructed using a call to the class 29 | local odd = pair(1, 3) 30 | local even = pair(2, 4) 31 | 32 | --instances store their own properties 33 | print_var("odd", odd) 34 | print_var("even", even) 35 | 36 | --methods of the class can be called on instances 37 | print_var("odd sum", odd:sum()) 38 | print_var("even sum", even:sum()) 39 | 40 | ------------------------------------------------------------------------------- 41 | heading("inheritance") 42 | 43 | --a class that inherits from another is defined 44 | --by passing the superclass to the `class` function 45 | local fancy_pair = class({ 46 | name = "fancy pair", 47 | extends = pair 48 | }) 49 | 50 | --constructors work mostly the same, 51 | --except you need to call the super constructor somehow 52 | function fancy_pair:new(a, b) 53 | self:super(a, b) 54 | self.fancy = true 55 | end 56 | 57 | --new methods can be declared on the class 58 | --and they can use properties from the superclass 59 | function fancy_pair:product() 60 | return self.first * self.second 61 | end 62 | 63 | --instances are constructed the same way 64 | local fancy = fancy_pair(3, 5) 65 | print_var("fancy pair", fancy) 66 | --methods from the super class can be used 67 | print_var("fancy sum", fancy:sum()) 68 | --as well as the freshly extended methods 69 | print_var("fancy product", fancy:product()) 70 | 71 | ------------------------------------------------------------------------------- 72 | heading("mixins or interfaces") 73 | 74 | --whatever you know them by, classes can implement as many of them as needed 75 | --they are overlaid onto the class in the order provided 76 | --(ie the first interface takes precedence) 77 | 78 | --they can be proper classes 79 | local pair_add = class({ 80 | name = "pair_add", 81 | }) 82 | function pair_add:add(v) 83 | self.first = self.first + v 84 | self.second = self.second + v 85 | end 86 | 87 | --or just plain tables with function keys 88 | local pair_sub = {} 89 | function pair_sub:sub(v) 90 | self.first = self.first - v 91 | self.second = self.second - v 92 | end 93 | 94 | --a new class that extends pair and implements both interfaces 95 | local addsub_pair = class({ 96 | name = "addsub_pair", 97 | extends = pair, 98 | implements = {pair_add, pair_sub}, 99 | }) 100 | 101 | --(by default the constructor is just the super constructor, 102 | --so we don't need anything new) 103 | 104 | local p = addsub_pair(2, 5) 105 | print_var("p as constructed", p) 106 | p:add(2) 107 | print_var("p after add 2", p) 108 | p:sub(4) 109 | print_var("p after sub 4", p) 110 | 111 | ------------------------------------------------------------------------------- 112 | heading("dynamic typing stuff") 113 | 114 | --generally recommended against leveraging this too hard, but can be 115 | --a useful escape hatch sometimes 116 | 117 | --various membership cases: 118 | --base class is true 119 | print_var("p is pair", p:is(pair)) 120 | --concrete class is true 121 | print_var("p is pair_add", p:is(pair_add)) 122 | --sibling class is false 123 | print_var("p is fancy_pair", p:is(fancy_pair)) 124 | --implemented interfaces are true 125 | print_var("p is addsub_pair", p:is(addsub_pair)) 126 | 127 | -------------------------------------------------------------------------------- /common.lua: -------------------------------------------------------------------------------- 1 | --work with standalone luajit 2 | --which doesn't search the local path for init.lua style modules by default... 3 | if not love then 4 | package.path = "./?/init.lua;" .. package.path 5 | end 6 | 7 | --load batteries globally 8 | require("batteries"):export() 9 | 10 | --print a named variable, prettified 11 | function print_var(name, t) 12 | print(name, string.pretty(t, {indent = false})) 13 | end 14 | 15 | --print a heading with a fixed-width line surrounding it 16 | function heading(s) 17 | print(("-- %s %s"):format(s, ("-"):rep(50 - s:len()))) 18 | end 19 | -------------------------------------------------------------------------------- /conf.lua: -------------------------------------------------------------------------------- 1 | function love.conf(t) 2 | t.identity = "batteries_examples" 3 | t.window.title = "batteries examples" 4 | t.window.width = 1000 5 | t.window.height = 600 6 | t.gammacorrect = true 7 | end 8 | -------------------------------------------------------------------------------- /font/Licence.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2004, 2005 Tristan Grimmer 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. 8 | -------------------------------------------------------------------------------- /font/ProggyClean.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1bardesign/batteries-examples/2b033cc35a38728c30d04112802a08a00b1e04ae/font/ProggyClean.ttf -------------------------------------------------------------------------------- /functional.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- functional programming 3 | -- 4 | 5 | -- load helpers 6 | require("common") 7 | 8 | ------------------------------------------------------------------------------- 9 | heading("numeric data") 10 | -- we'll set up some example numeric data to demo the basics 11 | local numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10} 12 | print_var("numbers", numbers) 13 | 14 | ------------------------------------------------------------------------------- 15 | heading("map and filter") 16 | -- map and filter are the "go to" functional programming primitives 17 | -- map creates a new table with each member replaced with the result 18 | -- of a function call. 19 | 20 | -- lets map each value to its square 21 | print_var("squares", functional.map(numbers, function(v) 22 | return v * v 23 | end)) 24 | 25 | -- filter creates a new table containing only the elements for 26 | -- which the passed function returns true 27 | 28 | -- filter only the even elements 29 | print_var("even", functional.filter(numbers, function(v) 30 | return (v % 2) == 0 31 | end)) 32 | 33 | ---map tables by their keys into one sequence 34 | local tables = { 35 | {a = 1, b = 4}, 36 | {a = 2, b = 5}, 37 | {a = 3, b = 6}, 38 | } 39 | print_var("mapped_field", functional.map_field(tables, "b")) 40 | 41 | -- map objects by their function call results 42 | -- or map objects into a sequence from the result of a function 43 | function my_function(element, one, two) 44 | local x, y = element:unpack() 45 | return (one * x) * (two * y) 46 | end 47 | 48 | local vectors = { 49 | vec2(4, 20), 50 | vec2(6, 9) 51 | } 52 | print_var("mapped_seq_a", functional.map_call(vectors, "rotate", math.pi)) 53 | print_var("mapped_seq_b", functional.map_call(vectors, my_function, 1, 2)) 54 | 55 | ------------------------------------------------------------------------------- 56 | heading("aggregate") 57 | -- these functions work on the passed table as an aggregate, returning 58 | -- some information about the collection as a whole 59 | 60 | -- sum adds up all the elements 61 | local sum = functional.sum(numbers) 62 | print("sum", sum) 63 | 64 | -- mean gets the average of all the elements 65 | local mean = functional.mean(numbers) 66 | print("mean", mean) 67 | 68 | -- minmax gets the minimum and maximum in a single pass 69 | -- there are separate min and max functions if you just need one :) 70 | local min, max = functional.minmax(numbers) 71 | print("min", min, "max", max) 72 | 73 | -- all checks that all members pass a certain test 74 | print("all numbers", functional.all(numbers, function(v) 75 | return type(v) == "number" 76 | end)) 77 | -- any checks if any member passes the test 78 | print("any functions", functional.any(numbers, function(v) 79 | return type(v) == "function" 80 | end)) 81 | -- none checks that no member passes 82 | print("no strings", functional.none(numbers, function(v) 83 | return type(v) == "string" 84 | end)) 85 | -- count tallies up the number of matches; 86 | -- in this case, we count the multiples of 3 87 | print("count 3 * n", functional.count(numbers, function(v) 88 | return (v % 3) == 0 89 | end)) 90 | -- contains checks if a specific element is present 91 | print("contains 7", functional.contains(numbers, 7)) 92 | 93 | ------------------------------------------------------------------------------- 94 | heading("partition and zip") 95 | -- partition and zip are sort of the opposite of each other 96 | 97 | -- partition is a two part filter; 98 | -- those that pass the test are returned in one table, 99 | -- and those that dont are returned in another. 100 | 101 | -- zip takes two tables of the same length and combines the 102 | -- elements together into a single table. 103 | 104 | -- lets partition the numbers into halves, 105 | -- and then zip them together as a sum of each pair 106 | local bottom, top = functional.partition(numbers, function(v) 107 | return v <= mean 108 | end) 109 | print_var("bottom", bottom) 110 | print_var("top", top) 111 | print_var("zip", functional.zip(bottom, top, function(a, b) 112 | return a + b 113 | end)) 114 | 115 | ------------------------------------------------------------------------------- 116 | heading("table data") 117 | 118 | -- now we'll do some examples with more complex input data 119 | local seq_pairs = {{1, 2}, {3, 4}, {5, 6}, {7, 8}} 120 | print_var("seq pairs", seq_pairs) 121 | 122 | -- find_min and find_max can be used to perform a search on some data 123 | -- this can be very useful for performing things like nearest neighbour 124 | -- searches, or getting the most dangerous enemy out of those visible 125 | 126 | -- note that we're passing in functional.sum here as the function argument 127 | -- but any function that returns a numeric result works 128 | local sum = functional.sum 129 | 130 | print_var("pair min sum", functional.find_min(seq_pairs, sum)) 131 | print_var("pair max sum", functional.find_max(seq_pairs, sum)) 132 | 133 | -- find_nearest can be useful if you have a specific value in mind 134 | -- but often a find_min or find_max will result in clearer code 135 | print_var("sum nearest 10", functional.find_nearest(seq_pairs, sum, 10)) 136 | 137 | -- find_match can be used as a single-element filter; it can save 138 | -- creating another table if you only need one result 139 | print_var("second elem 8", functional.find_match(seq_pairs, function(v) 140 | return v[2] == 8 141 | end)) 142 | -- if no matching element exists, you get nil 143 | print_var("both even", functional.find_match(seq_pairs, function(v) 144 | return (v[1] % 2) == 0 145 | and (v[2] % 2) == 0 146 | end)) 147 | 148 | ------------------------------------------------------------------------------- 149 | -- heading("a little more complex") 150 | -- todo: a nice more complex "gamey" example that highlights the benefits of 151 | -- not mutating the input and the more self-documenting nature 152 | -- of the query functions 153 | 154 | -- that's it for now! 155 | -------------------------------------------------------------------------------- /main.lua: -------------------------------------------------------------------------------- 1 | require("common") 2 | 3 | -- index of examples to choose from 4 | local examples = { 5 | {"table", [[ 6 | table extension routines 7 | easy to understand shorthand for common operations 8 | randomisation 9 | ]]}, 10 | {"math", [[ 11 | mathematical extension routines 12 | clamp, lerp, rotations... 13 | ]]}, 14 | {"class", [[ 15 | basic object oriented programming 16 | classes and objects and inheritance 17 | ]]}, 18 | {"functional", [[ 19 | basic functional programming 20 | non-destructive - map/filter/reduce 21 | declarative code options - any/all/find_match 22 | ]]}, 23 | {"sequence", [[ 24 | oop method call sugar for sequential tables 25 | simpler functional programming 26 | less typing for you 27 | ]]}, 28 | {"2d_geom", [[ 29 | interactive 2d physics 30 | basic physics integration 31 | line and circle collision 32 | ]]}, 33 | {"set", [[ 34 | in/out sets 35 | set operations - union/intersection 36 | ]]}, 37 | {"pathfind", [[ 38 | A* pathfinding 39 | ]]}, 40 | {"quit", [[ 41 | bye! 42 | ]]}, 43 | } 44 | 45 | -- colours 46 | local heading_col = {colour.unpack_rgb(0xffffff)} 47 | local heading_bg_col = {colour.unpack_rgb(0x606060)} 48 | 49 | local code_col = {colour.unpack_rgb(0xffd0a0)} 50 | local comment_col = {colour.unpack_rgb(0xa08080)} 51 | local code_bg_col = {colour.unpack_rgb(0x403020)} 52 | 53 | local output_col = {colour.unpack_rgb(0xaaffcc)} 54 | local caret_col = {colour.unpack_rgb(0xffaacc)} 55 | local output_bg_col = {colour.unpack_rgb(0x204030)} 56 | 57 | local line_height = 16 58 | local margin = 10 59 | 60 | -- class for loading, running, and drawing the results of an example 61 | local example = class({ 62 | name = "example", 63 | }) 64 | function example:new(example_name) 65 | local src = {} 66 | for line in love.filesystem.lines(example_name .. ".lua") do 67 | table.insert(src, line) 68 | end 69 | local exe, err = loadstring(table.concat(src, "\n")) 70 | if exe == nil then 71 | print(("error loading %s, %s"):format(example_name, err)) 72 | return nil 73 | end 74 | 75 | local output = {} 76 | -- patch the global environment 77 | local gprint = print 78 | function print(...) 79 | table.insert(output, table.map({...}, tostring)) 80 | end 81 | 82 | local result = exe() 83 | 84 | print = gprint 85 | 86 | local output_producing_lines = {"print%(", "print_var%(", "heading%("} 87 | 88 | -- match print lines to source lines 89 | local print_lines = {} 90 | for i, v in ipairs(src) do 91 | if table.any(output_producing_lines, function(m) 92 | return v:match(m) ~= nil 93 | end) then 94 | print_lines[i] = table.remove(output, 1) 95 | end 96 | end 97 | 98 | --todo: parse comments here instead of at draw time so we can properly handle multiline comments, and don't repeat work 99 | 100 | self.name = example_name 101 | self.src = src 102 | self.print_lines = print_lines 103 | self.offset = 1 104 | self.result = result or {} 105 | end 106 | 107 | function example:scroll(amount) 108 | -- shift enables faster scrolling 109 | local multiplier = love.keyboard.isDown("lshift") and 3 or 1 110 | amount = amount * multiplier 111 | -- clamp scroll within the file source 112 | self.offset = math.clamp(self.offset + amount, 1, #self.src) 113 | end 114 | 115 | function example:draw() 116 | local code_x = margin 117 | local output_x = 570 118 | 119 | local total_width_available = love.graphics.getWidth() - margin * 2 120 | local height_available = love.graphics.getHeight() - margin * 2 121 | 122 | love.graphics.push("all") 123 | 124 | -- headings 125 | love.graphics.translate(margin, margin) 126 | height_available = height_available - margin 127 | love.graphics.setColor(heading_bg_col) 128 | love.graphics.rectangle("fill", 0, 0, total_width_available, line_height * 2 + margin * 2) 129 | 130 | love.graphics.translate(0, margin) 131 | love.graphics.setColor(heading_col) 132 | love.graphics.print("example: "..self.name, code_x, 0) 133 | height_available = height_available - line_height 134 | 135 | love.graphics.translate(0, line_height) 136 | love.graphics.print("code:", code_x, 0) 137 | love.graphics.print("output:", output_x, 0) 138 | love.graphics.translate(0, line_height + margin) 139 | height_available = height_available - (line_height + margin) 140 | 141 | love.graphics.setColor(code_bg_col) 142 | 143 | love.graphics.rectangle("fill", code_x - margin, 0, output_x - code_x + margin, height_available) 144 | love.graphics.setColor(output_bg_col) 145 | love.graphics.rectangle("fill", output_x, 0, total_width_available - output_x, height_available) 146 | 147 | love.graphics.translate(0, margin / 2) 148 | local line_count = math.floor(height_available / line_height) - 1 149 | for line = 0, line_count - 1 do 150 | local i = self.offset + line 151 | local v = self.src[i] 152 | if v then 153 | --single line comment 154 | local is_comment = v:match("^%s*%-%-") ~= nil 155 | love.graphics.setColor(is_comment and comment_col or code_col) 156 | love.graphics.print(v, code_x, 0) 157 | end 158 | local print_line = self.print_lines[i] 159 | if print_line then 160 | love.graphics.setColor(caret_col) 161 | love.graphics.print(">", output_x - 16, 0) 162 | love.graphics.setColor(output_col) 163 | local output_w = total_width_available - output_x - margin * 2 164 | local heading_w = (#print_line == 1) and output_w or 120 165 | local elem_w = (output_w - heading_w) / math.max(1, #print_line - 1) 166 | local x_pos = output_x + 8 167 | for j, p in ipairs(print_line) do 168 | local current_w = (j == 1) and heading_w or elem_w 169 | love.graphics.printf(p, x_pos, 0, current_w, "left") 170 | x_pos = x_pos + current_w 171 | end 172 | end 173 | love.graphics.translate(0, line_height) 174 | end 175 | 176 | love.graphics.pop() 177 | 178 | if self.result.draw then 179 | love.graphics.push("all") 180 | local x = margin + output_x 181 | local y = margin * 3 + line_height * 2 182 | local w = total_width_available - output_x 183 | local h = height_available 184 | OUTPUT_SIZE = vec2(w, h) 185 | 186 | love.graphics.setScissor(x, y, w, h) 187 | love.graphics.translate(x, y) 188 | self.result:draw() 189 | love.graphics.pop() 190 | end 191 | end 192 | 193 | function example:update(dt) 194 | if self.result.update then 195 | return self.result:update(dt) 196 | end 197 | end 198 | 199 | function example:keypressed(k) 200 | if self.result.keypressed then 201 | return self.result:keypressed(k) 202 | end 203 | -- arrows 204 | if k == "up" then 205 | self:scroll(-1) 206 | end 207 | if k == "down" then 208 | self:scroll(1) 209 | end 210 | -- pgup/dn 211 | local page_amount = 16 212 | if k == "pageup" then 213 | self:scroll(-10) 214 | end 215 | if k == "pagedown" then 216 | self:scroll(10) 217 | end 218 | end 219 | 220 | local current 221 | 222 | local menu = { 223 | selected = 1, 224 | options = functional.map(examples, function(v) 225 | v[2] = v[2]:deindent():split("\n") 226 | return v 227 | end), 228 | help = ([[ 229 | hi there! 230 | 231 | thank you for checking out the examples, 232 | i hope they help you understand how to use batteries 233 | 234 | [arrow keys] navigate 235 | [space] or [return] select 236 | [escape] go back 237 | ]]):deindent():split("\n") 238 | } 239 | 240 | function menu:scroll(amount) 241 | self.selected = math.floor(math.wrap(self.selected + amount, 1, #self.options + 1)) 242 | end 243 | 244 | function menu:keypressed(k) 245 | if k == "up" then 246 | self:scroll(-1) 247 | end 248 | if k == "down" then 249 | self:scroll(1) 250 | end 251 | if k == "space" or k == "return" then 252 | local name = self.options[self.selected][1] 253 | if name == "quit" then 254 | return love.event.quit() 255 | end 256 | current = example(name) 257 | end 258 | end 259 | 260 | function menu:draw() 261 | love.graphics.push("all") 262 | local w = 520 263 | love.graphics.translate(margin, margin) 264 | 265 | -- draw menu 266 | love.graphics.push() 267 | 268 | love.graphics.setColor(heading_bg_col) 269 | love.graphics.rectangle("fill", 0, 0, w, line_height + margin) 270 | love.graphics.setColor(heading_col) 271 | love.graphics.print("select an example:", margin, margin / 2) 272 | 273 | love.graphics.translate(0, line_height + margin) 274 | love.graphics.setColor(code_bg_col) 275 | love.graphics.rectangle("fill", 0, 0, w, line_height * #self.options + margin * 2) 276 | love.graphics.translate(margin, margin) 277 | for i, v in ipairs(self.options) do 278 | if i == self.selected then 279 | love.graphics.setColor(caret_col) 280 | love.graphics.print(">", 0, 0) 281 | love.graphics.push() 282 | love.graphics.setColor(comment_col) 283 | love.graphics.translate(130, 0) 284 | for _, v in ipairs(v[2]) do 285 | love.graphics.print(v) 286 | love.graphics.translate(0, line_height) 287 | end 288 | love.graphics.pop() 289 | end 290 | love.graphics.setColor(code_col) 291 | love.graphics.print(v[1], margin, 0) 292 | love.graphics.translate(0, line_height) 293 | end 294 | 295 | love.graphics.pop() 296 | 297 | -- draw help 298 | love.graphics.push() 299 | 300 | w = 400 301 | local box_height = line_height * #self.help + margin * 2 302 | 303 | love.graphics.translate(0, love.graphics.getHeight() - box_height - line_height - margin * 3) 304 | love.graphics.setColor(heading_bg_col) 305 | love.graphics.rectangle("fill", 0, 0, w, line_height + margin) 306 | love.graphics.setColor(heading_col) 307 | love.graphics.print("help:", margin, margin / 2) 308 | 309 | love.graphics.translate(0, line_height + margin) 310 | love.graphics.setColor(code_bg_col) 311 | love.graphics.rectangle("fill", 0, 0, w, box_height) 312 | love.graphics.setColor(code_col) 313 | love.graphics.translate(margin, margin) 314 | for i, v in ipairs(self.help) do 315 | love.graphics.print(v, margin, 0) 316 | love.graphics.translate(0, line_height) 317 | end 318 | 319 | love.graphics.pop() 320 | 321 | love.graphics.pop() 322 | end 323 | 324 | function love.load() 325 | love.keyboard.setKeyRepeat(true) 326 | local font = love.graphics.newFont("font/ProggyClean.ttf", 16) 327 | love.graphics.setFont(font) 328 | end 329 | 330 | function love.update(dt) 331 | if current then 332 | current:update(dt) 333 | end 334 | end 335 | 336 | function love.draw() 337 | if current then 338 | current:draw() 339 | else 340 | menu:draw() 341 | end 342 | end 343 | 344 | function love.keypressed(k) 345 | -- toggle back 346 | if k == "escape" then 347 | if current then 348 | current = nil 349 | else 350 | love.event.quit() 351 | end 352 | return 353 | end 354 | -- quit or restart 355 | if love.keyboard.isDown("lctrl") then 356 | if k == "r" then 357 | return love.event.quit("restart") 358 | elseif k == "q" then 359 | return love.event.quit() 360 | end 361 | end 362 | -- reload current 363 | if k == "r" and love.keyboard.isDown("lshift") then 364 | current = example(current.name) 365 | end 366 | -- pass through 367 | if current then 368 | current:keypressed(k) 369 | else 370 | menu:keypressed(k) 371 | end 372 | end 373 | -------------------------------------------------------------------------------- /math.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- math extension 3 | -- 4 | 5 | require("common") 6 | 7 | ------------------------------------------------------------------------------- 8 | heading("wrap") 9 | 10 | -- wrap turns a linear value into a value on the interval [lo, hi) 11 | -- which can be very useful for things like wrapping maps and the like 12 | local pre_wrap = 27 13 | print("pre wrap", pre_wrap) 14 | print("wrapped 0, 10", math.wrap(pre_wrap, 0, 10)) 15 | print("wrapped -20, 20", math.wrap(pre_wrap, -20, 20)) 16 | 17 | -- you can also wrap indices onto sequential tables 18 | local t = {1, 2, 3, 4, 5} 19 | print_var("t", t) 20 | print("wrapped index", math.wrap_index(pre_wrap, t)) 21 | 22 | ------------------------------------------------------------------------------- 23 | heading("clamp") 24 | 25 | -- clamp lets you ensure a number is on an interval, with it being set to 26 | -- either the low or high end if it's too high or two low respectively 27 | -- (this is somehow much easier to get right first time than math.max/min!) 28 | local pre_clamp = 13.2 29 | print("pre clamp", pre_clamp) 30 | print("clamped 0, 10", math.clamp(pre_clamp, 0, 10)) 31 | print("clamped 20, 30", math.clamp(pre_clamp, 20, 30)) 32 | 33 | -- clamp01 is available as a shorthand for clamping something on the interval 34 | -- from 0 to 1, such as for colours or lerp factors or whatever 35 | pre_clamp = {-0.5, 0.0, 0.5, 1.0, 1.5} 36 | print_var("pre clamp", pre_clamp) 37 | print_var("clamp01", table.map(pre_clamp, math.clamp01)) 38 | 39 | ------------------------------------------------------------------------------- 40 | heading("lerp") 41 | 42 | -- speaking of lerp, lets learn about that 43 | -- lerp is a common shortening of "linear interpolation", which 44 | -- is maths-speak for going straight from one value to another based 45 | -- on a factor between zero and one 46 | 47 | -- lets set up some data to demo this 48 | local a, b = 5, 10 49 | print("a", a) 50 | print("b", b) 51 | 52 | -- if the factor is zero, the first value should be returned 53 | -- if the factor is one, the second should be returned 54 | print("f = 0", math.lerp(a, b, 0)) 55 | print("f = 1", math.lerp(a, b, 1)) 56 | 57 | -- if the factor is 0.5, it should be halfway between both values 58 | print("f = 0.5", math.lerp(a, b, 0.5)) 59 | 60 | -- this works for any factor between zero and one 61 | print("f = 0.1", math.lerp(a, b, 0.1)) 62 | 63 | -- and extrapolates linearly beyond that 64 | print("f = -1", math.lerp(a, b, -1)) 65 | print("f = 2", math.lerp(a, b, 2)) 66 | 67 | ------------------------------------------------------------------------------- 68 | heading("smoothstep") 69 | -- lerp is commonly used to animate things procedurally 70 | -- and to make transitions smoother, you can use easing functions on the 71 | -- interpolation factor, to transform things from a linear blend to 72 | -- a non-linear blend. 73 | local factors = {0, 0.25, 0.5, 0.75, 1.0} 74 | print_var("factors", factors) 75 | print_var("plain lerp", table.map(factors, function(f) 76 | return math.lerp(a, b, f) 77 | end)) 78 | -- a common interpolant smoothing function is smoothstep, which goes through 79 | -- the same points as usual at 0, 0.5 and 1, but ramps up and down "smoothly" 80 | print_var("smoothstep", table.map(factors, function(f) 81 | return math.lerp(a, b, math.smoothstep(f)) 82 | end)) 83 | -- smootherstep is a slightly more expensive extension to that which 84 | -- has slightly better properties for chained animations 85 | print_var("smootherstep", table.map(factors, function(f) 86 | return math.lerp(a, b, math.smootherstep(f)) 87 | end)) 88 | 89 | -- you can read more about these online :) 90 | -- batteries provides an implementation of both 91 | 92 | ------------------------------------------------------------------------------- 93 | heading("angle handling") 94 | 95 | -- wrap any angle onto the interval [-math.pi, math.pi) 96 | -- useful for not getting extremely high angles 97 | local big_angle = 5.5 98 | print("big angle", big_angle) 99 | print("normalised", math.normalise_angle(big_angle)) 100 | 101 | -- get the difference between two angles 102 | -- neither required to be normalised, result is normalised 103 | local a1 = 0.9 * math.tau 104 | local a2 = 0.1 * math.tau 105 | print("a1", a1) 106 | print("a2", a2) 107 | local dif = math.angle_difference(a1, a2) 108 | print("angle difference", dif) 109 | -- direction is from the first angle to the second; 110 | -- ie you can add the difference to the first to get the second 111 | -- (or at least, the same position on the circle) 112 | print("a1 + difference", math.normalise_angle(a1 + dif)) 113 | 114 | -- lerp between angles, taking the shortest path around the circle 115 | print("angle lerp", math.lerp_angle(a1, a2, 0.5)) 116 | -- this is needed because a straight lerp might take the wrong path! 117 | print("wrong lerp", math.lerp(a1, a2, 0.5)) 118 | 119 | -- geometric rotation multi-return is provided, but seriously consider 120 | -- using vec2 instead of separate coordinates where you can! 121 | -- quarter turn 122 | local ox, oy = 1, 2 123 | print("original point", ox, oy) 124 | local rx, ry = math.rotate(ox, oy, math.tau * 0.25) 125 | print("quarter turn", rx, ry) 126 | -------------------------------------------------------------------------------- /pathfind.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- pathfinding example 3 | -- 4 | 5 | -- load helpers 6 | require("common") 7 | 8 | ------------------------------------------------------------------------------- 9 | --generate some nodes 10 | local nodes = {} 11 | for x = 1, 5 do 12 | for y = 1, 5 do 13 | table.insert(nodes, vec2(x, y)) 14 | end 15 | end 16 | 17 | ------------------------------------------------------------------------------- 18 | --lets find from the top left corner to the bottom right corner 19 | local a = table.front(nodes) 20 | local b = table.back(nodes) 21 | print_var("a", a) 22 | print_var("b", b) 23 | 24 | local path = pathfind { 25 | start = a, 26 | is_goal = function(v) return v == b end, 27 | neighbours = function(n) 28 | --find all the neighbour nodes within a distance of 2 units 29 | --note: for real code you should definitely cache this or do it some faster way :) 30 | return functional.filter(nodes, function(v) 31 | return v:distance(n) < 2 32 | end) 33 | end, 34 | distance = vec2.distance, 35 | heuristic = function(v) return v:distance(b) end, 36 | } 37 | 38 | print_var("path", path) 39 | 40 | --todo: interactive example 41 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # ⚡ Examples for `batteries` 2 | 3 | A collection of example usage scripts for [batteries](https://github.com/1bardesign/batteries). 4 | 5 | Kept separate from the main `batteries` repo to avoid cluttering the commit history and bloating the repo size. 6 | 7 | Comes with a [love](https://love2d.org)-compatible `main.lua` which allows browsing the source and output interactively. 8 | 9 | # Index of Examples 10 | 11 | - `table.lua` - table api extensions 12 | - `math.lua` - math api extensions. 13 | - `class.lua` - simple object oriented basics 14 | - `functional.lua` - functional programming facilities 15 | - `sequence.lua` - oop sugar for anything sequential 16 | - `2d_geom.lua` - basic colliding and overlapping entities using `intersect` and `vec2`; requires love. 17 | - `set.lua` - in/out sets 18 | 19 | # TODO 20 | 21 | - `state_boring.lua` - barebones state machine example. 22 | - `stable_sort.lua` - show the benefit of a stable sort for sprite z sorting; requires love. 23 | - `state_ai.lua` - visualised state machine ai for a sparring partner; requires love. 24 | - `colour.lua` - various colour routines; requires love. 25 | - `benchmark.lua` - benchmark various functionality interactively; requires love. 26 | 27 | # Library Culture and Contributing 28 | 29 | As with batteries - I'm open to collaboration and happy to talk through issues or shortcomings. 30 | 31 | Pull Requests with new demos are welcome, though it will be required that they work with the rest of the example framework. 32 | 33 | Fixes and enhancements to existing demos are also great and will generally be merged optimistically. 34 | -------------------------------------------------------------------------------- /sequence.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- sequence - simple oop wrapper on ordered tables, with method chaining 3 | -- 4 | 5 | require("common") 6 | 7 | ------------------------------------------------------------------------------- 8 | heading("seamless") 9 | 10 | -- everything in table and tablex work basically out of the box 11 | -- here is the stack and queue section from the table example 12 | -- translated, with comments stripped - looking at both 13 | -- might help to see how things map 14 | 15 | local t = sequence{1, 2, 3, 4, 5} 16 | print_var("t", t) 17 | 18 | print("front", t:front()) 19 | print("back", t:back()) 20 | 21 | v = t:pop() 22 | print("popped", v) 23 | print_var("after pop", t) 24 | t:push(v) 25 | print_var("after push", t) 26 | 27 | v = t:shift() 28 | print("shifted", v) 29 | print_var("after shift", t) 30 | t:push(v) 31 | print_var("after push", t) 32 | 33 | t:unshift(t:pop()) 34 | print_var("replace at front", t) 35 | 36 | ------------------------------------------------------------------------------- 37 | heading("functional and type preserving") 38 | 39 | -- anything sequential returned from a sequence method is also a sequence 40 | -- allows method chaining for a sequentially readable functional approach 41 | -- not for use in some hot inner loop, but leads to very readable code 42 | 43 | local doubled_and_squared = t:map(function(v) 44 | return v * 2 45 | end):map(function(v) 46 | return v * v 47 | end) 48 | print_var("doubled, squared", doubled_and_squared) 49 | 50 | local min, max = doubled_and_squared:minmax() 51 | print("min, max", min, max) 52 | 53 | --can be used to upgrade some existing table transparently too 54 | --(generate data here, but it could come from anywhere and look like anything) 55 | local preexisting = functional.generate(10, function() 56 | return {length = love.math.random() * 100} 57 | end) 58 | 59 | --find the mean length 60 | local mean_length = 61 | --convert 62 | sequence(preexisting) 63 | --map values 64 | :map(function(v) 65 | return v.length 66 | end) 67 | --get the mean 68 | :mean() 69 | 70 | print("mean length", mean_length) 71 | -------------------------------------------------------------------------------- /set.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- set example 3 | -- 4 | 5 | -- load helpers 6 | require("common") 7 | 8 | ------------------------------------------------------------------------------- 9 | heading("working sets") 10 | local a = set({1, 2, 3,}) 11 | local b = set({3, 4, 5,}) 12 | print_var("a", a:values()) 13 | print_var("b", b:values()) 14 | 15 | ------------------------------------------------------------------------------- 16 | heading("set ops") 17 | 18 | print_var("union", a:union(b):values()) 19 | print_var("intersection", a:intersection(b):values()) 20 | print_var("complement", a:complement(b):values()) 21 | print_var("xor", a:symmetric_difference(b):values()) 22 | 23 | ------------------------------------------------------------------------------- 24 | heading("checking membership") 25 | 26 | print_var("a has 1", a:has(1)) 27 | print_var("a has 5", a:has(5)) 28 | print_var("b has 5", b:has(5)) 29 | 30 | ------------------------------------------------------------------------------- 31 | heading("modifying") 32 | 33 | local c = a:copy() 34 | print_var("copy", c:values()) 35 | 36 | print_var("add", c:add(6):values()) 37 | print_var("remove", c:remove(6):values()) 38 | 39 | print_var("add_set", c:add_set(b):values()) 40 | print_var("subtract_set", c:subtract_set(a):values()) 41 | -------------------------------------------------------------------------------- /table.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- table extension routines 3 | -- 4 | 5 | require("common") 6 | 7 | -- (temp storage) 8 | local v 9 | 10 | ------------------------------------------------------------------------------- 11 | heading("stack and queue") 12 | 13 | local t = {1, 2, 3, 4, 5} 14 | print_var("t", t) 15 | 16 | -- front and back can be slightly clearer than directly indexing 17 | -- (moreso for back than front, for variables with long names) 18 | print("front", table.front(t)) 19 | print("back", table.back(t)) 20 | 21 | -- push/pop and shift/unshift are like in many other languages (eg js), 22 | -- allowing queue and stack operation 23 | 24 | -- push and pop can be used for stack operation, adding and removing values 25 | -- at the back of the table in a "last-in, first-out" order 26 | v = table.pop(t) 27 | print("popped", v) 28 | print_var("after pop", t) 29 | table.push(t, v) 30 | print_var("after push", t) 31 | 32 | -- push and shift can be used for queue operation; "first-in, first-out" 33 | 34 | v = table.shift(t) 35 | print("shifted", v) 36 | print_var("after shift", t) 37 | table.push(t, v) 38 | print_var("after push", t) 39 | 40 | -- unshift is a push at the front of the table, allowing fully 41 | -- double-ended interaction 42 | table.unshift(t, table.pop(t, v)) 43 | print_var("replace at front", t) 44 | 45 | ------------------------------------------------------------------------------- 46 | heading("collection management") 47 | 48 | -- index_of can be used to find the index of a value 49 | print("4 at", table.index_of(t, 4)) 50 | -- or nil if the value is not present 51 | print("'monkey' at", tostring(table.index_of(t, "monkey"))) 52 | -- key_of does the same for non-sequential tables 53 | print("key search", table.key_of({ 54 | a = 1, 55 | b = 2, 56 | }, 1)) 57 | 58 | -- add_value and remove_value avoid doubling up values in sequential tables 59 | -- and allow removing by a value rather than index (including references, 60 | -- such as to an ai target or inventory item) 61 | -- they return whether anything was actually added or removed 62 | print_var("state before", t) 63 | print("add 5?", table.add_value(t, 5)) 64 | print_var("failed add", t) 65 | print("remove 5?", table.remove_value(t, 5)) 66 | print_var("after remove", t) 67 | print("remove 5 again?", table.remove_value(t, 5)) 68 | print_var("failed remove", t) 69 | print("add 5 again?", table.add_value(t, 5)) 70 | print_var("after re-add", t) 71 | 72 | -- careful; they perform a scan (ipairs) over the whole table though so 73 | -- should not be used too heavily with big tables! 74 | 75 | ------------------------------------------------------------------------------- 76 | heading("randomisation") 77 | 78 | t = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10} 79 | -- pick_random can choose a random variable from a table 80 | -- prefers love.math.random over math.random but works with both 81 | -- (results here will change with each run) 82 | print_var("random pick", table.pick_random(t)) 83 | print_var("random pick", table.pick_random(t)) 84 | print_var("random pick", table.pick_random(t)) 85 | print_var("random pick", table.pick_random(t)) 86 | 87 | -- you may like to pass in a random generator to get deterministic results 88 | -- for example in a world generator or for network sync or whatever 89 | -- just pass it into the call and it'll be used 90 | -- (anything that supports gen:random(min, max) will work) 91 | local seed = 1234 92 | local gen = love.math.newRandomGenerator(seed) 93 | print_var("deterministic", table.pick_random(t, gen)) 94 | 95 | -- shuffle does what it says on the tin 96 | -- it also takes an optional random generator with the same requirements 97 | -- as pick_random; a call is made for each element of the table in both cases 98 | print_var("shuffle order", table.shuffle(t)) 99 | 100 | -- todo: 101 | -- table.reverse(t) 102 | -- table.keys(t) 103 | -- table.values(t) 104 | -- table.append_inplace(t1, t2) 105 | -- table.append(t1, t2) 106 | -- table.dedupe(t) 107 | -- table.clear(t) + explain luajit differences but why it's supported 108 | -- table.copy(t, deep_or_into) + explain dissatisfaction 109 | -- table.overlay(to, from) 110 | -- table.unpack2(t) and friends + explain why 111 | --------------------------------------------------------------------------------