Handle requeues with REST instead of websockets

This commit is contained in:
Andrew Guibert 2018-05-14 22:55:39 -05:00
parent c08ec14c33
commit 3a1057dd88
8 changed files with 136 additions and 85 deletions

View File

@ -1,4 +1,5 @@
import { Component, NgZone, OnInit, OnDestroy } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Router } from '@angular/router';
import { GameService } from '../game/game.service';
import { Triangle } from '../geom/triangle';
@ -46,18 +47,16 @@ export class ControlsComponent implements OnInit, OnDestroy {
constructor(private router: Router,
private ngZone: NgZone,
private http: HttpClient,
private gameService: GameService) {
gameService.messages.subscribe((msg) => {
const json = msg as any;
console.log(`received: ${JSON.stringify(json)}`);
if (json.requeue) {
this.processRequeue(json.requeue);
}
if (json.keepAlive) {
this.gameService.send({ keepAlive: true });
}
if (json.gameStatus === 'FINISHED') {
if (confirm('Game is over, would you like to requeue?')) {
if (confirm('Game is over, requeue to next round?')) {
this.requeue();
} else {
this.ngZone.run(() => {
@ -147,6 +146,7 @@ export class ControlsComponent implements OnInit, OnDestroy {
}
processRequeue(newRoundId) {
console.log(`Requeueing to round ${newRoundId}`);
this.roundId = newRoundId;
sessionStorage.setItem('roundId', this.roundId);
location.reload();
@ -263,6 +263,7 @@ export class ControlsComponent implements OnInit, OnDestroy {
touchEnded(evt: TouchEvent) {
this.canvasReleased(evt.changedTouches[0].pageX, evt.changedTouches[0].pageY);
this.verifyOpen();
}
mouseDown(evt: MouseEvent) {
@ -271,6 +272,7 @@ export class ControlsComponent implements OnInit, OnDestroy {
mouseUp(evt: MouseEvent) {
this.canvasReleased(evt.pageX, evt.pageY);
this.verifyOpen();
}
canvasPressed(x: number, y: number) {
@ -333,17 +335,16 @@ export class ControlsComponent implements OnInit, OnDestroy {
}
window.requestAnimationFrame(() => this.draw());
this.verifyOpen();
}
// Game actions
startGame() {
this.gameService.send({ message: 'GAME_START' });
}
requeue() {
async requeue() {
let partyId: string = sessionStorage.getItem('partyId');
if (partyId === null) {
this.gameService.send({ message: 'GAME_REQUEUE' });
let roundId: string = sessionStorage.getItem('roundId');
let nextRoundID: any = await this.http.get(`${environment.API_URL_GAME_ROUND}/${roundId}/requeue?isPlayer=true`, { responseType: 'text' }).toPromise();
this.processRequeue(nextRoundID);
} else {
let queueCallback = new EventSource(`${environment.API_URL_PARTY}/${partyId}/queue`);
queueCallback.onmessage = msg => {
@ -375,11 +376,21 @@ export class ControlsComponent implements OnInit, OnDestroy {
this.gameService.send({ direction: `${newDir}` });
}
}
verifyOpen() {
if (!this.gameService.isOpen()) {
console.log('GameService socket not open');
this.ngZone.run(() => {
this.router.navigate(['/login']);
});
}
}
ngOnDestroy() {
window.removeEventListener('touchmove', this.preventScrolling);
window.removeEventListener('orientationchange', this.pageWasResized);
window.removeEventListener('resize', this.pageWasResized);
sessionStorage.removeItem('roundId');
this.gameService.close();
}
}

View File

@ -1,5 +1,6 @@
import { Component, OnInit, OnDestroy, NgZone } from '@angular/core';
import { Router } from '@angular/router';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Meta } from '@angular/platform-browser';
import { GameService } from './game.service';
import { LoginComponent } from '../login/login.component';
@ -46,13 +47,22 @@ export class GameComponent implements OnInit, OnDestroy {
constructor(private meta: Meta,
private router: Router,
private ngZone: NgZone,
private http: HttpClient,
private gameService: GameService,
) {
this.ngZone.runOutsideAngular(() => {
gameService.messages.subscribe((msg) => {
const json = msg as any;
if (json.requeue) {
this.processRequeue(json.requeue);
if (json.countdown) {
this.ngZone.run(() => this.startingCountdown(json.countdown));
}
if (json.keepAlive) {
this.gameService.send({ keepAlive: true });
}
if (json.gameStatus === 'FINISHED') {
if (sessionStorage.getItem('isSpectator') === 'true') {
this.requeue();
}
}
if (json.obstacles) {
for (let obstacle of json.obstacles) {
@ -160,13 +170,6 @@ export class GameComponent implements OnInit, OnDestroy {
}
}
if (json.countdown) {
this.ngZone.run(() => this.startingCountdown(json.countdown));
}
if (json.keepAlive) {
this.gameService.send({ keepAlive: true });
}
this.stage.update();
}, (err) => {
console.log(`Error occurred: ${err}`);
@ -233,17 +236,30 @@ export class GameComponent implements OnInit, OnDestroy {
ngOnDestroy() {
sessionStorage.removeItem('roundId');
this.gameService.close();
}
// Game actions
startGame() {
this.verifyOpen();
this.gameService.send({ message: 'GAME_START' });
}
requeue() {
async requeue() {
let partyId = sessionStorage.getItem('partyId');
if (sessionStorage.getItem('isSpectator') === 'true' || partyId === null) {
this.gameService.send({ message: 'GAME_REQUEUE' });
let isSpectator: boolean = sessionStorage.getItem('isSpectator') === 'true' ? true : false;
if (isSpectator || partyId === null) {
let roundId: string = sessionStorage.getItem('roundId');
let nextRoundID: any = await this.http.get(`${environment.API_URL_GAME_ROUND}/${roundId}/requeue?isPlayer=${!isSpectator}`, { responseType: 'text' }).toPromise();
// if a spectator, wait 5s before moving to next round to let people look at the final state of the board a bit
if (isSpectator) {
console.log(`Will requeue to round ${nextRoundID} in 5 seconds.`);
setTimeout(() => {
this.processRequeue(nextRoundID);
}, 5000);
} else {
this.processRequeue(nextRoundID);
}
} else {
let queueCallback = new EventSource(`${environment.API_URL_PARTY}/${partyId}/queue`);
queueCallback.onmessage = msg => {
@ -270,22 +286,27 @@ export class GameComponent implements OnInit, OnDestroy {
}
moveUp() {
this.verifyOpen()
this.gameService.send({ direction: 'UP' });
}
moveDown() {
this.verifyOpen();
this.gameService.send({ direction: 'DOWN' });
}
moveLeft() {
this.verifyOpen();
this.gameService.send({ direction: 'LEFT' });
}
moveRight() {
this.verifyOpen();
this.gameService.send({ direction: 'RIGHT' });
}
processRequeue(newRoundId) {
console.log(`Requeueing to round ${newRoundId}`);
this.roundId = newRoundId;
sessionStorage.setItem('roundId', this.roundId);
location.reload();
@ -297,5 +318,14 @@ export class GameComponent implements OnInit, OnDestroy {
this.showLoader = false;
}, (1000 * seconds));
}
verifyOpen() {
if (!this.gameService.isOpen()) {
console.log('GameService socket not open');
this.ngZone.run(() => {
this.router.navigate(['/login']);
});
}
}
}

View File

@ -12,7 +12,7 @@ export class GameService {
roundId: string;
constructor(socketService: SocketService) {
constructor(private socketService: SocketService) {
this.roundId = sessionStorage.getItem('roundId');
console.log(`Round ID: ${this.roundId}`);
@ -27,4 +27,12 @@ export class GameService {
public send(message: any) {
this.messages.next(JSON.stringify(message));
}
public isOpen() {
return this.socketService.socketOpen;
}
public close() {
this.socketService.close();
}
}

View File

@ -7,6 +7,7 @@ import 'rxjs/add/operator/share';
@Injectable()
export class SocketService {
private ws;
private socketUrl: string;
private subject: Subject<MessageEvent>;
private hasUrl = false;
@ -19,10 +20,11 @@ export class SocketService {
set url(newUrl: string) {
console.log(`Setting url to ${newUrl}`);
if (this.socketOpen) {
this.close();
}
if (this.socketUrl !== newUrl) {
console.log(`Updating socket with new URL`);
this.open = false;
this.hasUrl = false;
this.subject = this.create(newUrl);
this.socketUrl = newUrl;
@ -49,34 +51,44 @@ export class SocketService {
}
return this.subject;
}
public close() {
this.open = false;
this.ws.close();
}
private create(url): Subject<MessageEvent> {
console.log(`Creating new socket for ${url}`);
const ws = new WebSocket(url);
this.ws = new WebSocket(url);
ws.onopen = () => {
this.ws.onopen = () => {
console.log('Socket open, sending buffered messages');
this.open = true;
while (this.messageBuffer.length > 0) {
this.subject.next(this.messageBuffer.shift() as any);
}
};
this.ws.onclose = () => {
console.log('Socket closed');
this.open = false;
}
const observable = Observable.create(
(obs: Observer<MessageEvent>) => {
ws.onmessage = obs.next.bind(obs);
ws.onerror = obs.error.bind(obs);
ws.onclose = obs.complete.bind(obs);
this.ws.onmessage = obs.next.bind(obs);
this.ws.onerror = obs.error.bind(obs);
this.ws.onclose = obs.complete.bind(obs);
return ws.close.bind(ws);
return this.ws.close.bind(this.ws);
}
);
const observer = {
next: (data: string) => {
if (ws.readyState === ws.OPEN) {
if (this.ws.readyState === this.ws.OPEN) {
console.log(`sending text: ${data}`);
ws.send(data);
this.ws.send(data);
} else {
console.log('socket not open, buffering message');
this.open = false;

View File

@ -1,6 +1,6 @@
package org.libertybikes.game.core;
import static org.libertybikes.game.round.service.GameRoundWebsocket.sendTextToClient;
import static org.libertybikes.game.round.service.GameRoundWebsocket.sendToClient;
import static org.libertybikes.game.round.service.GameRoundWebsocket.sendToClients;
import java.time.Instant;
@ -21,8 +21,6 @@ import javax.enterprise.concurrent.LastExecution;
import javax.enterprise.concurrent.ManagedScheduledExecutorService;
import javax.enterprise.concurrent.Trigger;
import javax.enterprise.inject.spi.CDI;
import javax.json.bind.Jsonb;
import javax.json.bind.JsonbBuilder;
import javax.json.bind.annotation.JsonbPropertyOrder;
import javax.json.bind.annotation.JsonbTransient;
import javax.naming.InitialContext;
@ -31,8 +29,6 @@ import javax.websocket.Session;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.libertybikes.game.core.Player.STATUS;
import org.libertybikes.game.round.service.GameRoundService;
import org.libertybikes.game.round.service.GameRoundWebsocket;
import org.libertybikes.restclient.PlayerService;
@JsonbPropertyOrder({ "id", "gameState", "board", "nextRoundId" })
@ -46,11 +42,10 @@ public class GameRound implements Runnable {
FINISHED // game has ended and a winner has been declared
}
private static final Jsonb jsonb = JsonbBuilder.create();
private static final int GAME_TICK_SPEED_DEFAULT = 50; // ms
private static final int DELAY_BETWEEN_ROUNDS = 5; //ticks
private static final int STARTING_COUNTDOWN = 3; // seconds
private static final int MAX_TIME_BETWEEN_ROUNDS = Integer.getInteger("game-service.time.between.rounds", 20); // 20 seconds default
private static final int MAX_TIME_BETWEEN_ROUNDS_DEFAULT = 20; // seconds
private static final int FULL_GAME_TIME_BETWEEN_ROUNDS = 5; //seconds
private static final Random r = new Random();
private static final AtomicInteger runningGames = new AtomicInteger();
@ -67,7 +62,7 @@ public class GameRound implements Runnable {
private final Map<Session, Client> clients = new HashMap<>();
private final Deque<Player> playerRanks = new ArrayDeque<>();
private final Set<LifecycleCallback> lifecycleCallbacks = new HashSet<>();
private final int GAME_TICK_SPEED;
private final int GAME_TICK_SPEED, MAX_TIME_BETWEEN_ROUNDS;
private LobbyCountdown lobbyCountdown;
private AtomicBoolean lobbyCountdownStarted = new AtomicBoolean();
@ -89,6 +84,7 @@ public class GameRound implements Runnable {
this.id = id;
nextRoundId = getRandomId();
// Get game tick speed
Integer tickSpeed = GAME_TICK_SPEED_DEFAULT;
try {
tickSpeed = InitialContext.doLookup("round/gameSpeed");
@ -96,6 +92,15 @@ public class GameRound implements Runnable {
log("Unable to perform JNDI lookup to determine game tick speed, using default value");
}
GAME_TICK_SPEED = (tickSpeed < 20 || tickSpeed > 100) ? GAME_TICK_SPEED_DEFAULT : tickSpeed;
// Get delay between rounds
Integer maxTimeBetweenRounds = MAX_TIME_BETWEEN_ROUNDS_DEFAULT;
try {
maxTimeBetweenRounds = InitialContext.doLookup("round/autoStartCooldown");
} catch (Exception e) {
log("Unable to perform JNDI lookup to determine time between rounds, using default value");
}
MAX_TIME_BETWEEN_ROUNDS = (maxTimeBetweenRounds < 5 || maxTimeBetweenRounds > 60) ? MAX_TIME_BETWEEN_ROUNDS_DEFAULT : maxTimeBetweenRounds;
}
public GameBoard getBoard() {
@ -115,7 +120,7 @@ public class GameRound implements Runnable {
}
}
if (!isPhone)
sendTextToClient(s, new OutboundMessage.AwaitPlayersCountdown(lobbyCountdown.roundStartCountdown));
sendToClient(s, new OutboundMessage.AwaitPlayersCountdown(lobbyCountdown.roundStartCountdown));
}
public void updatePlayerDirection(Session playerSession, InboundMessage msg) {
@ -170,8 +175,8 @@ public class GameRound implements Runnable {
public void addSpectator(Session s) {
log("A spectator has joined.");
clients.put(s, new Client(s));
sendTextToClient(s, new OutboundMessage.PlayerList(getPlayers()));
sendTextToClient(s, board);
sendToClient(s, new OutboundMessage.PlayerList(getPlayers()));
sendToClient(s, board);
beginHeartbeat();
beginLobbyCountdown(s, false);
}
@ -258,10 +263,6 @@ public class GameRound implements Runnable {
if (gameState != State.FINISHED)
throw new IllegalStateException("Canot update player stats while game is still running.");
Set<Player> players = getPlayers();
if (players.size() < 2)
return; // Don't update player stats for single-player games
PlayerService playerSvc = CDI.current().select(PlayerService.class, RestClient.LITERAL).get();
int rank = 1;
for (Player p : playerRanks) {
@ -406,7 +407,6 @@ public class GameRound implements Runnable {
log("<<< Finished round");
broadcastPlayerList();
long start = System.nanoTime();
try {
ManagedScheduledExecutorService exec = InitialContext.doLookup("java:comp/DefaultManagedScheduledExecutorService");
exec.submit(() -> {
@ -420,16 +420,11 @@ public class GameRound implements Runnable {
e.printStackTrace();
}
// 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);
else
sendTextToClient(c.session, new OutboundMessage.GameStatus(State.FINISHED));
// Tell each client that the game is done and close the websockets
for (Session s : clients.keySet())
sendToClient(s, new OutboundMessage.GameStatus(State.FINISHED));
for (Session s : clients.keySet())
removeClient(s);
}
private void log(String msg) {

View File

@ -85,21 +85,25 @@ public class GameRoundService {
return allRounds.get(roundId);
}
public GameRound requeue(GameRound oldRound, boolean isPlayer) {
@GET
@Path("/{roundId}/requeue")
public String requeue(@PathParam("roundId") String oldRoundId, @QueryParam("isPlayer") boolean isPlayer) {
GameRound oldRound = getRound(oldRoundId);
// Do not allow anyone to skip ahead past a round that has not started yet
if (!oldRound.isStarted())
if (oldRound == null || !oldRound.isStarted())
return null;
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())
return requeue(nextRound, isPlayer);
return requeue(nextRound.id, isPlayer);
// If next round is already done, requeue ahead to next game
else if (nextRound.gameState == GameRound.State.FINISHED)
return requeue(nextRound, isPlayer);
return requeue(nextRound.id, isPlayer);
else
return nextRound;
return nextRound.id;
}
public void deleteRound(GameRound round) {

View File

@ -19,9 +19,9 @@ import javax.websocket.server.ServerEndpoint;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.libertybikes.game.core.GameRound;
import org.libertybikes.game.core.GameRound.State;
import org.libertybikes.game.core.InboundMessage;
import org.libertybikes.game.core.InboundMessage.GameEvent;
import org.libertybikes.game.core.OutboundMessage;
import org.libertybikes.restclient.PlayerService;
@Dependent
@ -63,27 +63,24 @@ public class GameRoundWebsocket {
try {
final InboundMessage msg = jsonb.fromJson(message, InboundMessage.class);
final GameRound round = gameSvc.getRound(roundId);
if (round == null) {
log(roundId, "[onMessage] unable to locate roundId=" + roundId);
if (round == null || round.gameState == State.FINISHED) {
log(roundId, "[onMessage] Received message for round that did not exist or has completed. Closing this websocket connection.");
session.close();
return;
}
// System.out.println("[onMessage] roundId=" + roundId + " msg=" + message);
if (GameEvent.GAME_REQUEUE == msg.event) {
requeueClient(gameSvc, round, session);
}
if (GameEvent.GAME_START == msg.event) {
round.startGame();
}
if (msg.direction != null) {
} else if (msg.direction != null) {
round.updatePlayerDirection(session, msg);
}
if (msg.playerJoinedId != null) {
} else if (msg.playerJoinedId != null) {
org.libertybikes.restclient.Player playerResponse = playerSvc.getPlayerById(msg.playerJoinedId);
round.addPlayer(session, msg.playerJoinedId, playerResponse.name, msg.hasGameBoard);
}
if (Boolean.TRUE == msg.isSpectator) {
} else if (Boolean.TRUE == msg.isSpectator) {
round.addSpectator(session);
} else {
log(roundId, "ERR: Unrecognized message: " + jsonb.toJson(msg));
}
} catch (Exception e) {
log(roundId, "ERR: " + e.getMessage());
@ -91,14 +88,7 @@ public class GameRoundWebsocket {
}
}
public static void requeueClient(GameRoundService gameSvc, GameRound oldRound, Session s) {
GameRound nextGame = gameSvc.requeue(oldRound, oldRound.isPlayer(s));
if (nextGame == null)
return;
sendTextToClient(s, new OutboundMessage.RequeueGame(nextGame.id));
}
public static void sendTextToClient(Session client, Object message) {
public static void sendToClient(Session client, Object message) {
if (client != null) {
String msg = message instanceof String ? (String) message : jsonb.toJson(message);
try {
@ -113,7 +103,7 @@ public class GameRoundWebsocket {
String msg = message instanceof String ? (String) message : jsonb.toJson(message);
// System.out.println("Sending " + clients.size() + " clients the message: " + message);
for (Session client : clients)
sendTextToClient(client, msg);
sendToClient(client, msg);
}
private static void log(String roundId, String msg) {

View File

@ -17,6 +17,7 @@
<!-- Dynamically configurable settings -->
<jndiEntry jndiName="round/gameSpeed" value="50"/> <!-- Default = 50(ms) -->
<jndiEntry jndiName="round/map" value="-1"/> <!-- Default = -1 (random map) -->
<jndiEntry jndiName="round/autoStartCooldown" value="20"/> <!-- Default = 20(sec) -->
<applicationManager autoExpand="true"/>