mirror of
https://github.com/OpenLiberty/liberty-bikes.git
synced 2024-11-27 01:20:01 +08:00
Player queues for parties
This commit is contained in:
parent
dd410f6b48
commit
5aacfcf659
@ -1,13 +1,15 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Component, OnInit, OnDestroy, NgZone } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { Meta } from '@angular/platform-browser';
|
||||
import { GameService } from './game.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-game',
|
||||
templateUrl: './game.component.html',
|
||||
styleUrls: ['./game.component.scss']
|
||||
styleUrls: ['./game.component.scss'],
|
||||
providers: [ GameService ],
|
||||
})
|
||||
export class GameComponent implements OnInit {
|
||||
export class GameComponent implements OnInit, OnDestroy {
|
||||
static readonly BOX_SIZE = 5;
|
||||
|
||||
roundId: string;
|
||||
@ -16,7 +18,6 @@ export class GameComponent implements OnInit {
|
||||
|
||||
partyId: string;
|
||||
showPartyId = false;
|
||||
|
||||
showLoader = false;
|
||||
|
||||
output: HTMLElement;
|
||||
@ -24,14 +25,16 @@ export class GameComponent implements OnInit {
|
||||
|
||||
canvas: any;
|
||||
context: CanvasRenderingContext2D;
|
||||
|
||||
constructor(private meta: Meta, private gameService: GameService) {
|
||||
|
||||
constructor(private meta: Meta,
|
||||
private router: Router,
|
||||
private ngZone: NgZone,
|
||||
private gameService: GameService,
|
||||
) {
|
||||
gameService.messages.subscribe((msg) => {
|
||||
const json = msg as any;
|
||||
if (json.requeue) {
|
||||
this.roundId = json.requeue;
|
||||
sessionStorage.setItem('roundId', this.roundId);
|
||||
location.reload();
|
||||
this.processRequeue(json.requeue);
|
||||
}
|
||||
if (json.obstacles) {
|
||||
for (let obstacle of json.obstacles) {
|
||||
@ -66,7 +69,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
|
||||
// Set the Party ID and make visible
|
||||
this.partyId = sessionStorage.getItem('partyId');
|
||||
this.showPartyId = true;
|
||||
this.gameService.send({'spectatorjoined': true});
|
||||
@ -97,6 +100,10 @@ export class GameComponent implements OnInit {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
sessionStorage.removeItem('roundId');
|
||||
}
|
||||
|
||||
// Game actions
|
||||
startGame() {
|
||||
@ -104,7 +111,14 @@ export class GameComponent implements OnInit {
|
||||
}
|
||||
|
||||
requeue() {
|
||||
this.gameService.send({ message: 'GAME_REQUEUE' });
|
||||
if (sessionStorage.getItem('isSpectator') === 'true' ||
|
||||
sessionStorage.getItem('partyId') === null) {
|
||||
this.gameService.send({ message: 'GAME_REQUEUE' });
|
||||
} else {
|
||||
this.ngZone.run(() => {
|
||||
this.router.navigate(['/login']);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
moveUp() {
|
||||
@ -122,6 +136,12 @@ export class GameComponent implements OnInit {
|
||||
moveRight() {
|
||||
this.gameService.send({ direction: 'RIGHT' });
|
||||
}
|
||||
|
||||
processRequeue(newRoundId) {
|
||||
this.roundId = newRoundId;
|
||||
sessionStorage.setItem('roundId', this.roundId);
|
||||
location.reload();
|
||||
}
|
||||
|
||||
// Update display
|
||||
drawPlayer(player) {
|
||||
|
@ -20,14 +20,12 @@ export class LeaderboardComponent implements OnInit {
|
||||
|
||||
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(() => {
|
||||
|
@ -5,20 +5,19 @@ import { PlayersService } from './players.service';
|
||||
@Component({
|
||||
selector: 'app-player-list',
|
||||
templateUrl: './playerlist.component.html',
|
||||
styleUrls: ['./playerlist.component.scss']
|
||||
styleUrls: ['./playerlist.component.scss'],
|
||||
providers: [ PlayersService ],
|
||||
})
|
||||
export class PlayerListComponent implements OnInit {
|
||||
players: Player[] = new Array();
|
||||
|
||||
constructor(private playersService: PlayersService, private ngZone: NgZone) {
|
||||
playersService.messages.subscribe((msg) => {
|
||||
console.log('Updating player list');
|
||||
const json = msg as any;
|
||||
if (json.playerlist) {
|
||||
const newPlayers = new Array();
|
||||
console.log(`Got ${JSON.stringify(json.playerlist)}`);
|
||||
//console.log(`Got playerlist ${JSON.stringify(json.playerlist)}`);
|
||||
for (let player of json.playerlist) {
|
||||
console.log(`Adding player ${player.name}`);
|
||||
newPlayers.push(new Player(player.name, player.status, player.color));
|
||||
}
|
||||
this.ngZone.run(() => {
|
||||
|
@ -11,7 +11,7 @@ export class PlayersService {
|
||||
|
||||
this.messages = <Subject<Object>>socketService.socket
|
||||
.map((response: MessageEvent): any => {
|
||||
console.log(`Players service handling message: ${response.data}`);
|
||||
//console.log(`Players service handling message: ${response.data}`);
|
||||
return JSON.parse(response.data);
|
||||
});
|
||||
}
|
||||
|
@ -64,7 +64,7 @@
|
||||
<input type="text" id="roundid" name="roundid" [(ngModel)]="party" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false">
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<button type="button" (click)="joinRound()">Join Party</button>
|
||||
<button type="button" (click)="joinParty()">Join Party</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@ -73,6 +73,14 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div queuePane>
|
||||
<div class="form-item">
|
||||
<h2>The party is full! Hang out a bit and you will automatically join the next round</h2>
|
||||
<hr/>
|
||||
<h2>You are number {{queuePosition}} in queue</h2>
|
||||
</div>
|
||||
</div>
|
||||
</app-slider>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -116,6 +116,10 @@ button {
|
||||
border-color: #bbb;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
width: 100%;
|
||||
margin-bottom: 15px;
|
||||
|
@ -1,7 +1,9 @@
|
||||
import { Component, OnInit, NgZone, HostBinding } from '@angular/core';
|
||||
import { Component, OnInit, NgZone, HostBinding, Injectable, Output } from '@angular/core';
|
||||
import { Meta } from '@angular/platform-browser';
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import * as EventSource from 'eventsource';
|
||||
import { trigger, animate, style, transition, group, query, stagger, state } from '@angular/animations';
|
||||
import { environment } from './../../environments/environment';
|
||||
import { PaneType } from '../slider/slider.component';
|
||||
@ -16,6 +18,7 @@ export class LoginComponent implements OnInit {
|
||||
pane: PaneType = sessionStorage.getItem('username') === null ? 'left' : 'right';
|
||||
username: string;
|
||||
party: string;
|
||||
@Output() queuePosition: number;
|
||||
player = new Player('PLAYER NAME HERE', 'none', '#FFFFFF');
|
||||
|
||||
constructor(
|
||||
@ -28,7 +31,6 @@ export class LoginComponent implements OnInit {
|
||||
|
||||
|
||||
ngOnInit() {
|
||||
|
||||
this.meta.removeTag('viewport');
|
||||
let viewWidth = window.innerWidth;
|
||||
let viewHeight = window.innerHeight;
|
||||
@ -47,6 +49,13 @@ export class LoginComponent implements OnInit {
|
||||
this.username = sessionStorage.getItem('username');
|
||||
this.player.name = this.username;
|
||||
}
|
||||
|
||||
if (sessionStorage.getItem('partyId') !== null &&
|
||||
sessionStorage.getItem('isSpectator') !== 'true') {
|
||||
this.party = sessionStorage.getItem('partyId');
|
||||
console.log(`User already associated with party ${this.party}, entering queue`);
|
||||
this.enterQueue();
|
||||
}
|
||||
}
|
||||
|
||||
loginGoogle() {
|
||||
@ -60,9 +69,10 @@ export class LoginComponent implements OnInit {
|
||||
this.joinRoundById(roundID);
|
||||
}
|
||||
|
||||
async joinRound() {
|
||||
async joinParty() {
|
||||
let roundID: any = await this.http.get(`${environment.API_URL_PARTY}/${this.party}/round`, { responseType: 'text' }).toPromise();
|
||||
console.log(`Got roundID=${roundID} for partyID=${this.party}`);
|
||||
sessionStorage.setItem('partyId', this.party);
|
||||
this.joinRoundById(roundID);
|
||||
}
|
||||
|
||||
@ -90,16 +100,15 @@ export class LoginComponent implements OnInit {
|
||||
alert('Game round does not exist!');
|
||||
return;
|
||||
}
|
||||
if (data.gameState === 'FULL') {
|
||||
alert('Game round is Full!');
|
||||
return;
|
||||
}
|
||||
if (data.gameState === 'RUNNING') {
|
||||
alert('Game round has already started!');
|
||||
return;
|
||||
}
|
||||
if (data.gameState === 'FINISHED') {
|
||||
alert('Game round has already finished!');
|
||||
|
||||
if (data.gameState === 'FULL' ||
|
||||
data.gameState === 'RUNNING' ||
|
||||
data.gameState === 'FINISHED') {
|
||||
if (this.party === null) {
|
||||
alert('Game has already begun! Try again later.');
|
||||
} else {
|
||||
this.enterQueue();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@ -107,9 +116,7 @@ export class LoginComponent implements OnInit {
|
||||
let response: any = await this.http.post(`${environment.API_URL_PLAYERS}/create?name=${this.username}&id=${id}`, '', {
|
||||
responseType: 'text'
|
||||
}).toPromise();
|
||||
|
||||
console.log(JSON.stringify(response));
|
||||
|
||||
console.log('Created player: ' + JSON.stringify(response));
|
||||
|
||||
// TEMP: to prevent a race condition, putting this code inside of the player create callback to ensure that
|
||||
// userId is set in the session storage before proceeding to the game board
|
||||
@ -157,19 +164,42 @@ export class LoginComponent implements OnInit {
|
||||
showGuestLogin() {
|
||||
this.pane = 'center';
|
||||
}
|
||||
|
||||
enterQueue() {
|
||||
console.log(`enering queue for party ${this.party}`);
|
||||
let queueCallback = new EventSource(`${environment.API_URL_PARTY}/${this.party}/queue`);
|
||||
queueCallback.onmessage = msg => {
|
||||
let queueMsg = JSON.parse(msg.data);
|
||||
if (queueMsg.queuePosition) {
|
||||
this.ngZone.run(() => {
|
||||
this.queuePosition = queueMsg.queuePosition;
|
||||
console.log(`Still waiting in queue at position ${this.queuePosition}`);
|
||||
this.pane = 'queue';
|
||||
});
|
||||
} else if (queueMsg.requeue) {
|
||||
console.log(`ready to join game! Joining round ${queueMsg.requeue}`);
|
||||
queueCallback.close();
|
||||
this.joinRoundById(queueMsg.requeue);
|
||||
} else {
|
||||
console.log('Error: unrecognized message ' + msg.data);
|
||||
}
|
||||
}
|
||||
queueCallback.onerror = msg => {
|
||||
console.log('Error showing queue position: ' + JSON.stringify(msg.data));
|
||||
}
|
||||
}
|
||||
|
||||
loginAsGuest(username: string) {
|
||||
username = username.trim();
|
||||
console.log(`Username input: "${username}"`);
|
||||
|
||||
let usernameError = this.validateUsername(username);
|
||||
if(usernameError !== null) {
|
||||
alert(usernameError);
|
||||
return;
|
||||
alert(usernameError);
|
||||
return;
|
||||
}
|
||||
|
||||
this.player.name = username;
|
||||
this.username = username;
|
||||
this.player.name = username.trim();
|
||||
this.username = username.trim();
|
||||
sessionStorage.setItem('username', username);
|
||||
this.pane = 'right';
|
||||
}
|
||||
@ -198,7 +228,7 @@ export class LoginComponent implements OnInit {
|
||||
}
|
||||
|
||||
validateUsername(username: string) {
|
||||
if (username.length < 1 || username.length > 20) {
|
||||
if (username === undefined || username.trim().length < 1 || username.trim().length > 20) {
|
||||
return 'Username must be between 1 and 20 chars';
|
||||
}
|
||||
let usernameRegex: RegExp = /^[a-zA-Z0-9 -]{1,20}$/;
|
||||
|
@ -2,4 +2,5 @@
|
||||
<div class="left-pane" [@fade]="isActivePane('left')"><ng-content select="[leftPane]"></ng-content></div>
|
||||
<div class="center-pane" [@fade]="isActivePane('center')"><ng-content select="[centerPane]"></ng-content></div>
|
||||
<div class="right-pane" [@fade]="isActivePane('right')"><ng-content select="[rightPane]"></ng-content></div>
|
||||
<div class="queue-pane" [@fade]="isActivePane('queue')"><ng-content select="[queuePane]"></ng-content></div>
|
||||
</div>
|
||||
|
@ -5,7 +5,7 @@
|
||||
|
||||
.parent {
|
||||
height: 100%;
|
||||
width: 300%;
|
||||
width: 400%;
|
||||
|
||||
display: flex;
|
||||
|
||||
|
@ -9,8 +9,9 @@ import { trigger, state, style, transition, animate, query, group } from '@angul
|
||||
animations: [
|
||||
trigger('slide', [
|
||||
state('left', style({ transform: 'translateX(0)' })),
|
||||
state('center', style({ transform: 'translateX(-33.333%)' })),
|
||||
state('right', style({ transform: 'translateX(-66.666%)' })),
|
||||
state('center', style({ transform: 'translateX(-25%)' })),
|
||||
state('right', style({ transform: 'translateX(-50%)' })),
|
||||
state('queue', style({ transform: 'translateX(-75%)'})),
|
||||
transition('void => *', animate(0)),
|
||||
transition('* => *', animate(300))
|
||||
]),
|
||||
@ -29,4 +30,4 @@ export class SliderComponent {
|
||||
}
|
||||
}
|
||||
|
||||
export type PaneType = 'left' | 'center' | 'right';
|
||||
export type PaneType = 'left' | 'center' | 'right' | 'queue';
|
||||
|
@ -15,7 +15,6 @@ 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;
|
||||
@ -100,7 +99,7 @@ public class GameRound implements Runnable {
|
||||
public void addPlayer(Session s, String playerId, String playerName, Boolean hasGameBoard) {
|
||||
// Front end should be preventing a player joining a full game but
|
||||
// defensive programming
|
||||
if (gameState != State.OPEN) {
|
||||
if (!isOpen()) {
|
||||
log("Cannot add player " + playerId + " to game because game has already started.");
|
||||
return;
|
||||
}
|
||||
@ -124,7 +123,7 @@ public class GameRound implements Runnable {
|
||||
}
|
||||
|
||||
public void addAI() {
|
||||
if (gameState != State.OPEN) {
|
||||
if (!isOpen()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -182,7 +181,7 @@ public class GameRound implements Runnable {
|
||||
gameState = State.OPEN;
|
||||
}
|
||||
|
||||
if (gameState == State.OPEN) {
|
||||
if (isOpen()) {
|
||||
board.removePlayer(p);
|
||||
} else if (gameState == State.RUNNING) {
|
||||
checkForWinner();
|
||||
@ -218,21 +217,7 @@ public class GameRound implements Runnable {
|
||||
if (ticksFromGameEnd > DELAY_BETWEEN_ROUNDS)
|
||||
gameRunning.set(false); // end the game if nobody can move anymore
|
||||
}
|
||||
runningGames.decrementAndGet();
|
||||
log("<<< Finished round");
|
||||
broadcastPlayerList();
|
||||
|
||||
long start = System.nanoTime();
|
||||
updatePlayerStats();
|
||||
|
||||
// Wait for 5 seconds, but subtract the amount of time it took to update player stats
|
||||
long nanoWait = TimeUnit.SECONDS.toNanos(5) - (System.nanoTime() - start);
|
||||
delay(TimeUnit.NANOSECONDS.toMillis(nanoWait));
|
||||
log("Clients flagged for auto-requeue will be redirected to the next round now");
|
||||
GameRoundService gameSvc = CDI.current().select(GameRoundService.class).get();
|
||||
for (Client c : clients.values())
|
||||
if (c.autoRequeue)
|
||||
GameRoundWebsocket.requeueClient(gameSvc, this, c.session);
|
||||
endGame();
|
||||
}
|
||||
|
||||
private void updatePlayerStats() {
|
||||
@ -344,11 +329,16 @@ public class GameRound implements Runnable {
|
||||
return gameState != State.OPEN && gameState != State.FULL;
|
||||
}
|
||||
|
||||
@JsonbTransient
|
||||
public boolean isOpen() {
|
||||
return gameState == State.OPEN;
|
||||
}
|
||||
|
||||
public void startGame() {
|
||||
if (isStarted())
|
||||
return;
|
||||
|
||||
while (gameState == State.OPEN) {
|
||||
while (isOpen()) {
|
||||
addAI();
|
||||
}
|
||||
|
||||
@ -358,8 +348,6 @@ 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())
|
||||
@ -377,11 +365,34 @@ public class GameRound implements Runnable {
|
||||
gameState = State.RUNNING;
|
||||
}
|
||||
|
||||
private void endGame() {
|
||||
runningGames.decrementAndGet();
|
||||
log("<<< Finished round");
|
||||
broadcastPlayerList();
|
||||
|
||||
long start = System.nanoTime();
|
||||
updatePlayerStats();
|
||||
lifecycleCallbacks.forEach(c -> c.gameEnding());
|
||||
|
||||
// Wait for 5 seconds, but subtract the amount of time it took to update player stats
|
||||
long nanoWait = TimeUnit.SECONDS.toNanos(5) - (System.nanoTime() - start);
|
||||
delay(TimeUnit.NANOSECONDS.toMillis(nanoWait));
|
||||
log("Clients flagged for auto-requeue will be redirected to the next round now");
|
||||
GameRoundService gameSvc = CDI.current().select(GameRoundService.class).get();
|
||||
for (Client c : clients.values())
|
||||
if (c.autoRequeue)
|
||||
GameRoundWebsocket.requeueClient(gameSvc, this, c.session);
|
||||
}
|
||||
|
||||
private void log(String msg) {
|
||||
System.out.println("[GameRound-" + id + "] " + msg);
|
||||
}
|
||||
|
||||
public interface LifecycleCallback extends Supplier<Void> {}
|
||||
public interface LifecycleCallback {
|
||||
|
||||
public void gameEnding();
|
||||
|
||||
}
|
||||
|
||||
private class HeartbeatTrigger implements Trigger {
|
||||
|
||||
@ -398,8 +409,7 @@ public class GameRound implements Runnable {
|
||||
if (round.clients.size() == 0) {
|
||||
log("No clients remaining. Cancelling heartbeat.");
|
||||
// Ensure that game state is closed off so that no other players can quick join while a round is marked for deletion
|
||||
if (gameState == State.OPEN)
|
||||
gameState = State.FINISHED;
|
||||
gameState = State.FINISHED;
|
||||
return null;
|
||||
}
|
||||
return Date.from(Instant.now().plusSeconds(HEARTBEAT_INTERVAL_SEC));
|
||||
|
@ -1,6 +1,3 @@
|
||||
/**
|
||||
*
|
||||
*/
|
||||
package org.libertybikes.game.core;
|
||||
|
||||
import java.util.Set;
|
||||
@ -54,4 +51,13 @@ public class OutboundMessage {
|
||||
public Heartbeat() {}
|
||||
}
|
||||
|
||||
public static class QueuePosition {
|
||||
@JsonbProperty("queuePosition")
|
||||
public final int queuePosition;
|
||||
|
||||
public QueuePosition(int pos) {
|
||||
queuePosition = pos;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,12 +1,16 @@
|
||||
package org.libertybikes.game.round.service;
|
||||
package org.libertybikes.game.party;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
import javax.enterprise.context.Dependent;
|
||||
import javax.inject.Inject;
|
||||
import javax.json.bind.annotation.JsonbTransient;
|
||||
import javax.ws.rs.sse.Sse;
|
||||
import javax.ws.rs.sse.SseEventSink;
|
||||
|
||||
import org.libertybikes.game.core.GameRound;
|
||||
import org.libertybikes.game.core.GameRound.LifecycleCallback;
|
||||
import org.libertybikes.game.round.service.GameRoundService;
|
||||
|
||||
@Dependent
|
||||
public class Party {
|
||||
@ -20,7 +24,7 @@ public class Party {
|
||||
GameRoundService roundService;
|
||||
|
||||
public final String id;
|
||||
|
||||
private final PartyQueue queue = new PartyQueue(this);
|
||||
private volatile GameRound currentRound;
|
||||
|
||||
@Inject
|
||||
@ -37,10 +41,38 @@ public class Party {
|
||||
currentRound = roundService.getRound(roundService.createRound());
|
||||
installCallback(currentRound);
|
||||
}
|
||||
log("Current round id=" + currentRound.id);
|
||||
return this.currentRound;
|
||||
}
|
||||
|
||||
public void enqueueClient(SseEventSink sink, Sse sse) {
|
||||
queue.add(sink, sse);
|
||||
}
|
||||
|
||||
public void close() {
|
||||
queue.close();
|
||||
}
|
||||
|
||||
public 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);
|
||||
LifecycleCallback callback = new LifecycleCallback() {
|
||||
@Override
|
||||
public void gameEnding() {
|
||||
log("Updating current round from " + round.id + " -> " + round.nextRoundId);
|
||||
currentRound = roundService.createRoundById(round.nextRoundId);
|
||||
Party.this.installCallback(currentRound);
|
||||
|
||||
log("Processing next members in queue...");
|
||||
queue.promoteClients();
|
||||
}
|
||||
};
|
||||
round.addCallback(callback);
|
||||
}
|
||||
|
||||
// Get a string of 4 random letters
|
||||
private static String getRandomPartyID() {
|
||||
char[] chars = new char[4];
|
||||
@ -48,20 +80,4 @@ public class Party {
|
||||
chars[i] = SAFE_CHARS[r.nextInt(SAFE_CHARS.length)];
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,93 @@
|
||||
package org.libertybikes.game.party;
|
||||
|
||||
import java.util.concurrent.ConcurrentLinkedDeque;
|
||||
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.sse.OutboundSseEvent;
|
||||
import javax.ws.rs.sse.Sse;
|
||||
import javax.ws.rs.sse.SseEventSink;
|
||||
|
||||
import org.libertybikes.game.core.GameRound;
|
||||
import org.libertybikes.game.core.OutboundMessage;
|
||||
import org.libertybikes.game.core.Player;
|
||||
|
||||
public class PartyQueue {
|
||||
|
||||
private final ConcurrentLinkedDeque<QueuedClient> waitingPlayers = new ConcurrentLinkedDeque<>();
|
||||
private final Party party;
|
||||
private int queueCounter = 0, firstClient = 0;
|
||||
|
||||
public PartyQueue(Party p) {
|
||||
this.party = p;
|
||||
}
|
||||
|
||||
public void add(SseEventSink sink, Sse sse) {
|
||||
QueuedClient client = new QueuedClient(sink, sse);
|
||||
party.log("Adding client " + client.queueNumber + " into the queue in position " + client.queuePosition());
|
||||
waitingPlayers.add(client);
|
||||
if (party.getCurrentRound().isOpen())
|
||||
promoteClients();
|
||||
else
|
||||
client.notifyPosition();
|
||||
}
|
||||
|
||||
public void promoteClients() {
|
||||
GameRound newRound = party.getCurrentRound();
|
||||
int availableSpots = Player.MAX_PLAYERS - newRound.getPlayers().size();
|
||||
for (int i = 0; i < availableSpots; i++) {
|
||||
QueuedClient first = waitingPlayers.pollFirst();
|
||||
if (first != null) {
|
||||
first.promoteToGame(newRound.id);
|
||||
firstClient++;
|
||||
}
|
||||
}
|
||||
for (QueuedClient client : waitingPlayers)
|
||||
client.notifyPosition();
|
||||
}
|
||||
|
||||
public void close() {
|
||||
party.log("Closing party queue");
|
||||
for (QueuedClient client : waitingPlayers)
|
||||
client.close();
|
||||
}
|
||||
|
||||
private class QueuedClient {
|
||||
private final int queueNumber;
|
||||
private final SseEventSink sink;
|
||||
private final Sse sse;
|
||||
|
||||
public QueuedClient(SseEventSink sink, Sse sse) {
|
||||
this.sink = sink;
|
||||
this.sse = sse;
|
||||
this.queueNumber = PartyQueue.this.queueCounter++;
|
||||
}
|
||||
|
||||
public int queuePosition() {
|
||||
return this.queueNumber - PartyQueue.this.firstClient + 1;
|
||||
}
|
||||
|
||||
public void notifyPosition() {
|
||||
OutboundSseEvent event = sse.newEventBuilder()
|
||||
.mediaType(MediaType.APPLICATION_JSON_TYPE)
|
||||
.data(new OutboundMessage.QueuePosition(queuePosition()))
|
||||
.build();
|
||||
party.log("Notifying queued client " + queueNumber + " who is currently at position " + queuePosition());
|
||||
sink.send(event);
|
||||
}
|
||||
|
||||
public void promoteToGame(String roundId) {
|
||||
OutboundSseEvent event = sse.newEventBuilder()
|
||||
.mediaType(MediaType.APPLICATION_JSON_TYPE)
|
||||
.data(new OutboundMessage.RequeueGame(roundId))
|
||||
.build();
|
||||
party.log("Promoting queued client " + queueNumber + " into round " + roundId);
|
||||
sink.send(event);
|
||||
close();
|
||||
}
|
||||
|
||||
public void close() {
|
||||
sink.close();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
/**
|
||||
*
|
||||
*/
|
||||
package org.libertybikes.game.round.service;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.ws.rs.container.ContainerRequestContext;
|
||||
import javax.ws.rs.container.ContainerResponseContext;
|
||||
import javax.ws.rs.container.ContainerResponseFilter;
|
||||
import javax.ws.rs.ext.Provider;
|
||||
|
||||
@Provider
|
||||
public class CORSFilter implements ContainerResponseFilter {
|
||||
@Override
|
||||
public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) throws IOException {
|
||||
responseContext.getHeaders().add("Access-Control-Allow-Origin", "*");
|
||||
responseContext.getHeaders().add("Access-Control-Allow-Headers", "origin, content-type, accept, authorization");
|
||||
responseContext.getHeaders().add("Access-Control-Allow-Credentials", "true");
|
||||
responseContext.getHeaders().add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, HEAD");
|
||||
responseContext.getHeaders().add("Access-Control-Max-Age", "1209600");
|
||||
}
|
||||
}
|
@ -70,7 +70,7 @@ public class GameRoundService {
|
||||
public String getAvailableRound() {
|
||||
Optional<GameRound> availableRound = allRounds.values()
|
||||
.stream()
|
||||
.filter(r -> r.gameState == GameRound.State.OPEN)
|
||||
.filter(r -> r.isOpen())
|
||||
.findFirst();
|
||||
if (availableRound.isPresent())
|
||||
return availableRound.get().id;
|
||||
@ -104,7 +104,7 @@ public class GameRoundService {
|
||||
|
||||
public void deleteRound(GameRound round) {
|
||||
String roundId = round.id;
|
||||
if (round.gameState == State.OPEN)
|
||||
if (round.isOpen())
|
||||
round.gameState = State.FINISHED;
|
||||
System.out.println("Scheduling round id=" + roundId + " for deletion in 5 minutes");
|
||||
// Do not immediately delete rounds in order to give players/spectators time to move along to the next game
|
||||
|
@ -4,6 +4,6 @@ import javax.ws.rs.ApplicationPath;
|
||||
import javax.ws.rs.core.Application;
|
||||
|
||||
@ApplicationPath("/")
|
||||
public class GameRoundApp extends Application {
|
||||
public class GameServiceApp extends Application {
|
||||
|
||||
}
|
@ -4,9 +4,13 @@
|
||||
package org.libertybikes.game.round.service;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.enterprise.concurrent.ManagedScheduledExecutorService;
|
||||
import javax.enterprise.context.ApplicationScoped;
|
||||
import javax.enterprise.inject.spi.CDI;
|
||||
import javax.ws.rs.GET;
|
||||
@ -14,44 +18,70 @@ 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.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.sse.Sse;
|
||||
import javax.ws.rs.sse.SseEventSink;
|
||||
|
||||
import org.libertybikes.game.party.Party;
|
||||
|
||||
@Path("/party")
|
||||
@ApplicationScoped
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public class PartyService {
|
||||
|
||||
// Map of PartyID to current RoundID
|
||||
private final Map<String, Party> allParties = new ConcurrentHashMap<>();
|
||||
|
||||
@Resource
|
||||
private ManagedScheduledExecutorService exec;
|
||||
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Collection<Party> listParties() {
|
||||
return allParties.values();
|
||||
return Collections.unmodifiableCollection(allParties.values());
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/create")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Party createParty() {
|
||||
Party p = CDI.current().select(Party.class).get();
|
||||
allParties.put(p.id, p);
|
||||
// Put a max lifetime of 12 hours on a party
|
||||
exec.schedule(() -> this.deleteParty(p.id), 12, TimeUnit.HOURS);
|
||||
return p;
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/{partyId}")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Party getParty(@PathParam("partyId") String partyId) {
|
||||
if (partyId == null)
|
||||
if (partyId == null) {
|
||||
System.out.println("WARN: got null partyId request");
|
||||
return null;
|
||||
}
|
||||
return allParties.get(partyId.toUpperCase());
|
||||
}
|
||||
|
||||
public void deleteParty(String partyId) {
|
||||
if (allParties.remove(partyId) != null)
|
||||
System.out.println("Deleted party " + partyId);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/{partyId}/round")
|
||||
public String getCurrentRound(@PathParam("partyId") String partyId) {
|
||||
if (partyId == null)
|
||||
return null;
|
||||
Party p = getParty(partyId.toUpperCase());
|
||||
Party p = getParty(partyId);
|
||||
return p == null ? null : p.getCurrentRound().id;
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/{partyId}/queue")
|
||||
@Produces(MediaType.SERVER_SENT_EVENTS)
|
||||
public void joinQueue(@PathParam("partyId") String partyId, @Context SseEventSink sink, @Context Sse sse) {
|
||||
Party p = getParty(partyId);
|
||||
if (p != null)
|
||||
p.enqueueClient(sink, sse);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -6,6 +6,7 @@
|
||||
<feature>jaxrs-2.1</feature>
|
||||
<feature>jsonb-1.0</feature>
|
||||
<feature>concurrent-1.0</feature>
|
||||
<feature>apiDiscovery-1.0</feature>
|
||||
</featureManager>
|
||||
|
||||
<httpEndpoint id="defaultHttpEndpoint" host="*" httpPort="${httpPort}" httpsPort="${httpsPort}" />
|
||||
@ -27,10 +28,10 @@
|
||||
|
||||
<!-- This configuration allows cross-origin HTTP requests, such
|
||||
as those from the front-end component (different port). -->
|
||||
<cors domain="/round"
|
||||
<cors domain="/"
|
||||
allowedOrigins="*"
|
||||
allowedMethods="GET, DELETE, POST, PUT"
|
||||
allowedHeaders="Accept, Content-Type, Authorization"
|
||||
allowedHeaders="origin, content-type, accept, authorization, cache-control"
|
||||
maxAge="3600" />
|
||||
|
||||
<logging traceSpecification="*=info:com.ibm.ws.security.*=all:Authentication=all:com.ibm.ws.container.service.security.internal.*=all" />
|
||||
|
Loading…
Reference in New Issue
Block a user