Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add current score to embed, display due up on 3rd out, indicate barrels #22

Merged
merged 4 commits into from
Jul 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
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 @@
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 @@
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 All @@ -52,7 +53,7 @@
spyOn(gameday, 'processMatchingPlay').and.callFake((
matchingPlay, messages, messageTrackers, playId, hitDistance
) => {
messageTrackers.forEach(mt => mt.done = true);

Check warning on line 56 in spec/gameday-spec.js

View workflow job for this annotation

GitHub Actions / build (20.x)

Arrow function should not return assignment
});
jasmine.clock().install();
await gameday.pollForSavantData(1, 'abc', [{}, {}], 350);
Expand All @@ -69,7 +70,7 @@
spyOn(gameday, 'processMatchingPlay').and.callFake((
matchingPlay, messages, messageTrackers, playId, hitDistance
) => {
messageTrackers.forEach(mt => mt.done = true);

Check warning on line 73 in spec/gameday-spec.js

View workflow job for this annotation

GitHub Actions / build (20.x)

Arrow function should not return assignment
});
jasmine.clock().install();
await gameday.pollForSavantData(1, 'xyz', [{}, {}], 350);
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();
});
});
});
Loading