Skip to content

Commit

Permalink
Allow customized ranking statistics and sorting (#68)
Browse files Browse the repository at this point in the history
* Updated testing players

* Added sorting of players by arbitrary stats

* Changed naming of text fields in statisticslist

* Refactored ranking statistics function to be more javascripty

* Made unit calculation much more simple
  • Loading branch information
0spooky authored Oct 7, 2018
1 parent 9ed561e commit 951ec1a
Show file tree
Hide file tree
Showing 10 changed files with 256 additions and 92 deletions.
8 changes: 6 additions & 2 deletions client/Main.css
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,12 @@ tr:nth-of-type(even){
text-align: center;
}

.ranking .rank, .ranking .elo, .ranking .games-played {
width: 10%;
.ranking .rank {
width: 5%
}

.ranking .sortStat, .ranking .additionalStat {
width: 20%;
}

.ranking .players {
Expand Down
4 changes: 2 additions & 2 deletions imports/api/Constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,9 @@ const Constants = {
// rules. Possibly add more if ever decided to
GAME_TYPE: {
// Hong Kong Old Style
HONG_KONG: "hkg",
HONG_KONG: "hongKong",
// Japanese Riichi Style
JAPANESE: "jpn"
JAPANESE: "japanese"
},

// Round End Conditions
Expand Down
96 changes: 57 additions & 39 deletions imports/api/utils/GameTypeUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,13 @@ export default {
return standardizePlayerStatistics(format, Players.findOne(criteria));
},

getPlayers(format, sort) {
/**
* Retrieve the player collection sorted by the relevant ELO with standard statistics
* @param[in] format The Constants.GAME_TYPE game format to used
* @param[in] rankingSort The mongoDB collections sort option to use
* @returns A list of players sorted by rankingSort
*/
getPlayers(format, rankingSort) {
let hasPlayedGames = {};
switch (format) {
case Constants.GAME_TYPE.JAPANESE:
Expand All @@ -31,55 +37,67 @@ export default {
logInvalidFormat(format);
}

return Players.find(hasPlayedGames, { sort }).map((player) => standardizePlayerStatistics(format, player));
return Players.find(hasPlayedGames, { sort: rankingSort }).map((player) => standardizePlayerStatistics(format, player));
}
}

/**
* Take a player's database entry and convert it into standard statistics
* @param[in] format The Constants.GAME_TYPE game format to use
* @param[in] player The player to calculate statistics for
* @returns A formated player object with all relevant statistics
*/
function standardizePlayerStatistics(format, player) {
let formatPlayer = {};

/* Gate against invalid formats */
if (!Object.values(Constants.GAME_TYPE).includes(format)) {
logInvalidFormat(format);
return formatPlayer;
}

let gamesPlayed = player[format + "GamesPlayed"];
let wonHands = player[format + "HandsWin"];
let dealinHands = player[format + "HandsLose"];
let totalHands = player[format + "HandsTotal"];
let totalPoints = player[format + "WinPointsTotal"];
let weightedPositionSum =
1 * player[format + "FirstPlaceSum"] +
2 * player[format + "SecondPlaceSum"] +
3 * player[format + "ThirdPlaceSum"] +
4 * player[format + "FourthPlaceSum"];

/* Calculate standard statistics */
/* We can always assume gamesPlayed and totalHands both > 0, but not other stats */
formatPlayer["name"] = player[format + "LeagueName"];
formatPlayer["elo"] = player[format + "Elo"];
formatPlayer["gamesPlayed"] = gamesPlayed;
formatPlayer["handWinRate"] = (wonHands / totalHands * 100).toFixed(2);
formatPlayer["dealinRate"] = (dealinHands / totalHands * 100).toFixed(2);
formatPlayer["averageHandSize"] = ((wonHands > 0) ? totalPoints / wonHands : 0).toFixed(2);
formatPlayer["averagePosition"] = (weightedPositionSum / gamesPlayed).toFixed(2);
formatPlayer["flyRate"] = (player[format + "BankruptTotal"] / gamesPlayed * 100).toFixed(2);
formatPlayer["chomboTotal"] = player[format + "ChomboTotal"];

switch (format) {
case Constants.GAME_TYPE.JAPANESE:
formatPlayer["leagueName"] = player["japaneseLeagueName"];
formatPlayer["elo"] = player["japaneseElo"];
formatPlayer["gamesPlayed"] = player["japaneseGamesPlayed"];
formatPlayer["handsWin"] = player["japaneseHandsWin"];
formatPlayer["handsLose"] = player["japaneseHandsLose"];
formatPlayer["handsTotal"] = player["japaneseHandsTotal"];
formatPlayer["winPointsTotal"] = player["japaneseWinPointsTotal"];
formatPlayer["winDoraTotal"] = player["japaneseWinDoraTotal"];
formatPlayer["riichiTotal"] = player["japaneseRiichiTotal"];
formatPlayer["winRiichiTotal"] = player["japaneseWinRiichiTotal"];
formatPlayer["chomboTotal"] = player["japaneseChomboTotal"];
formatPlayer["bankruptTotal"] = player["japaneseBankruptTotal"];
formatPlayer["firstPlaceSum"] = player["japaneseFirstPlaceSum"];
formatPlayer["secondPlaceSum"] = player["japaneseSecondPlaceSum"];
formatPlayer["thirdPlaceSum"] = player["japaneseThirdPlaceSum"];
formatPlayer["fourthPlaceSum"] = player["japaneseFourthPlaceSum"];
let riichiTotal = player[format + "RiichiTotal"];
formatPlayer["riichiRate"] = (riichiTotal / totalHands * 100).toFixed(2);
formatPlayer["riichiWinRate"] = ((riichiTotal > 0) ? (player[format + "WinRiichiTotal"] / riichiTotal * 100) : 0).toFixed(2);
formatPlayer["averageHandDora"] = ((wonHands > 0) ? player[format + "WinDoraTotal"] / wonHands : 0).toFixed(2);
break;

case Constants.GAME_TYPE.HONG_KONG:
formatPlayer["leagueName"] = player["hongKongLeagueName"];
formatPlayer["elo"] = player["hongKongElo"];
formatPlayer["gamesPlayed"] = player["hongKongGamesPlayed"];
formatPlayer["handsWin"] = player["hongKongHandsWin"];
formatPlayer["handsLose"] = player["hongKongHandsLose"];
formatPlayer["handsTotal"] = player["hongKongHandsTotal"];
formatPlayer["winPointsTotal"] = player["hongKongWinPointsTotal"];
formatPlayer["chomboTotal"] = player["hongKongChomboTotal"];
formatPlayer["bankruptTotal"] = player["hongKongBankruptTotal"];
formatPlayer["firstPlaceSum"] = player["hongKongFirstPlaceSum"];
formatPlayer["secondPlaceSum"] = player["hongKongSecondPlaceSum"];
formatPlayer["thirdPlaceSum"] = player["hongKongThirdPlaceSum"];
formatPlayer["fourthPlaceSum"] = player["hongKongFourthPlaceSum"];
break;

default:
logInvalidFormat(format);
formatPlayer = player;
break;
}

formatPlayer["id"] = player["_id"];
return formatPlayer;
}
};

function logInvalidFormat(format) { console.log("Format '" + format + "' is invalid") }
/**
* Print out a console warning saying format is invalid
* @param[in] format The format to warn about
*/
function logInvalidFormat(format) {
console.warn("Format '" + format + "' is invalid")
};
6 changes: 5 additions & 1 deletion imports/ui/ranking/HongKongRanking.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ import Constants from '../../api/Constants';
import './HongKongRanking.html';

Template.HongKongRanking.helpers({
/**
* Provide a context for the ranking page
* @returns the Hong Kong ranking context
*/
getContext() {
return {
format: Constants.GAME_TYPE.HONG_KONG,
sort: {
rankingSort: {
hongKongElo: -1
}
};
Expand Down
6 changes: 5 additions & 1 deletion imports/ui/ranking/JapaneseRanking.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ import Constants from '../../api/Constants';
import './JapaneseRanking.html';

Template.JapaneseRanking.helpers({
/**
* Provide a context for the ranking page
* @returns the Japanese ranking context
*/
getContext() {
return {
format: Constants.GAME_TYPE.JAPANESE,
sort: {
rankingSort: {
japaneseElo: -1
}
};
Expand Down
43 changes: 35 additions & 8 deletions imports/ui/ranking/Ranking.html
Original file line number Diff line number Diff line change
@@ -1,23 +1,50 @@
<!--
Template for Ranking page
@param[in] format The Constants.GAME_TYPE game format
@param[in] rankingSort The MongoDB collection sort option to use
-->
<template name="Ranking">
<h2>{{ getName format }} {{ MAHJONG_CLUB_LEAGUE }}</h2>
{{ > PlayerModal }}
<table class="ranking">
<tr>
<th class="rank">#</th>
<th class="players">Players</th>
<th class="elo">ELO</th>
<th class="games-played">Games Played</th>
<th class="players">Player</th>
<th class="sortStat">
<select name="sortStatistic" class="sortStatisticSelect">
<option disabled selected>Sort by: ELO</option>
<option value="elo">ELO</option>
{{ #each statistic in (getRankingStatistics format sortRankingExclusion) }}
{{ > statisticsChoiceOptions statistic=statistic}}
{{ /each }}
</select>
</th>
<th class="additionalStat">
<select name="additionalStatistic" class="additionalStatisticSelect">
<option value="gamesPlayed">Games</option>
{{ #each statistic in (getRankingStatistics format additionalRankingExclusion) }}
{{ > statisticsChoiceOptions statistic=statistic}}
{{ /each }}
</select>
</th>
</tr>
<!-- The following lines get the format argument from the templates JapaneseRanking or HongKongRanking -->
{{ #each player in (getPlayers format sort) }}
{{ #each player in (getOrderedPlayers format rankingSort orderSort=orderSort) }}
<tr class="player"
data-player="{{ player.id }}"
data-format="{{ format }}">
<td class="rank">{{ player.rank }}</td>
<td class="players">{{ player.leagueName }}</td>
<td class="elo">{{ player.elo }}</td>
<td class="games-played">{{ player.gamesPlayed }}</td>
<td class="players">{{ player.name }}</td>
<td class="sortStat">{{ getSortStatistic player }}{{ getSortStatUnit }}</td>
<td class="additionalStat">{{ getAdditionalStatistic player }}{{ getAdditionalStatUnit }}</td>
</tr>
{{ /each }}
</table>
</template>

<!--
Template to display a single option for rankings
@param statistic Object with a value and formatted value
-->
<template name="statisticsChoiceOptions">
<option value={{statistic.value}}>{{statistic.displayText}}</option>
</template>
127 changes: 124 additions & 3 deletions imports/ui/ranking/Ranking.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,118 @@
import GameTypeUtils from '../../api/utils/GameTypeUtils';
import Constants from '../../api/Constants';

import '../statistics/PlayerModal';
import './HongKongRanking';
import './JapaneseRanking';
import './Ranking.html';

Template.Ranking.onCreated(
/**
* Setup "constructor" for Ranking page, ensure that default stats are shown
*/
function() {
Session.set("sortStatistic", "elo");
Session.set("additionalStatistic", "gamesPlayed");
}
);

Template.Ranking.helpers({
getName(format) {
return GameTypeUtils.formatName(format);
},

getPlayers(format, sort) {
return GameTypeUtils.getPlayers(format, sort).map((p, i) => ({
/**
* Return the list of valid players, ranked by ELO and sorted by orderSort
* @param[in] format The Constants.GAME_TYPE game format
* @param[in] rankingSort The order to rank players by (currently always ELO)
* @param[in] orderSort The order to sort players by
* @returns A list of players with statistics
*/
getOrderedPlayers(format, rankingSort, orderSort) {
return GameTypeUtils.getPlayers(format, rankingSort).map((p, i) => ({
...p,
elo: p.elo.toFixed(3),
rank: i + 1
}));
})).sort(orderSort.hash.orderSort);
},

/**
* List generator for ranking page statistics options
* @param[in] format The Constants.GAME_TYPE format
* @param[in] exclusions List of statistics to exclude by value (Ignore format!)
* @returns an object of formatted statistics keyed with statistics
*/
getRankingStatistics(format, exclusions) {
let defaultStatisticsList = [
{value: "elo", displayText: "ELO"},
{value: "gamesPlayed", displayText: "Games"},
{value: "handWinRate", displayText: "Hand Win %"},
{value: "dealinRate", displayText: "Deal-in %"},
{value: "averageHandSize", displayText: "Avg Hand Size"},
{value: "averagePosition", displayText: "Avg Position"},
{value: "flyRate", displayText: "Bankrupt %"},
{value: "chomboTotal", displayText: "Chombos"}
];
const japaneseStatisticsList = [
{value: "averageHandDora", displayText: "Avg Hand Dora"},
{value: "riichiRate", displayText: "Riichi %"},
{value: "riichiWinRate", displayText: "Riichi Win %"}
];

// Filter out excluded statistics and non-format statistics
return [...defaultStatisticsList, ...japaneseStatisticsList].filter(
(statistic) =>
!exclusions.find((exclusion) => exclusion.value === statistic.value) &&
!(japaneseStatisticsList.includes(statistic) && format !== Constants.GAME_TYPE.JAPANESE));
},

/**
* Get statistic for a player, either sort or additional
* @returns the selected statistic for a player
*/
getSortStatistic(player) {
return player[Session.get("sortStatistic")];
},
getAdditionalStatistic(player) {
return player[Session.get("additionalStatistic")];
},

/**
* Get formatted unit for a statistic if it has one
* @returns String of formatted unit or empty string
*/
getSortStatUnit() {
return getStatisticUnit(Session.get("sortStatistic"));
},
getAdditionalStatUnit() {
return getStatisticUnit(Session.get("additionalStatistic"));
},

/**
* Exclusion lists for statistics options. Allows for manual entries
* @returns a list of statistics to exclude
*/
sortRankingExclusion() {
return [{value: "elo"}];
},
additionalRankingExclusion() {
return [{value: "gamesPlayed"}];
},

/**
* Lambda generator for Array.sort() on player statistics
* @param[in] a See Array.sort, param a
* @param[in] b See Array.sort, param b
* @returns A sorting lambda
*/
orderSort(a, b) {
return ((a, b) => {
let sortBy = Session.get("sortStatistic");
let sortOrder = (["dealinRate", "averagePosition"].includes(sortBy)) ? 1 : -1;
let first = Number(a[sortBy]);
let second = Number(b[sortBy]);
return (first > second) ? sortOrder : ((second > first) ? -sortOrder : 0)
});
}
});

Expand All @@ -26,5 +123,29 @@ Template.Ranking.events({

Session.set("selectedStatistics", statistics);
$("#modal").modal('show');
},

/**
* Event handler for modifying ranking sort statistic
* @param[in] event The event to handle
*/
'change select[name="sortStatistic"]'(event) {
Session.set("sortStatistic", event.target.value);
},

/**
* Event handler for modifying ranking additional statistic
* @param[in] event The event to handle
*/
'change select[name="additionalStatistic"]'(event) {
Session.set("additionalStatistic", event.target.value);
}
});

function getStatisticUnit(statistic) {
return ["handWinRate",
"dealinRate",
"flyRate",
"riichiRate",
"riichiWinRate"].includes(statistic) ? " %" : "";
}
Loading

0 comments on commit 951ec1a

Please sign in to comment.