feat(ui): Implement countless feature using the SJWP #89

Merged
Janis merged 11 commits from feat/implement-events into main 2025-11-27 08:53:38 +01:00
14 changed files with 258 additions and 8 deletions
Showing only changes of commit e0f16a224d - Show all commits

View File

@@ -40,8 +40,8 @@ lazy val knockoutwhistweb = project.in(file("knockoutwhistweb"))
libraryDependencies += "de.mkammerer" % "argon2-jvm" % "2.12", libraryDependencies += "de.mkammerer" % "argon2-jvm" % "2.12",
libraryDependencies += "com.auth0" % "java-jwt" % "4.5.0", libraryDependencies += "com.auth0" % "java-jwt" % "4.5.0",
libraryDependencies += "com.github.ben-manes.caffeine" % "caffeine" % "3.2.3", libraryDependencies += "com.github.ben-manes.caffeine" % "caffeine" % "3.2.3",
libraryDependencies += "tools.jackson.module" %% "jackson-module-scala" % "3.0.2", libraryDependencies += "com.fasterxml.jackson.module" %% "jackson-module-scala" % "2.16.1",
JsEngineKeys.engineType := JsEngineKeys.EngineType.Node //JsEngineKeys.engineType := JsEngineKeys.EngineType.Node
) )
lazy val root = (project in file(".")) lazy val root = (project in file("."))

View File

@@ -2,7 +2,7 @@ package logic.game
import de.knockoutwhist.cards.{Hand, Suit} import de.knockoutwhist.cards.{Hand, Suit}
import de.knockoutwhist.control.GameLogic import de.knockoutwhist.control.GameLogic
import de.knockoutwhist.control.GameState.{Lobby, MainMenu} import de.knockoutwhist.control.GameState.{InGame, Lobby, MainMenu}
Janis marked this conversation as resolved Outdated
Outdated
Review

Remove unused import

Remove unused import
import de.knockoutwhist.control.controllerBaseImpl.sublogic.util.{MatchUtil, PlayerUtil} import de.knockoutwhist.control.controllerBaseImpl.sublogic.util.{MatchUtil, PlayerUtil}
import de.knockoutwhist.events.global.{GameStateChangeEvent, SessionClosed} import de.knockoutwhist.events.global.{GameStateChangeEvent, SessionClosed}
import de.knockoutwhist.events.player.PlayerEvent import de.knockoutwhist.events.player.PlayerEvent
@@ -59,6 +59,9 @@ class GameLobby private(
if (event.oldState == MainMenu && event.newState == Lobby) { if (event.oldState == MainMenu && event.newState == Lobby) {
return return
} }
if (event.oldState == Lobby && event.newState == InGame) {
lq64 marked this conversation as resolved Outdated
Outdated
Review

Nope

Nope
println("RECEIVED GAMESTATEEVENT")
}
users.values.foreach(session => session.updatePlayer(event)) users.values.foreach(session => session.updatePlayer(event))
case event: SimpleEvent => case event: SimpleEvent =>
users.values.foreach(session => session.updatePlayer(event)) users.values.foreach(session => session.updatePlayer(event))
@@ -71,6 +74,7 @@ class GameLobby private(
* @param user the user who wants to start the game. * @param user the user who wants to start the game.
*/ */
def startGame(user: User): Unit = { def startGame(user: User): Unit = {
println("STARTED GAME IN LOGIC")
val sessionOpt = users.get(user.id) val sessionOpt = users.get(user.id)
if (sessionOpt.isEmpty) { if (sessionOpt.isEmpty) {
throw new NotInThisGameException("You are not in this game!") throw new NotInThisGameException("You are not in this game!")

View File

@@ -2,9 +2,11 @@ package model.sessions
import de.knockoutwhist.events.player.{RequestCardEvent, RequestTieChoiceEvent, RequestTrumpSuitEvent} import de.knockoutwhist.events.player.{RequestCardEvent, RequestTieChoiceEvent, RequestTrumpSuitEvent}
import de.knockoutwhist.utils.events.SimpleEvent import de.knockoutwhist.utils.events.SimpleEvent
import logic.PodManager
Janis marked this conversation as resolved Outdated
Outdated
Review

Excessive imports

Excessive imports
import logic.game.GameLobby import logic.game.GameLobby
import model.users.User import model.users.User
import play.api.libs.json.JsObject import play.api.libs.json.Format.GenericFormat
import play.api.libs.json.{JsError, JsObject, JsResult, JsSuccess, JsValue}
import java.util.UUID import java.util.UUID
import java.util.concurrent.locks.ReentrantLock import java.util.concurrent.locks.ReentrantLock
@@ -26,7 +28,7 @@ class UserSession(val user: User, val host: Boolean, val gameLobby: GameLobby) e
else canInteract = Some(InteractionType.Card) else canInteract = Some(InteractionType.Card)
case _ => case _ =>
} }
websocketActor.foreach(_.transmitEventToClient(event)) websocketActor.foreach(_.transmitEventToClient(event, gameLobby))
} }
override def id: UUID = user.id override def id: UUID = user.id
@@ -44,6 +46,21 @@ class UserSession(val user: User, val host: Boolean, val gameLobby: GameLobby) e
case "Ping" => case "Ping" =>
// No action needed for Ping // No action needed for Ping
() ()
case "Start Game" =>
lq64 marked this conversation as resolved Outdated
Outdated
Review

Get debug statements

Get debug statements
println("INSIDE HANDLE WEB RESPONSE" + data)
val gameId: String = (data \ "gameId").get.toString
val cleanGameId: String = gameId.replaceAll("^[\"']|[\"']$", "")
val user: JsObject = (data \ "user").asOpt[JsObject].get
val gameLobby: GameLobby = PodManager.getGame(cleanGameId).get
val realUser: JsResult[User] = user.validate[User]
val uu: User = realUser match {
case JsSuccess(extractedUser, _) =>
extractedUser
case e: JsError =>
println("FAILED" + JsError.toJson(e).toString())
throw new Exception("Failed to deserialize User object: " + JsError.toJson(e).toString())
}
gameLobby.startGame(uu)
} }
} }
lock.unlock() lock.unlock()

