Repository: CataniaUnited/catania-united-app
Pull request #30: #15 - rolling the dice
Author: login: matthiasva
Created: 2025-04-22T13:52:46Z | Merged: 2025-05-09T20:09:54Z



Diff:
diff --git a/.idea/androidTestResultsUserPreferences.xml b/.idea/androidTestResultsUserPreferences.xml
index ef579aa..51a88b6 100644
--- a/.idea/androidTestResultsUserPreferences.xml
+++ b/.idea/androidTestResultsUserPreferences.xml
@@ -16,19 +16,6 @@
             </AndroidTestResultsTableState>
           </value>
         </entry>
-        <entry key="-1909069134">
-          <value>
-            <AndroidTestResultsTableState>
-              <option name="preferredColumnWidths">
-                <map>
-                  <entry key="Duration" value="90" />
-                  <entry key="Medium_Phone_API_36" value="120" />
-                  <entry key="Tests" value="360" />
-                </map>
-              </option>
-            </AndroidTestResultsTableState>
-          </value>
-        </entry>
         <entry key="-1159659618">
           <value>
             <AndroidTestResultsTableState>
@@ -36,7 +23,6 @@
                 <map>
                   <entry key="Duration" value="90" />
                   <entry key="Medium_Phone" value="120" />
-                  <entry key="Medium_Phone_API_36" value="120" />
                   <entry key="Tests" value="360" />
                 </map>
               </option>
@@ -77,20 +63,6 @@
                 <map>
                   <entry key="Duration" value="90" />
                   <entry key="Medium_Phone" value="120" />
-                  <entry key="Medium_Phone_API_36" value="120" />
-                  <entry key="Tests" value="360" />
-                </map>
-              </option>
-            </AndroidTestResultsTableState>
-          </value>
-        </entry>
-        <entry key="-579567806">
-          <value>
-            <AndroidTestResultsTableState>
-              <option name="preferredColumnWidths">
-                <map>
-                  <entry key="Duration" value="90" />
-                  <entry key="Medium_Phone_API_36" value="120" />
                   <entry key="Tests" value="360" />
                 </map>
               </option>
@@ -104,7 +76,6 @@
                 <map>
                   <entry key="Duration" value="90" />
                   <entry key="Medium_Phone" value="120" />
-                  <entry key="Medium_Phone_API_36" value="120" />
                   <entry key="Tests" value="360" />
                 </map>
               </option>
@@ -124,19 +95,6 @@
             </AndroidTestResultsTableState>
           </value>
         </entry>
-        <entry key="171881487">
-          <value>
-            <AndroidTestResultsTableState>
-              <option name="preferredColumnWidths">
-                <map>
-                  <entry key="Duration" value="90" />
-                  <entry key="Medium_Phone_API_36" value="120" />
-                  <entry key="Tests" value="360" />
-                </map>
-              </option>
-            </AndroidTestResultsTableState>
-          </value>
-        </entry>
         <entry key="721821273">
           <value>
             <AndroidTestResultsTableState>
@@ -189,32 +147,6 @@
             </AndroidTestResultsTableState>
           </value>
         </entry>
-        <entry key="1587289091">
-          <value>
-            <AndroidTestResultsTableState>
-              <option name="preferredColumnWidths">
-                <map>
-                  <entry key="Duration" value="90" />
-                  <entry key="Medium_Phone_API_36" value="120" />
-                  <entry key="Tests" value="360" />
-                </map>
-              </option>
-            </AndroidTestResultsTableState>
-          </value>
-        </entry>
-        <entry key="1793529971">
-          <value>
-            <AndroidTestResultsTableState>
-              <option name="preferredColumnWidths">
-                <map>
-                  <entry key="Duration" value="90" />
-                  <entry key="Medium_Phone_API_36" value="120" />
-                  <entry key="Tests" value="360" />
-                </map>
-              </option>
-            </AndroidTestResultsTableState>
-          </value>
-        </entry>
         <entry key="1797276866">
           <value>
             <AndroidTestResultsTableState>
@@ -222,7 +154,6 @@
                 <map>
                   <entry key="Duration" value="90" />
                   <entry key="Medium_Phone" value="120" />
-                  <entry key="Medium_Phone_API_36" value="120" />
                   <entry key="Tests" value="360" />
                 </map>
               </option>
diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
index 8db5239..fb5a1ba 100644
--- a/.idea/deploymentTargetSelector.xml
+++ b/.idea/deploymentTargetSelector.xml
@@ -4,14 +4,36 @@
     <selectionStates>
       <SelectionState runConfigName="app">
         <option name="selectionMode" value="DROPDOWN" />
+        <DropdownSelection timestamp="2025-04-06T08:46:11.161619Z">
+          <Target type="DEFAULT_BOOT">
+            <handle>
+              <DeviceId pluginId="LocalEmulator" identifier="path=/Users/klavdijamarkovic/.android/avd/Medium_Phone.avd" />
+            </handle>
+          </Target>
+        </DropdownSelection>
+        <DialogSelection />
       </SelectionState>
-      <SelectionState runConfigName="MainApplicationInstrumentedTest">
+      <SelectionState runConfigName="testMainActivityUi()">
         <option name="selectionMode" value="DROPDOWN" />
       </SelectionState>
-      <SelectionState runConfigName="WebSocketListenerImplParameterizedTest">
+      <SelectionState runConfigName="MainActivityUiCoverageTest">
         <option name="selectionMode" value="DROPDOWN" />
       </SelectionState>
