Merge pull request #45 from aguibert/start-countdown

Game starting countdown
This commit is contained in:
Andrew Guibert 2018-03-23 01:04:53 -07:00 committed by GitHub
commit ee7499b505
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 211 additions and 64 deletions

View File

@ -11,6 +11,11 @@ buildscript {
}
}
task clean(type: Delete) {
subprojects.each { dependsOn("${it.name}:clean") };
delete 'build'
}
subprojects {
apply plugin: 'liberty'
apply plugin: 'war'
@ -38,7 +43,7 @@ subprojects {
providedCompile group: 'org.eclipse.microprofile', name: 'microprofile', version: '1.3'
providedCompile group: 'javax', name: 'javaee-api', version: '8.0'
testCompile group: 'junit', name: 'junit', version: '4.+'
testCompile group: 'org.eclipse', name: 'yasson', version: '1.0'
testCompile group: 'org.eclipse', name: 'yasson', version: '1.0.1'
testCompile group: 'org.glassfish', name: 'javax.json', version: '1.1.+'
}
@ -58,10 +63,11 @@ subprojects {
install {
// use 1 liberty install for the whole repo
baseDir = rootProject.buildDir
runtimeUrl = "https://public.dhe.ibm.com/ibmdl/export/pub/software/websphere/wasdev/downloads/wlp/beta/wlp-beta-2018.2.0.0.zip"
runtimeUrl = "https://public.dhe.ibm.com/ibmdl/export/pub/software/websphere/wasdev/downloads/wlp/beta/wlp-beta-2018.3.0.0.zip"
}
}
clean.dependsOn 'libertyStop'
libertyDebug.dependsOn 'libertyStop'
libertyStart.dependsOn 'libertyStop', 'test'
libertyRun.dependsOn 'libertyStop'

View File

@ -4,6 +4,11 @@
<h2>code: <span id="game-code"></span></h2>
</div>
</div>
<div id="loader-overlay" class="loader-overlay d-none" data-ng-hide="vm.hideLoader">
<div class="spin-loader" data-ng-class="{'shrink': vm.shrinkOnHide, 'expand': !vm.shrinkOnHide}"></div>
</div>
<div id="game-container">
<div id="game-board">
<canvas width="600" height="600" id="gameCanvas"></canvas>

View File

@ -134,3 +134,71 @@ body {
#footer .btn {
width: calc(50% - 10px);
}
$loaderHeightWidth:120px;
$loaderPrimaryColor:#18bc9c;
$loaderBorderSize:3px;
$loaderMinHeightWidth:10px;
$loaderMaxHeightWidth:400px;
$animationTime: 0.3s;
$animationStyle:linear;
$animationStyle:cubic-bezier(.23,.78,.69,.84);
.spin-loader {
position:fixed;
top:25%;
left:50%;
margin-left:-($loaderHeightWidth/2);
height:$loaderHeightWidth;
width:$loaderHeightWidth;
background:transparent;
border:$loaderBorderSize solid;
border-color:
$loaderPrimaryColor
$loaderPrimaryColor
$loaderPrimaryColor
darken($loaderPrimaryColor, 30);
border-radius:50%;
transition:all $animationTime $animationStyle;
animation: spin 0.75s infinite linear;
}
.loader-overlay {
position:fixed;
top:55px;
bottom:0;
right:0;
left:0;
background:rgba(0,0,0,0.5);
-moz-transition: opacity $animationTime $animationStyle;
-o-transition: opacity $animationTime $animationStyle;
-webkit-transition: opacity $animationTime $animationStyle;
transition: opacity $animationTime $animationStyle;
&.ng-hide {
opacity: 0;
.spin-loader.shrink {
top: calc(25% + #{($loaderHeightWidth/2)});
margin-left:0;
width:$loaderMinHeightWidth;
height:$loaderMinHeightWidth;
}
.spin-loader.expand {
top: calc(25% - #{($loaderMaxHeightWidth/4)});
margin-left:-($loaderMaxHeightWidth/2);
width:$loaderMaxHeightWidth;
height:$loaderMaxHeightWidth;
}
}
}
@keyframes spin {
0% {
transform:rotate(0deg);
}
100% {
transform:rotate(360deg);
}
}

View File

@ -47,6 +47,9 @@ export class GameComponent implements OnInit {
}
}
}
if (json.countdown) {
this.startingCountdown(json.countdown);
}
}, (err) => {
console.log(`Error occurred: ${err}`);
});
@ -99,7 +102,7 @@ export class GameComponent implements OnInit {
requeue() {
this.gameService.send({ message: 'GAME_REQUEUE' });
}
moveUp() {
this.gameService.send({ direction: 'UP' });
}
@ -169,5 +172,13 @@ export class GameComponent implements OnInit {
return '<span class=\'badge badge-pill badge-secondary\'>Disconnected</span>';
}
}
startingCountdown(seconds) {
const loader = $('#loader-overlay');
loader.removeClass('d-none');
setTimeout(function() {
loader.addClass('d-none');
}, (1000 * seconds));
}
}

