From 3239d6ac95a4bd95a511b66e421d451d92c60135 Mon Sep 17 00:00:00 2001 From: Sky Johnson Date: Thu, 24 Jul 2025 17:24:54 -0500 Subject: [PATCH] rewrite math module to global, improve test suite --- modules/math+/math.lua | 732 ++++++++++++++++++++++++++++++++++++ modules/math/math.go | 57 --- modules/math/math.lua | 817 ----------------------------------------- modules/registry.go | 2 - tests/math.lua | 448 ++++++++++++++++------ tests/tests.lua | 10 + 6 files changed, 1077 insertions(+), 989 deletions(-) create mode 100644 modules/math+/math.lua delete mode 100644 modules/math/math.go delete mode 100644 modules/math/math.lua diff --git a/modules/math+/math.lua b/modules/math+/math.lua new file mode 100644 index 0000000..57f7f17 --- /dev/null +++ b/modules/math+/math.lua @@ -0,0 +1,732 @@ +-- ====================================================================== +-- ENHANCED CONSTANTS (higher precision) +-- ====================================================================== + +math.pi = 3.14159265358979323846 -- Replace with higher precision +math.tau = 6.28318530717958647693 -- 2*pi, useful for full rotations +math.e = 2.71828182845904523536 +math.phi = 1.61803398874989484820 -- Golden ratio (1 + sqrt(5)) / 2 +math.sqrt2 = 1.41421356237309504880 +math.sqrt3 = 1.73205080756887729353 +math.ln2 = 0.69314718055994530942 -- Natural log of 2 +math.ln10 = 2.30258509299404568402 -- Natural log of 10 +math.infinity = 1/0 +math.nan = 0/0 + +-- ====================================================================== +-- EXTENDED FUNCTIONS +-- ====================================================================== + +-- Cube root that handles negative numbers correctly +function math.cbrt(x) + return x < 0 and -(-x)^(1/3) or x^(1/3) +end + +-- Euclidean distance - more accurate than sqrt(x*x + y*y) +function math.hypot(x, y) + return math.sqrt(x * x + y * y) +end + +-- IEEE 754 NaN check +function math.isnan(x) + return x ~= x +end + +-- Check if number is finite +function math.isfinite(x) + return x > -math.infinity and x < math.infinity +end + +-- Mathematical sign function +function math.sign(x) + return x > 0 and 1 or (x < 0 and -1 or 0) +end + +-- Constrain value to range +function math.clamp(x, min, max) + return x < min and min or (x > max and max or x) +end + +-- Linear interpolation +function math.lerp(a, b, t) + return a + (b - a) * t +end + +-- Smooth interpolation using Hermite polynomial +function math.smoothstep(a, b, t) + t = math.clamp((t - a) / (b - a), 0, 1) + return t * t * (3 - 2 * t) +end + +-- Map value from input range to output range +function math.map(x, in_min, in_max, out_min, out_max) + return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min +end + +-- Round to nearest integer +function math.round(x) + return x >= 0 and math.floor(x + 0.5) or math.ceil(x - 0.5) +end + +-- Round to specified decimal places +function math.roundto(x, decimals) + local mult = 10 ^ (decimals or 0) + return math.floor(x * mult + 0.5) / mult +end + +-- Normalize angle to [-π, π] range +function math.normalize_angle(angle) + return angle - 2 * math.pi * math.floor((angle + math.pi) / (2 * math.pi)) +end + +-- 2D Euclidean distance +function math.distance(x1, y1, x2, y2) + local dx, dy = x2 - x1, y2 - y1 + return math.sqrt(dx * dx + dy * dy) +end + +-- Factorial with bounds checking +function math.factorial(n) + if n < 0 or n ~= math.floor(n) or n > 170 then + return nil + end + local result = 1 + for i = 2, n do + result = result * i + end + return result +end + +-- Greatest common divisor using Euclidean algorithm +function math.gcd(a, b) + a, b = math.floor(math.abs(a)), math.floor(math.abs(b)) + while b ~= 0 do + a, b = b, a % b + end + return a +end + +-- Least common multiple +function math.lcm(a, b) + if a == 0 or b == 0 then return 0 end + return math.abs(a * b) / math.gcd(a, b) +end + +-- ====================================================================== +-- ENHANCED RANDOM FUNCTIONS +-- ====================================================================== + +-- Random float in range +function math.randomf(min, max) + if not min and not max then + return math.random() + elseif not max then + max = min + min = 0 + end + return min + math.random() * (max - min) +end + +-- Random integer in range +function math.randint(min, max) + if not max then + max = min + min = 1 + end + return math.floor(math.random() * (max - min + 1) + min) +end + +-- Random boolean with probability +function math.randboolean(p) + p = p or 0.5 + return math.random() < p +end + +-- ====================================================================== +-- STATISTICS FUNCTIONS +-- ====================================================================== + +function math.sum(t) + if type(t) ~= "table" then return 0 end + local sum = 0 + for i=1, #t do + if type(t[i]) == "number" then + sum = sum + t[i] + end + end + return sum +end + +function math.mean(t) + if type(t) ~= "table" or #t == 0 then return 0 end + local sum = 0 + local count = 0 + for i=1, #t do + if type(t[i]) == "number" then + sum = sum + t[i] + count = count + 1 + end + end + return count > 0 and sum / count or 0 +end + +function math.median(t) + if type(t) ~= "table" or #t == 0 then return 0 end + local nums = {} + local count = 0 + for i=1, #t do + if type(t[i]) == "number" then + count = count + 1 + nums[count] = t[i] + end + end + if count == 0 then return 0 end + table.sort(nums) + if count % 2 == 0 then + return (nums[count/2] + nums[count/2 + 1]) / 2 + else + return nums[math.ceil(count/2)] + end +end + +function math.variance(t) + if type(t) ~= "table" then return 0 end + local count = 0 + local m = math.mean(t) + local sum = 0 + for i=1, #t do + if type(t[i]) == "number" then + local dev = t[i] - m + sum = sum + dev * dev + count = count + 1 + end + end + return count > 1 and sum / count or 0 +end + +function math.stdev(t) + return math.sqrt(math.variance(t)) +end + +function math.pvariance(t) + if type(t) ~= "table" then return 0 end + local count = 0 + local m = math.mean(t) + local sum = 0 + for i=1, #t do + if type(t[i]) == "number" then + local dev = t[i] - m + sum = sum + dev * dev + count = count + 1 + end + end + return count > 0 and sum / count or 0 +end + +function math.pstdev(t) + return math.sqrt(math.pvariance(t)) +end + +function math.mode(t) + if type(t) ~= "table" or #t == 0 then return nil end + local counts = {} + local most_frequent = nil + local max_count = 0 + for i=1, #t do + local v = t[i] + counts[v] = (counts[v] or 0) + 1 + if counts[v] > max_count then + max_count = counts[v] + most_frequent = v + end + end + return most_frequent +end + +function math.minmax(t) + if type(t) ~= "table" or #t == 0 then return nil, nil end + local min, max + for i=1, #t do + if type(t[i]) == "number" then + min = t[i] + max = t[i] + break + end + end + if min == nil then return nil, nil end + for i=1, #t do + if type(t[i]) == "number" then + if t[i] < min then min = t[i] end + if t[i] > max then max = t[i] end + end + end + return min, max +end + +-- ====================================================================== +-- 2D VECTOR OPERATIONS +-- ====================================================================== + +math.vec2 = { + new = function(x, y) + return {x = x or 0, y = y or 0} + end, + + copy = function(v) + return {x = v.x, y = v.y} + end, + + add = function(a, b) + return {x = a.x + b.x, y = a.y + b.y} + end, + + sub = function(a, b) + return {x = a.x - b.x, y = a.y - b.y} + end, + + mul = function(a, b) + if type(b) == "number" then + return {x = a.x * b, y = a.y * b} + end + return {x = a.x * b.x, y = a.y * b.y} + end, + + div = function(a, b) + if type(b) == "number" then + local inv = 1 / b + return {x = a.x * inv, y = a.y * inv} + end + return {x = a.x / b.x, y = a.y / b.y} + end, + + dot = function(a, b) + return a.x * b.x + a.y * b.y + end, + + length = function(v) + return math.sqrt(v.x * v.x + v.y * v.y) + end, + + length_squared = function(v) + return v.x * v.x + v.y * v.y + end, + + distance = function(a, b) + local dx, dy = b.x - a.x, b.y - a.y + return math.sqrt(dx * dx + dy * dy) + end, + + distance_squared = function(a, b) + local dx, dy = b.x - a.x, b.y - a.y + return dx * dx + dy * dy + end, + + normalize = function(v) + local len = math.sqrt(v.x * v.x + v.y * v.y) + if len > 1e-10 then + local inv_len = 1 / len + return {x = v.x * inv_len, y = v.y * inv_len} + end + return {x = 0, y = 0} + end, + + rotate = function(v, angle) + local c, s = math.cos(angle), math.sin(angle) + return { + x = v.x * c - v.y * s, + y = v.x * s + v.y * c + } + end, + + angle = function(v) + return math.atan2(v.y, v.x) + end, + + lerp = function(a, b, t) + t = math.clamp(t, 0, 1) + return { + x = a.x + (b.x - a.x) * t, + y = a.y + (b.y - a.y) * t + } + end, + + reflect = function(v, normal) + local dot = v.x * normal.x + v.y * normal.y + return { + x = v.x - 2 * dot * normal.x, + y = v.y - 2 * dot * normal.y + } + end +} + +-- ====================================================================== +-- 3D VECTOR OPERATIONS +-- ====================================================================== + +math.vec3 = { + new = function(x, y, z) + return {x = x or 0, y = y or 0, z = z or 0} + end, + + copy = function(v) + return {x = v.x, y = v.y, z = v.z} + end, + + add = function(a, b) + return {x = a.x + b.x, y = a.y + b.y, z = a.z + b.z} + end, + + sub = function(a, b) + return {x = a.x - b.x, y = a.y - b.y, z = a.z - b.z} + end, + + mul = function(a, b) + if type(b) == "number" then + return {x = a.x * b, y = a.y * b, z = a.z * b} + end + return {x = a.x * b.x, y = a.y * b.y, z = a.z * b.z} + end, + + div = function(a, b) + if type(b) == "number" then + local inv = 1 / b + return {x = a.x * inv, y = a.y * inv, z = a.z * inv} + end + return {x = a.x / b.x, y = a.y / b.y, z = a.z / b.z} + end, + + dot = function(a, b) + return a.x * b.x + a.y * b.y + a.z * b.z + end, + + cross = function(a, b) + return { + x = a.y * b.z - a.z * b.y, + y = a.z * b.x - a.x * b.z, + z = a.x * b.y - a.y * b.x + } + end, + + length = function(v) + return math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z) + end, + + length_squared = function(v) + return v.x * v.x + v.y * v.y + v.z * v.z + end, + + distance = function(a, b) + local dx, dy, dz = b.x - a.x, b.y - a.y, b.z - a.z + return math.sqrt(dx * dx + dy * dy + dz * dz) + end, + + distance_squared = function(a, b) + local dx, dy, dz = b.x - a.x, b.y - a.y, b.z - a.z + return dx * dx + dy * dy + dz * dz + end, + + normalize = function(v) + local len = math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z) + if len > 1e-10 then + local inv_len = 1 / len + return {x = v.x * inv_len, y = v.y * inv_len, z = v.z * inv_len} + end + return {x = 0, y = 0, z = 0} + end, + + lerp = function(a, b, t) + t = math.clamp(t, 0, 1) + return { + x = a.x + (b.x - a.x) * t, + y = a.y + (b.y - a.y) * t, + z = a.z + (b.z - a.z) * t + } + end, + + reflect = function(v, normal) + local dot = v.x * normal.x + v.y * normal.y + v.z * normal.z + return { + x = v.x - 2 * dot * normal.x, + y = v.y - 2 * dot * normal.y, + z = v.z - 2 * dot * normal.z + } + end +} + +-- ====================================================================== +-- MATRIX OPERATIONS +-- ====================================================================== + +math.mat2 = { + new = function(a, b, c, d) + return { + {a or 1, b or 0}, + {c or 0, d or 1} + } + end, + + identity = function() + return {{1, 0}, {0, 1}} + end, + + mul = function(a, b) + return { + { + a[1][1] * b[1][1] + a[1][2] * b[2][1], + a[1][1] * b[1][2] + a[1][2] * b[2][2] + }, + { + a[2][1] * b[1][1] + a[2][2] * b[2][1], + a[2][1] * b[1][2] + a[2][2] * b[2][2] + } + } + end, + + det = function(m) + return m[1][1] * m[2][2] - m[1][2] * m[2][1] + end, + + inverse = function(m) + local det = m[1][1] * m[2][2] - m[1][2] * m[2][1] + if math.abs(det) < 1e-10 then + return nil + end + local inv_det = 1 / det + return { + {m[2][2] * inv_det, -m[1][2] * inv_det}, + {-m[2][1] * inv_det, m[1][1] * inv_det} + } + end, + + rotation = function(angle) + local cos, sin = math.cos(angle), math.sin(angle) + return { + {cos, -sin}, + {sin, cos} + } + end, + + transform = function(m, v) + return { + x = m[1][1] * v.x + m[1][2] * v.y, + y = m[2][1] * v.x + m[2][2] * v.y + } + end, + + scale = function(sx, sy) + sy = sy or sx + return { + {sx, 0}, + {0, sy} + } + end +} + +math.mat3 = { + identity = function() + return { + {1, 0, 0}, + {0, 1, 0}, + {0, 0, 1} + } + end, + + transform = function(x, y, angle, sx, sy) + sx = sx or 1 + sy = sy or sx + local cos, sin = math.cos(angle), math.sin(angle) + return { + {cos * sx, -sin * sy, x}, + {sin * sx, cos * sy, y}, + {0, 0, 1} + } + end, + + mul = function(a, b) + local result = { + {0, 0, 0}, + {0, 0, 0}, + {0, 0, 0} + } + for i = 1, 3 do + for j = 1, 3 do + for k = 1, 3 do + result[i][j] = result[i][j] + a[i][k] * b[k][j] + end + end + end + return result + end, + + transform_point = function(m, v) + local x = m[1][1] * v.x + m[1][2] * v.y + m[1][3] + local y = m[2][1] * v.x + m[2][2] * v.y + m[2][3] + local w = m[3][1] * v.x + m[3][2] * v.y + m[3][3] + if math.abs(w) < 1e-10 then + return {x = 0, y = 0} + end + return {x = x / w, y = y / w} + end, + + translation = function(x, y) + return { + {1, 0, x}, + {0, 1, y}, + {0, 0, 1} + } + end, + + rotation = function(angle) + local cos, sin = math.cos(angle), math.sin(angle) + return { + {cos, -sin, 0}, + {sin, cos, 0}, + {0, 0, 1} + } + end, + + scale = function(sx, sy) + sy = sy or sx + return { + {sx, 0, 0}, + {0, sy, 0}, + {0, 0, 1} + } + end, + + det = function(m) + return m[1][1] * (m[2][2] * m[3][3] - m[2][3] * m[3][2]) - + m[1][2] * (m[2][1] * m[3][3] - m[2][3] * m[3][1]) + + m[1][3] * (m[2][1] * m[3][2] - m[2][2] * m[3][1]) + end +} + +-- ====================================================================== +-- GEOMETRY FUNCTIONS +-- ====================================================================== + +math.geometry = { + point_line_distance = function(px, py, x1, y1, x2, y2) + local dx, dy = x2 - x1, y2 - y1 + local len_sq = dx * dx + dy * dy + if len_sq < 1e-10 then + return math.distance(px, py, x1, y1) + end + local t = ((px - x1) * dx + (py - y1) * dy) / len_sq + t = math.clamp(t, 0, 1) + local nearestX = x1 + t * dx + local nearestY = y1 + t * dy + return math.distance(px, py, nearestX, nearestY) + end, + + point_in_polygon = function(px, py, vertices) + local inside = false + local n = #vertices / 2 + for i = 1, n do + local x1, y1 = vertices[i*2-1], vertices[i*2] + local x2, y2 + if i == n then + x2, y2 = vertices[1], vertices[2] + else + x2, y2 = vertices[i*2+1], vertices[i*2+2] + end + if ((y1 > py) ~= (y2 > py)) and + (px < (x2 - x1) * (py - y1) / (y2 - y1) + x1) then + inside = not inside + end + end + return inside + end, + + triangle_area = function(x1, y1, x2, y2, x3, y3) + return math.abs((x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2)) / 2) + end, + + point_in_triangle = function(px, py, x1, y1, x2, y2, x3, y3) + local area = math.geometry.triangle_area(x1, y1, x2, y2, x3, y3) + local area1 = math.geometry.triangle_area(px, py, x2, y2, x3, y3) + local area2 = math.geometry.triangle_area(x1, y1, px, py, x3, y3) + local area3 = math.geometry.triangle_area(x1, y1, x2, y2, px, py) + return math.abs(area - (area1 + area2 + area3)) < 1e-10 + end, + + line_intersect = function(x1, y1, x2, y2, x3, y3, x4, y4) + local d = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1) + if math.abs(d) < 1e-10 then + return false, nil, nil + end + local ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / d + local ub = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / d + if ua >= 0 and ua <= 1 and ub >= 0 and ub <= 1 then + local x = x1 + ua * (x2 - x1) + local y = y1 + ua * (y2 - y1) + return true, x, y + end + return false, nil, nil + end, + + closest_point_on_segment = function(px, py, x1, y1, x2, y2) + local dx, dy = x2 - x1, y2 - y1 + local len_sq = dx * dx + dy * dy + if len_sq < 1e-10 then + return x1, y1 + end + local t = ((px - x1) * dx + (py - y1) * dy) / len_sq + t = math.clamp(t, 0, 1) + return x1 + t * dx, y1 + t * dy + end +} + +-- ====================================================================== +-- INTERPOLATION FUNCTIONS +-- ====================================================================== + +math.interpolation = { + bezier = function(t, p0, p1, p2, p3) + t = math.clamp(t, 0, 1) + local t2 = t * t + local t3 = t2 * t + local mt = 1 - t + local mt2 = mt * mt + local mt3 = mt2 * mt + return p0 * mt3 + 3 * p1 * mt2 * t + 3 * p2 * mt * t2 + p3 * t3 + end, + + catmull_rom = function(t, p0, p1, p2, p3) + t = math.clamp(t, 0, 1) + local t2 = t * t + local t3 = t2 * t + return 0.5 * ( + (2 * p1) + + (-p0 + p2) * t + + (2 * p0 - 5 * p1 + 4 * p2 - p3) * t2 + + (-p0 + 3 * p1 - 3 * p2 + p3) * t3 + ) + end, + + hermite = function(t, p0, p1, m0, m1) + t = math.clamp(t, 0, 1) + local t2 = t * t + local t3 = t2 * t + local h00 = 2 * t3 - 3 * t2 + 1 + local h10 = t3 - 2 * t2 + t + local h01 = -2 * t3 + 3 * t2 + local h11 = t3 - t2 + return h00 * p0 + h10 * m0 + h01 * p1 + h11 * m1 + end, + + quadratic_bezier = function(t, p0, p1, p2) + t = math.clamp(t, 0, 1) + local mt = 1 - t + return mt * mt * p0 + 2 * mt * t * p1 + t * t * p2 + end, + + step = function(t, edge, x) + return t < edge and 0 or x + end, + + smootherstep = function(edge0, edge1, x) + local t = math.clamp((x - edge0) / (edge1 - edge0), 0, 1) + return t * t * t * (t * (t * 6 - 15) + 10) + end +} diff --git a/modules/math/math.go b/modules/math/math.go deleted file mode 100644 index 1ad8146..0000000 --- a/modules/math/math.go +++ /dev/null @@ -1,57 +0,0 @@ -package math - -import luajit "git.sharkk.net/Sky/LuaJIT-to-Go" - -func GetFunctionList() map[string]luajit.GoFunction { - return map[string]luajit.GoFunction{ - "math_factorial": math_factorial, - "math_gcd": math_gcd, - "math_lcm": math_lcm, - } -} - -func math_factorial(s *luajit.State) int { - n := s.ToNumber(1) - if n < 0 || n != float64(int(n)) || n > 170 { - s.PushNil() - s.PushString("invalid argument") - return 2 - } - - result := 1.0 - for i := 2; i <= int(n); i++ { - result *= float64(i) - } - - s.PushNumber(result) - return 1 -} - -func math_gcd(s *luajit.State) int { - a := int(s.ToNumber(1)) - b := int(s.ToNumber(2)) - - for b != 0 { - a, b = b, a%b - } - - s.PushNumber(float64(a)) - return 1 -} - -func math_lcm(s *luajit.State) int { - a := int(s.ToNumber(1)) - b := int(s.ToNumber(2)) - - // Calculate GCD - gcd := func(x, y int) int { - for y != 0 { - x, y = y, x%y - } - return x - } - - result := a * b / gcd(a, b) - s.PushNumber(float64(result)) - return 1 -} diff --git a/modules/math/math.lua b/modules/math/math.lua deleted file mode 100644 index ad0f67d..0000000 --- a/modules/math/math.lua +++ /dev/null @@ -1,817 +0,0 @@ --- math.lua - Extended math library with advanced functions and utilities - -local math_ext = {} - --- Import standard math functions to maintain compatibility -for name, func in pairs(_G.math) do - math_ext[name] = func -end - --- ====================================================================== --- CONSTANTS (higher precision than standard library) --- ====================================================================== - -math_ext.pi = 3.14159265358979323846 -math_ext.tau = 6.28318530717958647693 -- 2*pi, useful for full rotations -math_ext.e = 2.71828182845904523536 -math_ext.phi = 1.61803398874989484820 -- Golden ratio (1 + sqrt(5)) / 2 -math_ext.sqrt2 = 1.41421356237309504880 -math_ext.sqrt3 = 1.73205080756887729353 -math_ext.ln2 = 0.69314718055994530942 -- Natural log of 2 -math_ext.ln10 = 2.30258509299404568402 -- Natural log of 10 -math_ext.infinity = 1/0 -math_ext.nan = 0/0 - --- ====================================================================== --- EXTENDED FUNCTIONS --- ====================================================================== - --- Cube root that handles negative numbers correctly (unlike x^(1/3)) -function math_ext.cbrt(x) - return x < 0 and -(-x)^(1/3) or x^(1/3) -end - --- Euclidean distance (hypotenuse) - more accurate than sqrt(x*x + y*y) for edge cases -function math_ext.hypot(x, y) - return math.sqrt(x * x + y * y) -end - --- IEEE 754 NaN check - only reliable way to test for NaN -function math_ext.isnan(x) - return x ~= x -end - --- Check if number is finite (not infinity or NaN) -function math_ext.isfinite(x) - return x > -math_ext.infinity and x < math_ext.infinity -end - --- Mathematical sign function returning -1, 0, or 1 -function math_ext.sign(x) - return x > 0 and 1 or (x < 0 and -1 or 0) -end - --- Constrain value to range [min, max] - essential for safe calculations -function math_ext.clamp(x, min, max) - return x < min and min or (x > max and max or x) -end - --- Linear interpolation - fundamental for animation and gradients -function math_ext.lerp(a, b, t) - return a + (b - a) * t -end - --- Smooth interpolation using Hermite polynomial - gives eased motion -function math_ext.smoothstep(a, b, t) - t = math_ext.clamp((t - a) / (b - a), 0, 1) - return t * t * (3 - 2 * t) -end - --- Map value from input range to output range - useful for scaling -function math_ext.map(x, in_min, in_max, out_min, out_max) - return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min -end - --- Round to nearest integer (more predictable than math.floor(x + 0.5)) -function math_ext.round(x) - return x >= 0 and math.floor(x + 0.5) or math.ceil(x - 0.5) -end - --- Round to specified decimal places using multiplication/division -function math_ext.roundto(x, decimals) - local mult = 10 ^ (decimals or 0) - return math.floor(x * mult + 0.5) / mult -end - --- Normalize angle to [-π, π] range for consistent angle calculations -function math_ext.normalize_angle(angle) - return angle - 2 * math_ext.pi * math.floor((angle + math_ext.pi) / (2 * math_ext.pi)) -end - --- 2D Euclidean distance between two points -function math_ext.distance(x1, y1, x2, y2) - local dx, dy = x2 - x1, y2 - y1 - return math.sqrt(dx * dx + dy * dy) -end - --- ====================================================================== --- RANDOM NUMBER FUNCTIONS - Enhanced random utilities --- ====================================================================== - --- Random float in specified range [min, max) - more flexible than math.random() -function math_ext.randomf(min, max) - if not min and not max then - return math.random() - elseif not max then - max = min - min = 0 - end - return min + math.random() * (max - min) -end - --- Random integer in range [min, max] inclusive - handles single argument case -function math_ext.randint(min, max) - if not max then - max = min - min = 1 - end - return math.floor(math.random() * (max - min + 1) + min) -end - --- Random boolean with configurable probability - useful for procedural generation -function math_ext.randboolean(p) - p = p or 0.5 - return math.random() < p -end - --- ====================================================================== --- STATISTICS FUNCTIONS - Robust statistical calculations --- ====================================================================== - --- Sum of numeric values in table - filters out non-numbers automatically -function math_ext.sum(t) - if type(t) ~= "table" then return 0 end - - local sum = 0 - for i=1, #t do - if type(t[i]) == "number" then - sum = sum + t[i] - end - end - return sum -end - --- Arithmetic mean (average) - handles mixed-type arrays safely -function math_ext.mean(t) - if type(t) ~= "table" or #t == 0 then return 0 end - - local sum = 0 - local count = 0 - for i=1, #t do - if type(t[i]) == "number" then - sum = sum + t[i] - count = count + 1 - end - end - return count > 0 and sum / count or 0 -end - --- Median value - sorts data to find middle value (handles even/odd lengths) -function math_ext.median(t) - if type(t) ~= "table" or #t == 0 then return 0 end - - local nums = {} - local count = 0 - for i=1, #t do - if type(t[i]) == "number" then - count = count + 1 - nums[count] = t[i] - end - end - - if count == 0 then return 0 end - - table.sort(nums) - - if count % 2 == 0 then - return (nums[count/2] + nums[count/2 + 1]) / 2 - else - return nums[math.ceil(count/2)] - end -end - --- Sample variance - measures spread using n-1 denominator (Bessel's correction) -function math_ext.variance(t) - if type(t) ~= "table" then return 0 end - - local count = 0 - local m = math_ext.mean(t) - local sum = 0 - - for i=1, #t do - if type(t[i]) == "number" then - local dev = t[i] - m - sum = sum + dev * dev - count = count + 1 - end - end - - return count > 1 and sum / count or 0 -end - --- Sample standard deviation - square root of variance -function math_ext.stdev(t) - return math.sqrt(math_ext.variance(t)) -end - --- Population variance - uses n denominator instead of n-1 -function math_ext.pvariance(t) - if type(t) ~= "table" then return 0 end - - local count = 0 - local m = math_ext.mean(t) - local sum = 0 - - for i=1, #t do - if type(t[i]) == "number" then - local dev = t[i] - m - sum = sum + dev * dev - count = count + 1 - end - end - - return count > 0 and sum / count or 0 -end - --- Population standard deviation -function math_ext.pstdev(t) - return math.sqrt(math_ext.pvariance(t)) -end - --- Mode - most frequently occurring value (first encountered if tie) -function math_ext.mode(t) - if type(t) ~= "table" or #t == 0 then return nil end - - local counts = {} - local most_frequent = nil - local max_count = 0 - - for i=1, #t do - local v = t[i] - counts[v] = (counts[v] or 0) + 1 - if counts[v] > max_count then - max_count = counts[v] - most_frequent = v - end - end - - return most_frequent -end - --- Simultaneous min/max - more efficient than separate calls -function math_ext.minmax(t) - if type(t) ~= "table" or #t == 0 then return nil, nil end - - local min, max - for i=1, #t do - if type(t[i]) == "number" then - min = t[i] - max = t[i] - break - end - end - - if min == nil then return nil, nil end - - for i=1, #t do - if type(t[i]) == "number" then - if t[i] < min then min = t[i] end - if t[i] > max then max = t[i] end - end - end - - return min, max -end - --- ====================================================================== --- VECTOR OPERATIONS - 2D/3D vector math for graphics and physics --- ====================================================================== - --- 2D Vector operations - fundamental for 2D graphics, physics, and UI -math_ext.vec2 = { - -- Create new 2D vector with default zero values - new = function(x, y) - return {x = x or 0, y = y or 0} - end, - - -- Create independent copy to avoid reference issues - copy = function(v) - return {x = v.x, y = v.y} - end, - - -- Vector addition - combines two displacement vectors - add = function(a, b) - return {x = a.x + b.x, y = a.y + b.y} - end, - - -- Vector subtraction - difference between two points/vectors - sub = function(a, b) - return {x = a.x - b.x, y = a.y - b.y} - end, - - -- Scalar or component-wise multiplication - mul = function(a, b) - if type(b) == "number" then - return {x = a.x * b, y = a.y * b} - end - return {x = a.x * b.x, y = a.y * b.y} - end, - - -- Scalar or component-wise division (uses multiplication for efficiency) - div = function(a, b) - if type(b) == "number" then - local inv = 1 / b - return {x = a.x * inv, y = a.y * inv} - end - return {x = a.x / b.x, y = a.y / b.y} - end, - - -- Dot product - measures vector similarity/projection - dot = function(a, b) - return a.x * b.x + a.y * b.y - end, - - -- Euclidean length/magnitude of vector - length = function(v) - return math.sqrt(v.x * v.x + v.y * v.y) - end, - - -- Squared length - avoids sqrt for performance when comparing lengths - length_squared = function(v) - return v.x * v.x + v.y * v.y - end, - - -- Distance between two points - distance = function(a, b) - local dx, dy = b.x - a.x, b.y - a.y - return math.sqrt(dx * dx + dy * dy) - end, - - -- Squared distance - avoids sqrt for performance - distance_squared = function(a, b) - local dx, dy = b.x - a.x, b.y - a.y - return dx * dx + dy * dy - end, - - -- Convert to unit vector (length 1) - preserves direction - normalize = function(v) - local len = math.sqrt(v.x * v.x + v.y * v.y) - if len > 1e-10 then - local inv_len = 1 / len - return {x = v.x * inv_len, y = v.y * inv_len} - end - return {x = 0, y = 0} - end, - - -- Rotate vector by angle (counterclockwise) - rotate = function(v, angle) - local c, s = math.cos(angle), math.sin(angle) - return { - x = v.x * c - v.y * s, - y = v.x * s + v.y * c - } - end, - - -- Get angle of vector from positive x-axis - angle = function(v) - return math.atan2(v.y, v.x) - end, - - -- Linear interpolation between two vectors - lerp = function(a, b, t) - t = math_ext.clamp(t, 0, 1) - return { - x = a.x + (b.x - a.x) * t, - y = a.y + (b.y - a.y) * t - } - end, - - -- Reflect vector across normal (like light bouncing off surface) - reflect = function(v, normal) - local dot = v.x * normal.x + v.y * normal.y - return { - x = v.x - 2 * dot * normal.x, - y = v.y - 2 * dot * normal.y - } - end -} - --- 3D Vector operations - essential for 3D graphics and spatial calculations -math_ext.vec3 = { - new = function(x, y, z) - return {x = x or 0, y = y or 0, z = z or 0} - end, - - copy = function(v) - return {x = v.x, y = v.y, z = v.z} - end, - - add = function(a, b) - return {x = a.x + b.x, y = a.y + b.y, z = a.z + b.z} - end, - - sub = function(a, b) - return {x = a.x - b.x, y = a.y - b.y, z = a.z - b.z} - end, - - mul = function(a, b) - if type(b) == "number" then - return {x = a.x * b, y = a.y * b, z = a.z * b} - end - return {x = a.x * b.x, y = a.y * b.y, z = a.z * b.z} - end, - - div = function(a, b) - if type(b) == "number" then - local inv = 1 / b - return {x = a.x * inv, y = a.y * inv, z = a.z * inv} - end - return {x = a.x / b.x, y = a.y / b.y, z = a.z / b.z} - end, - - dot = function(a, b) - return a.x * b.x + a.y * b.y + a.z * b.z - end, - - -- Cross product - creates perpendicular vector (right-hand rule) - cross = function(a, b) - return { - x = a.y * b.z - a.z * b.y, - y = a.z * b.x - a.x * b.z, - z = a.x * b.y - a.y * b.x - } - end, - - length = function(v) - return math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z) - end, - - length_squared = function(v) - return v.x * v.x + v.y * v.y + v.z * v.z - end, - - distance = function(a, b) - local dx, dy, dz = b.x - a.x, b.y - a.y, b.z - a.z - return math.sqrt(dx * dx + dy * dy + dz * dz) - end, - - distance_squared = function(a, b) - local dx, dy, dz = b.x - a.x, b.y - a.y, b.z - a.z - return dx * dx + dy * dy + dz * dz - end, - - normalize = function(v) - local len = math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z) - if len > 1e-10 then - local inv_len = 1 / len - return {x = v.x * inv_len, y = v.y * inv_len, z = v.z * inv_len} - end - return {x = 0, y = 0, z = 0} - end, - - lerp = function(a, b, t) - t = math_ext.clamp(t, 0, 1) - return { - x = a.x + (b.x - a.x) * t, - y = a.y + (b.y - a.y) * t, - z = a.z + (b.z - a.z) * t - } - end, - - reflect = function(v, normal) - local dot = v.x * normal.x + v.y * normal.y + v.z * normal.z - return { - x = v.x - 2 * dot * normal.x, - y = v.y - 2 * dot * normal.y, - z = v.z - 2 * dot * normal.z - } - end -} - --- ====================================================================== --- MATRIX OPERATIONS - Linear transformations for graphics and math --- ====================================================================== - -math_ext.mat2 = { - -- Create 2x2 matrix with specified values (defaults to identity) - new = function(a, b, c, d) - return { - {a or 1, b or 0}, - {c or 0, d or 1} - } - end, - - -- 2x2 identity matrix - no transformation - identity = function() - return {{1, 0}, {0, 1}} - end, - - -- Matrix multiplication - combines transformations (order matters) - mul = function(a, b) - return { - { - a[1][1] * b[1][1] + a[1][2] * b[2][1], - a[1][1] * b[1][2] + a[1][2] * b[2][2] - }, - { - a[2][1] * b[1][1] + a[2][2] * b[2][1], - a[2][1] * b[1][2] + a[2][2] * b[2][2] - } - } - end, - - -- Determinant - measures area scaling factor - det = function(m) - return m[1][1] * m[2][2] - m[1][2] * m[2][1] - end, - - -- Inverse matrix - reverses transformation (if possible) - inverse = function(m) - local det = m[1][1] * m[2][2] - m[1][2] * m[2][1] - if math.abs(det) < 1e-10 then - return nil -- Matrix is not invertible - end - - local inv_det = 1 / det - return { - {m[2][2] * inv_det, -m[1][2] * inv_det}, - {-m[2][1] * inv_det, m[1][1] * inv_det} - } - end, - - -- Create rotation matrix for given angle - rotation = function(angle) - local cos, sin = math.cos(angle), math.sin(angle) - return { - {cos, -sin}, - {sin, cos} - } - end, - - -- Apply matrix transformation to 2D vector - transform = function(m, v) - return { - x = m[1][1] * v.x + m[1][2] * v.y, - y = m[2][1] * v.x + m[2][2] * v.y - } - end, - - -- Create scaling matrix (uniform if sy omitted) - scale = function(sx, sy) - sy = sy or sx - return { - {sx, 0}, - {0, sy} - } - end -} - -math_ext.mat3 = { - -- 3x3 identity matrix - useful for 2D transformations with translation - identity = function() - return { - {1, 0, 0}, - {0, 1, 0}, - {0, 0, 1} - } - end, - - -- Complete 2D transformation matrix (translate, rotate, scale) - transform = function(x, y, angle, sx, sy) - sx = sx or 1 - sy = sy or sx - local cos, sin = math.cos(angle), math.sin(angle) - return { - {cos * sx, -sin * sy, x}, - {sin * sx, cos * sy, y}, - {0, 0, 1} - } - end, - - -- 3x3 matrix multiplication - combines transformations - mul = function(a, b) - local result = { - {0, 0, 0}, - {0, 0, 0}, - {0, 0, 0} - } - - for i = 1, 3 do - for j = 1, 3 do - for k = 1, 3 do - result[i][j] = result[i][j] + a[i][k] * b[k][j] - end - end - end - - return result - end, - - -- Transform 2D point using homogeneous coordinates - transform_point = function(m, v) - local x = m[1][1] * v.x + m[1][2] * v.y + m[1][3] - local y = m[2][1] * v.x + m[2][2] * v.y + m[2][3] - local w = m[3][1] * v.x + m[3][2] * v.y + m[3][3] - - if math.abs(w) < 1e-10 then - return {x = 0, y = 0} - end - - return {x = x / w, y = y / w} - end, - - -- Translation matrix - translation = function(x, y) - return { - {1, 0, x}, - {0, 1, y}, - {0, 0, 1} - } - end, - - -- 2D rotation matrix in 3x3 form - rotation = function(angle) - local cos, sin = math.cos(angle), math.sin(angle) - return { - {cos, -sin, 0}, - {sin, cos, 0}, - {0, 0, 1} - } - end, - - -- Scaling matrix in 3x3 form - scale = function(sx, sy) - sy = sy or sx - return { - {sx, 0, 0}, - {0, sy, 0}, - {0, 0, 1} - } - end, - - -- 3x3 determinant - measures volume scaling - det = function(m) - return m[1][1] * (m[2][2] * m[3][3] - m[2][3] * m[3][2]) - - m[1][2] * (m[2][1] * m[3][3] - m[2][3] * m[3][1]) + - m[1][3] * (m[2][1] * m[3][2] - m[2][2] * m[3][1]) - end -} - --- ====================================================================== --- GEOMETRY FUNCTIONS - Computational geometry utilities --- ====================================================================== - -math_ext.geometry = { - -- Shortest distance from point to line segment - point_line_distance = function(px, py, x1, y1, x2, y2) - local dx, dy = x2 - x1, y2 - y1 - local len_sq = dx * dx + dy * dy - - if len_sq < 1e-10 then - return math_ext.distance(px, py, x1, y1) - end - - local t = ((px - x1) * dx + (py - y1) * dy) / len_sq - t = math_ext.clamp(t, 0, 1) - - local nearestX = x1 + t * dx - local nearestY = y1 + t * dy - - return math_ext.distance(px, py, nearestX, nearestY) - end, - - -- Point-in-polygon test using ray casting algorithm - point_in_polygon = function(px, py, vertices) - local inside = false - local n = #vertices / 2 - - for i = 1, n do - local x1, y1 = vertices[i*2-1], vertices[i*2] - local x2, y2 - - if i == n then - x2, y2 = vertices[1], vertices[2] - else - x2, y2 = vertices[i*2+1], vertices[i*2+2] - end - - if ((y1 > py) ~= (y2 > py)) and - (px < (x2 - x1) * (py - y1) / (y2 - y1) + x1) then - inside = not inside - end - end - - return inside - end, - - -- Calculate triangle area using cross product method - triangle_area = function(x1, y1, x2, y2, x3, y3) - return math.abs((x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2)) / 2) - end, - - -- Point-in-triangle test using barycentric coordinates - point_in_triangle = function(px, py, x1, y1, x2, y2, x3, y3) - local area = math_ext.geometry.triangle_area(x1, y1, x2, y2, x3, y3) - local area1 = math_ext.geometry.triangle_area(px, py, x2, y2, x3, y3) - local area2 = math_ext.geometry.triangle_area(x1, y1, px, py, x3, y3) - local area3 = math_ext.geometry.triangle_area(x1, y1, x2, y2, px, py) - - return math.abs(area - (area1 + area2 + area3)) < 1e-10 - end, - - -- Line segment intersection using parametric equations - line_intersect = function(x1, y1, x2, y2, x3, y3, x4, y4) - local d = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1) - - if math.abs(d) < 1e-10 then - return false, nil, nil -- Lines are parallel - end - - local ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / d - local ub = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / d - - if ua >= 0 and ua <= 1 and ub >= 0 and ub <= 1 then - local x = x1 + ua * (x2 - x1) - local y = y1 + ua * (y2 - y1) - return true, x, y - end - - return false, nil, nil - end, - - -- Find closest point on line segment to given point - closest_point_on_segment = function(px, py, x1, y1, x2, y2) - local dx, dy = x2 - x1, y2 - y1 - local len_sq = dx * dx + dy * dy - - if len_sq < 1e-10 then - return x1, y1 - end - - local t = ((px - x1) * dx + (py - y1) * dy) / len_sq - t = math_ext.clamp(t, 0, 1) - - return x1 + t * dx, y1 + t * dy - end -} - --- ====================================================================== --- INTERPOLATION FUNCTIONS - Smooth transitions and curve generation --- ====================================================================== - -math_ext.interpolation = { - -- Cubic Bézier curve - standard for smooth animations - bezier = function(t, p0, p1, p2, p3) - t = math_ext.clamp(t, 0, 1) - local t2 = t * t - local t3 = t2 * t - local mt = 1 - t - local mt2 = mt * mt - local mt3 = mt2 * mt - - return p0 * mt3 + 3 * p1 * mt2 * t + 3 * p2 * mt * t2 + p3 * t3 - end, - - -- Catmull-Rom spline - smooth interpolation through points - catmull_rom = function(t, p0, p1, p2, p3) - t = math_ext.clamp(t, 0, 1) - local t2 = t * t - local t3 = t2 * t - - return 0.5 * ( - (2 * p1) + - (-p0 + p2) * t + - (2 * p0 - 5 * p1 + 4 * p2 - p3) * t2 + - (-p0 + 3 * p1 - 3 * p2 + p3) * t3 - ) - end, - - -- Hermite interpolation with tangent control - hermite = function(t, p0, p1, m0, m1) - t = math_ext.clamp(t, 0, 1) - local t2 = t * t - local t3 = t2 * t - local h00 = 2 * t3 - 3 * t2 + 1 - local h10 = t3 - 2 * t2 + t - local h01 = -2 * t3 + 3 * t2 - local h11 = t3 - t2 - - return h00 * p0 + h10 * m0 + h01 * p1 + h11 * m1 - end, - - -- Quadratic Bézier - simpler curve with one control point - quadratic_bezier = function(t, p0, p1, p2) - t = math_ext.clamp(t, 0, 1) - local mt = 1 - t - return mt * mt * p0 + 2 * mt * t * p1 + t * t * p2 - end, - - -- Step function - instant transition at threshold - step = function(t, edge, x) - return t < edge and 0 or x - end, - - -- Smoothstep - S-curve interpolation (3rd order) - smoothstep = function(edge0, edge1, x) - local t = math_ext.clamp((x - edge0) / (edge1 - edge0), 0, 1) - return t * t * (3 - 2 * t) - end, - - -- Smootherstep - Even smoother S-curve (5th order, Ken Perlin) - smootherstep = function(edge0, edge1, x) - local t = math_ext.clamp((x - edge0) / (edge1 - edge0), 0, 1) - return t * t * t * (t * (t * 6 - 15) + 10) - end -} - -return math_ext \ No newline at end of file diff --git a/modules/registry.go b/modules/registry.go index 0da9809..2d67137 100644 --- a/modules/registry.go +++ b/modules/registry.go @@ -10,7 +10,6 @@ import ( "Moonshark/modules/fs" "Moonshark/modules/http" "Moonshark/modules/kv" - "Moonshark/modules/math" "Moonshark/modules/sql" lua_string "Moonshark/modules/string+" @@ -36,7 +35,6 @@ func New() *Registry { } maps.Copy(r.goFuncs, lua_string.GetFunctionList()) - maps.Copy(r.goFuncs, math.GetFunctionList()) maps.Copy(r.goFuncs, crypto.GetFunctionList()) maps.Copy(r.goFuncs, fs.GetFunctionList()) maps.Copy(r.goFuncs, http.GetFunctionList()) diff --git a/tests/math.lua b/tests/math.lua index cee6648..b05a32f 100644 --- a/tests/math.lua +++ b/tests/math.lua @@ -1,171 +1,393 @@ require("tests") -local math = require("math") --- Test constants -test("Constants", function() +-- Constants tests +test("math.pi", function() assert_close(math.pi, 3.14159265358979323846) - assert_close(math.tau, 6.28318530717958647693) - assert_close(math.e, 2.71828182845904523536) - assert_close(math.phi, 1.61803398874989484820) - assert_equal(math.infinity, 1/0) - assert_equal(math.isnan(math.nan), true) + assert(math.pi > 3.14 and math.pi < 3.15) end) --- Test extended functions -test("Extended Functions", function() +test("math.tau", function() + assert_close(math.tau, 6.28318530717958647693) + assert_close(math.tau, 2 * math.pi) +end) + +test("math.e", function() + assert_close(math.e, 2.71828182845904523536) + assert(math.e > 2.7 and math.e < 2.8) +end) + +test("math.phi", function() + assert_close(math.phi, 1.61803398874989484820) + assert_close(math.phi, (1 + math.sqrt(5)) / 2) +end) + +test("math.infinity", function() + assert_equal(math.infinity, 1/0) + assert(math.infinity > 0) +end) + +test("math.nan", function() + assert_equal(math.isnan(math.nan), true) + assert(math.nan ~= math.nan) -- NaN property +end) + +-- Extended functions tests +test("math.cbrt", function() assert_close(math.cbrt(8), 2) assert_close(math.cbrt(-8), -2) +end) + +test("math.hypot", function() assert_close(math.hypot(3, 4), 5) + assert_close(math.hypot(5, 12), 13) +end) + +test("math.isnan", function() assert_equal(math.isnan(0/0), true) assert_equal(math.isnan(5), false) +end) + +test("math.isfinite", function() assert_equal(math.isfinite(5), true) assert_equal(math.isfinite(math.infinity), false) +end) + +test("math.sign", function() assert_equal(math.sign(5), 1) assert_equal(math.sign(-5), -1) assert_equal(math.sign(0), 0) +end) + +test("math.clamp", function() assert_equal(math.clamp(5, 0, 3), 3) assert_equal(math.clamp(-1, 0, 3), 0) + assert_equal(math.clamp(2, 0, 3), 2) +end) + +test("math.lerp", function() assert_close(math.lerp(0, 10, 0.5), 5) + assert_close(math.lerp(2, 8, 0.25), 3.5) +end) + +test("math.smoothstep", function() + assert_close(math.smoothstep(0, 1, 0.5), 0.5) + assert_close(math.smoothstep(0, 10, 5), 0.5) +end) + +test("math.map", function() assert_close(math.map(5, 0, 10, 0, 100), 50) + assert_close(math.map(2, 0, 4, 10, 20), 15) +end) + +test("math.round", function() assert_equal(math.round(2.7), 3) assert_equal(math.round(-2.7), -3) + assert_equal(math.round(2.3), 2) +end) + +test("math.roundto", function() assert_close(math.roundto(3.14159, 2), 3.14) + assert_close(math.roundto(123.456, 1), 123.5) +end) + +test("math.normalize_angle", function() + assert_close(math.normalize_angle(math.pi * 2.5), math.pi * 0.5) + assert_close(math.normalize_angle(-math.pi * 2.5), -math.pi * 0.5) +end) + +test("math.distance", function() assert_close(math.distance(0, 0, 3, 4), 5) + assert_close(math.distance(1, 1, 4, 5), 5) end) --- Test random functions -test("Random Functions", function() - local r = math.randomf(0, 1) - assert(r >= 0 and r < 1, "randomf should be in range [0, 1)") - local i = math.randint(1, 10) - assert(i >= 1 and i <= 10, "randint should be in range [1, 10]") - assert_equal(type(math.randboolean()), "boolean") +test("math.factorial", function() + assert_equal(math.factorial(5), 120) + assert_equal(math.factorial(0), 1) + assert_equal(math.factorial(-1), nil) end) --- Test statistics -test("Statistics", function() - local data = {1, 2, 3, 4, 5} - assert_equal(math.sum(data), 15) - assert_equal(math.mean(data), 3) - assert_equal(math.median(data), 3) - assert_close(math.variance(data), 2) - assert_close(math.stdev(data), math.sqrt(2)) - local min, max = math.minmax(data) - assert_equal(min, 1) - assert_equal(max, 5) +test("math.gcd", function() + assert_equal(math.gcd(48, 18), 6) + assert_equal(math.gcd(100, 25), 25) +end) + +test("math.lcm", function() + assert_equal(math.lcm(4, 6), 12) + assert_equal(math.lcm(15, 20), 60) +end) + +-- Random functions tests +test("math.randomf", function() + local r1 = math.randomf(0, 1) + local r2 = math.randomf(5, 10) + assert(r1 >= 0 and r1 < 1) + assert(r2 >= 5 and r2 < 10) +end) + +test("math.randint", function() + local i1 = math.randint(1, 10) + local i2 = math.randint(50, 60) + assert(i1 >= 1 and i1 <= 10) + assert(i2 >= 50 and i2 <= 60) +end) + +test("math.randboolean", function() + local b1 = math.randboolean() + local b2 = math.randboolean(0.8) + assert_equal(type(b1), "boolean") + assert_equal(type(b2), "boolean") +end) + +-- Statistics tests +test("math.sum", function() + assert_equal(math.sum({1, 2, 3, 4, 5}), 15) + assert_equal(math.sum({10, 20, 30}), 60) +end) + +test("math.mean", function() + assert_equal(math.mean({1, 2, 3, 4, 5}), 3) + assert_equal(math.mean({10, 20, 30}), 20) +end) + +test("math.median", function() + assert_equal(math.median({1, 2, 3, 4, 5}), 3) + assert_equal(math.median({1, 2, 3, 4}), 2.5) +end) + +test("math.variance", function() + assert_close(math.variance({1, 2, 3, 4, 5}), 2) + assert_close(math.variance({10, 10, 10}), 0) +end) + +test("math.stdev", function() + assert_close(math.stdev({1, 2, 3, 4, 5}), math.sqrt(2)) + assert_close(math.stdev({10, 10, 10}), 0) +end) + +test("math.mode", function() assert_equal(math.mode({1, 2, 2, 3}), 2) + assert_equal(math.mode({5, 5, 4, 4, 4}), 4) end) --- Test 2D vectors -test("2D Vectors", function() +test("math.minmax", function() + local min1, max1 = math.minmax({1, 2, 3, 4, 5}) + local min2, max2 = math.minmax({-5, 0, 10}) + assert_equal(min1, 1) + assert_equal(max1, 5) + assert_equal(min2, -5) + assert_equal(max2, 10) +end) + +-- 2D Vector tests +test("math.vec2.new", function() local v1 = math.vec2.new(3, 4) - local v2 = math.vec2.new(1, 2) - + local v2 = math.vec2.new() assert_equal(v1.x, 3) assert_equal(v1.y, 4) - assert_close(math.vec2.length(v1), 5) - assert_equal(math.vec2.length_squared(v1), 25) - - local v3 = math.vec2.add(v1, v2) - assert_equal(v3.x, 4) - assert_equal(v3.y, 6) - - local v4 = math.vec2.sub(v1, v2) - assert_equal(v4.x, 2) - assert_equal(v4.y, 2) - - local v5 = math.vec2.mul(v1, 2) - assert_equal(v5.x, 6) - assert_equal(v5.y, 8) - + assert_equal(v2.x, 0) + assert_equal(v2.y, 0) +end) + +test("math.vec2.add", function() + local v1 = math.vec2.new(3, 4) + local v2 = math.vec2.new(1, 2) + local result = math.vec2.add(v1, v2) + assert_equal(result.x, 4) + assert_equal(result.y, 6) +end) + +test("math.vec2.sub", function() + local v1 = math.vec2.new(3, 4) + local v2 = math.vec2.new(1, 2) + local result = math.vec2.sub(v1, v2) + assert_equal(result.x, 2) + assert_equal(result.y, 2) +end) + +test("math.vec2.mul", function() + local v1 = math.vec2.new(3, 4) + local result1 = math.vec2.mul(v1, 2) + local result2 = math.vec2.mul(v1, math.vec2.new(2, 3)) + assert_equal(result1.x, 6) + assert_equal(result1.y, 8) + assert_equal(result2.x, 6) + assert_equal(result2.y, 12) +end) + +test("math.vec2.dot", function() + local v1 = math.vec2.new(3, 4) + local v2 = math.vec2.new(1, 2) assert_equal(math.vec2.dot(v1, v2), 11) - assert_close(math.vec2.distance(v1, v2), math.sqrt(8)) - + assert_equal(math.vec2.dot(math.vec2.new(1, 0), math.vec2.new(0, 1)), 0) +end) + +test("math.vec2.length", function() + local v1 = math.vec2.new(3, 4) + local v2 = math.vec2.new(0, 5) + assert_close(math.vec2.length(v1), 5) + assert_close(math.vec2.length(v2), 5) +end) + +test("math.vec2.distance", function() + local v1 = math.vec2.new(0, 0) + local v2 = math.vec2.new(3, 4) + assert_close(math.vec2.distance(v1, v2), 5) + assert_close(math.vec2.distance(math.vec2.new(1, 1), math.vec2.new(4, 5)), 5) +end) + +test("math.vec2.normalize", function() + local v1 = math.vec2.new(3, 4) local normalized = math.vec2.normalize(v1) assert_close(math.vec2.length(normalized), 1) + assert_close(normalized.x, 0.6) + assert_close(normalized.y, 0.8) end) --- Test 3D vectors -test("3D Vectors", function() +test("math.vec2.rotate", function() + local v1 = math.vec2.new(1, 0) + local rotated90 = math.vec2.rotate(v1, math.pi/2) + local rotated180 = math.vec2.rotate(v1, math.pi) + assert_close(rotated90.x, 0, 1e-10) + assert_close(rotated90.y, 1) + assert_close(rotated180.x, -1) + assert_close(rotated180.y, 0, 1e-10) +end) + +-- 3D Vector tests +test("math.vec3.new", function() local v1 = math.vec3.new(1, 2, 3) - local v2 = math.vec3.new(4, 5, 6) - - assert_close(math.vec3.length(v1), math.sqrt(14)) - assert_equal(math.vec3.dot(v1, v2), 32) - - local cross = math.vec3.cross(v1, v2) - assert_equal(cross.x, -3) - assert_equal(cross.y, 6) - assert_equal(cross.z, -3) - - local add = math.vec3.add(v1, v2) - assert_equal(add.x, 5) - assert_equal(add.y, 7) - assert_equal(add.z, 9) + local v2 = math.vec3.new() + assert_equal(v1.x, 1) + assert_equal(v1.y, 2) + assert_equal(v1.z, 3) + assert_equal(v2.x, 0) + assert_equal(v2.y, 0) + assert_equal(v2.z, 0) end) --- Test 2x2 matrices -test("2x2 Matrices", function() +test("math.vec3.cross", function() + local v1 = math.vec3.new(1, 0, 0) + local v2 = math.vec3.new(0, 1, 0) + local cross = math.vec3.cross(v1, v2) + assert_equal(cross.x, 0) + assert_equal(cross.y, 0) + assert_equal(cross.z, 1) +end) + +test("math.vec3.length", function() + local v1 = math.vec3.new(1, 2, 3) + local v2 = math.vec3.new(0, 0, 5) + assert_close(math.vec3.length(v1), math.sqrt(14)) + assert_close(math.vec3.length(v2), 5) +end) + +-- Matrix tests +test("math.mat2.det", function() + local m1 = math.mat2.new(1, 2, 3, 4) + local m2 = math.mat2.new(2, 0, 0, 3) + assert_equal(math.mat2.det(m1), -2) + assert_equal(math.mat2.det(m2), 6) +end) + +test("math.mat2.mul", function() local m1 = math.mat2.new(1, 2, 3, 4) local m2 = math.mat2.new(2, 0, 1, 3) - - assert_equal(math.mat2.det(m1), -2) - local product = math.mat2.mul(m1, m2) assert_equal(product[1][1], 4) assert_equal(product[1][2], 6) assert_equal(product[2][1], 10) assert_equal(product[2][2], 12) - - local rotation = math.mat2.rotation(math.pi/2) - assert_close(rotation[1][1], 0, 1e-10) - assert_close(rotation[1][2], -1) - assert_close(rotation[2][1], 1) - assert_close(rotation[2][2], 0, 1e-10) end) --- Test 3x3 matrices -test("3x3 Matrices", function() - local identity = math.mat3.identity() - assert_equal(identity[1][1], 1) - assert_equal(identity[2][2], 1) - assert_equal(identity[3][3], 1) - assert_equal(identity[1][2], 0) - +test("math.mat2.rotation", function() + local rot90 = math.mat2.rotation(math.pi/2) + local rot180 = math.mat2.rotation(math.pi) + assert_close(rot90[1][1], 0, 1e-10) + assert_close(rot90[1][2], -1) + assert_close(rot180[1][1], -1) + assert_close(rot180[2][2], -1) +end) + +test("math.mat3.transform_point", function() local translation = math.mat3.translation(5, 10) - assert_equal(translation[1][3], 5) - assert_equal(translation[2][3], 10) - - local point = {x = 1, y = 2} - local transformed = math.mat3.transform_point(translation, point) - assert_equal(transformed.x, 6) - assert_equal(transformed.y, 12) + local point1 = {x = 1, y = 2} + local point2 = {x = 0, y = 0} + local t1 = math.mat3.transform_point(translation, point1) + local t2 = math.mat3.transform_point(translation, point2) + assert_equal(t1.x, 6) + assert_equal(t1.y, 12) + assert_equal(t2.x, 5) + assert_equal(t2.y, 10) end) --- Test geometry functions -test("Geometry", function() - assert_close(math.geometry.triangle_area(0, 0, 4, 0, 0, 3), 6) - assert_equal(math.geometry.point_in_triangle(1, 1, 0, 0, 4, 0, 0, 3), true) - assert_equal(math.geometry.point_in_triangle(5, 5, 0, 0, 4, 0, 0, 3), false) - - local intersects, x, y = math.geometry.line_intersect(0, 0, 2, 2, 0, 2, 2, 0) - assert_equal(intersects, true) - assert_close(x, 1) - assert_close(y, 1) - - local closest_x, closest_y = math.geometry.closest_point_on_segment(1, 3, 0, 0, 4, 0) - assert_close(closest_x, 1) - assert_close(closest_y, 0) +test("math.mat3.det", function() + local identity = math.mat3.identity() + local scale = math.mat3.scale(2, 3) + assert_close(math.mat3.det(identity), 1) + assert_close(math.mat3.det(scale), 6) end) --- Test interpolation -test("Interpolation", function() - assert_close(math.interpolation.bezier(0.5, 0, 1, 2, 3), 1.5) - assert_close(math.interpolation.quadratic_bezier(0.5, 0, 2, 4), 2) - assert_close(math.interpolation.smoothstep(0, 1, 0.5), 0.5) - assert_close(math.interpolation.smootherstep(0, 1, 0.5), 0.5) - assert_close(math.interpolation.catmull_rom(0.5, 0, 1, 2, 3), 1.5) +-- Geometry tests +test("math.geometry.triangle_area", function() + local area1 = math.geometry.triangle_area(0, 0, 4, 0, 0, 3) + local area2 = math.geometry.triangle_area(0, 0, 2, 0, 1, 2) + assert_close(area1, 6) + assert_close(area2, 2) +end) + +test("math.geometry.point_in_triangle", function() + local inside1 = math.geometry.point_in_triangle(1, 1, 0, 0, 4, 0, 0, 3) + local inside2 = math.geometry.point_in_triangle(5, 5, 0, 0, 4, 0, 0, 3) + assert_equal(inside1, true) + assert_equal(inside2, false) +end) + +test("math.geometry.line_intersect", function() + local intersects1, x1, y1 = math.geometry.line_intersect(0, 0, 2, 2, 0, 2, 2, 0) + local intersects2, x2, y2 = math.geometry.line_intersect(0, 0, 1, 1, 2, 2, 3, 3) + assert_equal(intersects1, true) + assert_close(x1, 1) + assert_close(y1, 1) + assert_equal(intersects2, false) +end) + +test("math.geometry.closest_point_on_segment", function() + local x1, y1 = math.geometry.closest_point_on_segment(1, 3, 0, 0, 4, 0) + local x2, y2 = math.geometry.closest_point_on_segment(5, 1, 0, 0, 4, 0) + assert_close(x1, 1) + assert_close(y1, 0) + assert_close(x2, 4) + assert_close(y2, 0) +end) + +-- Interpolation tests +test("math.interpolation.bezier", function() + local result1 = math.interpolation.bezier(0.5, 0, 1, 2, 3) + local result2 = math.interpolation.bezier(0, 0, 1, 2, 3) + assert_close(result1, 1.5) + assert_close(result2, 0) +end) + +test("math.interpolation.quadratic_bezier", function() + local result1 = math.interpolation.quadratic_bezier(0.5, 0, 2, 4) + local result2 = math.interpolation.quadratic_bezier(1, 0, 2, 4) + assert_close(result1, 2) + assert_close(result2, 4) +end) + +test("math.interpolation.smootherstep", function() + local result1 = math.interpolation.smootherstep(0, 1, 0.5) + local result2 = math.interpolation.smootherstep(0, 10, 5) + assert_close(result1, 0.5) + assert_close(result2, 0.5) +end) + +test("math.interpolation.catmull_rom", function() + local result1 = math.interpolation.catmull_rom(0.5, 0, 1, 2, 3) + local result2 = math.interpolation.catmull_rom(0, 0, 1, 2, 3) + assert_close(result1, 1.5) + assert_close(result2, 1) end) summary() -test_exit() \ No newline at end of file +test_exit() diff --git a/tests/tests.lua b/tests/tests.lua index aeac696..be3286e 100644 --- a/tests/tests.lua +++ b/tests/tests.lua @@ -71,6 +71,16 @@ function assert_table_equal(expected, actual, message, path) return true end +function assert_close(expected, actual, tolerance, message) + tolerance = tolerance or 1e-10 + local diff = math.abs(expected - actual) + if diff <= tolerance then + return true + end + local msg = message or string.format("Expected %g, got %g (diff: %g > %g)", expected, actual, diff, tolerance) + assert(false, msg, 3) +end + function test(name, fn) print("Testing " .. name .. "...") total = total + 1