-      <SelectionState runConfigName="MainActivity">
+      <SelectionState runConfigName="CataniaUnitedThemeTest">
+        <option name="selectionMode" value="DROPDOWN" />
+        <DropdownSelection timestamp="2025-04-06T18:24:07.928064Z">
+          <Target type="DEFAULT_BOOT">
+            <handle>
+              <DeviceId pluginId="LocalEmulator" identifier="path=/Users/klavdijamarkovic/.android/avd/Medium_Phone.avd" />
+            </handle>
+          </Target>
+        </DropdownSelection>
+        <DialogSelection />
+      </SelectionState>
+      <SelectionState runConfigName="MainActivityInstrumentedTest">
+        <option name="selectionMode" value="DROPDOWN" />
+      </SelectionState>
+      <SelectionState runConfigName="TutorialScreenTest">
         <option name="selectionMode" value="DROPDOWN" />
       </SelectionState>
     </selectionStates>
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
deleted file mode 100644
index 7061a0d..0000000
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ /dev/null
@@ -1,61 +0,0 @@
-<component name="InspectionProjectProfileManager">
-  <profile version="1.0">
-    <option name="myName" value="Project Default" />
-    <inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
-      <option name="composableFile" value="true" />
-      <option name="previewFile" value="true" />
-    </inspection_tool>
-    <inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
-      <option name="composableFile" value="true" />
-      <option name="previewFile" value="true" />
-    </inspection_tool>
-    <inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
-      <option name="composableFile" value="true" />
-      <option name="previewFile" value="true" />
-    </inspection_tool>
-    <inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
-      <option name="composableFile" value="true" />
-      <option name="previewFile" value="true" />
-    </inspection_tool>
-    <inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
-      <option name="composableFile" value="true" />
-    </inspection_tool>
-    <inspection_tool class="GlancePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
-      <option name="composableFile" value="true" />
-    </inspection_tool>
-    <inspection_tool class="GlancePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
-      <option name="composableFile" value="true" />
-    </inspection_tool>
-    <inspection_tool class="GlancePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
-      <option name="composableFile" value="true" />
-    </inspection_tool>
-    <inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
-      <option name="composableFile" value="true" />
-      <option name="previewFile" value="true" />
-    </inspection_tool>
-    <inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
-      <option name="composableFile" value="true" />
-      <option name="previewFile" value="true" />
-    </inspection_tool>
-    <inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true">
-      <option name="composableFile" value="true" />
-      <option name="previewFile" value="true" />
-    </inspection_tool>
-    <inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
-      <option name="composableFile" value="true" />
-      <option name="previewFile" value="true" />
-    </inspection_tool>
-    <inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
-      <option name="composableFile" value="true" />
-      <option name="previewFile" value="true" />
-    </inspection_tool>
-    <inspection_tool class="PreviewParameterProviderOnFirstParameter" enabled="true" level="ERROR" enabled_by_default="true">
-      <option name="composableFile" value="true" />
-      <option name="previewFile" value="true" />
-    </inspection_tool>
-    <inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
-      <option name="composableFile" value="true" />
-      <option name="previewFile" value="true" />
-    </inspection_tool>
-  </profile>
-</component>
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/example/cataniaunited/WebSocketListenerImplInstrumentedTest.kt b/app/src/androidTest/java/com/example/cataniaunited/WebSocketListenerImplInstrumentedTest.kt
index 3772999..4107e0d 100644
--- a/app/src/androidTest/java/com/example/cataniaunited/WebSocketListenerImplInstrumentedTest.kt
+++ b/app/src/androidTest/java/com/example/cataniaunited/WebSocketListenerImplInstrumentedTest.kt
@@ -30,6 +30,7 @@ class WebSocketListenerImplInstrumentedTest {
     private val dummyOnGameBoardReceived: (String, String) -> Unit = { _, _ -> }
     private val dummyOnError: (Throwable) -> Unit = { e -> println("Parameterized Test onError: ${e.message}") }
     private val dummyOnClosed: (Int, String) -> Unit = { _, _ -> }
+    private val dummyOnDiceResult: (Int, Int) -> Unit = { _, _ -> }
 
     @Before
     fun setup() {
@@ -40,7 +41,8 @@ class WebSocketListenerImplInstrumentedTest {
             onLobbyCreated = { _ -> },
             onGameBoardReceived = { _, _ -> },
             onError = { _ -> },
-            onClosed = { _, _ -> }
+            onClosed = { _, _ -> },
+            onDiceResult = { _, _ -> }
         )
     }
 
@@ -66,7 +68,8 @@ class WebSocketListenerImplInstrumentedTest {
             onLobbyCreated = dummyOnLobbyCreated,
             onGameBoardReceived = dummyOnGameBoardReceived,
             onError = dummyOnError,
-            onClosed = dummyOnClosed
+            onClosed = dummyOnClosed,
+            onDiceResult = dummyOnDiceResult
         )
 
         specificTestListener.onMessage(mockWebSocket, messageJsonString)
@@ -89,7 +92,8 @@ class WebSocketListenerImplInstrumentedTest {
             onLobbyCreated = { lobbyId -> receivedLobbyId = lobbyId },
             onGameBoardReceived = { _, _ -> },
             onError = { e -> fail("onError called: ${e.message}") },
-            onClosed = { _, _ -> }
+            onClosed = { _, _ -> },
+            onDiceResult = { _, _ -> }
         )
         val messageDTO = MessageDTO(type = MessageType.LOBBY_CREATED, lobbyId = expectedLobbyId)
         val messageJson = jsonParser.encodeToString(MessageDTO.serializer(), messageDTO)
@@ -125,7 +129,8 @@ class WebSocketListenerImplInstrumentedTest {
                 receivedBoardJson = boardJson
             },
             onError = { e -> fail("onError called: ${e.message}") },
-            onClosed = { _, _ -> }
+            onClosed = { _, _ -> },
+            onDiceResult = { _, _ -> }
         )
 
         testListener.onMessage(mockWebSocket, messageJson)
@@ -142,7 +147,8 @@ class WebSocketListenerImplInstrumentedTest {
             onLobbyCreated = { _ -> fail("onLobbyCreated called") },
             onGameBoardReceived = { _, _ -> fail("onGameBoardReceived called")},
             onError = { error -> receivedError = error },
-            onClosed = { _, _ -> fail("onClosed called") }
+            onClosed = { _, _ -> fail("onClosed called") },
+            onDiceResult = { _, _ -> fail("onDiceResult called") }
         )
         val invalidJson = "{ type: INVALID JSON"
 
@@ -166,7 +172,8 @@ class WebSocketListenerImplInstrumentedTest {
             onClosed = { code, reason ->
                 closedCode = code
                 closedReason = reason
-            }
+            },
+            onDiceResult = { _, _ -> }
         )
 
         testListener.onClosed(mockWebSocket, expectedCode, expectedReason)
@@ -174,4 +181,69 @@ class WebSocketListenerImplInstrumentedTest {
         assertEquals("Close code mismatch", expectedCode, closedCode)
         assertEquals("Close reason mismatch", expectedReason, closedReason)
     }
