From 027bb31bffbfc3b35d528f511b4bca38a99c8e24 Mon Sep 17 00:00:00 2001 From: Sage Vaillancourt Date: Wed, 12 Feb 2025 22:49:37 -0500 Subject: [PATCH] A bit of testing for baserunning and fielding. Required minor reworking to get those test harnesses in place. --- src/baserunning.lua | 20 ++++- src/constants.lua | 8 +- src/dbg.lua | 6 +- src/fielding.lua | 4 + src/main.lua | 17 ++-- src/npc.lua | 6 +- src/test/mocks.lua | 21 +++++ src/test/testBaserunning.lua | 170 +++++++++++++++++++++++++++++++++++ src/test/testFielding.lua | 57 ++++++++++++ 9 files changed, 287 insertions(+), 22 deletions(-) create mode 100644 src/test/mocks.lua create mode 100644 src/test/testBaserunning.lua create mode 100644 src/test/testFielding.lua diff --git a/src/baserunning.lua b/src/baserunning.lua index d36e019..6f5b3cb 100644 --- a/src/baserunning.lua +++ b/src/baserunning.lua @@ -19,7 +19,7 @@ Baserunning = {} -- TODO: Implement slides. Would require making fielders' gloves "real objects" whose state is tracked. ----@param announcer any +---@param announcer Announcer ---@return Baserunning function Baserunning.new(announcer, onThirdOut) local o = setmetatable({ @@ -34,7 +34,7 @@ function Baserunning.new(announcer, onThirdOut) onThirdOut = onThirdOut, }, { __index = Baserunning }) - o.batter = o:newRunner() + o:pushNewBatter() return o end @@ -79,6 +79,7 @@ end function Baserunning:outEligibleRunners(fielder) local touchedBase = utils.isTouchingBase(fielder.x, fielder.y) local didOutRunner = false + for i, runner in pairs(self.runners) do local runnerOnBase = utils.isTouchingBase(runner.x, runner.y) if -- Force out @@ -116,6 +117,14 @@ function Baserunning:updateForcedRunners() end end +function Baserunning:convertBatterToRunner() + self.batter.nextBase = C.Bases[C.First] + self.batter.prevBase = C.Bases[C.Home] + self:updateForcedRunners() + self.batter.forcedTo = C.Bases[C.First] + self.batter = nil -- Demote batter to a mere runner +end + ---@param deltaSeconds number function Baserunning:walkAwayOutRunners(deltaSeconds) for i, runner in ipairs(self.outRunners) do @@ -129,7 +138,7 @@ function Baserunning:walkAwayOutRunners(deltaSeconds) end ---@return Runner -function Baserunning:newRunner() +function Baserunning:pushNewBatter() local new = { -- imageSet = math.random() < C.WokeMeter and FemmeSet or MascSet, -- TODO? lol. x = C.RightHandedBattersBox.x - 60, @@ -139,6 +148,7 @@ function Baserunning:newRunner() forcedTo = C.Bases[C.First], } self.runners[#self.runners + 1] = new + self.batter = new return new end @@ -244,3 +254,7 @@ function Baserunning:updateNonBatterRunners(appliedSpeed, forcedOnly, deltaSecon return someRunnerMoved, runnersScored end + +if not playdate or playdate.TEST_MODE then + return Baserunning +end diff --git a/src/constants.lua b/src/constants.lua index d658e7c..a95877e 100644 --- a/src/constants.lua +++ b/src/constants.lua @@ -2,8 +2,8 @@ C = {} C.Screen = { - W = playdate.display.getWidth(), - H = playdate.display.getHeight(), + W = playdate and playdate.display.getWidth() or 400, + H = playdate and playdate.display.getHeight() or 240, } C.Center = utils.xy(C.Screen.W / 2, C.Screen.H / 2) @@ -131,3 +131,7 @@ C.UserThrowPower = 0.3 C.WalkedRunnerSpeed = 10 C.ResetFieldersAfterSeconds = 2.5 + +if not playdate then + return C +end diff --git a/src/dbg.lua b/src/dbg.lua index 4a4a79d..460f400 100644 --- a/src/dbg.lua +++ b/src/dbg.lua @@ -21,9 +21,9 @@ end -- Only works if called with the bases empty (i.e. the only runner should be the batter. -- selene: allow(unused_variable) function dbg.loadTheBases(br) - br:newRunner() - br:newRunner() - br:newRunner() + br:pushNewBatter() + br:pushNewBatter() + br:pushNewBatter() br.runners[2].x = C.Bases[C.First].x br.runners[2].y = C.Bases[C.First].y diff --git a/src/fielding.lua b/src/fielding.lua index 9746b97..30258fd 100644 --- a/src/fielding.lua +++ b/src/fielding.lua @@ -195,3 +195,7 @@ function Fielding:drawFielders(fielderSprites, ball) end return ballIsHeld end + +if not playdate or playdate.TEST_MODE then + return { Fielding, newFielder } +end diff --git a/src/main.lua b/src/main.lua index 4c3a2e2..b42fc8a 100644 --- a/src/main.lua +++ b/src/main.lua @@ -248,13 +248,14 @@ local function nextBatter() playdate.timer.new(2000, function() pitchTracker:reset() if not baserunning.batter then - baserunning.batter = baserunning:newRunner() + baserunning:pushNewBatter() end end) end local function walk() announcer:say("Walk!") + -- TODO? Use baserunning:convertBatterToRunner() baserunning.batter.nextBase = C.Bases[C.First] baserunning.batter.prevBase = C.Bases[C.Home] offenseState = C.Offense.walking @@ -310,11 +311,7 @@ local function updateBatting(batDeg, batSpeed) return end - baserunning.batter.nextBase = C.Bases[C.First] - baserunning.batter.prevBase = C.Bases[C.Home] - baserunning:updateForcedRunners() - baserunning.batter.forcedTo = C.Bases[C.First] - baserunning.batter = nil -- Demote batter to a mere runner + baserunning:convertBatterToRunner() fielding:haveSomeoneChase(ballDestX, ballDestY) end @@ -427,7 +424,7 @@ local function updateGameState() offenseState = C.Offense.batting -- TODO: Remove, or replace with nextBatter() if not baserunning.batter then - baserunning.batter = baserunning:newRunner() + baserunning:pushNewBatter() end end end @@ -447,8 +444,8 @@ local function updateGameState() end if fielderHoldingBall then local outedSomeRunner = baserunning:outEligibleRunners(fielderHoldingBall) - if not userOnDefense then - npc:fielderAction(offenseState, fielderHoldingBall, outedSomeRunner, ball, launchBall) + if not userOnDefense and offenseState == C.Offense.running then + npc:fielderAction(fielderHoldingBall, outedSomeRunner, ball, launchBall) end end @@ -456,6 +453,8 @@ local function updateGameState() actionQueue:runWaiting(deltaSeconds) end +-- TODO: Swappable update() for main menu, etc. + function playdate.update() playdate.timer.updateTimers() gfx.animation.blinker.updateAll() diff --git a/src/npc.lua b/src/npc.lua index f888566..ca17096 100644 --- a/src/npc.lua +++ b/src/npc.lua @@ -136,15 +136,11 @@ local function tryToMakeAPlay(fielders, fielder, runners, ball, launchBall) end end ----@param offenseState OffenseState ---@param fielder Fielder ---@param outedSomeRunner boolean ---@param ball { x: number, y: number, heldBy: Fielder | nil } ---@param launchBall LaunchBall -function Npc:fielderAction(offenseState, fielder, outedSomeRunner, ball, launchBall) - if offenseState ~= C.Offense.running then - return - end +function Npc:fielderAction(fielder, outedSomeRunner, ball, launchBall) if outedSomeRunner then -- Delay a little before the next play playdate.timer.new(750, function() diff --git a/src/test/mocks.lua b/src/test/mocks.lua new file mode 100644 index 0000000..08d69bd --- /dev/null +++ b/src/test/mocks.lua @@ -0,0 +1,21 @@ +utils = require("utils") + +local mockPlaydate = { + TEST_MODE = true, + graphics = { + animator = { + new = function() + return utils.staticAnimator(0) + end, + }, + }, +} + +---@type Announcer +local mockAnnouncer = { + say = function(self, message) + self.lastMessage = message + end, +} + +return { mockPlaydate, mockAnnouncer } diff --git a/src/test/testBaserunning.lua b/src/test/testBaserunning.lua new file mode 100644 index 0000000..18d6cef --- /dev/null +++ b/src/test/testBaserunning.lua @@ -0,0 +1,170 @@ +import = function() end +luaunit = require("luaunit") +luaunit.ORDER_ACTUAL_EXPECTED = false + +utils = require("utils") +C = require("constants") +mocks = require("test/mocks") +playdate, announcer = mocks[1], mocks[2] + +Baserunning = require("baserunning") + +local _f = require("fielding") +Fielding, newFielder = _f[1], _f[2] + +---@return Baserunning, { called: boolean } +function buildBaserunning() + local thirdOutCallbackData = { called = false } + local baserunning = Baserunning.new(announcer, function() + thirdOutCallbackData.called = true + end) + return baserunning, thirdOutCallbackData +end + +---@alias BaseIndexOrXyPair (integer | XyPair) + +--- NOTE: in addition to the given runners, there is implicitly a batter running from first. +---@param runnerLocations BaseIndexOrXyPair[] +---@return Baserunning +function buildRunnersOn(runnerLocations) + local baserunning = buildBaserunning() + baserunning:convertBatterToRunner() + for _, location in ipairs(runnerLocations) do + baserunning:pushNewBatter() + local runner = baserunning.batter + baserunning:convertBatterToRunner() + if type(location) == "number" then + -- Is a base index + -- Push the runner *through* each base. + for b = 1, location do + runner.x = C.Bases[b].x + runner.y = C.Bases[b].y + baserunning:updateNonBatterRunners(0.001, false, 0.001) + end + else + -- Is a raw XyPair + runner.x = location.x + runner.y = location.y + end + end + return baserunning +end + +---@alias Condition { fielderWithBallAt: XyPair, outWhen: BaseIndexOrXyPair[][], safeWhen: BaseIndexOrXyPair[][] } + +---@param expected boolean +---@param fielderWithBallAt XyPair +---@param when integer[][] +function assertRunnerOutCondition(expected, when, fielderWithBallAt) + local msg = expected and "out" or "safe" + for _, runnersOn in ipairs(when) do + local baserunning = buildRunnersOn(runnersOn) + local outedSomeRunner = baserunning:outEligibleRunners(fielderWithBallAt) + luaunit.failIf(outedSomeRunner ~= expected, "Runner should have been " .. msg .. ", but was not!") + end +end + +---@param condition Condition +function assertRunnerStatuses(condition) + assertRunnerOutCondition(true, condition.outWhen, condition.fielderWithBallAt) + assertRunnerOutCondition(false, condition.safeWhen, condition.fielderWithBallAt) +end + +function testForceOutsAtFirst() + assertRunnerStatuses({ + fielderWithBallAt = C.Bases[C.First], + outWhen = { + {}, + { 1 }, + { 2 }, + { 3 }, + { 1, 2 }, + { 1, 3 }, + { 2, 3 }, + { 1, 2, 3 }, + }, + safeWhen = {}, + }) +end + +function testForceOutsAtSecond() + assertRunnerStatuses({ + fielderWithBallAt = C.Bases[C.Second], + outWhen = { + { 1 }, + { 1, 2 }, + { 1, 3 }, + { 1, 2, 3 }, + }, + safeWhen = { + {}, + { 2 }, + { 3 }, + { 2, 3 }, + }, + }) +end + +function testForceOutsAtThird() + assertRunnerStatuses({ + fielderWithBallAt = C.Bases[C.Third], + outWhen = { + { 1, 2 }, + { 1, 2, 3 }, + }, + safeWhen = { + { 1 }, + { 2 }, + { 3 }, + { 2, 3 }, + { 1, 3 }, + }, + }) +end + +function testForceOutsAtHome() + assertRunnerStatuses({ + fielderWithBallAt = C.Bases[C.Home], + outWhen = { + { 1, 2, 3 }, + }, + safeWhen = { + {}, + { 1 }, + { 2 }, + { 3 }, + { 1, 2 }, + { 1, 3 }, + { 2, 3 }, + }, + }) +end + +function testTagOutsShouldHappenOffBase() + local fielderWithBallAt = utils.xy(10, 10) -- Some location not on a base. + local farFromFielder = utils.xy(100, 100) + assertRunnerStatuses({ + fielderWithBallAt = fielderWithBallAt, + outWhen = { + { fielderWithBallAt }, + }, + safeWhen = { + { farFromFielder }, + }, + }) +end + +function testTagOutsShouldNotHappenOnBase() + assertRunnerStatuses({ + fielderWithBallAt = C.Bases[C.Third], + outWhen = {}, + safeWhen = { + { 2 }, + { 3 }, + { 2, 3 }, + { 2, 3, 4 }, + }, + }) +end + +os.exit(luaunit.LuaUnit.run()) diff --git a/src/test/testFielding.lua b/src/test/testFielding.lua new file mode 100644 index 0000000..b7edc3c --- /dev/null +++ b/src/test/testFielding.lua @@ -0,0 +1,57 @@ +import = function() end +luaunit = require("luaunit") +luaunit.ORDER_ACTUAL_EXPECTED = false + +utils = require("utils") +C = require("constants") +mocks = require("test/mocks") +playdate = mocks[1] + +_f = require("fielding") +Fielding = _f[1] + +---@return Fielding, number fielderCount +local function fieldersAtDefaultPositions() + local fielding = Fielding.new() + fielding:resetFielderPositions() + + local fielderCount = 0 + for _, fielder in pairs(fielding.fielders) do + fielder.x = fielder.target.x + fielder.y = fielder.target.y + fielderCount = fielderCount + 1 + end + + return fielding, fielderCount +end + +---@param x number +---@param y number +---@param z number | nil +function fakeBall(x, y, z) + return { + x = x, + y = y, + z = z or 0, + heldBy = nil, + } +end + +function testBallPickedUpByNearbyFielders() + local fielding, fielderCount = fieldersAtDefaultPositions() + luaunit.assertIs("table", type(fielding)) + luaunit.assertIs("table", type(fielding.fielders)) + luaunit.assertEquals(9, fielderCount) + + local ball = fakeBall(-100, -100, -100) + fielding:updateFielderPositions(ball, 0.01) + luaunit.assertIsNil(ball.heldBy, "Ball should not be held by a fielder yet") + + local secondBaseman = fielding.fielders.second + ball.x = secondBaseman.x + ball.y = secondBaseman.y + fielding:updateFielderPositions(ball, 0.01) + luaunit.assertIs(secondBaseman, ball.heldBy, "Ball should be held by the nearest fielder") +end + +os.exit(luaunit.LuaUnit.run())