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.
|
-- TODO: Implement slides. Would require making fielders' gloves "real objects" whose state is tracked.
|
||||||
|
|
||||||
---@param announcer any
|
---@param announcer Announcer
|
||||||
---@return Baserunning
|
---@return Baserunning
|
||||||
function Baserunning.new(announcer, onThirdOut)
|
function Baserunning.new(announcer, onThirdOut)
|
||||||
local o = setmetatable({
|
local o = setmetatable({
|
||||||
|
@ -34,7 +34,7 @@ function Baserunning.new(announcer, onThirdOut)
|
||||||
onThirdOut = onThirdOut,
|
onThirdOut = onThirdOut,
|
||||||
}, { __index = Baserunning })
|
}, { __index = Baserunning })
|
||||||
|
|
||||||
o.batter = o:newRunner()
|
o:pushNewBatter()
|
||||||
|
|
||||||
return o
|
return o
|
||||||
end
|
end
|
||||||
|
@ -79,6 +79,7 @@ end
|
||||||
function Baserunning:outEligibleRunners(fielder)
|
function Baserunning:outEligibleRunners(fielder)
|
||||||
local touchedBase = utils.isTouchingBase(fielder.x, fielder.y)
|
local touchedBase = utils.isTouchingBase(fielder.x, fielder.y)
|
||||||
local didOutRunner = false
|
local didOutRunner = false
|
||||||
|
|
||||||
for i, runner in pairs(self.runners) do
|
for i, runner in pairs(self.runners) do
|
||||||
local runnerOnBase = utils.isTouchingBase(runner.x, runner.y)
|
local runnerOnBase = utils.isTouchingBase(runner.x, runner.y)
|
||||||
if -- Force out
|
if -- Force out
|
||||||
|
@ -116,6 +117,14 @@ function Baserunning:updateForcedRunners()
|
||||||
end
|
end
|
||||||
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
|
---@param deltaSeconds number
|
||||||
function Baserunning:walkAwayOutRunners(deltaSeconds)
|
function Baserunning:walkAwayOutRunners(deltaSeconds)
|
||||||
for i, runner in ipairs(self.outRunners) do
|
for i, runner in ipairs(self.outRunners) do
|
||||||
|
@ -129,7 +138,7 @@ function Baserunning:walkAwayOutRunners(deltaSeconds)
|
||||||
end
|
end
|
||||||
|
|
||||||
---@return Runner
|
---@return Runner
|
||||||
function Baserunning:newRunner()
|
function Baserunning:pushNewBatter()
|
||||||
local new = {
|
local new = {
|
||||||
-- imageSet = math.random() < C.WokeMeter and FemmeSet or MascSet, -- TODO? lol.
|
-- imageSet = math.random() < C.WokeMeter and FemmeSet or MascSet, -- TODO? lol.
|
||||||
x = C.RightHandedBattersBox.x - 60,
|
x = C.RightHandedBattersBox.x - 60,
|
||||||
|
@ -139,6 +148,7 @@ function Baserunning:newRunner()
|
||||||
forcedTo = C.Bases[C.First],
|
forcedTo = C.Bases[C.First],
|
||||||
}
|
}
|
||||||
self.runners[#self.runners + 1] = new
|
self.runners[#self.runners + 1] = new
|
||||||
|
self.batter = new
|
||||||
return new
|
return new
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -244,3 +254,7 @@ function Baserunning:updateNonBatterRunners(appliedSpeed, forcedOnly, deltaSecon
|
||||||
|
|
||||||
return someRunnerMoved, runnersScored
|
return someRunnerMoved, runnersScored
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if not playdate or playdate.TEST_MODE then
|
||||||
|
return Baserunning
|
||||||
|
end
|
||||||
|
|
|
@ -2,8 +2,8 @@
|
||||||
C = {}
|
C = {}
|
||||||
|
|
||||||
C.Screen = {
|
C.Screen = {
|
||||||
W = playdate.display.getWidth(),
|
W = playdate and playdate.display.getWidth() or 400,
|
||||||
H = playdate.display.getHeight(),
|
H = playdate and playdate.display.getHeight() or 240,
|
||||||
}
|
}
|
||||||
|
|
||||||
C.Center = utils.xy(C.Screen.W / 2, C.Screen.H / 2)
|
C.Center = utils.xy(C.Screen.W / 2, C.Screen.H / 2)
|
||||||
|
@ -131,3 +131,7 @@ C.UserThrowPower = 0.3
|
||||||
C.WalkedRunnerSpeed = 10
|
C.WalkedRunnerSpeed = 10
|
||||||
|
|
||||||
C.ResetFieldersAfterSeconds = 2.5
|
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.
|
-- Only works if called with the bases empty (i.e. the only runner should be the batter.
|
||||||
-- selene: allow(unused_variable)
|
-- selene: allow(unused_variable)
|
||||||
function dbg.loadTheBases(br)
|
function dbg.loadTheBases(br)
|
||||||
br:newRunner()
|
br:pushNewBatter()
|
||||||
br:newRunner()
|
br:pushNewBatter()
|
||||||
br:newRunner()
|
br:pushNewBatter()
|
||||||
|
|
||||||
br.runners[2].x = C.Bases[C.First].x
|
br.runners[2].x = C.Bases[C.First].x
|
||||||
br.runners[2].y = C.Bases[C.First].y
|
br.runners[2].y = C.Bases[C.First].y
|
||||||
|
|
|
@ -195,3 +195,7 @@ function Fielding:drawFielders(fielderSprites, ball)
|
||||||
end
|
end
|
||||||
return ballIsHeld
|
return ballIsHeld
|
||||||
end
|
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()
|
playdate.timer.new(2000, function()
|
||||||
pitchTracker:reset()
|
pitchTracker:reset()
|
||||||
if not baserunning.batter then
|
if not baserunning.batter then
|
||||||
baserunning.batter = baserunning:newRunner()
|
baserunning:pushNewBatter()
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function walk()
|
local function walk()
|
||||||
announcer:say("Walk!")
|
announcer:say("Walk!")
|
||||||
|
-- TODO? Use baserunning:convertBatterToRunner()
|
||||||
baserunning.batter.nextBase = C.Bases[C.First]
|
baserunning.batter.nextBase = C.Bases[C.First]
|
||||||
baserunning.batter.prevBase = C.Bases[C.Home]
|
baserunning.batter.prevBase = C.Bases[C.Home]
|
||||||
offenseState = C.Offense.walking
|
offenseState = C.Offense.walking
|
||||||
|
@ -310,11 +311,7 @@ local function updateBatting(batDeg, batSpeed)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
baserunning.batter.nextBase = C.Bases[C.First]
|
baserunning:convertBatterToRunner()
|
||||||
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
|
|
||||||
|
|
||||||
fielding:haveSomeoneChase(ballDestX, ballDestY)
|
fielding:haveSomeoneChase(ballDestX, ballDestY)
|
||||||
end
|
end
|
||||||
|
@ -427,7 +424,7 @@ local function updateGameState()
|
||||||
offenseState = C.Offense.batting
|
offenseState = C.Offense.batting
|
||||||
-- TODO: Remove, or replace with nextBatter()
|
-- TODO: Remove, or replace with nextBatter()
|
||||||
if not baserunning.batter then
|
if not baserunning.batter then
|
||||||
baserunning.batter = baserunning:newRunner()
|
baserunning:pushNewBatter()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -447,8 +444,8 @@ local function updateGameState()
|
||||||
end
|
end
|
||||||
if fielderHoldingBall then
|
if fielderHoldingBall then
|
||||||
local outedSomeRunner = baserunning:outEligibleRunners(fielderHoldingBall)
|
local outedSomeRunner = baserunning:outEligibleRunners(fielderHoldingBall)
|
||||||
if not userOnDefense then
|
if not userOnDefense and offenseState == C.Offense.running then
|
||||||
npc:fielderAction(offenseState, fielderHoldingBall, outedSomeRunner, ball, launchBall)
|
npc:fielderAction(fielderHoldingBall, outedSomeRunner, ball, launchBall)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -456,6 +453,8 @@ local function updateGameState()
|
||||||
actionQueue:runWaiting(deltaSeconds)
|
actionQueue:runWaiting(deltaSeconds)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- TODO: Swappable update() for main menu, etc.
|
||||||
|
|
||||||
function playdate.update()
|
function playdate.update()
|
||||||
playdate.timer.updateTimers()
|
playdate.timer.updateTimers()
|
||||||
gfx.animation.blinker.updateAll()
|
gfx.animation.blinker.updateAll()
|
||||||
|
|
|
@ -136,15 +136,11 @@ local function tryToMakeAPlay(fielders, fielder, runners, ball, launchBall)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param offenseState OffenseState
|
|
||||||
---@param fielder Fielder
|
---@param fielder Fielder
|
||||||
---@param outedSomeRunner boolean
|
---@param outedSomeRunner boolean
|
||||||
---@param ball { x: number, y: number, heldBy: Fielder | nil }
|
---@param ball { x: number, y: number, heldBy: Fielder | nil }
|
||||||
---@param launchBall LaunchBall
|
---@param launchBall LaunchBall
|
||||||
function Npc:fielderAction(offenseState, fielder, outedSomeRunner, ball, launchBall)
|
function Npc:fielderAction(fielder, outedSomeRunner, ball, launchBall)
|
||||||
if offenseState ~= C.Offense.running then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
if outedSomeRunner then
|
if outedSomeRunner then
|
||||||
-- Delay a little before the next play
|
-- Delay a little before the next play
|
||||||
playdate.timer.new(750, function()
|
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