Skip to content

Commit

Permalink
PoolPoint system (#24)
Browse files Browse the repository at this point in the history
  • Loading branch information
zelytra authored Jun 11, 2024
1 parent b0357c4 commit 3d81ec4
Show file tree
Hide file tree
Showing 14 changed files with 525 additions and 19 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package fr.zelytra.poolpoint;

import fr.zelytra.logger.LogEndpoint;
import fr.zelytra.user.UserService;
import io.quarkus.security.identity.SecurityIdentity;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.Response;

@Path("/leaderboard")
public class LeaderboardEndpoint {

@Inject
UserService userService;

@Inject
SecurityIdentity securityIdentity;

@GET
@LogEndpoint
@Path("/all")
public Response getTopLeaderboard() {
return Response.ok(userService.getUsersOrderByPoolPoint()).build();
}

@GET
@LogEndpoint
@Path("/self")
public Response getUserLeaderboardPosition() {
return Response.ok(userService.getUsersLeaderboardPosition(securityIdentity.getPrincipal().getName())).build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package fr.zelytra.poolpoint;

import java.math.BigDecimal;
import java.math.RoundingMode;

public class PoolPointCalculator {

private final int poolpoint;
private final int k; // K-factor
private final int totalGamePlayed;

public PoolPointCalculator(int poolpoint, int totalGamePlayed) {
this.poolpoint = poolpoint;
this.totalGamePlayed = totalGamePlayed;
this.k = getFactorK();
}

/**
* @param partyStatus Win = 1 | Loose = 0 | Draw = 0.5
* @param opponentElo Poolpoint of opponent
* @return En+1 = En + K * (W - p(D)) The new elo of the player
*/
public int computeNewElo(double partyStatus, int opponentElo) {
return (int) Math.round(poolpoint + k * (partyStatus - getWinProbability(opponentElo)));
}

public int getFactorK() {
// New player with less than 30 games and less than 2300 of score
if (totalGamePlayed < 30 && poolpoint < 2300) {
return 40;
}
// Old player with less than 2400 of score
else if (poolpoint < 2400) {
return 20;
}
// Old player with a superior or equal score of 2400
return 10;
}

/**
* The function p(D) Which represent win probability
*
* @param opponentElo Poolpoint of opponent
* @return p(D) Gain probability
*/
public double getWinProbability(int opponentElo) {
double eloDelta = poolpoint - opponentElo; // D
if (Math.abs(eloDelta) > 400 && eloDelta != 0) {
eloDelta = (400 * eloDelta) / Math.abs(eloDelta);
}
return BigDecimal.valueOf(1 / (1 + Math.pow(10, -eloDelta / 400))).setScale(3, RoundingMode.HALF_UP).doubleValue();
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package fr.zelytra.poolpoint;

public record UserLeaderBoardPosition(String username, int pp, long position) {
}
1 change: 1 addition & 0 deletions backend/src/main/java/fr/zelytra/user/UserEndpoint.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public class UserEndpoint {

@GET
@Path("/preferences")

@LogEndpoint
@Transactional
public Response getPreferences() {
Expand Down
23 changes: 22 additions & 1 deletion backend/src/main/java/fr/zelytra/user/UserEntity.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,31 @@ public class UserEntity extends PanacheEntityBase {
@Column(columnDefinition = "integer")
private int pp;

@Column(columnDefinition = "integer")
private int gamePlayed;

public UserEntity() {
}
public UserEntity(String username){
Log.info("New user created : " + username);
this.pp = 100;//TODO Need to be modify when rating system is implemented
this.pp = 1200;
this.gamePlayed = 0;
this.authUsername=username;
this.username=username;
this.online=true;
this.persistAndFlush();
}

public UserEntity(UserEntity user) {
this.authUsername = user.authUsername;
this.username = user.username;
this.icon = user.icon;
this.online = user.online;
this.createdAt = user.createdAt;
this.pp = user.pp;
this.gamePlayed = user.gamePlayed;
}

@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
Expand Down Expand Up @@ -93,4 +107,11 @@ public void setPp(int pp) {
this.pp = pp;
}

public int getGamePlayed() {
return gamePlayed;
}

public void setGamePlayed(int gamePlayed) {
this.gamePlayed = gamePlayed;
}
}
25 changes: 24 additions & 1 deletion backend/src/main/java/fr/zelytra/user/UserService.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
package fr.zelytra.user;

import fr.zelytra.poolpoint.UserLeaderBoardPosition;
import fr.zelytra.user.reflections.LeaderBoardUser;
import io.quarkus.hibernate.orm.panache.PanacheRepository;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;

import java.util.List;

@ApplicationScoped
public class UserService {
public class UserService implements PanacheRepository<UserEntity> {

@PersistenceContext
EntityManager em;

public UserEntity getOrCreateUserByName(String username) {
UserEntity user = UserEntity.findById(username);
Expand All @@ -19,4 +29,17 @@ public UserEntity getOrCreateUserByName(String username) {
public UserEntity getUserByName(String username) {
return UserEntity.findById(username);
}

public List<LeaderBoardUser> getUsersOrderByPoolPoint() {
return UserEntity.find("ORDER BY pp").page(0, 100).project(LeaderBoardUser.class).list();
}

public UserLeaderBoardPosition getUsersLeaderboardPosition(String username) {
UserEntity userEntity = getUserByName(username);
// Count the number of users with higher pp
Long position = em.createQuery("SELECT COUNT(u) FROM UserEntity u WHERE u.pp > :userPp", Long.class)
.setParameter("userPp", userEntity.getPp())
.getSingleResult();
return new UserLeaderBoardPosition(userEntity.getAuthUsername(), userEntity.getPp(), position + 1);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package fr.zelytra.user.reflections;

import io.quarkus.runtime.annotations.RegisterForReflection;

@RegisterForReflection
public record LeaderBoardUser(String username, String icon, int pp) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ class FriendEndpointTest {
@Inject
FriendService friendService;


@BeforeEach
@Transactional
void init() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package fr.zelytra.poolpoint;

import fr.zelytra.user.UserEntity;
import fr.zelytra.user.reflections.LeaderBoardUser;
import io.quarkus.test.common.QuarkusTestResource;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.oidc.server.OidcWiremockTestResource;
import io.restassured.common.mapper.TypeRef;
import jakarta.transaction.Transactional;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

import java.util.List;
import java.util.Set;

import static io.quarkus.test.oidc.server.OidcWiremockTestResource.getAccessToken;
import static io.restassured.RestAssured.given;
import static org.junit.jupiter.api.Assertions.assertEquals;

@QuarkusTest
@QuarkusTestResource(OidcWiremockTestResource.class)
class LeaderboardEndpointTest {

@BeforeAll
@Transactional
static void init() {
for (int x = 0; x < 200; x++) {
UserEntity user = new UserEntity("User" + x);
user.setPp(1000 + x);
}
}

@Test
void getTopLeaderboard_maxResult100() {
List<LeaderBoardUser> list = given()
.auth().oauth2(getAccessToken("User1", Set.of("user")))
.when().get("/leaderboard/all")
.then().statusCode(200)
.extract()
.body()
.as(new TypeRef<>() {
});
assertEquals(100, list.size(), "Should return max 100 results");
}

@Test
void getTopLeaderboard_orderByPP() {
List<LeaderBoardUser> list = given()
.auth().oauth2(getAccessToken("User1", Set.of("user")))
.when().get("/leaderboard/all")
.then().statusCode(200)
.extract()
.body()
.as(new TypeRef<>() {
});
int index = 0;
for (LeaderBoardUser leaderBoardUser : list) {
assertEquals(1000 + index, leaderBoardUser.pp());
index++;
}
}

@Test
void getUserLeaderboardPosition_returnTheRightPosition1() {
UserLeaderBoardPosition userLeaderBoardPosition = given()
.auth().oauth2(getAccessToken("User0", Set.of("user")))
.when().get("/leaderboard/self")
.then().statusCode(200)
.extract()
.body()
.as(UserLeaderBoardPosition.class);
assertEquals(200, userLeaderBoardPosition.position());
assertEquals(1000,userLeaderBoardPosition.pp());
}

@Test
void getUserLeaderboardPosition_returnTheRightPosition2() {
UserLeaderBoardPosition userLeaderBoardPosition = given()
.auth().oauth2(getAccessToken("User42", Set.of("user")))
.when().get("/leaderboard/self")
.then().statusCode(200)
.extract()
.body()
.as(UserLeaderBoardPosition.class);
assertEquals(158, userLeaderBoardPosition.position());
assertEquals(1042,userLeaderBoardPosition.pp());
}

@AfterAll
@Transactional
static void cleanDataBase() {
UserEntity.deleteAll();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package fr.zelytra.poolpoint;

import fr.zelytra.poolpoint.simulation.PoolPlayerSimulator;
import io.quarkus.logging.Log;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.transaction.Transactional;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

@QuarkusTest
class PoolPointCalculatorTest {

@Test
@Transactional
void simulation() {
PoolPlayerSimulator poolPlayerSimulator = new PoolPlayerSimulator(101, 5000);
Log.info(poolPlayerSimulator);
poolPlayerSimulator.generateCSV("game_history.csv");
}

@Test
void getFactorK_lessThan30Games() {
int user1PP = 600;
int user1GamesPlayed = 25;
PoolPointCalculator poolPointCalculator = new PoolPointCalculator(user1PP, user1GamesPlayed);
assertEquals(40, poolPointCalculator.getFactorK());
}

@Test
void getFactorK_lessThan2400AndMoreThan30Games() {
int user1PP = 600;
int user1GamesPlayed = 80;
PoolPointCalculator poolPointCalculator = new PoolPointCalculator(user1PP, user1GamesPlayed);
assertEquals(20, poolPointCalculator.getFactorK());
}

@Test
void getFactorK_MoreThan2400() {
int user1PP = 2401;
int user1GamesPlayed = 80;
PoolPointCalculator poolPointCalculator = new PoolPointCalculator(user1PP, user1GamesPlayed);
assertEquals(10, poolPointCalculator.getFactorK());
}

@Test
void getWinProbability_deltaLessThan400() {
int user1PP = 1800;
int user1GamesPlayed = 80;
int user2PP = 1600;
PoolPointCalculator poolPointCalculator = new PoolPointCalculator(user1PP, user1GamesPlayed);
assertEquals(0.76, poolPointCalculator.getWinProbability(user2PP));
assertEquals(20, poolPointCalculator.getFactorK());
}

@Test
void getWinProbability_deltaMoreThan400() {
int user1PP = 2600;
int user1GamesPlayed = 80;
int user2PP = 1600;
PoolPointCalculator poolPointCalculator = new PoolPointCalculator(user1PP, user1GamesPlayed);
assertEquals(0.909, poolPointCalculator.getWinProbability(user2PP));
assertEquals(10, poolPointCalculator.getFactorK());
}

@Test
void computeNewElo_win() {
int user1PP = 2600;
int user1GamesPlayed = 80;
int user2PP = 1600;
PoolPointCalculator poolPointCalculator = new PoolPointCalculator(user1PP, user1GamesPlayed);
assertEquals(0.909, poolPointCalculator.getWinProbability(user2PP));
assertEquals(10, poolPointCalculator.getFactorK());
assertEquals(2601, poolPointCalculator.computeNewElo(1, user2PP));

}

@Test
void computeNewElo_loose() {
int user1PP = 2600;
int user1GamesPlayed = 80;
int user2PP = 1600;
PoolPointCalculator poolPointCalculator = new PoolPointCalculator(user1PP, user1GamesPlayed);
assertEquals(0.909, poolPointCalculator.getWinProbability(user2PP));
assertEquals(10, poolPointCalculator.getFactorK());
assertEquals(2591, poolPointCalculator.computeNewElo(0, user2PP));

}


}
Loading

0 comments on commit 3d81ec4

Please sign in to comment.