Skip to content

Commit

Permalink
Merge pull request #1 from wakingrufus/initial
Browse files Browse the repository at this point in the history
Initial Commit
  • Loading branch information
wakingrufus committed Nov 20, 2017
2 parents 1c67acd + 580674a commit 369fad2
Show file tree
Hide file tree
Showing 17 changed files with 459 additions and 0 deletions.
10 changes: 10 additions & 0 deletions .gitignore
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
5 changes: 5 additions & 0 deletions README.md
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)
67 changes: 67 additions & 0 deletions build.gradle
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"
}
}
1 change: 1 addition & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
rootProject.name = 'lib-elo'
27 changes: 27 additions & 0 deletions shippable.yml
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
26 changes: 26 additions & 0 deletions src/main/kotlin/com/github/wakingrufus/elo/BigDecimal.kt
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 src/main/kotlin/com/github/wakingrufus/elo/EloCalculator.kt
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
}
24 changes: 24 additions & 0 deletions src/main/kotlin/com/github/wakingrufus/elo/EloLeague.kt
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 src/main/kotlin/com/github/wakingrufus/elo/ExpectedScore.kt
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)
}
10 changes: 10 additions & 0 deletions src/main/kotlin/com/github/wakingrufus/elo/Game.kt
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>)
8 changes: 8 additions & 0 deletions src/main/kotlin/com/github/wakingrufus/elo/League.kt
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)
5 changes: 5 additions & 0 deletions src/main/kotlin/com/github/wakingrufus/elo/LeagueState.kt
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())
9 changes: 9 additions & 0 deletions src/main/kotlin/com/github/wakingrufus/elo/Player.kt
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
)
43 changes: 43 additions & 0 deletions src/main/kotlin/com/github/wakingrufus/elo/PlayerUtil.kt
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 src/main/kotlin/com/github/wakingrufus/elo/RatingHistoryEvents.kt
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
}
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
)
Loading

0 comments on commit 369fad2

Please sign in to comment.