View File

@@ -1,5 +1,7 @@
package model.users package model.users
import play.api.libs.json.{Format, Json}
lq64 marked this conversation as resolved
Review

Ne

Ne
import java.util.UUID import java.util.UUID
case class User( case class User(
@@ -16,5 +18,7 @@ case class User(
private def withPasswordHash(newPasswordHash: String): User = { private def withPasswordHash(newPasswordHash: String): User = {
this.copy(passwordHash = newPasswordHash) this.copy(passwordHash = newPasswordHash)
} }
}
object User {
implicit val userFormat: Format[User] = Json.format[User]
} }

View File

@@ -27,6 +27,10 @@ object WebsocketEventMapper {
registerCustomMapper(ReceivedHandEventMapper) registerCustomMapper(ReceivedHandEventMapper)
registerCustomMapper(GameStateEventMapper) registerCustomMapper(GameStateEventMapper)
registerCustomMapper(CardPlayedEventMapper) registerCustomMapper(CardPlayedEventMapper)
registerCustomMapper(NewRoundEventMapper)
registerCustomMapper(NewTrickEventMapper)
registerCustomMapper(TrickEndEventMapper)
registerCustomMapper(RequestCardEventMapper)
registerCustomMapper(LobbyUpdateEventMapper) registerCustomMapper(LobbyUpdateEventMapper)
registerCustomMapper(LeftEventMapper) registerCustomMapper(LeftEventMapper)
registerCustomMapper(KickEventMapper) registerCustomMapper(KickEventMapper)

View File

@@ -0,0 +1,18 @@
package util.mapper
import de.knockoutwhist.events.global.GameStateChangeEvent
import logic.game.GameLobby
import play.api.libs.json.{JsObject, Json}
object GameStateChangeEventMapper extends SimpleEventMapper[GameStateChangeEvent]{
override def id: String = "GameStateChangeEvent"
override def toJson(event: GameStateChangeEvent, gameLobby: GameLobby): JsObject = {
println("CALLED toJSON FOR GAMESTATECHANGE")
Json.obj(
"oldState" -> event.oldState.toString,
"newState" -> event.newState.toString,
"gameLobby" -> gameLobby.id
)
}
}

View File

@@ -0,0 +1,16 @@
package util.mapper
import de.knockoutwhist.events.global.NewRoundEvent
import logic.game.GameLobby
import play.api.libs.json.{JsObject, Json}
object NewRoundEventMapper extends SimpleEventMapper[NewRoundEvent]{
override def id: String = "NewRoundEvent"
override def toJson(event: NewRoundEvent, gameLobby: GameLobby): JsObject = {
Json.obj(
"trumpsuit" -> gameLobby.getLogic.getCurrentRound.get.trumpSuit.toString,
"players" -> gameLobby.getLogic.getCurrentMatch.get.playersIn.map(player => player.toString)
)
}
}

View File

@@ -0,0 +1,13 @@
package util.mapper
import de.knockoutwhist.events.global.NewTrickEvent
import logic.game.GameLobby
import play.api.libs.json.{JsObject, Json}
object NewTrickEventMapper extends SimpleEventMapper[NewTrickEvent]{
override def id: String = "NewTrickEvent"
override def toJson(event: NewTrickEvent, gameLobby: GameLobby): JsObject = {
Json.obj()
}
}

View File

@@ -0,0 +1,15 @@
package util.mapper
import de.knockoutwhist.events.player.RequestCardEvent
import logic.game.GameLobby
import play.api.libs.json.{JsObject, Json}
object RequestCardEventMapper extends SimpleEventMapper[RequestCardEvent]{
override def id: String = "RequestCardEvent"
override def toJson(event: RequestCardEvent, gameLobby: GameLobby): JsObject = {
Json.obj(
"player" -> event.player.name
)
}
}

View File

@@ -0,0 +1,18 @@
package util.mapper
import de.knockoutwhist.events.global.TrickEndEvent
import de.knockoutwhist.rounds.Trick
import logic.game.GameLobby
import play.api.libs.json.{JsObject, Json}
object TrickEndEventMapper extends SimpleEventMapper[TrickEndEvent]{
override def id: String = "TrickEndEvent"
override def toJson(event: TrickEndEvent, gameLobby: GameLobby): JsObject = {
Json.obj(
"playerwon" -> event.winner.name,
"playersin" -> gameLobby.getLogic.getCurrentMatch.get.playersIn.map(player => player.name),
"tricklist" -> gameLobby.getLogic.getCurrentRound.get.tricklist.map(trick => trick.winner.map(player => player.name).getOrElse("Trick in Progress"))
)
}
}

View File

@@ -69,7 +69,7 @@
} }
</div> </div>
<div class="col-12 text-center mb-5"> <div class="col-12 text-center mb-5">
<div class="btn btn-success" onclick="startGame('@gamelobby.id')">Start Game</div> <div class="btn btn-success" onclick="startGame('@gamelobby.id', '@user.get.id', '@user.get.name','@user.get.passwordHash', @user.get.internalId)">Start Game</div>
</div> </div>
} else { } else {
<div id="players" class="justify-content-center align-items-center d-flex"> <div id="players" class="justify-content-center align-items-center d-flex">
@@ -98,6 +98,7 @@
</div> </div>
</main> </main>
<script> <script>
/*
function waitForFunction(name, checkInterval = 100) { function waitForFunction(name, checkInterval = 100) {
return new Promise(resolve => { return new Promise(resolve => {
const timer = setInterval(() => { const timer = setInterval(() => {
@@ -109,5 +110,6 @@
}); });
} }
waitForFunction("pollForUpdates").then(fn => fn('@gamelobby.id')); waitForFunction("pollForUpdates").then(fn => fn('@gamelobby.id'));
*/
connectWebSocket() connectWebSocket()
</script> </script>

View File

@@ -30,4 +30,4 @@
</main> </main>
<script> <script>
disconnectWebSocket(); disconnectWebSocket();
</script> </script>

View File

@@ -1,3 +1,30 @@
function alertMessage(message) {
let newHtml = '';
const alertId = `alert-${Date.now()}`;
const fadeTime = 500;
const duration = 5000;
newHtml += `
<div class="fixed-top d-flex justify-content-center mt-3" style="z-index: 1050;">
<div
id="${alertId}" class="alert alert-primary d-flex align-items-center p-2 mb-0 w-auto" role="alert" style="display: none;">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" class="bi flex-shrink-0 me-2" viewBox="0 0 16 16" role="img" aria-label="Warning:">
<path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z"/>
</svg>
<div class="small">
<small>${message}</small>
</div>
</div>
</div>
`;
$('#main-body').prepend(newHtml);
const $notice = $(`#${alertId}`);
$notice.fadeIn(fadeTime);
setTimeout(function() {
$notice.fadeOut(fadeTime, function() {
$(this).parent().remove();
});
}, duration);
}
function receiveHandEvent(eventData) { function receiveHandEvent(eventData) {
//Data //Data
const dog = eventData.dog; const dog = eventData.dog;
@@ -38,6 +65,101 @@ function receiveHandEvent(eventData) {
} }
handElement.html(newHtml); handElement.html(newHtml);
} }
function newRoundEvent(eventData) {
const trumpsuit = eventData.trumpsuit;
const players = eventData.players;
const tableElement = $('#score-table-body');
let tablehtml = `
<h4 class="fw-bold mb-3 text-black">Tricks Won</h4>
<div class="d-flex justify-content-between score-header pb-1">
<div style="width: 50%">PLAYER</div>
<div style="width: 50%">TRICKS</div>
</div>
`;
players.forEach(
tablehtml += `
<div class="d-flex justify-content-between score-row pt-1">
<div style="width: 50%" class="text-truncate">'${players}'</div>
<div style="width: 50%">
0
</div>
</div>
`
);
tableElement.html(tablehtml);
const trumpsuitClass = $('#trumpsuit');
trumpsuitClass.html(trumpsuit);
}
function trickEndEvent(eventData) {
const winner = eventData.playerwon;
const players = eventData.playersin;
const tricklist = eventData.tricklist;
let newHtml = '';
let tricktable = $('#score-table-body');
newHtml += `
<h4 class="fw-bold mb-3 text-black">Tricks Won</h4>
<div class="d-flex justify-content-between score-header pb-1">
<div style="width: 50%">PLAYER</div>
<div style="width: 50%">TRICKS</div>
</div>
`;
let playercounts = new Map();
players.forEach( player => {
playercounts.set(player, 0)
});
tricklist.forEach( player => {
if ( player !== "Trick in Progress" && playercounts.has(player)) {
playercounts.set(player, playercounts.get(player) + 1)
}
}
)
const playerorder = players.sort((playerA, playerB) => {
const countA = playercounts.get(playerA.name) || 0;
const countB = playercounts.get(playerB.name) || 0;
return countB - countA;
});
playerorder.forEach( player => {
newHtml += `
<div class="d-flex justify-content-between score-row pt-1">
<div style="width: 50%" class="text-truncate">'${player}'</div>
<div style="width: 50%">
'${playercounts.get(player)}'
</div>
</div>
`
});
tricktable.html(newHtml);
}
function newTrickEvent() {
Janis marked this conversation as resolved
Review

playable cards reset

playable cards reset
const firstCardContainer = $('first-card-container');
let newHtml = '';
newHtml += `
<img src="images/cards/1B.png" alt="Blank Card" width="80px"/>
`;
firstCardContainer.html(newHtml);
}
function requestCardEvent(eventData) {
const player = eventData.player;
const handElement = $('#card-slide')
handElement.removeClass('inactive');
}
//alertMessage("It worked!")
function receiveGameStateChange(eventData) { function receiveGameStateChange(eventData) {
const content = eventData.content; const content = eventData.content;
@@ -218,6 +340,10 @@ function receiveTurnEvent(eventData) {
onEvent("ReceivedHandEvent", receiveHandEvent) onEvent("ReceivedHandEvent", receiveHandEvent)
onEvent("GameStateChangeEvent", receiveGameStateChange) onEvent("GameStateChangeEvent", receiveGameStateChange)
onEvent("NewRoundEvent", newRoundEvent)
onEvent("TrickEndEvent", trickEndEvent)
onEvent("NewTrickEvent", newTrickEvent)
onEvent("RequestCardEvent", requestCardEvent)
onEvent("CardPlayedEvent", receiveCardPlayedEvent) onEvent("CardPlayedEvent", receiveCardPlayedEvent)
onEvent("LobbyUpdateEvent", receiveLobbyUpdateEvent) onEvent("LobbyUpdateEvent", receiveLobbyUpdateEvent)
onEvent("LeftEvent", receiveGameStateChange) onEvent("LeftEvent", receiveGameStateChange)

View File

@@ -5,6 +5,19 @@ function handlePlayCard(card, dog) {
function handleSkipDogLife(button) { function handleSkipDogLife(button) {
// TODO needs implementation // TODO needs implementation
} }
function startGame(gameId, userId, username, userpasswordhash, userinternalid) {
const userpayload = {
internalId: userinternalid,
id: userId,
name: username,
passwordHash: userpasswordhash
}
const payload = {
gameId: gameId,
user: userpayload
};
sendEvent("Start Game", payload)
}
function handleKickPlayer(playerId) { function handleKickPlayer(playerId) {
// TODO needs implementation // TODO needs implementation
} }