+
+    @Test
+    fun handleDiceResult_callsOnDiceResultWithParsedValues() {
+        var receivedDice1 = -1
+        var receivedDice2 = -1
+
+        val listener = WebSocketListenerImpl(
+            onConnectionSuccess = {},
+            onLobbyCreated = {},
+            onGameBoardReceived = { _, _ -> },
+            onError = {},
+            onClosed = { _, _ -> },
+            onDiceResult = { d1, d2 ->
+                receivedDice1 = d1
+                receivedDice2 = d2
+            }
+        )
+
+        val message = buildJsonObject {
+            put("dice1", 3)
+            put("dice2", 4)
+        }
+
+        val dto = MessageDTO(
+            type = MessageType.DICE_RESULT,
+            player = "some-player",
+            lobbyId = "some-lobby",
+            message = message
+        )
+
+        listener.handleDiceResult(dto)
+
+        assertEquals(3, receivedDice1)
+        assertEquals(4, receivedDice2)
+    }
+
+    @Test
+    fun handleDiceResult_ignoresDuplicateResults() {
+        var callCount = 0
+
+        val listener = WebSocketListenerImpl(
+            onConnectionSuccess = {},
+            onLobbyCreated = {},
+            onGameBoardReceived = { _, _ -> },
+            onError = {},
+            onClosed = { _, _ -> },
+            onDiceResult = { _, _ -> callCount++ }
+        )
+
+        val message = buildJsonObject {
+            put("dice1", 2)
+            put("dice2", 5)
+        }
+
+        val dto = MessageDTO(
+            type = MessageType.DICE_RESULT,
+            player = "player",
+            lobbyId = "lobby",
+            message = message
+        )
+
+        listener.handleDiceResult(dto)
+
+        assertEquals(1, callCount)
+    }
 }
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/example/cataniaunited/WebSocketListenerImplParameterizedTest.kt b/app/src/androidTest/java/com/example/cataniaunited/WebSocketListenerImplParameterizedTest.kt
index 863b41a..0b689d1 100644
--- a/app/src/androidTest/java/com/example/cataniaunited/WebSocketListenerImplParameterizedTest.kt
+++ b/app/src/androidTest/java/com/example/cataniaunited/WebSocketListenerImplParameterizedTest.kt
@@ -29,6 +29,7 @@ class WebSocketListenerImplParameterizedTest {
     private val dummyOnGameBoardReceived: (String, String) -> Unit = { _, _ -> }
     private val dummyOnError: (Throwable) -> Unit = { e -> println("Parameterized Test onError: ${e.message}") }
     private val dummyOnClosed: (Int, String) -> Unit = { _, _ -> }
+    private val dummyOnDiceResult: (Int, Int) -> Unit = { _, _ -> }
 
     @Before
     fun setup() {
@@ -44,7 +45,8 @@ class WebSocketListenerImplParameterizedTest {
             onLobbyCreated = dummyOnLobbyCreated,
             onGameBoardReceived = dummyOnGameBoardReceived,
             onError = dummyOnError,
-            onClosed = dummyOnClosed
+            onClosed = dummyOnClosed,
+            onDiceResult = dummyOnDiceResult
         )
         println("Parameterized Test Setup Complete.")
     }
diff --git a/app/src/androidTest/java/com/example/cataniaunited/logic/game/GameBoardInstrumentedTest.kt b/app/src/androidTest/java/com/example/cataniaunited/logic/game/GameBoardInstrumentedTest.kt
index d989c37..088da13 100644
--- a/app/src/androidTest/java/com/example/cataniaunited/logic/game/GameBoardInstrumentedTest.kt
+++ b/app/src/androidTest/java/com/example/cataniaunited/logic/game/GameBoardInstrumentedTest.kt
@@ -88,7 +88,7 @@ class GameBoardInstrumentedTest {
     private val dummyOnLobbyCreated: (String) -> Unit = { lid -> println("DummyCallback: onLobbyCreated $lid") }
     private val dummyOnGameBoardReceived: (String, String) -> Unit = { lid, json -> println("DummyCallback: onGameBoardReceived $lid, json len: ${json.length}") }
     private val dummyOnClosed: (Int, String) -> Unit = { code, reason -> println("DummyCallback: onClosed $code $reason") }
-
+    private val dummyOnDiceResult: (Int, Int) -> Unit = { _, _ -> }
 
     @Test
     fun testPlaceSettlement() {
@@ -127,7 +127,8 @@ class GameBoardInstrumentedTest {
                     println("!!! Test onError CALLED: ${e.message}")
                     errorLatch.countDown()
                 },
-                onClosed = dummyOnClosed
+                onClosed = dummyOnClosed,
+                onDiceResult = dummyOnDiceResult
             ) {
                 override fun onOpen(webSocket: WebSocket, response: Response) {
                     super.onOpen(webSocket, response)
@@ -198,7 +199,8 @@ class GameBoardInstrumentedTest {
                     println("!!! Test onError CALLED: ${e.message}")
                     errorLatch.countDown()
                 },
-                onClosed = dummyOnClosed
+                onClosed = dummyOnClosed,
+                onDiceResult = dummyOnDiceResult
             ) {
                 override fun onOpen(webSocket: WebSocket, response: Response) {
                     super.onOpen(webSocket, response)
@@ -230,4 +232,79 @@ class GameBoardInstrumentedTest {
         t.join()
         println("testPlaceRoad finished successfully.")
     }
+
+    @Test
+    fun testRollDice() {
+        println("Running testRollDice...")
+        val lobbyId = "lobby-dice-${System.currentTimeMillis()}"
+
+        val messagePayload = buildJsonObject { put("action", "rollDice") }
+        val expectedMessageDTO = MessageDTO(MessageType.ROLL_DICE, playerId, lobbyId, null, messagePayload)
+        val expectedJson = testJsonParser.encodeToString(MessageDTO.serializer(), expectedMessageDTO)
+
+        val messageLatch = CountDownLatch(1)
+        val openLatch = CountDownLatch(1)
+        val errorLatch = CountDownLatch(1)
+        val receivedMessage = mutableListOf<String>()
+
+        mockWebServer.enqueue(MockResponse()
+            .setResponseCode(101)
+            .withWebSocketUpgrade(object : WebSocketListener() {
+                override fun onOpen(webSocket: WebSocket, response: Response) { println("MockServer: Opened") }
+                override fun onMessage(webSocket: WebSocket, text: String) {
+                    println("MockServer: Received '$text'")
+                    synchronized(receivedMessage) { receivedMessage.add(text) }
+                    messageLatch.countDown()
+                }
+                override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { println("MockServer: onFailure ${t.message}") }
+            })
+        )
+
+        println("Connecting spyClient...")
+        val t = thread {
+            spyClient.connect(object : WebSocketListenerImpl(
+                onConnectionSuccess = dummyOnConnectionSuccess,
+                onLobbyCreated = dummyOnLobbyCreated,
+                onGameBoardReceived = dummyOnGameBoardReceived,
+                onError = { e ->
+                    println("!!! Test onError CALLED: ${e.message}")
+                    errorLatch.countDown()
+                },
+                onClosed = dummyOnClosed,
+                onDiceResult = dummyOnDiceResult
+            ) {
+                override fun onOpen(webSocket: WebSocket, response: Response) {
+                    super.onOpen(webSocket, response)
+                    println("Client Listener: Opened connection (on background thread).")
+                    openLatch.countDown()
+                    try {
+                        println("Client Listener: Sending rollDice...")
+                        gameBoardLogic.rollDice(lobbyId)
+                    } catch (e: Exception) {
+                        println("!!! Error calling rollDice: ${e.message}")
+                        errorLatch.countDown()
+                    }
+                }
+                override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
+                    super.onFailure(webSocket, t, response)
+                    println("!!! Client Listener onFailure triggered in dice test.")
+                    errorLatch.countDown()
+                }
+            })
+        }
+
+        val connectionOpened = openLatch.await(5, TimeUnit.SECONDS)
+        assertTrue("WebSocket connection was not opened within timeout", connectionOpened)
+
+        val messageReceived = messageLatch.await(8, TimeUnit.SECONDS)
+        val errorOccurred = errorLatch.count == 0L
+
+        assertFalse("onError callback was triggered during test", errorOccurred)
+        assertTrue("Message not received by server within timeout", messageReceived)
+        assertEquals("Expected exactly one message", 1, receivedMessage.size)
+        assertEquals("Sent message JSON does not match expected", expectedJson, receivedMessage[0])
+
+        t.join()
+        println("testRollDice finished successfully.")
+    }
 }
\ No newline at end of file
diff --git a/app/src/main/java/com/example/cataniaunited/MainApplication.kt b/app/src/main/java/com/example/cataniaunited/MainApplication.kt
index 831b11f..e2ddfbf 100644
--- a/app/src/main/java/com/example/cataniaunited/MainApplication.kt
+++ b/app/src/main/java/com/example/cataniaunited/MainApplication.kt
@@ -2,6 +2,7 @@ package com.example.cataniaunited
 
 import android.app.Application
 import android.util.Log
+import com.example.cataniaunited.logic.game.GameViewModel
 import com.example.cataniaunited.ws.WebSocketClient
 import com.example.cataniaunited.ws.WebSocketListenerImpl
 import dagger.hilt.android.HiltAndroidApp
@@ -31,6 +32,7 @@ open class MainApplication : Application() {
     private val _currentLobbyIdFlow = MutableStateFlow<String?>(null) // Private Mutable StateFlow
     val currentLobbyIdFlow: StateFlow<String?> = _currentLobbyIdFlow.asStateFlow() // Public Immutable StateFlow
 
+    var gameViewModel: GameViewModel? = null
     var currentLobbyId: String?
         get() = _currentLobbyIdFlow.value // Getter reads from flow
         set(value) { // Setter updates the flow
@@ -85,6 +87,13 @@ open class MainApplication : Application() {
                 _playerId = null
                 currentLobbyId = null
                 latestBoardJson = null
+            },
+
+            onDiceResult = { dice1, dice2 ->
+                Log.d("MainApplication", "Callback: onDiceResult. Dice1: $dice1, Dice2: $dice2")
+                applicationScope.launch {
+                    gameViewModel?.updateDiceResult(dice1, dice2)
+                }
             }
         )
         webSocketClient.connect(listener)
diff --git a/app/src/main/java/com/example/cataniaunited/logic/dto/MessageType.kt b/app/src/main/java/com/example/cataniaunited/logic/dto/MessageType.kt
index 45cf190..962b529 100644
--- a/app/src/main/java/com/example/cataniaunited/logic/dto/MessageType.kt
+++ b/app/src/main/java/com/example/cataniaunited/logic/dto/MessageType.kt
@@ -9,6 +9,7 @@ enum class MessageType {
     PLAYER_JOINED,
     LOBBY_CREATED,
     GAME_BOARD_JSON,
+    DICE_RESULT,
 
     //Client Messages
     CREATE_LOBBY,
@@ -16,5 +17,6 @@ enum class MessageType {
     SET_USERNAME,
     PLACE_SETTLEMENT,
     PLACE_ROAD,
-    CREATE_GAME_BOARD
+    CREATE_GAME_BOARD,
+    ROLL_DICE
 }
\ No newline at end of file
diff --git a/app/src/main/java/com/example/cataniaunited/logic/game/GameBoardLogic.kt b/app/src/main/java/com/example/cataniaunited/logic/game/GameBoardLogic.kt
index 14430c4..02d6ed0 100644
--- a/app/src/main/java/com/example/cataniaunited/logic/game/GameBoardLogic.kt
+++ b/app/src/main/java/com/example/cataniaunited/logic/game/GameBoardLogic.kt
@@ -63,4 +63,26 @@ class GameBoardLogic @Inject constructor() {
             Log.e("GameBoardLogic", "WebSocket not connected when trying to create game board.")
         }
     }
+
+    fun rollDice(lobbyId: String) {
+        try {
+            Log.d("GameBoard", "Rolling dice for lobby: $lobbyId")
+            val playerId = MainApplication.getInstance().getPlayerId()
+            val message = buildJsonObject {
+                put("action", "rollDice")
+            }
+            val webSocketClient = MainApplication.getInstance().getWebSocketClient()
+            webSocketClient.sendMessage(
+                MessageDTO(
+                    MessageType.ROLL_DICE,
+                    playerId,
+                    lobbyId,
+                    null,
+                    message
+                )
+            )
+        } catch (e: Exception) {
+            Log.e("GameBoard", "Error rolling dice", e)
+        }
+    }
 }
\ No newline at end of file
diff --git a/app/src/main/java/com/example/cataniaunited/logic/game/GameBoardViewModel.kt b/app/src/main/java/com/example/cataniaunited/logic/game/GameBoardViewModel.kt
index e12a4b0..6a85ac5 100644
--- a/app/src/main/java/com/example/cataniaunited/logic/game/GameBoardViewModel.kt
+++ b/app/src/main/java/com/example/cataniaunited/logic/game/GameBoardViewModel.kt
@@ -18,15 +18,20 @@ import javax.inject.Inject
 
 @HiltViewModel
 class GameViewModel @Inject constructor(
-    private val gameBoardLogic: GameBoardLogic
-) : ViewModel() {
+    private val gameBoardLogic: GameBoardLogic,
+
+    ) : ViewModel() {
 
     private val _gameBoardState = MutableStateFlow<GameBoardModel?>(null)
     val gameBoardState: StateFlow<GameBoardModel?> = _gameBoardState.asStateFlow()
 
+    private val _diceResult = MutableStateFlow<Pair<Int, Int>?>(null)
+    val diceResult: StateFlow<Pair<Int, Int>?> = _diceResult
+
     init {
         Log.d("GameViewModel", "ViewModel Initialized (Hilt).")
         // Don't load initial board automatically here
+
     }
 
     // New function to be called externally (e.g., from the Composable's LaunchedEffect)
@@ -88,5 +93,27 @@ class GameViewModel @Inject constructor(
         // 4) Get lobbyId and PlayerId
         // 5) Call gameBoardLogic.placeRoad(road.id, lobbyId)
     }
+    var isProcessingRoll = false
+    fun rollDice(lobbyId: String) {
+        if (isProcessingRoll) return
+
+        isProcessingRoll = true
+        Log.d("GameViewModel", "Initiating dice roll for lobby: $lobbyId")
+        gameBoardLogic.rollDice(lobbyId)
+
+        viewModelScope.launch {
+            isProcessingRoll = false
+        }
+    }
+
+    fun updateDiceResult(dice1: Int?, dice2: Int?) {
+        viewModelScope.launch {
+            if (dice1 != null && dice2 != null) {
+                _diceResult.value = Pair(dice1, dice2)
+            } else {
+                _diceResult.value = null
+            }
+        }
+    }
 }
 
diff --git a/app/src/main/java/com/example/cataniaunited/ui/dice/DiceRollerPopup.kt b/app/src/main/java/com/example/cataniaunited/ui/dice/DiceRollerPopup.kt
new file mode 100644
index 0000000..5e0e9ad
--- /dev/null
+++ b/app/src/main/java/com/example/cataniaunited/ui/dice/DiceRollerPopup.kt
@@ -0,0 +1,109 @@
+package com.example.cataniaunited.ui.dice
+
+import androidx.compose.animation.core.*
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.*
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Dialog
+import com.example.cataniaunited.R
+import com.example.cataniaunited.ui.theme.catanClay
+import kotlinx.coroutines.delay
+
+@Composable
+fun DiceRollerPopup(
+    onDiceRolled: () -> Unit,
+    onClose: () -> Unit,
+    dice1Result: Int?,
+    dice2Result: Int?
+) {
+    var dice1 by remember { mutableIntStateOf(1) }
+    var dice2 by remember { mutableIntStateOf(1) }
+    var rolling by remember { mutableStateOf(true) }
+    var showFinalResult by remember { mutableStateOf(false) }
+
+    val rotation = rememberInfiniteTransition()
+    val angle by rotation.animateFloat(
+        initialValue = 0f,
+        targetValue = if (rolling) 360f else 0f,
+        animationSpec = infiniteRepeatable(
+            animation = tween(durationMillis = 300, easing = LinearEasing)
+        )
+    )
+
+    LaunchedEffect(Unit) {
+        onDiceRolled()
+    }
+
+    LaunchedEffect(dice1Result, dice2Result) {
+        if (dice1Result != null && dice2Result != null) {
+            delay(800)
+            dice1 = dice1Result
+            dice2 = dice2Result
+            rolling = false
+            showFinalResult = true
+            delay(2000)
+            onClose()
+        }
+    }
+
+    Dialog(onDismissRequest = onClose) {
+        Column(
+            modifier = Modifier
+                .background(catanClay, RoundedCornerShape(16.dp))
+                .padding(24.dp),
+            horizontalAlignment = Alignment.CenterHorizontally
+        ) {
+            Text(
+                text = if (rolling) "Rolling Dice..." else "Result!",
+                style = MaterialTheme.typography.titleLarge
+            )
+
+            Spacer(modifier = Modifier.height(24.dp))
+
+            Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
+                DiceImageDisplay(dice1, rolling, angle)
+                DiceImageDisplay(dice2, rolling, angle)
+            }
+
+            Spacer(modifier = Modifier.height(16.dp))
+
+            if (showFinalResult) {
+                Text(
+                    text = "Total: ${dice1 + dice2}",
+                    style = MaterialTheme.typography.titleLarge
+                )
+            }
+        }
+    }
+}
+
+@Composable
+fun DiceImageDisplay(value: Int, rolling: Boolean, angle: Float) {
+    val displayValue = if (rolling) (1..6).random() else value
+    val imageRes = when (displayValue) {
+        1 -> R.drawable.dice_1
+        2 -> R.drawable.dice_2
+        3 -> R.drawable.dice_3
+        4 -> R.drawable.dice_4
+        5 -> R.drawable.dice_5
+        else -> R.drawable.dice_6
+    }
+
+    Image(
+        painter = painterResource(id = imageRes),
+        contentDescription = "Dice $displayValue",
+        modifier = Modifier
+            .size(100.dp)
+            .graphicsLayer {
+                rotationZ = if (rolling) angle else 0f
+            }
+    )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/cataniaunited/ui/dice/ShakeDetector.kt b/app/src/main/java/com/example/cataniaunited/ui/dice/ShakeDetector.kt
new file mode 100644
index 0000000..fd4ccb0
--- /dev/null
+++ b/app/src/main/java/com/example/cataniaunited/ui/dice/ShakeDetector.kt
@@ -0,0 +1,51 @@
+package com.example.cataniaunited.ui.dice
+
+import android.content.Context
+import android.hardware.Sensor
+import android.hardware.SensorEvent
+import android.hardware.SensorEventListener
+import android.hardware.SensorManager
+import android.util.Log
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.mutableLongStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.platform.LocalContext
+import kotlin.math.sqrt
+
+@Composable
+fun ShakeDetector(
+    shakeThreshold: Float = 15f,
+    onShake: () -> Unit
+) {
+    val context = LocalContext.current
+    val sensorManager = remember { context.getSystemService(Context.SENSOR_SERVICE) as SensorManager }
+    val accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
+    val lastShakeTime = remember { mutableLongStateOf(0L) }
+
+    DisposableEffect(Unit) {
+        val listener = object : SensorEventListener {
+            override fun onSensorChanged(event: SensorEvent) {
+                val (x, y, z) = event.values
+                val magnitude = sqrt(x * x + y * y + z * z)
+                if (magnitude > shakeThreshold) {
+                    val now = System.currentTimeMillis()
+                    if (now - lastShakeTime.longValue > 1000) {
+                        lastShakeTime.longValue = now
+                        onShake()
+                    }
+                }
+            }
+
+            override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
+                Log.d("SensorListener", "Sensor accuracy changed: $sensor, Accuracy level: $accuracy")
+            }
+        }
+
+        sensorManager.registerListener(listener, accelerometer, SensorManager.SENSOR_DELAY_UI)
+
+        onDispose {
+            sensorManager.unregisterListener(listener)
+        }
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/cataniaunited/ui/game/RollDiceButton.kt b/app/src/main/java/com/example/cataniaunited/ui/game/RollDiceButton.kt
new file mode 100644
index 0000000..afea6b0
--- /dev/null
+++ b/app/src/main/java/com/example/cataniaunited/ui/game/RollDiceButton.kt
@@ -0,0 +1,39 @@
+package com.example.cataniaunited.ui.game
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.zIndex
+import com.example.cataniaunited.R
+
+@Composable
+fun RollDiceButton(
+    onClick: () -> Unit
+) {
+    Box(
+        modifier = Modifier
+            .clickable(onClick = onClick)
+            .border(
+                width = 1.dp,
+                color = MaterialTheme.colorScheme.outline,
+                shape = RoundedCornerShape(4.dp)
+            )
+            .padding(4.dp)
+            .zIndex(1f)
+    ) {
+        Image(
+            painter = painterResource(R.drawable.dice_6),
+            contentDescription = "Roll dice",
+            modifier = Modifier.size(32.dp)
+        )
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/cataniaunited/ui/game_board/board/GameScreen.kt b/app/src/main/java/com/example/cataniaunited/ui/game_board/board/GameScreen.kt
index 79eec07..79fd44d 100644
--- a/app/src/main/java/com/example/cataniaunited/ui/game_board/board/GameScreen.kt
+++ b/app/src/main/java/com/example/cataniaunited/ui/game_board/board/GameScreen.kt
@@ -3,17 +3,27 @@ package com.example.cataniaunited.ui.game_board.board
 import android.util.Log
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.padding
 import androidx.compose.material3.CircularProgressIndicator
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.zIndex
 import androidx.hilt.navigation.compose.hiltViewModel
 import com.example.cataniaunited.MainApplication
 import com.example.cataniaunited.logic.game.GameViewModel
+import com.example.cataniaunited.ui.dice.DiceRollerPopup
+import com.example.cataniaunited.ui.dice.ShakeDetector
+import com.example.cataniaunited.ui.game.RollDiceButton
 
 
 @Composable
@@ -23,6 +33,8 @@ fun GameScreen(
 ) {
     val gameBoardState by gameViewModel.gameBoardState.collectAsState()
     val application = LocalContext.current.applicationContext as MainApplication // Get app instance
+    var showDicePopup by remember { mutableStateOf(false) }
+    val diceResult by gameViewModel.diceResult.collectAsState()
 
     // Trigger initial load when the screen enters composition if state is null
     LaunchedEffect(Unit) { // Run once when GameScreen enters composition
@@ -31,6 +43,17 @@ fun GameScreen(
         }
     }
 
+    LaunchedEffect(gameViewModel) {
+        application.gameViewModel = gameViewModel
+    }
+
+    ShakeDetector(onShake = {
+        if (!showDicePopup) {
+            showDicePopup = true
+            gameViewModel.rollDice(lobbyId)
+        }
+    })
+
     Box(
         modifier = Modifier.fillMaxSize(),
         contentAlignment = Alignment.Center
@@ -60,7 +83,30 @@ fun GameScreen(
                         gameViewModel.handleRoadClick(road, lobbyId)
                     }
                 )
+
+                Box(
+                    modifier = Modifier
+                        .align(Alignment.TopStart)
+                        .offset(y = 32.dp)
+                        .zIndex(1f)
+                        .padding(horizontal = 4.dp)
+                ) {
+                    RollDiceButton {
+                        showDicePopup = true
+                    }
+                }
             }
         }
     }
+    if (showDicePopup) {
+        DiceRollerPopup(
+            onDiceRolled = { gameViewModel.rollDice(lobbyId) },
+            onClose = {
+                showDicePopup = false
+                gameViewModel.updateDiceResult(null, null)
+            },
+            dice1Result = diceResult?.first,
+            dice2Result = diceResult?.second
+        )
+    }
 }
\ No newline at end of file
diff --git a/app/src/main/java/com/example/cataniaunited/ui/test/TestPage.kt b/app/src/main/java/com/example/cataniaunited/ui/test/TestPage.kt
index ae993ad..fbb8786 100644
--- a/app/src/main/java/com/example/cataniaunited/ui/test/TestPage.kt
+++ b/app/src/main/java/com/example/cataniaunited/ui/test/TestPage.kt
@@ -32,6 +32,7 @@ import com.example.cataniaunited.viewmodel.TestPageViewModel
 
 @Composable
 fun TestPage(testPageViewModel: TestPageViewModel = TestPageViewModel()) {
+
     Box(modifier = Modifier.fillMaxSize()) {
         Image(
             painter = painterResource(id = R.drawable.catan_starting_page_background),
@@ -108,6 +109,28 @@ fun TestPage(testPageViewModel: TestPageViewModel = TestPageViewModel()) {
                     color = MaterialTheme.colorScheme.onPrimary,
                 )
             }
+
+            Button(
+                onClick = { },
+                shape = buttonShape,
+                colors = ButtonDefaults.buttonColors(containerColor = catanGold),
+                border = BorderStroke(1.dp, Color.Black),
+                modifier = Modifier
+                    .fillMaxWidth()
+                    .height(56.dp)
+                    .shadow(
+                        elevation = 13.dp,
+                        shape = buttonShape,
+                        ambientColor = Color.Black,
+                        spotColor = Color.Black
+                    )
+            ) {
+                Text(
+                    text = "ROLL DICE",
+                    style = MaterialTheme.typography.titleLarge,
+                    color = MaterialTheme.colorScheme.onPrimary,
+                )
+            }
         }
     }
 }
\ No newline at end of file
diff --git a/app/src/main/java/com/example/cataniaunited/ws/WebSocketClient.kt b/app/src/main/java/com/example/cataniaunited/ws/WebSocketClient.kt
index 5ec89fd..c061329 100644
--- a/app/src/main/java/com/example/cataniaunited/ws/WebSocketClient.kt
+++ b/app/src/main/java/com/example/cataniaunited/ws/WebSocketClient.kt
@@ -13,7 +13,6 @@ open class WebSocketClient(private val serverUrl: String) {
     private val client = OkHttpClient()
     private var webSocket: WebSocket? = null
     private val jsonParser = Json { encodeDefaults = true }
-
     open fun connect(listener: WebSocketListener) {
         val request = Request.Builder().url(serverUrl).build()
         webSocket = client.newWebSocket(request, listener)
diff --git a/app/src/main/java/com/example/cataniaunited/ws/WebSocketListenerImpl.kt b/app/src/main/java/com/example/cataniaunited/ws/WebSocketListenerImpl.kt
index a36cf68..18873b9 100644
--- a/app/src/main/java/com/example/cataniaunited/ws/WebSocketListenerImpl.kt
+++ b/app/src/main/java/com/example/cataniaunited/ws/WebSocketListenerImpl.kt
@@ -17,10 +17,12 @@ open class WebSocketListenerImpl(
     private val onError: (error: Throwable) -> Unit,
     private val onClosed: (code: Int, reason: String) -> Unit,
     private val onLobbyCreated: (lobbyId: String) -> Unit,
+    private val onDiceResult: ((dice1: Int, dice2: Int) -> Unit) = { _, _ -> }
 ) : WebSocketListener() {
 
     private val jsonParser = Json { ignoreUnknownKeys = true; isLenient = true }
 
+
     override fun onOpen(webSocket: WebSocket, response: Response) {
         Log.d("WebSocket", "Opened connection")
     }
@@ -35,6 +37,7 @@ open class WebSocketListenerImpl(
                 MessageType.CONNECTION_SUCCESSFUL -> handleConnectionSuccessful(messageDTO)
                 MessageType.GAME_BOARD_JSON -> handleGameBoardJson(messageDTO)
                 MessageType.LOBBY_CREATED -> handleLobbyCreated(messageDTO)
+                MessageType.DICE_RESULT -> handleDiceResult(messageDTO)
                 // TODO: Other Messages
 
                 MessageType.ERROR -> {
@@ -104,4 +107,12 @@ open class WebSocketListenerImpl(
             onError(IllegalArgumentException("Missing lobbyId in LOBBY_CREATED message"))
         }
     }
+
+    internal fun handleDiceResult(messageDTO: MessageDTO) {
+        val dice1 = messageDTO.message?.get("dice1")?.jsonPrimitive?.content?.toInt() ?: 0
+        val dice2 = messageDTO.message?.get("dice2")?.jsonPrimitive?.content?.toInt() ?: 0
+
+        Log.d("WebSocketListener", "Processing new dice result: $dice1, $dice2")
+        onDiceResult(dice1, dice2)
+    }
 }
\ No newline at end of file
diff --git a/app/src/main/res/drawable/dice_1.png b/app/src/main/res/drawable/dice_1.png
new file mode 100644
index 0000000..72294fb
Binary files /dev/null and b/app/src/main/res/drawable/dice_1.png differ
diff --git a/app/src/main/res/drawable/dice_2.png b/app/src/main/res/drawable/dice_2.png
new file mode 100644
index 0000000..f6fcebe
Binary files /dev/null and b/app/src/main/res/drawable/dice_2.png differ
diff --git a/app/src/main/res/drawable/dice_3.png b/app/src/main/res/drawable/dice_3.png
new file mode 100644
index 0000000..3767a55
Binary files /dev/null and b/app/src/main/res/drawable/dice_3.png differ
diff --git a/app/src/main/res/drawable/dice_4.png b/app/src/main/res/drawable/dice_4.png
new file mode 100644
index 0000000..c2117a3
Binary files /dev/null and b/app/src/main/res/drawable/dice_4.png differ
diff --git a/app/src/main/res/drawable/dice_5.png b/app/src/main/res/drawable/dice_5.png
new file mode 100644
index 0000000..414099f
Binary files /dev/null and b/app/src/main/res/drawable/dice_5.png differ
diff --git a/app/src/main/res/drawable/dice_6.png b/app/src/main/res/drawable/dice_6.png
new file mode 100644
index 0000000..0a81927
Binary files /dev/null and b/app/src/main/res/drawable/dice_6.png differ
diff --git a/app/src/test/java/com/example/cataniaunited/logic/game/GameBoardLogicTest.kt b/app/src/test/java/com/example/cataniaunited/logic/game/GameBoardLogicTest.kt
index c971c26..16ec3b7 100644
--- a/app/src/test/java/com/example/cataniaunited/logic/game/GameBoardLogicTest.kt
+++ b/app/src/test/java/com/example/cataniaunited/logic/game/GameBoardLogicTest.kt
@@ -152,4 +152,28 @@ class GameBoardLogicTest {
         gameBoardLogic.requestBoardForLobby(testLobbyId, testPlayerCount)
         verify(exactly = 0) { mockWebSocketClient.sendMessage(any()) }
     }
+
+    @Test
+    fun rollDiceSendsCorrectMessageWhenConnected() {
+        val expectedPayload = buildJsonObject { put("action", "rollDice") }
+        val expectedMessage = MessageDTO(
+            type = MessageType.ROLL_DICE,
+            player = testPlayerId,
+            lobbyId = testLobbyId,
+            players = null,
+            message = expectedPayload
+        )
+        gameBoardLogic.rollDice(testLobbyId)
+        val messageSlot = slot<MessageDTO>()
+        verify(exactly = 1) { mockWebSocketClient.sendMessage(capture(messageSlot)) }
+        assertEquals(expectedMessage, messageSlot.captured)
+    }
+
+    @Test
+    fun rollDiceDoesNotSendWhenGetPlayerIdThrows() {
+        val exception = IllegalStateException("Player ID not set")
+        every { mockMainApplication.getPlayerId() } throws exception
+        gameBoardLogic.rollDice(testLobbyId)
+        verify(exactly = 0) { mockWebSocketClient.sendMessage(any()) }
+    }
 }
\ No newline at end of file
diff --git a/app/src/test/java/com/example/cataniaunited/logic/game/GameViewModelTest.kt b/app/src/test/java/com/example/cataniaunited/logic/game/GameViewModelTest.kt
index be61bee..b295bdf 100644
--- a/app/src/test/java/com/example/cataniaunited/logic/game/GameViewModelTest.kt
+++ b/app/src/test/java/com/example/cataniaunited/logic/game/GameViewModelTest.kt
@@ -4,11 +4,13 @@ import android.util.Log
 import app.cash.turbine.test
 import com.example.cataniaunited.data.model.*
 import io.mockk.every
+import io.mockk.just
 import io.mockk.mockk
 import io.mockk.mockkStatic
 import io.mockk.unmockkStatic
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.test.*
 import org.junit.jupiter.api.AfterEach
 import org.junit.jupiter.api.Assertions.*
@@ -92,6 +94,90 @@ class GameViewModelTest {
         println("Test passed: initial state is null")
     }
 
+    @Test
+    fun testDiceResultIsInitiallyNull() = runTest {
+        assertNull(viewModel.diceResult.first(), "Initial diceResult should be null")
+    }
+
+    @Test
+    fun testRollDiceCallsGameBoardLogicWithCorrectLobbyId() = runTest {
+        val testLobbyId = "test-lobby-abc"
+        io.mockk.every { mockGameBoardLogic.rollDice(testLobbyId) } just io.mockk.Runs
+
+        viewModel.rollDice(testLobbyId)
+        io.mockk.verify(exactly = 1) { mockGameBoardLogic.rollDice(testLobbyId) }
+
+        assertNull(viewModel.diceResult.first())
+    }
+
+    @Test
+    fun rollDiceSetsIsProcessingRollFromFalseToTrue() = runTest {
+        val testLobbyId = "test-lobby-processing"
+        io.mockk.every { mockGameBoardLogic.rollDice(any()) } just io.mockk.Runs
+
+        val isProcessingRollField = GameViewModel::class.java.getDeclaredField("isProcessingRoll")
+        isProcessingRollField.isAccessible = true
+
+        assertEquals(false, isProcessingRollField.get(viewModel) as Boolean)
+
+        viewModel.rollDice(testLobbyId)
+
+        assertEquals(true, isProcessingRollField.get(viewModel) as Boolean)
+    }
+
+    @Test
+    fun testRollDiceDoesNothingWhenAlreadyProcessing() = runTest {
+        val testLobbyId = "test-lobby-456"
+        val isProcessingRollField = GameViewModel::class.java.getDeclaredField("isProcessingRoll")
+        isProcessingRollField.isAccessible = true
+        isProcessingRollField.set(viewModel, true)
+
+        viewModel.rollDice(testLobbyId)
+
+        io.mockk.verify(exactly = 0) { mockGameBoardLogic.rollDice(any()) }
+
+        assertNull(viewModel.diceResult.first())
+    }
+
+    @Test
+    fun testUpdateDiceResultSetsValidPairToStateFlow() = runTest {
+        val dice1 = 5
+        val dice2 = 2
+        val newDiceResult = Pair(dice1, dice2)
+
+        viewModel.updateDiceResult(dice1, dice2)
+
+        advanceUntilIdle()
+
+        assertEquals(newDiceResult, viewModel.diceResult.first())
+
+        val isProcessingRollField = GameViewModel::class.java.getDeclaredField("isProcessingRoll")
+        isProcessingRollField.isAccessible = true
+        assertEquals(false, isProcessingRollField.get(viewModel) as Boolean)
+    }
+
+    @Test
+    fun testUpdateDiceResultSetsNullWhenDice1IsNull() = runTest {
+        viewModel.updateDiceResult(null, 4)
+        advanceUntilIdle()
+        assertNull(viewModel.diceResult.first())
+    }
+
+    @Test
+    fun testUpdateDiceResultSetsNullWhenDice2IsNull() = runTest {
+        viewModel.updateDiceResult(3, null)
+        advanceUntilIdle()
+        assertNull(viewModel.diceResult.first())
+    }
+
+    @Test
+    fun testUpdateDiceResultSetsNullWhenBothDiceAreNull() = runTest {
+        viewModel.updateDiceResult(null, null)
+        advanceUntilIdle()
+        assertNull(viewModel.diceResult.first())
+    }
+
+
     @Nested
     @DisplayName("Initialization via initializeBoardState")
     inner class InitializationTests {


Reviews:
user: login: towurzer
body: Nice Implementation, just a few things:
1) Please Have a look at the 17 new Issues and Sonar cloud. Most of them should be easy to fix. (If you can't fix all, no problem, just leave a statement why)
2) Plese remove the delay (if possible)
3) Please improve the test coverage to 90% (or at least 80% if not possible to get higher)
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-05-09T08:13:44Z
user: login: matthiasva
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T14:15:22Z
user: login: matthiasva
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T14:16:00Z
user: login: towurzer
body: Looks good. Great job
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-09T19:31:22Z

Review comments:
diff_hunk: @@ -88,5 +96,29 @@ class GameViewModel @Inject constructor(
         // 4) Get lobbyId and PlayerId
         // 5) Call gameBoardLogic.placeRoad(road.id, lobbyId)
     }
+    private var isProcessingRoll = false
+
+    fun rollDice(lobbyId: String) {
+        if (isProcessingRoll) return
+
+        isProcessingRoll = true
+        Log.d("GameViewModel", "Initiating dice roll for lobby: $lobbyId")
+        gameBoardLogic.rollDice(lobbyId)
+
+        viewModelScope.launch {
+            delay(2000) // Cooldown period
path: app/src/main/java/com/example/cataniaunited/logic/game/GameBoardViewModel.kt
user: login: towurzer
body: Why would we wait for 2 Seconds? 
created_at: 2025-05-09T08:04:20Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,122 @@
+package com.example.cataniaunited.ui.dice
+
+import android.content.Context
+import android.os.VibrationEffect
+import android.os.Vibrator
+import androidx.compose.animation.core.*
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.*
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Dialog
+import com.example.cataniaunited.R
+import com.example.cataniaunited.ui.theme.catanClay
+import kotlinx.coroutines.delay
+
+@Composable
+fun DiceRollerPopup(
+    onDiceRolled: () -> Unit,
+    onClose: () -> Unit,
+    dice1Result: Int?,
+    dice2Result: Int?
+) {
+    val context = LocalContext.current
+    val scope = rememberCoroutineScope()
+
+    var dice1 by remember { mutableStateOf(1) }
+    var dice2 by remember { mutableStateOf(1) }
+    var rolling by remember { mutableStateOf(true) }
+    var showFinalResult by remember { mutableStateOf(false) }
+
+    val rotation = rememberInfiniteTransition()
+    val angle by rotation.animateFloat(
+        initialValue = 0f,
+        targetValue = if (rolling) 360f else 0f,
+        animationSpec = infiniteRepeatable(
+            animation = tween(durationMillis = 300, easing = LinearEasing)
+        )
+    )
+
+    LaunchedEffect(Unit) {
+        onDiceRolled()
+    }
+
+    LaunchedEffect(dice1Result, dice2Result) {
+        if (dice1Result != null && dice2Result != null) {
+            delay(800)
+            dice1 = dice1Result
+            dice2 = dice2Result
+            rolling = false
+            showFinalResult = true
+            vibratePhone(context)
+            delay(2000)
path: app/src/main/java/com/example/cataniaunited/ui/dice/DiceRollerPopup.kt
user: login: towurzer
body: why wait here?

created_at: 2025-05-09T08:05:24Z
author_association: MEMBER
diff_hunk: @@ -88,5 +96,29 @@ class GameViewModel @Inject constructor(
         // 4) Get lobbyId and PlayerId
         // 5) Call gameBoardLogic.placeRoad(road.id, lobbyId)
     }
+    private var isProcessingRoll = false
+
+    fun rollDice(lobbyId: String) {
+        if (isProcessingRoll) return
+
+        isProcessingRoll = true
+        Log.d("GameViewModel", "Initiating dice roll for lobby: $lobbyId")
+        gameBoardLogic.rollDice(lobbyId)
+
+        viewModelScope.launch {
+            delay(2000) // Cooldown period
path: app/src/main/java/com/example/cataniaunited/logic/game/GameBoardViewModel.kt
user: login: matthiasva
body: Had a problem with duplicate log messages but it is resolved now. Delay is removed.
created_at: 2025-05-09T14:15:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,122 @@
+package com.example.cataniaunited.ui.dice
+
+import android.content.Context
+import android.os.VibrationEffect
+import android.os.Vibrator
+import androidx.compose.animation.core.*
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.*
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Dialog
+import com.example.cataniaunited.R
+import com.example.cataniaunited.ui.theme.catanClay
+import kotlinx.coroutines.delay
+
+@Composable
+fun DiceRollerPopup(
+    onDiceRolled: () -> Unit,
+    onClose: () -> Unit,
+    dice1Result: Int?,
+    dice2Result: Int?
+) {
+    val context = LocalContext.current
+    val scope = rememberCoroutineScope()
+
+    var dice1 by remember { mutableStateOf(1) }
+    var dice2 by remember { mutableStateOf(1) }
+    var rolling by remember { mutableStateOf(true) }
+    var showFinalResult by remember { mutableStateOf(false) }
+
+    val rotation = rememberInfiniteTransition()
+    val angle by rotation.animateFloat(
+        initialValue = 0f,
+        targetValue = if (rolling) 360f else 0f,
+        animationSpec = infiniteRepeatable(
+            animation = tween(durationMillis = 300, easing = LinearEasing)
+        )
+    )
+
+    LaunchedEffect(Unit) {
+        onDiceRolled()
+    }
+
+    LaunchedEffect(dice1Result, dice2Result) {
+        if (dice1Result != null && dice2Result != null) {
+            delay(800)
+            dice1 = dice1Result
+            dice2 = dice2Result
+            rolling = false
+            showFinalResult = true
+            vibratePhone(context)
+            delay(2000)
path: app/src/main/java/com/example/cataniaunited/ui/dice/DiceRollerPopup.kt
user: login: matthiasva
body: Delay so players can see the popup with their dice roll result.
created_at: 2025-05-09T14:16:00Z
author_association: CONTRIBUTOR

PR comments:
user: login: matthiasva
created_at: 2025-04-22T15:05:48Z
body: Android Test funktionieren bei SonarQube nicht. Muss noch gefixed werden
author_association: CONTRIBUTOR
user: login: towurzer
created_at: 2025-04-30T13:30:51Z
body: > Android Test funktionieren bei SonarQube nicht. Muss noch gefixed werden

Is jetzt gefixt
author_association: MEMBER
user: login: sonarqubecloud[bot]
created_at: 2025-05-09T14:37:56Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=CataniaUnited_catania-united-app&pullRequest=30) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=CataniaUnited_catania-united-app&pullRequest=30&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=CataniaUnited_catania-united-app&pullRequest=30&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=CataniaUnited_catania-united-app&pullRequest=30&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [84.5% Coverage on New Code](https://sonarcloud.io/component_measures?id=CataniaUnited_catania-united-app&pullRequest=30&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=CataniaUnited_catania-united-app&pullRequest=30&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=CataniaUnited_catania-united-app&pullRequest=30)


author_association: NONE
user: login: matthiasva
created_at: 2025-05-09T14:43:42Z
body: I have tests for handleDiceResult in WebSocketListenerImplInstrumentedTest but it still apperars untested
author_association: CONTRIBUTOR