From 13e01f091df7194940751fe279edd101f8020527 Mon Sep 17 00:00:00 2001 From: Andrew Guibert Date: Wed, 14 Feb 2018 11:35:59 -0600 Subject: [PATCH] Implement non-moving obstacles --- build.gradle | 7 +- frontend/prebuild/src/app/game/websocket.ts | 11 ++- frontend/prebuild/src/app/game/whiteboard.ts | 17 ++-- game-service/.classpath | 1 + .../org/libertybikes/game/core/GameBoard.java | 70 +++++++++++++++ .../org/libertybikes/game/core/GameRound.java | 86 ++++++++++--------- .../org/libertybikes/game/core/Obstacle.java | 26 ++++++ .../org/libertybikes/game/core/Player.java | 6 +- .../libertybikes/game/core/PlayerFactory.java | 6 +- .../libertybikes/game/core/GameBoardTest.java | 67 +++++++++++++++ .../libertybikes/game/core/JsonDataTest.java | 81 +++++++++++++++++ 11 files changed, 319 insertions(+), 59 deletions(-) create mode 100644 game-service/src/main/java/org/libertybikes/game/core/GameBoard.java create mode 100644 game-service/src/main/java/org/libertybikes/game/core/Obstacle.java create mode 100644 game-service/src/test/java/org/libertybikes/game/core/GameBoardTest.java create mode 100644 game-service/src/test/java/org/libertybikes/game/core/JsonDataTest.java diff --git a/build.gradle b/build.gradle index cba58fd..40f3fdb 100644 --- a/build.gradle +++ b/build.gradle @@ -27,8 +27,11 @@ subprojects { } dependencies { - compileOnly group: 'org.eclipse.microprofile', name: 'microprofile', version: '1.2' - compileOnly group: 'javax', name: 'javaee-api', version: '8.0' + providedCompile group: 'org.eclipse.microprofile', name: 'microprofile', version: '1.2' + 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.glassfish', name: 'javax.json', version: '1.1.+' } liberty { diff --git a/frontend/prebuild/src/app/game/websocket.ts b/frontend/prebuild/src/app/game/websocket.ts index e1ebdd4..436ca69 100644 --- a/frontend/prebuild/src/app/game/websocket.ts +++ b/frontend/prebuild/src/app/game/websocket.ts @@ -47,9 +47,14 @@ export class GameWebsocket { sessionStorage.setItem('roundId', this.roundId); location.reload(); } - if (json.playerlocs) { - for (let playerLoc of json.playerlocs) { - this.whiteboard.drawSquare(playerLoc); + if (json.obstacles) { + for (let obstacle of json.obstacles) { + this.whiteboard.drawObstacle(obstacle); + } + } + if (json.players) { + for (let player of json.players) { + this.whiteboard.drawPlayer(player); } } } diff --git a/frontend/prebuild/src/app/game/whiteboard.ts b/frontend/prebuild/src/app/game/whiteboard.ts index 6c9730f..7d752c9 100644 --- a/frontend/prebuild/src/app/game/whiteboard.ts +++ b/frontend/prebuild/src/app/game/whiteboard.ts @@ -2,7 +2,7 @@ import * as $ from 'jquery'; import { GameWebsocket } from './websocket'; export class Whiteboard { - static readonly PLAYER_SIZE = 5; + static readonly BOX_SIZE = 5; canvas: any; context: any; gamesocket: GameWebsocket; @@ -27,11 +27,16 @@ export class Whiteboard { }; } - drawSquare(data) { - const json = JSON.parse(data); - this.context.fillStyle = json.color; - this.context.fillRect(Whiteboard.PLAYER_SIZE * json.coords.x, Whiteboard.PLAYER_SIZE * json.coords.y, - Whiteboard.PLAYER_SIZE, Whiteboard.PLAYER_SIZE); + drawPlayer(player) { + this.context.fillStyle = player.color; + this.context.fillRect(Whiteboard.BOX_SIZE * player.x, Whiteboard.BOX_SIZE * player.y, + Whiteboard.BOX_SIZE, Whiteboard.BOX_SIZE); + } + + drawObstacle(obstacle) { + this.context.fillStyle = '#808080'; // obstacles always grey + this.context.fillRect(Whiteboard.BOX_SIZE * obstacle.x, Whiteboard.BOX_SIZE * obstacle.y, + Whiteboard.BOX_SIZE * obstacle.height, Whiteboard.BOX_SIZE * obstacle.width); } updatePlayerList(json) { diff --git a/game-service/.classpath b/game-service/.classpath index 5880dcc..abd2974 100644 --- a/game-service/.classpath +++ b/game-service/.classpath @@ -1,6 +1,7 @@ + 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 new file mode 100644 index 0000000..eb34e56 --- /dev/null +++ b/game-service/src/main/java/org/libertybikes/game/core/GameBoard.java @@ -0,0 +1,70 @@ +/** + * + */ +package org.libertybikes.game.core; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +public class GameBoard { + + public static final int BOARD_SIZE = 121; + public static final boolean SPOT_AVAILABLE = true, SPOT_TAKEN = false; + + // @JsonbTransient // TODO use annotation here once OpenLiberty upgrades to yasson 1.0.1 (contains bug fix) + private final boolean[][] board = new boolean[BOARD_SIZE][BOARD_SIZE]; + + public final Set obstacles = new HashSet<>(); + public final Set players = new HashSet<>(); + + public GameBoard() { + for (int i = 0; i < BOARD_SIZE; i++) + Arrays.fill(board[i], SPOT_AVAILABLE); + } + + public boolean addObstacle(Obstacle o) { + if (o.x + o.width > BOARD_SIZE || o.y + o.height > BOARD_SIZE) + throw new IllegalArgumentException("Obstacle does not fit on board: " + o); + + // First make sure all spaces are available + for (int x = 0; x < o.width; x++) + for (int y = 0; y < o.height; y++) + if (!board[o.x + x][o.y + y]) { + System.out.println("Obstacle cannot be added to board because spot [" + o.x + x + "][" + o.y + y + "] is taken."); + return false; + } + + // If all spaces are available, claim them + for (int x = 0; x < o.width; x++) + for (int y = 0; y < o.height; y++) + board[o.x + x][o.y + y] = SPOT_TAKEN; + + return obstacles.add(o); + } + + public boolean addPlayer(Player p) { + if (p.x > BOARD_SIZE || p.y > BOARD_SIZE) + throw new IllegalArgumentException("Player does not fit on board: " + p); + + board[p.x][p.y] = SPOT_TAKEN; + + return players.add(p); + } + + // TODO: once OpenLiberty moves up to yasson 1.0.1 this method can be removed + public boolean[][] board() { + return board; + } + + // For debugging + public void dumpBoard() { + for (int i = 0; i < BOARD_SIZE; i++) { + StringBuilder row = new StringBuilder(); + for (int j = 0; j < BOARD_SIZE; j++) + row.append(board[i][j] == SPOT_TAKEN ? "X" : "_"); + System.out.println(String.format("%03d %s", i, row.toString())); + } + } + +} 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 66b52aa..f620a67 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 @@ -3,7 +3,6 @@ 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.HashMap; import java.util.Map; import java.util.Random; @@ -16,6 +15,9 @@ 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.naming.InitialContext; import javax.naming.NamingException; import javax.websocket.Session; @@ -25,31 +27,30 @@ import org.libertybikes.game.core.Player.STATUS; import org.libertybikes.game.round.service.GameRoundService; import org.libertybikes.game.round.service.GameRoundWebsocket; +@JsonbPropertyOrder({ "id", "gameState", "board", "nextRoundId" }) public class GameRound implements Runnable { public static enum State { OPEN, FULL, RUNNING, FINISHED } + public static final Jsonb jsonb = JsonbBuilder.create(); public static final int GAME_TICK_SPEED = 50; // ms - public static final int BOARD_SIZE = 121; - 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; - - private final Map clients = new HashMap<>(); public State gameState = State.OPEN; + private final GameBoard board = new GameBoard(); - private boolean[][] board = new boolean[BOARD_SIZE][BOARD_SIZE]; - private AtomicBoolean gameRunning = new AtomicBoolean(false); - private AtomicBoolean paused = new AtomicBoolean(false); - private int ticksWithoutMoves = 0; - private int numOfPlayers = 0; + private final AtomicBoolean gameRunning = new AtomicBoolean(false); + private final AtomicBoolean paused = new AtomicBoolean(false); + private final Map clients = new HashMap<>(); + private final boolean[] takenPlayerSlots = new boolean[PlayerFactory.MAX_PLAYERS]; - private boolean[] takenPlayerSlots = new boolean[PlayerFactory.MAX_PLAYERS]; + private int ticksWithoutMovement = 0; // Get a string of 6 random uppercase letters (A-Z) private static String getRandomId() { @@ -66,6 +67,11 @@ public class GameRound implements Runnable { public GameRound(String id) { this.id = id; nextRoundId = getRandomId(); + board.addObstacle(new Obstacle(5, 5, 60, 60)); + } + + public GameBoard getBoard() { + return board; } public void handleMessage(ClientMessage msg, Session session) { @@ -91,10 +97,11 @@ public class GameRound implements Runnable { // Front end should be preventing a player joining a full game but // defensive programming if (gameState != State.OPEN) { + System.out.println("Cannot add player " + playerId + " to game " + id + " because game has already started."); return; } - if (++numOfPlayers > PlayerFactory.MAX_PLAYERS - 1) { + if (board.players.size() + 1 > PlayerFactory.MAX_PLAYERS - 1) { gameState = State.FULL; } @@ -111,17 +118,18 @@ public class GameRound implements Runnable { // Initialize Player Player p = PlayerFactory.initNextPlayer(this, playerId, playerNum); + board.addPlayer(p); clients.put(s, new Client(s, p)); System.out.println("Player " + playerId + " has joined."); broadcastPlayerList(); - broadcastPlayerLocations(); + broadcastGameBoard(); } public void addSpectator(Session s) { System.out.println("A spectator has joined."); clients.put(s, new Client(s)); sendTextToClient(s, getPlayerList()); - sendTextToClient(s, getPlayerLocations()); + sendTextToClient(s, jsonb.toJson(board)); } private void removePlayer(Player p) { @@ -130,8 +138,8 @@ public class GameRound implements Runnable { broadcastPlayerList(); // Open player slot for new joiners - if (--numOfPlayers < PlayerFactory.MAX_PLAYERS) { - gameState = (gameState == State.FULL) ? State.OPEN : gameState; + if (State.FULL == gameState && board.players.size() - 1 < PlayerFactory.MAX_PLAYERS) { + gameState = State.OPEN; } takenPlayerSlots[p.getPlayerNum()] = false; } @@ -143,7 +151,8 @@ public class GameRound implements Runnable { return clients.size(); } - public Set getPlayers() { + // @JsonbTransient // TODO re-enable this anno once Liberty upgrades to yasson 1.0.1 + public Set players() { return clients.values() .stream() .filter(c -> c.isPlayer()) @@ -153,8 +162,6 @@ public class GameRound implements Runnable { @Override public void run() { - for (int i = 0; i < BOARD_SIZE; i++) - Arrays.fill(board[i], true); gameRunning.set(true); System.out.println("Starting round: " + id); int numGames = runningGames.incrementAndGet(); @@ -163,7 +170,7 @@ public class GameRound implements Runnable { while (gameRunning.get()) { delay(GAME_TICK_SPEED); gameTick(); - if (ticksWithoutMoves > 5) + if (ticksWithoutMovement > 5) gameRunning.set(false); // end the game if nobody can move anymore } runningGames.decrementAndGet(); @@ -181,9 +188,9 @@ public class GameRound implements Runnable { // Move all living players forward 1 boolean playerStatusChange = false; boolean playersMoved = false; - for (Player p : getPlayers()) { + for (Player p : players()) { if (p.isAlive) { - if (p.movePlayer(board)) { + if (p.movePlayer(board.board())) { playersMoved = true; } else { // Since someone died, check for winning player @@ -194,10 +201,10 @@ public class GameRound implements Runnable { } if (playersMoved) { - ticksWithoutMoves = 0; - broadcastPlayerLocations(); + ticksWithoutMovement = 0; + broadcastGameBoard(); } else { - ticksWithoutMoves++; + ticksWithoutMovement++; } if (playerStatusChange) @@ -211,26 +218,21 @@ public class GameRound implements Runnable { } } - private String getPlayerLocations() { - JsonArrayBuilder arr = Json.createArrayBuilder(); - for (Player p : getPlayers()) - arr.add(p.toJson()); - return Json.createObjectBuilder().add("playerlocs", arr).build().toString(); - } - private String getPlayerList() { + // TODO: Use JSON-B instead of JSON-P here JsonArrayBuilder array = Json.createArrayBuilder(); - for (Player p : getPlayers()) { - array.add(Json.createObjectBuilder() - .add("name", p.playerName) - .add("status", p.getStatus().toString()) - .add("color", p.color)); + for (Player p : players()) { + if (p.isAlive) + array.add(Json.createObjectBuilder() + .add("name", p.playerName) + .add("status", p.getStatus().toString()) + .add("color", p.color)); } return Json.createObjectBuilder().add("playerlist", array).build().toString(); } - private void broadcastPlayerLocations() { - sendTextToClients(clients.keySet(), getPlayerLocations()); + private void broadcastGameBoard() { + sendTextToClients(clients.keySet(), jsonb.toJson(board)); } private void broadcastPlayerList() { @@ -238,11 +240,11 @@ public class GameRound implements Runnable { } private void checkForWinner(Player dead) { - if (getPlayers().size() < 2) // 1 player game, no winner + if (players().size() < 2) // 1 player game, no winner return; int alivePlayers = 0; Player alive = null; - for (Player cur : getPlayers()) { + for (Player cur : players()) { if (cur.isAlive) { alivePlayers++; alive = cur; @@ -256,7 +258,7 @@ public class GameRound implements Runnable { public void startGame() { paused.set(false); - for (Player p : getPlayers()) + for (Player p : players()) if (STATUS.Connected == p.getStatus()) p.setStatus(STATUS.Alive); broadcastPlayerList(); diff --git a/game-service/src/main/java/org/libertybikes/game/core/Obstacle.java b/game-service/src/main/java/org/libertybikes/game/core/Obstacle.java new file mode 100644 index 0000000..ef0711f --- /dev/null +++ b/game-service/src/main/java/org/libertybikes/game/core/Obstacle.java @@ -0,0 +1,26 @@ +/** + * + */ +package org.libertybikes.game.core; + +import javax.json.bind.annotation.JsonbCreator; + +public class Obstacle { + + public final int height; + + public final int width; + + public int x; + + public int y; + + @JsonbCreator + public Obstacle(int w, int h, int x, int y) { + this.height = h; + this.width = w; + this.x = x; + this.y = y; + } + +} diff --git a/game-service/src/main/java/org/libertybikes/game/core/Player.java b/game-service/src/main/java/org/libertybikes/game/core/Player.java index 217584c..2910c21 100644 --- a/game-service/src/main/java/org/libertybikes/game/core/Player.java +++ b/game-service/src/main/java/org/libertybikes/game/core/Player.java @@ -72,7 +72,7 @@ public class Player { direction = newDirection; } - public DIRECTION getDrirection() { + public DIRECTION getDirection() { return direction; } @@ -98,11 +98,11 @@ public class Player { y--; break; case DOWN: - if (y + 1 < GameRound.BOARD_SIZE) + if (y + 1 < GameBoard.BOARD_SIZE) y++; break; case RIGHT: - if (x + 1 < GameRound.BOARD_SIZE) + if (x + 1 < GameBoard.BOARD_SIZE) x++; break; case LEFT: diff --git a/game-service/src/main/java/org/libertybikes/game/core/PlayerFactory.java b/game-service/src/main/java/org/libertybikes/game/core/PlayerFactory.java index 6497471..ab26890 100644 --- a/game-service/src/main/java/org/libertybikes/game/core/PlayerFactory.java +++ b/game-service/src/main/java/org/libertybikes/game/core/PlayerFactory.java @@ -11,9 +11,9 @@ public class PlayerFactory { private static enum PlayerData { START_1("#DF740C", 10, 10, DIRECTION.RIGHT), - START_2("#FF0000", 10, GameRound.BOARD_SIZE - 10, DIRECTION.UP), - START_3("#6FC3DF", GameRound.BOARD_SIZE - 10, 10, DIRECTION.DOWN), - START_4("#FFE64D", GameRound.BOARD_SIZE - 10, GameRound.BOARD_SIZE - 10, DIRECTION.LEFT); + START_2("#FF0000", 10, GameBoard.BOARD_SIZE - 10, DIRECTION.UP), + START_3("#6FC3DF", GameBoard.BOARD_SIZE - 10, 10, DIRECTION.DOWN), + START_4("#FFE64D", GameBoard.BOARD_SIZE - 10, GameBoard.BOARD_SIZE - 10, DIRECTION.LEFT); public final String color; public final int x; diff --git a/game-service/src/test/java/org/libertybikes/game/core/GameBoardTest.java b/game-service/src/test/java/org/libertybikes/game/core/GameBoardTest.java new file mode 100644 index 0000000..adc5f98 --- /dev/null +++ b/game-service/src/test/java/org/libertybikes/game/core/GameBoardTest.java @@ -0,0 +1,67 @@ +/** + * + */ +package org.libertybikes.game.core; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.libertybikes.game.core.GameBoard.BOARD_SIZE; + +import org.junit.Before; +import org.junit.Test; + +public class GameBoardTest { + + GameBoard board = null; + + @Before + public void initBoard() { + board = new GameBoard(); + } + + @Test + public void testAddObstacle() { + assertTrue(board.addObstacle(new Obstacle(5, 10, 0, 0))); + verifyTaken(0, 0); + verifyTaken(4, 9); + } + + @Test + public void testAdd2Obstacles() { + assertTrue(board.addObstacle(new Obstacle(5, 10, 0, 0))); + assertTrue(board.addObstacle(new Obstacle(25, 25, BOARD_SIZE - 25, BOARD_SIZE - 25))); + + verifyTaken(0, 0); + verifyTaken(4, 9); + + verifyTaken(BOARD_SIZE - 25, BOARD_SIZE - 25); + verifyTaken(BOARD_SIZE - 1, BOARD_SIZE - 1); + } + + @Test + public void testOverlappingObstacles() { + assertTrue(board.addObstacle(new Obstacle(5, 10, 0, 0))); + assertFalse(board.addObstacle(new Obstacle(5, 10, 4, 9))); + verifyTaken(0, 0); + verifyTaken(4, 9); + verifyAvailable(5, 9); + verifyAvailable(4, 10); + verifyAvailable(5, 10); + } + + private void verifyTaken(int x, int y) { + if (board.board()[x][y] == GameBoard.SPOT_AVAILABLE) { + 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_TAKEN) { + board.dumpBoard(); + fail("Spot should be availble but it was taken: [" + x + "][" + y + "]"); + } + } + +} diff --git a/game-service/src/test/java/org/libertybikes/game/core/JsonDataTest.java b/game-service/src/test/java/org/libertybikes/game/core/JsonDataTest.java new file mode 100644 index 0000000..dd3c2fc --- /dev/null +++ b/game-service/src/test/java/org/libertybikes/game/core/JsonDataTest.java @@ -0,0 +1,81 @@ +/** + * + */ +package org.libertybikes.game.core; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import javax.json.bind.Jsonb; +import javax.json.bind.JsonbBuilder; + +import org.junit.Test; +import org.libertybikes.game.core.ClientMessage.GameEvent; + +public class JsonDataTest { + + private static final Jsonb jsonb = JsonbBuilder.create(); + + @Test + public void testPlayerJoined() { + ClientMessage playerJoined = new ClientMessage(); + playerJoined.playerJoinedId = "1234"; + assertEquals("{\"playerjoined\":\"1234\"}", jsonb.toJson(playerJoined)); + } + + @Test + public void testGameEventRequeue() { + ClientMessage gameEvent = new ClientMessage(); + gameEvent.event = GameEvent.GAME_REQUEUE; + assertEquals("{\"message\":\"GAME_REQUEUE\"}", jsonb.toJson(gameEvent)); + } + + @Test + public void testGameEventStart() { + ClientMessage gameEvent = new ClientMessage(); + gameEvent.event = GameEvent.GAME_START; + assertEquals("{\"message\":\"GAME_START\"}", jsonb.toJson(gameEvent)); + } + + @Test + public void testSpectatorJoined() { + ClientMessage msg = new ClientMessage(); + msg.isSpectator = Boolean.FALSE; + assertEquals("{\"spectatorjoined\":false}", jsonb.toJson(msg)); + + msg.isSpectator = Boolean.TRUE; + assertEquals("{\"spectatorjoined\":true}", jsonb.toJson(msg)); + } + + @Test + public void testObstacle() { + Obstacle o = new Obstacle(1, 2, 3, 4); + assertEquals("{\"height\":2,\"width\":1,\"x\":3,\"y\":4}", jsonb.toJson(o)); + } + + @Test + public void testGameBoard() { + GameBoard board = new GameBoard(); + assertEquals("{\"obstacles\":[],\"players\":[]}", jsonb.toJson(board)); + + board.addObstacle(new Obstacle(1, 2, 3, 4)); + assertEquals("{\"obstacles\":[{\"height\":2,\"width\":1,\"x\":3,\"y\":4}],\"players\":[]}", jsonb.toJson(board)); + + board.addPlayer(new Player("#1234", 10, 11, 0)); + assertEquals("{\"obstacles\":[{\"height\":2,\"width\":1,\"x\":3,\"y\":4}],\"players\":[{\"color\":\"#1234\",\"direction\":\"RIGHT\",\"isAlive\":true,\"playerNum\":0,\"status\":\"Connected\",\"x\":10,\"y\":11}]}", + jsonb.toJson(board)); + } + + @Test + public void testGameRound() { + GameRound round = new GameRound("ABCDEF"); + System.out.println(jsonb.toJson(round)); + assertContains("{\"id\":\"ABCDEF\",\"gameState\":\"OPEN\",\"board\":{", jsonb.toJson(round)); + assertContains("nextRoundId\":\"", jsonb.toJson(round)); + } + + private void assertContains(String expected, String search) { + assertTrue("Did not find '" + expected + "' inside of the string: " + search, search.contains(expected)); + } + +}