feat(ci): Polling Added polling for when the game starts and a card gets played (#58)
Co-authored-by: LQ63 <lkhermann@web.de> Reviewed-on: #58
This commit is contained in:
@@ -25,6 +25,8 @@
|
||||
|
||||
.game-field-background {
|
||||
background-image: @background-image;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
min-height: 100vh;
|
||||
@@ -83,6 +85,10 @@ body {
|
||||
box-shadow: 0 1px 15px 0 #000000
|
||||
}
|
||||
|
||||
.ingame-side-shadow {
|
||||
box-shadow: 0 1px 15px 0 #000000
|
||||
}
|
||||
|
||||
#sessions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -117,45 +123,21 @@ body {
|
||||
font-size: 40px;
|
||||
font-family: Arial, serif;
|
||||
}
|
||||
#playercards {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
height: 20%;
|
||||
img {
|
||||
animation: slideIn 0.5s ease-out forwards;
|
||||
animation-fill-mode: backwards;
|
||||
&:nth-child(1) { animation-delay: 0.5s; }
|
||||
&:nth-child(2) { animation-delay: 1s; }
|
||||
&:nth-child(3) { animation-delay: 1.5s; }
|
||||
&:nth-child(4) { animation-delay: 2s; }
|
||||
&:nth-child(5) { animation-delay: 2.5s; }
|
||||
&:nth-child(6) { animation-delay: 3s; }
|
||||
&:nth-child(7) { animation-delay: 3.5s; }
|
||||
}
|
||||
}
|
||||
#card-slide {
|
||||
div {
|
||||
animation: slideIn 0.5s ease-out forwards;
|
||||
animation-fill-mode: backwards;
|
||||
&:nth-child(1) { animation-delay: 0.5s; }
|
||||
&:nth-child(2) { animation-delay: 1s; }
|
||||
&:nth-child(3) { animation-delay: 1.5s; }
|
||||
&:nth-child(4) { animation-delay: 2s; }
|
||||
&:nth-child(5) { animation-delay: 2.5s; }
|
||||
&:nth-child(6) { animation-delay: 3s; }
|
||||
&:nth-child(7) { animation-delay: 3.5s; }
|
||||
}
|
||||
|
||||
|
||||
.ingame-cards-slide {
|
||||
div {
|
||||
animation: slideIn 0.5s ease-out forwards;
|
||||
animation-fill-mode: backwards;
|
||||
&:nth-child(1) { animation-delay: 0.5s; }
|
||||
&:nth-child(2) { animation-delay: 1s; }
|
||||
&:nth-child(3) { animation-delay: 1.5s; }
|
||||
&:nth-child(4) { animation-delay: 2s; }
|
||||
&:nth-child(5) { animation-delay: 2.5s; }
|
||||
&:nth-child(6) { animation-delay: 3s; }
|
||||
&:nth-child(7) { animation-delay: 3.5s; }
|
||||
}
|
||||
}
|
||||
|
||||
#cardsplayed {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 10%;
|
||||
min-height: 10%
|
||||
}
|
||||
#playedcardplayer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -1,32 +1,27 @@
|
||||
package controllers
|
||||
|
||||
import auth.{AuthAction, AuthenticatedRequest}
|
||||
import de.knockoutwhist.cards.Hand
|
||||
import de.knockoutwhist.control.GameState.{InGame, Lobby, SelectTrump, TieBreak}
|
||||
import exceptions.{CantPlayCardException, GameFullException, NotEnoughPlayersException, NotHostException, NotInThisGameException}
|
||||
import exceptions.*
|
||||
import logic.PodManager
|
||||
import logic.game.PollingEvents.CardPlayed
|
||||
import logic.game.PollingEvents.GameStarted
|
||||
import logic.game.{GameLobby, PollingEvents}
|
||||
import model.sessions.{PlayerSession, UserSession}
|
||||
import model.users.User
|
||||
import logic.game.GameLobby
|
||||
import model.sessions.UserSession
|
||||
import play.api.*
|
||||
import play.api.libs.json.{JsArray, JsValue, Json}
|
||||
import play.api.libs.json.{JsValue, Json}
|
||||
import play.api.mvc.*
|
||||
import util.WebUIUtils
|
||||
|
||||
import java.util.UUID
|
||||
import javax.inject.*
|
||||
import scala.concurrent.Future
|
||||
import scala.util.Try
|
||||
import scala.concurrent.ExecutionContext
|
||||
@Singleton
|
||||
class IngameController @Inject()(
|
||||
val controllerComponents: ControllerComponents,
|
||||
val authAction: AuthAction,
|
||||
val podManager: PodManager
|
||||
) extends BaseController {
|
||||
import scala.util.Try
|
||||
|
||||
@Singleton
|
||||
class IngameController @Inject() (
|
||||
val cc: ControllerComponents,
|
||||
val podManager: PodManager,
|
||||
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)
|
||||
game match {
|
||||
@@ -41,12 +36,12 @@ class IngameController @Inject()(
|
||||
case SelectTrump =>
|
||||
Ok(views.html.ingame.selecttrump(
|
||||
g.getPlayerByUser(request.user),
|
||||
g.logic
|
||||
g
|
||||
))
|
||||
case TieBreak =>
|
||||
Ok(views.html.ingame.tie(
|
||||
g.getPlayerByUser(request.user),
|
||||
g.logic
|
||||
g
|
||||
))
|
||||
case _ =>
|
||||
InternalServerError(s"Invalid game state for in-game view. GameId: $gameId" + s" State: ${g.logic.getCurrentState}")
|
||||
@@ -54,7 +49,6 @@ class IngameController @Inject()(
|
||||
case None =>
|
||||
NotFound("Game not found")
|
||||
}
|
||||
//NotFound(s"Reached end of game method unexpectedly. GameId: $gameId")
|
||||
}
|
||||
def startGame(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||
val game = podManager.getGame(gameId)
|
||||
@@ -67,30 +61,70 @@ class IngameController @Inject()(
|
||||
}
|
||||
}
|
||||
if (result.isSuccess) {
|
||||
Redirect(routes.IngameController.game(gameId))
|
||||
Ok(Json.obj(
|
||||
"status" -> "success",
|
||||
"redirectUrl" -> routes.IngameController.game(gameId).url
|
||||
))
|
||||
} else {
|
||||
val throwable = result.failed.get
|
||||
throwable match {
|
||||
case _: NotInThisGameException =>
|
||||
BadRequest(throwable.getMessage)
|
||||
BadRequest(Json.obj(
|
||||
"status" -> "failure",
|
||||
"errorMessage" -> throwable.getMessage
|
||||
))
|
||||
case _: NotHostException =>
|
||||
Forbidden(throwable.getMessage)
|
||||
Forbidden(Json.obj(
|
||||
"status" -> "failure",
|
||||
"errorMessage" -> throwable.getMessage
|
||||
))
|
||||
case _: NotEnoughPlayersException =>
|
||||
BadRequest(throwable.getMessage)
|
||||
BadRequest(Json.obj(
|
||||
"status" -> "failure",
|
||||
"errorMessage" -> throwable.getMessage
|
||||
))
|
||||
case _ =>
|
||||
InternalServerError(throwable.getMessage)
|
||||
InternalServerError(Json.obj(
|
||||
"status" -> "failure",
|
||||
"errorMessage" -> throwable.getMessage
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
def kickPlayer(gameId: String, playerToKick: UUID): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||
def kickPlayer(gameId: String, playerToKick: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||
val game = podManager.getGame(gameId)
|
||||
game.get.leaveGame(playerToKick)
|
||||
Redirect(routes.IngameController.game(gameId))
|
||||
val playerToKickUUID = UUID.fromString(playerToKick)
|
||||
val result = Try {
|
||||
game.get.leaveGame(playerToKickUUID)
|
||||
}
|
||||
if(result.isSuccess) {
|
||||
Ok(Json.obj(
|
||||
"status" -> "success",
|
||||
"redirectUrl" -> routes.IngameController.game(gameId).url
|
||||
))
|
||||
} else {
|
||||
InternalServerError(Json.obj(
|
||||
"status" -> "failure",
|
||||
"errorMessage" -> "Something went wrong."
|
||||
))
|
||||
}
|
||||
}
|
||||
def leaveGame(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||
val game = podManager.getGame(gameId)
|
||||
game.get.leaveGame(request.user.id)
|
||||
Redirect(routes.MainMenuController.mainMenu())
|
||||
val result = Try {
|
||||
game.get.leaveGame(request.user.id)
|
||||
}
|
||||
if (result.isSuccess) {
|
||||
Ok(Json.obj(
|
||||
"status" -> "success",
|
||||
"redirectUrl" -> routes.MainMenuController.mainMenu().url
|
||||
))
|
||||
} else {
|
||||
InternalServerError(Json.obj(
|
||||
"status" -> "failure",
|
||||
"errorMessage" -> "Something went wrong."
|
||||
))
|
||||
}
|
||||
}
|
||||
def joinGame(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||
val game = podManager.getGame(gameId)
|
||||
@@ -122,7 +156,10 @@ class IngameController @Inject()(
|
||||
val game = podManager.getGame(gameId)
|
||||
game match {
|
||||
case Some(g) =>
|
||||
val cardIdOpt = request.body.asFormUrlEncoded.flatMap(_.get("cardId").flatMap(_.headOption))
|
||||
val jsonBody = request.body.asJson
|
||||
val cardIdOpt: Option[String] = jsonBody.flatMap { jsValue =>
|
||||
(jsValue \ "cardID").asOpt[String]
|
||||
}
|
||||
cardIdOpt match {
|
||||
case Some(cardId) =>
|
||||
var optSession: Option[UserSession] = None
|
||||
@@ -134,27 +171,51 @@ class IngameController @Inject()(
|
||||
}
|
||||
optSession.foreach(_.lock.unlock())
|
||||
if (result.isSuccess) {
|
||||
NoContent
|
||||
Ok(Json.obj(
|
||||
"status" -> "success",
|
||||
"redirectUrl" -> routes.IngameController.game(gameId).url
|
||||
))
|
||||
} else {
|
||||
val throwable = result.failed.get
|
||||
throwable match {
|
||||
case _: CantPlayCardException =>
|
||||
BadRequest(throwable.getMessage)
|
||||
BadRequest(Json.obj(
|
||||
"status" -> "failure",
|
||||
"errorMessage" -> throwable.getMessage
|
||||
))
|
||||
case _: NotInThisGameException =>
|
||||
BadRequest(throwable.getMessage)
|
||||
BadRequest(Json.obj(
|
||||
"status" -> "failure",
|
||||
"errorMessage" -> throwable.getMessage
|
||||
))
|
||||
case _: IllegalArgumentException =>
|
||||
BadRequest(throwable.getMessage)
|
||||
BadRequest(Json.obj(
|
||||
"status" -> "failure",
|
||||
"errorMessage" -> throwable.getMessage
|
||||
))
|
||||
case _: IllegalStateException =>
|
||||
BadRequest(throwable.getMessage)
|
||||
BadRequest(Json.obj(
|
||||
"status" -> "failure",
|
||||
"errorMessage" -> throwable.getMessage
|
||||
))
|
||||
case _ =>
|
||||
InternalServerError(throwable.getMessage)
|
||||
InternalServerError(Json.obj(
|
||||
"status" -> "failure",
|
||||
"errorMessage" -> throwable.getMessage
|
||||
))
|
||||
}
|
||||
}
|
||||
case None =>
|
||||
BadRequest("cardId parameter is missing")
|
||||
BadRequest(Json.obj(
|
||||
"status" -> "failure",
|
||||
"errorMessage" -> "cardId Parameter is missing"
|
||||
))
|
||||
}
|
||||
case None =>
|
||||
NotFound("Game not found")
|
||||
NotFound(Json.obj(
|
||||
"status" -> "failure",
|
||||
"errorMessage" -> "Game not found"
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -253,7 +314,7 @@ class IngameController @Inject()(
|
||||
val session = g.getUserSession(request.user.id)
|
||||
optSession = Some(session)
|
||||
session.lock.lock()
|
||||
g.selectTie(g.getUserSession(request.user.id), tie.toInt - 1)
|
||||
g.selectTie(g.getUserSession(request.user.id), tie.toInt)
|
||||
}
|
||||
optSession.foreach(_.lock.unlock())
|
||||
if (result.isSuccess) {
|
||||
|
||||
@@ -20,7 +20,7 @@ class JavaScriptRoutingController @Inject()(
|
||||
routes.javascript.IngameController.kickPlayer,
|
||||
routes.javascript.IngameController.leaveGame,
|
||||
routes.javascript.IngameController.playCard,
|
||||
routes.javascript.IngameController.polling
|
||||
routes.javascript.PollingController.polling
|
||||
)
|
||||
).as("text/javascript")
|
||||
}
|
||||
|
||||
134
knockoutwhistweb/app/controllers/PollingController.scala
Normal file
134
knockoutwhistweb/app/controllers/PollingController.scala
Normal file
@@ -0,0 +1,134 @@
|
||||
package controllers
|
||||
|
||||
import auth.{AuthAction, AuthenticatedRequest}
|
||||
import de.knockoutwhist.cards.Hand
|
||||
import logic.PodManager
|
||||
import logic.game.{GameLobby, PollingEvents}
|
||||
import logic.game.PollingEvents.{CardPlayed, LobbyUpdate, NewRound, 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}
|
||||
|
||||
@Singleton
|
||||
class PollingController @Inject() (
|
||||
val cc: ControllerComponents,
|
||||
val podManager: PodManager,
|
||||
val authAction: AuthAction,
|
||||
implicit val ec: ExecutionContext
|
||||
) extends AbstractController(cc) {
|
||||
|
||||
private def buildCardPlayResponse(game: GameLobby, hand: Option[Hand], 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,
|
||||
"currentPlayerName" -> game.logic.getCurrentPlayer.get.name,
|
||||
"trumpSuit" -> currentRound.trumpSuit.toString,
|
||||
"trickCards" -> trickCardsJson,
|
||||
"scoreTable" -> scoreTableJson,
|
||||
"firstCardId" -> firstCardId,
|
||||
"nextPlayer" -> nextPlayer
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
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, true)
|
||||
Ok(jsonResponse)
|
||||
case CardPlayed =>
|
||||
val player = game.getPlayerByUser(userSession.user)
|
||||
val hand = player.currentHand()
|
||||
val jsonResponse = buildCardPlayResponse(game, hand, false)
|
||||
Ok(jsonResponse)
|
||||
case LobbyUpdate =>
|
||||
Ok(buildLobbyUsersResponse(game, userSession))
|
||||
case ReloadEvent =>
|
||||
val jsonResponse = Json.obj(
|
||||
"status" -> "reloadEvent",
|
||||
"redirectUrl" -> routes.IngameController.game(game.id).url
|
||||
)
|
||||
Ok(jsonResponse)
|
||||
}
|
||||
}
|
||||
|
||||
// --- 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()
|
||||
|
||||
Future.successful(handleEvent(event, game, game.getUserSession(playerId)))
|
||||
} else {
|
||||
|
||||
val eventPromise = game.registerWaiter(playerId)
|
||||
|
||||
eventPromise.future.map { event =>
|
||||
game.removeWaiter(playerId)
|
||||
handleEvent(event, game, game.getUserSession(playerId))
|
||||
}.recover {
|
||||
case _: Throwable =>
|
||||
game.removeWaiter(playerId)
|
||||
NoContent
|
||||
}
|
||||
}
|
||||
|
||||
case None =>
|
||||
// Game not found
|
||||
Future.successful(NotFound("Game not found."))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -5,20 +5,20 @@ import de.knockoutwhist.control.GameLogic
|
||||
import de.knockoutwhist.control.GameState.{InGame, Lobby, MainMenu}
|
||||
import de.knockoutwhist.control.controllerBaseImpl.sublogic.util.{MatchUtil, PlayerUtil}
|
||||
import de.knockoutwhist.events.global.{CardPlayedEvent, GameStateChangeEvent, SessionClosed}
|
||||
import de.knockoutwhist.events.player.PlayerEvent
|
||||
import de.knockoutwhist.events.player.{PlayerEvent, ReceivedHandEvent}
|
||||
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, GameStarted}
|
||||
import logic.game.PollingEvents.{CardPlayed, LobbyUpdate, NewRound, ReloadEvent}
|
||||
import model.sessions.{InteractionType, UserSession}
|
||||
import model.users.User
|
||||
|
||||
import java.util.UUID
|
||||
import scala.collection.mutable
|
||||
import scala.collection.mutable.ListBuffer
|
||||
import scala.concurrent.{Promise => ScalaPromise}
|
||||
import scala.concurrent.Promise as ScalaPromise
|
||||
|
||||
class GameLobby private(
|
||||
val logic: GameLogic,
|
||||
@@ -31,7 +31,7 @@ class GameLobby private(
|
||||
logic.createSession()
|
||||
|
||||
private val users: mutable.Map[UUID, UserSession] = mutable.Map()
|
||||
private var pollingState: mutable.Queue[PollingEvents] = mutable.Queue()
|
||||
private val pollingState: mutable.Queue[PollingEvents] = mutable.Queue()
|
||||
private val waitingPromises: mutable.Map[UUID, ScalaPromise[PollingEvents]] = mutable.Map()
|
||||
|
||||
def registerWaiter(playerId: UUID): ScalaPromise[PollingEvents] = {
|
||||
@@ -53,19 +53,17 @@ class GameLobby private(
|
||||
host = false
|
||||
)
|
||||
users += (user.id -> userSession)
|
||||
addToQueue(LobbyUpdate)
|
||||
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 =>
|
||||
val newEvent = PollingEvents.CardPlayed
|
||||
if (waitingPromises.nonEmpty) {
|
||||
waitingPromises.values.foreach(_.success(newEvent))
|
||||
waitingPromises.clear()
|
||||
} else {
|
||||
pollingState.enqueue(newEvent)
|
||||
}
|
||||
addToQueue(CardPlayed)
|
||||
case event: PlayerEvent =>
|
||||
users.get(event.playerId).foreach(session => session.updatePlayer(event))
|
||||
case event: GameStateChangeEvent =>
|
||||
@@ -73,13 +71,9 @@ class GameLobby private(
|
||||
return
|
||||
}
|
||||
if (event.oldState == Lobby && event.newState == InGame) {
|
||||
val newEvent = PollingEvents.GameStarted
|
||||
if (waitingPromises.nonEmpty) {
|
||||
waitingPromises.values.foreach(_.success(newEvent))
|
||||
waitingPromises.clear()
|
||||
} else {
|
||||
pollingState.enqueue(newEvent)
|
||||
}
|
||||
addToQueue(ReloadEvent)
|
||||
}else {
|
||||
addToQueue(ReloadEvent)
|
||||
}
|
||||
users.values.foreach(session => session.updatePlayer(event))
|
||||
case event: SessionClosed =>
|
||||
@@ -89,6 +83,15 @@ class GameLobby private(
|
||||
}
|
||||
}
|
||||
|
||||
private def addToQueue(event: PollingEvents): Unit = {
|
||||
if (waitingPromises.nonEmpty) {
|
||||
waitingPromises.values.foreach(_.success(event))
|
||||
waitingPromises.clear()
|
||||
} else {
|
||||
pollingState.enqueue(event)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the game if the user is the host.
|
||||
* @param user the user who wants to start the game.
|
||||
@@ -125,6 +128,7 @@ class GameLobby private(
|
||||
throw new NotInThisGameException("You are not in this game!")
|
||||
}
|
||||
users.remove(userId)
|
||||
addToQueue(LobbyUpdate)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -188,9 +192,6 @@ class GameLobby private(
|
||||
*/
|
||||
def selectTie(userSession: UserSession, tieNumber: Int): Unit = {
|
||||
val player = getPlayerInteractable(userSession, InteractionType.TieChoice)
|
||||
val highestNumber = logic.playerTieLogic.highestAllowedNumber()
|
||||
if (tieNumber < 0 || tieNumber > highestNumber)
|
||||
throw new IllegalArgumentException(s"Selected number $tieNumber is out of allowed range (0 to $highestNumber)")
|
||||
userSession.resetCanInteract()
|
||||
logic.playerTieLogic.receivedTieBreakerCard(tieNumber)
|
||||
}
|
||||
@@ -269,7 +270,11 @@ class GameLobby private(
|
||||
}
|
||||
trickOpt.get
|
||||
}
|
||||
|
||||
|
||||
def getUsers: Set[User] = {
|
||||
users.values.map(d => d.user).toSet
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object GameLobby {
|
||||
|
||||
@@ -2,5 +2,7 @@ package logic.game
|
||||
|
||||
enum PollingEvents {
|
||||
case CardPlayed
|
||||
case GameStarted
|
||||
case NewRound
|
||||
case ReloadEvent
|
||||
case LobbyUpdate
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import model.users.User
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.locks.{Lock, ReentrantLock}
|
||||
|
||||
class UserSession(user: User, val host: Boolean) extends PlayerSession {
|
||||
class UserSession(val user: User, val host: Boolean) extends PlayerSession {
|
||||
var canInteract: Option[InteractionType] = None
|
||||
val lock: ReentrantLock = ReentrantLock()
|
||||
|
||||
|
||||
@@ -8,6 +8,10 @@ import scalafx.scene.image.Image
|
||||
|
||||
object WebUIUtils {
|
||||
def cardtoImage(card: Card): Html = {
|
||||
views.html.render.card.apply(f"images/cards/${cardtoString(card)}.png")(card.toString)
|
||||
}
|
||||
|
||||
def cardtoString(card: Card) = {
|
||||
val s = card.suit match {
|
||||
case Spades => "S"
|
||||
case Hearts => "H"
|
||||
@@ -29,6 +33,7 @@ object WebUIUtils {
|
||||
case Three => "3"
|
||||
case Two => "2"
|
||||
}
|
||||
views.html.render.card.apply(f"images/cards/$cv$s.png")(card.toString)
|
||||
f"$cv$s"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
@main("Ingame") {
|
||||
<div class="lobby-background vh-100">
|
||||
<main class="game-field-background vh-100">
|
||||
<main class="game-field-background vh-100 ingame-side-shadow">
|
||||
<div class="py-5 container-xxl">
|
||||
|
||||
<div class="row ms-4 me-4">
|
||||
@@ -72,7 +72,7 @@
|
||||
</div>
|
||||
|
||||
<div class="row justify-content-center g-2 mt-4 bottom-div" style="backdrop-filter: blur(4px); margin-left: 0; margin-right: 0;">
|
||||
<div class="row justify-content-center" id="card-slide">
|
||||
<div class="row justify-content-center ingame-cards-slide" id="card-slide">
|
||||
@for(i <- player.currentHand().get.cards.indices) {
|
||||
<div class="col-auto handcard" style="border-radius: 6px">
|
||||
<div class="btn btn-outline-light p-0 border-0 shadow-none" data-card-id="@i" style="border-radius: 6px" onclick="handlePlayCard(this, '@gamelobby.id')">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@(player: de.knockoutwhist.player.AbstractPlayer, logic: de.knockoutwhist.control.GameLogic, gameId: String)
|
||||
@(player: de.knockoutwhist.player.AbstractPlayer, gamelobby: logic.game.GameLobby)
|
||||
|
||||
@main("Selecting Trumpsuit...") {
|
||||
<div id="selecttrumpsuit" class="game-field game-field-background">
|
||||
@@ -11,14 +11,14 @@
|
||||
<h3 class="mb-0">Select Trump Suit</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@if(player.equals(logic.getCurrentMatch.get.roundlist.last.winner.get)) {
|
||||
@if(player.equals(gamelobby.logic.getCurrentMatch.get.roundlist.last.winner.get)) {
|
||||
<div class="alert alert-info" role="alert" aria-live="polite">
|
||||
You (@player.toString) won the last round. Choose the trump suit for the next round.
|
||||
</div>
|
||||
|
||||
<div class="row justify-content-center col-auto mb-5">
|
||||
<div class="col-auto handcard">
|
||||
<form action="@routes.IngameController.playTrump(gameId)" method="post">
|
||||
<form action="@routes.IngameController.playTrump(gamelobby.id)" method="post">
|
||||
<input type="hidden" name="cardId" value="0" />
|
||||
<button type="submit" class="btn btn-outline-light p-0 border-0 shadow-none" name="trump" value="0" style="border-radius: 6px">
|
||||
@util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Spades)) width="120px" style="border-radius: 6px"/>
|
||||
@@ -26,7 +26,7 @@
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-auto handcard">
|
||||
<form action="@routes.IngameController.playTrump(gameId)" method="post">
|
||||
<form action="@routes.IngameController.playTrump(gamelobby.id)" method="post">
|
||||
<input type="hidden" name="cardId" value="1" />
|
||||
<button type="submit" class="btn btn-outline-light p-0 border-0 shadow-none" name="trump" value="1" style="border-radius: 6px">
|
||||
@util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Hearts)) width="120px" style="border-radius: 6px"/>
|
||||
@@ -34,7 +34,7 @@
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-auto handcard">
|
||||
<form action="@routes.IngameController.playTrump(gameId)" method="post">
|
||||
<form action="@routes.IngameController.playTrump(gamelobby.id)" method="post">
|
||||
<input type="hidden" name="cardId" value="2" />
|
||||
<button type="submit" class="btn btn-outline-light p-0 border-0 shadow-none" name="trump" value="2" style="border-radius: 6px">
|
||||
@util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Diamonds)) width="120px" style="border-radius: 6px"/>
|
||||
@@ -42,7 +42,7 @@
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-auto handcard">
|
||||
<form action="@routes.IngameController.playTrump(gameId)" method="post">
|
||||
<form action="@routes.IngameController.playTrump(gamelobby.id)" method="post">
|
||||
<input type="hidden" name="cardId" value="3" />
|
||||
<button type="submit" class="btn btn-outline-light p-0 border-0 shadow-none" name="trump" value="3" style="border-radius: 6px">
|
||||
@util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Clubs)) width="120px" style="border-radius: 6px"/>
|
||||
@@ -50,7 +50,7 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row justify-content-center" id="card-slide">
|
||||
<div class="row justify-content-center ingame-cards-slide" id="card-slide">
|
||||
@for(i <- player.currentHand().get.cards.indices) {
|
||||
<div class="col-auto" style="border-radius: 6px">
|
||||
@util.WebUIUtils.cardtoImage(player.currentHand().get.cards(i)) width="120px" style="border-radius: 6px"/>
|
||||
@@ -59,7 +59,7 @@
|
||||
</div>
|
||||
} else {
|
||||
<div class="alert alert-warning" role="alert" aria-live="polite">
|
||||
@logic.getCurrentMatch.get.roundlist.last.winner.get.name is choosing a trumpsuit. The new round will start once a suit is picked.
|
||||
@gamelobby.logic.getCurrentMatch.get.roundlist.last.winner.get.name is choosing a trumpsuit. The new round will start once a suit is picked.
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@(player: de.knockoutwhist.player.AbstractPlayer, logic: de.knockoutwhist.control.GameLogic, gameId: String)
|
||||
@(player: de.knockoutwhist.player.AbstractPlayer, gamelobby: logic.game.GameLobby)
|
||||
|
||||
@main("Tie") {
|
||||
<div id="tie" class="game-field game-field-background">
|
||||
@@ -15,20 +15,20 @@
|
||||
<p class="card-text">
|
||||
The last round was tied between:
|
||||
<span class="ms-1">
|
||||
@for(players <- logic.playerTieLogic.getTiedPlayers) {
|
||||
@for(players <- gamelobby.logic.playerTieLogic.getTiedPlayers) {
|
||||
<span class="badge text-bg-secondary me-1">@players</span>
|
||||
}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@if(player.equals(logic.playerTieLogic.currentTiePlayer())) {
|
||||
@defining(logic.playerTieLogic.highestAllowedNumber()) { maxNum =>
|
||||
@if(player.equals(gamelobby.logic.playerTieLogic.currentTiePlayer())) {
|
||||
@defining(gamelobby.logic.playerTieLogic.highestAllowedNumber()) { maxNum =>
|
||||
<div class="alert alert-info" role="alert" aria-live="polite">
|
||||
Pick a number between 1 and @{maxNum + 1}. The resulting card will be your card for the cut.
|
||||
</div>
|
||||
|
||||
<form class="row g-2 align-items-center" method="post" action="@routes.IngameController.playTie(gameId)">
|
||||
<form class="row g-2 align-items-center" method="post" action="@routes.IngameController.playTie(gamelobby.id)">
|
||||
<div class="col-auto">
|
||||
<label for="tieNumber" class="col-form-label">Your number</label>
|
||||
</div>
|
||||
@@ -42,8 +42,8 @@
|
||||
<h6 class="mt-4 mb-3">Currently Picked Cards</h6>
|
||||
|
||||
<div id="cardsplayed" class="row g-3 justify-content-center">
|
||||
@if(logic.playerTieLogic.getSelectedCard.nonEmpty) {
|
||||
@for((player, card) <- logic.playerTieLogic.getSelectedCard) {
|
||||
@if(gamelobby.logic.playerTieLogic.getSelectedCard.nonEmpty) {
|
||||
@for((player, card) <- gamelobby.logic.playerTieLogic.getSelectedCard) {
|
||||
<div class="col-6">
|
||||
<div class="card shadow-sm border-0 h-100 text-center">
|
||||
<div class="card-body d-flex flex-column justify-content-between">
|
||||
@@ -67,14 +67,14 @@
|
||||
}
|
||||
} else {
|
||||
<div class="alert alert-warning" role="alert" aria-live="polite">
|
||||
<strong>@logic.playerTieLogic.currentTiePlayer()</strong> is currently picking a number for the cut.
|
||||
<strong>@gamelobby.logic.playerTieLogic.currentTiePlayer()</strong> is currently picking a number for the cut.
|
||||
</div>
|
||||
|
||||
<h6 class="mt-4 mb-3">Currently Picked Cards</h6>
|
||||
|
||||
<div id="cardsplayed" class="row g-3 justify-content-center">
|
||||
@if(logic.playerTieLogic.getSelectedCard.nonEmpty) {
|
||||
@for((player, card) <- logic.playerTieLogic.getSelectedCard) {
|
||||
@if(gamelobby.logic.playerTieLogic.getSelectedCard.nonEmpty) {
|
||||
@for((player, card) <- gamelobby.logic.playerTieLogic.getSelectedCard) {
|
||||
<div class="col-6 col-sm-4 col-md-3 col-lg-2">
|
||||
<div class="card shadow-sm border-0 h-100 text-center">
|
||||
<div class="card-body d-flex flex-column justify-content-between">
|
||||
|
||||
@@ -20,42 +20,43 @@
|
||||
</div>
|
||||
<div class="row justify-content-center align-items-center flex-grow-1">
|
||||
@if((gamelobby.getUserSession(user.get.id).host)) {
|
||||
@for(playersession <- gamelobby.getPlayers.values) {
|
||||
<div class="col-auto my-auto">
|
||||
<div class="card" style="width: 18rem;">
|
||||
<img src="@routes.Assets.versioned("images/profile.png")" alt="Profile" class="card-img-top w-50 mx-auto mt-3" />
|
||||
<div class="card-body">
|
||||
<div id="players" class="justify-content-center align-items-center d-flex">
|
||||
@for(playersession <- gamelobby.getPlayers.values) {
|
||||
<div class="col-auto my-auto m-3">
|
||||
<div class="card" style="width: 18rem;">
|
||||
<img src="@routes.Assets.versioned("images/profile.png")" alt="Profile" class="card-img-top w-50 mx-auto mt-3" />
|
||||
<div class="card-body">
|
||||
@if(playersession.id == user.get.id) {
|
||||
<h5 class="card-title">@playersession.name (You)</h5>
|
||||
@* <p class="card-text">Your text could be here!</p>*@
|
||||
<a href="#" class="btn btn-danger disabled" aria-disabled="true" tabindex="-1">Remove</a>
|
||||
} else {
|
||||
<h5 class="card-title">@playersession.name</h5>
|
||||
@* <p class="card-text">Your text could be here!</p>*@
|
||||
<div class="btn btn-danger" onclick="removePlayer('@gamelobby.id', '@playersession.id')">Remove</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
<div class="col-12 text-center mb-5">
|
||||
<div class="btn btn-success" onclick="startGame('@gamelobby.id')">Start Game</div>
|
||||
</div>
|
||||
} else {
|
||||
@for(playersession <- gamelobby.getPlayers.values) {
|
||||
<div class="col-auto my-auto"> <div class="card" style="width: 18rem;">
|
||||
<img src="@routes.Assets.versioned("images/profile.png")" alt="Profile" class="card-img-top w-50 mx-auto mt-3" />
|
||||
<div class="card-body">
|
||||
<div id="players" class="justify-content-center align-items-center d-flex">
|
||||
@for(playersession <- gamelobby.getPlayers.values) {
|
||||
<div class="col-auto my-auto m-3"> <div class="card" style="width: 18rem;">
|
||||
<img src="@routes.Assets.versioned("images/profile.png")" alt="Profile" class="card-img-top w-50 mx-auto mt-3" />
|
||||
<div class="card-body">
|
||||
@if(playersession.id == user.get.id) {
|
||||
<h5 class="card-title">@playersession.name (You)</h5>
|
||||
<h5 class="card-title">@playersession.name (You)</h5>
|
||||
} else {
|
||||
<h5 class="card-title">@playersession.name</h5>
|
||||
<h5 class="card-title">@playersession.name</h5>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
}
|
||||
</div>
|
||||
<div class="col-12 text-center mt-3">
|
||||
<p class="fs-4">Waiting for the host to start the game...</p>
|
||||
<div class="spinner-border mt-1" role="status">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@(user: Option[model.users.User])
|
||||
<nav class="navbar navbar-expand-lg bg-body-tertiary">
|
||||
<nav class="navbar navbar-expand-lg bg-body-tertiary navbar-drop-shadow">
|
||||
<div class="container d-flex justify-content-start">
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navBar" aria-controls="navBar" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
|
||||
@@ -34,4 +34,4 @@ POST /game/:id/tie controllers.IngameController.playTie(id:
|
||||
GET /game/:id/leaveGame controllers.IngameController.leaveGame(id: String)
|
||||
POST /game/:id/playCard controllers.IngameController.playCard(id: String)
|
||||
# Polling
|
||||
GET /polling controllers.IngameController.polling(gameId: String)
|
||||
GET /polling controllers.PollingController.polling(gameId: String)
|
||||
@@ -93,7 +93,7 @@ function pollForUpdates(gameId) {
|
||||
setTimeout(() => pollForUpdates(gameId), 5000);
|
||||
return;
|
||||
}
|
||||
const route = jsRoutes.controllers.IngameController.polling(gameId);
|
||||
const route = jsRoutes.controllers.PollingController.polling(gameId);
|
||||
|
||||
// Call your specific controller endpoint
|
||||
fetch(route.url)
|
||||
@@ -106,13 +106,21 @@ function pollForUpdates(gameId) {
|
||||
} else if (response.ok && response.status === 200) {
|
||||
response.json().then(data => {
|
||||
|
||||
if (data.status === "cardPlayed" && data.handData) {
|
||||
if (data.status === "cardPlayed" && data.handData && element) {
|
||||
console.log("Event received: Card played. Redrawing hand.");
|
||||
|
||||
const newHand = data.handData;
|
||||
let newHandHTML = '';
|
||||
element.innerHTML = '';
|
||||
|
||||
if(data.animation) {
|
||||
if (!element.classList.contains('ingame-cards-slide')) {
|
||||
element.classList.add('ingame-cards-slide');
|
||||
}
|
||||
} else {
|
||||
element.classList.remove('ingame-cards-slide');
|
||||
}
|
||||
|
||||
newHand.forEach((cardId, index) => {
|
||||
const cardHtml = `
|
||||
<div class="col-auto handcard" style="border-radius: 6px">
|
||||
@@ -211,8 +219,45 @@ function pollForUpdates(gameId) {
|
||||
// Clear the container and insert the new image
|
||||
firstCardContainer.innerHTML = newImageHTML;
|
||||
}
|
||||
} else if (data.status === "gameStart") {
|
||||
} else if (data.status === "reloadEvent") {
|
||||
window.location.href = data.redirectUrl;
|
||||
} else if (data.status === "lobbyUpdate") {
|
||||
const players = document.getElementById("players");
|
||||
let newHtml = ''
|
||||
|
||||
if (data.host) {
|
||||
data.users.forEach(user => {
|
||||
|
||||
const inner = user.self ? `<h5 class="card-title">${user.name} (You)</h5>
|
||||
<a href="#" class="btn btn-danger disabled" aria-disabled="true" tabindex="-1">Remove</a>`
|
||||
: ` <h5 class="card-title">${user.name}</h5>
|
||||
<div class="btn btn-danger" onclick="removePlayer('${gameId}', '${user.id}')">Remove</div>`
|
||||
|
||||
newHtml += `<div class="col-auto my-auto m-3">
|
||||
<div class="card" style="width: 18rem;">
|
||||
<img src="/assets/images/profile.png" alt="Profile" class="card-img-top w-50 mx-auto mt-3" />
|
||||
<div class="card-body">
|
||||
${inner}
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
} else {
|
||||
data.users.forEach(user => {
|
||||
|
||||
const inner = user.self ? `<h5 class="card-title">${user.name} (You)</h5>` : ` <h5 class="card-title">${user.name}</h5>`
|
||||
|
||||
newHtml += `<div class="col-auto my-auto m-3">
|
||||
<div class="card" style="width: 18rem;">
|
||||
<img src="/assets/images/profile.png" alt="Profile" class="card-img-top w-50 mx-auto mt-3" />
|
||||
<div class="card-body">
|
||||
${inner}
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
}
|
||||
players.innerHTML = newHtml;
|
||||
}
|
||||
pollForUpdates(gameId);
|
||||
});
|
||||
@@ -340,9 +385,11 @@ function sendRemovePlayerRequest(gameId, playersessionId) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function leaveGame(gameId) {
|
||||
sendLeavePlayerRequest(gameId)
|
||||
}
|
||||
|
||||
function sendLeavePlayerRequest(gameId) {
|
||||
|
||||
const route = jsRoutes.controllers.IngameController.leaveGame(gameId);
|
||||
|
||||
Reference in New Issue
Block a user