feat: implemented Character selection and relaying to lobby

This commit is contained in:
Yannik Bretschneider 2021-06-06 17:45:16 +02:00
parent 7af0fd40a1
commit 21bfac7d75

View File

@ -1,5 +1,6 @@
package uulm.teamname.marvelous.server.lobbymanager; package uulm.teamname.marvelous.server.lobbymanager;
import org.java_websocket.WebSocket;
import org.tinylog.Logger; import org.tinylog.Logger;
import uulm.teamname.marvelous.gamelibrary.Tuple; import uulm.teamname.marvelous.gamelibrary.Tuple;
import uulm.teamname.marvelous.gamelibrary.config.CharacterProperties; import uulm.teamname.marvelous.gamelibrary.config.CharacterProperties;
@ -8,10 +9,9 @@ import uulm.teamname.marvelous.gamelibrary.messages.BasicMessage;
import uulm.teamname.marvelous.gamelibrary.messages.ParticipantType; import uulm.teamname.marvelous.gamelibrary.messages.ParticipantType;
import uulm.teamname.marvelous.gamelibrary.messages.client.CharacterSelectionMessage; import uulm.teamname.marvelous.gamelibrary.messages.client.CharacterSelectionMessage;
import uulm.teamname.marvelous.gamelibrary.messages.client.RequestMessage; import uulm.teamname.marvelous.gamelibrary.messages.client.RequestMessage;
import uulm.teamname.marvelous.gamelibrary.messages.server.GameAssignmentMessage; import uulm.teamname.marvelous.gamelibrary.messages.server.*;
import uulm.teamname.marvelous.server.Server; import uulm.teamname.marvelous.server.Server;
import uulm.teamname.marvelous.server.lobby.Lobby; import uulm.teamname.marvelous.server.lobby.Lobby;
import uulm.teamname.marvelous.server.netconnector.UserManager;
import java.util.*; import java.util.*;
import java.util.concurrent.BlockingQueue; import java.util.concurrent.BlockingQueue;
@ -20,44 +20,65 @@ import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
/** /**
* A class that handles the connection to the lobby. It contains the participants inside of the lobby. * A class that handles the connection to the lobby. It contains the participants inside of the lobby. The class
* The class is meant to be used in conjecture with {@link MessageRelay}. * implements runnable and harbors a thread, but should exclusively be run from the {@link LobbyRunner} class.
*/ */
public class LobbyConnection implements Runnable { public class LobbyConnection implements Runnable {
private Lobby lobby; private Lobby lobby;
public final String gameID; public final String gameID;
private Participant player1, player2; private Participant player1, player2;
private boolean characterSelection;
/** Whether the character selection phase is reached */ /** Whether the character selection phase is active. True while active, false after done */
private boolean characterSelectionActive;
/** Whether the ingame phase is reached */
private boolean inGame; private boolean inGame;
/** A boolean set true if the thread should be closed. Basically a terminator. */
private boolean active;
private final HashSet<Participant> spectators; private final HashSet<Participant> spectators;
private final BlockingQueue<Tuple<Participant, BasicMessage>> incomingMessages; private final BlockingQueue<Tuple<Participant, BasicMessage>> incomingMessages;
private final BiConsumer<WebSocket, BasicMessage> sendMessageCallback;
private final BiConsumer<WebSocket, String> sendErrorCallback;
// TODO: FIX THIS JAVADOC // TODO: FIX THIS JAVADOC
/** Creates a new LobbyConnection */ /** Creates a new LobbyConnection */
public LobbyConnection(String gameID) { public LobbyConnection(String gameID,
BiConsumer<WebSocket, BasicMessage> sendMessageCallback,
BiConsumer<WebSocket, String> sendErrorCallback) {
this.gameID = gameID; this.gameID = gameID;
this.sendMessageCallback = sendMessageCallback;
this.sendErrorCallback = sendErrorCallback;
this.spectators = new HashSet<>(10); this.spectators = new HashSet<>(10);
this.incomingMessages = new LinkedBlockingQueue<>(); this.incomingMessages = new LinkedBlockingQueue<>();
this.characterSelection = false; this.characterSelectionActive = false;
this.inGame = false; this.inGame = false;
this.active = false;
} }
// Variables for Character Selection
Tuple<CharacterProperties[], CharacterProperties[]> selectionPossibilities;
CharacterProperties[] playerOneSelection = null;
CharacterProperties[] playerTwoSelection = null;
@Override @Override
public void run() { public void run() {
Logger.info("Starting Lobby thread for lobby '{}'", gameID); Logger.info("Starting Lobby thread for lobby '{}'", gameID);
Logger.trace("Initializing lobby...");
initializeLobby();
Logger.trace("Activating characterSelection state"); Logger.debug("Activating lobbyConnection");
this.characterSelection = true; this.active = true;
Logger.debug("Activating characterSelection state");
this.characterSelectionActive = true;
Logger.info("Starting character selection process"); Logger.info("Starting character selection process");
Logger.trace("Finding twenty-four random characters"); Logger.trace("Finding twenty-four random characters");
Tuple<CharacterProperties[], CharacterProperties[]> selectionPossibilities = selectionPossibilities = Server.getCharacterConfig().getDisjointSetsOfPropertiesOfSize(12);
Server.getCharacterConfig().getDisjointSetsOfPropertiesOfSize(12);
Logger.info("Sending GameAssignment message with random characters to players"); Logger.info("Sending GameAssignment message with random characters to players");
var gameAssignmentMessage = new GameAssignmentMessage(); var gameAssignmentMessage = new GameAssignmentMessage();
@ -65,115 +86,226 @@ public class LobbyConnection implements Runnable {
// Send to player one with characters for player one // Send to player one with characters for player one
gameAssignmentMessage.characterSelection = selectionPossibilities.item1; gameAssignmentMessage.characterSelection = selectionPossibilities.item1;
UserManager.getInstance().sendMessage(player1.getConnection(), gameAssignmentMessage); sendMessage(player1, gameAssignmentMessage);
// And send the others to player 2 // And send the others to player 2
gameAssignmentMessage.characterSelection = selectionPossibilities.item2; gameAssignmentMessage.characterSelection = selectionPossibilities.item2;
UserManager.getInstance().sendMessage(player2.getConnection(), gameAssignmentMessage); sendMessage(player2, gameAssignmentMessage);
CharacterProperties[] playerOneSelection = null; // Also, send the GeneralAssignment message to all spectators
CharacterProperties[] playerTwoSelection = null; var generalAssignmentMessage = new GeneralAssignmentMessage();
generalAssignmentMessage.gameID = gameID;
broadcastToSpectators(generalAssignmentMessage);
Logger.info("Entering GameAssignment state. Waiting for answer about selected characters from players."); Logger.info("Entering GameAssignment state. Waiting for answer about selected characters from players.");
while (characterSelection) { while (characterSelectionActive && active) {
Tuple<Participant, BasicMessage> currentMessage = null; var currentMessage = getMessageAsync(1000);
try { // TODO: Exact duplication. Maybe extract? if (!active) {
Logger.trace("Checking for messages. Currently the amount of messages is {}", Logger.info("Lobby '{}' is terminating. Exiting...", gameID);
incomingMessages.size()); return;
} else if (currentMessage == null) {
if (incomingMessages.isEmpty()) { Logger.trace("Message was null, continuing"); // TODO: remove for production
Logger.trace("LobbyConnection thread waiting for new messages...");
Thread.currentThread().wait();
Logger.trace("Lobby '{}' woken up", gameID);
}
Logger.trace("Polling incoming message queue");
currentMessage = incomingMessages.poll(100, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
Logger.warn("LobbyConnection thread got interrupted. Exception: " + e.getMessage());
}
if (currentMessage == null) {
continue; continue;
} else if (currentMessage.item2 instanceof CharacterSelectionMessage) { } else if (currentMessage.item2 instanceof CharacterSelectionMessage) {
var origin = currentMessage.item1; var origin = currentMessage.item1;
var message = (CharacterSelectionMessage) currentMessage.item2; var message = (CharacterSelectionMessage) currentMessage.item2;
if (origin.type.equals(ParticipantType.Spectator)) {
Logger.info("Spectator sent CharacterSelectionMessage. Sending error...");
UserManager.getInstance().sendError(
origin.getConnection(),
"Spectators can't select characters");
} else if (origin.type.equals(ParticipantType.PlayerOne)) {
if (playerOneSelection == null) { // TODO: Extract this. This isn't beautiful at all.
ArrayList<CharacterProperties> chosenCharacters = new ArrayList<>();
for (int i = 0; i < 12; i++) {
if (message.characters[i]) {
chosenCharacters.add(selectionPossibilities.item1[i]);
}
}
playerOneSelection = chosenCharacters.toArray(new CharacterProperties[0]);
}
Logger.info("Player 1 has selected their characters");
} else {
if (playerTwoSelection == null) {
ArrayList<CharacterProperties> chosenCharacters = new ArrayList<>();
for (int i = 0; i < 12; i++) {
if (message.characters[i]) {
chosenCharacters.add(selectionPossibilities.item2[i]);
}
}
playerTwoSelection = chosenCharacters.toArray(new CharacterProperties[0]);
}
Logger.info("Player 2 has selected their characters");
}
}
Logger.debug("CharacterSelectionMessage sent by Player '{}' during character selection phase",
origin.name);
characterSelectionActive = receiveCharacterSelection(origin, message);
Logger.trace("Sending confirmSelectionMessage to player1");
var replyMessage = new ConfirmSelectionMessage();
replyMessage.selectionComplete = !characterSelectionActive;
sendMessage(origin, replyMessage);
} else if (currentMessage.item2 instanceof RequestMessage) {
var origin = currentMessage.item1;
// var message = (RequestMessage) currentMessage.item2; this is ignored here
Logger.debug("RequestMessage sent by Player '{}' during character selection phase",
origin.name);
sendError(
currentMessage.item1,
"Requests rejected as ingame phase not reached yet");
} else {
Logger.warn("Message that isn't of type RequestMessage or CharacacterSelectionMessage" +
"received in Lobby. This is probably a bug.");
sendError(
currentMessage.item1,
"Message couldn't be processed by the lobby, as its type was invalid");
}
} }
while(inGame) { Logger.trace("End of character selection phase reached. Lobby termination is '{}'", active);
Tuple<Participant, BasicMessage> currentMessage = null; if (!active) {
try { Logger.info("Lobby '{}' is terminating. Exiting... ", gameID);
Logger.trace("Checking for messages. Currently the amount of messages is {}", return;
incomingMessages.size()); } else {
if (incomingMessages.isEmpty()) { Logger.info("Sending GameStructure message to clients as the game is starting");
Logger.trace("LobbyConnection thread waiting for new messages..."); // Building GameStructure message
Thread.currentThread().wait(); var gameStructureMessage = new GameStructureMessage();
Logger.trace("Lobby '{}' woken up", gameID); gameStructureMessage.playerOneName = player1.name;
} gameStructureMessage.playerTwoName = player2.name;
gameStructureMessage.playerOneCharacters = playerOneSelection;
gameStructureMessage.playerTwoCharacters = playerTwoSelection;
gameStructureMessage.matchconfig = Server.getPartyConfig();
gameStructureMessage.scenarioconfig = Server.getScenarioConfig();
Logger.trace("Polling incoming message queue"); // Sending GameStructure message with fitting assignment
currentMessage = incomingMessages.poll(100, TimeUnit.MILLISECONDS); gameStructureMessage.assignment = ParticipantType.PlayerOne;
sendMessage(player1, gameStructureMessage);
} catch (InterruptedException e) { gameStructureMessage.assignment = ParticipantType.PlayerTwo;
Logger.warn("LobbyConnection thread got interrupted. Exception: " + e.getMessage()); sendMessage(player2, gameStructureMessage);
}
if (currentMessage == null) { gameStructureMessage.assignment = ParticipantType.Spectator;
Logger.trace("Message was null, continuing"); broadcastToSpectators(gameStructureMessage);
}
Logger.info("Entering Ingame phase");
inGame = true;
Logger.trace("Initializing lobby...");
initializeLobby();
while (inGame && active) {
Tuple<Participant, BasicMessage> currentMessage = getMessageAsync(1000);
if (!active) {
Logger.info("Lobby '{}' is terminating. Exiting...", gameID);
return;
} else if (currentMessage == null) {
Logger.trace("Message was null, continuing"); // TODO: remove for production
continue; continue;
} else if (currentMessage.item2 instanceof CharacterSelectionMessage) { } else if (currentMessage.item2 instanceof CharacterSelectionMessage) {
receiveCharacterSelection((CharacterSelectionMessage) currentMessage.item2); var origin = currentMessage.item1;
var message = (CharacterSelectionMessage) currentMessage.item2;
Logger.debug("CharacterSelectionMessage sent by Player '{}' during ingame phase", origin.name);
receiveCharacterSelection(origin, message);
} else if (currentMessage.item2 instanceof RequestMessage) { } else if (currentMessage.item2 instanceof RequestMessage) {
receiveRequests((RequestMessage) currentMessage.item2); var origin = currentMessage.item1;
var message = (RequestMessage) currentMessage.item2;
Logger.debug("RequestMessage sent by Player '{}' during ingame phase",
origin.name);
receiveRequests(origin, message);
} else { } else {
Logger.warn("Message that isn't of type RequestMessage or CharacacterSelectionMessage" +
"received in Lobby. This is probably a bug.");
sendError(
currentMessage.item1,
"Message couldn't be processed by the lobby, as its type was invalid");
} }
} }
} }
/** Send messages to the lobbyConnection */ /**
public void receiveMessage(Participant participant, BasicMessage message) { * Tries to read the next message in the messageQueue. waits for timeoutMillis
this.incomingMessages.add(Tuple.of(participant, message)); * for a queue item
*
* @param timeoutMillis is the amount of time the method waits until continuing even though not being notified.
*/
private Tuple<Participant, BasicMessage> getMessageAsync(int timeoutMillis) {
Tuple<Participant, BasicMessage> currentMessage = null;
try {
Logger.trace("Checking for messages. Currently the amount of messages is {}",
incomingMessages.size());
Logger.trace("Polling incoming message queue");
currentMessage = incomingMessages.poll(timeoutMillis, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
Logger.warn("LobbyConnection thread got interrupted. Exception: " + e.getMessage());
}
return currentMessage;
} }
private void receiveRequests(RequestMessage message) { /** Send messages to the lobbyConnection. This is thread-safe, and meant to be called from the outside. */
// TODO: implement this public void receiveMessage(Participant origin, BasicMessage message) {
Logger.trace("Lobby '{}' received message from participant '{}'", gameID, origin.name);
this.incomingMessages.add(Tuple.of(origin, message));
} }
private void receiveCharacterSelection(CharacterSelectionMessage message) { private void receiveRequests(Participant origin, RequestMessage message) {
// TODO: Implement proper character selection System.out.println("REMOVE THIS PRINTLN: received request message " + message.toString());
}
/**
* Method executed on Character selection. Hereby the local fields {@link LobbyConnection#selectionPossibilities},
* {@link LobbyConnection#playerOneSelection} and {@link LobbyConnection#playerTwoSelection} are checked and
* populated
*
* @param origin is the participant that sent the message
* @param message is the message triggering the CharacterSelection method
* @return true if CharacterSelection is now completed, and false otherwise. If the method is called outside of the
* CharacterSelection phase, true is returned, as the characterSelection is after all done
*/
private boolean receiveCharacterSelection(Participant origin, CharacterSelectionMessage message) {
if (this.characterSelectionActive) {
if (!hasRightNumberOfTrues(message.characters, 6)) {
Logger.debug("Player chose non-six number of characters, which is invalid. Sending error");
sendError(origin, "Cannot select characters as character choices wasn't 6");
} else {
switch (origin.type) {
case PlayerOne -> {
if (playerOneSelection == null) {
playerOneSelection = getChosenCharacters(
selectionPossibilities.item1,
message.characters);
Logger.info("Player 1 has selected their characters");
} else {
Logger.debug("Player 1 tried to select characters twice, sending error");
sendError(origin,
"Cannot select characters as characters were already selected"
);
}
}
case PlayerTwo -> {
if (playerTwoSelection == null) {
Logger.trace("Checking whether there are actually 6 choices");
playerTwoSelection = getChosenCharacters(
selectionPossibilities.item1,
message.characters);
Logger.info("Player 1 has selected their characters");
} else {
Logger.debug("Player 1 tried to select characters twice, sending error");
sendError(origin,
"Cannot select characters as characters were already selected");
}
}
case Spectator -> {
Logger.info("Spectator sent CharacterSelectionMessage. Sending error...");
sendError(origin, "Spectators can't select characters");
}
}
}
} else {
Logger.debug("Participant '{}' sent a CharacterSelectionMessage outside of " +
"the CharacterSelectionPhase, sending error", origin.name);
sendError(origin, "The character selection phase is already over");
}
return !characterSelectionActive || (playerOneSelection != null && playerTwoSelection != null);
}
private CharacterProperties[] getChosenCharacters(CharacterProperties[] possibleChoices, Boolean[] choices) {
ArrayList<CharacterProperties> chosenCharacters = new ArrayList<>();
for (int i = 0; i < 12; i++) {
if (choices[i]) {
chosenCharacters.add(possibleChoices[i]);
}
}
return chosenCharacters.toArray(new CharacterProperties[0]);
}
private boolean hasRightNumberOfTrues(Boolean[] values, int expected) {
int choices = Arrays.stream(values)
.mapToInt(choice -> choice ? 1 : 0)
.sum();
if (choices == expected) {
return true;
} else {
return false;
}
} }
private void initializeLobby() { private void initializeLobby() {
@ -199,8 +331,8 @@ public class LobbyConnection implements Runnable {
} }
/** @return whether a player can join the lobby */ /** @return whether a player can join the lobby */
public boolean hasFreePlayerSpot() { public boolean isFull() {
return !(hasPlayer1() && hasPlayer2()); return hasPlayer1() && hasPlayer2();
} }
public Participant getPlayer1() { public Participant getPlayer1() {
@ -217,6 +349,7 @@ public class LobbyConnection implements Runnable {
/** /**
* Adds a new player into the player1 slot * Adds a new player into the player1 slot
*
* @param player is the websocket to be added * @param player is the websocket to be added
* @return true if added successfully, and false otherwise * @return true if added successfully, and false otherwise
*/ */
@ -232,6 +365,7 @@ public class LobbyConnection implements Runnable {
/** /**
* Adds a new player into the player1 slot * Adds a new player into the player1 slot
*
* @param player is the websocket to be added * @param player is the websocket to be added
* @return true if added successfully, and false otherwise * @return true if added successfully, and false otherwise
*/ */
@ -247,6 +381,7 @@ public class LobbyConnection implements Runnable {
/** /**
* Adds a new player into either the player1, or if full, player2 slot * Adds a new player into either the player1, or if full, player2 slot
*
* @param player is the websocket to be added * @param player is the websocket to be added
* @return true if added successfully, and false otherwise * @return true if added successfully, and false otherwise
*/ */
@ -287,35 +422,65 @@ public class LobbyConnection implements Runnable {
return player1 == participant || player2 == participant || spectators.contains(participant); return player1 == participant || player2 == participant || spectators.contains(participant);
} }
// Methods to send messages
private void sendMessage(Participant recipient, BasicMessage message) {
sendMessageCallback.accept(recipient.getConnection(), message);
}
private void broadcast(BasicMessage message) {
sendMessage(player1, message);
sendMessage(player2, message);
spectators.forEach(spectator -> sendMessage(spectator, message));
}
private void broadcastToSpectators(BasicMessage message) {
spectators.forEach(spectator -> sendMessage(spectator, message));
}
private void broadcastToAllExcept(Participant except, BasicMessage message) {
if (!except.equals(player1)) sendMessage(player1, message);
if (!except.equals(player2)) sendMessage(player2, message);
spectators.stream().filter(spectator -> !except.equals(spectator))
.forEach(spectator -> sendMessage(spectator, message));
}
private void sendError(Participant recipient, String errorMessage) {
sendErrorCallback.accept(recipient.getConnection(), errorMessage);
}
// Methods to send events // Methods to send events
public void sendEvents(Participant recipient, Event... events) { public void sendEvents(Participant recipient, Event... events) {
MessageRelay.getInstance().sendMessage(this, recipient, events); var message = new EventMessage();
message.messages = events;
sendMessage(recipient, message);
} }
public void broadcastEvents(Event... events) { public void broadcastEvents(Event... events) {
// TODO: implement var message = new EventMessage();
MessageRelay.getInstance().broadcastEvents(this, events); message.messages = events;
broadcast(message);
} }
public void broadcastToAllExcept(Participant except, Event... events) { public void broadcastToAllExcept(Participant except, Event... events) {
// TODO: implement var message = new EventMessage();
var messageRelayInstance = MessageRelay.getInstance(); message.messages = events;
if (except.type == ParticipantType.Spectator) {
spectators.stream() broadcastToAllExcept(except, message);
.filter(spectator -> !spectator.equals(except))
.forEach(spectator -> messageRelayInstance.sendMessage(this, spectator, events));
messageRelayInstance.sendMessage(this, player1, events);
messageRelayInstance.sendMessage(this, player2, events);
} else {
messageRelayInstance.sendMessage(this, except.equals(player1) ? player2 : player1, events);
spectators.forEach(spectator -> messageRelayInstance.sendMessage(this, spectator, events));
}
} }
/** Kills all connections to client, as well as the lobby */ /** Kills all connections to client, as well as the lobby. Notifying the thread has to be done separately. */
public void terminateConnection() { public void terminateConnection() {
// TODO: implement this Logger.debug("Setting termination flag for lobby '{}'", gameID);
this.active = true;
}
public boolean isActive() {
return active;
} }
@Override @Override
@ -323,12 +488,12 @@ public class LobbyConnection implements Runnable {
if (this == o) return true; if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false; if (o == null || getClass() != o.getClass()) return false;
LobbyConnection that = (LobbyConnection) o; LobbyConnection that = (LobbyConnection) o;
return characterSelection == that.characterSelection && inGame == that.inGame && Objects.equals(lobby, that.lobby) && Objects.equals(gameID, that.gameID) && Objects.equals(player1, that.player1) && Objects.equals(player2, that.player2) && Objects.equals(spectators, that.spectators) && Objects.equals(incomingMessages, that.incomingMessages); return characterSelectionActive == that.characterSelectionActive && inGame == that.inGame && Objects.equals(lobby, that.lobby) && Objects.equals(gameID, that.gameID) && Objects.equals(player1, that.player1) && Objects.equals(player2, that.player2) && Objects.equals(spectators, that.spectators) && Objects.equals(incomingMessages, that.incomingMessages);
} }
@Override @Override
public int hashCode() { public int hashCode() {
return Objects.hash(lobby, gameID, player1, player2, characterSelection, inGame, spectators, incomingMessages); return Objects.hash(lobby, gameID, player1, player2, characterSelectionActive, inGame, spectators, incomingMessages);
} }
@Override @Override
@ -338,7 +503,7 @@ public class LobbyConnection implements Runnable {
", gameID='" + gameID + '\'' + ", gameID='" + gameID + '\'' +
", player1=" + player1 + ", player1=" + player1 +
", player2=" + player2 + ", player2=" + player2 +
", characterSelection=" + characterSelection + ", characterSelection=" + characterSelectionActive +
", inGame=" + inGame + ", inGame=" + inGame +
", spectators=" + spectators + ", spectators=" + spectators +
", incomingMessages=" + incomingMessages + ", incomingMessages=" + incomingMessages +