feat(websocket)!: Implement WebSocket connection and event handling (#82)
Co-authored-by: LQ63 <lkhermann@web.de> Reviewed-on: #82 Co-authored-by: Janis <janis.e.20@gmx.de> Co-committed-by: Janis <janis.e.20@gmx.de>
This commit is contained in:
@@ -2,66 +2,36 @@ package logic.game
|
||||
|
||||
import de.knockoutwhist.cards.{Hand, Suit}
|
||||
import de.knockoutwhist.control.GameLogic
|
||||
import de.knockoutwhist.control.GameState.{InGame, Lobby, MainMenu}
|
||||
import de.knockoutwhist.control.GameState.{Lobby, MainMenu}
|
||||
import de.knockoutwhist.control.controllerBaseImpl.sublogic.util.{MatchUtil, PlayerUtil}
|
||||
import de.knockoutwhist.events.global.tie.TieTurnEvent
|
||||
import de.knockoutwhist.events.global.{CardPlayedEvent, GameStateChangeEvent, NewTrickEvent, SessionClosed}
|
||||
import de.knockoutwhist.events.player.{PlayerEvent, ReceivedHandEvent}
|
||||
import de.knockoutwhist.events.global.{GameStateChangeEvent, SessionClosed}
|
||||
import de.knockoutwhist.events.player.PlayerEvent
|
||||
import de.knockoutwhist.player.Playertype.HUMAN
|
||||
import de.knockoutwhist.player.{AbstractPlayer, PlayerFactory}
|
||||
import de.knockoutwhist.rounds.{Match, Round, Trick}
|
||||
import de.knockoutwhist.utils.events.{EventListener, SimpleEvent}
|
||||
import exceptions.*
|
||||
import logic.game.PollingEvents.{CardPlayed, LobbyCreation, LobbyUpdate, NewRound, NewTrick, ReloadEvent}
|
||||
import logic.PodManager
|
||||
import model.sessions.{InteractionType, UserSession}
|
||||
import model.users.User
|
||||
import play.api.libs.json.{JsObject, Json}
|
||||
|
||||
import java.util.UUID
|
||||
import scala.collection.mutable
|
||||
import scala.collection.mutable.ListBuffer
|
||||
import scala.concurrent.Promise as ScalaPromise
|
||||
|
||||
class GameLobby private(
|
||||
val logic: GameLogic,
|
||||
val id: String,
|
||||
val internalId: UUID,
|
||||
val name: String,
|
||||
val maxPlayers: Int
|
||||
) extends EventListener {
|
||||
|
||||
val logic: GameLogic,
|
||||
val id: String,
|
||||
val internalId: UUID,
|
||||
val name: String,
|
||||
val maxPlayers: Int
|
||||
) extends EventListener {
|
||||
|
||||
|
||||
private val users: mutable.Map[UUID, UserSession] = mutable.Map()
|
||||
private val eventsPerPlayer: mutable.Map[UUID, mutable.Queue[PollingEvents]] = mutable.Map()
|
||||
private val waitingPromises: mutable.Map[UUID, ScalaPromise[PollingEvents]] = mutable.Map()
|
||||
private val lock = new Object
|
||||
lock.synchronized {
|
||||
logic.addListener(this)
|
||||
logic.createSession()
|
||||
}
|
||||
|
||||
|
||||
def registerWaiter(playerId: UUID): ScalaPromise[PollingEvents] = {
|
||||
val promise = ScalaPromise[PollingEvents]()
|
||||
lock.synchronized {
|
||||
val queue = eventsPerPlayer.getOrElseUpdate(playerId, mutable.Queue())
|
||||
|
||||
if (queue.nonEmpty) {
|
||||
val evt = queue.dequeue()
|
||||
promise.success(evt)
|
||||
promise
|
||||
} else {
|
||||
waitingPromises.put(playerId, promise)
|
||||
promise
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def removeWaiter(playerId: UUID): Unit = {
|
||||
lock.synchronized {
|
||||
waitingPromises.remove(playerId)
|
||||
}
|
||||
}
|
||||
logic.addListener(this)
|
||||
logic.createSession()
|
||||
|
||||
def addUser(user: User): UserSession = {
|
||||
if (users.size >= maxPlayers) throw new GameFullException("The game is full!")
|
||||
@@ -69,60 +39,32 @@ class GameLobby private(
|
||||
if (logic.getCurrentState != Lobby) throw new IllegalStateException("The game has already started!")
|
||||
val userSession = new UserSession(
|
||||
user = user,
|
||||
host = false
|
||||
host = false,
|
||||
gameLobby = this
|
||||
)
|
||||
users += (user.id -> userSession)
|
||||
addToQueue(LobbyUpdate)
|
||||
PodManager.registerUserToGame(user, id)
|
||||
//TODO : transmit Lobby Update transmitToAll()
|
||||
userSession
|
||||
}
|
||||
|
||||
override def listen(event: SimpleEvent): Unit = {
|
||||
event match {
|
||||
case event: ReceivedHandEvent =>
|
||||
addToQueue(NewRound)
|
||||
users.get(event.playerId).foreach(session => session.updatePlayer(event))
|
||||
case event: CardPlayedEvent =>
|
||||
addToQueue(CardPlayed)
|
||||
case event: TieTurnEvent =>
|
||||
addToQueue(ReloadEvent)
|
||||
users.get(event.player.id).foreach(session => session.updatePlayer(event))
|
||||
case event: PlayerEvent =>
|
||||
users.get(event.playerId).foreach(session => session.updatePlayer(event))
|
||||
case event: NewTrickEvent =>
|
||||
addToQueue(NewTrick)
|
||||
case event: GameStateChangeEvent =>
|
||||
if (event.oldState == MainMenu && event.newState == Lobby) {
|
||||
return
|
||||
}
|
||||
addToQueue(ReloadEvent)
|
||||
users.values.foreach(session => session.updatePlayer(event))
|
||||
case event: SessionClosed =>
|
||||
users.values.foreach(session => session.updatePlayer(event))
|
||||
case event: SimpleEvent =>
|
||||
users.values.foreach(session => session.updatePlayer(event))
|
||||
}
|
||||
}
|
||||
|
||||
private def addToQueue(event: PollingEvents): Unit = {
|
||||
lock.synchronized {
|
||||
users.keys.foreach { playerId =>
|
||||
val q = eventsPerPlayer.getOrElseUpdate(playerId, mutable.Queue())
|
||||
q.enqueue(event)
|
||||
}
|
||||
val waiterIds = waitingPromises.keys.toList
|
||||
waiterIds.foreach { playerId =>
|
||||
val q = eventsPerPlayer.getOrElseUpdate(playerId, mutable.Queue())
|
||||
if (q.nonEmpty) {
|
||||
val evt = q.dequeue()
|
||||
val p = waitingPromises.remove(playerId)
|
||||
p.foreach(_.success(evt))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the game if the user is the host.
|
||||
*
|
||||
* @param user the user who wants to start the game.
|
||||
*/
|
||||
def startGame(user: User): Unit = {
|
||||
@@ -149,6 +91,7 @@ class GameLobby private(
|
||||
|
||||
/**
|
||||
* Remove the user from the game lobby.
|
||||
*
|
||||
* @param user the user who wants to leave the game.
|
||||
*/
|
||||
def leaveGame(userId: UUID): Unit = {
|
||||
@@ -156,14 +99,29 @@ class GameLobby private(
|
||||
if (sessionOpt.isEmpty) {
|
||||
throw new NotInThisGameException("You are not in this game!")
|
||||
}
|
||||
if (sessionOpt.get.host) {
|
||||
logic.invoke(SessionClosed())
|
||||
users.clear()
|
||||
PodManager.removeGame(id)
|
||||
return
|
||||
}
|
||||
sessionOpt.get.websocketActor.foreach(act => act.transmitJsonToClient(Json.obj(
|
||||
"id" -> "-1",
|
||||
"event" -> "SessionClosed",
|
||||
"data" -> Json.obj(
|
||||
"reason" -> "You left the game (or got kicked)."
|
||||
)
|
||||
)))
|
||||
users.remove(userId)
|
||||
addToQueue(LobbyUpdate)
|
||||
PodManager.unregisterUserFromGame(sessionOpt.get.user)
|
||||
//TODO: transmit Lobby Update transmitToAll()
|
||||
}
|
||||
|
||||
/**
|
||||
* Play a card from the player's hand.
|
||||
*
|
||||
* @param userSession the user session of the player.
|
||||
* @param cardIndex the index of the card in the player's hand.
|
||||
* @param cardIndex the index of the card in the player's hand.
|
||||
*/
|
||||
def playCard(userSession: UserSession, cardIndex: Int): Unit = {
|
||||
val player = getPlayerInteractable(userSession, InteractionType.Card)
|
||||
@@ -179,10 +137,35 @@ class GameLobby private(
|
||||
logic.playerInputLogic.receivedCard(card)
|
||||
}
|
||||
|
||||
private def getHand(player: AbstractPlayer): Hand = {
|
||||
val handOption = player.currentHand()
|
||||
if (handOption.isEmpty) {
|
||||
throw new IllegalStateException("You have no cards!")
|
||||
}
|
||||
handOption.get
|
||||
}
|
||||
|
||||
private def getRound: Round = {
|
||||
val roundOpt = logic.getCurrentRound
|
||||
if (roundOpt.isEmpty) {
|
||||
throw new IllegalStateException("No round is currently running!")
|
||||
}
|
||||
roundOpt.get
|
||||
}
|
||||
|
||||
private def getTrick: Trick = {
|
||||
val trickOpt = logic.getCurrentTrick
|
||||
if (trickOpt.isEmpty) {
|
||||
throw new IllegalStateException("No trick is currently running!")
|
||||
}
|
||||
trickOpt.get
|
||||
}
|
||||
|
||||
/**
|
||||
* Play a card from the player's hand while in dog life or skip the round.
|
||||
*
|
||||
* @param userSession the user session of the player.
|
||||
* @param cardIndex the index of the card in the player's hand or -1 if the player wants to skip the round.
|
||||
* @param cardIndex the index of the card in the player's hand or -1 if the player wants to skip the round.
|
||||
*/
|
||||
def playDogCard(userSession: UserSession, cardIndex: Int): Unit = {
|
||||
val player = getPlayerInteractable(userSession, InteractionType.DogCard)
|
||||
@@ -204,8 +187,9 @@ class GameLobby private(
|
||||
|
||||
/**
|
||||
* Select the trump suit for the round.
|
||||
*
|
||||
* @param userSession the user session of the player.
|
||||
* @param trumpIndex the index of the trump suit.
|
||||
* @param trumpIndex the index of the trump suit.
|
||||
*/
|
||||
def selectTrump(userSession: UserSession, trumpIndex: Int): Unit = {
|
||||
val player = getPlayerInteractable(userSession, InteractionType.TrumpSuit)
|
||||
@@ -215,8 +199,21 @@ class GameLobby private(
|
||||
logic.playerInputLogic.receivedTrumpSuit(selectedTrump)
|
||||
}
|
||||
|
||||
|
||||
//-------------------
|
||||
|
||||
private def getPlayerInteractable(userSession: UserSession, iType: InteractionType): AbstractPlayer = {
|
||||
if (!userSession.lock.isHeldByCurrentThread) {
|
||||
throw new IllegalStateException("The user session is not locked!")
|
||||
}
|
||||
if (userSession.canInteract.isEmpty || userSession.canInteract.get != iType) {
|
||||
throw new NotInteractableException("You can't play a card!")
|
||||
}
|
||||
getPlayerBySession(userSession)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @param userSession
|
||||
* @param tieNumber
|
||||
*/
|
||||
@@ -239,9 +236,10 @@ class GameLobby private(
|
||||
logic.createSession()
|
||||
}
|
||||
|
||||
|
||||
//-------------------
|
||||
|
||||
def getPlayerByUser(user: User): AbstractPlayer = {
|
||||
getPlayerBySession(getUserSession(user.id))
|
||||
}
|
||||
|
||||
def getUserSession(userId: UUID): UserSession = {
|
||||
val sessionOpt = users.get(userId)
|
||||
if (sessionOpt.isEmpty) {
|
||||
@@ -250,8 +248,20 @@ class GameLobby private(
|
||||
sessionOpt.get
|
||||
}
|
||||
|
||||
def getPlayerByUser(user: User): AbstractPlayer = {
|
||||
getPlayerBySession(getUserSession(user.id))
|
||||
private def getPlayerBySession(userSession: UserSession): AbstractPlayer = {
|
||||
val playerOption = getMatch.totalplayers.find(_.id == userSession.id)
|
||||
if (playerOption.isEmpty) {
|
||||
throw new NotInThisGameException("You are not in this game!")
|
||||
}
|
||||
playerOption.get
|
||||
}
|
||||
|
||||
private def getMatch: Match = {
|
||||
val matchOpt = logic.getCurrentMatch
|
||||
if (matchOpt.isEmpty) {
|
||||
throw new IllegalStateException("No match is currently running!")
|
||||
}
|
||||
matchOpt.get
|
||||
}
|
||||
|
||||
def getPlayers: mutable.Map[UUID, UserSession] = {
|
||||
@@ -261,63 +271,17 @@ class GameLobby private(
|
||||
def getLogic: GameLogic = {
|
||||
logic
|
||||
}
|
||||
def getEventsOfPlayer(playerId: UUID): mutable.Queue[PollingEvents] = {
|
||||
eventsPerPlayer.getOrElseUpdate(playerId, mutable.Queue())
|
||||
}
|
||||
private def getPlayerBySession(userSession: UserSession): AbstractPlayer = {
|
||||
val playerOption = getMatch.totalplayers.find(_.id == userSession.id)
|
||||
if (playerOption.isEmpty) {
|
||||
throw new NotInThisGameException("You are not in this game!")
|
||||
}
|
||||
playerOption.get
|
||||
}
|
||||
|
||||
private def getPlayerInteractable(userSession: UserSession, iType: InteractionType): AbstractPlayer = {
|
||||
if (!userSession.lock.isHeldByCurrentThread) {
|
||||
throw new IllegalStateException("The user session is not locked!")
|
||||
}
|
||||
if (userSession.canInteract.isEmpty || userSession.canInteract.get != iType) {
|
||||
throw new NotInteractableException("You can't play a card!")
|
||||
}
|
||||
getPlayerBySession(userSession)
|
||||
}
|
||||
|
||||
private def getHand(player: AbstractPlayer): Hand = {
|
||||
val handOption = player.currentHand()
|
||||
if (handOption.isEmpty) {
|
||||
throw new IllegalStateException("You have no cards!")
|
||||
}
|
||||
handOption.get
|
||||
}
|
||||
|
||||
private def getMatch: Match = {
|
||||
val matchOpt = logic.getCurrentMatch
|
||||
if (matchOpt.isEmpty) {
|
||||
throw new IllegalStateException("No match is currently running!")
|
||||
}
|
||||
matchOpt.get
|
||||
}
|
||||
|
||||
private def getRound: Round = {
|
||||
val roundOpt = logic.getCurrentRound
|
||||
if (roundOpt.isEmpty) {
|
||||
throw new IllegalStateException("No round is currently running!")
|
||||
}
|
||||
roundOpt.get
|
||||
}
|
||||
|
||||
private def getTrick: Trick = {
|
||||
val trickOpt = logic.getCurrentTrick
|
||||
if (trickOpt.isEmpty) {
|
||||
throw new IllegalStateException("No trick is currently running!")
|
||||
}
|
||||
trickOpt.get
|
||||
}
|
||||
|
||||
def getUsers: Set[User] = {
|
||||
users.values.map(d => d.user).toSet
|
||||
}
|
||||
|
||||
private def transmitToAll(event: JsObject): Unit = {
|
||||
users.values.foreach(session => {
|
||||
session.websocketActor.foreach(act => act.transmitJsonToClient(event))
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object GameLobby {
|
||||
@@ -338,7 +302,8 @@ object GameLobby {
|
||||
)
|
||||
lobby.users += (host.id -> new UserSession(
|
||||
user = host,
|
||||
host = true
|
||||
host = true,
|
||||
gameLobby = lobby
|
||||
))
|
||||
lobby
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
package logic.game
|
||||
|
||||
enum PollingEvents {
|
||||
case CardPlayed
|
||||
case NewRound
|
||||
case NewTrick
|
||||
case ReloadEvent
|
||||
case LobbyUpdate
|
||||
case LobbyCreation
|
||||
}
|
||||
Reference in New Issue
Block a user