Skip to content

Commit 4d48327

Browse files
committed
feat: add immutable GameResult
1 parent f77bbb6 commit 4d48327

14 files changed

Lines changed: 239 additions & 108 deletions

File tree

api/src/main/kotlin/pp/api/Rooms.kt

Lines changed: 19 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,8 @@ import jakarta.websocket.CloseReason.CloseCodes.VIOLATED_POLICY
88
import jakarta.websocket.Session
99
import pp.api.data.ChangeName
1010
import pp.api.data.ChatMessage
11-
import pp.api.data.GamePhase
12-
import pp.api.data.GamePhase.CARDS_REVEALED
13-
import pp.api.data.GamePhase.PLAYING
11+
import pp.api.data.GamePhase.CardsRevealed
12+
import pp.api.data.GamePhase.Playing
1413
import pp.api.data.PlayCard
1514
import pp.api.data.RevealCards
1615
import pp.api.data.Room
@@ -100,8 +99,8 @@ class Rooms {
10099
is ChangeName -> changeName(session, request.name)
101100
is PlayCard -> playCard(session, request.cardValue)
102101
is ChatMessage -> chatMessage(session, request.message)
103-
is RevealCards -> changeGamePhase(session, CARDS_REVEALED)
104-
is StartNewRound -> changeGamePhase(session, PLAYING)
102+
is RevealCards -> changeGamePhaseToCardsRevealed(session)
103+
is StartNewRound -> changeGamePhaseToPlaying(session)
105104
else -> {
106105
// spotlessApply keeps generating this else if it doesnt exist
107106
}
@@ -179,7 +178,7 @@ class Rooms {
179178

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

203-
private fun changeGamePhase(session: Session, newGamePhase: GamePhase) {
202+
private fun changeGamePhaseToPlaying(session: Session) {
204203
get(session)?.let { (room, user) ->
205-
val canRevealCards = room.gamePhase == PLAYING && newGamePhase == CARDS_REVEALED
206-
val canStartNextRound = room.gamePhase == CARDS_REVEALED && newGamePhase == PLAYING
207-
208-
if (canRevealCards || canStartNextRound) {
204+
if (room.gamePhase is CardsRevealed) {
209205
val updatedRoom = room.run {
210206
copy(
211-
gamePhase = newGamePhase,
212-
users = if (newGamePhase == CARDS_REVEALED) {
213-
users
214-
} else {
215-
users.map { user ->
216-
user.copy(
217-
cardValue = null
218-
)
219-
}
207+
gamePhase = Playing,
208+
users = users.map { user ->
209+
user.copy(
210+
cardValue = null
211+
)
220212
}
221213
)
222214
}
223-
val message = if (newGamePhase == CARDS_REVEALED) "revealed the cards" else "started a new round"
224-
update(updatedRoom withInfo "${user.username} $message")
225-
} else {
226-
val error = "${user.username} tried to change game phase to $newGamePhase, but that's illegal"
227-
update(room withInfo error)
215+
update(updatedRoom withInfo "${user.username} started a new round")
228216
}
229217
}
230218
}
231219

220+
private fun changeGamePhaseToCardsRevealed(session: Session) {
221+
get(session)?.let { (room, user) ->
222+
update(room withCardsRevealedBy user)
223+
}
224+
}
225+
232226
private operator fun get(roomId: String): Room? = allRooms.firstOrNull { it.roomId == roomId }
233227
private operator fun get(session: Session): Pair<Room, User>? = allRooms
234228
.firstOrNull {

api/src/main/kotlin/pp/api/RoomsResource.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,5 +188,13 @@ class RoomsResource(
188188
*/
189189
@GET
190190
@Produces(APPLICATION_JSON)
191-
fun getRooms(): List<RoomDto> = rooms.getRooms().sortedBy { it.roomId }.map { RoomDto(it) }
191+
fun getRooms(): List<RoomDto> = rooms
192+
.getRooms()
193+
.sortedBy { it.roomId }
194+
.map {
195+
RoomDto(
196+
room = it,
197+
yourUser = null
198+
)
199+
}
192200
}

api/src/main/kotlin/pp/api/Util.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,11 @@ import java.nio.charset.StandardCharsets
99
import java.time.LocalTime
1010
import java.time.LocalTime.now
1111
import java.time.temporal.ChronoUnit.MILLIS
12+
import kotlin.random.Random
1213
import kotlin.time.Duration.Companion.minutes
1314

15+
private val chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".toCharArray()
16+
1417
/**
1518
* Parses a query string.
1619
*
@@ -40,3 +43,17 @@ fun parseQuery(query: String?): Map<String, String> {
4043
* @return a time 3 minutes from [LocalTime.now]
4144
*/
4245
fun threeMinutesFromNow(): LocalTime = now().plus(3.minutes.inWholeMilliseconds, MILLIS)
46+
47+
/**
48+
* Generate a random string of given length.
49+
*
50+
* @param length length of the string to generate
51+
* @return a randome string of the given length, consisting of A-Za-z0-9
52+
*/
53+
fun generateRandomId(length: Int = 6): String {
54+
val sb = StringBuilder(length)
55+
repeat(length) {
56+
sb.append(chars[Random.nextInt(chars.size)])
57+
}
58+
return sb.toString()
59+
}
Lines changed: 77 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,94 @@
1+
/**
2+
* This file contains all classes that represent a games phase.
3+
*/
4+
15
package pp.api.data
26

7+
import java.util.Locale.US
8+
9+
/**
10+
* A thing thad plays a card
11+
*
12+
* @property username name of the user at the time the cards were revealed.
13+
* Note that the user might have changed its name afterwards.
14+
* @property userId id of the user that played the card.
15+
* Note that the user might have left the [Room] already.
16+
*/
17+
data class CardPlayer(
18+
val username: String,
19+
val userId: String,
20+
) {
21+
constructor(user: User) : this(user.username, user.id)
22+
}
23+
24+
/**
25+
* A single card laying on the table at the time the cards in a [Room] were revealed.
26+
*
27+
* @property playedBy user that played the card
28+
* @property value value of the card
29+
*/
30+
data class Card(
31+
val playedBy: CardPlayer,
32+
val value: String?,
33+
)
34+
35+
/**
36+
* A games result
37+
*
38+
* @property cards the cards that were played
39+
* @property average average value of the cards
40+
*/
41+
data class GameResult(
42+
val cards: List<Card>,
43+
val average: String,
44+
)
45+
346
/**
447
* Phase the pp game is in.
548
*
649
* The game phase restrict what users can do. While some actions (eg. playing a card) are only allowed during a specific
750
* game phase, other actions (eg. sending a chat message) are independent of the phase.
851
*/
9-
enum class GamePhase {
52+
sealed class GamePhase {
1053
/**
1154
* In this phase, users can play cards or change the phase to [CardsRevealed]. Users cannot see any other players
1255
* played cards
1356
*/
14-
PLAYING,
57+
data object Playing : GamePhase()
1558

1659
/**
1760
* In this phase, players cannot play cards but only observe the results or change the phase to [Playing]
61+
*
62+
* @property gameResult
1863
*/
19-
CARDS_REVEALED,
20-
;
64+
data class CardsRevealed(
65+
val gameResult: GameResult,
66+
) : GamePhase() {
67+
constructor(room: Room) : this(
68+
GameResult(
69+
cards = room.users.filter { it.userType == UserType.PARTICIPANT }
70+
.map {
71+
Card(
72+
playedBy = CardPlayer(it),
73+
value = it.cardValue
74+
)
75+
},
76+
average = if (1 == room.participants
77+
.groupBy { it.cardValue }.size && room.users.first().cardValue != null
78+
) {
79+
room.participants.first().cardValue!!
80+
} else {
81+
val hasSomeNoInt = room.participants.any { it.cardValue?.toIntOrNull() == null }
82+
room.users
83+
.mapNotNull {
84+
it.cardValue?.toIntOrNull()
85+
}
86+
.average()
87+
.run {
88+
"%.1f".format(US, this) + (if (hasSomeNoInt) " (?)" else "")
89+
}
90+
}
91+
)
92+
)
93+
}
2194
}

api/src/main/kotlin/pp/api/data/Room.kt

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package pp.api.data
22

33
import io.quarkus.logging.Log
44
import jakarta.websocket.Session
5-
import pp.api.data.GamePhase.PLAYING
5+
import pp.api.data.GamePhase.Playing
66
import pp.api.data.UserType.PARTICIPANT
77

88
/**
@@ -25,7 +25,7 @@ class Room(
2525
val roomId: String,
2626
val users: List<User> = listOf(),
2727
val deck: List<String> = listOf("1", "2", "3", "5", "8", "13", "☕"),
28-
val gamePhase: GamePhase = PLAYING,
28+
val gamePhase: GamePhase = Playing,
2929
val log: List<LogEntry> = emptyList(),
3030
) {
3131
/**
@@ -100,6 +100,20 @@ class Room(
100100
)
101101
}
102102

103+
/**
104+
* Create a copy of this room, with the cards revealed by the given [user]
105+
*
106+
* @param user the [User] that change the phase
107+
* @return a copy of this room, with the cards revealed
108+
*/
109+
infix fun withCardsRevealedBy(user: User): Room = if (gamePhase is Playing) {
110+
copy(
111+
gamePhase = GamePhase.CardsRevealed(this)
112+
) withInfo "${user.username} revealed the cards"
113+
} else {
114+
this
115+
}
116+
103117
/**
104118
* Crate a copy of this room, with the given message added as [LogEntry] with level [LogLevel.INFO]
105119
*

api/src/main/kotlin/pp/api/data/User.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package pp.api.data
22

33
import jakarta.websocket.Session
44
import pp.api.data.UserType.SPECTATOR
5+
import pp.api.generateRandomId
56
import pp.api.parseQuery
67
import pp.api.threeMinutesFromNow
78
import java.time.LocalTime
@@ -783,6 +784,11 @@ data class User(
783784
val session: Session,
784785
var connectionDeadline: LocalTime = threeMinutesFromNow(),
785786
) {
787+
/**
788+
* This user's unique id
789+
*/
790+
val id: String = generateRandomId()
791+
786792
/**
787793
* Create a new [User] for the given [Session]
788794
*
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package pp.api.dto
2+
3+
import pp.api.data.GamePhase
4+
5+
/**
6+
* Game phase as presented to clients
7+
*/
8+
enum class ClientGamePhase {
9+
PLAYING,
10+
CARDS_REVEALED,
11+
;
12+
13+
companion object {
14+
/**
15+
* Determine the [ClientGamePhase] for a given [GamePhase]
16+
*
17+
* @param gamePhase a [GamePhase]
18+
* @return [PLAYING], if [gamePhase] is [GamePhase.Playing], else [CARDS_REVEALED]
19+
*/
20+
operator fun invoke(gamePhase: GamePhase): ClientGamePhase =
21+
when (gamePhase) {
22+
is GamePhase.Playing -> PLAYING
23+
is GamePhase.CardsRevealed -> CARDS_REVEALED
24+
}
25+
}
26+
}

api/src/main/kotlin/pp/api/dto/RoomDto.kt

Lines changed: 18 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,10 @@ package pp.api.dto
22

33
import io.quarkus.runtime.annotations.RegisterForReflection
44
import pp.api.data.GamePhase
5-
import pp.api.data.GamePhase.CARDS_REVEALED
5+
import pp.api.data.GameResult
66
import pp.api.data.LogEntry
77
import pp.api.data.Room
88
import pp.api.data.User
9-
import java.util.Locale.US
109

1110
/**
1211
* State of a room as presented to clients
@@ -18,45 +17,38 @@ import java.util.Locale.US
1817
* @property deck card values that are playable in this room
1918
* @property gamePhase [GamePhase] the room is currently in
2019
* @property average represents the average of the card values played. Will only show real data if [gamePhase] is
21-
* [CARDS_REVEALED]
20+
* [GamePhase.CardsRevealed]
2221
* @property log list of [LogEntry]s for this rooms
22+
* @property gameResult result of the current round will be null if [gamePhase] is [ClientGamePhase.PLAYING]
2323
*/
2424
// see https://quarkus.io/guides/writing-native-applications-tips#registerForReflection
2525
@RegisterForReflection(registerFullHierarchy = true)
2626
data class RoomDto(
2727
val roomId: String,
2828
val deck: List<String>,
29-
val gamePhase: GamePhase,
29+
val gamePhase: ClientGamePhase,
3030
val users: List<UserDto>,
3131
val average: String,
3232
val log: List<LogEntry>,
33+
val gameResult: GameResult?,
3334
) {
3435
constructor(room: Room, yourUser: User? = null) : this(
3536
roomId = room.roomId,
3637
deck = room.deck,
37-
gamePhase = room.gamePhase,
38-
users = room.users.map { user ->
39-
UserDto(user, isYourUser = user == yourUser, room.gamePhase)
40-
}.sortedBy { it.username },
41-
average = (if (room.gamePhase == CARDS_REVEALED) {
42-
if (1 == room.participants
43-
.groupBy { it.cardValue }.size && room.users.first().cardValue != null
44-
) {
45-
room.participants.first().cardValue!!
46-
} else {
47-
val hasSomeNoInt = room.participants.any { it.cardValue?.toIntOrNull() == null }
48-
room.users
49-
.mapNotNull {
50-
it.cardValue?.toIntOrNull()
51-
}
52-
.average()
53-
.run {
54-
"%.1f".format(US, this) + (if (hasSomeNoInt) " (?)" else "")
55-
}
38+
gamePhase = ClientGamePhase(room.gamePhase),
39+
users = room.users
40+
.map { user ->
41+
UserDto(user, isYourUser = user == yourUser, room.gamePhase)
5642
}
57-
} else {
58-
"?"
59-
}),
43+
.sortedBy { it.username },
44+
average = when (room.gamePhase) {
45+
is GamePhase.CardsRevealed -> room.gamePhase.gameResult.average
46+
else -> "?"
47+
},
6048
log = room.log,
49+
gameResult = when (room.gamePhase) {
50+
is GamePhase.CardsRevealed -> room.gamePhase.gameResult
51+
else -> null
52+
}
6153
)
6254
}

0 commit comments

Comments
 (0)