-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1 from wakingrufus/initial
Initial Commit
- Loading branch information
Showing
17 changed files
with
459 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
build/ | ||
.gradle/ | ||
gradle/ | ||
gradlew.bat | ||
gradlew | ||
.idea/ | ||
shippable/ | ||
*.iml | ||
*.ipr | ||
*.iws |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
#lib-elo | ||
Library for ELO calculations for game leagues | ||
|
||
[![Run Status](https://api.shippable.com/projects/5a12e20c79f26b0700c37035/badge?branch=master)](https://app.shippable.com/github/wakingrufus/lib-elo) | ||
[![Coverage Badge](https://api.shippable.com/projects/5a12e20c79f26b0700c37035/coverageBadge?branch=master)](https://app.shippable.com/github/wakingrufus/lib-elo) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
buildscript { | ||
dependencies { | ||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.1.4" | ||
} | ||
repositories { | ||
mavenCentral() | ||
} | ||
} | ||
|
||
plugins { | ||
id 'java' | ||
id 'idea' | ||
id "org.jetbrains.kotlin.jvm" version "1.1.4" | ||
id 'jacoco' | ||
} | ||
|
||
repositories { | ||
mavenCentral() | ||
} | ||
|
||
version = "0.1.0" | ||
|
||
dependencies { | ||
compile "org.jetbrains.kotlin:kotlin-stdlib-jre8" | ||
compile 'org.projectlombok:lombok:1.16.18' | ||
compile group: 'org.slf4j', name: 'slf4j-api', version: '1.7.25' | ||
compile 'io.github.microutils:kotlin-logging:1.4.4' | ||
compile "org.jetbrains.kotlin:kotlin-reflect" | ||
compile "org.jsoup:jsoup:1.10.3" | ||
|
||
testCompile "org.jetbrains.kotlin:kotlin-test" | ||
testCompile "org.jetbrains.kotlin:kotlin-test-junit" | ||
testCompile 'org.mockito:mockito-core:2.8.9' | ||
testCompile group: 'junit', name: 'junit', version: '4.12' | ||
testCompile "com.nhaarman:mockito-kotlin-kt1.1:1.5.0" | ||
testCompile group: 'org.slf4j', name: 'slf4j-log4j12', version: '1.7.25' | ||
} | ||
|
||
idea { | ||
project { | ||
languageLevel = '1.8' | ||
} | ||
} | ||
|
||
task wrapper(type: Wrapper) { | ||
gradleVersion = '4.3.1' | ||
} | ||
|
||
jacocoTestReport { | ||
reports { | ||
xml.enabled true | ||
} | ||
} | ||
|
||
jacocoTestReport.dependsOn test | ||
build.dependsOn jacocoTestReport | ||
|
||
compileKotlin { | ||
kotlinOptions { | ||
jvmTarget = "1.8" | ||
} | ||
} | ||
compileTestKotlin { | ||
kotlinOptions { | ||
jvmTarget = "1.8" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
rootProject.name = 'lib-elo' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
language: java | ||
|
||
jdk: | ||
- oraclejdk8 | ||
|
||
env: | ||
global: | ||
|
||
build: | ||
ci: | ||
- sudo apt-get install xvfb | ||
- Xvfb :99 &>/dev/null & | ||
- export DISPLAY=:99 | ||
- mkdir -p shippable/testresults | ||
- mkdir -p shippable/codecoverage | ||
- mkdir -p shippable/codecoverage/target/site/jacoco | ||
- rm -rf build | ||
- gradle wrapper | ||
- ./gradlew clean build | ||
- cp build/test-results/test/*.xml shippable/testresults | ||
- cp build/reports/jacoco/test/jacocoTestReport.xml shippable/codecoverage | ||
- cp -r build/jacoco/test.exec shippable/codecoverage/target/site/jacoco | ||
- cp -r build/classes shippable/codecoverage/target | ||
on_success: | ||
- export TAG_NAME=`git describe --exact-match --tags HEAD` | ||
- export BRANCH_NAME=`git rev-parse --abbrev-ref HEAD` | ||
- if [ "$TAG_NAME" != "" ]; then ./gradlew tasks; fi |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
package com.github.wakingrufus.elo | ||
|
||
import java.math.BigDecimal | ||
import java.math.RoundingMode | ||
|
||
fun pow(base: BigDecimal, exponent: BigDecimal): BigDecimal { | ||
var result = BigDecimal.ZERO | ||
val signOf2 = exponent.signum() | ||
|
||
// Perform X^(A+B)=X^A*X^B (B = remainder) | ||
val dn1 = base.toDouble() | ||
// Compare the same row of digits according to context | ||
val n2 = exponent.multiply(BigDecimal(signOf2)) // n2 is now positive | ||
val remainderOf2 = n2.remainder(BigDecimal.ONE) | ||
val n2IntPart = n2.subtract(remainderOf2) | ||
// Calculate big part of the power using context - | ||
// bigger range and performance but lower accuracy | ||
val intPow = base.pow(n2IntPart.intValueExact()) | ||
val doublePow = BigDecimal(Math.pow(dn1, remainderOf2.toDouble())) | ||
result = intPow.multiply(doublePow) | ||
|
||
// Fix negative power | ||
if (signOf2 == -1) | ||
result = BigDecimal.ONE.divide(result, RoundingMode.HALF_UP) | ||
return result | ||
} |
24 changes: 24 additions & 0 deletions
24
src/main/kotlin/com/github/wakingrufus/elo/EloCalculator.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
package com.github.wakingrufus.elo | ||
|
||
import java.math.BigDecimal | ||
|
||
fun calculateAdjustment(kFactor: Int, score: BigDecimal, expectedScore: BigDecimal): Int { | ||
return BigDecimal(kFactor).multiply(score.subtract(expectedScore)).toInt() | ||
} | ||
|
||
fun calculateKfactor(kfactorBase: Int, | ||
trialPeriod: Int, | ||
trialMultiplier: Int, | ||
playerGamesPlayed: Int, | ||
otherPlayerGamesPlayed: Int): Int { | ||
var kFactor = kfactorBase | ||
|
||
if (playerGamesPlayed < trialPeriod) { | ||
kFactor *= trialMultiplier | ||
|
||
} | ||
if (otherPlayerGamesPlayed < trialPeriod) { | ||
kFactor /= trialMultiplier | ||
} | ||
return kFactor | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
package com.github.wakingrufus.elo | ||
|
||
fun calculateNewLeague(league: League, games: List<Game>): LeagueState { | ||
var leagueState = LeagueState(league = league) | ||
games.stream() | ||
.sorted({ g1: Game, g2: Game -> (g2.entryDate.epochSecond - g1.entryDate.epochSecond).toInt() }) | ||
.forEach { leagueState = addGameToLeague(leagueState, it) } | ||
return leagueState | ||
} | ||
|
||
fun addGameToLeague(leagueState: LeagueState, game: Game): LeagueState { | ||
var newLeagueState = leagueState.copy( | ||
players = addNewPlayers( | ||
existingPlayers = leagueState.players, | ||
game = game, | ||
startingRating = leagueState.league.startingRating)) | ||
val changes = calculateChanges( | ||
league = newLeagueState.league, | ||
players = newLeagueState.players, | ||
game = game) | ||
newLeagueState = newLeagueState.copy(players = applyChanges(players = newLeagueState.players, changes = changes)) | ||
newLeagueState = newLeagueState.copy(history = leagueState.history + changes) | ||
return newLeagueState | ||
} |
19 changes: 19 additions & 0 deletions
19
src/main/kotlin/com/github/wakingrufus/elo/ExpectedScore.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
package com.github.wakingrufus.elo | ||
|
||
import java.math.BigDecimal | ||
import java.math.MathContext | ||
|
||
|
||
fun calculateExpectedScore(rating1: Int, rating2: Int, xi: Int): BigDecimal { | ||
val q1 = calculateQ(teamRating = rating1, xi = xi) | ||
val q2 = calculateQ(teamRating = rating2, xi = xi) | ||
return calculateE(q1, q2) | ||
} | ||
|
||
private fun calculateQ(teamRating: Int, xi: Int): BigDecimal { | ||
return pow(BigDecimal.TEN, BigDecimal(teamRating).divide(BigDecimal(xi), MathContext.DECIMAL32)) | ||
} | ||
|
||
private fun calculateE(q1: BigDecimal, q2: BigDecimal): BigDecimal { | ||
return q1.divide(q1.add(q2), MathContext.DECIMAL32) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package com.github.wakingrufus.elo | ||
|
||
import java.time.Instant | ||
|
||
data class Game(val id: String, | ||
val team1Score: Int, | ||
val team2Score: Int, | ||
val entryDate: Instant, | ||
val team1PlayerIds: List<String>, | ||
val team2PlayerIds: List<String>) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
package com.github.wakingrufus.elo; | ||
|
||
data class League(val startingRating: Int = 1500, | ||
val xi: Int = 1000, | ||
val kFactorBase: Int = 32, | ||
val trialPeriod: Int = 10, | ||
val trialKFactorMultiplier: Int = 2, | ||
val teamSize: Int) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
package com.github.wakingrufus.elo | ||
|
||
data class LeagueState(val league: League, | ||
val players: Map<String, Player> = HashMap(), | ||
val history: List<RatingHistoryItem> = ArrayList()) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package com.github.wakingrufus.elo | ||
|
||
data class Player( | ||
val id: String, | ||
val currentRating: Int, | ||
val gamesPlayed: Int, | ||
val wins: Int, | ||
val losses: Int | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
package com.github.wakingrufus.elo | ||
|
||
|
||
fun calculateTeamStartingRating(players: List<Player>): Int { | ||
var teamStartingRating = 0 | ||
players.forEach { teamStartingRating += it.currentRating } | ||
return teamStartingRating / players.size | ||
} | ||
|
||
fun calculateTeamAverageGamesPlayed(players: List<Player>): Int { | ||
var teamGamesPlayed = 0 | ||
players.forEach { teamGamesPlayed += it.gamesPlayed } | ||
return teamGamesPlayed / players.size | ||
} | ||
|
||
fun buildTeam(allPlayers: Map<String, Player>, teamIds: List<String>): List<Player> { | ||
val teamPlayers = ArrayList<Player>() | ||
teamIds.forEach { teamPlayers += allPlayers[it]!! } | ||
return teamPlayers | ||
} | ||
|
||
fun addNewPlayers(existingPlayers: Map<String, Player>, game: Game, startingRating: Int): Map<String, Player> { | ||
var newPlayerMap = existingPlayers | ||
val allPlayerIds = game.team1PlayerIds + game.team2PlayerIds | ||
allPlayerIds.forEach { playerId -> | ||
if (!newPlayerMap.containsKey(playerId)) { | ||
newPlayerMap += Pair(playerId, Player(id = playerId, | ||
gamesPlayed = 0, | ||
wins = 0, | ||
losses = 0, | ||
currentRating = startingRating)) | ||
} | ||
} | ||
return newPlayerMap | ||
} | ||
|
||
fun updatePlayer(player: Player, ratingAdjustment: Int, games: Int, wins: Int, losses: Int): Player { | ||
return player.copy( | ||
currentRating = player.currentRating + ratingAdjustment, | ||
gamesPlayed = player.gamesPlayed + games, | ||
wins = player.wins + wins, | ||
losses = player.losses + losses) | ||
} |
78 changes: 78 additions & 0 deletions
78
src/main/kotlin/com/github/wakingrufus/elo/RatingHistoryEvents.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
package com.github.wakingrufus.elo | ||
|
||
import mu.KotlinLogging | ||
import java.math.BigDecimal | ||
import java.math.MathContext | ||
|
||
private val logger = KotlinLogging.logger {} | ||
fun calculateChanges(league: League, players: Map<String, Player>, game: Game): List<RatingHistoryItem> { | ||
|
||
val team1Players = buildTeam(allPlayers = players, teamIds = game.team1PlayerIds) | ||
val team1StartingRating = calculateTeamStartingRating(team1Players) | ||
val team1AvgGamesPlayed = calculateTeamAverageGamesPlayed(team1Players) | ||
|
||
val team2Players = buildTeam(allPlayers = players, teamIds = game.team2PlayerIds) | ||
val team2StartingRating = calculateTeamStartingRating(team2Players) | ||
val team2AvgGamesPlayed = calculateTeamAverageGamesPlayed(team2Players) | ||
|
||
val team1ExpectedScoreRatio = calculateExpectedScore(team1StartingRating, team2StartingRating, league.xi) | ||
val team2ExpectedScoreRatio = calculateExpectedScore(team2StartingRating, team1StartingRating, league.xi) | ||
|
||
logger.debug("team1ExpectedScoreRatio: " + team1ExpectedScoreRatio.toPlainString()) | ||
logger.debug("team2ExpectedScoreRatio: " + team2ExpectedScoreRatio.toPlainString()) | ||
|
||
val totalGoals = game.team1Score + game.team2Score | ||
val team1ActualScore = BigDecimal(game.team1Score).divide(BigDecimal(totalGoals), MathContext.DECIMAL32) | ||
val team2ActualScore = BigDecimal(game.team2Score).divide(BigDecimal(totalGoals), MathContext.DECIMAL32) | ||
|
||
logger.debug("team1ActualScore: " + team1ActualScore.toPlainString()) | ||
logger.debug("team2ActualScore: " + team2ActualScore.toPlainString()) | ||
|
||
var changeList: List<RatingHistoryItem> = ArrayList() | ||
for (player in team1Players) { | ||
logger.debug("applying changes to player: " + player.toString()) | ||
val kFactor = calculateKfactor(league.kFactorBase, league.trialPeriod, | ||
league.trialKFactorMultiplier, player.gamesPlayed, team2AvgGamesPlayed) | ||
logger.debug("using kfactor: " + kFactor) | ||
val adjustment = calculateAdjustment(kFactor, team1ActualScore, team1ExpectedScoreRatio) | ||
logger.debug("adjustment: " + adjustment) | ||
val newHistory = RatingHistoryItem( | ||
gameId = game.id, | ||
playerId = player.id, | ||
ratingAdjustment = adjustment, | ||
startingRating = player.currentRating, | ||
win = game.team1Score > game.team2Score | ||
) | ||
changeList += newHistory | ||
} | ||
|
||
for (player in team2Players) { | ||
logger.debug("applying changes to player: " + player.toString()) | ||
val kFactor = calculateKfactor(league.kFactorBase, league.trialPeriod, | ||
league.trialKFactorMultiplier, player.gamesPlayed, team1AvgGamesPlayed) | ||
logger.debug("using kfactor: " + kFactor) | ||
val adjustment = calculateAdjustment(kFactor, team2ActualScore, team2ExpectedScoreRatio) | ||
logger.debug("adjustment: " + adjustment) | ||
val newHistory = RatingHistoryItem( | ||
gameId = game.id, | ||
playerId = player.id, | ||
ratingAdjustment = adjustment, | ||
startingRating = player.currentRating, | ||
win = game.team2Score > game.team1Score | ||
) | ||
changeList += newHistory | ||
} | ||
return changeList | ||
} | ||
|
||
fun applyChanges(players: Map<String, Player>, changes: List<RatingHistoryItem>): Map<String, Player> { | ||
var newMap = players | ||
changes.forEach { | ||
newMap += Pair(it.playerId, updatePlayer( | ||
players[it.playerId]!!, | ||
it.ratingAdjustment, | ||
1, | ||
if (it.win) 1 else 0, if (it.win) 0 else 1)) | ||
} | ||
return newMap | ||
} |
9 changes: 9 additions & 0 deletions
9
src/main/kotlin/com/github/wakingrufus/elo/RatingHistoryItem.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package com.github.wakingrufus.elo | ||
|
||
data class RatingHistoryItem( | ||
val playerId: String, | ||
val gameId: String, | ||
val ratingAdjustment: Int, | ||
val startingRating: Int, | ||
val win: Boolean | ||
) |
Oops, something went wrong.