diff --git a/frontend/src/app/components/ProfileRatingGraph/index.js b/frontend/src/app/components/ProfileRatingGraph/index.js index a48bc47f..beb73885 100644 --- a/frontend/src/app/components/ProfileRatingGraph/index.js +++ b/frontend/src/app/components/ProfileRatingGraph/index.js @@ -9,7 +9,7 @@ import { LineSeries, } from 'react-vis' -import { getRatingHistoryGraphForPlayer } from '../../modules/matches/matches-selectors' +import { getRatingHistoryGraphForPlayer } from '../../modules/players/players-selectors' import { Box, TextSpan } from '../../../styles/blocks' import { plotAxisStyle, plotGridStyle, plotLineStyle, plotMainGridStyle } from '../../../styles/svg' diff --git a/frontend/src/app/modules/matches/matches-computations.js b/frontend/src/app/modules/matches/matches-computations.js index 2fa49acd..4950a82d 100644 --- a/frontend/src/app/modules/matches/matches-computations.js +++ b/frontend/src/app/modules/matches/matches-computations.js @@ -1,17 +1,11 @@ -import { DEFAULT_DAYS_STATISTICS, DEFAULT_PLAYERS_STATISTICS } from '../../const/constants' - -const didPlayerWin = (playerId, match) => { - const winningTeam = match.team1Won ? match.team1 : match.team2 - return Boolean(winningTeam.find(player => player.id === playerId)) +export const didPlayerPlayMatch = (playerId, match) => { + const allPlayers = [...match.team1, ...match.team2] + return !!allPlayers.find(player => player.id === playerId) } -const getTeamMates = (playerId, match) => { - const playerTeam = match.team1.find(player => player.id === playerId) ? match.team1 : match.team2 - return playerTeam.filter(player => player.id !== playerId) -} - -const getOpponents = (playerId, match) => { - return match.team1.find(player => player.id === playerId) ? match.team2 : match.team1 +export const didPlayerWin = (playerId, { team1Won, team1, team2 }) => { + const winningTeam = team1Won ? team1 : team2 + return Boolean(winningTeam.find(player => player.id === playerId)) } export const generateMatchRatingChanges = (playerId, playerMatches) => playerMatches.map(match => { @@ -74,122 +68,3 @@ export const computeLongestWinStreak = (playerId, playerMatches) => { } }, initialState).longest } - -export const computeWinRatio = (playerId, playerMatches) => { - const wonMatchesCount = playerMatches.filter(match => didPlayerWin(playerId, match)).length - return (playerMatches.length > 0) ? (wonMatchesCount / playerMatches.length) : 0 -} - -export const computeWins = (playerId, playerMatches) => - playerMatches.filter(match => didPlayerWin(playerId, match)).length - -const findWithComparator = (comparator, arr, getField, baseScore) => - arr.reduce((acc, node) => - comparator(acc.score, getField(node)) - ? acc - : { value: node.value, score: getField(node) } - , { score: baseScore }) - -export const findMin = (arr, getField = node => node.score) => - findWithComparator((e1, e2) => e1 <= e2, arr, getField, Infinity) - -export const findMax = (arr, getField = node => node.score) => - findWithComparator((e1, e2) => e1 >= e2, arr, getField, -Infinity) - -export const computeDays = matchChanges => { - const daysMap = new Map() - - for (const match of matchChanges) { - const day = match.date.toLocaleDateString() - const score = Number(match.ratingChangeString) - daysMap.set(day, daysMap.has(day) - ? { - ...daysMap.get(day), - score: daysMap.get(day).score + score, - } - : { - value: day, - score, - }) - } - return matchChanges.length - ? Array.from(daysMap.values()) - : DEFAULT_DAYS_STATISTICS -} - -export const computeTeammateStats = (playerId, playerMatches) => - computePlayerStats(playerId, playerMatches, getTeamMates) || {} - -export const computeOpponentsStats = (playerId, playerMatches) => - computePlayerStats(playerId, playerMatches, getOpponents) || {} - -const computePlayerStats = (playerId, playerMatches, playersProvider) => { - const playersMap = new Map() - - for (const match of playerMatches) { - const players = playersProvider(playerId, match) - players.forEach(player => - playersMap.set(player.id, playersMap.has(player.id) - ? { - ...playersMap.get(player.id), - matches: playersMap.get(player.id)['matches'] + 1, - wins: playersMap.get(player.id)['wins'] += Number(didPlayerWin(playerId, match)), - losses: playersMap.get(player.id)['losses'] += Number(!didPlayerWin(playerId, match)), - } - : { - value: player, - matches: 1, - wins: Number(didPlayerWin(playerId, match)), - losses: Number(!didPlayerWin(playerId, match)), - })) - } - return playerMatches.length - ? Array.from(playersMap.values()) - : DEFAULT_PLAYERS_STATISTICS -} - -export const computeKingStreakDuration = (matchesLast, playersLast) => { - if (playersLast.length < 1) { - return null - } - - const playersMap = playersLast.reduce((map, player) => { - map[player.id] = player.rating - return map - }, new Map()) - - const kingId = playersLast[0].id - let matchFound = false - for (const match of matchesLast) { - const players = [...match.team1, ...match.team2] - - // #1 recomptute king's rating before in case he played the match - const kingPlayer = players.find(player => player.id === kingId) - let kingWon = false - if (kingPlayer) { - kingWon = playersMap[kingId] > kingPlayer.matchRating - playersMap[kingId] = kingPlayer.matchRating - } - - // #2 check whether none of the other players beat the king - for (const player of players) { - playersMap[player.id] = player.matchRating - matchFound |= (player.id !== kingId && player.matchRating > playersMap[kingId]) - } - - // #3 check if king was first before winning - if (kingPlayer && kingWon && !matchFound) { - for (const key in playersMap) { - matchFound |= (playersMap[key] > playersMap[kingId]) - } - } - - if (matchFound) { - return { - id: kingId, - since: match.date, - } - } - } - return matchesLast[matchesLast.length - 1] -} diff --git a/frontend/src/app/modules/matches/matches-selectors.js b/frontend/src/app/modules/matches/matches-selectors.js index d47630b6..d9694d7b 100644 --- a/frontend/src/app/modules/matches/matches-selectors.js +++ b/frontend/src/app/modules/matches/matches-selectors.js @@ -1,17 +1,5 @@ import { createSelector } from 'reselect' -import { - computeLongestWinStreak, - computeWinRatio, - computeWins, - generateMatchRatingChanges, - plotRatingHistory, - findMax, - findMin, - computeDays, - computeTeammateStats, - computeOpponentsStats, - computeKingStreakDuration, -} from './matches-computations' +import { didPlayerPlayMatch } from './matches-computations' const fillPlayers = (team, state) => team.map(emptyPlayer => { const player = state.players.find(player => player.id === emptyPlayer.id) @@ -27,57 +15,13 @@ const getMatches = state => state.matches.map(match => ({ team2: fillPlayers(match.team2, state), })) -export const didPlayerPlayMatch = (playerId, match) => { - const allPlayers = [...match.team1, ...match.team2] - return !!allPlayers.find(player => player.id === playerId) -} - export const getLastMatches = createSelector( getMatches, matches => matches.sort((match1, match2) => match2.date - match1.date), ) -const getLastMatchesForPlayer = createSelector( +export const getLastMatchesForPlayer = createSelector( getLastMatches, (state, playerId) => playerId, (matches, playerId) => matches.filter(match => didPlayerPlayMatch(playerId, match)), ) - -const generateStatisticsForPlayer = (playerId, playerMatches) => { - const matchChanges = generateMatchRatingChanges(playerId, playerMatches) - const days = computeDays(matchChanges) - const teammateStats = computeTeammateStats(playerId, playerMatches) - const opponentStats = computeOpponentsStats(playerId, playerMatches) - - return { - matchChanges, - longestStreak: computeLongestWinStreak(playerId, playerMatches), - winRatio: computeWinRatio(playerId, playerMatches), - totalMatches: playerMatches.length, - wins: computeWins(playerId, playerMatches), - bestDay: findMax(days), - worstDay: findMin(days), - - mostFrequentTeammate: findMax(teammateStats, teammate => teammate.matches), - leastFrequentTeammate: findMin(teammateStats, teammate => teammate.matches), - mostSuccessTeammate: findMax(teammateStats, teammate => teammate.wins), - leastSuccessTeammate: findMax(teammateStats, teammate => teammate.losses), - - mostFrequentOpponent: findMax(opponentStats, opponent => opponent.matches), - leastFrequentOpponent: findMin(opponentStats, opponent => opponent.matches), - mostSuccessOpponent: findMax(opponentStats, opponent => opponent.wins), - leastSuccessOpponent: findMax(opponentStats, opponent => opponent.losses), - } -} - -export const getStatisticsForPlayer = createSelector( - getLastMatchesForPlayer, - (state, playerId) => playerId, - (playerMatches, playerId) => generateStatisticsForPlayer(playerId, playerMatches), -) - -export const getRatingHistoryGraphForPlayer = createSelector( - getLastMatchesForPlayer, - (state, playerId) => state.players.find(player => player.id === playerId), - (playerMatches, player) => plotRatingHistory(playerMatches, player.id, player.initialRating), -) diff --git a/frontend/src/app/modules/players/players-computations.js b/frontend/src/app/modules/players/players-computations.js new file mode 100644 index 00000000..5e94ffcb --- /dev/null +++ b/frontend/src/app/modules/players/players-computations.js @@ -0,0 +1,97 @@ +import * as Filters from '../../const/leaderboard-filters' + +import { computeWinRatio } from './statistics-computations' +import { didPlayerPlayMatch, computeLongestWinStreak } from '../matches/matches-computations' + +const getPlayerCriteriaPoints = (player, matchesLast, criteria) => { + const playerId = Number(player.id) + const playerMatches = matchesLast.filter(match => didPlayerPlayMatch(playerId, match)) + const winRatio = computeWinRatio(playerId, playerMatches) + switch (criteria) { + case Filters.criteriaTypes.Wins: + return Math.round(playerMatches.length * winRatio) + case Filters.criteriaTypes.Ratio: + return (winRatio * 100).toFixed(2) + case Filters.criteriaTypes.Streak: + return computeLongestWinStreak(playerId, playerMatches) + case Filters.criteriaTypes.Matches: + return playerMatches.length + default: + return player.rating + } +} + +export const sortTopPlayers = (players, matches, filters) => { + const order = filters.order === Filters.orderTypes.ASC ? -1 : 1 + const minDate = new Date() + minDate.setDate(minDate.getDate() - filters.timespan) + const matchesLast = matches + .filter(match => filters.timespan === Filters.timespanTypes.AllTime || match.date > minDate) + .sort((match1, match2) => match2.date - match1.date) + + const playersWithStatistics = players.map(player => ({ + ...player, + criteriaPoints: getPlayerCriteriaPoints(player, matchesLast, filters.criteria), + })) + + return playersWithStatistics.sort((u1, u2) => (u2.criteriaPoints - u1.criteriaPoints) * order) +} + +export const getRatingStatisticsForPlayer = (players, playerId) => { + const index = players.findIndex(player => player.id === playerId) + return { + ranking: index + 1, + scoreToNextRank: index > 0 + ? 1 + (players[index - 1].rating - players[index].rating) + : 0, + scoreToPrevRank: index < players.length - 1 + ? 1 + (players[index].rating - players[index + 1].rating) + : 0, + } +} + +export const computeKingStreakDuration = (lastMatches, lastPlayers) => { + if (lastPlayers.length < 1) { + return null + } + + const playersMap = lastPlayers.reduce((map, player) => { + map[player.id] = player.rating + return map + }, new Map()) + + const kingId = lastPlayers[0].id + let matchFound = false + for (const match of lastMatches) { + const players = [...match.team1, ...match.team2] + + // #1 recomptute king's rating before in case he played the match + const kingPlayer = players.find(player => player.id === kingId) + let kingWon = false + if (kingPlayer) { + kingWon = playersMap[kingId] > kingPlayer.matchRating + playersMap[kingId] = kingPlayer.matchRating + } + + // #2 check whether none of the other players beat the king + for (const player of players) { + playersMap[player.id] = player.matchRating + matchFound |= (player.id !== kingId && player.matchRating > playersMap[kingId]) + } + + // #3 check if king was first before winning + if (kingPlayer && kingWon && !matchFound) { + for (const key in playersMap) { + matchFound |= (playersMap[key] > playersMap[kingId]) + } + } + + if (matchFound) { + return { + id: kingId, + since: match.date, + } + } + } + return lastMatches[lastMatches.length - 1] +} diff --git a/frontend/src/app/modules/players/players-selectors.js b/frontend/src/app/modules/players/players-selectors.js index 88a8bdf9..a8be4fad 100644 --- a/frontend/src/app/modules/players/players-selectors.js +++ b/frontend/src/app/modules/players/players-selectors.js @@ -1,12 +1,9 @@ import { createSelector } from 'reselect' -import * as Filters from '../../const/leaderboard-filters' -import { didPlayerPlayMatch, getLastMatches } from '../matches/matches-selectors' -import { - computeLongestWinStreak, - computeWinRatio, - computeKingStreakDuration, -} from '../matches/matches-computations' +import { getLastMatches, getLastMatchesForPlayer } from '../matches/matches-selectors' +import { generateStatisticsForPlayer } from './statistics-computations' +import { sortTopPlayers, getRatingStatisticsForPlayer, computeKingStreakDuration } from './players-computations' +import { plotRatingHistory } from '../matches/matches-computations' export const getPlayers = state => state.players const getMatches = state => state.matches @@ -26,65 +23,30 @@ export const getTopRatedPlayers = createSelector( export const getKing = createSelector( getLastMatches, getTopRatedPlayers, - (playerMatches, players) => computeKingStreakDuration(playerMatches, players), + computeKingStreakDuration, ) export const getTopPlayers = createSelector( getPlayers, getMatches, getFilters, - (players, matches, filters) => sortTopPlayers(players, matches, filters), + sortTopPlayers, ) -const sortTopPlayers = (players, matches, filters) => { - const order = filters.order === Filters.orderTypes.ASC ? -1 : 1 - const minDate = new Date() - minDate.setDate(minDate.getDate() - filters.timespan) - const matchesLast = matches - .filter(match => filters.timespan === Filters.timespanTypes.AllTime || match.date > minDate) - .sort((match1, match2) => match2.date - match1.date) - - const playersWithStatistics = players.map(player => ({ - ...player, - criteriaPoints: getPlayerCriteriaPoints(player, matchesLast, filters.criteria), - })) - - return playersWithStatistics.sort((u1, u2) => (u2.criteriaPoints - u1.criteriaPoints) * order) -} - -const getPlayerCriteriaPoints = (player, matchesLast, criteria) => { - const playerId = Number(player.id) - const playerMatches = matchesLast.filter(match => didPlayerPlayMatch(playerId, match)) - const winRatio = computeWinRatio(playerId, playerMatches) - switch (criteria) { - case Filters.criteriaTypes.Wins: - return Math.round(playerMatches.length * winRatio) - case Filters.criteriaTypes.Ratio: - return (winRatio * 100).toFixed(2) - case Filters.criteriaTypes.Streak: - return computeLongestWinStreak(playerId, playerMatches) - case Filters.criteriaTypes.Matches: - return playerMatches.length - default: - return player.rating - } -} - -const getRatingStatisticsForPlayer = (players, playerId) => { - const index = players.findIndex(player => player.id === playerId) - return { - ranking: index + 1, - scoreToNextRank: index > 0 - ? 1 + (players[index - 1].rating - players[index].rating) - : 0, - scoreToPrevRank: index < players.length - 1 - ? 1 + (players[index].rating - players[index + 1].rating) - : 0, - } -} - export const getRankingsForPlayer = createSelector( state => getTopRatedPlayers(state), (state, playerId) => playerId, getRatingStatisticsForPlayer, ) + +export const getStatisticsForPlayer = createSelector( + getLastMatchesForPlayer, + (state, playerId) => playerId, + generateStatisticsForPlayer, +) + +export const getRatingHistoryGraphForPlayer = createSelector( + getLastMatchesForPlayer, + getPlayer, + (playerMatches, player) => plotRatingHistory(playerMatches, player.id, player.initialRating), +) diff --git a/frontend/src/app/modules/players/statistics-computations.js b/frontend/src/app/modules/players/statistics-computations.js new file mode 100644 index 00000000..e9ea81e2 --- /dev/null +++ b/frontend/src/app/modules/players/statistics-computations.js @@ -0,0 +1,114 @@ +import { DEFAULT_DAYS_STATISTICS, DEFAULT_PLAYERS_STATISTICS } from '../../const/constants' +import { + didPlayerWin, + computeLongestWinStreak, + generateMatchRatingChanges, +} from '../matches/matches-computations' + +const getTeamMates = (playerId, { team1, team2 }) => { + const playerTeam = team1.find(player => player.id === playerId) ? team1 : team2 + return playerTeam.filter(player => player.id !== playerId) +} + +const getOpponents = (playerId, { team1, team2 }) => + team1.find(player => player.id === playerId) ? team2 : team1 + +const findWithComparator = (comparator, arr, getField, baseScore) => + arr.reduce((acc, node) => + comparator(acc.score, getField(node)) + ? acc + : { value: node.value, score: getField(node) } + , { score: baseScore }) + +const findMin = (arr, getField = node => node.score) => + findWithComparator((e1, e2) => e1 <= e2, arr, getField, Infinity) + +const findMax = (arr, getField = node => node.score) => + findWithComparator((e1, e2) => e1 >= e2, arr, getField, -Infinity) + +const computeWins = (playerId, playerMatches) => + playerMatches.filter(match => didPlayerWin(playerId, match)).length + +const computeDays = matchChanges => { + const daysMap = new Map() + + for (const match of matchChanges) { + const day = match.date.toLocaleDateString() + const score = Number(match.ratingChangeString) + daysMap.set(day, daysMap.has(day) + ? { + ...daysMap.get(day), + score: daysMap.get(day).score + score, + } + : { + value: day, + score, + }) + } + return matchChanges.length + ? Array.from(daysMap.values()) + : DEFAULT_DAYS_STATISTICS +} + +const computeTeammateStats = (playerId, playerMatches) => + computePlayerStats(playerId, playerMatches, getTeamMates) || {} + +const computeOpponentsStats = (playerId, playerMatches) => + computePlayerStats(playerId, playerMatches, getOpponents) || {} + +const computePlayerStats = (playerId, playerMatches, playersProvider) => { + const playersMap = new Map() + + for (const match of playerMatches) { + const players = playersProvider(playerId, match) + players.forEach(player => + playersMap.set(player.id, playersMap.has(player.id) + ? { + ...playersMap.get(player.id), + matches: playersMap.get(player.id)['matches'] + 1, + wins: playersMap.get(player.id)['wins'] += Number(didPlayerWin(playerId, match)), + losses: playersMap.get(player.id)['losses'] += Number(!didPlayerWin(playerId, match)), + } + : { + value: player, + matches: 1, + wins: Number(didPlayerWin(playerId, match)), + losses: Number(!didPlayerWin(playerId, match)), + })) + } + return playerMatches.length + ? Array.from(playersMap.values()) + : DEFAULT_PLAYERS_STATISTICS +} + +export const computeWinRatio = (playerId, playerMatches) => { + const wonMatchesCount = playerMatches.filter(match => didPlayerWin(playerId, match)).length + return (playerMatches.length > 0) ? (wonMatchesCount / playerMatches.length) : 0 +} + +export const generateStatisticsForPlayer = (playerMatches, playerId) => { + const matchChanges = generateMatchRatingChanges(playerId, playerMatches) + const days = computeDays(matchChanges) + const teammateStats = computeTeammateStats(playerId, playerMatches) + const opponentStats = computeOpponentsStats(playerId, playerMatches) + + return { + matchChanges, + longestStreak: computeLongestWinStreak(playerId, playerMatches), + winRatio: computeWinRatio(playerId, playerMatches), + totalMatches: playerMatches.length, + wins: computeWins(playerId, playerMatches), + bestDay: findMax(days), + worstDay: findMin(days), + + mostFrequentTeammate: findMax(teammateStats, teammate => teammate.matches), + leastFrequentTeammate: findMin(teammateStats, teammate => teammate.matches), + mostSuccessTeammate: findMax(teammateStats, teammate => teammate.wins), + leastSuccessTeammate: findMax(teammateStats, teammate => teammate.losses), + + mostFrequentOpponent: findMax(opponentStats, opponent => opponent.matches), + leastFrequentOpponent: findMin(opponentStats, opponent => opponent.matches), + mostSuccessOpponent: findMax(opponentStats, opponent => opponent.wins), + leastSuccessOpponent: findMax(opponentStats, opponent => opponent.losses), + } +} diff --git a/frontend/src/app/pages/Profile/index.js b/frontend/src/app/pages/Profile/index.js index 1a8939e6..65811d67 100644 --- a/frontend/src/app/pages/Profile/index.js +++ b/frontend/src/app/pages/Profile/index.js @@ -8,8 +8,11 @@ import { import { ProfileBattleHistory } from './../../components/ProfileBattleHistory' import { ProfileRatingGraph } from '../../components/ProfileRatingGraph' import { ProfileStatistics } from '../../components/ProfileStatistics' -import { getPlayer, getRankingsForPlayer } from '../../modules/players/players-selectors' -import { getStatisticsForPlayer } from '../../modules/matches/matches-selectors' +import { + getPlayer, + getRankingsForPlayer, + getStatisticsForPlayer, +} from '../../modules/players/players-selectors' class ProfileComponent extends Component { render() {