Skip to content

Commit

Permalink
#6 remove team size (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
wakingrufus authored Dec 29, 2017
1 parent ed66170 commit 73ec3d3
Show file tree
Hide file tree
Showing 10 changed files with 149 additions and 62 deletions.
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ ext {
isReleaseVersion = !(project.version =~ /-SNAPSHOT$/)
}

version = "0.2.2"
version = "0.3.0"
project.group = "com.github.wakingrufus"

dependencies {
Expand Down
12 changes: 6 additions & 6 deletions src/main/kotlin/com/github/wakingrufus/elo/BigDecimal.kt
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
package com.github.wakingrufus.elo

import java.math.BigDecimal
import java.math.MathContext
import java.math.RoundingMode

fun pow(base: BigDecimal, exponent: BigDecimal): BigDecimal {
var result = BigDecimal.ZERO
fun BigDecimal.pow( exponent: BigDecimal): BigDecimal {
val signOf2 = exponent.signum()

// Perform X^(A+B)=X^A*X^B (B = remainder)
val dn1 = base.toDouble()
val dn1 = this.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 intPow = this.pow(n2IntPart.intValueExact())
val doublePow = BigDecimal(Math.pow(dn1, remainderOf2.toDouble()))
result = intPow.multiply(doublePow)
var result = intPow.multiply(doublePow)

// Fix negative power
if (signOf2 == -1)
result = BigDecimal.ONE.divide(result, RoundingMode.HALF_UP)
result = BigDecimal.ONE.divide(result, MathContext.DECIMAL64)
return result
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ fun calculateExpectedScore(rating1: Int, rating2: Int, xi: Int): BigDecimal {
}

private fun calculateQ(teamRating: Int, xi: Int): BigDecimal {
return pow(BigDecimal.TEN, BigDecimal(teamRating).divide(BigDecimal(xi), MathContext.DECIMAL32))
return BigDecimal.TEN.pow(BigDecimal(teamRating).divide(BigDecimal(xi), MathContext.DECIMAL32))
}

private fun calculateE(q1: BigDecimal, q2: BigDecimal): BigDecimal {
Expand Down
3 changes: 1 addition & 2 deletions src/main/kotlin/com/github/wakingrufus/elo/League.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,4 @@ 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)
val trialKFactorMultiplier: Int = 2)
6 changes: 3 additions & 3 deletions src/main/kotlin/com/github/wakingrufus/elo/Player.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package com.github.wakingrufus.elo
data class Player(
val id: String,
val currentRating: Int,
val gamesPlayed: Int,
val wins: Int,
val losses: Int
val gamesPlayed: Int = 0,
val wins: Int = 0,
val losses: Int = 0
)
30 changes: 13 additions & 17 deletions src/main/kotlin/com/github/wakingrufus/elo/PlayerUtil.kt
Original file line number Diff line number Diff line change
@@ -1,34 +1,30 @@
package com.github.wakingrufus.elo


fun calculateTeamStartingRating(players: List<Player>): Int {
var teamStartingRating = 0
players.forEach { teamStartingRating += it.currentRating }
return teamStartingRating / players.size
fun calculateTeamRating(players: Collection<Player>): Int {
return players.sumBy { it.currentRating }.div(players.size)
}

fun calculateTeamAverageGamesPlayed(players: List<Player>): Int {
var teamGamesPlayed = 0
players.forEach { teamGamesPlayed += it.gamesPlayed }
return teamGamesPlayed / players.size
fun calculateTeamGamesPlayed(players: Collection<Player>): Int {
return players.sumBy { it.gamesPlayed }.div(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 buildTeam(allPlayers: Map<String, Player>, teamIds: List<String>): Collection<Player> {
if(teamIds.any { !allPlayers.containsKey(it) }) {
throw RuntimeException("playerId list contains ids which no player has")
}
return allPlayers.filterKeys { teamIds.contains(it) }.values
}

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))
newPlayerMap += Pair(playerId, Player(
id = playerId,
currentRating = startingRating
))
}
}
return newPlayerMap
Expand Down
61 changes: 31 additions & 30 deletions src/main/kotlin/com/github/wakingrufus/elo/RatingHistoryEvents.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,10 @@ 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 team1StartingRating = calculateTeamRating(team1Players)

val team2Players = buildTeam(allPlayers = players, teamIds = game.team2PlayerIds)
val team2StartingRating = calculateTeamStartingRating(team2Players)
val team2AvgGamesPlayed = calculateTeamAverageGamesPlayed(team2Players)
val team2StartingRating = calculateTeamRating(team2Players)

val team1ExpectedScoreRatio = calculateExpectedScore(team1StartingRating, team2StartingRating, league.xi)
val team2ExpectedScoreRatio = calculateExpectedScore(team2StartingRating, team1StartingRating, league.xi)
Expand All @@ -28,44 +26,47 @@ fun calculateChanges(league: League, players: Map<String, Player>, game: Game):
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(
return team1Players.map { player ->
RatingHistoryItem(
gameId = game.id,
playerId = player.id,
ratingAdjustment = adjustment,
ratingAdjustment = calculateAdjustment(
kFactor = calculateKfactor(
kfactorBase = league.kFactorBase,
trialPeriod = league.trialPeriod,
trialMultiplier = league.trialKFactorMultiplier,
playerGamesPlayed = player.gamesPlayed,
otherPlayerGamesPlayed = calculateTeamGamesPlayed(team2Players)
),
score = team1ActualScore,
expectedScore = team1ExpectedScoreRatio
),
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(
}.plus(team2Players.map { player ->
RatingHistoryItem(
gameId = game.id,
playerId = player.id,
ratingAdjustment = adjustment,
ratingAdjustment = calculateAdjustment(
kFactor = calculateKfactor(
kfactorBase = league.kFactorBase,
trialPeriod = league.trialPeriod,
trialMultiplier = league.trialKFactorMultiplier,
playerGamesPlayed = player.gamesPlayed,
otherPlayerGamesPlayed = calculateTeamGamesPlayed(team1Players)
),
score = team2ActualScore,
expectedScore = team2ExpectedScoreRatio
),
startingRating = player.currentRating,
win = game.team2Score > game.team1Score
)
changeList += newHistory
}
return changeList
}).toList()
}

fun applyChanges(players: Map<String, Player>, changes: List<RatingHistoryItem>): Map<String, Player> {
fun applyChanges(players: Map<String, Player>, changes: List<RatingHistoryItem>)
: Map<String, Player> {
var newMap = players
changes.forEach {
newMap += Pair(it.playerId, updatePlayer(
Expand Down
37 changes: 37 additions & 0 deletions src/test/kotlin/com/github/wakingrufus/elo/BigDecimalKtTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.github.wakingrufus.elo

import org.junit.Test
import java.math.BigDecimal
import kotlin.test.assertTrue

class BigDecimalKtTest {
@Test
fun pow() {
2 `to the` 3 equals 8
10 `to the` 3 equals 1000
10 `to the` 1.5 equals BigDecimal("31.622776601683795227870632515987381339073181152343750")
5 `to the` 0 equals 1
5 `to the` 1 equals 5
10 `to the` -2 equals BigDecimal("0.01")
10 `to the` -1 equals BigDecimal("0.1")
}

infix fun Int.`to the`(exp: Int): BigDecimal {
return this.toBigDecimal().pow(exp.toBigDecimal())
}

infix fun Int.`to the`(exp: Double): BigDecimal {
return this.toBigDecimal().pow(exp.toBigDecimal())
}

infix fun Number.equals(result: Int): Unit {
assertTrue(BigDecimal(this.toString()).compareTo(result.toBigDecimal()) == 0,
"expected ${this} but was $result")
}

infix fun Number.equals(expected: BigDecimal): Unit {
assertTrue(BigDecimal(this.toString()).compareTo(expected) == 0,
"expected $expected but was $this")
}

}
4 changes: 2 additions & 2 deletions src/test/kotlin/com/github/wakingrufus/elo/EloLeagueKtTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class EloLeagueKtTest {
// data
val player1Id = UUID.randomUUID().toString()
val player2Id = UUID.randomUUID().toString()
val league = League(teamSize = 1, kFactorBase = 32, trialKFactorMultiplier = 1)
val league = League(kFactorBase = 32, trialKFactorMultiplier = 1)
val game = Game(
id = UUID.randomUUID().toString(),
team1Score = 10,
Expand Down Expand Up @@ -48,7 +48,7 @@ class EloLeagueKtTest {
val player2Id = UUID.randomUUID().toString()
val player1 = Player(id = player1Id, gamesPlayed = 20, currentRating = 1200, wins = 5, losses = 15)
val player2 = Player(id = player2Id, gamesPlayed = 15, currentRating = 1600, losses = 0, wins = 15)
val league = League(teamSize = 1, kFactorBase = 32, trialKFactorMultiplier = 1)
val league = League(kFactorBase = 32, trialKFactorMultiplier = 1)
val game = Game(
id = UUID.randomUUID().toString(),
team1Score = 9,
Expand Down
54 changes: 54 additions & 0 deletions src/test/kotlin/com/github/wakingrufus/elo/PlayerUtilKtTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.github.wakingrufus.elo

import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
import org.junit.Test

class PlayerUtilKtTest {
@Test
fun `test calculateTeamRating`() {
assertEquals("should return average of ratings",
1500,
calculateTeamRating(listOf(
Player(id = "1", currentRating = 1400),
Player(id = "2", currentRating = 1600)
)))
}

@Test
fun `test calculateTeamAverageGamesPlayed`() {
assertEquals("returns average",
3,
calculateTeamGamesPlayed(listOf(
Player(id = "1", currentRating = 1400, gamesPlayed = 2),
Player(id = "2", currentRating = 1600, gamesPlayed = 4)
)))
assertEquals("returns average truncated",
3,
calculateTeamGamesPlayed(listOf(
Player(id = "1", currentRating = 1400, gamesPlayed = 3),
Player(id = "2", currentRating = 1600, gamesPlayed = 4)
)))
}

@Test
fun `test buildTeam`() {
val p1 = Player(id = "1", currentRating = 1550)
val p2 = Player(id = "2", currentRating = 1000)
assertArrayEquals(listOf(p1).toTypedArray(),
buildTeam(
mapOf("1" to p1, "2" to p2),
listOf("1")
).toTypedArray())
}

@Test(expected = RuntimeException::class)
fun `test buildTeam with missing id`() {
val p1 = Player(id = "1", currentRating = 1550)
val p2 = Player(id = "2", currentRating = 1000)
buildTeam(
mapOf("1" to p1, "2" to p2),
listOf("1", "3")
)
}
}

0 comments on commit 73ec3d3

Please sign in to comment.