Merge pull request #69 from aguibert/party-ids

Party ids
This commit is contained in:
Andrew Guibert 2018-04-13 11:32:00 -05:00 committed by GitHub
commit d5fec1fa14
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 168 additions and 33 deletions

View File

@ -64,7 +64,7 @@ export class GameComponent implements OnInit {
if (sessionStorage.getItem('isSpectator') === 'true') {
console.log('is a spectator... showing game id');
// Set the Round ID and make visible
$('#game-code').html(this.roundId);
$('#game-code').html(sessionStorage.getItem('partyId'));
const gameId = $('#game-code-display');
gameId.removeClass('d-none');
gameId.addClass('d-inline-block');

View File

@ -12,17 +12,17 @@
<div class="button-bar">
<button type="button" (click)="quickJoin()">Quick Join</button>
</div>
<div></div>
<hr/>
<div class="login-form">
<div class="form-item">
<label>Code</label>
<input type="text" id="roundid" name="roundid" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false">
<input type="text" id="partyid" name="roundid" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false">
</div>
</div>
<div class="button-bar">
<button type="button" (click)="joinRound()">Join Round</button>
<button type="button" (click)="createRound()">Create Round</button>
<button id="hostButton" type="button" (click)="hostRound()" data-loading-text="Connecting..." autocomplete="off">Host Round</button>
<button type="button" (click)="joinRound()">Join Game</button>
<hr/>
<button id="hostButton" type="button" (click)="hostRound()" data-loading-text="Connecting..." autocomplete="off">Host Games</button>
</div>
</div>
</div>

View File

@ -81,7 +81,7 @@
outline: none;
}
#roundid {
#partyid {
text-transform: uppercase;
}

View File

