feat(websocket)!: Implement WebSocket connection and event handling
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
package controllers
|
||||
|
||||
import auth.{AuthAction, AuthenticatedRequest}
|
||||
import de.knockoutwhist.control.GameState.{FinishedMatch, InGame, Lobby, SelectTrump, TieBreak}
|
||||
import de.knockoutwhist.control.GameState.*
|
||||
import exceptions.*
|
||||
import logic.PodManager
|
||||
import logic.game.GameLobby
|
||||
@@ -18,39 +18,11 @@ import scala.concurrent.ExecutionContext
|
||||
import scala.util.Try
|
||||
|
||||
@Singleton
|
||||
class IngameController @Inject() (
|
||||
val cc: ControllerComponents,
|
||||
val authAction: AuthAction,
|
||||
implicit val ec: ExecutionContext
|
||||
) extends AbstractController(cc) {
|
||||
|
||||
def returnInnerHTML(gameLobby: GameLobby, user: User): Html = {
|
||||
gameLobby.logic.getCurrentState match {
|
||||
case Lobby => views.html.lobby.lobby(Some(user), gameLobby)
|
||||
case InGame =>
|
||||
views.html.ingame.ingame(
|
||||
gameLobby.getPlayerByUser(user),
|
||||
gameLobby
|
||||
)
|
||||
case SelectTrump =>
|
||||
views.html.ingame.selecttrump(
|
||||
gameLobby.getPlayerByUser(user),
|
||||
gameLobby
|
||||
)
|
||||
case TieBreak =>
|
||||
views.html.ingame.tie(
|
||||
gameLobby.getPlayerByUser(user),
|
||||
gameLobby
|
||||
)
|
||||
case FinishedMatch =>
|
||||
views.html.ingame.finishedMatch(
|
||||
Some(user),
|
||||
gameLobby
|
||||
)
|
||||
case _ =>
|
||||
throw new IllegalStateException(s"Invalid game state for in-game view. GameId: ${gameLobby.id}" + s" State: ${gameLobby.logic.getCurrentState}")
|
||||
}
|
||||
}
|
||||
class IngameController @Inject()(
|
||||
val cc: ControllerComponents,
|
||||
val authAction: AuthAction,
|
||||
implicit val ec: ExecutionContext
|
||||
) extends AbstractController(cc) {
|
||||
|
||||
def game(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||
val game = PodManager.getGame(gameId)
|
||||
@@ -68,6 +40,35 @@ class IngameController @Inject() (
|
||||
Redirect(routes.MainMenuController.mainMenu())
|
||||
}
|
||||
}
|
||||
|
||||
def returnInnerHTML(gameLobby: GameLobby, user: User): Html = {
|
||||
gameLobby.logic.getCurrentState match {
|
||||
case Lobby => views.html.lobby.lobby(Some(user), gameLobby)
|
||||
case InGame =>
|
||||
views.html.ingame.ingame(
|
||||
gameLobby.getPlayerByUser(user),
|
||||
gameLobby
|
||||
)
|
||||
case SelectTrump =>
|
||||
views.html.ingame.selecttrump(
|
||||
gameLobby.getPlayerByUser(user),
|
||||
gameLobby
|
||||
)
|
||||
case TieBreak =>
|
||||
views.html.ingame.tie(
|
||||
gameLobby.getPlayerByUser(user),
|
||||
gameLobby
|
||||
)
|
||||
case FinishedMatch =>
|
||||
views.html.ingame.finishedMatch(
|
||||
Some(user),
|
||||
gameLobby
|
||||
)
|
||||
case _ =>
|
||||
throw new IllegalStateException(s"Invalid game state for in-game view. GameId: ${gameLobby.id}" + s" State: ${gameLobby.logic.getCurrentState}")
|
||||
}
|
||||
}
|
||||
|
||||
def startGame(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||
val game = PodManager.getGame(gameId)
|
||||
val result = Try {
|
||||
@@ -109,13 +110,14 @@ class IngameController @Inject() (
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def kickPlayer(gameId: String, playerToKick: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||
val game = PodManager.getGame(gameId)
|
||||
val playerToKickUUID = UUID.fromString(playerToKick)
|
||||
val result = Try {
|
||||
game.get.leaveGame(playerToKickUUID)
|
||||
}
|
||||
if(result.isSuccess) {
|
||||
if (result.isSuccess) {
|
||||
Ok(Json.obj(
|
||||
"status" -> "success",
|
||||
"redirectUrl" -> routes.IngameController.game(gameId).url
|
||||
@@ -127,6 +129,7 @@ class IngameController @Inject() (
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
def leaveGame(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||
val game = PodManager.getGame(gameId)
|
||||
val result = Try {
|
||||
@@ -135,7 +138,8 @@ class IngameController @Inject() (
|
||||
if (result.isSuccess) {
|
||||
Ok(Json.obj(
|
||||
"status" -> "success",
|
||||
"redirectUrl" -> routes.MainMenuController.mainMenu().url
|
||||
"redirectUrl" -> routes.MainMenuController.mainMenu().url,
|
||||
"content" -> views.html.mainmenu.creategame(Some(request.user)).toString
|
||||
))
|
||||
} else {
|
||||
InternalServerError(Json.obj(
|
||||
@@ -144,7 +148,7 @@ class IngameController @Inject() (
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def playCard(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => {
|
||||
val game = PodManager.getGame(gameId)
|
||||
game match {
|
||||
@@ -216,6 +220,7 @@ class IngameController @Inject() (
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def playDogCard(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => {
|
||||
val game = PodManager.getGame(gameId)
|
||||
game match {
|
||||
@@ -280,6 +285,7 @@ class IngameController @Inject() (
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def playTrump(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||
val game = PodManager.getGame(gameId)
|
||||
game match {
|
||||
@@ -332,6 +338,7 @@ class IngameController @Inject() (
|
||||
NotFound("Game not found")
|
||||
}
|
||||
}
|
||||
|
||||
def playTie(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||
val game = PodManager.getGame(gameId)
|
||||
game match {
|
||||
@@ -384,8 +391,8 @@ class IngameController @Inject() (
|
||||
NotFound("Game not found")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
def returnToLobby(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||
val game = PodManager.getGame(gameId)
|
||||
game match {
|
||||
|
||||
@@ -6,28 +6,19 @@ import play.api.routing.JavaScriptReverseRouter
|
||||
|
||||
import javax.inject.Inject
|
||||
|
||||
class JavaScriptRoutingController @Inject()(
|
||||
val controllerComponents: ControllerComponents,
|
||||
val authAction: AuthAction,
|
||||
) extends BaseController {
|
||||
class JavaScriptRoutingController @Inject()(
|
||||
val controllerComponents: ControllerComponents,
|
||||
val authAction: AuthAction,
|
||||
) extends BaseController {
|
||||
def javascriptRoutes(): Action[AnyContent] =
|
||||
Action { implicit request =>
|
||||
Ok(
|
||||
JavaScriptReverseRouter("jsRoutes")(
|
||||
Ok(
|
||||
JavaScriptReverseRouter("jsRoutes")(
|
||||
routes.javascript.MainMenuController.createGame,
|
||||
routes.javascript.MainMenuController.joinGame,
|
||||
routes.javascript.MainMenuController.navSPA,
|
||||
routes.javascript.IngameController.startGame,
|
||||
routes.javascript.IngameController.kickPlayer,
|
||||
routes.javascript.IngameController.leaveGame,
|
||||
routes.javascript.IngameController.playCard,
|
||||
routes.javascript.IngameController.playDogCard,
|
||||
routes.javascript.IngameController.playTrump,
|
||||
routes.javascript.IngameController.playTie,
|
||||
routes.javascript.IngameController.returnToLobby,
|
||||
routes.javascript.PollingController.polling,
|
||||
routes.javascript.UserController.login_Post
|
||||
)
|
||||
).as("text/javascript")
|
||||
}
|
||||
)
|
||||
).as("text/javascript")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,9 +54,9 @@ class MainMenuController @Inject()(
|
||||
"errorMessage" -> "Invalid form submission"
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
def joinGame(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||
val jsonBody = request.body.asJson
|
||||
val gameId: Option[String] = jsonBody.flatMap { jsValue =>
|
||||
@@ -90,7 +90,7 @@ class MainMenuController @Inject()(
|
||||
Ok(views.html.main("Knockout Whist - Rules")(views.html.mainmenu.rules(Some(request.user))))
|
||||
}
|
||||
|
||||
def navSPA(location: String) : Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||
def navSPA(location: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||
location match {
|
||||
case "0" => // Main Menu
|
||||
Ok(Json.obj(
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
package controllers
|
||||
|
||||
import auth.{AuthAction, AuthenticatedRequest}
|
||||
import controllers.PollingController.{scheduler, timeoutDuration}
|
||||
import de.knockoutwhist.cards.Hand
|
||||
import de.knockoutwhist.player.AbstractPlayer
|
||||
import logic.PodManager
|
||||
import logic.game.{GameLobby, PollingEvents}
|
||||
import logic.game.PollingEvents.{CardPlayed, LobbyCreation, LobbyUpdate, NewRound, NewTrick, ReloadEvent}
|
||||
import model.sessions.UserSession
|
||||
import model.users.User
|
||||
import play.api.libs.json.{JsArray, JsValue, Json}
|
||||
import play.api.mvc.{AbstractController, Action, AnyContent, ControllerComponents, Result}
|
||||
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,
|
||||
val authAction: AuthAction,
|
||||
val ingameController: IngameController,
|
||||
implicit val ec: ExecutionContext
|
||||
) extends AbstractController(cc) {
|
||||
|
||||
private def buildCardPlayResponse(game: GameLobby, hand: Option[Hand], player: AbstractPlayer, newRound: Boolean): JsValue = {
|
||||
val currentRound = game.logic.getCurrentRound.get
|
||||
val currentTrick = game.logic.getCurrentTrick.get
|
||||
|
||||
val trickCardsJson = Json.toJson(
|
||||
currentTrick.cards.map { case (card, player) =>
|
||||
Json.obj("cardId" -> WebUIUtils.cardtoString(card), "player" -> player.name)
|
||||
}
|
||||
)
|
||||
val scoreTableJson = Json.toJson(
|
||||
game.getLogic.getPlayerQueue.get.toList.map { player =>
|
||||
Json.obj(
|
||||
"name" -> player.name,
|
||||
"tricks" -> currentRound.tricklist.count(_.winner.contains(player))
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
val stringHand = hand.map { h =>
|
||||
val cardStrings = h.cards.map(WebUIUtils.cardtoString)
|
||||
Json.toJson(cardStrings).as[JsArray]
|
||||
}.getOrElse(Json.arr())
|
||||
|
||||
val firstCardId = currentTrick.firstCard.map(WebUIUtils.cardtoString).getOrElse("BLANK")
|
||||
val nextPlayer = game.getLogic.getPlayerQueue.get.duplicate().nextPlayer().name
|
||||
Json.obj(
|
||||
"status" -> "cardPlayed",
|
||||
"animation" -> newRound,
|
||||
"handData" -> stringHand,
|
||||
"dog" -> player.isInDogLife,
|
||||
"currentPlayerName" -> game.logic.getCurrentPlayer.get.name,
|
||||
"trumpSuit" -> currentRound.trumpSuit.toString,
|
||||
"trickCards" -> trickCardsJson,
|
||||
"scoreTable" -> scoreTableJson,
|
||||
"firstCardId" -> firstCardId,
|
||||
"nextPlayer" -> nextPlayer,
|
||||
"yourTurn" -> (game.logic.getCurrentPlayer.get == player)
|
||||
)
|
||||
}
|
||||
|
||||
private def buildLobbyUsersResponse(game: GameLobby, userSession: UserSession): JsValue = {
|
||||
Json.obj(
|
||||
"status" -> "lobbyUpdate",
|
||||
"host" -> userSession.host,
|
||||
"users" -> game.getUsers.map(u => Json.obj(
|
||||
"name" -> u.name,
|
||||
"id" -> u.id,
|
||||
"self" -> (u.id == userSession.id)
|
||||
)),
|
||||
"maxPlayers" -> game.maxPlayers
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
def handleEvent(event: PollingEvents, game: GameLobby, userSession: UserSession): Result = {
|
||||
event match {
|
||||
case NewRound =>
|
||||
val player = game.getPlayerByUser(userSession.user)
|
||||
val hand = player.currentHand()
|
||||
val jsonResponse = buildCardPlayResponse(game, hand, player, true)
|
||||
Ok(jsonResponse)
|
||||
case NewTrick =>
|
||||
val player = game.getPlayerByUser(userSession.user)
|
||||
val hand = player.currentHand()
|
||||
val jsonResponse = buildCardPlayResponse(game, hand, player, false)
|
||||
Ok(jsonResponse)
|
||||
case CardPlayed =>
|
||||
val player = game.getPlayerByUser(userSession.user)
|
||||
val hand = player.currentHand()
|
||||
val jsonResponse = buildCardPlayResponse(game, hand, player, false)
|
||||
Ok(jsonResponse)
|
||||
case LobbyUpdate =>
|
||||
Ok(buildLobbyUsersResponse(game, userSession))
|
||||
case ReloadEvent =>
|
||||
val jsonResponse = Json.obj(
|
||||
"status" -> "reloadEvent",
|
||||
"redirectUrl" -> routes.IngameController.game(game.id).url,
|
||||
"content" -> ingameController.returnInnerHTML(game, userSession.user).toString
|
||||
)
|
||||
Ok(jsonResponse)
|
||||
}
|
||||
}
|
||||
|
||||
def polling(gameId: String): Action[AnyContent] = authAction.async { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||
|
||||
val playerId = request.user.id
|
||||
|
||||
PodManager.getGame(gameId) match {
|
||||
case Some(game) =>
|
||||
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 {
|
||||
case _: Throwable =>
|
||||
game.removeWaiter(playerId)
|
||||
NoContent
|
||||
}
|
||||
}
|
||||
|
||||
case None =>
|
||||
Future.successful(NotFound("Game not found."))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -20,13 +20,6 @@ class WebsocketController @Inject()(
|
||||
val sessionManger: SessionManager,
|
||||
)(implicit system: ActorSystem, mat: Materializer) extends AbstractController(cc) {
|
||||
|
||||
object KnockOutWebSocketActorFactory {
|
||||
def create(out: ActorRef, userSession: UserSession): Props = {
|
||||
Props(new UserWebsocketActor(out, userSession))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def socket(): WebSocket = WebSocket.accept[String, String] { request =>
|
||||
val session = request.cookies.get("sessionId")
|
||||
if (session.isEmpty) throw new Exception("No session cookie found")
|
||||
@@ -42,5 +35,11 @@ class WebsocketController @Inject()(
|
||||
}
|
||||
}
|
||||
|
||||
object KnockOutWebSocketActorFactory {
|
||||
def create(out: ActorRef, userSession: UserSession): Props = {
|
||||
Props(new UserWebsocketActor(out, userSession))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user