Merge pull request #64 from aguibert/leaderboard

Initial draft of leaderboard
This commit is contained in:
Andrew Guibert 2018-04-07 12:37:10 -05:00 committed by GitHub
commit 7ef54ea77d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 233 additions and 47 deletions

View File

@ -73,6 +73,10 @@ task copyFrontend(type: Copy) {
into "${rootDir}/frontend/src/main/webapp"
}
npm_start {
dependsOn 'libertyStop'
}
liberty {
server {
name = 'frontendServer'

View File

@ -12,6 +12,7 @@ import { GameComponent } from './game/game.component';
import { ControlsComponent } from './controls/controls.component';
import { PlayerListComponent } from './game/playerlist/playerlist.component';
import { PlayerComponent } from './game/player/player.component';
import { LeaderboardComponent } from './game/leaderboard/leaderboard.component';
@NgModule({
imports: [
@ -27,6 +28,7 @@ import { PlayerComponent } from './game/player/player.component';
ControlsComponent,
PlayerListComponent,
PlayerComponent,
LeaderboardComponent,
],
providers: [ ],
bootstrap: [ AppComponent ]

View File

@ -19,7 +19,7 @@
</div>
<div id="leaderboard">
<rank-list></rank-list>
</div>
<div id="footer" class="navbar">

View File

@ -0,0 +1,23 @@
<div id="title"><h2>Leaderboard</h2></div>
<table class="leaderboard-container">
<thead>
<tr>
<td>Rank</td>
<td>Player Name</td>
<td>Wins</td>
<td>Games Played</td>
<td>Win rate</td>
<td>Rating</td>
</tr>
</thead>
<tbody>
<tr *ngFor="let ranking of rankings">
<td>{{ ranking.rank }}</td>
<td>{{ ranking.name }}</td>
<td>{{ ranking.numWins }}</td>
<td>{{ ranking.totalGames }}</td>
<td>{{ ranking.winLossRatio }} %</td>
<td>{{ ranking.rating }}</td>
</tr>
</tbody>
</table>

View File

@ -0,0 +1,34 @@
:host {
display: grid;
grid-template-rows: repeat(2, 1fr);
grid-template-columns: 50px 1fr;
min-height: 100%;
align-content: center;
}
#title {
grid-area: 1 / 1 / span 2 / 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
#title h2 {
transform: rotate(-90deg);
color: white;
text-transform: lowercase;
font-variant: small-caps;
letter-spacing: .1em;
margin: 0;
padding: 0;
}
.leaderboard-container {
margin-left: 15px;
margin-top: 15px;
margin-right: 15px;
}

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { LeaderboardComponent } from './leaderboard.component';
describe('LeaderboardComponent', () => {
let component: LeaderboardComponent;
let fixture: ComponentFixture<LeaderboardComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ LeaderboardComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(LeaderboardComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,42 @@
import { Component, OnInit, NgZone } from '@angular/core';
import { Ranking } from './ranking/ranking';
import { HttpClient } from '@angular/common/http';
import { environment } from './../../../environments/environment';
@Component({
selector: 'rank-list',
templateUrl: './leaderboard.component.html',
styleUrls: ['./leaderboard.component.scss']
})
export class LeaderboardComponent implements OnInit {
rankings: Ranking[] = new Array();
constructor(private ngZone: NgZone, private http: HttpClient) {
}
ngOnInit() {
this.getLeaders();
}
async getLeaders() {
try {
console.log("Calling rank service");
let data = await this.http.get(`${environment.API_URL_RANKS}/top/10`).toPromise();
console.log(`Got leaders: ${JSON.stringify(data)}`);
const json = data as any;
const rankingsArr = new Array();
let i = 1;
for (let ranking of json) {
console.log(`Got rank ${JSON.stringify(ranking)}`);
rankingsArr.push(new Ranking(i++, ranking.name, ranking.stats.numWins, ranking.stats.totalGames, ranking.stats.rating));
}
this.ngZone.run(() => {
this.rankings = rankingsArr;
});
} catch (error) {
console.log(error);
}
}
}

View File

@ -0,0 +1,17 @@
export class Ranking {
public rank: number;
public name: string;
public numWins: number;
public winLossRatio: string;
public totalGames: number;
public rating: number;
constructor(rank: number, name: string, numWins: number, totalGames: number, rating: number) {
this.rank = rank;
this.name = name;
this.numWins = numWins;
this.totalGames = totalGames;
this.winLossRatio = totalGames === 0 ? '--' : Number((numWins / totalGames) * 100).toFixed();
this.rating = rating;
}
}

View File

@ -79,7 +79,6 @@ public class GameBoard {
preferredPlayerSlots.put(playerId, playerNum);
}
takenPlayerSlots[playerNum] = true;
System.out.println("Player slot " + playerNum + " taken");
// Don't let the preferred player slot map take up too much memory
if (preferredPlayerSlots.size() > 1000)

View File

@ -4,7 +4,9 @@ import static org.libertybikes.game.round.service.GameRoundWebsocket.sendTextToC
import static org.libertybikes.game.round.service.GameRoundWebsocket.sendTextToClients;
import java.time.Instant;
import java.util.ArrayDeque;
import java.util.Date;
import java.util.Deque;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
@ -60,6 +62,7 @@ public class GameRound implements Runnable {
private final AtomicBoolean paused = new AtomicBoolean();
private final AtomicBoolean heartbeatStarted = new AtomicBoolean();
private final Map<Session, Client> clients = new HashMap<>();
private final Deque<Player> playerRanks = new ArrayDeque<>();
private int ticksFromGameEnd = 0;
@ -250,14 +253,12 @@ public class GameRound implements Runnable {
return; // Don't update player stats for single-player games
PlayerService playerSvc = CDI.current().select(PlayerService.class, RestClient.LITERAL).get();
for (Player p : players) {
if (p.getStatus() == STATUS.Winner) {
log("Player " + p.name + " has won the round");
playerSvc.addWin(p.id);
} else {
log("Player " + p.name + " has participated in the round");
playerSvc.addLoss(p.id);
}
int rank = 1;
for (Player p : playerRanks) {
log("Player " + p.name + " came in place " + rank);
if (p.isRealPlayer())
playerSvc.recordGame(p.id, rank);
rank++;
}
}
@ -281,6 +282,7 @@ public class GameRound implements Runnable {
playersMoved = true;
} else {
death = true;
playerRanks.push(p);
}
}
}
@ -337,6 +339,7 @@ public class GameRound implements Runnable {
}
if (alivePlayers == 1) {
alive.setStatus(STATUS.Winner);
playerRanks.push(alive);
gameState = State.FINISHED;
}

View File

@ -262,8 +262,13 @@ public class Player {
return isAlive;
}
@JsonbTransient
public boolean isRealPlayer() {
return ai == null;
}
public void processAIMove(short[][] board) {
if (ai == null)
if (isRealPlayer())
return;
try {
direction = ai.processGameTick(board);

View File

@ -6,8 +6,8 @@ import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
@ -22,11 +22,7 @@ public interface PlayerService {
public Player getPlayerById(@PathParam("playerId") String id);
@POST
@Path("/{playerId}/win")
public Response addWin(@PathParam("playerId") String id);
@POST
@Path("/{playerId}/loss")
public Response addLoss(@PathParam("playerId") String id);
@Path("/{playerId}/recordGame")
public void recordGame(@PathParam("playerId") String id, @QueryParam("place") int place);
}

View File

@ -30,7 +30,7 @@ public class PlayerDB {
public Collection<Player> topPlayers(int numPlayers) {
return allPlayers.values()
.stream()
.sorted(Player::compareByWins)
.sorted(Player::compareOverall)
.limit(numPlayers)
.collect(Collectors.toList());
}

View File

@ -2,10 +2,14 @@ package org.libertybikes.player.service;
import java.util.UUID;
import javax.json.bind.Jsonb;
import javax.json.bind.JsonbBuilder;
import javax.json.bind.annotation.JsonbCreator;
public class Player {
private static final Jsonb jsonb = JsonbBuilder.create();
public final String id;
public final String name;
@ -23,11 +27,30 @@ public class Player {
}
public static int compareByWins(Player a, Player b) {
return b.stats.numWins - a.stats.numWins;
return Integer.compare(b.stats.numWins, a.stats.numWins);
}
public static double compareByWinRatio(Player a, Player b) {
return b.stats.winLossRatio() - a.stats.winLossRatio();
public static int compareByWinRatio(Player a, Player b) {
return Double.compare(b.stats.winLossRatio(), a.stats.winLossRatio());
}
public static int compareByRating(Player a, Player b) {
return Integer.compare(b.stats.rating, a.stats.rating);
}
public static int compareOverall(Player a, Player b) {
int rating = compareByRating(a, b);
if (rating != 0)
return rating;
int wins = compareByWins(a, b);
if (wins != 0)
return wins;
return compareByWinRatio(a, b);
}
@Override
public String toString() {
return jsonb.toJson(this);
}
}

View File

@ -29,12 +29,8 @@ public class PlayerService {
Random r = new Random();
for (int i = 0; i < 10; i++) {
String id = createPlayer("SamplePlayer-" + i);
int wins = r.nextInt(3);
int losses = r.nextInt(3);
for (int w = 0; w < wins; w++)
addWin(id);
for (int l = 0; l < losses; l++)
addLoss(id);
for (int j = 0; j < 3; j++)
recordGame(id, r.nextInt(4) + 1);
}
}
@ -64,26 +60,27 @@ public class PlayerService {
}
@POST
@Path("/{playerId}/win")
public void addWin(@PathParam("playerId") String id) {
Player p = getPlayerById(id);
if (p == null)
return;
p.stats.numWins++;
p.stats.totalGames++;
db.put(p);
System.out.println("Player " + id + " has won " + p.stats.numWins + " games and played in " + p.stats.totalGames + " games.");
}
@POST
@Path("/{playerId}/loss")
public void addLoss(@PathParam("playerId") String id) {
@Path("/{playerId}/recordGame")
public void recordGame(@PathParam("playerId") String id, @QueryParam("place") int place) {
Player p = getPlayerById(id);
if (p == null)
return;
p.stats.totalGames++;
switch (place) {
case 1:
p.stats.numWins++;
p.stats.rating += 28;
break;
case 2:
p.stats.rating += 14;
break;
case 3:
p.stats.rating -= 5;
break;
default:
p.stats.rating -= 12;
}
db.put(p);
System.out.println("Player " + id + " has won " + p.stats.numWins + " games and played in " + p.stats.totalGames + " games.");
System.out.println(p);
}
}

View File

@ -6,8 +6,10 @@ public class PlayerStats {
public int numWins;
public int rating = 1000;
public double winLossRatio() {
return numWins / totalGames;
return totalGames == 0 ? 0 : numWins / totalGames;
}
}

View File

@ -1,6 +1,7 @@
package org.libertybikes.player.service;
import java.util.Collection;
import java.util.Collections;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
@ -20,9 +21,21 @@ public class RankingService {
PlayerDB db;
@GET
@Path("/top")
@Produces(MediaType.APPLICATION_JSON)
public Collection<Player> topPlayers() {
return db.topPlayers(5);
public Collection<Player> topFivePlayers() {
return topNPlayers(5);
}
@GET
@Path("/top/{numPlayers}")
@Produces(MediaType.APPLICATION_JSON)
public Collection<Player> topNPlayers(@PathParam("numPlayers") Integer numPlayers) {
if (numPlayers < 0)
return Collections.emptySet();
if (numPlayers > 100)
numPlayers = 100;
return db.topPlayers(numPlayers);
}
@GET

View File

@ -2,6 +2,7 @@
<featureManager>
<feature>microProfile-1.2</feature>
<feature>jaxrs-2.1</feature>
<feature>jsonb-1.0</feature>
</featureManager>
<httpEndpoint id="defaultHttpEndpoint" host="*" httpPort="${httpPort}" httpsPort="${httpsPort}" />