diff --git a/frontend/prebuild/src/app/game/game.component.ts b/frontend/prebuild/src/app/game/game.component.ts index 001a57e..f17eebe 100644 --- a/frontend/prebuild/src/app/game/game.component.ts +++ b/frontend/prebuild/src/app/game/game.component.ts @@ -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'); diff --git a/frontend/prebuild/src/app/login/login.component.html b/frontend/prebuild/src/app/login/login.component.html index f5bf640..a3178aa 100644 --- a/frontend/prebuild/src/app/login/login.component.html +++ b/frontend/prebuild/src/app/login/login.component.html @@ -12,16 +12,16 @@
-
+
- +
- +
diff --git a/frontend/prebuild/src/app/login/login.component.scss b/frontend/prebuild/src/app/login/login.component.scss index 26f77bb..9773c05 100644 --- a/frontend/prebuild/src/app/login/login.component.scss +++ b/frontend/prebuild/src/app/login/login.component.scss @@ -81,7 +81,7 @@ outline: none; } -#roundid { +#partyid { text-transform: uppercase; } diff --git a/frontend/prebuild/src/app/login/login.component.ts b/frontend/prebuild/src/app/login/login.component.ts index e58d32f..02f6776 100644 --- a/frontend/prebuild/src/app/login/login.component.ts +++ b/frontend/prebuild/src/app/login/login.component.ts @@ -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']); }); diff --git a/frontend/prebuild/src/environments/environment.ts b/frontend/prebuild/src/environments/environment.ts index 0cd3bf4..d0a45e7 100644 --- a/frontend/prebuild/src/environments/environment.ts +++ b/frontend/prebuild/src/environments/environment.ts @@ -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`, diff --git a/game-service/src/main/java/org/libertybikes/game/core/GameBoard.java b/game-service/src/main/java/org/libertybikes/game/core/GameBoard.java index d419c3e..bef6b1d 100644 --- a/game-service/src/main/java/org/libertybikes/game/core/GameBoard.java +++ b/game-service/src/main/java/org/libertybikes/game/core/GameBoard.java @@ -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; } } diff --git a/game-service/src/main/java/org/libertybikes/game/core/GameRound.java b/game-service/src/main/java/org/libertybikes/game/core/GameRound.java index 87422f0..f43e2cd 100644 --- a/game-service/src/main/java/org/libertybikes/game/core/GameRound.java +++ b/game-service/src/main/java/org/libertybikes/game/core/GameRound.java @@ -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 clients = new HashMap<>(); private final Deque playerRanks = new ArrayDeque<>(); + private final Set 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 {} + private class HeartbeatTrigger implements Trigger { private static final int HEARTBEAT_INTERVAL_SEC = 100; diff --git a/game-service/src/main/java/org/libertybikes/game/round/service/GameRoundApp.java b/game-service/src/main/java/org/libertybikes/game/round/service/GameRoundApp.java index 9cf35fe..55723e1 100644 --- a/game-service/src/main/java/org/libertybikes/game/round/service/GameRoundApp.java +++ b/game-service/src/main/java/org/libertybikes/game/round/service/GameRoundApp.java @@ -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 { } diff --git a/game-service/src/main/java/org/libertybikes/game/round/service/GameRoundService.java b/game-service/src/main/java/org/libertybikes/game/round/service/GameRoundService.java index 549631c..bf2f64f 100644 --- a/game-service/src/main/java/org/libertybikes/game/round/service/GameRoundService.java +++ b/game-service/src/main/java/org/libertybikes/game/round/service/GameRoundService.java @@ -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 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()) diff --git a/game-service/src/main/java/org/libertybikes/game/round/service/Party.java b/game-service/src/main/java/org/libertybikes/game/round/service/Party.java new file mode 100644 index 0000000..1530592 --- /dev/null +++ b/game-service/src/main/java/org/libertybikes/game/round/service/Party.java @@ -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; + }); + } + +} diff --git a/game-service/src/main/java/org/libertybikes/game/round/service/PartyService.java b/game-service/src/main/java/org/libertybikes/game/round/service/PartyService.java new file mode 100644 index 0000000..8908412 --- /dev/null +++ b/game-service/src/main/java/org/libertybikes/game/round/service/PartyService.java @@ -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 allParties = new ConcurrentHashMap<>(); + + @GET + public Collection 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; + } + +}