From 779b13d56b8381a641ce95cfc1977d0753634467 Mon Sep 17 00:00:00 2001 From: Sage Vaillancourt Date: Wed, 5 Feb 2025 13:52:12 -0500 Subject: [PATCH] Add basic testing. Requires luaunit. Put utils functions into a module. Of sorts, anyway. Add benchAllFielders() Add playerIsOn() and associated "sides". Pass appliedSpeed through into runner funcs. Should make it easier to put under NPC control. --- Makefile | 3 + src/main.lua | 234 ++++++++++++++++++++++------------------- src/test/testUtils.lua | 15 +++ src/utils.lua | 55 +++------- 4 files changed, 161 insertions(+), 146 deletions(-) create mode 100644 src/test/testUtils.lua diff --git a/Makefile b/Makefile index 81bca62..bf675bc 100644 --- a/Makefile +++ b/Makefile @@ -7,5 +7,8 @@ check: stylua -c --indent-type Spaces src/ cat __stub.ext.lua <(sed 's/^function/-- selene: allow(unused_variable)\nfunction/' ${PLAYDATE_SDK_PATH}/CoreLibs/__types.lua) ${SOURCE_FILES} | grep -v '^import' | sed 's///g' | selene - +test: check + (cd src; find ./test -name '*lua' | xargs -L1 lua) + lint: stylua --indent-type Spaces src/ diff --git a/src/main.lua b/src/main.lua index 2b440e3..ac47f4c 100644 --- a/src/main.lua +++ b/src/main.lua @@ -47,7 +47,7 @@ local Screen = { H = playdate.display.getHeight(), } -local Center = xy(Screen.W / 2, Screen.H / 2) +local Center = utils.xy(Screen.W / 2, Screen.H / 2) local BootTune = playdate.sound.sampleplayer.new("sounds/boot-tune.wav") -- local BootTune = playdate.sound.sampleplayer.new("sounds/boot-tune-organy.wav") @@ -62,7 +62,7 @@ local PlayerImageBlipper = blipper.new(100, "images/game/player.png", "i local DanceBounceMs = 500 local DanceBounceCount = 4 -local FielderDanceAnimator = gfx.animator.new(1, 10, 0, easingHill) +local FielderDanceAnimator = gfx.animator.new(1, 10, 0, utils.easingHill) FielderDanceAnimator.repeatCount = DanceBounceCount - 1 -- selene: allow(unused_variable) @@ -91,12 +91,12 @@ local Pitches = { }, -- Slider { - x = gfx.animator.new(PitchFlyMs, PitchStartX - 20, PitchStartX, easingHill), + x = gfx.animator.new(PitchFlyMs, PitchStartX - 20, PitchStartX, utils.easingHill), y = gfx.animator.new(PitchFlyMs, PitchStartY, PitchEndY, playdate.easingFunctions.linear), }, -- Curve ball { - x = gfx.animator.new(PitchFlyMs, PitchStartX + 20, PitchStartX, easingHill), + x = gfx.animator.new(PitchFlyMs, PitchStartX + 20, PitchStartX, utils.easingHill), y = gfx.animator.new(PitchFlyMs, PitchStartY, PitchEndY, playdate.easingFunctions.linear), }, -- Wobbbleball @@ -112,12 +112,12 @@ local Pitches = { } local CrankOffsetDeg = 90 -local BatOffset = xy(10, 25) +local BatOffset = utils.xy(10, 25) -local batBase = xy(Center.x - 34, 215) -local batTip = xy(0, 0) +local batBase = utils.xy(Center.x - 34, 215) +local batTip = utils.xy(0, 0) -local TagDistance = 20 +local TagDistance = 15 local SmallestBallRadius = 6 @@ -130,18 +130,50 @@ local ball = { local BatLength = 50 --45 -local Modes = { +local Offense = { batting = {}, running = {}, } -local currentMode = Modes.batting +local Sides = { + offense = {}, + defense = {}, +} + +local offenseMode = Offense.batting + +---@alias Team { score: number, benchPosition: XYPair } + +---@type table +local teams = { + home = { + score = 0, + benchPosition = utils.xy(Screen.W + 10, Center.y), + }, + away = { + score = 0, + benchPosition = utils.xy(-10, Center.y), + }, +} + +local PlayerTeam = teams.away +local battingTeam = teams.away +local outs = 0 +local inning = 1 + +function playerIsOn(side) + if PlayerTeam == battingTeam then + return side == Sides.offense + else + return side == Sides.defense + end +end -- TODO? Replace this AND ballSizeAnimator with a ballHeightAnimator -- ...that might lose some of the magic of both. Compromise available? idk -local ballFloatAnimator = gfx.animator.new(2000, -60, 0, easingHill) +local ballFloatAnimator = gfx.animator.new(2000, -60, 0, utils.easingHill) local BallSizeMs = 2000 -local ballSizeAnimator = gfx.animator.new(BallSizeMs, 9, SmallestBallRadius, easingHill) +local ballSizeAnimator = gfx.animator.new(BallSizeMs, 9, SmallestBallRadius, utils.easingHill) local HitMult = 20 @@ -151,14 +183,14 @@ local First , Second , Third , Home = 1, 2, 3, 4 ---@type Base[] local Bases = { - xy(Screen.W * 0.93, Screen.H * 0.52), - xy(Screen.W * 0.47, Screen.H * 0.19), - xy(Screen.W * 0.03, Screen.H * 0.52), - xy(Screen.W * 0.474, Screen.H * 0.79), + utils.xy(Screen.W * 0.93, Screen.H * 0.52), + utils.xy(Screen.W * 0.47, Screen.H * 0.19), + utils.xy(Screen.W * 0.03, Screen.H * 0.52), + utils.xy(Screen.W * 0.474, Screen.H * 0.79), } -- Pseudo-base for batter to target -local RightHandedBattersBox = xy(Bases[Home].x - 35, Bases[Home].y) +local RightHandedBattersBox = utils.xy(Bases[Home].x - 35, Bases[Home].y) ---@type table local NextBaseMap = { @@ -196,25 +228,36 @@ local PitcherStartPos = { y = Screen.H * 0.40, } +--- Actually only benches the infield, because outfielders are far away! +---@param position XYPair +function benchAllFielders(position) + fielders.first.target = position + fielders.second.target = position + fielders.shortstop.target = position + fielders.third.target = position + fielders.pitcher.target = position + fielders.catcher.target = position +end + --- Resets the target positions of all fielders to their defaults (at their field positions). ----@param fromOffTheField boolean If provided, also sets all runners' current position to one centralized location. +---@param fromOffTheField XYPair | nil If provided, also sets all runners' current position to one centralized location. function resetFielderPositions(fromOffTheField) if fromOffTheField then for _, fielder in pairs(fielders) do - fielder.x = Center.x - fielder.y = Screen.H + fielder.x = fromOffTheField.x + fielder.y = fromOffTheField.y end end - fielders.first.target = xy(Screen.W - 65, Screen.H * 0.48) - fielders.second.target = xy(Screen.W * 0.70, Screen.H * 0.30) - fielders.shortstop.target = xy(Screen.W * 0.30, Screen.H * 0.30) - fielders.third.target = xy(Screen.W * 0.1, Screen.H * 0.48) - fielders.pitcher.target = xy(PitcherStartPos.x, PitcherStartPos.y) - fielders.catcher.target = xy(Screen.W * 0.475, Screen.H * 0.92) - fielders.left.target = xy(Screen.W * -1, Screen.H * -0.2) - fielders.center.target = xy(Center.x, Screen.H * -0.4) - fielders.right.target = xy(Screen.W * 2, fielders.left.target.y) + fielders.first.target = utils.xy(Screen.W - 65, Screen.H * 0.48) + fielders.second.target = utils.xy(Screen.W * 0.70, Screen.H * 0.30) + fielders.shortstop.target = utils.xy(Screen.W * 0.30, Screen.H * 0.30) + fielders.third.target = utils.xy(Screen.W * 0.1, Screen.H * 0.48) + fielders.pitcher.target = utils.xy(PitcherStartPos.x, PitcherStartPos.y) + fielders.catcher.target = utils.xy(Screen.W * 0.475, Screen.H * 0.92) + fielders.left.target = utils.xy(Screen.W * -1, Screen.H * -0.2) + fielders.center.target = utils.xy(Center.x, Screen.H * -0.4) + fielders.right.target = utils.xy(Screen.W * 2, fielders.left.target.y) end local BatterStartingX = Bases[Home].x - 40 @@ -251,14 +294,14 @@ local batter = newRunner() ---@param customBallScaler pd_animator | nil function throwBall(destX, destY, easingFunc, flyTimeMs, floaty, customBallScaler) if not flyTimeMs then - flyTimeMs = distanceBetween(ball.x, ball.y, destX, destY) * 5 + flyTimeMs = utils.distanceBetween(ball.x, ball.y, destX, destY) * 5 end ball.heldBy = nil if customBallScaler then ballSizeAnimator = customBallScaler else -- TODO? Scale based on distance? - ballSizeAnimator = gfx.animator.new(flyTimeMs, 9, SmallestBallRadius, easingHill) + ballSizeAnimator = gfx.animator.new(flyTimeMs, 9, SmallestBallRadius, utils.easingHill) end ballAnimatorY = gfx.animator.new(flyTimeMs, ball.y, destY, easingFunc) ballAnimatorX = gfx.animator.new(flyTimeMs, ball.x, destX, easingFunc) @@ -275,7 +318,7 @@ local catcherThrownBall = false function pitch() catcherThrownBall = false - currentMode = Modes.batting + offenseMode = Offense.batting local current = Pitches[math.random(#Pitches)] ballAnimatorX = current.x @@ -293,9 +336,8 @@ function pitch() secondsSincePitchAllowed = 0 end -local elapsedSec = 0 local crankChange = 0 -local acceleratedChange +local acceleratedChange = 0 local BaseHitbox = 10 @@ -305,7 +347,7 @@ local BaseHitbox = 10 ---@return Base | nil function isTouchingBase(x, y) for _, base in ipairs(Bases) do - if distanceBetween(x, y, base.x, base.y) < BaseHitbox then + if utils.distanceBetween(x, y, base.x, base.y) < BaseHitbox then return base end end @@ -320,26 +362,10 @@ local BallCatchHitbox = 3 ---@param y number ---@return boolean function isTouchingBall(x, y) - local ballDistance = distanceBetween(x, y, ball.x, ball.y) + local ballDistance = utils.distanceBetween(x, y, ball.x, ball.y) return ballDistance < BallCatchHitbox end ----@alias Team { score: number } - ----@type table -local teams = { - home = { - score = 0, - }, - away = { - score = 0, - }, -} - -local battingTeam = teams.away -local outs = 0 -local inning = 1 - ---@param base Base ---@return Runner | nil function getRunnerTargeting(base) @@ -367,6 +393,10 @@ function updateForcedRunners() end end +local ResetFieldersAfterSeconds = 5 +-- TODO: Replace with a timer, repeatedly reset, instead of setting to 0 +local secondsSinceLastRunnerMove = 0 + ---@param runnerIndex integer function outRunner(runnerIndex) outs = outs + 1 @@ -376,9 +406,12 @@ function outRunner(runnerIndex) announcer:say("YOU'RE OUT!") if outs == 3 then + local currentlyFieldingTeam = battingTeam == teams.home and teams.away or teams.home local gameOver = inning == 9 and teams.away.score ~= teams.home.score if not gameOver then fieldersDance() + secondsSinceLastRunnerMove = -7 + benchAllFielders(currentlyFieldingTeam.benchPosition) announcer:say("SWITCHING SIDES...") end while #runners > 0 do @@ -387,14 +420,13 @@ function outRunner(runnerIndex) -- Delay to keep end-of-inning on the scoreboard for a few seconds playdate.timer.new(3000, function() outs = 0 - if battingTeam == teams.home then - battingTeam = teams.away - inning = inning + 1 - else - battingTeam = teams.home - end + battingTeam = currentlyFieldingTeam if gameOver then announcer:say("AND THAT'S THE BALL GAME!") + else + if battingTeam == teams.home then + inning = inning + 1 + end end end) end @@ -427,9 +459,7 @@ end function getBaseOfStrandedRunner() local farRunnersBase, farDistance for _, runner in pairs(runners) do - local nearestBase, distance = getNearestOf(Bases, runner.x, runner.y, function(base) - return runner.nextBase == base - end) + local nearestBase, distance = utils.getNearestOf(Bases, runner.x, runner.y) if farRunnersBase == nil or farDistance < distance then farRunnersBase = nearestBase farDistance = distance @@ -459,8 +489,8 @@ end function tryToMakeAnOut(fielder) local targetX, targetY = getNextOutTarget() if targetX ~= nil and targetY ~= nil then - local nearestFielder = getNearestOf(fielders, targetX, targetY) - nearestFielder.target = xy(targetX, targetY) + local nearestFielder = utils.getNearestOf(fielders, targetX, targetY) + nearestFielder.target = utils.xy(targetX, targetY) if nearestFielder == fielder then ball.heldBy = fielder else @@ -476,7 +506,7 @@ end ---@param target { x: number, y: number } ---@return boolean function moveAtSpeed(mover, speed, target) - local x, y, distance = normalizeVector(mover.x, mover.y, target.x, target.y) + local x, y, distance = utils.normalizeVector(mover.x, mover.y, target.x, target.y) if distance > 1 then mover.x = mover.x - (x * speed) @@ -488,35 +518,30 @@ function moveAtSpeed(mover, speed, target) end ---@param fielder Fielder ----@param runnerBaseCache Cache -function updateFielder(fielder, runnerBaseCache) - -- TODO: Target unforced runners (or their target bases) for tagging - -- With new Position-based scheme, fielders are now able to set `fielder.target = runner` to track directly - +function updateFielder(fielder) if fielder.target ~= nil then if not moveAtSpeed(fielder, fielder.speed * deltaSeconds, fielder.target) then fielder.target = nil end end - if currentMode ~= Modes.running or not isTouchingBall(fielder.x, fielder.y) then + if offenseMode ~= Offense.running or not isTouchingBall(fielder.x, fielder.y) then return end -- TODO: Check for double-plays or other available outs. local touchedBase = isTouchingBase(fielder.x, fielder.y) for i, runner in pairs(runners) do + local runnerOnBase = isTouchingBase(runner.x, runner.y) if ( -- Force out touchedBase -- and runner.prevBase -- Make sure the runner is not standing at home and runner.forcedTo == touchedBase - and touchedBase ~= runnerBaseCache.get(runner) - ) - or ( -- Tag out - not runnerBaseCache.get(runner) - and distanceBetween(runner.x, runner.y, fielder.x, fielder.y) < TagDistance + and touchedBase ~= runnerOnBase ) + -- Tag out + or (not runnerOnBase and utils.distanceBetween(runner.x, runner.y, fielder.x, fielder.y) < TagDistance) then outRunner(i) playdate.timer.new(750, function() @@ -529,15 +554,11 @@ function updateFielder(fielder, runnerBaseCache) end function updateFielders() - local runnerBaseCache = buildCache(function(runner) - return isTouchingBase(runner.x, runner.y) - end) - for _, fielder in pairs(fielders) do - updateFielder(fielder, runnerBaseCache) + updateFielder(fielder) end - -- if currentMode == Modes.batting then + -- if offenseMode == Offense.batting then -- moveAtSpeed( -- fielders.catcher, -- fielders.catcher.speed * 2 * deltaSeconds, @@ -549,8 +570,9 @@ end --- Returns true only if the given runner moved during this update. ---@param runner Runner | nil ---@param runnerIndex integer | nil May only be nil if runner == batter +---@param appliedSpeed number ---@return boolean -function updateRunner(runner, runnerIndex) +function updateRunner(runner, runnerIndex, appliedSpeed) local autoRunSpeed = 20 * deltaSeconds --autoRunSpeed = 140 @@ -558,8 +580,7 @@ function updateRunner(runner, runnerIndex) return false end - local appliedSpeed = crankChange -- TODO: Allow for individual runner control via buttons - local nearestBase, nearestBaseDistance = getNearestOf(Bases, runner.x, runner.y) + local nearestBase, nearestBaseDistance = utils.getNearestOf(Bases, runner.x, runner.y) if nearestBaseDistance < 5 @@ -572,9 +593,9 @@ function updateRunner(runner, runnerIndex) end local nb = runner.nextBase - local x, y, distance = normalizeVector(runner.x, runner.y, nb.x, nb.y) + local x, y, distance = utils.normalizeVector(runner.x, runner.y, nb.x, nb.y) - if distance < 1 then + if distance < 2 then runner.nextBase = NextBaseMap[runner.nextBase] runner.forcedTo = nil return false @@ -602,10 +623,6 @@ function updateRunner(runner, runnerIndex) return prevX ~= runner.x or prevY ~= runner.y end -local ResetFieldersAfterSeconds = 5 --- TODO: Replace with a timer, repeatedly reset, instead of setting to 0 -local secondsSinceLastRunnerMove = 0 - ---@type number local batAngleDeg @@ -624,12 +641,12 @@ function updateBatting() batTip.y = batBase.y + (BatLength * math.cos(batAngle)) if - acceleratedChange > 0 - and pointDirectlyUnderLine(ball.x, ball.y, batBase.x, batBase.y, batTip.x, batTip.y, Screen.H) + acceleratedChange >= 0 -- > 0 + and utils.pointDirectlyUnderLine(ball.x, ball.y, batBase.x, batBase.y, batTip.x, batTip.y, Screen.H) and ball.y < 232 --not isTouchingBall(fielders.catcher.x, fielders.catcher.y) then BatCrackSound:play() - currentMode = Modes.running + offenseMode = Offense.running local ballAngle = batAngle + math.rad(90) local mult = math.abs(crankChange / 15) @@ -648,7 +665,7 @@ function updateBatting() playdate.easingFunctions.outQuint, 2000, nil, - gfx.animator.new(2000, 9 + (mult * mult * 0.5), SmallestBallRadius, easingHill) + gfx.animator.new(2000, 9 + (mult * mult * 0.5), SmallestBallRadius, utils.easingHill) ) fielders.first.target = Bases[First] @@ -658,18 +675,19 @@ function updateBatting() batter.forcedTo = Bases[First] batter = nil -- Demote batter to a mere runner - local chasingFielder = getNearestOf(fielders, ballDestX, ballDestY) + local chasingFielder = utils.getNearestOf(fielders, ballDestX, ballDestY) chasingFielder.target = { x = ballDestX, y = ballDestY } end end --- Update non-batter runners. --- Returns true only if at least one of the given runners moved during this update +---@param appliedSpeed number ---@return boolean -function updateRunning() +function updateRunning(appliedSpeed) ball.size = ballSizeAnimator:currentValue() - local nonBatterRunners = filter(runners, function(runner) + local nonBatterRunners = utils.filter(runners, function(runner) return runner ~= batter end) @@ -677,7 +695,7 @@ function updateRunning() -- TODO: Filter for the runner closest to the currently-held direction button for runnerIndex, runner in ipairs(nonBatterRunners) do - runnerMoved = updateRunner(runner, runnerIndex) or runnerMoved + runnerMoved = updateRunner(runner, runnerIndex, appliedSpeed) or runnerMoved end return runnerMoved @@ -697,7 +715,6 @@ end function updateGameState() deltaSeconds = playdate.getElapsedTime() or 0 playdate.resetElapsedTime() - elapsedSec = elapsedSec + deltaSeconds crankChange, acceleratedChange = playdate.getCrankChange() --[[@as number, number]] if ball.heldBy then @@ -708,7 +725,7 @@ function updateGameState() ball.y = ballAnimatorY:currentValue() + ballFloatAnimator:currentValue() end - if currentMode == Modes.batting then + if offenseMode == Offense.batting then secondsSincePitchAllowed = secondsSincePitchAllowed + deltaSeconds if secondsSincePitchAllowed > 3.5 and not catcherThrownBall then throwBall(PitchStartX, PitchStartY, playdate.easingFunctions.linear, nil, true) @@ -719,16 +736,19 @@ function updateGameState() end updateBatting() -- TODO: Ensure batter can't be nil, here - updateRunner(batter) - elseif currentMode == Modes.running then - if updateRunning() then + updateRunner(batter, nil, crankChange) + elseif offenseMode == Offense.running then + if playerIsOn(Sides.defense) then + updateRunning(999) + end + if updateRunning(crankChange) then secondsSinceLastRunnerMove = 0 else secondsSinceLastRunnerMove = secondsSinceLastRunnerMove + deltaSeconds if secondsSinceLastRunnerMove > ResetFieldersAfterSeconds then throwBall(PitchStartX, PitchStartY, playdate.easingFunctions.linear, nil, true) - resetFielderPositions(false) - currentMode = Modes.batting + resetFielderPositions() + offenseMode = Offense.batting batter = newRunner() end end @@ -763,7 +783,7 @@ function playdate.update() gfx.fillRect(fielder.x, fielder.y - fielderDanceHeight, 14, 25) end - if currentMode == Modes.batting then + if offenseMode == Offense.batting then gfx.setLineWidth(5) gfx.drawLine(batBase.x, batBase.y, batTip.x, batTip.y) end @@ -809,7 +829,7 @@ function init() playdate.display.setRefreshRate(50) gfx.setBackgroundColor(gfx.kColorWhite) playdate.setMenuImage(gfx.image.new("images/game/menu-image.png")) - resetFielderPositions(true) + resetFielderPositions(teams.home.benchPosition) playdate.getSystemMenu():addMenuItem("Restart game", function() end) -- TODO? playdate.timer.new(2000, function() diff --git a/src/test/testUtils.lua b/src/test/testUtils.lua new file mode 100644 index 0000000..9959762 --- /dev/null +++ b/src/test/testUtils.lua @@ -0,0 +1,15 @@ +import = function() end +luaunit = require("luaunit") +luaunit.ORDER_ACTUAL_EXPECTED = false + +utils = require("utils") + +function testFilter() + local numArr = { 5, 10, 15, 20 } + local greaterThanTen = function(n) + return n > 10 + end + luaunit.assertEquals({ 15, 20 }, utils.filter(numArr, greaterThanTen)) +end + +os.exit(luaunit.LuaUnit.run()) diff --git a/src/utils.lua b/src/utils.lua index 065d581..d3a9211 100644 --- a/src/utils.lua +++ b/src/utils.lua @@ -3,7 +3,10 @@ import 'CoreLibs/animation.lua' import 'CoreLibs/graphics.lua' -- stylua: ignore end -function easingHill(t, b, c, d) +-- selene: allow(unscoped_variables) +utils = {} + +function utils.easingHill(t, b, c, d) c = c + 0.0 -- convert to float to prevent integer overflow t = t / d t = ((t * 2) - 1) @@ -13,7 +16,7 @@ end -- Useful for quick print-the-value-in-place debugging. -- selene: allow(unused_variable) -function label(value, name) +function utils.label(value, name) if type(value) == "table" then print(name .. ":") printTable(value) @@ -26,7 +29,7 @@ end ---@param x number ---@param y number ---@return XYPair -function xy(x, y) +function utils.xy(x, y) return { x = x, y = y, @@ -39,8 +42,8 @@ end ---@param x2 number ---@param y2 number ---@return number x, number y, number distance -function normalizeVector(x1, y1, x2, y2) - local distance, a, b = distanceBetween(x1, y1, x2, y2) +function utils.normalizeVector(x1, y1, x2, y2) + local distance, a, b = utils.distanceBetween(x1, y1, x2, y2) return a / distance, b / distance, distance end @@ -48,7 +51,7 @@ end ---@param array T[] ---@param condition fun(T): boolean ---@return T[] -function filter(array, condition) +function utils.filter(array, condition) local newArray = {} for _, element in pairs(array) do if condition(element) then @@ -59,7 +62,7 @@ function filter(array, condition) end ---@return number, number, number -function distanceBetween(x1, y1, x2, y2) +function utils.distanceBetween(x1, y1, x2, y2) local a = x1 - x2 local b = y1 - y2 return math.sqrt((a * a) + (b * b)), a, b @@ -67,7 +70,7 @@ end --- Returns true only if the point is below the given line, within the x bounds of said line, and above the bottomBound --- @return boolean -function pointDirectlyUnderLine(pointX, pointY, lineX1, lineY1, lineX2, lineY2, bottomBound) +function utils.pointDirectlyUnderLine(pointX, pointY, lineX1, lineY1, lineX2, lineY2, bottomBound) -- This check currently assumes right-handedness. -- I.e. it assumes the ball is to the right of batBaseX if pointX < lineX1 or pointX > lineX2 or pointY > bottomBound then @@ -92,15 +95,15 @@ end ---@param x number ---@param y number ---@return T,number|nil -function getNearestOf(array, x, y, extraCondition) +function utils.getNearestOf(array, x, y, extraCondition) local nearest, nearestDistance = nil, nil for _, element in pairs(array) do if not extraCondition or extraCondition(element) then if nearest == nil then nearest = element - nearestDistance = distanceBetween(element.x, element.y, x, y) + nearestDistance = utils.distanceBetween(element.x, element.y, x, y) else - local distance = distanceBetween(element.x, element.y, x, y) + local distance = utils.distanceBetween(element.x, element.y, x, y) if distance < nearestDistance then nearest = element nearestDistance = distance @@ -114,32 +117,6 @@ end ---@alias Cache { get: fun(key: `Key`): `Value` } ---- Marker used by buildCache to indicate a cached `nil` value. -local NoValue = {} - ---- Build a simple fetcher cache. On calling `get()`, if no value has already ---- been fetched, calls `fetcher(key)`, then caches and returns that value. ---- ---- On reflection, it's probably pretty early for this optimization, and it's ---- optimizing in favor of CPU at the expense of memory, which is probably not ---- where the playdate's limitaitons lie. But it can stay for now. ---- ----@generic Key ----@generic Value ----@return Cache -function buildCache(fetcher) - local cacheData = {} - return { - get = function(key) - if cacheData[key] == NoValue then - return nil - end - if cacheData[key] ~= nil then - return cacheData[key] - end - local fetched = fetcher(key) - cacheData[key] = fetched ~= nil and fetched or NoValue - return cacheData[key] - end, - } +if not playdate then + return utils end