Skip to content

Commit

Permalink
Merge pull request #22 from AlecM33/score-on-embed
Browse files Browse the repository at this point in the history
Add current score to embed, display due up on 3rd out, indicate barrels
  • Loading branch information
AlecM33 authored Jul 23, 2024
2 parents 66faf36 + e702995 commit 6f6fc75
Show file tree
Hide file tree
Showing 9 changed files with 170 additions and 51 deletions.
30 changes: 18 additions & 12 deletions modules/current-play-processor.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const globalCache = require('./global-cache');
const globals = require('../config/globals');
const liveFeed = require('./livefeed');

module.exports = {
process: (currentPlayJSON) => {
Expand Down Expand Up @@ -37,23 +38,32 @@ module.exports = {
}
}
}
/* two kinds of objects get processed - "at bats", which will have a "result" object, and events within at bats, which put
the same information in a "details" object. So we often have to check for both.
*/
return {
reply,
isStartEvent: currentPlayJSON.playEvents?.find(event => event?.details?.description === 'Status Change - In Progress'),
isOut: currentPlayJSON.result?.isOut || currentPlayJSON.details?.isOut,
outs: currentPlayJSON.count?.outs,
homeScore: (currentPlayJSON.result ? currentPlayJSON.result.homeScore : currentPlayJSON.details?.homeScore),
awayScore: (currentPlayJSON.result ? currentPlayJSON.result.awayScore : currentPlayJSON.details?.awayScore),
isComplete: currentPlayJSON.about?.isComplete,
description: (currentPlayJSON.result?.description || currentPlayJSON.details?.description),
event: (currentPlayJSON.result?.event || currentPlayJSON.details?.event),
eventType: (currentPlayJSON.result?.eventType || currentPlayJSON.details?.eventType),
isScoringPlay: (currentPlayJSON.about?.isScoringPlay || currentPlayJSON.details?.isScoringPlay),
isInPlay: (lastEvent?.details?.isInPlay || currentPlayJSON.details?.isInPlay),
playId: (lastEvent?.playId || currentPlayJSON.playId),
metricsAvailable: (lastEvent?.hitData?.launchSpeed !== undefined || currentPlayJSON.hitData?.launchSpeed !== undefined),
hitDistance: (lastEvent?.hitData?.totalDistance || currentPlayJSON.hitData?.totalDistance)
};
}
};

