Repository: Tmpecho/idatt2003-mappe-2025-gr57
Pull request #64: feat: create cluedo game board with view
Author: login: eilifhl
Labels: name: enhancement
description: New feature or request
Created: 2025-04-28T15:34:08Z | Merged: 2025-05-16T18:38:36Z

This pull request introduces a significant update to the Cluedo game implementation, including the addition of a new `CluedoBoard` class and supporting tile types, as well as improvements to existing interfaces and enums. The changes focus on creating a structured representation of the Cluedo game board, supporting player movement, and defining tile behaviors.

### Cluedo Board Implementation:
* Introduced the `CluedoBoard` class to represent the Cluedo game board, including methods for initializing tiles (e.g., borders, rooms, corridors), managing player positions, and ensuring valid moves. Added logic for placing start positions and handling special cases like non-walkable tiles. (`src/main/java/edu/ntnu/idi/idatt/boardgame/games/cluedo/domain/board/CluedoBoard.java`)

### Tile Types:
* Added `AbstractCluedoTile` as a base class for tiles, providing common functionality like player management, observer notification, and walkability. (`src/main/java/edu/ntnu/idi/idatt/boardgame/games/cluedo/domain/board/AbstractCluedoTile.java`)
* Created specific tile types:
  - [`BorderTile`](diffhunk://#diff-ba7fc995da92838713db9108fcf9c96bee6202814bdb5ca10ddb26f048ae55f7R1-R23): Represents non-walkable border tiles. (`src/main/java/edu/ntnu/idi/idatt/boardgame/games/cluedo/domain/board/BorderTile.java`)
  - [`CorridorTile`](diffhunk://#diff-59462413f173c70fde3d283eee31a88ad09664953bd00b269b1a24da18697804R1-R14): Represents walkable corridor tiles. (`src/main/java/edu/ntnu/idi/idatt/boardgame/games/cluedo/domain/board/CorridorTile.java`)
  - [`RoomTile`](diffhunk://#diff-ef719115448cb772c8a75cd6a5733fb623a7227c59f5030f3d880d41740dde1dR1-R100): Represents rooms with defined perimeters and doorways, allowing controlled entry. (`src/main/java/edu/ntnu/idi/idatt/boardgame/games/cluedo/domain/board/RoomTile.java`)

### Interface and Enum Updates:
* Updated the `GameBoard` interface to support generic position types and improved flexibility. (`src/main/java/edu/ntnu/idi/idatt/boardgame/core/domain/board/GameBoard.java`)
* Enhanced the `TileObserver` interface to support generic position types, aligning with the new tile system. (`src/main/java/edu/ntnu/idi/idatt/boardgame/core/engine/event/TileObserver.java`)
* Added a new color (`WHITE`) to the `PlayerColor` enum to support additional player tokens. (`src/main/java/edu/ntnu/idi/idatt/boardgame/core/domain/player/PlayerColor.java`)#### **This PR adds the GameBoard for CLuedo, including Views.** 

This pull request introduces significant enhancements and new features for the Cluedo board game implementation, including the addition of new tile types, a concrete implementation of the `GameBoard` interface for Cluedo, and updates to support grid-based positions. The changes also improve type safety and flexibility by making interfaces more generic.

### Cluedo Board and Tile Implementation:

* Added `AbstractCluedoTile`, a base class for tiles in the Cluedo game, supporting players, observers, and walkability.
* Introduced `BorderTile`, representing non-walkable tiles on the board's edges. 
* Added `CorridorTile`, representing walkable tiles for player movement. (`src/main/java/edu/ntnu/idi/idatt/boardgame/games/cluedo/domain/board/CorridorTile.java`)
* Implemented `RoomTile`, representing rooms with walls and doors, supporting room-specific logic like entering through doors. (`src/main/java/edu/ntnu/idi/idatt/boardgame/games/cluedo/domain/board/RoomTile.java`)
* Created `CluedoBoard`, a concrete implementation of `GameBoard`, defining the board's layout, including rooms, corridors, and borders, and player movement logic. (`src/main/java/edu/ntnu/idi/idatt/boardgame/games/cluedo/domain/board/CluedoBoard.java`)

### Type Safety and Flexibility:

* Updated `TileObserver` to use a generic `Position` type instead of the specific `LinearPos`, improving flexibility for different board implementations. (`src/main/java/edu/ntnu/idi/idatt/boardgame/core/engine/event/TileObserver.java`)

### Enum Extension:

* Added `WHITE` to the `PlayerColor` enum, aligning with Cluedo's character tokens. (`src/main/java/edu/ntnu/idi/idatt/boardgame/core/domain/player/PlayerColor.java`)

### Code Cleanup:

* Adjusted import order in `GameBoard` for consistency. (`src/main/java/edu/ntnu/idi/idatt/boardgame/core/domain/board/GameBoard.java`)


Diff:
diff --git a/src/main/java/edu/ntnu/idi/idatt/boardgame/core/domain/board/GameBoard.java b/src/main/java/edu/ntnu/idi/idatt/boardgame/core/domain/board/GameBoard.java
index 91a1921..d8d2724 100644
--- a/src/main/java/edu/ntnu/idi/idatt/boardgame/core/domain/board/GameBoard.java
+++ b/src/main/java/edu/ntnu/idi/idatt/boardgame/core/domain/board/GameBoard.java
@@ -1,8 +1,9 @@
 package edu.ntnu.idi.idatt.boardgame.core.domain.board;
 
+import java.util.Map;
+
 import edu.ntnu.idi.idatt.boardgame.core.domain.player.Player;
 import edu.ntnu.idi.idatt.boardgame.core.domain.player.Position;
-import java.util.Map;
 
 public interface GameBoard<P extends Position> {
 
@@ -10,7 +11,7 @@ public interface GameBoard<P extends Position> {
 
   void incrementPlayerPosition(Player<P> player, int increment);
 
-    int getBoardSize();
+  int getBoardSize();
 
   void setPlayerPosition(Player<P> player, P position);
 }
diff --git a/src/main/java/edu/ntnu/idi/idatt/boardgame/core/domain/player/PlayerColor.java b/src/main/java/edu/ntnu/idi/idatt/boardgame/core/domain/player/PlayerColor.java
index 54c10eb..b1e03ae 100644
--- a/src/main/java/edu/ntnu/idi/idatt/boardgame/core/domain/player/PlayerColor.java
+++ b/src/main/java/edu/ntnu/idi/idatt/boardgame/core/domain/player/PlayerColor.java
@@ -1,6 +1,7 @@
 package edu.ntnu.idi.idatt.boardgame.core.domain.player;
 
 public enum PlayerColor {
+  WHITE,
   RED,
   BLUE,
   GREEN,
diff --git a/src/main/java/edu/ntnu/idi/idatt/boardgame/core/engine/event/TileObserver.java b/src/main/java/edu/ntnu/idi/idatt/boardgame/core/engine/event/TileObserver.java
index ffcbf41..c709897 100644
--- a/src/main/java/edu/ntnu/idi/idatt/boardgame/core/engine/event/TileObserver.java
+++ b/src/main/java/edu/ntnu/idi/idatt/boardgame/core/engine/event/TileObserver.java
@@ -1,8 +1,8 @@
 package edu.ntnu.idi.idatt.boardgame.core.engine.event;
 
 import edu.ntnu.idi.idatt.boardgame.core.domain.board.Tile;
-import edu.ntnu.idi.idatt.boardgame.core.domain.player.LinearPos;
+import edu.ntnu.idi.idatt.boardgame.core.domain.player.Position;
 
-public interface TileObserver {
-  void onTileChanged(Tile<LinearPos> tile);
+public interface TileObserver<P extends Position> {
+  void onTileChanged(Tile<P> tile);
 }
\ No newline at end of file
diff --git a/src/main/java/edu/ntnu/idi/idatt/boardgame/games/cluedo/domain/board/AbstractCluedoTile.java b/src/main/java/edu/ntnu/idi/idatt/boardgame/games/cluedo/domain/board/AbstractCluedoTile.java
new file mode 100644
index 0000000..610c870
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/boardgame/games/cluedo/domain/board/AbstractCluedoTile.java
@@ -0,0 +1,77 @@
+package edu.ntnu.idi.idatt.boardgame.games.cluedo.domain.board;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import edu.ntnu.idi.idatt.boardgame.core.domain.board.Tile;
+import edu.ntnu.idi.idatt.boardgame.core.domain.player.GridPos;
+import edu.ntnu.idi.idatt.boardgame.core.domain.player.Player;
+import edu.ntnu.idi.idatt.boardgame.core.engine.event.TileObserver;
+
+/** Grid-based tile with (row,col) coordinates and default observer support. */
+public abstract class AbstractCluedoTile implements Tile<GridPos> {
+
+  protected final int row; // 0-based board coordinates
+  protected final int col;
+  protected final List<Player<GridPos>> players = new ArrayList<>();
+
+  protected boolean walkable = true;
+
+  private final List<TileObserver> observers = new ArrayList<>();
+
+  protected AbstractCluedoTile(int row, int col) {
+    this.row = row;
+    this.col = col;
+  }
+
+  @Override
+  public void addPlayer(Player<GridPos> player) {
+    players.add(player);
+    notifyChange();
+  }
+
+  @Override
+  public void removePlayer(Player<GridPos> player) {
+    players.remove(player);
+    notifyChange();
+  }
+
+  @Override
+  public String getIdentifier() {
+    return "%s(%d,%d)".formatted(getClass().getSimpleName(), row, col);
+  }
+
+  @Override
+  public void addObserver(TileObserver obs) {
+    observers.add(obs);
+  }
+
+  @Override
+  public void removeObserver(TileObserver obs) {
+    observers.remove(obs);
+  }
+
+  protected void notifyChange() {
+    observers.forEach(o -> o.onTileChanged(this));
+  }
+
+  public int row() {
+    return row;
+  }
+
+  public int col() {
+    return col;
+  }
+
+  public List<Player<GridPos>> getPlayers() {
+    return List.copyOf(players);
+  }
+
+  public void setWalkable(boolean value) {
+    walkable = value;
+  }
+
+  public boolean isWalkable() {
+    return walkable;
+  }
+}
diff --git a/src/main/java/edu/ntnu/idi/idatt/boardgame/games/cluedo/domain/board/BorderTile.java b/src/main/java/edu/ntnu/idi/idatt/boardgame/games/cluedo/domain/board/BorderTile.java
new file mode 100644
index 0000000..38d4c59
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/boardgame/games/cluedo/domain/board/BorderTile.java
@@ -0,0 +1,23 @@
+package edu.ntnu.idi.idatt.boardgame.games.cluedo.domain.board;
+
+/**
+ * Represents a solid, non-walkable border tile on the Cluedo board.
+ */
+public final class BorderTile extends AbstractCluedoTile {
+
+  public BorderTile(int row, int col) {
+    super(row, col);
+    this.walkable = false; // Border tiles are not walkable
+  }
+
+  /** Border tiles are never walkable. */
+  @Override
+  public boolean isWalkable() {
+    return false;
+  }
+
+  @Override
+  public String getIdentifier() {
+    return "BorderTile(" + row + "," + col + ")";
+  }
+}
\ No newline at end of file
diff --git a/src/main/java/edu/ntnu/idi/idatt/boardgame/games/cluedo/domain/board/CluedoBoard.java b/src/main/java/edu/ntnu/idi/idatt/boardgame/games/cluedo/domain/board/CluedoBoard.java
new file mode 100644
index 0000000..f6e019f
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/boardgame/games/cluedo/domain/board/CluedoBoard.java
@@ -0,0 +1,466 @@
+package edu.ntnu.idi.idatt.boardgame.games.cluedo.domain.board;
+
+import edu.ntnu.idi.idatt.boardgame.core.domain.board.GameBoard;
+import edu.ntnu.idi.idatt.boardgame.core.domain.player.GridPos;
+import edu.ntnu.idi.idatt.boardgame.core.domain.player.Player;
+import edu.ntnu.idi.idatt.boardgame.core.domain.player.PlayerColor;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.IntStream;
+
+public final class CluedoBoard implements GameBoard<GridPos> {
+
+  private static final int BOARD_SIZE = 25;
+
+  private static final GridPos START_POS_MISS_SCARLETT = new GridPos(23, 7); // WHITE
+  private static final GridPos START_POS_COL_MUSTARD = new GridPos(17, 1); // RED
+  private static final GridPos START_POS_MRS_WHITE = new GridPos(1, 7); // YELLOW
+  private static final GridPos START_POS_REV_GREEN = new GridPos(1, 16); // GREEN
+  private static final GridPos START_POS_MRS_PEACOCK = new GridPos(6, 23); // BLUE
+  private static final GridPos START_POS_PROF_PLUM = new GridPos(19, 23); // PURPLE
+
+
+  // Inner record for defining doors
+  private record DoorDefinition(RoomTile.Point roomSide, RoomTile.Point corridorSide) {}
+
+  private static final List<RoomSpec> ROOM_SPECS =
+          List.of(
+                  new RoomSpec("Kitchen", new RoomDimensions(1, 1, 6, 5), true, List.of(
+                          new DoorDefinition(new RoomTile.Point(6, 4), new RoomTile.Point(7, 4))
+                  )),
+                  new RoomSpec("Ball Room", new RoomDimensions(1, 8, 7, 15), false, List.of(
+                          new DoorDefinition(new RoomTile.Point(5, 8), new RoomTile.Point(5, 7)),
+                          new DoorDefinition(new RoomTile.Point(7, 9), new RoomTile.Point(8, 9)),
+                          new DoorDefinition(new RoomTile.Point(7, 14), new RoomTile.Point(8, 14)),
+                          new DoorDefinition(new RoomTile.Point(5, 15), new RoomTile.Point(5, 16))
+                  )),
+                  new RoomSpec("Conservatory", new RoomDimensions(1, 18, 4, 23), true, List.of(
+                          new DoorDefinition(new RoomTile.Point(4, 18), new RoomTile.Point(4, 17))
+                  )),
+                  new RoomSpec("Dining Room", new RoomDimensions(10, 1, 15, 7), false, List.of(
+                          new DoorDefinition(new RoomTile.Point(12, 7), new RoomTile.Point(12, 8)),
+                          new DoorDefinition(new RoomTile.Point(15, 6), new RoomTile.Point(16, 6))
+                  )),
+                  new RoomSpec("Billiard Room", new RoomDimensions(7, 18, 12, 23), false, List.of(
+                          new DoorDefinition(new RoomTile.Point(8, 18), new RoomTile.Point(8, 17)),
+                          new DoorDefinition(new RoomTile.Point(12, 22), new RoomTile.Point(13, 22))
+                  )),
+                  new RoomSpec("Library", new RoomDimensions(14, 17, 18, 23), false, List.of(
+                          new DoorDefinition(new RoomTile.Point(14, 20), new RoomTile.Point(13, 20)),
+                          new DoorDefinition(new RoomTile.Point(16, 17), new RoomTile.Point(16, 16))
+                  )),
+                  new RoomSpec("Study", new RoomDimensions(21, 17, 23, 23), true, List.of(
+                          new DoorDefinition(new RoomTile.Point(21, 18), new RoomTile.Point(20, 18))
+                  )),
+                  new RoomSpec("Hall", new RoomDimensions(18, 9, 23, 14), false, List.of(
+                          new DoorDefinition(new RoomTile.Point(18, 11), new RoomTile.Point(17, 11)),
+                          new DoorDefinition(new RoomTile.Point(18, 12), new RoomTile.Point(17, 12)),
+                          new DoorDefinition(new RoomTile.Point(20, 9), new RoomTile.Point(20, 8)),
+                          new DoorDefinition(new RoomTile.Point(21, 14), new RoomTile.Point(21, 15))
+                  )),
+                  new RoomSpec("Lounge", new RoomDimensions(19, 1, 23, 6), true, List.of(
+                          new DoorDefinition(new RoomTile.Point(19, 6), new RoomTile.Point(18, 6))
+                  )),
+                  new RoomSpec("Cluedo", new RoomDimensions(10, 10, 14, 14), true, List.of())
+          );
+
+  private final AbstractCluedoTile[][] board = new AbstractCluedoTile[BOARD_SIZE][BOARD_SIZE];
+
+  public CluedoBoard() {
+    initializeTiles();
+  }
+
+  private void initializeTiles() {
+    insertBorderTiles();
+    insertRooms();
+    insertCorridorTiles();
+    placeStartPosAdjacentBorders();
+    // TODO: Secret passages are not yet implemented here
+  }
+
+  private void insertBorderTiles() {
+    IntStream.range(0, BOARD_SIZE)
+        .forEach(
+            i -> {
+              if (board[0][i] == null) {
+                board[0][i] = new BorderTile(0, i);
+              }
+              if (board[BOARD_SIZE - 1][i] == null) {
+                board[BOARD_SIZE - 1][i] = new BorderTile(BOARD_SIZE - 1, i);
+              }
+              if (i > 0 && i < BOARD_SIZE - 1) {
+                if (board[i][0] == null) {
+                  board[i][0] = new BorderTile(i, 0);
+                }
+                if (board[i][BOARD_SIZE - 1] == null) {
+                  board[i][BOARD_SIZE - 1] = new BorderTile(i, BOARD_SIZE - 1);
+                }
+              }
+            });
+  }
+
+  private List<RoomTile.Point> createRectangularOutline(RoomDimensions roomDimensions) {
+    List<RoomTile.Point> outline = new ArrayList<>();
+    outline.add(new RoomTile.Point(roomDimensions.top, roomDimensions.left));
+    outline.add(new RoomTile.Point(roomDimensions.top, roomDimensions.right));
+    outline.add(new RoomTile.Point(roomDimensions.bottom, roomDimensions.right));
+    outline.add(new RoomTile.Point(roomDimensions.bottom, roomDimensions.left));
+    outline.add(new RoomTile.Point(roomDimensions.top, roomDimensions.left)); // Close the loop
+    return outline;
+  }
+
+  private void populateRoomTiles(RoomDimensions roomDimensions, RoomTile room) {
+    for (int r = roomDimensions.top; r <= roomDimensions.bottom; r++) {
+      for (int c = roomDimensions.left; c <= roomDimensions.right; c++) {
+        if (r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE) {
+          if (board[r][c] != null) {
+            System.err.println(
+                "Warning: Overwriting existing tile at ("
+                    + r
+                    + ", "
+                    + c
+                    + ") while populating room "
+                    + room.getRoomName()
+                    + ". Existing: "
+                    + board[r][c].getIdentifier());
+          }
+          board[r][c] = room;
+          room.setWalkable(false);
+        } else {
+          System.err.println(
+              "Warning: Attempted to populate room tile outside board bounds at ("
+                  + r
+                  + ", "
+                  + c
+                  + ")");
+        }
+      }
+    }
+  }
+
+  private void insertRooms() {
+    ROOM_SPECS.forEach(
+        spec -> {
+          // Validate room dimensions against border
+          if (spec.dims.top < 1
+              || spec.dims.left < 1
+              || spec.dims.bottom >= BOARD_SIZE - 1
+              || spec.dims.right >= BOARD_SIZE - 1) {
+            System.err.println(
+                "CRITICAL Error: Room "
+                    + spec.name
+                    + " with dimensions "
+                    + spec.dims
+                    + " extends into the border area or outside bounds.");
+            return;
+          }
+          List<RoomTile.Point> outline = createRectangularOutline(spec.dims);
+          RoomTile room = new RoomTile(spec.name, outline);
+          populateRoomTiles(spec.dims, room);
+
+          for (DoorDefinition doorDef : spec.doors) {
+            try {
+              room.addDoor(doorDef.roomSide(), doorDef.corridorSide());
+            } catch (IllegalArgumentException e) {
+              System.err.println("Error adding door for room "
+                      + spec.name
+                      + " between "
+                      + doorDef.roomSide()
+                      + " and "
+                      + doorDef.corridorSide()
+                      + ": "
+                      + e.getMessage());
+            }
+          }
+          // TODO: if (spec.hasSecretPassage) addSecretPassage(room);
+        });
+  }
+
+  private void insertCorridorTiles() {
+    for (int row = 0; row < BOARD_SIZE; row++) {
+      for (int col = 0; col < BOARD_SIZE; col++) {
+        if (board[row][col] == null) { // If not a border and not a room, it's a corridor
+          board[row][col] = new CorridorTile(row, col);
+        }
+      }
+    }
+    // Making the central "Cluedo" area (which is a RoomTile) non-walkable internally.
+    // And the corridor tiles directly around it also non-walkable.
+    for (int r = 10; r <= 16; r++) {
+      for (int c = 9; c <= 14; c++) {
+        if (isValidPosition(new GridPos(r,c))) {
+          AbstractCluedoTile tile = board[r][c];
+          if (r >= 10 && r <= 14 && c >= 10 && c <= 14) { // Inside "Cluedo" room dimensions
+            if (tile instanceof RoomTile roomTile && roomTile.getRoomName().equals("Cluedo")){
+              roomTile.setWalkable(false);
+            }
+          } else {
+            if (tile instanceof CorridorTile) {
+              tile.setWalkable(false);
+            }
+          }
+        }
+      }
+    }
+  }
+
+  private void placeStartPosAdjacentBorders() {
+    // PlayerColor.WHITE (Miss Scarlett) -> START_POS_MISS_SCARLETT = new GridPos(23, 7);
+    replaceWithBorderIfCorridor(23, 6);
+    replaceWithBorderIfCorridor(23, 8);
+
+    // PlayerColor.RED (Col. Mustard) -> START_POS_COL_MUSTARD = new GridPos(17, 1);
+    replaceWithBorderIfCorridor(16, 1);
+    replaceWithBorderIfCorridor(18, 1);
+
+    // PlayerColor.YELLOW (Mrs. White) -> START_POS_MRS_WHITE = new GridPos(1, 7);
+    replaceWithBorderIfCorridor(1, 6);
+    replaceWithBorderIfCorridor(1, 8);
+
+
+    // PlayerColor.GREEN (Rev. Green) -> START_POS_REV_GREEN = new GridPos(1, 16);
+    replaceWithBorderIfCorridor(1, 15); // Adjusted from 1,13 to 1,15
+    replaceWithBorderIfCorridor(1, 17); // Adjusted from 1,15 to 1,17
+
+
+    // PlayerColor.BLUE (Mrs. Peacock) -> START_POS_MRS_PEACOCK = new GridPos(6, 23);
+    replaceWithBorderIfCorridor(5, 23);
+    replaceWithBorderIfCorridor(7, 23);
+
+    // PlayerColor.PURPLE (Prof. Plum) -> START_POS_PROF_PLUM = new GridPos(19, 23);
+    replaceWithBorderIfCorridor(18, 23);
+    replaceWithBorderIfCorridor(20, 23);
+  }
+
+  private void replaceWithBorderIfCorridor(int r, int c) {
+    if (!isValidPosition(new GridPos(r, c))) {
+      return;
+    }
+    AbstractCluedoTile tile = board[r][c];
+    if (tile instanceof CorridorTile) {
+      board[r][c] = new BorderTile(r, c);
+    }
+  }
+
+  @Override
+  public int getBoardSize() {
+    return BOARD_SIZE;
+  }
+
+  public int getRows() {
+    return BOARD_SIZE;
+  }
+
+  public int getCols() {
+    return BOARD_SIZE;
+  }
+
+  @Override
+  public void setPlayerPosition(Player<GridPos> player, GridPos position) {
+    if (!isValidPosition(position)) {
+      System.err.println("Error: Attempt to set invalid position " + position);
+      return;
+    }
+    AbstractCluedoTile targetTile = getTileAtPosition(position);
+    if (targetTile == null) { // Should not happen if isValidPosition is true and board is fully initialized
+      System.err.println("CRITICAL Error: Target tile is null at valid position " + position);
+      return;
+    }
+
+    if (targetTile instanceof BorderTile) {
+      System.err.println("Error: Cannot set player position to a BorderTile at " + position);
+      return;
+    }
+    if (!targetTile.isWalkable()
+        && !(targetTile
+            instanceof RoomTile)) { // Allow moving into rooms even if "not walkable" squares
+      System.err.println(
+          "Error: Cannot set player position to a non-walkable tile at "
+              + position
+              + " of type "
+              + targetTile.getClass().getSimpleName());
+      return;
+    }
+
+    GridPos oldPos = player.getPosition();
+
+    if (isValidPosition(oldPos)) {
+      AbstractCluedoTile oldTile = getTileAtPosition(oldPos);
+      if (oldTile != null) {
+        oldTile.removePlayer(player);
+      } else {
+        System.err.println(
+            "Warning: Player " + player.getName() + " had no valid old tile at " + oldPos);
+      }
+    }
+
+    player.setPosition(position);
+    AbstractCluedoTile newTile =
+        getTileAtPosition(position); // Re-fetch, could be same as targetTile
+    if (newTile != null) {
+      newTile.addPlayer(player);
+    } else {
+      System.err.println(
+          "CRITICAL Error: New tile is null at valid position "
+              + position
+              + " for player "
+              + player.getName());
+    }
+  }
+
+  @Override
+  public void addPlayersToStart(Map<Integer, Player<GridPos>> players) {
+    players
+        .values()
+        .forEach(
+            player -> {
+              GridPos startPos = getPlayerStartPosition(player);
+              if (isValidPosition(startPos)) {
+                AbstractCluedoTile startTile = getTileAtPosition(startPos);
+                if (startTile instanceof BorderTile) {
+                  System.err.println(
+                      "CRITICAL Error: Calculated start position "
+                          + startPos
+                          + " for player "
+                          + player.getName()
+                          + " is a BorderTile. Adjust start positions.");
+                  return;
+                }
+                if (startTile == null) {
+                  System.err.println(
+                      "CRITICAL Error: Start tile is null at supposedly valid position "
+                          + startPos
+                          + " for player "
+                          + player.getName()
+                          + ". This might be due to board initialization order or incorrect coordinates.");
+                  return;
+                }
+
+                        player.setPosition(startPos);
+
+                if (!startTile.isWalkable()
+                    && !(startTile instanceof RoomTile)) { // Rooms are fine to start in.
+                  System.err.println(
+                      "Error: Start tile at "
+                          + startPos
+                          + " for player "
+                          + player.getName()
+                          + " is not walkable and not a Room! Type: "
+                          + startTile.getClass().getSimpleName());
+                }
+                startTile.addPlayer(player); // Add player to the tile model
+
+                      } else {
+                        System.err.println(
+                                "Error: Invalid start position "
+                                        + startPos
+                                        + " calculated for player "
+                                        + player.getName());
+                      }
+                    });
+  }
+
+  @Override
+  public void incrementPlayerPosition(Player<GridPos> player, int increment) {
+    System.out.println(
+            "Warning: incrementPlayerPosition called. This method is not suitable for standard Cluedo movement.");
+  }
+
+  public AbstractCluedoTile getTileAtPosition(GridPos pos) {
+    if (isValidPosition(pos)) {
+      return board[pos.row()][pos.col()];
+    }
+    return null;
+  }
+
+  public void movePlayer(Player<GridPos> player, GridPos toPos) {
+    if (!isValidPosition(toPos)) {
+      System.err.println("Invalid move target: " + toPos);
+      return;
+    }
+    AbstractCluedoTile targetTile = getTileAtPosition(toPos);
+    if (targetTile == null) {
+      System.err.println("Cannot move to null tile: " + toPos);
+      return;
+    }
+    if (targetTile instanceof BorderTile) {
+      System.err.println("Cannot move to a BorderTile: " + toPos);
+      return;
+    }
+    AbstractCluedoTile currentTile = getTileAtPosition(player.getPosition());
+    if (targetTile instanceof RoomTile roomTarget) {
+      if (currentTile instanceof CorridorTile) {
+        if (!roomTarget.canEnterFrom(player.getPosition().row(), player.getPosition().col())) {
+          System.err.println(
+                  "Cannot enter room "
+                          + roomTarget.getRoomName()
+                          + " from "
+                          + player.getPosition()
+                          + ". No door or invalid entry point.");
+          return;
+        }
+      }
+      // Moving from Room to Room (secret passage - not implemented, so disallow for now)
+      else if (currentTile instanceof RoomTile && currentTile != targetTile) {
+        System.err.println("Secret passages not implemented. Cannot move directly between rooms "
+                + ((RoomTile) currentTile).getRoomName() + " and " + roomTarget.getRoomName());
+        return;
+      }
+      // Moving within the same room is allowed.
+    }
+    // Moving from Room to Corridor
+    else if (targetTile instanceof CorridorTile && currentTile instanceof RoomTile roomOrigin) {
+      if (!roomOrigin.canExitTo(toPos.row(), toPos.col())) {
+        System.err.println("Cannot exit room " + roomOrigin.getRoomName() + " to " + toPos + ". No door or invalid exit point.");
+        return;
+      }
+    }
+    // Moving to a non-walkable tile (that isn't a room)
+    else if (!targetTile.isWalkable()) {
+      System.err.println(
+              "Cannot move to non-walkable tile: "
+                      + toPos
+                      + " of type "
+                      + targetTile.getClass().getSimpleName());
+      return;
+    }
+    setPlayerPosition(player, toPos);
+  }
+
+
+  private boolean isValidPosition(GridPos pos) {
+    return pos != null
+            && pos.row() >= 0
+            && pos.row() < BOARD_SIZE
+            && pos.col() >= 0
+            && pos.col() < BOARD_SIZE;
+  }
+
+  private GridPos getPlayerStartPosition(Player<GridPos> player) {
+    PlayerColor color = player.getColor();
+    return switch (color) {
+      case WHITE -> START_POS_MISS_SCARLETT;
+      case RED -> START_POS_COL_MUSTARD;
+      case YELLOW -> START_POS_MRS_WHITE;
+      case GREEN -> START_POS_REV_GREEN;
+      case BLUE -> START_POS_MRS_PEACOCK;
+      case PURPLE -> START_POS_PROF_PLUM;
+      default -> {
+        System.err.println(
+                "Warning: Unmapped player color for start position: "
+                        + color);
+        throw new IllegalArgumentException(
+                "Invalid or unmapped player color for start position: " + color);
+      }
+    };
+  }
+
+  public AbstractCluedoTile[][] getBoardGrid() {
+    return board;
+  }
+
+  private record RoomDimensions(int top, int left, int bottom, int right) {}
+
+  private record RoomSpec(String name, RoomDimensions dims, boolean hasSecretPassage, List<DoorDefinition> doors) {}
+}
diff --git a/src/main/java/edu/ntnu/idi/idatt/boardgame/games/cluedo/domain/board/CorridorTile.java b/src/main/java/edu/ntnu/idi/idatt/boardgame/games/cluedo/domain/board/CorridorTile.java
new file mode 100644
index 0000000..507c3f5
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/boardgame/games/cluedo/domain/board/CorridorTile.java
@@ -0,0 +1,14 @@
+package edu.ntnu.idi.idatt.boardgame.games.cluedo.domain.board;
+
+/** A single walkable square on the Cluedo board. */
+public final class CorridorTile extends AbstractCluedoTile {
+
+  public CorridorTile(int row, int col) {
+    super(row, col);
+  }
+
+  /** Corridor squares are always enterable. */
+  public boolean isWalkable() {
+    return true;
+  }
+}
diff --git a/src/main/java/edu/ntnu/idi/idatt/boardgame/games/cluedo/domain/board/RoomTile.java b/src/main/java/edu/ntnu/idi/idatt/boardgame/games/cluedo/domain/board/RoomTile.java
new file mode 100644
index 0000000..919a13c
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/boardgame/games/cluedo/domain/board/RoomTile.java
@@ -0,0 +1,146 @@
+package edu.ntnu.idi.idatt.boardgame.games.cluedo.domain.board;
+
+import java.util.*;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+/**
+ * A room described by an ordered list of corner points that form the perimeter.
+ *
+ * <p>Edges (p[i] → p[i+1]) are walls except those registered via {@link #addDoor}. Coordinates are
+ * board squares (row, col), 0-based.
+ */
+public final class RoomTile extends AbstractCluedoTile {
+
+  /** Name shown on cards / UI */
+  private final String roomName;
+
+  /** Ordered perimeter; first == last. */
+  private final List<Point> outline;
+
+  /** Set of edges that are open doorways. Each Edge connects a room point and an adjacent corridor point. */
+  private final Set<Edge> doorEdges = new HashSet<>();
+
+  private final int minRow, maxRow, minCol, maxCol;
+
+
+  /**
+   * @param roomName logical name (“Kitchen”)
+   * @param outlinePerimeter ordered points, first = last (minimum 4)
+   */
+  public RoomTile(String roomName, List<Point> outlinePerimeter) {
+    // we store row/col of an arbitrary interior square from the outline
+    super(outlinePerimeter.get(0).row(), outlinePerimeter.get(0).col());
+    if (outlinePerimeter.size() < 4)
+      throw new IllegalArgumentException("Outline requires ≥ 4 points");
+    if (!outlinePerimeter.get(0).equals(outlinePerimeter.get(outlinePerimeter.size() - 1)))
+      throw new IllegalArgumentException("First and last outline point must match");
+    this.roomName = Objects.requireNonNull(roomName);
+    this.outline = List.copyOf(outlinePerimeter);
+
+    this.minRow = outlinePerimeter.stream().mapToInt(Point::row).min().orElseThrow();
+    this.maxRow = outlinePerimeter.stream().mapToInt(Point::row).max().orElseThrow();
+    this.minCol = outlinePerimeter.stream().mapToInt(Point::col).min().orElseThrow();
+    this.maxCol = outlinePerimeter.stream().mapToInt(Point::col).max().orElseThrow();
+    this.walkable = false; // Room interiors are not "walkable" in the same way corridors are.
+  }
+
+  /**
+   * Marks the edge between a room boundary point and an adjacent corridor point as a doorway.
+   *
+   * @param roomBoundaryPoint The point on the room's boundary.
+   * @param adjacentCorridorPoint The adjacent point in the corridor.
+   */
+  public void addDoor(Point roomBoundaryPoint, Point adjacentCorridorPoint) {
+    // Check roomBoundaryPoint is actually on the boundary of this room
+    boolean onHorizontalEdge = (roomBoundaryPoint.row() == this.minRow || roomBoundaryPoint.row() == this.maxRow) &&
+            (roomBoundaryPoint.col() >= this.minCol && roomBoundaryPoint.col() <= this.maxCol);
+    boolean onVerticalEdge = (roomBoundaryPoint.col() == this.minCol || roomBoundaryPoint.col() == this.maxCol) &&
+            (roomBoundaryPoint.row() >= this.minRow && roomBoundaryPoint.row() <= this.maxRow);
+
+    if (! (onHorizontalEdge || onVerticalEdge) ) {
+      throw new IllegalArgumentException("Room boundary point " + roomBoundaryPoint +
+              " is not on the calculated perimeter (minR:" + this.minRow +
+              ", maxR:" + this.maxRow + ", minC:" + this.minCol +
+              ", maxC:" + this.maxCol + ") of room " + roomName);
+    }
+
+    // Check adjacentCorridorPoint is outside the room's bounds
+    boolean corridorIsOutside = adjacentCorridorPoint.row() < this.minRow || adjacentCorridorPoint.row() > this.maxRow ||
+            adjacentCorridorPoint.col() < this.minCol || adjacentCorridorPoint.col() > this.maxCol;
+    if (!corridorIsOutside) {
+      throw new IllegalArgumentException("Corridor point " + adjacentCorridorPoint +
+              " is not outside room " + roomName + " (bounds: minR:" + this.minRow +
+              ", maxR:" + this.maxRow + ", minC:" + this.minCol +
+              ", maxC:" + this.maxCol + ")");
+    }
+
+    Edge doorEdge = new Edge(roomBoundaryPoint, adjacentCorridorPoint);
+    doorEdges.add(doorEdge);
+  }
+
+
+  /** Checks if a player can enter this room from the given corridor coordinates. */
+  public boolean canEnterFrom(int corridorRow, int corridorCol) {
+    Point corridorPoint = new Point(corridorRow, corridorCol);
+    for (Edge door : doorEdges) {
+      // A door edge connects a room point (door.a) and a corridor point (door.b) or vice versa.
+      // We need to check if the given corridorPoint matches the corridor-side point of any door.
+      if (door.a().equals(corridorPoint) && isPointInsideRoom(door.b())) { // corridorPoint is a, roomPoint is b
+        return true;
+      }
+      if (door.b().equals(corridorPoint) && isPointInsideRoom(door.a())) { // corridorPoint is b, roomPoint is a
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /** Checks if a player can exit this room to the given corridor coordinates. */
+  public boolean canExitTo(int corridorRow, int corridorCol) {
+    // This is symmetric to canEnterFrom for the current door model
+    return canEnterFrom(corridorRow, corridorCol);
+  }
+
+  private boolean isPointInsideRoom(Point p) {
+    return p.row() >= this.minRow && p.row() <= this.maxRow &&
+            p.col() >= this.minCol && p.col() <= this.maxCol;
+  }
+
+
+  public String getRoomName() {
+    return roomName;
+  }
+
+  public List<Point> getOutline() {
+    return outline;
+  }
+
+  public Set<Edge> getDoors() {
+    return Set.copyOf(doorEdges);
+  }
+
+  public record Point(int row, int col) {}
+
+  public record Edge(Point a, Point b) {
+    public Edge {
+      if (Math.abs(a.row() - b.row()) + Math.abs(a.col() - b.col()) != 1)
+        throw new IllegalArgumentException("Edge must connect adjacent squares: " + a + ", " + b);
+    }
+
+    boolean adjacentTo(Point p) {
+      return p.equals(a) || p.equals(b);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      return o instanceof Edge e
+              && (e.a.equals(a) && e.b.equals(b) || e.a.equals(b) && e.b.equals(a));
+    }
+
+    @Override
+    public int hashCode() {
+      return a.hashCode() + b.hashCode();
+    }
+  }
+}
diff --git a/src/main/java/edu/ntnu/idi/idatt/boardgame/games/cluedo/engine/controller/CluedoController.java b/src/main/java/edu/ntnu/idi/idatt/boardgame/games/cluedo/engine/controller/CluedoController.java
new file mode 100644
index 0000000..db248cd
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/boardgame/games/cluedo/engine/controller/CluedoController.java
@@ -0,0 +1,131 @@
+package edu.ntnu.idi.idatt.boardgame.games.cluedo.engine.controller;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.IntStream;
+
+import edu.ntnu.idi.idatt.boardgame.core.domain.dice.Dice;
+import edu.ntnu.idi.idatt.boardgame.core.domain.player.GridPos;
+import edu.ntnu.idi.idatt.boardgame.core.domain.player.Player;
+import edu.ntnu.idi.idatt.boardgame.core.domain.player.PlayerColor;
+import edu.ntnu.idi.idatt.boardgame.core.engine.controller.GameController;
+import edu.ntnu.idi.idatt.boardgame.games.cluedo.domain.board.CluedoBoard;
+
+public class CluedoController extends GameController<GridPos> {
+
+    private final int numberOfPlayers;
+    private final List<PlayerColor> playerColors = List.of(
+            PlayerColor.WHITE, // Miss Scarlett
+            PlayerColor.RED, // Col. Mustard
+            PlayerColor.BLUE, // Mrs. Peacock
+            PlayerColor.GREEN, // Rev. Green
+            PlayerColor.YELLOW, // Mrs. White
+            PlayerColor.PURPLE // Prof. Plum
+    );
+
+    private final Map<PlayerColor, String> playerNames = Map.of(
+            PlayerColor.WHITE, "Miss Scarlett",
+            PlayerColor.RED, "Col. Mustard",
+            PlayerColor.BLUE, "Mrs. Peacock",
+            PlayerColor.GREEN, "Rev. Green",
+            PlayerColor.YELLOW, "Mrs. White",
+            PlayerColor.PURPLE, "Prof. Plum");
+
+    public CluedoController(int numberOfPlayers) {
+        super(new CluedoBoard(), new Dice(2));
+        if (numberOfPlayers < 2 || numberOfPlayers > 6) {
+            throw new IllegalArgumentException("Cluedo requires 2 to 6 players.");
+        }
+        this.numberOfPlayers = numberOfPlayers;
+        initialize(numberOfPlayers);
+    }
+
+    @Override
+    protected Map<Integer, Player<GridPos>> createPlayers(int numPlayers) {
+        Map<Integer, Player<GridPos>> playersMap = new HashMap<>();
+        CluedoBoard board = (CluedoBoard) this.gameBoard;
+
+        IntStream.range(0, numPlayers)
+                .forEach(i -> {
+                    int playerId = i + 1;
+                    PlayerColor color = playerColors.get(i % playerColors.size());
+                    String name = playerNames.getOrDefault(color, "Player " + playerId);
+                    GridPos startPos = new GridPos(0, 0);
+                    Player<GridPos> player = new Player<>(playerId, name, color, startPos);
+                    playersMap.put(playerId, player);
+                });
+        return playersMap;
+    }
+
+    public Map<Integer, Player<GridPos>> getPlayers() {
+        return players;
+    }
+
+    public Player<GridPos> getCurrentPlayer() {
+        return currentPlayer;
+    }
+
+    // PLACEHOLDERS:
+    // TODO: Implement game-specific logic for Cluedo
+
+    @Override
+    protected boolean isGameOver() {
+        // Game over condition: Correct accusation made or only one player left?
+        // Placeholder: Game never ends for now
+        return false;
+    }
+
+    @Override
+    protected void onGameFinish() {
+        // Actions when the game ends (e.g., declare winner)
+        notifyGameFinished(currentPlayer); // Example notification
+    }
+
+    @Override
+    protected Player<GridPos> getNextPlayer() {
+        // Cycle through players based on ID
+        int nextPlayerId = (currentPlayer.getId() % numberOfPlayers) + 1;
+        return players.get(nextPlayerId);
+    }
+
+    @Override
+    public void saveGameState(String filePath) {
+        // TODO: Implement Cluedo-specific game state saving
+        System.out.println("Cluedo save game state not implemented yet for path: " + filePath);
+        // Need a CluedoGameStateDTO, Mapper, and Repository similar to SnL
+    }
+
+    @Override
+    public void loadGameState(String filePath) {
+        // TODO: Implement Cluedo-specific game state loading
+        System.out.println("Cluedo load game state not implemented yet from path: " + filePath);
+    }
+
+    // --- Cluedo Specific Actions (Examples - Need Implementation) ---
+
+    public void rollDiceAndMove() {
+        int roll = dice.roll();
+        // TODO: Implement movement logic based on roll
+        // Player chooses path, limited by roll, cannot pass through walls (except
+        // doors), cannot land on occupied corridor tile.
+        notifyObservers(currentPlayer.getName() + " rolled a " + roll + ". Movement logic TBD.");
+
+        // After movement (or if movement ends turn):
+        // Check for room entry -> Suggestion?
+        // End turn
+        currentPlayer = getNextPlayer();
+        notifyObservers("It is now " + currentPlayer.getName() + "'s turn.");
+    }
+
+    public void makeSuggestion() {
+        // TODO: Implement suggestion logic (only in rooms, move suspect/weapon)
+        notifyObservers(currentPlayer.getName() + " suggestion logic TBD.");
+    }
+
+    public void makeAccusation() {
+        // TODO: Implement accusation logic (check against solution, handle win/loss)
+        notifyObservers(currentPlayer.getName() + " accusation logic TBD.");
+        // if (isGameOver()) onGameFinish();
+    }
+}
diff --git a/src/main/java/edu/ntnu/idi/idatt/boardgame/games/cluedo/view/CluedoBoardView.java b/src/main/java/edu/ntnu/idi/idatt/boardgame/games/cluedo/view/CluedoBoardView.java
new file mode 100644
index 0000000..683a462
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/boardgame/games/cluedo/view/CluedoBoardView.java
@@ -0,0 +1,189 @@
+package edu.ntnu.idi.idatt.boardgame.games.cluedo.view;
+
+import edu.ntnu.idi.idatt.boardgame.core.domain.player.GridPos;
+import edu.ntnu.idi.idatt.boardgame.games.cluedo.domain.board.AbstractCluedoTile;
+import edu.ntnu.idi.idatt.boardgame.games.cluedo.domain.board.BorderTile;
+import edu.ntnu.idi.idatt.boardgame.games.cluedo.domain.board.CluedoBoard;
+import edu.ntnu.idi.idatt.boardgame.games.cluedo.domain.board.CorridorTile;
+import edu.ntnu.idi.idatt.boardgame.games.cluedo.domain.board.RoomTile;
+import javafx.geometry.Pos;
+import javafx.scene.control.Label;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.Pane;
+import javafx.scene.layout.StackPane;
+import javafx.scene.paint.Color;
+import javafx.scene.shape.Rectangle;
+import javafx.scene.text.Font;
+import javafx.scene.text.TextAlignment;
+
+public class CluedoBoardView extends Pane {
+    private static final int TILE_SIZE = 30;
+    private static final int GAP_SIZE = 1;
+    private final GridPane grid;
+    private final CluedoBoard boardModel;
+
+    public CluedoBoardView(CluedoBoard boardModel) {
+        this.boardModel = boardModel;
+        this.grid = new GridPane();
+
+        grid.setHgap(GAP_SIZE);
+        grid.setVgap(GAP_SIZE);
+
+        initializeBoard();
+        getChildren().add(grid);
+    }
+
+    private boolean isCorridorTileADoor(int corridorRow, int corridorCol) {
+        int[] dr = {-1, 1, 0, 0};
+        int[] dc = {0, 0, -1, 1};
+
+        for (int i = 0; i < 4; i++) {
+            int neighborRow = corridorRow + dr[i];
+            int neighborCol = corridorCol + dc[i];
+
+            // Check bounds for neighbor
+            if (neighborRow >= 0 && neighborRow < boardModel.getRows() &&
+                    neighborCol >= 0 && neighborCol < boardModel.getCols()) {
+
+                AbstractCluedoTile neighborTile = boardModel.getTileAtPosition(new GridPos(neighborRow, neighborCol));
+                if (neighborTile instanceof RoomTile adjacentRoomTile) {
+                    if (adjacentRoomTile.canEnterFrom(corridorRow, corridorCol)) {
+                        return true;
+                    }
+                }
+            }
+        }
+        return false;
+    }
+
+    private void initializeBoard() {
+        grid.getChildren().clear();
+        int numRows = boardModel.getRows();
+        int numCols = boardModel.getCols();
+        AbstractCluedoTile[][] boardGrid = boardModel.getBoardGrid();
+        boolean[][] visitedRoomCells = new boolean[numRows][numCols];
+
+        for (int row = 0; row < numRows; row++) {
+            for (int col = 0; col < numCols; col++) {
+                if (visitedRoomCells[row][col] && boardGrid[row][col] instanceof RoomTile) {
+                    continue;
+                }
+
+                AbstractCluedoTile tileModel = boardGrid[row][col];
+
+                if (tileModel == null) {
+                    Pane emptyPane = new Pane();
+                    emptyPane.setPrefSize(TILE_SIZE, TILE_SIZE);
+                    emptyPane.setStyle("-fx-background-color: #1A1A1A;");
+                    grid.add(emptyPane, col, row);
+                    continue;
+                }
+
+                if (tileModel instanceof RoomTile roomTile) {
+                    int minR = numRows, minC = numCols, maxR = -1, maxC = -1;
+                    for (int rScan = 0; rScan < numRows; rScan++) {
+                        for (int cScan = 0; cScan < numCols; cScan++) {
+                            if (boardGrid[rScan][cScan] == roomTile) {
+                                minR = Math.min(minR, rScan);
+                                maxR = Math.max(maxR, rScan);
+                                minC = Math.min(minC, cScan);
+                                maxC = Math.max(maxC, cScan);
+                            }
+                        }
+                    }
+
+                    int rowSpan = maxR - minR + 1;
+                    int colSpan = maxC - minC + 1;
+
+                    Rectangle roomBackground = new Rectangle(colSpan * TILE_SIZE + (colSpan - 1) * GAP_SIZE,
+                            rowSpan * TILE_SIZE + (rowSpan - 1) * GAP_SIZE);
+                    roomBackground.setFill(Color.LIGHTSLATEGRAY);
+                    roomBackground.setStroke(Color.DARKGRAY);
+                    roomBackground.setArcWidth(10);
+                    roomBackground.setArcHeight(10);
+
+                    Label roomLabel = new Label(roomTile.getRoomName());
+                    roomLabel.setFont(Font.font("Arial", Math.max(10, TILE_SIZE * 0.4)));
+                    roomLabel.setTextAlignment(TextAlignment.CENTER);
+                    roomLabel.setTextFill(Color.WHITE);
+                    roomLabel.setPadding(new javafx.geometry.Insets(5));
+
+                    StackPane roomPane = new StackPane(roomBackground, roomLabel);
+                    roomPane.setAlignment(Pos.CENTER);
+
+                    grid.add(roomPane, minC, minR, colSpan, rowSpan);
+
+                    for (int r = minR; r <= maxR; r++) {
+                        for (int c = minC; c <= maxC; c++) {
+                            if (boardGrid[r][c] == roomTile) {
+                                visitedRoomCells[r][c] = true;
+                            }
+                        }
+                    }
+                    addPlayerTokensToRoomPane(roomPane, roomTile);
+
+                } else if (tileModel instanceof CorridorTile) {
+                    CluedoTileView tileView = new CluedoTileView(tileModel, TILE_SIZE);
+                    if (isCorridorTileADoor(row, col)) {
+                        tileView.setAsDoorCorridor(true);
+                    }
+                    grid.add(tileView.getNode(), col, row);
+                } else if (tileModel instanceof BorderTile) {
+                    CluedoTileView tileView = new CluedoTileView(tileModel, TILE_SIZE);
+                    grid.add(tileView.getNode(), col, row);
+                } else {
+                    Pane unknownTilePane = new Pane();
+                    unknownTilePane.setPrefSize(TILE_SIZE, TILE_SIZE);
+                    unknownTilePane.setStyle("-fx-background-color: #FF00FF;"); // Magenta for unknown
+                    Label unknownLabel = new Label("?");
+                    StackPane.setAlignment(unknownLabel, Pos.CENTER);
+                    StackPane wrapper = new StackPane(unknownTilePane, unknownLabel);
+                    grid.add(wrapper, col, row);
+                    System.err.println(
+                            "CluedoBoardView: Unhandled tile type at ("+row+","+col+"): "
+                                    + tileModel.getClass().getSimpleName());
+                }
+            }
+        }
+    }
+
+    private void addPlayerTokensToRoomPane(StackPane roomPane, RoomTile roomTile) {
+        if (!roomTile.getPlayers().isEmpty()) {
+            javafx.scene.layout.FlowPane playerTokenPane = new javafx.scene.layout.FlowPane();
+            playerTokenPane.setAlignment(Pos.BOTTOM_CENTER);
+            playerTokenPane.setHgap(3);
+            playerTokenPane.setVgap(3);
+            playerTokenPane.setPadding(new javafx.geometry.Insets(5));
+            // Make token pane pick up mouse events if needed later, but not block room label
+            playerTokenPane.setPickOnBounds(false);
+
+
+            double roomPaneWidth = roomPane.getWidth();
+            if (roomPaneWidth <= 0) roomPaneWidth = TILE_SIZE * 2;
+            double roomPaneHeight = roomPane.getHeight();
+            if (roomPaneHeight <= 0) roomPaneHeight = TILE_SIZE * 2;
+
+            playerTokenPane.setMaxWidth(Math.max(TILE_SIZE, roomPaneWidth - 20));
+            playerTokenPane.setMaxHeight(Math.max(TILE_SIZE / 2.0, roomPaneHeight / 3.0));
+
+
+            roomTile.getPlayers().forEach(player -> {
+                javafx.scene.shape.Circle circle = new javafx.scene.shape.Circle(Math.max(5, TILE_SIZE * 0.25));
+                circle.setFill(edu.ntnu.idi.idatt.boardgame.ui.util.PlayerColorMapper.toPaint(player.getColor()));
+                circle.setStroke(Color.BLACK);
+                circle.setStrokeWidth(1.5);
+                playerTokenPane.getChildren().add(circle);
+            });
+
+            roomPane.getChildren().add(playerTokenPane);
+            StackPane.setAlignment(playerTokenPane, Pos.BOTTOM_CENTER);
+        }
+    }
+
+
+    public void updateView() {
+        // This full refresh can be slow for large boards or frequent updates.
+        // For player movement, only updating affected tiles would be more efficient.
+        initializeBoard();
+    }
+}
diff --git a/src/main/java/edu/ntnu/idi/idatt/boardgame/games/cluedo/view/CluedoTileView.java b/src/main/java/edu/ntnu/idi/idatt/boardgame/games/cluedo/view/CluedoTileView.java
new file mode 100644
index 0000000..7c4abe1
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/boardgame/games/cluedo/view/CluedoTileView.java
@@ -0,0 +1,111 @@
+package edu.ntnu.idi.idatt.boardgame.games.cluedo.view;
+
+import edu.ntnu.idi.idatt.boardgame.core.domain.board.Tile;
+import edu.ntnu.idi.idatt.boardgame.core.domain.player.GridPos;
+import edu.ntnu.idi.idatt.boardgame.core.engine.event.TileObserver;
+import edu.ntnu.idi.idatt.boardgame.games.cluedo.domain.board.AbstractCluedoTile;
+import edu.ntnu.idi.idatt.boardgame.games.cluedo.domain.board.CorridorTile;
+import edu.ntnu.idi.idatt.boardgame.games.cluedo.domain.board.RoomTile;
+import edu.ntnu.idi.idatt.boardgame.games.cluedo.domain.board.BorderTile;
+import edu.ntnu.idi.idatt.boardgame.ui.util.PlayerColorMapper;
+import javafx.geometry.Pos;
+import javafx.scene.control.Label;
+import javafx.scene.layout.FlowPane;
+import javafx.scene.layout.StackPane;
+import javafx.scene.paint.Color;
+import javafx.scene.shape.Circle;
+import javafx.scene.shape.Rectangle;
+import javafx.scene.text.Font;
+import javafx.scene.text.TextAlignment;
+
+public class CluedoTileView implements TileObserver<GridPos> {
+    private final StackPane tilePane;
+    private final Rectangle tileBackground;
+    private final Label infoLabel;
+    private final FlowPane playerPane;
+    private final AbstractCluedoTile tileModel;
+    private final int tileSize;
+    private boolean isDoorCorridor = false;
+
+    public CluedoTileView(AbstractCluedoTile tileModel, int tileSize) {
+        this.tileModel = tileModel;
+        this.tileSize = tileSize;
+        tileModel.addObserver(this);
+
+        tileBackground = new Rectangle(tileSize, tileSize);
+        tileBackground.setStroke(Color.DARKGRAY);
+        tileBackground.setArcWidth(3);
+        tileBackground.setArcHeight(3);
+
+        infoLabel = new Label();
+        infoLabel.setFont(Font.font(Math.max(8, tileSize * 0.15)));
+        infoLabel.setTextAlignment(TextAlignment.CENTER);
+        infoLabel.setWrapText(true);
+
+        playerPane = new FlowPane();
+        playerPane.setAlignment(Pos.CENTER);
+        playerPane.setHgap(1);
+        playerPane.setVgap(1);
+        playerPane.setPrefWrapLength(tileSize - 4);
+
+        tilePane = new StackPane(tileBackground, infoLabel, playerPane);
+        StackPane.setAlignment(infoLabel, Pos.TOP_CENTER);
+        StackPane.setAlignment(playerPane, Pos.CENTER);
+        tilePane.setAlignment(Pos.CENTER);
+
+        updateDisplay();
+    }
+
+    public StackPane getNode() {
+        return tilePane;
+    }
+
+    public void setAsDoorCorridor(boolean isDoor) {
+        this.isDoorCorridor = isDoor;
+        updateDisplay();
+    }
+
+    @Override
+    public void onTileChanged(Tile<GridPos> tile) {
+        if (tile == this.tileModel) {
+            updateDisplay();
+        }
+    }
+
+    void updateDisplay() {
+        playerPane.getChildren().clear();
+
+        if (tileModel instanceof RoomTile roomTile) {
+            tileBackground.setFill(Color.LIGHTSLATEGRAY); // Room color
+            infoLabel.setText(roomTile.getRoomName());
+            infoLabel.setVisible(true);
+            infoLabel.setText("");
+        } else if (tileModel instanceof CorridorTile) {
+            if (this.isDoorCorridor) {
+                tileBackground.setFill(Color.KHAKI);
+            } else {
+                tileBackground.setFill(Color.BEIGE);
+            }
+            infoLabel.setVisible(false);
+        } else if (tileModel instanceof BorderTile) {
+            tileBackground.setFill(Color.DARKSLATEGRAY); // Border color
+            infoLabel.setVisible(false);
+        }
+        else {
+            tileBackground.setFill(Color.LIGHTGRAY);
+            infoLabel.setText("?");
+            infoLabel.setVisible(true);
+        }
+
+        // Add player circles if any players are on this tile
+        if (!tileModel.getPlayers().isEmpty()) {
+            tileModel.getPlayers().forEach(player -> {
+                Circle circle = new Circle(Math.max(3, tileSize * 0.1)); // Smaller player circle
+                circle.setFill(PlayerColorMapper.toPaint(player.getColor()));
+                circle.setStroke(Color.BLACK);
+                circle.setStrokeWidth(0.5);
+                playerPane.getChildren().add(circle);
+            });
+        }
+    }
+}
diff --git a/src/main/java/edu/ntnu/idi/idatt/boardgame/games/cluedo/view/CluedoView.java b/src/main/java/edu/ntnu/idi/idatt/boardgame/games/cluedo/view/CluedoView.java
new file mode 100644
index 0000000..8801ac8
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/boardgame/games/cluedo/view/CluedoView.java
@@ -0,0 +1,74 @@
+package edu.ntnu.idi.idatt.boardgame.games.cluedo.view;
+
+import edu.ntnu.idi.idatt.boardgame.core.domain.player.GridPos;
+import edu.ntnu.idi.idatt.boardgame.core.domain.player.Player;
+import edu.ntnu.idi.idatt.boardgame.core.engine.event.GameObserver;
+import edu.ntnu.idi.idatt.boardgame.games.cluedo.domain.board.CluedoBoard;
+import edu.ntnu.idi.idatt.boardgame.games.cluedo.engine.controller.CluedoController;
+import javafx.geometry.Insets;
+import javafx.scene.control.Button;
+import javafx.scene.control.Label;
+import javafx.scene.control.ScrollPane;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.VBox;
+
+public class CluedoView implements GameObserver<GridPos> {
+    private final BorderPane root;
+    private final CluedoBoardView boardView;
+    private final Label statusLabel;
+    private final Button rollDiceButton; // Example action button
+    private final CluedoController controller;
+
+    public CluedoView(CluedoController controller) {
+        this.controller = controller;
+        this.root = new BorderPane();
+        root.setPadding(new Insets(10));
+
+        CluedoBoard board = (CluedoBoard) controller.getGameBoard();
+        this.boardView = new CluedoBoardView(board);
+        ScrollPane scrollPane = new ScrollPane(boardView);
+        scrollPane.setFitToWidth(true);
+        scrollPane.setFitToHeight(true);
+        root.setCenter(scrollPane);
+
+        // Control Panel (Right Side)
+        VBox controlPanel = new VBox(10);
+        controlPanel.setPadding(new Insets(10));
+        controlPanel.setStyle("-fx-background-color: #f0f0f0;");
+
+        this.statusLabel = new Label("Welcome to Cluedo!");
+        statusLabel.setWrapText(true);
+
+        this.rollDiceButton = new Button("Roll Dice & Move");
+        rollDiceButton.setOnAction(e -> controller.rollDiceAndMove());
+
+        // TODO: Add more controls
+        Button suggestButton = new Button("Make Suggestion");
+        suggestButton.setOnAction(e -> controller.makeSuggestion());
+        suggestButton.setDisable(true); // Enable only when in a room
+
+        Button accuseButton = new Button("Make Accusation");
+        accuseButton.setOnAction(e -> controller.makeAccusation());
+
+        controller.addObserver(this);
+        update("Game started. It's " + controller.getCurrentPlayer().getName() + "'s turn.");
+    }
+
+    public BorderPane getRoot() {
+        return root;
+    }
+
+    @Override
+    public void update(String message) {
+        statusLabel.setText(message);
+    }
+
+    @Override
+    public void gameFinished(Player<GridPos> winner) {
+        statusLabel.setText("Game Over! " + winner.getName() + " wins!");
+        // Disable game action buttons
+        rollDiceButton.setDisable(true);
+        // suggestButton.setDisable(true);
+        // accuseButton.setDisable(true);
+    }
+}
diff --git a/src/main/java/edu/ntnu/idi/idatt/boardgame/games/snakesandladders/domain/board/SnLBoard.java b/src/main/java/edu/ntnu/idi/idatt/boardgame/games/snakesandladders/domain/board/SnLBoard.java
index 911ed26..c6c7ca1 100644
--- a/src/main/java/edu/ntnu/idi/idatt/boardgame/games/snakesandladders/domain/board/SnLBoard.java
+++ b/src/main/java/edu/ntnu/idi/idatt/boardgame/games/snakesandladders/domain/board/SnLBoard.java
@@ -17,24 +17,22 @@ public final class SnLBoard implements GameBoard<LinearPos> {
   private final Map<Integer, SnLTile> tiles = new HashMap<>();
   private final Map<Integer, Connector> connectors = new HashMap<>();
 
-  private static final Map<Integer, Integer> SNAKES =
-      Map.of(
-          30, 14,
-          34, 7,
-          47, 7,
-          54, 35,
-          65, 5,
-          87, 31);
-
-  private static final Map<Integer, Integer> LADDERS =
-      Map.of(
-          8, 6,
-          21, 10,
-          33, 5,
-          48, 7,
-          61, 8,
-          70, 9,
-          81, 2);
+  private static final Map<Integer, Integer> SNAKES = Map.of(
+      30, 14,
+      34, 7,
+      47, 7,
+      54, 35,
+      65, 5,
+      87, 31);
+
+  private static final Map<Integer, Integer> LADDERS = Map.of(
+      8, 6,
+      21, 10,
+      33, 5,
+      48, 7,
+      61, 8,
+      70, 9,
+      81, 2);
 
   public SnLBoard() {
     initializeTiles();
diff --git a/src/main/java/edu/ntnu/idi/idatt/boardgame/games/snakesandladders/engine/controller/SnLController.java b/src/main/java/edu/ntnu/idi/idatt/boardgame/games/snakesandladders/engine/controller/SnLController.java
index 2ee49d3..136ff56 100644
--- a/src/main/java/edu/ntnu/idi/idatt/boardgame/games/snakesandladders/engine/controller/SnLController.java
+++ b/src/main/java/edu/ntnu/idi/idatt/boardgame/games/snakesandladders/engine/controller/SnLController.java
@@ -1,5 +1,12 @@
 package edu.ntnu.idi.idatt.boardgame.games.snakesandladders.engine.controller;
 
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.IntStream;
+
 import edu.ntnu.idi.idatt.boardgame.core.domain.dice.Dice;
 import edu.ntnu.idi.idatt.boardgame.core.domain.player.LinearPos;
 import edu.ntnu.idi.idatt.boardgame.core.domain.player.Player;
@@ -12,25 +19,18 @@
 import edu.ntnu.idi.idatt.boardgame.games.snakesandladders.persistence.dto.SnLGameStateDTO;
 import edu.ntnu.idi.idatt.boardgame.games.snakesandladders.persistence.mapper.SnLMapper;
 import edu.ntnu.idi.idatt.boardgame.ui.util.LoggingNotification;
-import java.nio.file.Path;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.stream.IntStream;
 
 public class SnLController extends GameController<LinearPos> {
   private final GameStateRepository<SnLGameStateDTO> repo;
 
   private final int numberOfPlayers;
-  private final List<PlayerColor> playerColors =
-      List.of(
-          PlayerColor.RED,
-          PlayerColor.BLUE,
-          PlayerColor.GREEN,
-          PlayerColor.YELLOW,
-          PlayerColor.ORANGE,
-          PlayerColor.PURPLE);
+  private final List<PlayerColor> playerColors = List.of(
+      PlayerColor.RED,
+      PlayerColor.BLUE,
+      PlayerColor.GREEN,
+      PlayerColor.YELLOW,
+      PlayerColor.ORANGE,
+      PlayerColor.PURPLE);
 
   public SnLController(
       int numberOfPlayers, GameStateRepository<SnLGameStateDTO> repo) {
@@ -59,8 +59,7 @@ protected Map<Integer, Player<LinearPos>> createPlayers(int numberOfPlayers) {
         .forEach(
             playerId -> {
               PlayerColor color = playerColors.get((playerId - 1) % playerColors.size());
-              Player<LinearPos> player =
-                  new Player<>(playerId, "Player " + playerId, color, new LinearPos(1));
+              Player<LinearPos> player = new Player<>(playerId, "Player " + playerId, color, new LinearPos(1));
               players.put(playerId, player);
             });
     return players;
diff --git a/src/main/java/edu/ntnu/idi/idatt/boardgame/games/snakesandladders/view/SnLBoardView.java b/src/main/java/edu/ntnu/idi/idatt/boardgame/games/snakesandladders/view/SnLBoardView.java
index d99f490..b248746 100644
--- a/src/main/java/edu/ntnu/idi/idatt/boardgame/games/snakesandladders/view/SnLBoardView.java
+++ b/src/main/java/edu/ntnu/idi/idatt/boardgame/games/snakesandladders/view/SnLBoardView.java
@@ -10,7 +10,7 @@
 import javafx.scene.layout.Pane;
 import javafx.scene.shape.Line;
 
-public class SnLBoardView extends Pane implements TileObserver {
+public class SnLBoardView extends Pane implements TileObserver<LinearPos> {
   private static final int TILE_SIZE = 60;
   private static final int GAP_SIZE = 5;
   private final GridPane grid;
diff --git a/src/main/java/edu/ntnu/idi/idatt/boardgame/games/snakesandladders/view/SnLTileView.java b/src/main/java/edu/ntnu/idi/idatt/boardgame/games/snakesandladders/view/SnLTileView.java
index deded96..1872a59 100644
--- a/src/main/java/edu/ntnu/idi/idatt/boardgame/games/snakesandladders/view/SnLTileView.java
+++ b/src/main/java/edu/ntnu/idi/idatt/boardgame/games/snakesandladders/view/SnLTileView.java
@@ -15,7 +15,7 @@
 import javafx.scene.shape.Rectangle;
 import javafx.scene.text.Font;
 
-public class SnLTileView implements TileObserver {
+public class SnLTileView implements TileObserver<LinearPos> {
   private final StackPane tilePane;
   private final Label posLabel;
   private final HBox playerBox;
diff --git a/src/main/java/edu/ntnu/idi/idatt/boardgame/ui/MainView.java b/src/main/java/edu/ntnu/idi/idatt/boardgame/ui/MainView.java
index 1f7ab9f..9d7be2a 100644
--- a/src/main/java/edu/ntnu/idi/idatt/boardgame/ui/MainView.java
+++ b/src/main/java/edu/ntnu/idi/idatt/boardgame/ui/MainView.java
@@ -1,6 +1,5 @@
 package edu.ntnu.idi.idatt.boardgame.ui;
 
-import edu.ntnu.idi.idatt.boardgame.core.domain.player.LinearPos;
 import edu.ntnu.idi.idatt.boardgame.core.engine.controller.GameController;
 import edu.ntnu.idi.idatt.boardgame.games.snakesandladders.engine.controller.SnLController;
 import edu.ntnu.idi.idatt.boardgame.games.snakesandladders.persistence.JsonSnLGameStateRepository;
@@ -10,6 +9,10 @@
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
+
+import edu.ntnu.idi.idatt.boardgame.games.cluedo.engine.controller.CluedoController;
+import edu.ntnu.idi.idatt.boardgame.games.cluedo.view.CluedoView;
+
 import javafx.application.Platform;
 import javafx.geometry.Insets;
 import javafx.scene.control.Button;
@@ -23,13 +26,14 @@
 import javafx.stage.Stage;
 
 /**
- * The {@code MainView} class serves as the primary container for the application, featuring a
+ * The {@code MainView} class serves as the primary container for the
+ * application, featuring a
  * sidebar with game selection and placeholders for save/load functionality.
  */
 public class MainView {
   private final BorderPane root;
   private final StackPane contentWrapper;
-  private GameController<LinearPos> currentController;
+  private GameController<?> currentController;
   private Button saveGameButton;
   private Button loadGameButton;
 
@@ -142,6 +146,7 @@ private Button buildLoadButton() {
           }
           File file = chooser.showOpenDialog(stage);
           if (file == null) {
+            System.out.println("Load cancelled.");
             return;
           }
           if (currentController == null) {
@@ -167,7 +172,7 @@ private void addGames(VBox games) {
     snakesAndLaddersButton.setOnAction(e -> loadSnakesAndLadders());
 
     Button cluedoButton = new Button("Cluedo");
-    cluedoButton.setOnAction(e -> System.out.println("Cluedo not implemented yet."));
+    cluedoButton.setOnAction(e -> loadCluedo());
 
     games.getChildren().addAll(snakesAndLaddersButton, cluedoButton);
   }
@@ -193,6 +198,34 @@ private void loadSnakesAndLadders() {
     loadGameButton.setDisable(false);
   }
 
+  private void loadCluedo() {
+    int numberOfPlayers = 6;
+    try {
+      CluedoController cluedoController = new CluedoController(numberOfPlayers);
+      this.currentController = cluedoController;
+
+      CluedoView view = new CluedoView(cluedoController);
+      contentWrapper.getChildren().setAll(view.getRoot());
+
+      saveGameButton.setDisable(true);
+      loadGameButton.setDisable(true);
+
+    } catch (IllegalArgumentException ex) {
+      System.err.println("Failed to load Cluedo: " + ex.getMessage());
+      contentWrapper.getChildren().setAll(new Label("Error loading Cluedo: " + ex.getMessage()));
+      saveGameButton.setDisable(true);
+      loadGameButton.setDisable(true);
+      this.currentController = null;
+    } catch (Exception ex) {
+      System.err.println("An unexpected error occurred while loading Cluedo: " + ex.getMessage());
+      ex.printStackTrace();
+      contentWrapper.getChildren().setAll(new Label("Unexpected error loading Cluedo."));
+      saveGameButton.setDisable(true);
+      loadGameButton.setDisable(true);
+      this.currentController = null;
+    }
+  }
+
   private Stage getStage() {
     if (root.getScene() != null && root.getScene().getWindow() != null) {
       return (Stage) root.getScene().getWindow();
diff --git a/src/main/java/edu/ntnu/idi/idatt/boardgame/ui/util/PlayerColorMapper.java b/src/main/java/edu/ntnu/idi/idatt/boardgame/ui/util/PlayerColorMapper.java
index d2a3c5a..6b58775 100644
--- a/src/main/java/edu/ntnu/idi/idatt/boardgame/ui/util/PlayerColorMapper.java
+++ b/src/main/java/edu/ntnu/idi/idatt/boardgame/ui/util/PlayerColorMapper.java
@@ -6,8 +6,9 @@
 import javafx.scene.paint.Color;
 
 public final class PlayerColorMapper {
-	public static Paint toPaint(PlayerColor c) {
-		return switch(c) {
+	public static Paint toPaint(PlayerColor playerColor) {
+		return switch(playerColor) {
+			case WHITE  -> Color.WHITE;
 			case RED    -> Color.RED;
 			case BLUE   -> Color.BLUE;
 			case GREEN  -> Color.GREEN;


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR adds a Cluedo game board feature with a dedicated view and controller, while updating related board and player components to support the new game state and UI.  
- Integrated Cluedo-specific view components (CluedoView, CluedoBoardView, CluedoTileView) into the main UI.  
- Updated board game logic—including room detection, player start positions, and border handling—to support Cluedo rules.  
- Adjusted shared components (e.g. TileObserver generics and PlayerColor enum) for enhanced type safety and consistency.

### Reviewed Changes

Copilot reviewed 18 out of 18 changed files in this pull request and generated 2 comments.

<details>
<summary>Show a summary per file</summary>

| File | Description |
| ---- | ----------- |
| PlayerColorMapper.java | Renamed parameter to improve clarity in the color-to-paint mapping. |
| MainView.java | Updated game controller type and integrated Cluedo view loading logic. |
| SnLTileView.java / SnLBoardView.java | Added explicit generics to TileObserver implementations. |
| CluedoView.java, CluedoTileView.java, CluedoBoardView.java | Introduced Cluedo-specific UI components and room rendering logic. |
| CluedoController.java | Added Cluedo game logic placeholders and customized player initialization. |
| RoomTile.java, CorridorTile.java, BorderTile.java, AbstractCluedoTile.java | Introduced new board tile types and logic supporting Cluedo. |
| CluedoBoard.java | Implemented board initialization for Cluedo including room, corridor, and border placement. |
| TileObserver.java, PlayerColor.java, GameBoard.java | Minor updates to generics and enum definitions for consistency. |
</details>



<details>
<summary>Comments suppressed due to low confidence (2)</summary>

**src/main/java/edu/ntnu/idi/idatt/boardgame/games/cluedo/domain/board/CluedoBoard.java:90**
* The warning indicates that room specifications might be overlapping with existing tiles; consider adding pre-validation to room configurations to prevent such overlaps.
```
System.err.println("Warning: Overwriting existing tile at (" + r + ", " + c + ") while populating room " + room.getRoomName() + ". Existing: " + board[r][c].getIdentifier());
```
**src/main/java/edu/ntnu/idi/idatt/boardgame/core/domain/player/PlayerColor.java:3**
* The addition of the WHITE constant changes the ordering of the enum, which may affect any logic relying on ordinal values; ensure that all usages respect the new ordering.
```
public enum PlayerColor {
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-10T10:17:48Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This pull request introduces a Cluedo game board with dedicated view components, new tile types, and an updated controller to enhance the Cluedo game implementation. Key changes include the addition of a new CluedoController with supporting game state logic, dedicated view classes (CluedoView, CluedoTileView, CluedoBoardView) for rendering the board and tiles, and updates to various interfaces and enums to support these new features.

### Reviewed Changes

Copilot reviewed 18 out of 18 changed files in this pull request and generated no comments.

<details>
<summary>Show a summary per file</summary>

| File | Description |
| ---- | ----------- |
| src/main/java/edu/ntnu/idi/idatt/boardgame/ui/util/PlayerColorMapper.java | Renamed parameter for clarity in mapping player colors. |
| src/main/java/edu/ntnu/idi/idatt/boardgame/ui/MainView.java | Updated controller type and implemented loadCluedo() to initialize Cluedo game view. |
| src/main/java/edu/ntnu/idi/idatt/boardgame/games/snakesandladders/* | Adjusted generics usage in TileObserver implementations. |
| src/main/java/edu/ntnu/idi/idatt/boardgame/games/cluedo/view/* | Introduced multiple view classes for Cluedo and refined display logic for tiles. |
| src/main/java/edu/ntnu/idi/idatt/boardgame/games/cluedo/engine/controller/CluedoController.java | Added Cluedo-specific game logic and player initialization. |
| src/main/java/edu/ntnu/idi/idatt/boardgame/games/cluedo/domain/board/* | Implemented new tile types (RoomTile, CorridorTile, BorderTile) with specific behavior. |
| src/main/java/edu/ntnu/idi/idatt/boardgame/core/* | Updated interfaces and enums (TileObserver, GameBoard, PlayerColor) to support changes. |
</details>





state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T16:54:37Z

Review comments:
diff_hunk: @@ -0,0 +1,172 @@
+package edu.ntnu.idi.idatt.boardgame.games.cluedo.view;
+
+import edu.ntnu.idi.idatt.boardgame.games.cluedo.domain.board.AbstractCluedoTile;
+import edu.ntnu.idi.idatt.boardgame.games.cluedo.domain.board.BorderTile; // Import BorderTile
+import edu.ntnu.idi.idatt.boardgame.games.cluedo.domain.board.CluedoBoard;
+import edu.ntnu.idi.idatt.boardgame.games.cluedo.domain.board.CorridorTile;
+import edu.ntnu.idi.idatt.boardgame.games.cluedo.domain.board.RoomTile;
+import javafx.geometry.Pos;
+import javafx.scene.control.Label;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.Pane;
+import javafx.scene.layout.StackPane;
+import javafx.scene.paint.Color;
+import javafx.scene.shape.Rectangle;
+import javafx.scene.text.Font;
+import javafx.scene.text.TextAlignment;
+
+public class CluedoBoardView extends Pane {
+    private static final int TILE_SIZE = 30;
+    private static final int GAP_SIZE = 0;
+    private final GridPane grid;
+    private final CluedoBoard boardModel;
+
+    public CluedoBoardView(CluedoBoard boardModel) {
+        this.boardModel = boardModel;
+        this.grid = new GridPane();
+
+        grid.setHgap(GAP_SIZE);
+        grid.setVgap(GAP_SIZE);
+
+        initializeBoard();
+        getChildren().add(grid);
+    }
+
+    private void initializeBoard() {
+        grid.getChildren().clear(); // Clear previous children if re-initializing
+        int numRows = boardModel.getRows();
+        int numCols = boardModel.getCols();
+        AbstractCluedoTile[][] boardGrid = boardModel.getBoardGrid();
+        boolean[][] visited = new boolean[numRows][numCols]; // Track visited cells for room spanning
+
+        for (int row = 0; row < numRows; row++) {
+            for (int col = 0; col < numCols; col++) {
+                if (visited[row][col]) {
+                    continue;
+                }
+
+                AbstractCluedoTile tileModel = boardGrid[row][col];
+
+                if (tileModel == null) { // Handle potential null tiles (though CluedoBoard aims to fill all)
+                    Pane emptyPane = new Pane();
+                    emptyPane.setPrefSize(TILE_SIZE, TILE_SIZE);
+                    emptyPane.setStyle("-fx-background-color: #333;"); // Dark color for uninitialized
+                    grid.add(emptyPane, col, row);
+                    visited[row][col] = true;
+                    continue;
+                }
+
+
+                if (tileModel instanceof RoomTile roomTile) {
+                    // Logic for finding room span and drawing RoomTile
+                    // (Assuming this part correctly identifies all cells belonging to the same RoomTile instance)
+                    int minR = row, minC = col, maxR = row, maxC = col;
+
+                    // Simple BFS/DFS or iteration needed here to correctly find all connected parts of THE SAME room instance
+                    // The provided loop might not be robust for complex room shapes or if boardGrid[r][c] could be a *different* RoomTile
+                    // For rectangular rooms defined as one instance, the existing logic might work if boardGrid[r][c] == roomTile is the check.
+
+                    // To correctly find the span of a single RoomTile instance:
+                    for(int rScan = 0; rScan < numRows; rScan++) {
path: src/main/java/edu/ntnu/idi/idatt/boardgame/games/cluedo/view/CluedoBoardView.java
user: login: Copilot
body: Consider optimizing the room span detection logic to avoid iterating over the entire grid for each room tile; while acceptable for a 25x25 board, this approach may lead to performance issues if board dimensions increase.
created_at: 2025-05-10T10:17:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,357 @@
+package edu.ntnu.idi.idatt.boardgame.games.cluedo.domain.board;
+
+import edu.ntnu.idi.idatt.boardgame.core.domain.board.GameBoard;
+import edu.ntnu.idi.idatt.boardgame.core.domain.player.GridPos;
+import edu.ntnu.idi.idatt.boardgame.core.domain.player.Player;
+import edu.ntnu.idi.idatt.boardgame.core.domain.player.PlayerColor;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+public final class CluedoBoard implements GameBoard<GridPos> {
+
+  private static final int BOARD_SIZE = 25; // This size INCLUDES the 1-tile thick border
+
+  // Adjusted Start Positions (moved 1 tile inwards from the border)
+  private static final GridPos PLAYER_WHITE_START_POSITION = new GridPos(1, 9); // Mrs. White (Yellow Token)
+  private static final GridPos PLAYER_RED_START_POSITION = new GridPos(17, 1); // Col. Mustard (Red Token)
+  private static final GridPos PLAYER_BLUE_START_POSITION = new GridPos(6, 23); // Mrs. Peacock (Blue Token)
+  private static final GridPos PLAYER_GREEN_START_POSITION = new GridPos(1, 14); // Rev. Green (Green Token)
+  private static final GridPos PLAYER_YELLOW_START_POSITION = new GridPos(23, 7); // Miss Scarlett (Yellow Token)
+  private static final GridPos PLAYER_PURPLE_START_POSITION = new GridPos(19, 23); // Prof. Plum (Purple Token)
+
+
+  // Adjusted START_POS constants to match the character names more clearly and adjusted positions
+  private static final GridPos START_POS_MISS_SCARLETT = new GridPos(23, 7); // PlayerColor.WHITE (Scarlett)
+  private static final GridPos START_POS_COL_MUSTARD = new GridPos(17, 1);   // PlayerColor.RED (Mustard)
+  private static final GridPos START_POS_MRS_WHITE = new GridPos(1, 9);     // PlayerColor.YELLOW (Mrs. White)
+  private static final GridPos START_POS_REV_GREEN = new GridPos(1, 14);    // PlayerColor.GREEN (Rev. Green)
+  private static final GridPos START_POS_MRS_PEACOCK = new GridPos(6, 23);  // PlayerColor.BLUE (Mrs. Peacock)
+  private static final GridPos START_POS_PROF_PLUM = new GridPos(19, 23);   // PlayerColor.PURPLE (Prof. Plum)
+
+  private static final List<RoomSpec> ROOM_SPECS =
+      List.of(
+          new RoomSpec("Kitchen", new RoomDimensions(1, 1, 6, 5), true),
+          new RoomSpec("Ball Room", new RoomDimensions(1, 8, 7, 15), false),
+          new RoomSpec("Conservatory", new RoomDimensions(1, 18, 4, 23), true),
+          new RoomSpec("Dining Room", new RoomDimensions(10, 1, 15, 7), false),
+          new RoomSpec("Billiard Room", new RoomDimensions(7, 18, 12, 23), false),
+          new RoomSpec("Library", new RoomDimensions(14, 17, 18, 23), false),
+          new RoomSpec("Study", new RoomDimensions(21, 17, 23, 23), true),
+          new RoomSpec("Hall", new RoomDimensions(18, 9, 23, 14), false),
+          new RoomSpec("Lounge", new RoomDimensions(19, 1, 23, 6), true));
+
+  private final AbstractCluedoTile[][] board = new AbstractCluedoTile[BOARD_SIZE][BOARD_SIZE];
+
+  public CluedoBoard() {
+    initializeTiles();
+  }
+
+  private void initializeTiles() {
+    insertBorderTiles(); // Place border tiles first
+    insertRooms();
+    insertCorridorTiles(); // Fills remaining nulls with corridors
+    placeStartPosAdjacentBorders(); // Add specific borders around start positions
+    // TODO: Secret passages and doors are not yet implemented here
+  }
+
+  private void insertBorderTiles() {
+    for (int i = 0; i < BOARD_SIZE; i++) {
+      // Top and bottom rows
+      if (board[0][i] == null) board[0][i] = new BorderTile(0, i);
+      if (board[BOARD_SIZE - 1][i] == null) board[BOARD_SIZE - 1][i] = new BorderTile(BOARD_SIZE - 1, i);
+
+      // Left and right columns (excluding corners already set)
+      if (i > 0 && i < BOARD_SIZE - 1) {
+        if (board[i][0] == null) board[i][0] = new BorderTile(i, 0);
+        if (board[i][BOARD_SIZE - 1] == null) board[i][BOARD_SIZE - 1] = new BorderTile(i, BOARD_SIZE - 1);
+      }
+    }
+  }
+
+
+  private List<RoomTile.Point> createRectangularOutline(RoomDimensions roomDimensions) {
+    List<RoomTile.Point> outline = new ArrayList<>();
+    outline.add(new RoomTile.Point(roomDimensions.top, roomDimensions.left));
+    outline.add(new RoomTile.Point(roomDimensions.top, roomDimensions.right));
+    outline.add(new RoomTile.Point(roomDimensions.bottom, roomDimensions.right));
+    outline.add(new RoomTile.Point(roomDimensions.bottom, roomDimensions.left));
+    outline.add(new RoomTile.Point(roomDimensions.top, roomDimensions.left)); // Close the loop
+    return outline;
+  }
+
+  private void populateRoomTiles(RoomDimensions roomDimensions, RoomTile room) {
+    for (int r = roomDimensions.top; r <= roomDimensions.bottom; r++) {
+      for (int c = roomDimensions.left; c <= roomDimensions.right; c++) {
+        // Basic bounds check before assignment
+        if (r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE) {
+          if (board[r][c] != null) {
+             // This might happen if a room specification overlaps with border or another room
+             // Given the adjustments, rooms should be within the non-border area.
+             System.err.println("Warning: Overwriting existing tile at (" + r + ", " + c + ") while populating room " + room.getRoomName()
+                                + ". Existing: " + board[r][c].getIdentifier());
+          }
+          board[r][c] = room;
+          room.setWalkable(false); // Room interiors are not walkable squares in Cluedo movement
+        } else {
+          System.err.println("Warning: Attempted to populate room tile outside board bounds at (" + r + ", " + c + ")");
+        }
+      }
+    }
+  }
+
+  private void insertRooms() {
+    ROOM_SPECS.forEach(
+        spec -> {
+          // Validate room dimensions against border
+          if (spec.dims.top < 1 || spec.dims.left < 1 || spec.dims.bottom >= BOARD_SIZE -1 || spec.dims.right >= BOARD_SIZE -1) {
+              System.err.println("CRITICAL Error: Room " + spec.name + " with dimensions " + spec.dims + " extends into the border area or outside bounds.");
+              return; // Skip this room to prevent errors
+          }
+          List<RoomTile.Point> outline = createRectangularOutline(spec.dims);
+          RoomTile room = new RoomTile(spec.name, outline);
+          populateRoomTiles(spec.dims, room);
+          // TODO: if (spec.hasSecretPassage) addSecretPassage(room);
+        });
+  }
+
+  private void insertCorridorTiles() {
+    for (int row = 0; row < BOARD_SIZE; row++) {
+      for (int col = 0; col < BOARD_SIZE; col++) {
+        if (board[row][col] == null) { // If not a border and not a room, it's a corridor
+	        board[row][col] = new CorridorTile(row, col);
+        }
+      }
+    }
+    // Mark center area (cellar/stairs) as non-walkable if desired
+    // These coordinates should be fine as they are well within the board.
+    for (int r = 10; r <= 16; r++) { // Approximate bounds
+      for (int c = 9; c <= 14; c++) { // Approximate bounds
+        if (isValidPosition(new GridPos(r, c)) && board[r][c] instanceof CorridorTile) {
+          board[r][c].setWalkable(false); // Mark cellar area tiles non-walkable
+        }
+      }
+    }
+  }
+
+  private void placeStartPosAdjacentBorders() {
+    // PlayerColor.WHITE (Miss Scarlett) -> START_POS_MISS_SCARLETT = new GridPos(23, 7);
+    // Bottom edge: place borders left (23,6) and right (23,8)
+    replaceWithBorderIfCorridor(23, 6);
+    replaceWithBorderIfCorridor(23, 8);
+
+    // PlayerColor.RED (Col. Mustard) -> START_POS_COL_MUSTARD = new GridPos(17, 1);
+    // Left edge: place borders above (16,1) and below (18,1)
+    replaceWithBorderIfCorridor(16, 1);
+    replaceWithBorderIfCorridor(18, 1);
+
+    // PlayerColor.YELLOW (Mrs. White) -> START_POS_MRS_WHITE = new GridPos(1, 9);
+    // Top edge: place borders left (1,8) and right (1,10)
+    replaceWithBorderIfCorridor(1, 8);
+    replaceWithBorderIfCorridor(1, 10);
+
+    // PlayerColor.GREEN (Rev. Green) -> START_POS_REV_GREEN = new GridPos(1, 14);
+    // Top edge: place borders left (1,13) and right (1,15)
+    replaceWithBorderIfCorridor(1, 13);
+    replaceWithBorderIfCorridor(1, 15);
+
+    // PlayerColor.BLUE (Mrs. Peacock) -> START_POS_MRS_PEACOCK = new GridPos(6, 23);
+    // Right edge: place borders above (5,23) and below (7,23)
+    replaceWithBorderIfCorridor(5, 23);
+    replaceWithBorderIfCorridor(7, 23);
+
+    // PlayerColor.PURPLE (Prof. Plum) -> START_POS_PROF_PLUM = new GridPos(19, 23);
+    // Right edge: place borders above (18,23) and below (20,23)
+    replaceWithBorderIfCorridor(18, 23);
+    replaceWithBorderIfCorridor(20, 23);
+  }
+
+  private void replaceWithBorderIfCorridor(int r, int c) {
+    if (isValidPosition(new GridPos(r, c))) {
+      AbstractCluedoTile tile = board[r][c];
+      // Only replace if it's a CorridorTile and not part of a room or already a border.
+      // The initial border is at 0 and BOARD_SIZE-1. These replacements are for tiles within the playable area.
+      if (tile instanceof CorridorTile) {
+        board[r][c] = new BorderTile(r, c);
+      }
+    }
+  }
+
+  @Override
+  public int getBoardSize() {
+    return BOARD_SIZE;
+  }
+
+  public int getRows() {
+    return BOARD_SIZE;
+  }
+
+  public int getCols() {
+    return BOARD_SIZE;
+  }
+
+  @Override
+  public void setPlayerPosition(Player<GridPos> player, GridPos position) {
+    if (!isValidPosition(position)) {
+      System.err.println("Error: Attempt to set invalid position " + position);
+      return;
+    }
+    AbstractCluedoTile targetTile = getTileAtPosition(position);
+    if (targetTile instanceof BorderTile) {
+        System.err.println("Error: Cannot set player position to a BorderTile at " + position);
+        return;
+    }
+    if (!targetTile.isWalkable() && !(targetTile instanceof RoomTile)) { // Allow moving into rooms even if "not walkable" squares
+        System.err.println("Error: Cannot set player position to a non-walkable tile at " + position + " of type " + targetTile.getClass().getSimpleName());
+        return;
+    }
+
+
+    GridPos oldPos = player.getPosition();
+
+    if (isValidPosition(oldPos)) {
+      AbstractCluedoTile oldTile = getTileAtPosition(oldPos);
+      if (oldTile != null) {
+        oldTile.removePlayer(player);
+      } else {
+        // This can happen if player was at an uninitialized/invalid old position
+        System.err.println("Warning: Player " + player.getName() + " had no valid old tile at " + oldPos);
+      }
+    } else if (oldPos != null) {
+        // Player might be starting, oldPos could be (0,0) by default from Player constructor if not immediately set
+        // System.err.println("Warning: Player " + player.getName() + " had an invalid old position " + oldPos);
+    }
+
+
+    player.setPosition(position);
+    AbstractCluedoTile newTile = getTileAtPosition(position); // Re-fetch, could be same as targetTile
+    if (newTile != null) {
+      newTile.addPlayer(player);
+    } else {
+      // This should not happen if isValidPosition passed and position is not BorderTile
+      System.err.println("CRITICAL Error: New tile is null at valid position " + position + " for player " + player.getName());
+    }
+  }
+
+  @Override
+  public void addPlayersToStart(Map<Integer, Player<GridPos>> players) {
+    players
+        .values()
+        .forEach(
+            player -> {
+              GridPos startPos = getPlayerStartPosition(player);
+              if (isValidPosition(startPos)) {
+                AbstractCluedoTile startTile = getTileAtPosition(startPos);
+                if (startTile instanceof BorderTile) {
+                    System.err.println(
+                        "CRITICAL Error: Calculated start position " + startPos + " for player " + player.getName() + " is a BorderTile. Adjust start positions.");
+                    return;
+                }
+                if (startTile == null) {
+                     System.err.println(
+                      "CRITICAL Error: Start tile is null at supposedly valid position "
+                          + startPos + " for player " + player.getName() + ". This might be due to board initialization order or incorrect coordinates.");
+                    return;
+                }
+
+                player.setPosition(startPos); // Set player's internal position
+
+                if (!startTile.isWalkable() && !(startTile instanceof RoomTile)) { // Rooms are fine to start in.
+                  System.err.println(
+                      "Error: Start tile at " + startPos + " for player " + player.getName() + " is not walkable and not a Room! Type: " + startTile.getClass().getSimpleName());
+                }
+                startTile.addPlayer(player); // Add player to the tile model
+
+              } else {
+                System.err.println(
+                    "Error: Invalid start position "
+                        + startPos
+                        + " calculated for player "
+                        + player.getName());
+              }
+            });
+  }
+
+  // PLACEHOLDER METHOD
+  @Override
+  public void incrementPlayerPosition(Player<GridPos> player, int increment) {
+    System.out.println(
+        "Warning: incrementPlayerPosition called. This method is not suitable for standard Cluedo movement.");
+  }
+
+  public AbstractCluedoTile getTileAtPosition(GridPos pos) {
+    if (isValidPosition(pos)) {
+      return board[pos.row()][pos.col()];
+    }
+    return null; // Out of bounds
+  }
+
+  public void movePlayer(Player<GridPos> player, GridPos toPos) {
+    if (!isValidPosition(toPos)) {
+      System.err.println("Invalid move target: " + toPos);
+      return;
+    }
+    AbstractCluedoTile targetTile = getTileAtPosition(toPos);
+    if (targetTile == null) {
+        System.err.println("Cannot move to null tile: " + toPos);
+        return;
+    }
+    if (targetTile instanceof BorderTile) {
+        System.err.println("Cannot move to a BorderTile: " + toPos);
+        return;
+    }
+    if (!targetTile.isWalkable() && !(targetTile instanceof RoomTile)) { // Allow entering rooms
+      System.err.println("Cannot move to non-walkable tile: " + toPos + " of type " + targetTile.getClass().getSimpleName());
+      return;
+    }
+    setPlayerPosition(player, toPos);
+  }
+
+  private boolean isAdjacent(GridPos from, GridPos to) {
+    if (from == null || to == null)
+      return false;
+    int rowDiff = Math.abs(from.row() - to.row());
+    int colDiff = Math.abs(from.col() - to.col());
+    return (rowDiff == 1 && colDiff == 0) || (rowDiff == 0 && colDiff == 1);
+  }
+
+  private boolean isValidPosition(GridPos pos) {
+    return pos != null
+        && pos.row() >= 0
+        && pos.row() < BOARD_SIZE
+        && pos.col() >= 0
+        && pos.col() < BOARD_SIZE;
+  }
+
+  private GridPos getPlayerStartPosition(Player<GridPos> player) {
+    // Mapping player character names (implied by PlayerColor choice in controller) to their start positions
+    PlayerColor color = player.getColor();
+    return switch (color) {
path: src/main/java/edu/ntnu/idi/idatt/boardgame/games/cluedo/domain/board/CluedoBoard.java
user: login: Copilot
body: [nitpick] The default case in the switch expression throws an exception for unmapped player colors, which might be too strict if new colors are added; consider defining a safe default or logging a warning to handle future extensions more gracefully.
created_at: 2025-05-10T10:17:48Z
author_association: CONTRIBUTOR

PR comments:
user: login: eilifhl
created_at: 2025-05-09T20:42:02Z
body: Oops skal fikse
author_association: COLLABORATOR
user: login: Tmpecho
created_at: 2025-05-14T16:08:01Z
body: ![image](https://github.com/user-attachments/assets/0cfda1ae-424c-4c16-b5ba-a321183a14bb)

3 things:
1. Increase the width of the center tile to match real board more closely?
2. Why do two people start in a room (and why are their circles bigger)? Fix starting positions?
3. Why does some edge tiles not have the white seperators? i.e the bottom right corner
author_association: OWNER
user: login: eilifhl
created_at: 2025-05-14T16:34:35Z
body: >     3. Why does some edge tiles not have the white seperators? i.e the bottom right corner

Something weird happening with the hGap and vGap i think. Tried to set it to zero, but the majority still remained
author_association: COLLABORATOR
user: login: eilifhl
created_at: 2025-05-14T16:39:03Z
body: Adjusted border gaps, but in the future they should ideally be a continuous segment. 
author_association: COLLABORATOR
user: login: Tmpecho
created_at: 2025-05-14T16:49:55Z
body: Only doors remaining then?
author_association: OWNER
user: login: eilifhl
created_at: 2025-05-14T16:51:07Z
body: Yeah, just deciding on the view implementation. Im probably just gonna draw lines across the already existing RoomTiles
author_association: COLLABORATOR
user: login: Tmpecho
created_at: 2025-05-14T17:00:38Z
body: > Yeah, just deciding on the view implementation. Im probably just gonna draw lines across the already existing RoomTiles

It just has to be an MVP implementation.
author_association: OWNER