Compare commits

..

No commits in common. "027bb31bffbfc3b35d528f511b4bca38a99c8e24" and "b9d25e18d810b9b9eae83e7c5041a93acd98ea46" have entirely different histories.

12 changed files with 118 additions and 449 deletions

View File

@ -23,7 +23,7 @@ function Announcer.new()
}, { __index = Announcer })
end
local DurationMs <const> = 2000
local DurationMs <const> = 3000
function Announcer:popIn()
self.animatorY = AnnouncerAnimatorInY

View File

@ -1,5 +1,6 @@
-- selene: allow(unscoped_variables)
---@class Ball
---@field private animator pd_animator_lib
---@field x number
---@field y number
---@field z number
@ -9,15 +10,13 @@
---@field yAnimator SimpleAnimator
---@field sizeAnimator SimpleAnimator
---@field floatAnimator SimpleAnimator
---@field private animatorLib pd_animator_lib
---@field private flyTimeMs number
Ball = {}
---@param animatorLib pd_animator_lib
---@param animator pd_animator_lib
---@return Ball
function Ball.new(animatorLib)
function Ball.new(animator)
return setmetatable({
animatorLib = animatorLib,
animator = animator,
x = C.Center.x --[[@as number]],
y = C.Center.y --[[@as number]],
z = 0,
@ -30,26 +29,10 @@ function Ball.new(animatorLib)
-- TODO? Replace these with a ballAnimatorZ?
-- ...that might lose some of the magic of both. Compromise available? idk
sizeAnimator = utils.staticAnimator(C.SmallestBallRadius),
floatAnimator = animatorLib.new(2000, -60, 0, utils.easingHill),
floatAnimator = animator.new(2000, -60, 0, utils.easingHill),
}, { __index = Ball })
end
function Ball:updatePosition()
if self.heldBy then
self.x = self.heldBy.x
self.y = self.heldBy.y
self.z = C.GloveZ
self.size = C.SmallestBallRadius
else
self.x = self.xAnimator:currentValue()
local z = self.floatAnimator:currentValue()
-- TODO: This `+ z` is more graphics logic than physics logic
self.y = self.yAnimator:currentValue() + z
self.z = z
self.size = self.sizeAnimator:currentValue()
end
end
--- Launches the ball from its current position to the given destination.
---@param destX number
---@param destY number
@ -64,16 +47,14 @@ function Ball:launch(destX, destY, easingFunc, flyTimeMs, floaty, customBallScal
flyTimeMs = utils.distanceBetween(self.x, self.y, destX, destY) * C.DefaultLaunchPower
end
self.flyTimeMs = flyTimeMs
if customBallScaler then
self.sizeAnimator = customBallScaler
else
-- TODO? Scale based on distance?
self.sizeAnimator = self.animatorLib.new(flyTimeMs, 9, C.SmallestBallRadius, utils.easingHill)
self.sizeAnimator = self.animator.new(flyTimeMs, 9, C.SmallestBallRadius, utils.easingHill)
end
self.yAnimator = self.animatorLib.new(flyTimeMs, self.y, destY, easingFunc)
self.xAnimator = self.animatorLib.new(flyTimeMs, self.x, destX, easingFunc)
self.yAnimator = self.animator.new(flyTimeMs, self.y, destY, easingFunc)
self.xAnimator = self.animator.new(flyTimeMs, self.x, destX, easingFunc)
if floaty then
self.floatAnimator:reset(flyTimeMs)
end

View File

@ -17,9 +17,7 @@
---@field onThirdOut fun()
Baserunning = {}
-- TODO: Implement slides. Would require making fielders' gloves "real objects" whose state is tracked.
---@param announcer Announcer
---@param announcer any
---@return Baserunning
function Baserunning.new(announcer, onThirdOut)
local o = setmetatable({
@ -34,7 +32,7 @@ function Baserunning.new(announcer, onThirdOut)
onThirdOut = onThirdOut,
}, { __index = Baserunning })
o:pushNewBatter()
o.batter = o:newRunner()
return o
end
@ -79,7 +77,6 @@ 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
@ -117,14 +114,6 @@ 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
@ -138,7 +127,7 @@ function Baserunning:walkAwayOutRunners(deltaSeconds)
end
---@return Runner
function Baserunning:pushNewBatter()
function Baserunning:newRunner()
local new = {
-- imageSet = math.random() < C.WokeMeter and FemmeSet or MascSet, -- TODO? lol.
x = C.RightHandedBattersBox.x - 60,
@ -148,7 +137,6 @@ function Baserunning:pushNewBatter()
forcedTo = C.Bases[C.First],
}
self.runners[#self.runners + 1] = new
self.batter = new
return new
end
@ -191,10 +179,8 @@ function Baserunning:updateRunner(runner, runnerIndex, appliedSpeed, deltaSecond
-- TODO: Do a better job drifting runners toward their bases when appliedSpeed is low/zero
if distance < 2 then
if runner.prevBase ~= nearestBase then
runner.prevBase = runner.nextBase
runner.nextBase = C.NextBaseMap[runner.nextBase]
end
runner.prevBase = runner.nextBase
runner.nextBase = C.NextBaseMap[runner.nextBase]
runner.forcedTo = nil
return false, false
end
@ -210,9 +196,6 @@ function Baserunning:updateRunner(runner, runnerIndex, appliedSpeed, deltaSecond
end
end
-- TODO: Make this less "sticky" for the user.
-- Currently it can be a little hard to run *past* a base.
local autoRun = (nearestBaseDistance > 40 or runner.forcedTo) and mult * autoRunSpeed
or nearestBaseDistance < 5 and 0
or (nearestBase == runner.nextBase and autoRunSpeed or -1 * autoRunSpeed)
@ -226,21 +209,16 @@ end
--- Update non-batter runners.
--- Returns true only if at least one of the given runners moved during this update
---@param appliedSpeed number | fun(runner: Runner): number
---@param appliedSpeed number
---@return boolean someRunnerMoved, number runnersScored
function Baserunning:updateNonBatterRunners(appliedSpeed, forcedOnly, deltaSeconds)
local someRunnerMoved = false
local runnersScored = 0
local speedIsFunction = type(appliedSpeed) == "function"
-- TODO: Filter for the runner closest to the currently-held direction button
for runnerIndex, runner in ipairs(self.runners) do
if runner ~= self.batter and (not forcedOnly or runner.forcedTo) then
local speed = appliedSpeed
if speedIsFunction then
speed = appliedSpeed(runner)
end
local thisRunnerMoved, thisRunnerScored = self:updateRunner(runner, runnerIndex, speed, deltaSeconds)
local thisRunnerMoved, thisRunnerScored = self:updateRunner(runner, runnerIndex, appliedSpeed, deltaSeconds)
someRunnerMoved = someRunnerMoved or thisRunnerMoved
if thisRunnerScored then
runnersScored = runnersScored + 1
@ -254,7 +232,3 @@ function Baserunning:updateNonBatterRunners(appliedSpeed, forcedOnly, deltaSecon
return someRunnerMoved, runnersScored
end
if not playdate or playdate.TEST_MODE then
return Baserunning
end

View File

@ -2,8 +2,8 @@
C = {}
C.Screen = {
W = playdate and playdate.display.getWidth() or 400,
H = playdate and playdate.display.getHeight() or 240,
W = playdate.display.getWidth(),
H = playdate.display.getHeight(),
}
C.Center = utils.xy(C.Screen.W / 2, C.Screen.H / 2)
@ -86,7 +86,6 @@ C.BaseHitbox = 10
C.BattingPower = 25
C.BatterHandPos = utils.xy(25, 15)
C.GloveZ = 0 -- 10
C.SmallestBallRadius = 6
@ -131,7 +130,3 @@ C.UserThrowPower = 0.3
C.WalkedRunnerSpeed = 10
C.ResetFieldersAfterSeconds = 2.5
if not playdate then
return C
end

View File

@ -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:pushNewBatter()
br:pushNewBatter()
br:pushNewBatter()
br:newRunner()
br:newRunner()
br:newRunner()
br.runners[2].x = C.Bases[C.First].x
br.runners[2].y = C.Bases[C.First].y

View File

@ -1,16 +1,14 @@
--- @class Fielder {
--- @field catchEligible boolean
--- @field x number
--- @field y number
--- @field target XyPair | nil
--- @field speed number
-- TODO: Run down baserunners in a pickle.
--- @alias Fielder {
--- x: number,
--- y: number,
--- target: XyPair | nil,
--- speed: number,
--- }
-- selene: allow(unscoped_variables)
---@class Fielding
---@field fielders table<string, Fielder>
---@field fielderHoldingBall Fielder | nil
---@field fielderTouchingBall Fielder | nil
Fielding = {}
local FielderDanceAnimator <const> = playdate.graphics.animator.new(1, 10, 0, utils.easingHill)
@ -23,7 +21,6 @@ local function newFielder(name, speed)
return {
name = name,
speed = speed * C.FielderRunMult,
catchEligible = true,
}
end
@ -41,7 +38,7 @@ function Fielding.new()
right = newFielder("Right", 50),
},
---@type Fielder | nil
fielderHoldingBall = nil,
fielderTouchingBall = nil,
}, { __index = Fielding })
end
@ -80,7 +77,7 @@ end
---@param deltaSeconds number
---@param fielder Fielder
---@param ballPos XyPair
---@return boolean inCatchingRange
---@return boolean isTouchingBall
local function updateFielderPosition(deltaSeconds, fielder, ballPos)
if fielder.target ~= nil then
if not utils.moveAtSpeed(fielder, fielder.speed * deltaSeconds, fielder.target) then
@ -109,62 +106,37 @@ function Fielding:haveSomeoneChase(ballDestX, ballDestY)
end
end
---@param ball Ball
---@param ball XyPair
---@param deltaSeconds number
---@return Fielder | nil fielderHoldingBall nil if no fielder is currently touching the ball
---@return Fielder | nil fielderTouchingBall nil if no fielder is currently touching the ball
function Fielding:updateFielderPositions(ball, deltaSeconds)
local fielderHoldingBall = nil
local fielderTouchingBall = nil
for _, fielder in pairs(self.fielders) do
local inCatchingRange = updateFielderPosition(deltaSeconds, fielder, ball)
if inCatchingRange and fielder.catchEligible then
-- TODO: Base this catch on fielder skill?
fielderHoldingBall = fielder
ball.heldBy = fielder -- How much havoc will this wreak?
local isTouchingBall = updateFielderPosition(deltaSeconds, fielder, ball)
if isTouchingBall then
fielderTouchingBall = fielder
end
end
-- TODO: The need is growing for a distinction between touching the ball and holding the ball.
-- Or, at least, fielders need to start *stopping* the ball when they make contact with it.
-- Right now, a line-drive *through* first will be counted as an out.
self.fielderHoldingBall = fielderHoldingBall
return fielderHoldingBall
end
---@param fielder Fielder
function Fielding.markIneligible(fielder)
fielder.catchEligible = false
playdate.timer.new(500, function()
fielder.catchEligible = true
end)
end
function Fielding:markAllIneligible()
for _, fielder in pairs(self.fielders) do
fielder.catchEligible = false
end
playdate.timer.new(750, function()
for _, fielder in pairs(self.fielders) do
fielder.catchEligible = true
end
end)
self.fielderTouchingBall = fielderTouchingBall
return fielderTouchingBall
end
-- TODO? Start moving target fielders close sooner?
---@param field Fielding
---@param field table
---@param targetBase Base
---@param launchBall LaunchBall
---@param throwFlyMs number
---@return ActionResult
local function userThrowToImpl(field, targetBase, launchBall, throwFlyMs)
if field.fielderHoldingBall == nil then
if field.fielderTouchingBall == nil then
return ActionResult.NeedsMoreTime
end
local closestFielder = utils.getNearestOf(field.fielders, targetBase.x, targetBase.y, function(fielder)
return fielder ~= field.fielderHoldingBall -- presumably, this is who will be doing the throwing
return fielder ~= field.fielderTouchingBall -- presumably, this is who will be doing the throwing
end)
closestFielder.target = targetBase
launchBall(targetBase.x, targetBase.y, playdate.easingFunctions.linear, throwFlyMs)
Fielding.markIneligible(field.fielderHoldingBall)
return ActionResult.Succeeded
end
@ -195,7 +167,3 @@ function Fielding:drawFielders(fielderSprites, ball)
end
return ballIsHeld
end
if not playdate or playdate.TEST_MODE then
return { Fielding, newFielder }
end

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 31 KiB

View File

@ -42,23 +42,15 @@ local gfx <const>, C <const> = playdate.graphics, C
local announcer = Announcer.new()
local fielding = Fielding.new()
-- TODO: Find a way to get baserunning and npc instantiated closer to the top, here.
-- Currently difficult because they depend on nextHalfInning/each other.
------------------
-- GLOBAL STATE --
------------------
---@alias SimpleAnimator { currentValue: fun(self): number; reset: fun(self, durationMs: number | nil) }
local deltaSeconds = 0
local ball = Ball.new(gfx.animator)
local batBase <const> = utils.xy(C.Center.x - 34, 215)
local batTip <const> = utils.xy(0, 0)
local batAngleDeg = C.CrankOffsetDeg
local catcherThrownBall = false
---@alias Team { score: number, benchPosition: XyPair }
---@type table<string, Team>
local teams <const> = {
home = {
@ -71,28 +63,25 @@ local teams <const> = {
},
}
local inning = 1
local UserTeam <const> = teams.away
local battingTeam = teams.away
local battingTeamSprites = AwayTeamSprites
local fieldingTeamSprites = HomeTeamSprites
local runnerBlipper = battingTeam == teams.away and AwayTeamBlipper or HomeTeamBlipper
local inning = 1
local offenseState = C.Offense.batting
-- TODO: Replace with timers, repeatedly reset, instead of constantly setting to 0
local secondsSinceLastRunnerMove = 0
local secondsSincePitchAllowed = 0
local secondsSincePitchAllowed = -5
-- These are only sort-of global state. They are purely graphical,
-- but they need to be kept in sync with the rest of the globals.
local runnerBlipper = battingTeam == teams.away and AwayTeamBlipper or HomeTeamBlipper
local battingTeamSprites = AwayTeamSprites
local fieldingTeamSprites = HomeTeamSprites
local catcherThrownBall = false
-------------------------
-- END OF GLOBAL STATE --
-------------------------
local batBase <const> = utils.xy(C.Center.x - 34, 215)
local batTip <const> = utils.xy(0, 0)
local UserTeam <const> = teams.away
local batAngleDeg = C.CrankOffsetDeg
---@alias SimpleAnimator { currentValue: fun(self): number; reset: fun(self, durationMs: number | nil) }
---@alias Pitch { x: SimpleAnimator, y: SimpleAnimator, z: SimpleAnimator | nil }
---@type Pitch[]
@ -124,12 +113,8 @@ local Pitches <const> = {
},
}
---@return boolean userIsOnSide, boolean userIsOnOtherSide
---@return boolean userIsOnSide, boolean playerIsOnOtherSide
local function userIsOn(side)
if UserTeam == nil then
-- Both teams are NPC-driven
return false, false
end
local ret
if UserTeam == battingTeam then
ret = side == C.Sides.offense
@ -139,7 +124,13 @@ local function userIsOn(side)
return ret, not ret
end
---@type LaunchBall
--- Launches the ball from its current position to the given destination.
---@param destX number
---@param destY number
---@param easingFunc EasingFunc
---@param flyTimeMs number | nil
---@param floaty boolean | nil
---@param customBallScaler pd_animator | nil
local function launchBall(destX, destY, easingFunc, flyTimeMs, floaty, customBallScaler)
throwMeter:reset()
ball:launch(destX, destY, easingFunc, flyTimeMs, floaty, customBallScaler)
@ -148,8 +139,6 @@ end
---@param pitchFlyTimeMs number | nil
---@param pitchTypeIndex number | nil
local function pitch(pitchFlyTimeMs, pitchTypeIndex)
Fielding.markIneligible(fielding.fielders.pitcher)
ball.heldBy = nil
catcherThrownBall = false
offenseState = C.Offense.batting
@ -185,26 +174,28 @@ local function nextHalfInning()
announcer:say("SWITCHING SIDES...")
end
if gameOver then
announcer:say("AND THAT'S THE BALL GAME!")
else
fielding:resetFielderPositions()
if battingTeam == teams.home then
inning = inning + 1
end
-- TODO: Make the overlay handle its own dang delay.
-- Delay to keep end-of-inning on the scoreboard for a few seconds
playdate.timer.new(3000, function()
battingTeam = currentlyFieldingTeam
playdate.timer.new(2000, function()
if battingTeam == teams.home then
battingTeamSprites = HomeTeamSprites
runnerBlipper = HomeTeamBlipper
fieldingTeamSprites = AwayTeamSprites
else
battingTeamSprites = AwayTeamSprites
fieldingTeamSprites = HomeTeamSprites
runnerBlipper = AwayTeamBlipper
end
if gameOver then
announcer:say("AND THAT'S THE BALL GAME!")
else
fielding:resetFielderPositions()
if battingTeam == teams.home then
battingTeamSprites = HomeTeamSprites
runnerBlipper = HomeTeamBlipper
fieldingTeamSprites = AwayTeamSprites
else
battingTeamSprites = AwayTeamSprites
fieldingTeamSprites = HomeTeamSprites
runnerBlipper = AwayTeamBlipper
inning = inning + 1
end
end)
end
end
end)
end
local baserunning = Baserunning.new(announcer, nextHalfInning)
@ -248,14 +239,13 @@ local function nextBatter()
playdate.timer.new(2000, function()
pitchTracker:reset()
if not baserunning.batter then
baserunning:pushNewBatter()
baserunning.batter = baserunning:newRunner()
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
@ -311,13 +301,17 @@ local function updateBatting(batDeg, batSpeed)
return
end
baserunning:convertBatterToRunner()
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
fielding:haveSomeoneChase(ballDestX, ballDestY)
end
end
---@param appliedSpeed number | fun(runner: Runner): number
---@param appliedSpeed number
---@return boolean someRunnerMoved
local function updateNonBatterRunners(appliedSpeed, forcedOnly)
local runnerMoved, runnersScored = baserunning:updateNonBatterRunners(appliedSpeed, forcedOnly, deltaSeconds)
@ -350,7 +344,16 @@ local function updateGameState()
crankLimited = crankLimited * -1
end
ball:updatePosition()
if ball.heldBy then
ball.x = ball.heldBy.x
ball.y = ball.heldBy.y
ball.size = C.SmallestBallRadius
else
ball.x = ball.xAnimator:currentValue()
ball.z = ball.floatAnimator:currentValue()
ball.y = ball.yAnimator:currentValue() + ball.z
ball.size = ball.sizeAnimator:currentValue()
end
local userOnOffense, userOnDefense = userIsOn(C.Sides.offense)
@ -408,10 +411,7 @@ local function updateGameState()
end
end
elseif offenseState == C.Offense.running then
local appliedSpeed = userOnOffense and crankLimited
or function(runner)
return npc:runningSpeed(runner, ball)
end
local appliedSpeed = userOnOffense and crankLimited or npc:runningSpeed(ball)
if updateNonBatterRunners(appliedSpeed) then
secondsSinceLastRunnerMove = 0
else
@ -419,12 +419,11 @@ local function updateGameState()
if secondsSinceLastRunnerMove > C.ResetFieldersAfterSeconds then
-- End of play. Throw the ball back to the pitcher
launchBall(C.PitchStartX, C.PitchStartY, playdate.easingFunctions.linear, nil, true)
fielding:markAllIneligible() -- This is ugly, and ideally would not be necessary if Fielding handled the return throw directly.
fielding:resetFielderPositions()
offenseState = C.Offense.batting
-- TODO: Remove, or replace with nextBatter()
if not baserunning.batter then
baserunning:pushNewBatter()
baserunning.batter = baserunning:newRunner()
end
end
end
@ -444,8 +443,8 @@ local function updateGameState()
end
if fielderHoldingBall then
local outedSomeRunner = baserunning:outEligibleRunners(fielderHoldingBall)
if not userOnDefense and offenseState == C.Offense.running then
npc:fielderAction(fielderHoldingBall, outedSomeRunner, ball, launchBall)
if userOnOffense then
npc:fielderAction(offenseState, fielderHoldingBall, outedSomeRunner, ball, launchBall)
end
end
@ -453,8 +452,6 @@ local function updateGameState()
actionQueue:runWaiting(deltaSeconds)
end
-- TODO: Swappable update() for main menu, etc.
function playdate.update()
playdate.timer.updateTimers()
gfx.animation.blinker.updateAll()

View File

@ -39,25 +39,26 @@ end
local baseRunningSpeed = 25
--- TODO: Individual runner control.
---@param runner Runner
---@param ball Point3d
---@return number
function Npc:runningSpeed(runner, ball)
function Npc:runningSpeed(ball)
if #self.runners == 0 then
return 0
end
local distanceFromBall = utils.distanceBetweenZ(ball.x, ball.y, ball.z, runner.x, runner.y, 0)
local runner1 = self.runners[1]
if distanceFromBall > 400 or runner.forcedTo then
local ballIsFar = utils.distanceBetweenZ(ball.x, ball.y, ball.z, runner1.x, runner1.y, 0) > 250
if ballIsFar or runner1.forcedTo then
return baseRunningSpeed
end
local touchedBase = utils.isTouchingBase(runner.x, runner.y)
if not touchedBase and runner.nextBase then
local distToNext = utils.distanceBetween(runner.x, runner.y, runner.nextBase.x, runner.nextBase.y)
local distToPrev = utils.distanceBetween(runner.x, runner.y, runner.prevBase.x, runner.prevBase.y)
if distToNext < distToPrev or distanceFromBall > 350 then
local touchedBase = utils.isTouchingBase(runner1.x, runner1.y)
if not touchedBase and runner1.nextBase then
local distToNext = utils.distanceBetween(runner1.x, runner1.y, runner1.nextBase.x, runner1.nextBase.y)
local distToPrev = utils.distanceBetween(runner1.x, runner1.y, runner1.prevBase.x, runner1.prevBase.y)
if distToNext < distToPrev then
return baseRunningSpeed
else
return -1 * baseRunningSpeed
@ -123,24 +124,25 @@ end
local function tryToMakeAPlay(fielders, fielder, runners, ball, launchBall)
local targetX, targetY = getNextOutTarget(runners)
if targetX ~= nil and targetY ~= nil then
local nearestFielder = utils.getNearestOf(fielders, targetX, targetY, function(grabCandidate)
return grabCandidate.catchEligible
end)
local nearestFielder = utils.getNearestOf(fielders, targetX, targetY)
nearestFielder.target = utils.xy(targetX, targetY)
if nearestFielder == fielder then
ball.heldBy = fielder
else
launchBall(targetX, targetY, playdate.easingFunctions.linear, nil, true)
Fielding.markIneligible(nearestFielder)
end
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(fielder, outedSomeRunner, ball, launchBall)
function Npc:fielderAction(offenseState, fielder, outedSomeRunner, ball, launchBall)
if offenseState ~= C.Offense.running then
return
end
if outedSomeRunner then
-- Delay a little before the next play
playdate.timer.new(750, function()

View File

@ -1,21 +0,0 @@
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 }

View File

@ -1,170 +0,0 @@
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())

View File

@ -1,57 +0,0 @@
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())