Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package com.codenames.frontend.data.model

import com.codenames.frontend.data.model.enums.CheatExposureResult

data class ChatDomainModel(
val sender: String,
val text: String,
val isFromMe: Boolean,
val cheatExposureResult: CheatExposureResult? = null,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.codenames.frontend.data.model.enums

enum class CheatExposureResult {
CORRECT,
WRONG,
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.codenames.frontend.data.repository

import com.codenames.frontend.data.model.ChatDomainModel
import com.codenames.frontend.data.model.enums.ChatMessageType
import com.codenames.frontend.data.model.enums.CheatExposureResult
import com.codenames.frontend.network.dto.ChatMessageDto
import com.codenames.frontend.network.websocket.ChatWebSocketController
import kotlinx.coroutines.flow.Flow
Expand Down Expand Up @@ -50,8 +52,9 @@ class ChatRepository
emit(
ChatDomainModel(
sender = dto.senderUsername,
text = dto.content,
text = dto.toDisplayText(),
isFromMe = dto.senderUsername == currentUsername,
cheatExposureResult = dto.toCheatExposureResult(),
),
)
}
Expand Down Expand Up @@ -117,10 +120,33 @@ class ChatRepository
emit(
ChatDomainModel(
sender = dto.senderUsername,
text = dto.content,
text = dto.toDisplayText(),
isFromMe = dto.senderUsername == currentUsername,
cheatExposureResult = dto.toCheatExposureResult(),
),
)
}
}

private fun ChatMessageDto.toCheatExposureResult(): CheatExposureResult? =
if (type == ChatMessageType.SYSTEM) {
when (content) {
"EXPOSE_CORRECT" -> CheatExposureResult.CORRECT
"EXPOSE_WRONG" -> CheatExposureResult.WRONG
else -> null
}
} else {
null
}

private fun ChatMessageDto.toDisplayText(): String =
if (type == ChatMessageType.SYSTEM) {
when (content) {
"EXPOSE_CORRECT" -> "Cheat exposed correctly. The opposing team's next turn is skipped."
"EXPOSE_WRONG" -> "Wrong accusation. Your team's turn ends."
else -> content
}
} else {
content
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.codenames.frontend.data.repository
import com.codenames.frontend.data.model.enums.Team
import com.codenames.frontend.network.dto.CheatCardMessage
import com.codenames.frontend.network.dto.ClueMessageDto
import com.codenames.frontend.network.dto.ExposeCheatMessage
import com.codenames.frontend.network.dto.GuessMessage
import com.codenames.frontend.network.dto.PassTurnMessage
import com.codenames.frontend.network.dto.StartGameMessage
Expand Down Expand Up @@ -86,4 +87,12 @@ class GameRepository

webSocketHandler.sendCheat(msg)
}

suspend fun exposeCheat(
lobbyCode: String,
username: String,
) {
val msg = ExposeCheatMessage(lobbyCode, username)
webSocketHandler.exposeCheat(msg)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.codenames.frontend.network.dto

import kotlinx.serialization.Serializable

@Serializable
data class ExposeCheatMessage(
val lobbyCode: String,
val username: String,
)
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.codenames.frontend.network.websocket

import com.codenames.frontend.network.dto.CheatCardMessage
import com.codenames.frontend.network.dto.ClueMessageDto
import com.codenames.frontend.network.dto.ExposeCheatMessage
import com.codenames.frontend.network.dto.GameMessage
import com.codenames.frontend.network.dto.GuessMessage
import com.codenames.frontend.network.dto.PassTurnMessage
Expand Down Expand Up @@ -58,4 +59,12 @@ class GameWebSocketController
CheatCardMessage.serializer(),
)
}