function addScore (reply, currentPlayJSON) {
reply += '\n';
const feed = liveFeed.init(globalCache.values.game.currentLiveFeed);
let homeScore, awayScore;
if (currentPlayJSON.result) {
homeScore = currentPlayJSON.result.homeScore;
Expand All @@ -62,11 +72,11 @@ function addScore (reply, currentPlayJSON) {
homeScore = currentPlayJSON.details.homeScore;
awayScore = currentPlayJSON.details.awayScore;
}
reply += (globalCache.values.game.currentLiveFeed.liveData.plays.currentPlay.about.halfInning === 'top'
? '# _' + globalCache.values.game.currentLiveFeed.gameData.teams.away.abbreviation + ' ' + awayScore + '_, ' +
globalCache.values.game.currentLiveFeed.gameData.teams.home.abbreviation + ' ' + homeScore
: '# ' + globalCache.values.game.currentLiveFeed.gameData.teams.away.abbreviation + ' ' + awayScore + ', _' +
globalCache.values.game.currentLiveFeed.gameData.teams.home.abbreviation + ' ' + homeScore + '_');
reply += (feed.halfInning() === 'top'
? '# _' + feed.awayAbbreviation() + ' ' + awayScore + '_, ' +
feed.homeAbbreviation() + ' ' + homeScore
: '# ' + feed.awayAbbreviation() + ' ' + awayScore + ', _' +
feed.homeAbbreviation() + ' ' + homeScore + '_');

return reply;
}
Expand All @@ -78,15 +88,11 @@ function addMetrics (lastEvent, reply) {
getFireEmojis(lastEvent.hitData.launchSpeed) + '\n';
reply += 'Launch Angle: ' + lastEvent.hitData.launchAngle + '° \n';
reply += 'Distance: ' + lastEvent.hitData.totalDistance + ' ft.\n';
reply += 'xBA: Pending...\n';
reply += lastEvent.hitData.totalDistance && lastEvent.hitData.totalDistance >= 300 ? 'HR/Park: Pending...' : '';
reply += 'xBA: Pending...';
reply += lastEvent.hitData.totalDistance && lastEvent.hitData.totalDistance >= 300 ? '\nHR/Park: Pending...' : '';
} else {
reply += '\n\n**Statcast Metrics:**\n';
reply += 'Exit Velocity: Unavailable\n';
reply += 'Launch Angle: Unavailable\n';
reply += 'Distance: Unavailable\n';
reply += 'xBA: Unavailable\n';
reply += 'HR/Park: Unavailable';
reply += 'Data was not available.';
}

return reply;
Expand Down
42 changes: 42 additions & 0 deletions modules/gameday-util.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
const liveFeed = require('./livefeed');
const globalCache = require('./global-cache');
const globals = require('../config/globals');
const ColorContrastChecker = require('color-contrast-checker');

module.exports = {
deriveHalfInning: (halfInningFull) => {
return halfInningFull === 'top' ? 'TOP' : 'BOT';
},

didGameEnd: (homeScore, awayScore) => {
const feed = liveFeed.init(globalCache.values.game.currentLiveFeed);
return feed.inning() >= 9
&& (
(homeScore > awayScore && feed.halfInning() === 'top')
|| (awayScore > homeScore && feed.halfInning() === 'bottom')
);
},

getConstrastingEmbedColors: () => {
const feed = liveFeed.init(globalCache.values.game.currentLiveFeed);
globalCache.values.game.homeTeamColor = globals.TEAMS.find(
team => team.id === feed.homeTeamId()
).primaryColor;
const awayTeam = globals.TEAMS.find(
team => team.id === feed.awayTeamId()
);
const colorContrastChecker = new ColorContrastChecker();
if (colorContrastChecker.isLevelCustom(globalCache.values.game.homeTeamColor, awayTeam.primaryColor, globals.TEAM_COLOR_CONTRAST_RATIO)) {
globalCache.values.game.awayTeamColor = awayTeam.primaryColor;
} else {
globalCache.values.game.awayTeamColor = awayTeam.secondaryColor;
}
},

getDueUp: () => {
const feed = liveFeed.init(globalCache.values.game.currentLiveFeed);
const linescore = feed.linescore();

return '\n\n**Due up**: ' + linescore.offense?.batter?.fullName + ', ' + linescore.offense?.onDeck?.fullName + ', ' + linescore.offense?.inHole?.fullName;
}
};
52 changes: 19 additions & 33 deletions modules/gameday.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ const currentPlayProcessor = require('./current-play-processor');
const { EmbedBuilder } = require('discord.js');
const globals = require('../config/globals');
const LOGGER = require('./logger')(process.env.LOG_LEVEL?.trim() || globals.LOG_LEVEL.INFO);
const ColorContrastChecker = require('color-contrast-checker');
const liveFeed = require('./livefeed');
const gamedayUtil = require('./gameday-util');

module.exports = {
statusPoll, subscribe, getConstrastingEmbedColors, processAndPushPlay, pollForSavantData, processMatchingPlay
statusPoll, subscribe, processAndPushPlay, pollForSavantData, processMatchingPlay
};

async function statusPoll (bot) {
Expand All @@ -29,7 +30,7 @@ async function statusPoll (bot) {
LOGGER.info('Gameday: polling stopped: a game is live.');
globalCache.resetGameCache();
globalCache.values.game.currentLiveFeed = await mlbAPIUtil.liveFeed(inProgressGame.gamePk);
module.exports.getConstrastingEmbedColors();
gamedayUtil.getConstrastingEmbedColors();
module.exports.subscribe(bot, inProgressGame, nearestGames);
} else {
setTimeout(pollingFunction, globals.SLOW_POLL_INTERVAL);
Expand Down Expand Up @@ -102,27 +103,13 @@ function subscribe (bot, liveGame, games) {
ws.addEventListener('close', (e) => LOGGER.info('Gameday socket closed: ' + JSON.stringify(e)));
}

function getConstrastingEmbedColors () {
globalCache.values.game.homeTeamColor = globals.TEAMS.find(
team => team.id === globalCache.values.game.currentLiveFeed.gameData.teams.home.id
).primaryColor;
const awayTeam = globals.TEAMS.find(
team => team.id === globalCache.values.game.currentLiveFeed.gameData.teams.away.id
);
const colorContrastChecker = new ColorContrastChecker();
if (colorContrastChecker.isLevelCustom(globalCache.values.game.homeTeamColor, awayTeam.primaryColor, globals.TEAM_COLOR_CONTRAST_RATIO)) {
globalCache.values.game.awayTeamColor = awayTeam.primaryColor;
} else {
globalCache.values.game.awayTeamColor = awayTeam.secondaryColor;
}
}

async function reportPlays (bot, gamePk) {
const currentPlay = globalCache.values.game.currentLiveFeed.liveData.plays.currentPlay;
const feed = liveFeed.init(globalCache.values.game.currentLiveFeed);
const currentPlay = feed.currentPlay();
const atBatIndex = currentPlay.atBatIndex;
const lastReportedCompleteAtBatIndex = globalCache.values.game.lastReportedCompleteAtBatIndex;
if (atBatIndex > 0) {
const lastAtBat = globalCache.values.game.currentLiveFeed.liveData.plays.allPlays
const lastAtBat = feed.allPlays()
.find((play) => play.about.atBatIndex === atBatIndex - 1);
if (lastAtBat && lastAtBat.about.hasReview) { // a play that's been challenged. We should report updates on it.
await processAndPushPlay(bot, currentPlayProcessor.process(lastAtBat), gamePk, atBatIndex - 1);
Expand Down Expand Up @@ -154,16 +141,19 @@ async function processAndPushPlay (bot, play, gamePk, atBatIndex) {
&& !globalCache.values.game.reportedDescriptions
.find(reportedDescription => reportedDescription.description === play.description && reportedDescription.atBatIndex === atBatIndex)) {
globalCache.values.game.reportedDescriptions.push({ description: play.description, atBatIndex });
const feed = liveFeed.init(globalCache.values.game.currentLiveFeed);
if (play.isComplete) {
globalCache.values.game.lastReportedCompleteAtBatIndex = atBatIndex;
}
const embed = new EmbedBuilder()
.setTitle(deriveHalfInning(globalCache.values.game.currentLiveFeed.liveData.plays.currentPlay.about.halfInning) + ' ' +
globalCache.values.game.currentLiveFeed.liveData.plays.currentPlay.about.inning + ', ' +
globalCache.values.game.currentLiveFeed.gameData.teams.away.abbreviation + ' vs. ' +
globalCache.values.game.currentLiveFeed.gameData.teams.home.abbreviation + (play.isScoringPlay ? ' - Scoring Play \u2757' : ''))
.setDescription(play.reply)
.setColor((globalCache.values.game.currentLiveFeed.liveData.plays.currentPlay.about.halfInning === 'top'
.setTitle(gamedayUtil.deriveHalfInning(feed.halfInning()) + ' ' +
feed.inning() + ', ' +
feed.awayAbbreviation() + (play.isScoringPlay
? ' vs. '
: ' ' + play.awayScore + ' - ' + play.homeScore + ' ') +
feed.homeAbbreviation() + (play.isScoringPlay ? ' - Scoring Play \u2757' : ''))
.setDescription(play.reply + (play.isOut && play.outs === 3 && !gamedayUtil.didGameEnd(play.homeScore, play.awayScore) ? gamedayUtil.getDueUp() : ''))
.setColor((feed.halfInning() === 'top'
? globalCache.values.game.awayTeamColor
: globalCache.values.game.homeTeamColor
));
Expand Down Expand Up @@ -209,7 +199,7 @@ async function sendDelayedMessage (play, gamePk, channelSubscription, returnedCh
}

async function maybePopulateAdvancedStatcastMetrics (play, messages, gamePk) {
if (play.isInPlay) {
if (play.isInPlay && play.metricsAvailable) {
if (play.playId) {
try {
// xBA and HR/Park for balls in play is available on a delay via baseballsavant.
Expand All @@ -224,7 +214,7 @@ async function maybePopulateAdvancedStatcastMetrics (play, messages, gamePk) {
notifySavantDataUnavailable(messages);
}
} else {
LOGGER.debug('Skipping savant poll - not in play.');
LOGGER.debug('Skipping savant poll - not in play or metrics unavailable.');
}
}

Expand Down Expand Up @@ -273,7 +263,7 @@ function processMatchingPlay (matchingPlay, messages, messageTrackers, playId, h
if (matchingPlay.xba && description.includes('xBA: Pending...')) {
LOGGER.debug('Editing with xba: ' + playId);
description = description.replaceAll('xBA: Pending...', 'xBA: ' + matchingPlay.xba +
(parseFloat(matchingPlay.xba) > 0.5 ? ' \uD83D\uDFE2' : ''));
(matchingPlay.is_barrel === 1 ? ' \uD83D\uDFE2 (Barreled)' : ''));
receivedEmbed.setDescription(description);
messages[i].edit({
embeds: [receivedEmbed]
Expand Down Expand Up @@ -301,7 +291,3 @@ function processMatchingPlay (matchingPlay, messages, messageTrackers, playId, h
}
}
}

function deriveHalfInning (halfInningFull) {
return halfInningFull === 'top' ? 'TOP' : 'BOT';
}
36 changes: 36 additions & 0 deletions modules/livefeed.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
module.exports = {
init: (liveFeed) => {
return {
timestamp: () => {
return liveFeed.metaData.timeStamp;
},
inning: () => {
return liveFeed.liveData.plays.currentPlay.about.inning;
},
halfInning: () => {
return liveFeed.liveData.plays.currentPlay.about.halfInning;
},
awayAbbreviation: () => {
return liveFeed.gameData.teams.away.abbreviation;
},
homeAbbreviation: () => {
return liveFeed.gameData.teams.home.abbreviation;
},
homeTeamId: () => {
return liveFeed.gameData.teams.home.id;
},
awayTeamId: () => {
return liveFeed.gameData.teams.away.id;
},
currentPlay: () => {
return liveFeed.liveData.plays.currentPlay;
},
allPlays: () => {
return liveFeed.liveData.plays.allPlays;
},
linescore: () => {
return liveFeed.liveData.linescore;
}
};
}
};
2 changes: 1 addition & 1 deletion spec/command-util-spec.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const commandUtil = require('../modules/command-util');

describe('commandUtil', () => {
describe('command-util', () => {
beforeAll(() => {});
describe('#formatSplits', () => {
it('should format splits for a player that has played on multiple teams in a season', async () => {
Expand Down
2 changes: 1 addition & 1 deletion spec/current-play-processor-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ const currentPlayProcessor = require('../modules/current-play-processor');
const globalCache = require('../modules/global-cache');
const examplePlays = require('./data/example-plays');

describe('currentPlayProcessor', () => {
describe('current-play-processor', () => {
beforeAll(() => {
globalCache.values.game.currentLiveFeed = require('./data/example-live-feed');
});
Expand Down
2 changes: 1 addition & 1 deletion spec/diff-patch-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const fs = require('fs');
const globalCache = require('../modules/global-cache');
const path = require('path');

describe('diffPatch', () => {
describe('diff-patch', () => {
let diff;

beforeAll(() => {
Expand Down
7 changes: 4 additions & 3 deletions spec/gameday-spec.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const gameday = require('../modules/gameday');
const gamedayUtil = require('../modules/gameday-util');
const mlbAPIUtil = require('../modules/MLB-API-util');
const globals = require('../config/globals');
const mockResponses = require('./data/mock-responses');
Expand All @@ -8,7 +9,7 @@ const { EmbedBuilder } = require('discord.js');
describe('gameday', () => {
describe('#statusPoll', () => {
beforeEach(() => {
spyOn(gameday, 'getConstrastingEmbedColors').and.stub();
spyOn(gamedayUtil, 'getConstrastingEmbedColors').and.stub();
spyOn(mlbAPIUtil, 'liveFeed').and.callFake((gamePk, fields) => {
return {};
});
Expand All @@ -23,7 +24,7 @@ describe('gameday', () => {
expect(gameday.subscribe).toHaveBeenCalled();
expect(mlbAPIUtil.liveFeed).toHaveBeenCalled();
expect(globalCache.resetGameCache).toHaveBeenCalled();
expect(gameday.getConstrastingEmbedColors).toHaveBeenCalled();
expect(gamedayUtil.getConstrastingEmbedColors).toHaveBeenCalled();
});

it('should continue polling if no game is live', async () => {
Expand All @@ -39,7 +40,7 @@ describe('gameday', () => {
expect(gameday.subscribe).not.toHaveBeenCalled();
expect(mlbAPIUtil.liveFeed).not.toHaveBeenCalled();
expect(globalCache.resetGameCache).not.toHaveBeenCalled();
expect(gameday.getConstrastingEmbedColors).not.toHaveBeenCalled();
expect(gamedayUtil.getConstrastingEmbedColors).not.toHaveBeenCalled();
jasmine.clock().uninstall();
});
});
Expand Down
48 changes: 48 additions & 0 deletions spec/gameday-util-spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
const gamedayUtil = require('../modules/gameday-util');
const liveFeed = require('../modules/livefeed');

describe('gameday-util', () => {
beforeAll(() => {});

describe('#didGameEnd', () => {
it('should say the game ended when the top of the 9th ended with the home team leading', async () => {
spyOn(liveFeed, 'init').and.returnValue({
inning: () => { return 9; },
halfInning: () => { return 'top'; }
});
expect(gamedayUtil.didGameEnd(3, 2)).toBeTrue();
});

it('should say the game ended when the bottom of the 9th ended with the away team leading', async () => {
spyOn(liveFeed, 'init').and.returnValue({
inning: () => { return 9; },
halfInning: () => { return 'bottom'; }
});
expect(gamedayUtil.didGameEnd(2, 3)).toBeTrue();
});

it('should say the game is still going if the game is tied', async () => {
spyOn(liveFeed, 'init').and.returnValue({
inning: () => { return 9; },
halfInning: () => { return 'bottom'; }
});
expect(gamedayUtil.didGameEnd(3, 3)).toBeFalse();
});

it('should say the game is still going the top of the 9th has ended with the away team leading', async () => {
spyOn(liveFeed, 'init').and.returnValue({
inning: () => { return 9; },
halfInning: () => { return 'top'; }
});
expect(gamedayUtil.didGameEnd(3, 10)).toBeFalse();
});

it('should say the game ended when the top of an extra inning ended with the home team leading', async () => {
spyOn(liveFeed, 'init').and.returnValue({
inning: () => { return 15; },
halfInning: () => { return 'top'; }
});
expect(gamedayUtil.didGameEnd(3, 2)).toBeTrue();
});
});
});

0 comments on commit 6f6fc75

Please sign in to comment.