Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions api/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
quarkus.analytics.disabled=true
quarkusPlatformArtifactId=quarkus-bom
quarkusPlatformGroupId=io.quarkus.platform
quarkusPlatformVersion=3.10.1
quarkusPlatformVersion=3.11.1
quarkusPluginId=io.quarkus
quarkusPluginVersion=3.10.1
quarkusPluginVersion=3.11.1
2 changes: 1 addition & 1 deletion api/gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
kotlin = "2.0.0"
spotless = "6.25.0"
detekt = "1.23.6"
quarkus = "3.10.1"
quarkus = "3.11.1"
hamcrest-json = "0.3"
mockito = "5.3.1"
mockito-inline = "5.2.0"
Expand Down
44 changes: 19 additions & 25 deletions api/src/main/kotlin/pp/api/Rooms.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,8 @@ import jakarta.websocket.CloseReason.CloseCodes.VIOLATED_POLICY
import jakarta.websocket.Session
import pp.api.data.ChangeName
import pp.api.data.ChatMessage
import pp.api.data.GamePhase
import pp.api.data.GamePhase.CARDS_REVEALED
import pp.api.data.GamePhase.PLAYING
import pp.api.data.GamePhase.CardsRevealed
import pp.api.data.GamePhase.Playing
import pp.api.data.PlayCard
import pp.api.data.RevealCards
import pp.api.data.Room
Expand Down Expand Up @@ -100,8 +99,8 @@ class Rooms {
is ChangeName -> changeName(session, request.name)
is PlayCard -> playCard(session, request.cardValue)
is ChatMessage -> chatMessage(session, request.message)
is RevealCards -> changeGamePhase(session, CARDS_REVEALED)
is StartNewRound -> changeGamePhase(session, PLAYING)
is RevealCards -> changeGamePhaseToCardsRevealed(session)
is StartNewRound -> changeGamePhaseToPlaying(session)
else -> {
// spotlessApply keeps generating this else if it doesnt exist
}
Expand Down Expand Up @@ -179,7 +178,7 @@ class Rooms {

private fun playCard(session: Session, cardValue: String?) {
get(session)?.let { (room, user) ->
if (room.gamePhase == PLAYING) {
if (room.gamePhase is Playing) {
if (cardValue != null && cardValue !in room.deck) {
update(room withInfo "${user.username} tried to play card with illegal value: $cardValue")
} else {
Expand All @@ -200,35 +199,30 @@ class Rooms {
}
}

private fun changeGamePhase(session: Session, newGamePhase: GamePhase) {
private fun changeGamePhaseToPlaying(session: Session) {
get(session)?.let { (room, user) ->
val canRevealCards = room.gamePhase == PLAYING && newGamePhase == CARDS_REVEALED
val canStartNextRound = room.gamePhase == CARDS_REVEALED && newGamePhase == PLAYING

if (canRevealCards || canStartNextRound) {
if (room.gamePhase is CardsRevealed) {
val updatedRoom = room.run {
copy(
gamePhase = newGamePhase,
users = if (newGamePhase == CARDS_REVEALED) {
users
} else {
users.map { user ->
user.copy(
cardValue = null
)
}
gamePhase = Playing,
users = users.map { user ->
user.copy(
cardValue = null
)
}
)
}
val message = if (newGamePhase == CARDS_REVEALED) "revealed the cards" else "started a new round"
update(updatedRoom withInfo "${user.username} $message")
} else {
val error = "${user.username} tried to change game phase to $newGamePhase, but that's illegal"
update(room withInfo error)
update(updatedRoom withInfo "${user.username} started a new round")
}
}
}

private fun changeGamePhaseToCardsRevealed(session: Session) {
get(session)?.let { (room, user) ->
update(room withCardsRevealedBy user)
}
}

private operator fun get(roomId: String): Room? = allRooms.firstOrNull { it.roomId == roomId }
private operator fun get(session: Session): Pair<Room, User>? = allRooms
.firstOrNull {
Expand Down
10 changes: 9 additions & 1 deletion api/src/main/kotlin/pp/api/RoomsResource.kt
Original file line number Diff line number Diff line change
Expand Up @@ -188,5 +188,13 @@ class RoomsResource(
*/
@GET
@Produces(APPLICATION_JSON)
fun getRooms(): List<RoomDto> = rooms.getRooms().sortedBy { it.roomId }.map { RoomDto(it) }
fun getRooms(): List<RoomDto> = rooms
.getRooms()
.sortedBy { it.roomId }
.map {
RoomDto(
room = it,
yourUser = null
)
}
}
17 changes: 17 additions & 0 deletions api/src/main/kotlin/pp/api/Util.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ import java.nio.charset.StandardCharsets
import java.time.LocalTime
import java.time.LocalTime.now
import java.time.temporal.ChronoUnit.MILLIS
import kotlin.random.Random
import kotlin.time.Duration.Companion.minutes

private val chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".toCharArray()

/**
* Parses a query string.
*
Expand Down Expand Up @@ -40,3 +43,17 @@ fun parseQuery(query: String?): Map<String, String> {
* @return a time 3 minutes from [LocalTime.now]
*/
fun threeMinutesFromNow(): LocalTime = now().plus(3.minutes.inWholeMilliseconds, MILLIS)

/**
* Generate a random string of given length.
*
* @param length length of the string to generate
* @return a randome string of the given length, consisting of A-Za-z0-9
*/
fun generateRandomId(length: Int = 6): String {
val sb = StringBuilder(length)
repeat(length) {
sb.append(chars[Random.nextInt(chars.size)])
}
return sb.toString()
}
81 changes: 77 additions & 4 deletions api/src/main/kotlin/pp/api/data/GamePhase.kt
Original file line number Diff line number Diff line change
@@ -1,21 +1,94 @@
/**
* This file contains all classes that represent a games phase.
*/

package pp.api.data

import java.util.Locale.US

/**
* A thing thad plays a card
*
* @property username name of the user at the time the cards were revealed.
* Note that the user might have changed its name afterwards.
* @property id id of the user that played the card.
* Note that the user might have left the [Room] already.
*/
data class CardPlayer(
val username: String,
val id: String,
) {
constructor(user: User) : this(user.username, user.id)
}

/**
* A single card laying on the table at the time the cards in a [Room] were revealed.
*
* @property playedBy user that played the card
* @property value value of the card
*/
data class Card(
val playedBy: CardPlayer,
val value: String?,
)

/**
* A games result
*
* @property cards the cards that were played
* @property average average value of the cards
*/
data class GameResult(
val cards: List<Card>,
val average: String,
)

/**
* Phase the pp game is in.
*
* The game phase restrict what users can do. While some actions (eg. playing a card) are only allowed during a specific
* game phase, other actions (eg. sending a chat message) are independent of the phase.
*/
enum class GamePhase {
sealed class GamePhase {
/**
* In this phase, users can play cards or change the phase to [CardsRevealed]. Users cannot see any other players
* played cards
*/
PLAYING,
data object Playing : GamePhase()

/**
* In this phase, players cannot play cards but only observe the results or change the phase to [Playing]
*
* @property gameResult
*/
CARDS_REVEALED,
;
data class CardsRevealed(
val gameResult: GameResult,
) : GamePhase() {
constructor(room: Room) : this(
GameResult(
cards = room.users.sortedBy { it.username }.filter { it.userType == UserType.PARTICIPANT }
.map {
Card(
playedBy = CardPlayer(it),
value = it.cardValue
)
},
average = if (1 == room.participants
.groupBy { it.cardValue }.size && room.users.first().cardValue != null
) {
room.participants.first().cardValue!!
} else {
val hasSomeNoInt = room.participants.any { it.cardValue?.toIntOrNull() == null }
room.users
.mapNotNull {
it.cardValue?.toIntOrNull()
}
.average()
.run {
"%.1f".format(US, this) + (if (hasSomeNoInt) " (?)" else "")
}
}
)
)
}
}
18 changes: 16 additions & 2 deletions api/src/main/kotlin/pp/api/data/Room.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package pp.api.data

import io.quarkus.logging.Log
import jakarta.websocket.Session
import pp.api.data.GamePhase.PLAYING
import pp.api.data.GamePhase.Playing
import pp.api.data.UserType.PARTICIPANT

/**
Expand All @@ -25,7 +25,7 @@ class Room(
val roomId: String,
val users: List<User> = listOf(),
val deck: List<String> = listOf("1", "2", "3", "5", "8", "13", "☕"),
val gamePhase: GamePhase = PLAYING,
val gamePhase: GamePhase = Playing,
val log: List<LogEntry> = emptyList(),
) {
/**
Expand Down Expand Up @@ -100,6 +100,20 @@ class Room(
)
}

/**
* Create a copy of this room, with the cards revealed by the given [user]
*
* @param user the [User] that change the phase
* @return a copy of this room, with the cards revealed
*/
infix fun withCardsRevealedBy(user: User): Room = if (gamePhase is Playing) {
copy(
gamePhase = GamePhase.CardsRevealed(this)
) withInfo "${user.username} revealed the cards"
} else {
this
}

/**
* Crate a copy of this room, with the given message added as [LogEntry] with level [LogLevel.INFO]
*
Expand Down
6 changes: 6 additions & 0 deletions api/src/main/kotlin/pp/api/data/User.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package pp.api.data

import jakarta.websocket.Session
import pp.api.data.UserType.SPECTATOR
import pp.api.generateRandomId
import pp.api.parseQuery
import pp.api.threeMinutesFromNow
import java.time.LocalTime
Expand Down Expand Up @@ -783,6 +784,11 @@ data class User(
val session: Session,
var connectionDeadline: LocalTime = threeMinutesFromNow(),
) {
/**
* This user's unique id
*/
val id: String = generateRandomId()

/**
* Create a new [User] for the given [Session]
*
Expand Down
26 changes: 26 additions & 0 deletions api/src/main/kotlin/pp/api/dto/ClientGamePhase.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package pp.api.dto

import pp.api.data.GamePhase

/**
* Game phase as presented to clients
*/
enum class ClientGamePhase {
PLAYING,
CARDS_REVEALED,
;

companion object {
/**
* Determine the [ClientGamePhase] for a given [GamePhase]
*
* @param gamePhase a [GamePhase]
* @return [PLAYING], if [gamePhase] is [GamePhase.Playing], else [CARDS_REVEALED]
*/
operator fun invoke(gamePhase: GamePhase): ClientGamePhase =
when (gamePhase) {
is GamePhase.Playing -> PLAYING
is GamePhase.CardsRevealed -> CARDS_REVEALED
}
}
}
Loading