Implement game hosting as a non-player

This commit is contained in:
Andrew Guibert 2018-02-09 22:48:35 -06:00
parent 9ef789acba
commit 4f7a0846e8
14 changed files with 220 additions and 142 deletions

View File

@ -10,10 +10,15 @@
<br>
<br>
</div>
<ul id="playerList" class='list-group col-md-3 d-inline-block align-top'>
<div class="col-md-3 d-inline-block align-top">
<button id="gameIdDisplay" class='d-none btn-primary btn-lg' disabled>
</button>
<br>
<br>
<ul id="playerList" class='list-group'>
</ul>
</div>
</div>
<div class="panel-footer">
<button type="button" class="btn btn-success" (click)="startGame()">Start Game</button>
<button type="button" class="btn btn-danger" (click)="pauseGame()">Pause Game</button>

View File

@ -1,3 +1,4 @@
import * as $ from 'jquery';
import { Whiteboard } from './whiteboard';
export class GameWebsocket {
@ -9,7 +10,7 @@ export class GameWebsocket {
output: HTMLElement;
constructor(whiteboard: Whiteboard) {
this.roundId = localStorage.getItem('roundId');
this.roundId = sessionStorage.getItem('roundId');
this.baseUri = `ws://${document.location.hostname}:8080/round/ws`;
this.wsUri = `${this.baseUri}/${this.roundId}`;
this.websocket = new WebSocket(this.wsUri);
@ -43,7 +44,7 @@ export class GameWebsocket {
}
if (json.requeue) {
this.roundId = json.requeue;
localStorage.setItem('roundId', this.roundId);
sessionStorage.setItem('roundId', this.roundId);
location.reload();
}
if (json.playerlocs) {
@ -59,9 +60,20 @@ export class GameWebsocket {
}
onConnect(evt: MessageEvent) {
const name = localStorage.getItem('username');
const name = sessionStorage.getItem('username');
const isSpectator = sessionStorage.getItem('isSpectator');
if (isSpectator === 'true') {
console.log('is a spectator... showing game id');
// Set the Round ID and make visible
const gameId = $('#gameIdDisplay');
gameId.html('Round ID: ' + this.roundId);
gameId.removeClass('d-none');
gameId.addClass('d-inline-block');
this.sendText(JSON.stringify({'spectatorjoined': true}));
} else {
this.sendText(JSON.stringify({'playerjoined': name}));
}
}
writeToScreen(message: string) {
const pre = document.createElement('p');

View File

@ -27,14 +27,6 @@ export class Whiteboard {
};
}
getCurrentPos(evt) {
const rect = this.canvas.getBoundingClientRect();
return {
x: evt.clientX - rect.left,
y: evt.clientY - rect.top
};
}
drawSquare(data) {
const json = JSON.parse(data);
this.context.fillStyle = json.color;

View File

@ -23,8 +23,11 @@
</div>
<div class="row">
<div class="col-md-2 offset-md-1">
<input type="submit" class="btn btn-success" value="Login" (click)="joinRound()">
<input type="submit" class="btn btn-success" value="Create Round" (click)="createRound()">
<button type="button" class="btn btn-success" (click)="joinRound()">Login</button>
<button type="button" class="btn btn-success" (click)="createRound()">Create Round</button>
<button id="hostButton" type="button" class="btn btn-info" (click)="hostRound()" data-loading-text="Connecting..." autocomplete="off">
Host A Round
</button>
</div>
</div>
</div>

View File

@ -17,13 +17,25 @@ export class LoginComponent implements OnInit {
createRound() {
$.post(`http://${document.location.hostname}:8080/round/create`, function(data) {
alert('Response is: ' + data);
alert('Created round: ' + data);
});
}
joinRound() {
localStorage.setItem('username', $('#username').val());
localStorage.setItem('roundId', $('#roundid').val());
sessionStorage.setItem('username', $('#username').val());
sessionStorage.setItem('roundId', $('#roundid').val());
this.router.navigate(['/game']);
}
hostRound() {
// TODO: update the button on click to indicate a waiting state in case
// this post request takes a noticeable amount of time
let router = this.router;
$.post(`http://${document.location.hostname}:8080/round/create`, function(data) {
console.log(`Created round with id=` + data);
sessionStorage.setItem('isSpectator', 'true');
sessionStorage.setItem('roundId', data);
router.navigate(['/game']);
});
}
}

View File

@ -70,7 +70,6 @@
"no-string-literal": false,
"no-string-throw": true,
"no-switch-case-fall-through": true,
"no-trailing-whitespace": true,
"no-unnecessary-initializer": true,
"no-unused-expression": true,
"no-use-before-declare": true,

View File

@ -0,0 +1,32 @@
/**
*
*/
package org.libertybikes.game.core;
import javax.websocket.Session;
public class Client {
public final Session session;
public final Player player;
/**
* Create a client which is only a spectator of a game
*/
public Client(Session s) {
this(s, null);
}
/**
* Create a player who will be participating in the game
*/
public Client(Session s, Player p) {
session = s;
player = p;
}
public boolean isPlayer() {
return player != null;
}
}

View File

@ -3,18 +3,10 @@
*/
package org.libertybikes.game.core;
import javax.json.bind.Jsonb;
import javax.json.bind.JsonbBuilder;
import javax.json.bind.annotation.JsonbProperty;
/**
* @author Andrew
*
*/
public class ClientMessage {
private static final Jsonb jsonb = JsonbBuilder.create();
public static enum GameEvent {
GAME_START,
GAME_PAUSE,
@ -29,9 +21,7 @@ public class ClientMessage {
@JsonbProperty("message")
public GameEvent event;
@Override
public String toString() {
return jsonb.toJson(this);
}
@JsonbProperty("spectatorjoined")
public Boolean isSpectator;
}

View File

@ -1,18 +1,24 @@
package org.libertybikes.game.core;
import static org.libertybikes.game.round.service.GameRoundWebsocket.sendTextToClient;
import static org.libertybikes.game.round.service.GameRoundWebsocket.sendTextToClients;
import java.util.Arrays;
import java.util.HashSet;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
import javax.json.Json;
import javax.json.JsonArrayBuilder;
import javax.json.JsonObject;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.websocket.Session;
import org.libertybikes.game.core.ClientMessage.GameEvent;
import org.libertybikes.game.core.Player.STATUS;
public class GameRound implements Runnable {
@ -29,7 +35,7 @@ public class GameRound implements Runnable {
public final String id;
public final String nextRoundId;
public Set<Player> players = new HashSet<Player>();
public final Map<Session, Client> clients = new HashMap<>();
public State state = State.OPEN;
private boolean[][] board = new boolean[BOARD_SIZE][BOARD_SIZE];
@ -53,19 +59,64 @@ public class GameRound implements Runnable {
nextRoundId = getRandomId();
}
public void addPlayer(Player p) {
players.add(p);
System.out.println("Player " + players.size() + " has joined.");
broadcastPlayerLocations();
broadcastPlayerList();
public void handleMessage(ClientMessage msg, Session client) {
if (msg.event != null) {
if (GameEvent.GAME_START == msg.event)
startGame();
else if (GameEvent.GAME_PAUSE == msg.event)
pause();
}
public void removePlayer(Player p) {
if (msg.direction != null) {
Player p = clients.get(client).player;
p.setDirection(msg.direction);
}
if (msg.playerJoinedId != null) {
addPlayer(client, msg.playerJoinedId);
}
if (Boolean.TRUE == msg.isSpectator) {
addSpectator(client);
}
}
public void addPlayer(Session s, String playerId) {
Player p = PlayerFactory.initNextPlayer(this, playerId);
clients.put(s, new Client(s, p));
System.out.println("Player " + getPlayers().size() + " has joined.");
broadcastPlayerList();
broadcastPlayerLocations();
}
public void addSpectator(Session s) {
System.out.println("A spectator has joined.");
clients.put(s, new Client(s));
sendTextToClient(s, getPlayerList());
sendTextToClient(s, getPlayerLocations());
}
private void removePlayer(Player p) {
p.disconnect();
System.out.println(p.playerName + " disconnected.");
broadcastPlayerList();
}
public int removeClient(Session client) {
Client c = clients.remove(client);
if (c != null && c.player != null)
removePlayer(c.player);
return clients.size();
}
public Set<Player> getPlayers() {
return clients.values()
.stream()
.filter(c -> c.isPlayer())
.map(c -> c.player)
.collect(Collectors.toSet());
}
@Override
public void run() {
for (int i = 0; i < BOARD_SIZE; i++)
@ -87,7 +138,7 @@ public class GameRound implements Runnable {
// Move all living players forward 1
boolean playerStatusChange = false;
boolean playersMoved = false;
for (Player p : players) {
for (Player p : getPlayers()) {
if (p.isAlive) {
if (p.movePlayer(board)) {
playersMoved = true;
@ -112,36 +163,38 @@ public class GameRound implements Runnable {
}
}
private void broadcastPlayerLocations() {
private String getPlayerLocations() {
JsonArrayBuilder arr = Json.createArrayBuilder();
for (Player p : players)
for (Player p : getPlayers())
arr.add(p.toJson());
String playerLocations = Json.createObjectBuilder().add("playerlocs", arr).build().toString();
for (Player client : players)
client.sendTextToClient(playerLocations);
return Json.createObjectBuilder().add("playerlocs", arr).build().toString();
}
private void broadcastPlayerList() {
private String getPlayerList() {
JsonArrayBuilder array = Json.createArrayBuilder();
for (Player p : players) {
for (Player p : getPlayers()) {
array.add(Json.createObjectBuilder()
.add("name", p.playerName)
.add("status", p.getStatus().toString())
.add("color", p.color));
}
JsonObject obj = Json.createObjectBuilder().add("playerlist", array).build();
System.out.println("Playerlist: " + obj.toString());
return Json.createObjectBuilder().add("playerlist", array).build().toString();
}
for (Player player : players)
player.sendTextToClient(obj.toString());
private void broadcastPlayerLocations() {
sendTextToClients(clients.keySet(), getPlayerLocations());
}
private void broadcastPlayerList() {
sendTextToClients(clients.keySet(), getPlayerList());
}
private void checkForWinner(Player dead) {
if (players.size() < 2) // 1 player game, no winner
if (getPlayers().size() < 2) // 1 player game, no winner
return;
int alivePlayers = 0;
Player alive = null;
for (Player cur : players) {
for (Player cur : getPlayers()) {
if (cur.isAlive) {
alivePlayers++;
alive = cur;
@ -153,7 +206,7 @@ public class GameRound implements Runnable {
public void startGame() {
paused.set(false);
for (Player p : players)
for (Player p : getPlayers())
if (STATUS.Connected == p.getStatus())
p.setStatus(STATUS.Alive);
broadcastPlayerList();

View File

@ -3,13 +3,6 @@
*/
package org.libertybikes.game.core;
import java.io.IOException;
import javax.websocket.Session;
/**
* @author Andrew
*/
public class Player {
public static enum DIRECTION {
@ -27,7 +20,6 @@ public class Player {
Disconnected
}
private Session client;
public final String color;
public DIRECTION direction = DIRECTION.RIGHT;
private DIRECTION lastDirection = null;
@ -38,14 +30,12 @@ public class Player {
public boolean isAlive = true;
private STATUS playerStatus = STATUS.Connected;
public Player(Session client, String color) {
public Player(String color) {
this.color = color;
this.client = client;
}
public Player(Session client, String color, int xstart, int ystart) {
public Player(String color, int xstart, int ystart) {
this.color = color;
this.client = client;
x = xstart;
y = ystart;
}
@ -63,6 +53,9 @@ public class Player {
}
public void setDirection(DIRECTION newDirection) {
if (newDirection == direction)
return;
// Make sure the player doesn't move backwards on themselves
if (lastDirection != null) {
if ((newDirection == DIRECTION.UP && lastDirection == DIRECTION.DOWN) ||
@ -125,18 +118,7 @@ public class Player {
return isAlive;
}
public void sendTextToClient(String message) {
if (client != null) {
try {
client.getBasicRemote().sendText(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}
public void disconnect() {
this.client = null;
setStatus(STATUS.Disconnected);
}

View File

@ -3,14 +3,8 @@
*/
package org.libertybikes.game.core;
import javax.websocket.Session;
import org.libertybikes.game.core.Player.DIRECTION;
/**
* @author Andrew
*
*/
public class PlayerFactory {
public static int MAX_PLAYERS = PlayerData.values().length;
@ -36,9 +30,9 @@ public class PlayerFactory {
private static final PlayerData[] startingData = new PlayerData[] { PlayerData.START_1, PlayerData.START_2, PlayerData.START_3, PlayerData.START_4 };
public static Player initNextPlayer(GameRound g, Session client, String name) {
PlayerData data = startingData[g.players.size()];
Player p = new Player(client, data.color, data.x, data.y);
public static Player initNextPlayer(GameRound g, String name) {
PlayerData data = startingData[g.getPlayers().size()];
Player p = new Player(data.color, data.x, data.y);
p.direction = data.dir;
p.playerName = name;
return p;

View File

@ -10,15 +10,10 @@ import javax.ws.rs.container.ContainerResponseContext;
import javax.ws.rs.container.ContainerResponseFilter;
import javax.ws.rs.ext.Provider;
/**
* @author aguibert
*
*/
@Provider
public class CORSFilter implements ContainerResponseFilter {
@Override
public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext)
throws IOException {
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");

View File

@ -23,7 +23,7 @@ public class GameRoundService {
@Context
UriInfo uri;
Map<String, GameRound> allRounds = new HashMap<>();
private final Map<String, GameRound> allRounds = new HashMap<>();
@GET
@Produces(MediaType.APPLICATION_JSON)
@ -37,6 +37,9 @@ public class GameRoundService {
GameRound p = new GameRound();
allRounds.put(p.id, p);
System.out.println("Created round id=" + p.id);
if (allRounds.size() > 3)
System.out.println("WARNING: Found more than 3 active games in GameRoundService. " +
"They are probably not being cleaned up properly: " + allRounds.keySet());
return p.id;
}
@ -44,10 +47,7 @@ public class GameRoundService {
@Path("/{roundId}")
@Produces(MediaType.APPLICATION_JSON)
public GameRound getRound(@PathParam("roundId") String roundId) {
GameRound r = allRounds.get(roundId);
if (r == null)
return null;
return r;
return allRounds.get(roundId);
}
public GameRound requeue(GameRound oldRound) {
@ -56,4 +56,9 @@ public class GameRoundService {
return existingRound == null ? nextRound : existingRound;
}
public GameRound deleteRound(String roundId) {
System.out.println("Deleting round id=" + roundId);
return allRounds.remove(roundId);
}
}

View File

@ -3,8 +3,8 @@
*/
package org.libertybikes.game.round.service;
import java.util.HashMap;
import java.util.Map;
import java.io.IOException;
import java.util.Set;
import javax.enterprise.context.Dependent;
import javax.inject.Inject;
@ -21,11 +21,9 @@ import javax.websocket.server.ServerEndpoint;
import org.libertybikes.game.core.ClientMessage;
import org.libertybikes.game.core.ClientMessage.GameEvent;
import org.libertybikes.game.core.GameRound;
import org.libertybikes.game.core.Player;
import org.libertybikes.game.core.PlayerFactory;
@Dependent
@ServerEndpoint("/round/ws/{roundId}") //
@ServerEndpoint("/round/ws/{roundId}")
public class GameRoundWebsocket {
@Inject
@ -33,8 +31,6 @@ public class GameRoundWebsocket {
private final Jsonb jsonb = JsonbBuilder.create();
private static final Map<Session, Player> clients = new HashMap<>();
@OnOpen
public void onOpen(@PathParam("roundId") String roundId, Session session) {
System.out.println("Opened a session for game round: " + roundId);
@ -43,46 +39,54 @@ public class GameRoundWebsocket {
@OnClose
public void onClose(@PathParam("roundId") String roundId, Session peer) {
System.out.println("Closed a session for game round: " + roundId);
GameRound game = gameSvc.getRound(roundId);
game.removePlayer(clients.get(peer));
clients.remove(peer);
try {
GameRound round = gameSvc.getRound(roundId);
if (round != null)
if (round.removeClient(peer) == 0)
gameSvc.deleteRound(roundId);
} catch (Exception e) {
e.printStackTrace();
}
}
@OnMessage
public void onMessage(@PathParam("roundId") final String roundId, String message, Session session) {
try {
final ClientMessage msg = jsonb.fromJson(message, ClientMessage.class);
final GameRound round = gameSvc.getRound(roundId);
System.out.println("[onMessage] roundId=" + roundId + " msg=" + msg);
System.out.println("[onMessage] roundId=" + roundId + " msg=" + message);
if (msg.event != null) {
if (GameEvent.GAME_START == msg.event)
round.startGame();
else if (GameEvent.GAME_PAUSE == msg.event)
round.pause();
else if (GameEvent.GAME_REQUEUE == msg.event) {
if (msg.event != null && GameEvent.GAME_REQUEUE == msg.event) {
GameRound nextGame = gameSvc.requeue(round);
String requeueMsg = Json.createObjectBuilder()
.add("requeue", nextGame.id)
.build()
.toString();
Player p = clients.get(session);
p.sendTextToClient(requeueMsg);
sendTextToClient(session, requeueMsg);
if (round.removeClient(session) == 0)
gameSvc.deleteRound(roundId);
} else {
round.handleMessage(msg, session);
}
} catch (Exception e) {
e.printStackTrace();
}
}
if (msg.direction != null) {
Player p = clients.get(session);
Player.DIRECTION curDir = p.getDrirection();
if (curDir != msg.direction) {
p.setDirection(msg.direction);
public static void sendTextToClient(Session client, String message) {
if (client != null) {
try {
client.getBasicRemote().sendText(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}
if (msg.playerJoinedId != null) {
Player p = PlayerFactory.initNextPlayer(round, session, msg.playerJoinedId);
clients.put(session, p);
round.addPlayer(p);
}
public static void sendTextToClients(Set<Session> clients, String message) {
System.out.println("Sending " + clients.size() + " clients the message: " + message);
for (Session client : clients)
sendTextToClient(client, message);
}
}