A bit of testing for baserunning and fielding.
Required minor reworking to get those test harnesses in place.
This commit is contained in:
parent
a801b64f55
commit
027bb31bff
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
17
src/main.lua
17
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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 }
|
|
@ -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())
|
|
@ -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())
|
Loading…
Reference in New Issue