suspend fun exposeCheat(msg: ExposeCheatMessage) {
webSocketSessionManager.getSession().convertAndSend(
"/app/expose-cheat",
msg,
ExposeCheatMessage.serializer(),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.navigation.NavHostController
import com.codenames.frontend.data.model.enums.CheatExposureResult
import com.codenames.frontend.ui.navigation.Screen
import com.codenames.frontend.viewmodel.ChatViewModel
import com.codenames.frontend.viewmodel.GameViewModel
Expand Down Expand Up @@ -32,6 +33,10 @@ fun GameScreenWrapper(
val userRole = lobbyViewModel.getRoleForUser(username)
val availableChatTabs = lobbyViewModel.getAvailableChatTabsForUser(username)
val winner = gameState.winner
val cheatSuccessfullyExposed =
chatState.operativeMessages.any {
it.cheatExposureResult == CheatExposureResult.CORRECT
}

LaunchedEffect(winner) {
if (winner != null) {
Expand All @@ -41,6 +46,7 @@ fun GameScreenWrapper(

GameboardScreen(
userRole = userRole,
isExposeCheatAvailable = !cheatSuccessfullyExposed,
gameState =
gameState.copy(
chatLists = chatState,
Expand All @@ -62,6 +68,12 @@ fun GameScreenWrapper(
positions = positions,
)
},
onExposeCheat = {
gameViewModel.exposeCheat(
lobbyCode = lobbyCode,
username = username,
)
},
onSendChatMessage = { tab, message ->
chatViewModel.sendChatMessage(
tab = tab,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,24 +112,28 @@
val messages: ChatLists,
val selectedTab: ChatTab,
val availableTabs: List<ChatTab>,
val canExposeCheat: Boolean,
)

private data class ChatOverlayActions(
val onTabSelected: (ChatTab) -> Unit,
val onInputChange: (String) -> Unit,
val onSendMessage: (ChatTab, String) -> Unit,
val onExposeCheat: () -> Unit,
)

@Suppress("ktlint:standard:function-naming")
@Composable
fun GameboardScreen(

Check failure on line 127 in app/src/main/java/com/codenames/frontend/ui/screens/GameboardScreen.kt

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 17 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=SS26-SE2-Codenames_Frontend&issues=AZ77Ymt8aN4rNUSQ2wAS&open=AZ77Ymt8aN4rNUSQ2wAS&pullRequest=92
userRole: PlayerRoles,
isExposeCheatAvailable: Boolean = true,
gameState: GameState,
onHintChange: (String, Int) -> Unit,
onReveal: (List<Int>) -> Unit,
onCheatRequest: (List<Int>) -> Unit,
modifier: Modifier = Modifier,
onPassTurn: () -> Unit = {},
onExposeCheat: () -> Unit = {},
onSendChatMessage: (ChatTab, String) -> Unit = { _, _ -> },
onSettingsClick: (() -> Unit)? = null,
onReturnToHome: (() -> Unit),
Expand Down Expand Up @@ -157,7 +161,8 @@
val isSpymaster =
userRole == PlayerRoles.BLUE_SPYMASTER || userRole == PlayerRoles.RED_SPYMASTER
val isActiveSpymaster = userRole == currentTurn && isSpymaster
val canEndTurn = userRole == currentTurn && !isSpymaster && remainingGuesses > 0
val isActiveOperative = userRole == currentTurn && !isSpymaster
val canEndTurn = isActiveOperative && remainingGuesses > 0
val keyboardController = LocalSoftwareKeyboardController.current
val focusManager = LocalFocusManager.current

Expand Down Expand Up @@ -304,12 +309,14 @@
messages = chatLists,
selectedTab = selectedChatTab,
availableTabs = availableChatTabs,
canExposeCheat = isActiveOperative && isExposeCheatAvailable,
),
actions =
ChatOverlayActions(
onTabSelected = { selectedChatTab = it },
onInputChange = { chatInput = it },
onSendMessage = onSendChatMessage,
onExposeCheat = onExposeCheat,
),
modifier =
Modifier
Expand Down Expand Up @@ -568,6 +575,8 @@
onTabSelected = actions.onTabSelected,
onChatInputChange = actions.onInputChange,
onSendClick = actions.onSendMessage,
canExposeCheat = state.canExposeCheat,
onExposeCheat = actions.onExposeCheat,
modifier = modifier,
)
}
Expand Down Expand Up @@ -655,6 +664,8 @@
onTabSelected: (ChatTab) -> Unit,
onChatInputChange: (String) -> Unit,
onSendClick: (ChatTab, String) -> Unit,
canExposeCheat: Boolean = false,
onExposeCheat: () -> Unit = {},
modifier: Modifier = Modifier,
) {
val dimensions = LocalResponsiveDimensions.current
Expand Down Expand Up @@ -707,6 +718,25 @@

Spacer(modifier = Modifier.height(dimensions.itemSpacing))

if (selectedTab == ChatTab.OPERATIVES && canExposeCheat) {
AppButton(
text = "Expose Cheat",
onClick = onExposeCheat,
modifier =
Modifier
.fillMaxWidth()
.height(dimensions.secondaryButtonHeight),
style =
AppButtonStyle(
containerColor = AppRed,
contentColor = AppWhite,
fontSize = dimensions.smallFontSize,
),
)

Spacer(modifier = Modifier.height(dimensions.itemSpacing))
}

Row(
modifier =
Modifier
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,23 @@ class GameViewModel
}
}

fun exposeCheat(
lobbyCode: String,
username: String,
) {
if (lobbyCode.isBlank() || username.isBlank()) {
return
}

viewModelScope.launch {
try {
gameRepository.exposeCheat(lobbyCode, username)
} catch (e: Exception) {
setConnectionError(e)
}
}
}

fun passTurn(
lobbyCode: String,
team: Team?,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.codenames.frontend.data.repository

import com.codenames.frontend.data.model.enums.ChatMessageType
import com.codenames.frontend.data.model.enums.CheatExposureResult
import com.codenames.frontend.network.dto.ChatMessageDto
import com.codenames.frontend.network.websocket.ChatWebSocketController
import io.mockk.Runs
Expand Down Expand Up @@ -337,4 +339,31 @@ class ChatRepositoryTest {
assertEquals("System", result[0].sender)
assertEquals("London is correct", result[0].text)
}

@Test
fun testObserveOperativeChat_mapsCorrectExposeResult() =
runTest {
val dto = ChatMessageDto("System", "EXPOSE_CORRECT", ChatMessageType.SYSTEM)
coEvery { chatSocketHandler.subscribeToChat(operativeTopic) } returns flowOf(dto)

val result = repository.observeOperativeChat(testLobbyCode, testTeam, testUser).toList()

assertEquals(
"Cheat exposed correctly. The opposing team's next turn is skipped.",
result[0].text,
)
assertEquals(CheatExposureResult.CORRECT, result[0].cheatExposureResult)
}

@Test
fun testObserveOperativeChat_mapsWrongExposeResult() =
runTest {
val dto = ChatMessageDto("System", "EXPOSE_WRONG", ChatMessageType.SYSTEM)
coEvery { chatSocketHandler.subscribeToChat(operativeTopic) } returns flowOf(dto)

val result = repository.observeOperativeChat(testLobbyCode, testTeam, testUser).toList()

assertEquals("Wrong accusation. Your team's turn ends.", result[0].text)
assertEquals(CheatExposureResult.WRONG, result[0].cheatExposureResult)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,16 @@ class GameRepositoryTest {
}
}

@Test
fun testExposeCheat() =
runTest {
coEvery { webSocketHandler.exposeCheat(any()) } just Runs

gameRepository.exposeCheat("ABCDE", "Max")

coVerify { webSocketHandler.exposeCheat(any()) }
}

@Test
fun testPassTurn() =
runTest {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.codenames.frontend.network.websocket
import com.codenames.frontend.data.model.enums.Team
import com.codenames.frontend.network.dto.CheatCardMessage
import com.codenames.frontend.network.dto.ClueMessageDto
import com.codenames.frontend.network.dto.ExposeCheatMessage
import com.codenames.frontend.network.dto.GameMessage
import com.codenames.frontend.network.dto.GuessMessage
import com.codenames.frontend.network.dto.PassTurnMessage
Expand Down Expand Up @@ -150,6 +151,22 @@ class GameWebSocketControllerTest {
}
}

@Test
fun testExposeCheat() =
runTest {
val message = ExposeCheatMessage("LOBBY123", "Max")

wsClient.exposeCheat(message)

coVerify {
session.convertAndSend(
"/app/expose-cheat",
message,
ExposeCheatMessage.serializer(),
)
}
}

@Test
fun testPassTurn() =
runTest {
Expand Down
Loading
Loading