View File

@ -7,13 +7,15 @@ import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import javax.json.bind.annotation.JsonbTransient;
public class GameBoard {
public static final int BOARD_SIZE = 121;
public static final short SPOT_AVAILABLE = 0, TRAIL_SPOT_TAKEN = -10, OBJECT_SPOT_TAKEN = -8, PLAYER_SPOT_TAKEN = 1;
// @JsonbTransient // TODO use annotation here once OpenLiberty upgrades to yasson 1.0.1 (contains bug fix)
private final short[][] board = new short[BOARD_SIZE][BOARD_SIZE];
@JsonbTransient
public final short[][] board = new short[BOARD_SIZE][BOARD_SIZE];
public final Set<Obstacle> obstacles = new HashSet<>();
public final Set<MovingObstacle> movingObstacles = new HashSet<>();
@ -93,11 +95,6 @@ public class GameBoard {
return players.remove(p);
}
// TODO: once OpenLiberty moves up to yasson 1.0.1 this method can be removed
public short[][] board() {
return board;
}
// For debugging
public void dumpBoard() {
for (int i = 0; i < BOARD_SIZE; i++) {

View File

@ -14,11 +14,10 @@ import java.util.stream.Collectors;
import javax.enterprise.concurrent.ManagedScheduledExecutorService;
import javax.enterprise.inject.spi.CDI;
import javax.json.Json;
import javax.json.JsonArrayBuilder;
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;
import javax.naming.NamingException;
import javax.websocket.Session;
@ -33,19 +32,20 @@ import org.libertybikes.restclient.PlayerService;
public class GameRound implements Runnable {
public static enum State {
OPEN, FULL, RUNNING, FINISHED
OPEN, FULL, STARTING, RUNNING, FINISHED
}
public static final Jsonb jsonb = JsonbBuilder.create();
public static final int GAME_TICK_SPEED = 50; // ms
private static final int DELAY_BETWEEN_ROUNDS = 5; //ticks
private static final int STARTING_COUNTDOWN = 3; // seconds
private static final Random r = new Random();
private static final AtomicInteger runningGames = new AtomicInteger();
// Properties exposed in JSON representation of object
public final String id;
public final String nextRoundId;
public State gameState = State.OPEN;
public volatile State gameState = State.OPEN;
private final GameBoard board = new GameBoard();
private final AtomicBoolean gameRunning = new AtomicBoolean(false);
@ -79,7 +79,7 @@ public class GameRound implements Runnable {
return board;
}
public void updatePlayerDirection(Session playerSession, ClientMessage msg) {
public void updatePlayerDirection(Session playerSession, InboundMessage msg) {
Client c = clients.get(playerSession);
if (c.isPlayer())
c.player.setDirection(msg.direction);
@ -93,7 +93,7 @@ public class GameRound implements Runnable {
return;
}
if (players().size() + 1 >= Player.MAX_PLAYERS) {
if (getPlayers().size() + 1 >= Player.MAX_PLAYERS) {
gameState = State.FULL;
}
@ -113,7 +113,7 @@ public class GameRound implements Runnable {
public void addSpectator(Session s) {
System.out.println("A spectator has joined.");
clients.put(s, new Client(s));
sendTextToClient(s, getPlayerList());
sendTextToClient(s, jsonb.toJson(new OutboundMessage.PlayerList(getPlayers())));
sendTextToClient(s, jsonb.toJson(board));
}
@ -122,7 +122,7 @@ public class GameRound implements Runnable {
System.out.println(p.name + " disconnected.");
// Open player slot for new joiners
if (gameState == State.FULL && players().size() - 1 < Player.MAX_PLAYERS) {
if (gameState == State.FULL && getPlayers().size() - 1 < Player.MAX_PLAYERS) {
gameState = State.OPEN;
}
@ -143,8 +143,8 @@ public class GameRound implements Runnable {
return clients.size();
}
// @JsonbTransient // TODO re-enable this anno once Liberty upgrades to yasson 1.0.1
public Set<Player> players() {
@JsonbTransient
public Set<Player> getPlayers() {
return board.players;
}
@ -183,7 +183,7 @@ public class GameRound implements Runnable {
if (gameState != State.FINISHED)
throw new IllegalStateException("Canot update player stats while game is still running.");
Set<Player> players = players();
Set<Player> players = getPlayers();
if (players.size() < 2)
return; // Don't update player stats for single-player games
@ -211,9 +211,9 @@ 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.board())) {
if (p.movePlayer(board.board)) {
playersMoved = true;
} else {
death = true;
@ -242,38 +242,30 @@ public class GameRound implements Runnable {
}
}
private String getPlayerList() {
// TODO: Use JSON-B instead of JSON-P here
JsonArrayBuilder array = Json.createArrayBuilder();
for (Player p : players()) {
array.add(Json.createObjectBuilder()
.add("name", p.name)
.add("status", p.getStatus().toString())
.add("color", p.color));
}
return Json.createObjectBuilder().add("playerlist", array).build().toString();
}
public Set<Session> nonMobileSessions() {
return clients.entrySet().stream().filter(c -> !c.getValue().isPhone).map(s -> s.getKey()).collect(Collectors.toSet());
private Set<Session> getNonMobileSessions() {
return clients.entrySet()
.stream()
.filter(c -> !c.getValue().isPhone)
.map(s -> s.getKey())
.collect(Collectors.toSet());
}
private void broadcastGameBoard() {
sendTextToClients(nonMobileSessions(), jsonb.toJson(board));
sendTextToClients(getNonMobileSessions(), jsonb.toJson(board));
}
private void broadcastPlayerList() {
sendTextToClients(nonMobileSessions(), getPlayerList());
sendTextToClients(getNonMobileSessions(), jsonb.toJson(new OutboundMessage.PlayerList(getPlayers())));
}
private void checkForWinner() {
if (players().size() < 2) {// 1 player game, no winner
if (getPlayers().size() < 2) {// 1 player game, no winner
gameState = State.FINISHED;
return;
}
int alivePlayers = 0;
Player alive = null;
for (Player cur : players()) {
for (Player cur : getPlayers()) {
if (cur.isAlive) {
alivePlayers++;
alive = cur;
@ -290,8 +282,17 @@ public class GameRound implements Runnable {
}
public void startGame() {
if (gameState != State.OPEN && gameState != State.FULL)
return;
// Issue a countdown to all of the clients
gameState = State.STARTING;
sendTextToClients(clients.keySet(), jsonb.toJson(new OutboundMessage.StartingCountdown(STARTING_COUNTDOWN)));
delay(TimeUnit.SECONDS.toMillis(STARTING_COUNTDOWN));
paused.set(false);
for (Player p : players())
for (Player p : getPlayers())
if (STATUS.Connected == p.getStatus())
p.setStatus(STATUS.Alive);
broadcastPlayerList();

View File

@ -5,7 +5,7 @@ package org.libertybikes.game.core;
import javax.json.bind.annotation.JsonbProperty;
public class ClientMessage {
public class InboundMessage {
public static enum GameEvent {
GAME_START,

View File

@ -0,0 +1,39 @@
/**
*
*/
package org.libertybikes.game.core;
import java.util.Set;
import javax.json.bind.annotation.JsonbProperty;
public class OutboundMessage {
public static class PlayerList {
@JsonbProperty("playerlist")
public final Set<Player> playerlist;
public PlayerList(Set<Player> playerlist) {
this.playerlist = playerlist;
}
}
public static class RequeueGame {
@JsonbProperty("requeue")
public final String roundId;
public RequeueGame(String nextRoundId) {
this.roundId = nextRoundId;
}
}
public static class StartingCountdown {
@JsonbProperty("countdown")
public final int seconds;
public StartingCountdown(int startingSeconds) {
this.seconds = startingSeconds;
}
}
}

View File

@ -8,7 +8,6 @@ import java.util.Set;
import javax.enterprise.context.Dependent;
import javax.inject.Inject;
import javax.json.Json;
import javax.json.bind.Jsonb;
import javax.json.bind.JsonbBuilder;
import javax.websocket.OnClose;
@ -19,9 +18,10 @@ import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.libertybikes.game.core.ClientMessage;
import org.libertybikes.game.core.ClientMessage.GameEvent;
import org.libertybikes.game.core.GameRound;
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
@ -35,7 +35,7 @@ public class GameRoundWebsocket {
@RestClient
PlayerService playerSvc;
private final Jsonb jsonb = JsonbBuilder.create();
private final static Jsonb jsonb = JsonbBuilder.create();
@OnOpen
public void onOpen(@PathParam("roundId") String roundId, Session session) {
@ -58,7 +58,7 @@ public class GameRoundWebsocket {
@OnMessage
public void onMessage(@PathParam("roundId") final String roundId, String message, Session session) {
try {
final ClientMessage msg = jsonb.fromJson(message, ClientMessage.class);
final InboundMessage msg = jsonb.fromJson(message, InboundMessage.class);
final GameRound round = gameSvc.getRound(roundId);
System.out.println("[onMessage] roundId=" + roundId + " msg=" + message);
@ -84,10 +84,7 @@ public class GameRoundWebsocket {
public static void requeueClient(GameRoundService gameSvc, GameRound oldRound, Session s) {
GameRound nextGame = gameSvc.requeue(oldRound);
String requeueMsg = Json.createObjectBuilder()
.add("requeue", nextGame.id)
.build()
.toString();
String requeueMsg = jsonb.toJson(new OutboundMessage.RequeueGame(nextGame.id));
sendTextToClient(s, requeueMsg);
if (oldRound.removeClient(s) == 0)
gameSvc.deleteRound(oldRound.id);

View File

@ -49,33 +49,33 @@ public class GameBoardTest {
verifyAvailable(4, 10);
verifyAvailable(5, 10);
}
@Test
public void testIllegalObstacle() {
try {
board.addObstacle(new Obstacle(2,1,-3,4));
board.addObstacle(new Obstacle(2, 1, -3, 4));
fail("Should not be able to add obstacle off of board");
} catch (IllegalArgumentException e) {
// expected
}
try {
board.addObstacle(new Obstacle(BOARD_SIZE + 1, 1, 3, 4));
fail("Should not be able to add obstacle off of board");
} catch (IllegalArgumentException e) {
} catch (IllegalArgumentException e) {
// expected
}
}
private void verifyTaken(int x, int y) {
if (board.board()[x][y] != GameBoard.OBJECT_SPOT_TAKEN) {
if (board.board[x][y] != GameBoard.OBJECT_SPOT_TAKEN) {
board.dumpBoard();
fail("Spot should be taken but it was available: [" + x + "][" + y + "]");
}
}
private void verifyAvailable(int x, int y) {
if (board.board()[x][y] != GameBoard.SPOT_AVAILABLE) {
if (board.board[x][y] != GameBoard.SPOT_AVAILABLE) {
board.dumpBoard();
fail("Spot should be availble but it was taken: [" + x + "][" + y + "]");
}

View File

@ -6,12 +6,16 @@ package org.libertybikes.game.core;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.util.Collections;
import javax.json.bind.Jsonb;
import javax.json.bind.JsonbBuilder;
import javax.json.bind.annotation.JsonbPropertyOrder;
import org.junit.Test;
import org.libertybikes.game.core.ClientMessage.GameEvent;
import org.libertybikes.game.core.InboundMessage.GameEvent;
import org.libertybikes.game.core.OutboundMessage.PlayerList;
import org.libertybikes.game.core.OutboundMessage.RequeueGame;
import org.libertybikes.game.core.OutboundMessage.StartingCountdown;
public class JsonDataTest {
@ -19,28 +23,28 @@ public class JsonDataTest {
@Test
public void testPlayerJoined() {
ClientMessage playerJoined = new ClientMessage();
InboundMessage playerJoined = new InboundMessage();
playerJoined.playerJoinedId = "1234";
assertEquals("{\"playerjoined\":\"1234\"}", jsonb.toJson(playerJoined));
}
@Test
public void testGameEventRequeue() {
ClientMessage gameEvent = new ClientMessage();
InboundMessage gameEvent = new InboundMessage();
gameEvent.event = GameEvent.GAME_REQUEUE;
assertEquals("{\"message\":\"GAME_REQUEUE\"}", jsonb.toJson(gameEvent));
}
@Test
public void testGameEventStart() {
ClientMessage gameEvent = new ClientMessage();
InboundMessage gameEvent = new InboundMessage();
gameEvent.event = GameEvent.GAME_START;
assertEquals("{\"message\":\"GAME_START\"}", jsonb.toJson(gameEvent));
}
@Test
public void testSpectatorJoined() {
ClientMessage msg = new ClientMessage();
InboundMessage msg = new InboundMessage();
msg.isSpectator = Boolean.FALSE;
assertEquals("{\"spectatorjoined\":false}", jsonb.toJson(msg));
@ -91,6 +95,25 @@ public class JsonDataTest {
assertEquals("andy", p.name);
}
@Test
public void testPlayerList() {
PlayerList list = new OutboundMessage.PlayerList(Collections.singleton(new Player("123", "Bob", (short) 1)));
assertEquals("{\"playerlist\":[{\"id\":\"123\",\"name\":\"Bob\",\"color\":\"#FF0000\",\"status\":\"Connected\",\"isAlive\":true,\"x\":9,\"y\":110,\"width\":3,\"height\":3,\"oldX\":9,\"oldY\":110,\"trailPosX\":10,\"trailPosY\":111,\"trailPosX2\":10,\"trailPosY2\":111}]}",
jsonb.toJson(list));
}
@Test
public void testRequeue() {
RequeueGame req = new OutboundMessage.RequeueGame("1234");
assertEquals("{\"requeue\":\"1234\"}", jsonb.toJson(req));
}
@Test
public void testCountdown() {
StartingCountdown countdown = new OutboundMessage.StartingCountdown(5);
assertEquals("{\"countdown\":5}", jsonb.toJson(countdown));
}
private void assertContains(String expected, String search) {
assertTrue("Did not find '" + expected + "' inside of the string: " + search, search.contains(expected));
}