diff --git a/knockoutwhistweb/app/controllers/IngameController.scala b/knockoutwhistweb/app/controllers/IngameController.scala
index 346bb83..6f93a83 100644
--- a/knockoutwhistweb/app/controllers/IngameController.scala
+++ b/knockoutwhistweb/app/controllers/IngameController.scala
@@ -172,8 +172,7 @@ class IngameController @Inject() (
optSession.foreach(_.lock.unlock())
if (result.isSuccess) {
Ok(Json.obj(
- "status" -> "success",
- "redirectUrl" -> routes.IngameController.game(gameId).url
+ "status" -> "success"
))
} else {
val throwable = result.failed.get
@@ -198,6 +197,11 @@ class IngameController @Inject() (
"status" -> "failure",
"errorMessage" -> throwable.getMessage
))
+ case _: NotInteractableException =>
+ BadRequest(Json.obj(
+ "status" -> "failure",
+ "errorMessage" -> throwable.getMessage
+ ))
case _ =>
InternalServerError(Json.obj(
"status" -> "failure",
diff --git a/knockoutwhistweb/app/controllers/PollingController.scala b/knockoutwhistweb/app/controllers/PollingController.scala
index 73d4b11..4ed62d1 100644
--- a/knockoutwhistweb/app/controllers/PollingController.scala
+++ b/knockoutwhistweb/app/controllers/PollingController.scala
@@ -1,10 +1,11 @@
package controllers
import auth.{AuthAction, AuthenticatedRequest}
+import controllers.PollingController.{scheduler, timeoutDuration}
import de.knockoutwhist.cards.Hand
import logic.PodManager
import logic.game.{GameLobby, PollingEvents}
-import logic.game.PollingEvents.{CardPlayed, LobbyUpdate, NewRound, ReloadEvent}
+import logic.game.PollingEvents.{CardPlayed, LobbyCreation, LobbyUpdate, NewRound, ReloadEvent}
import model.sessions.UserSession
import model.users.User
import play.api.libs.json.{JsArray, JsValue, Json}
@@ -13,7 +14,12 @@ import util.WebUIUtils
import javax.inject.{Inject, Singleton}
import scala.concurrent.{ExecutionContext, Future}
-
+import java.util.concurrent.{Executors, ScheduledExecutorService, TimeUnit}
+import scala.concurrent.duration.*
+object PollingController {
+ private val scheduler: ScheduledExecutorService = Executors.newSingleThreadScheduledExecutor()
+ private val timeoutDuration = 25.seconds
+}
@Singleton
class PollingController @Inject() (
val cc: ControllerComponents,
@@ -96,25 +102,28 @@ class PollingController @Inject() (
}
}
- // --- Main Polling Action ---
def polling(gameId: String): Action[AnyContent] = authAction.async { implicit request: AuthenticatedRequest[AnyContent] =>
val playerId = request.user.id
- // 1. Safely look up the game
podManager.getGame(gameId) match {
case Some(game) =>
-
- // 2. Short-Poll Check (Check for missed events)
- if (game.getPollingState.nonEmpty) {
- val event = game.getPollingState.dequeue()
-
+ val playerEventQueue = game.getEventsOfPlayer(playerId)
+ if (playerEventQueue.nonEmpty) {
+ val event = playerEventQueue.dequeue()
Future.successful(handleEvent(event, game, game.getUserSession(playerId)))
} else {
-
val eventPromise = game.registerWaiter(playerId)
-
+ val scheduledFuture = scheduler.schedule(
+ new Runnable {
+ override def run(): Unit =
+ eventPromise.tryFailure(new java.util.concurrent.TimeoutException("Polling Timeout"))
+ },
+ timeoutDuration.toMillis,
+ TimeUnit.MILLISECONDS
+ )
eventPromise.future.map { event =>
+ scheduledFuture.cancel(false)
game.removeWaiter(playerId)
handleEvent(event, game, game.getUserSession(playerId))
}.recover {
@@ -125,7 +134,6 @@ class PollingController @Inject() (
}
case None =>
- // Game not found
Future.successful(NotFound("Game not found."))
}
}
diff --git a/knockoutwhistweb/app/logic/game/GameLobby.scala b/knockoutwhistweb/app/logic/game/GameLobby.scala
index 1cf7dfa..3f638a8 100644
--- a/knockoutwhistweb/app/logic/game/GameLobby.scala
+++ b/knockoutwhistweb/app/logic/game/GameLobby.scala
@@ -11,7 +11,7 @@ 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, LobbyUpdate, NewRound, ReloadEvent}
+import logic.game.PollingEvents.{CardPlayed, LobbyCreation, LobbyUpdate, NewRound, ReloadEvent}
import model.sessions.{InteractionType, UserSession}
import model.users.User
@@ -27,21 +27,39 @@ class GameLobby private(
val name: String,
val maxPlayers: Int
) extends EventListener {
- logic.addListener(this)
- logic.createSession()
+
+
private val users: mutable.Map[UUID, UserSession] = mutable.Map()
- private val pollingState: mutable.Queue[PollingEvents] = mutable.Queue()
+ 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]()
- waitingPromises.put(playerId, promise)
- promise
+ 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 = {
- waitingPromises.remove(playerId)
+ lock.synchronized {
+ waitingPromises.remove(playerId)
+ }
}
def addUser(user: User): UserSession = {
@@ -72,7 +90,8 @@ class GameLobby private(
}
if (event.oldState == Lobby && event.newState == InGame) {
addToQueue(ReloadEvent)
- }else {
+ return
+ } else {
addToQueue(ReloadEvent)
}
users.values.foreach(session => session.updatePlayer(event))
@@ -84,11 +103,29 @@ class GameLobby private(
}
private def addToQueue(event: PollingEvents): Unit = {
- if (waitingPromises.nonEmpty) {
- waitingPromises.values.foreach(_.success(event))
- waitingPromises.clear()
- } else {
- pollingState.enqueue(event)
+ 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))
+ }
+ }
+ }
+
+ waitingPromises.keys.foreach { playerId =>
+ val queue = eventsPerPlayer(playerId)
+ if (queue.nonEmpty) {
+ val promise = waitingPromises(playerId)
+ promise.success(queue.dequeue())
+ waitingPromises.remove(playerId)
+ }
}
}
@@ -218,8 +255,8 @@ class GameLobby private(
def getLogic: GameLogic = {
logic
}
- def getPollingState: mutable.Queue[PollingEvents] = {
- pollingState
+ 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)
diff --git a/knockoutwhistweb/app/logic/game/PollingEvents.scala b/knockoutwhistweb/app/logic/game/PollingEvents.scala
index 5e5bc8a..3fa5525 100644
--- a/knockoutwhistweb/app/logic/game/PollingEvents.scala
+++ b/knockoutwhistweb/app/logic/game/PollingEvents.scala
@@ -5,4 +5,5 @@ enum PollingEvents {
case NewRound
case ReloadEvent
case LobbyUpdate
+ case LobbyCreation
}
\ No newline at end of file
diff --git a/knockoutwhistweb/app/views/main.scala.html b/knockoutwhistweb/app/views/main.scala.html
index e1cc5d1..4d8c0b2 100644
--- a/knockoutwhistweb/app/views/main.scala.html
+++ b/knockoutwhistweb/app/views/main.scala.html
@@ -25,5 +25,6 @@
+