From 4c9fbcdee7360d76581e60b58d9aa01a5ddfcfe0 Mon Sep 17 00:00:00 2001 From: Sage Vaillancourt Date: Mon, 17 Feb 2025 20:17:26 -0500 Subject: [PATCH] More advanced statistics and displays. --- src/assets.lua | 2 + src/dbg.lua | 68 ++++++-- src/draw/box-score.lua | 195 +++++++++++++++++++++-- src/images/game/GrassBackgroundSmall.png | Bin 0 -> 7202 bytes src/main.lua | 41 +++-- src/utils.lua | 15 +- 6 files changed, 275 insertions(+), 46 deletions(-) create mode 100644 src/images/game/GrassBackgroundSmall.png diff --git a/src/assets.lua b/src/assets.lua index 1a9b8a6..d1a5dba 100644 --- a/src/assets.lua +++ b/src/assets.lua @@ -26,6 +26,8 @@ Minimap = playdate.graphics.image.new("images/game/Minimap.png") -- luacheck: ignore GrassBackground = playdate.graphics.image.new("images/game/GrassBackground.png") -- luacheck: ignore +GrassBackgroundSmall = playdate.graphics.image.new("images/game/GrassBackgroundSmall.png") +-- luacheck: ignore LightPlayerBase = playdate.graphics.image.new("images/game/LightPlayerBase.png") -- luacheck: ignore LightPlayerBack = playdate.graphics.image.new("images/game/LightPlayerBack.png") diff --git a/src/dbg.lua b/src/dbg.lua index f92ffa6..7005789 100644 --- a/src/dbg.lua +++ b/src/dbg.lua @@ -35,23 +35,69 @@ function dbg.loadTheBases(br) br.runners[4].nextBase = C.Bases[C.Home] end ----@return BoxScoreData -function dbg.mockBoxScoreData(inningCount) +local hitSamples = { + away = { + { + utils.xy(7.88733, -16.3434), + utils.xy(378.3376, 30.49521), + utils.xy(367.1036, 21.55336), + }, + { + utils.xy(379.8051, -40.82794), + utils.xy(-444.5791, -30.30901), + utils.xy(-30.43079, -30.50307), + }, + { + utils.xy(227.8881, -14.56854), + utils.xy(293.5208, 39.38919), + utils.xy(154.4738, -26.55899), + }, + }, + home = { + { + utils.xy(146.2505, -89.12155), + utils.xy(429.5428, 59.62944), + utils.xy(272.4666, -78.578), + }, + { + utils.xy(485.0516, 112.8341), + utils.xy(290.9232, -4.946442), + utils.xy(263.4262, -6.482407), + }, + { + utils.xy(260.6927, -63.63049), + utils.xy(392.1548, -44.22421), + utils.xy(482.5545, 105.3476), + utils.xy(125.5928, 18.53091), + }, + }, +} + +---@return Statistics +function dbg.mockStatistics(inningCount) inningCount = inningCount or 9 - local data = { - home = {}, - away = {}, - } - for i = 1, inningCount do - data.home[i] = math.floor(math.random() * 5) - data.away[i] = math.floor(math.random() * 5) + local stats = Statistics.new() + for i = 1, inningCount - 1 do + stats.innings[i].home.score = math.floor(math.random() * 5) + stats.innings[i].away.score = math.floor(math.random() * 5) + if hitSamples.home[i] ~= nil then + stats.innings[i].home.hits = hitSamples.home[i] + end + if hitSamples.away[i] ~= nil then + stats.innings[i].away.hits = hitSamples.away[i] + end + stats:pushInning() end - return data + + local homeScore, awayScore = utils.totalScores(stats) + if homeScore == awayScore then + stats.innings[#stats.innings].home.score = 1 + stats.innings[#stats.innings].home.score + end + return stats end ---@param points XyPair[] function dbg.drawLine(points) - playdate.graphics.setColor(playdate.graphics.kColorWhite) for i = 2, #points do local prev = points[i - 1] local next = points[i] diff --git a/src/draw/box-score.lua b/src/draw/box-score.lua index 6fcd8e1..f6fcced 100644 --- a/src/draw/box-score.lua +++ b/src/draw/box-score.lua @@ -1,11 +1,51 @@ +---@alias TeamInningData { score: number, pitching: { balls: number, strikes: number }, hits: XyPair[] } + +--- E.g. statistics[1].home.pitching.balls +---@class Statistics +---@field innings: (table)[] + +Statistics = {} + +local function newTeamInning() + return { + score = 0, + pitching = { + balls = 0, + strikes = 0, + }, + hits = {}, + } +end + +---@return table +local function newInning() + return { + home = newTeamInning(), + away = newTeamInning(), + } +end + +---@return Statistics +function Statistics.new() + return setmetatable({ + innings = { newInning() }, + }, { __index = Statistics }) +end + +function Statistics:pushInning() + self.innings[#self.innings + 1] = newInning() +end + ---@class BoxScore ----@field data BoxScoreData +---@field stats Statistics +---@field private targetY number BoxScore = {} ----@param data BoxScoreData -function BoxScore.new(data) +---@param stats Statistics +function BoxScore.new(stats) return setmetatable({ - data = data, + stats = stats, + targetY = 0, }, { __index = BoxScore }) end @@ -19,6 +59,7 @@ end local MarginY = 70 +local SmallFont = playdate.graphics.font.new("fonts/font-full-circle.pft") local ScoreFont = playdate.graphics.font.new("fonts/Asheville-Sans-14-Bold.pft") local NumWidth = ScoreFont:getTextWidth("0") local NumHeight = ScoreFont:getHeight() @@ -61,19 +102,147 @@ local function drawInning(x, inningNumber, homeScore, awayScore) ScoreFont:drawTextAligned(homeScore, x, AwayY, gfx.kAlignRight) end -function BoxScore:update() - local originalDrawMode = gfx.getImageDrawMode() - gfx.clear(gfx.kColorBlack) - gfx.setImageDrawMode(gfx.kDrawModeInverted) - gfx.setColor(gfx.kColorBlack) +function BoxScore:drawBoxScore() local inningStart = 4 + (AwayWidth * 1.5) local widthAndMarg = InningDrawWidth + 4 ScoreFont:drawTextAligned(" HOME", 10, HomeY, gfx.kAlignRight) ScoreFont:drawTextAligned("AWAY", 10, AwayY, gfx.kAlignRight) - for i = 1, #self.data.away do - drawInning(inningStart + ((i - 1) * widthAndMarg), i, self.data.home[i], self.data.away[i]) + for i = 1, #self.stats.innings do + local inningStats = self.stats.innings[i] + drawInning(inningStart + ((i - 1) * widthAndMarg), i, inningStats.home.score, inningStats.away.score) end - local homeScore, awayScore = utils.totalScores(self.data) - drawInning(4 + inningStart + (widthAndMarg * #self.data.away), "F", homeScore, awayScore) + local homeScore, awayScore = utils.totalScores(self.stats) + drawInning(4 + inningStart + (widthAndMarg * #self.stats.innings), "F", homeScore, awayScore) + ScoreFont:drawTextAligned("v", C.Center.x, C.Screen.H - 40, gfx.kAlignCenter) +end + +local GraphM = 10 +local GraphW = C.Screen.W - (GraphM * 2) +local GraphH = C.Screen.H - (GraphM * 2) + +function BoxScore:drawScoreGraph(y) + -- TODO: Actually draw score legend + + -- Offset by 2 to support a) the zero-index b) the score legend + local segmentWidth = GraphW / (#self.stats.innings + 2) + + local legendX = segmentWidth * (#self.stats.innings + 2) - GraphM + gfx.drawLine(GraphM / 2, y + GraphM + GraphH, legendX, y + GraphM + GraphH) + gfx.drawLine(legendX, y + GraphM, legendX, y + GraphH + GraphM) + + gfx.setLineWidth(3) + local homeScore, awayScore = utils.totalScores(self.stats) + local highestScore = math.max(homeScore, awayScore) + + local heightPerPoint = (GraphH - 6) / highestScore + + function point(inning, score) + return utils.xy(GraphM + (inning * segmentWidth), y + GraphM + GraphH + (score * -heightPerPoint)) + end + + function drawLine(teamId) + local linePoints = { point(0, 0) } + local scoreTotal = 0 + for i, inning in ipairs(self.stats.innings) do + scoreTotal = scoreTotal + inning[teamId].score + linePoints[#linePoints + 1] = point(i, scoreTotal) + end + dbg.drawLine(linePoints) + local finalPoint = linePoints[#linePoints] + SmallFont:drawTextAligned(string.upper(teamId), finalPoint.x + 3, finalPoint.y - 7, gfx.kAlignRight) + end + + drawLine("home") + gfx.setDitherPattern(0.5) + drawLine("away") + gfx.setDitherPattern(0) +end + +---@param realHit XyPair +---@return XyPair +function convertHitToMini(realHit) + -- Convert to all-positive y + local y = realHit.y + C.Screen.H + y = y / 2 + + local x = realHit.x + C.Screen.W + x = x / 3 + return utils.xy(x, y) +end + +function BoxScore:drawHitChart(y) + local leftMargin = 8 + GrassBackgroundSmall:drawCentered(C.Center.x, y + C.Center.y + 54) + gfx.setLineWidth(1) + ScoreFont:drawTextAligned("AWAY", leftMargin, y + C.Screen.H - NumHeight, gfx.kAlignRight) + gfx.setColor(gfx.kColorBlack) + gfx.setDitherPattern(0.5, gfx.image.kDitherTypeBayer2x2) + gfx.fillRect(leftMargin, y + C.Screen.H - NumHeight, ScoreFont:getTextWidth("AWAY"), NumHeight) + + gfx.setColor(gfx.kColorWhite) + gfx.setDitherPattern(0.5) + for _, inning in ipairs(self.stats.innings) do + for _, hit in ipairs(inning.away.hits) do + local miniHitPos = convertHitToMini(hit) + gfx.fillCircleAtPoint(miniHitPos.x + 10, miniHitPos.y + y, 4) + end + end + gfx.setColor(gfx.kColorWhite) + gfx.setDitherPattern(0) + ScoreFont:drawTextAligned(" HOME", leftMargin, y + C.Screen.H - (NumHeight * 2), gfx.kAlignRight) + for _, inning in ipairs(self.stats.innings) do + for _, hit in ipairs(inning.home.hits) do + local miniHitPos = convertHitToMini(hit) + gfx.fillCircleAtPoint(miniHitPos.x + 10, miniHitPos.y + y, 4) + end + end +end + +local screens = { + BoxScore.drawBoxScore, + BoxScore.drawScoreGraph, + BoxScore.drawHitChart, +} + +function BoxScore:render() + local originalDrawMode = gfx.getImageDrawMode() + gfx.clear(gfx.kColorBlack) + gfx.setImageDrawMode(gfx.kDrawModeInverted) + gfx.setColor(gfx.kColorBlack) + + for i, screen in ipairs(screens) do + screen(self, (i - 1) * C.Screen.H) + end + gfx.setImageDrawMode(originalDrawMode) end + +local renderedImage + +function BoxScore:update() + if not renderedImage then + renderedImage = gfx.image.new(C.Screen.W, C.Screen.H * #screens) + gfx.pushContext(renderedImage) + self:render() + gfx.popContext() + end + + local deltaSeconds = playdate.getElapsedTime() + playdate.resetElapsedTime() + + gfx.setDrawOffset(0, self.targetY) + renderedImage:draw(0, 0) + + local crankChange = playdate.getCrankChange() + if crankChange ~= 0 then + self.targetY = self.targetY - (crankChange * 0.8) + else + local closestScreen = math.floor(0.5 + (self.targetY / C.Screen.H)) * C.Screen.H + if math.abs(self.targetY - closestScreen) > 3 then + local needsIncrease = self.targetY < closestScreen + local change = needsIncrease and 200 * deltaSeconds or -200 * deltaSeconds + self.targetY = self.targetY + change + end + end + self.targetY = math.max(math.min(self.targetY, 0), -C.Screen.H * (#screens - 1)) +end diff --git a/src/images/game/GrassBackgroundSmall.png b/src/images/game/GrassBackgroundSmall.png new file mode 100644 index 0000000000000000000000000000000000000000..d712d20bf9c23f0b2fe1d1058c9e389bbff980fb GIT binary patch literal 7202 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 QX8x86gBArInH!z?e`CXKZvX%Q literal 0 HcmV?d00001 diff --git a/src/main.lua b/src/main.lua index d10ae19..74e53a8 100644 --- a/src/main.lua +++ b/src/main.lua @@ -59,7 +59,6 @@ local teams = { }, } ----@alias BoxScoreData table ---@alias TeamId 'home' | 'away' --- Well, maybe not "Settings", but passive state that probably won't change much, if at all, during a game. @@ -76,7 +75,7 @@ local teams = { ---@field catcherThrownBall boolean ---@field offenseState OffenseState ---@field inning number ----@field boxScore BoxScoreData +---@field stats Statistics ---@field batBase XyPair ---@field batTip XyPair ---@field batAngleDeg number @@ -139,10 +138,7 @@ function Game.new(settings, announcer, fielding, baserunning, npc, state) battingTeamSprites = settings.awayTeamSprites, fieldingTeamSprites = settings.homeTeamSprites, runnerBlipper = runnerBlipper, - boxScore = { - home = { 0 }, - away = { 0 }, - }, + stats = Statistics.new(), }, }, { __index = Game }) @@ -267,7 +263,8 @@ end function Game:nextHalfInning() pitchTracker:reset() - local homeScore, awayScore = utils.totalScores(self.state.boxScore) + local homeScore, awayScore = utils.totalScores(self.state.stats) + -- TODO end the game if away team just batted and home team is winning local gameOver = self.state.battingTeam == "home" and self.state.inning == self.settings.finalInning and awayScore ~= homeScore @@ -275,7 +272,7 @@ function Game:nextHalfInning() if gameOver then self.announcer:say("AND THAT'S THE BALL GAME!") playdate.timer.new(3000, function() - transitionTo(BoxScore.new(self.state.boxScore)) + transitionTo(BoxScore.new(self.state.stats)) end) return end @@ -288,8 +285,7 @@ function Game:nextHalfInning() self.fielding:resetFielderPositions() if self.state.battingTeam == "home" then self.state.inning = self.state.inning + 1 - self.state.boxScore.home[self.state.inning] = 0 - self.state.boxScore.away[self.state.inning] = 0 + self.state.stats:pushInning() end self.state.battingTeam = getOppositeTeamId(self.state.battingTeam) playdate.timer.new(2000, function() @@ -305,10 +301,19 @@ function Game:nextHalfInning() end) end +---@return TeamInningData +function Game:battingTeamCurrentInning() + return self.state.stats.innings[self.state.inning][self.state.battingTeam] +end + +---@return TeamInningData +function Game:fieldingTeamCurrentInning() + return self.state.stats.innings[self.state.inning][getOppositeTeamId(self.state.battingTeam)] +end + ---@param scoredRunCount number function Game:score(scoredRunCount) - local battingTeamBoxScore = self.state.boxScore[self.state.battingTeam] - battingTeamBoxScore[self.state.inning] = battingTeamBoxScore[self.state.inning] + scoredRunCount + self:battingTeamCurrentInning().score = self:battingTeamCurrentInning().score + scoredRunCount self.announcer:say("SCORE!") end @@ -405,6 +410,8 @@ function Game:updateBatting(batDeg, batSpeed) local hitBallScaler = gfx.animator.new(2000, 9 + (mult * mult * 0.5), C.SmallestBallRadius, utils.easingHill) self.state.ball:launch(ballDestX, ballDestY, playdate.easingFunctions.outQuint, 2000, nil, hitBallScaler) -- TODO? A dramatic eye-level view on a home-run could be sick. + local battingTeamStats = self:battingTeamCurrentInning() + battingTeamStats.hits[#battingTeamStats.hits + 1] = utils.xy(ballDestX, ballDestY) if utils.isFoulBall(ballDestX, ballDestY) then self.announcer:say("Foul ball!") @@ -412,7 +419,6 @@ function Game:updateBatting(batDeg, batSpeed) -- TODO: Have a fielder chase for the fly-out return end - self.baserunning:convertBatterToRunner() self.fielding:haveSomeoneChase(ballDestX, ballDestY) @@ -476,6 +482,13 @@ function Game:updateGameState() if self.state.secondsSincePitchAllowed > C.ReturnToPitcherAfterSeconds and not self.state.catcherThrownBall then local outcome = pitchTracker:updatePitchCounts() + local currentPitchingStats = self:fieldingTeamCurrentInning().pitching + if outcome == PitchOutcomes.Strike or outcome == PitchOutcomes.StrikeOut then + currentPitchingStats.strikes = currentPitchingStats.strikes + 1 + end + if outcome == PitchOutcomes.Ball or outcome == PitchOutcomes.Walk then + currentPitchingStats.balls = currentPitchingStats.balls + 1 + end if outcome == PitchOutcomes.StrikeOut then self:strikeOut() elseif outcome == PitchOutcomes.Walk then @@ -638,7 +651,7 @@ function Game:update() if math.abs(offsetX) > 10 or math.abs(offsetY) > 10 then drawMinimap(self.baserunning.runners, self.fielding.fielders) end - local homeScore, awayScore = utils.totalScores(self.state.boxScore) + local homeScore, awayScore = utils.totalScores(self.state.stats) drawScoreboard( 0, C.Screen.H * 0.77, diff --git a/src/utils.lua b/src/utils.lua index 7f55c63..f010ce0 100644 --- a/src/utils.lua +++ b/src/utils.lua @@ -232,24 +232,23 @@ function utils.getNearestOf(array, x, y, extraCondition) return nearest, nearestDistance end ----@param box BoxScore +---@param stats Statistics ---@return number homeScore, number awayScore -function utils.totalScores(box) +function utils.totalScores(stats) local homeScore = 0 - for _, score in pairs(box.home) do - homeScore = homeScore + score - end - local awayScore = 0 - for _, score in pairs(box.away) do - awayScore = awayScore + score + for _, inning in pairs(stats.innings) do + homeScore = homeScore + inning.home.score + awayScore = awayScore + inning.away.score end return homeScore, awayScore end PitchOutcomes = { + Strike = {}, StrikeOut = {}, + Ball = {}, Walk = {}, }