@ -27,27 +27,19 @@ export class LoginComponent implements OnInit {
this.meta.addTag({name: 'viewport', content: `width=${viewWidth}px, height=${viewHeight}px, initial-scale=1.0`}, true);
}
async createRound() {
try {
let data = await this.http.post(`${environment.API_URL_GAME_ROUND}/create`, "", { responseType: 'text'}).toPromise();
$('#roundid').val(`${data}`)
} catch (error) {
console.log(error);
}
}
async quickJoin() {
// First get an unstarted round ID
let roundID = await this.http.post(`${environment.API_URL_GAME_ROUND}/available`, "", { responseType: 'text' }).toPromise();
let roundID = await this.http.get(`${environment.API_URL_GAME_ROUND}/available`, { responseType: 'text' }).toPromise();
// Then join the round
this.joinRoundById(roundID);
}
async joinRound() {
let roundID: string = $('#roundid').val();
this.joinRoundById(roundID);
let partyID: string = $('#partyid').val();
let roundID: any = await this.http.get(`${environment.API_URL_PARTY}/${partyID}/round`, { responseType: 'text' }).toPromise();
console.log(`Got roundID=${roundID} for partyID=${partyID}`);
this.joinRoundById(roundID);
}
async joinRoundById(roundID: string) {
@ -128,10 +120,10 @@ async joinRoundById(roundID: string) {
let router = this.router;
try {
let data = await this.http.post(`${environment.API_URL_GAME_ROUND}/create`, "", { responseType: 'text' }).toPromise();
console.log(`Created round with id=${data}`);
let party: any = await this.http.post(`${environment.API_URL_PARTY}/create`, "", { responseType: 'json' }).toPromise();
sessionStorage.setItem('isSpectator', 'true');
sessionStorage.setItem('roundId', data);
sessionStorage.setItem('partyId', party.id);
sessionStorage.setItem('roundId', party.currentRound.id);
ngZone.run(() => {
router.navigate(['/game']);
});

View File

@ -6,6 +6,7 @@
export const environment = {
production: false,
API_URL_AUTH: `${document.location.hostname}:8082`,
API_URL_PARTY: `http://${document.location.hostname}:8080/party`,
API_URL_GAME_ROUND: `http://${document.location.hostname}:8080/round`,
API_URL_GAME_WS: `ws://${document.location.hostname}:8080/round/ws`,
API_URL_PLAYERS: `http://${document.location.hostname}:8081/player`,

View File

@ -182,9 +182,6 @@ public class GameBoard {
}
}
/**
*
*/
public void addAI() {
// Find first open player slot to fill, which determines position
short playerNum = -1;
@ -192,7 +189,6 @@ public class GameBoard {
if (!takenPlayerSlots[i]) {
playerNum = i;
takenPlayerSlots[i] = true;
System.out.println("Player slot " + i + " taken");
break;
}
}

View File

@ -8,12 +8,14 @@ import java.util.ArrayDeque;
import java.util.Date;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import javax.enterprise.concurrent.LastExecution;
@ -63,6 +65,7 @@ public class GameRound implements Runnable {
private final AtomicBoolean heartbeatStarted = new AtomicBoolean();
private final Map<Session, Client> clients = new HashMap<>();
private final Deque<Player> playerRanks = new ArrayDeque<>();
private final Set<LifecycleCallback> lifecycleCallbacks = new HashSet<>();
private int ticksFromGameEnd = 0;
@ -142,6 +145,10 @@ public class GameRound implements Runnable {
beginHeartbeat();
}
public void addCallback(LifecycleCallback callback) {
lifecycleCallbacks.add(callback);
}
private void beginHeartbeat() {
// Send a heartbeat to connected clients every 100 seconds in an attempt to keep them connected.
// It appears that when running in IBM Cloud, sockets time out after 120 seconds
@ -351,6 +358,8 @@ public class GameRound implements Runnable {
sendTextToClients(clients.keySet(), jsonb.toJson(new OutboundMessage.StartingCountdown(STARTING_COUNTDOWN)));
delay(TimeUnit.SECONDS.toMillis(STARTING_COUNTDOWN));
lifecycleCallbacks.forEach(c -> c.get());
paused.set(false);
for (Player p : getPlayers())
if (STATUS.Connected == p.getStatus())
@ -372,6 +381,8 @@ public class GameRound implements Runnable {
System.out.println("[GameRound-" + id + "] " + msg);
}
public interface LifecycleCallback extends Supplier<Void> {}
private class HeartbeatTrigger implements Trigger {
private static final int HEARTBEAT_INTERVAL_SEC = 100;

View File

@ -3,7 +3,7 @@ package org.libertybikes.game.round.service;
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;
@ApplicationPath("/round")
@ApplicationPath("/")
public class GameRoundApp extends Application {
}

View File

@ -14,12 +14,13 @@ 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 org.libertybikes.game.core.GameRound;
import org.libertybikes.game.core.GameRound.State;
@Path("/")
@Path("/round")
@ApplicationScoped
public class GameRoundService {
@ -47,6 +48,19 @@ public class GameRoundService {
}
@POST
@Path("/create")
public GameRound createRoundById(@QueryParam("gameId") String gameId) {
GameRound newRound = new GameRound(gameId);
GameRound existingRound = allRounds.putIfAbsent(gameId, newRound);
GameRound round = existingRound == null ? newRound : existingRound;
System.out.println("Created round id=" + round.id);
if (allRounds.size() > 5)
System.out.println("WARNING: Found " + allRounds.size() + " active games in GameRoundService. " +
"They are probably not being cleaned up properly: " + allRounds.keySet());
return round;
}
@GET
@Path("/available")
public String getAvailableRound() {
Optional<GameRound> availableRound = allRounds.values()
@ -71,10 +85,7 @@ public class GameRoundService {
if (!oldRound.isStarted())
return null;
GameRound newRound = new GameRound(oldRound.nextRoundId);
GameRound existingRound = allRounds.putIfAbsent(oldRound.nextRoundId, newRound);
GameRound nextRound = existingRound == null ? newRound : existingRound;
System.out.println("Created round id=" + nextRound.id);
GameRound nextRound = createRoundById(oldRound.nextRoundId);
// If player tries to requeue and next game is already in progress, requeue ahead to the next game
if (isPlayer && nextRound.isStarted())

View File

@ -0,0 +1,65 @@
package org.libertybikes.game.round.service;
import java.util.Random;
import javax.enterprise.context.Dependent;
import javax.inject.Inject;
import javax.json.bind.annotation.JsonbTransient;
import org.libertybikes.game.core.GameRound;
@Dependent
public class Party {
private static final Random r = new Random();
@Inject
@JsonbTransient
GameRoundService roundService;
public final String id;
private volatile GameRound currentRound;
@Inject
public Party() {
this(getRandomPartyID());
}
public Party(String id) {
this.id = id;
}
public GameRound getCurrentRound() {
if (currentRound == null) {
currentRound = roundService.getRound(roundService.createRound());
installCallback(currentRound);
}
log("Current round id=" + currentRound.id);
return this.currentRound;
}
// Get a string of 4 random uppercase letters (A-Z)
private static String getRandomPartyID() {
char[] chars = new char[4];
for (int i = 0; i < 4; i++)
chars[i] = (char) (r.nextInt(26) + 65);
return new String(chars);
}
private void log(String msg) {
System.out.println("[Party-" + id + "] " + msg);
}
// Installs a callback on the GameRound that updates this party's current round
private void installCallback(GameRound round) {
log("Install callback for round id=" + round.id);
round.addCallback(() -> {
log("Updating current round from " + round.id + " -> " + round.nextRoundId);
currentRound = roundService.createRoundById(round.nextRoundId);
this.installCallback(currentRound);
return null;
});
}
}

View File

@ -0,0 +1,57 @@
/**
*
*/
package org.libertybikes.game.round.service;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.spi.CDI;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
@Path("/party")
@ApplicationScoped
@Produces(MediaType.APPLICATION_JSON)
public class PartyService {
// Map of PartyID to current RoundID
private final Map<String, Party> allParties = new ConcurrentHashMap<>();
@GET
public Collection<Party> listParties() {
return allParties.values();
}
@POST
@Path("/create")
public Party createParty() {
Party p = CDI.current().select(Party.class).get();
allParties.put(p.id, p);
return p;
}
@GET
@Path("/{partyId}")
public Party getParty(@PathParam("partyId") String partyId) {
if (partyId == null)
return null;
return allParties.get(partyId.toUpperCase());
}
@GET
@Path("/{partyId}/round")
public String getCurrentRound(@PathParam("partyId") String partyId) {
if (partyId == null)
return null;
Party p = getParty(partyId.toUpperCase());
return p == null ? null : p.getCurrentRound().id;
}
}

View File

@ -31,6 +31,8 @@ public class PlayerService {
String id = createPlayer("SamplePlayer-" + i, null);
for (int j = 0; j < 3; j++)
recordGame(id, r.nextInt(4) + 1);
for (int j = 0; j < 10; j++)
recordGame(id, 4);
}
}