climb_stairs

BDD Scenarios

client/game-view.feature

Feature: Game View UI (Canvas)
  As a player or host, I want to see the ladder game rendered on canvas so I can watch the reveal animations

  Background:
    Given I am in the game view of an active room

  # --- 遊戲開始時顯示梯子 ---

  Scenario: Display ladder on game start with correct rail count
    Given the game has started with 3 players
    When I view the game canvas
    Then I should see 3 vertical rails on the canvas
    And each rail should use the dimmed color (colorFromIndexDim) for the player's color index
    And each rail should have RAIL_WIDTH of 3px

  Scenario: Player names displayed at top of canvas
    Given the game has started with players "Alice", "Bob", "Carol"
    When I view the game canvas
    Then I should see "Alice", "Bob", and "Carol" displayed above their respective rails
    And names should be rendered at 13px system-ui font
    And my own name should be in bold purple (#a78bfa)

  Scenario: Canvas uses dark background
    When I view the game canvas
    Then the canvas background should be #0f0f1a
    And the canvas should adapt to devicePixelRatio for Retina displays

  Scenario: Canvas padding matches specification
    When I view the game canvas
    Then the top padding should be 60px for player names
    And the side padding should be 24px on each side
    And the bottom padding should be 32px for result slots

  Scenario: Horizontal rungs rendered between rails
    Given a ladder with rungs defined by the seed
    When I view the game canvas
    Then the rungs should be rendered at RUNG_WIDTH of 2px
    And rungs should have color #3a3a60

  # --- 揭曉路徑動畫 ---

  Scenario: Reveal path animation plays when host clicks next
    Given the game state is "revealing"
    And it is player "Bob"'s turn to be revealed
    When the host clicks "下一位"
    Then I should see an animated ball moving along "Bob"'s path
    And the ball should have BALL_RADIUS of 8px with white core and player color shadow

  Scenario: Path uses player's assigned color during animation
    Given player "Bob" has colorIndex 1
    When "Bob"'s path is being revealed
    Then the animated path should use color hsl(7.2, 70%, 60%)
    And the ball marker should glow with that color at shadowBlur 16

  Scenario: Animating path is fully opaque
    When a path is currently being animated
    Then the path should render with globalAlpha of 1.0

  Scenario: Reveal all paths via REVEAL_ALL broadcast
    Given the game state is "revealing"
    When the host clicks "全部揭曉"
    Then all remaining paths should animate sequentially
    And all animations should complete within 2 seconds
    And if 2 seconds is exceeded the animation jumps to the final frame

  Scenario: Host controls visible in revealing state
    Given I am the host
    And the game state is "revealing"
    Then I should see the "下一位" button
    And I should see the "全部揭曉" button with gold styling
    And I should see the auto-reveal toggle switch

  Scenario: Host begin reveal button visible in running state
    Given I am the host
    And the game state is "running"
    Then I should see the "開始揭曉" button
    And I should not see the "下一位" button

  Scenario: Auto-reveal toggle controls interval input
    Given I am the host and the auto-reveal toggle is off
    Then the interval input should be disabled with opacity 0.4
    When I enable the auto-reveal toggle
    Then the interval input should become enabled
    And the toggle should show aria-checked="true"

  Scenario: Auto-reveal interval validation
    Given I am the host and auto-reveal is enabled
    When I enter 0 in the interval input
    And I start auto-reveal
    Then I should see an error "間隔須為 1~30 的整數"

  # --- 中獎者以金色光暈高亮 ---

  Scenario: Winner highlighted with gold glow effect
    Given all paths are revealed
    And "Alice" is the winner
    When the winner is determined
    Then "Alice"'s path should have shadowColor #ffd700 and shadowBlur 10
    And a star "★" should appear at "Alice"'s bottom position in gold (#ffd700) at 11px

  Scenario: Winner endpoint has gold glow
    Given "Alice" is the winner
    When her path reaches the bottom
    Then the endpoint circle (radius 6px) should have a gold glow

  # --- 已完成路徑半透明顯示 ---

  Scenario: Completed paths are semi-transparent
    Given "Bob"'s path has been fully revealed
    When I view the canvas
    Then "Bob"'s path should render with globalAlpha of 0.6

  Scenario: Currently animating path is fully opaque
    Given "Bob"'s path has been revealed (opacity 0.6)
    And "Carol"'s path is currently being animated
    When I view the canvas
    Then "Carol"'s path should render with globalAlpha of 1.0
    And "Bob"'s path should still render with globalAlpha of 0.6

  Scenario: Multiple revealed paths are semi-transparent
    Given 3 paths have been revealed for "Alice", "Bob", "Carol"
    When I view the canvas
    Then all 3 paths should render with globalAlpha of 0.6
    And winner paths should additionally show the gold glow

  # --- Canvas 視覺層次 ---

  Scenario: Canvas renders visual layers in correct order
    Given a game is in revealing state
    When I view the canvas
    Then the visual layers from bottom to top should be:
      | Layer              | Description                        |
      | rails              | Vertical tracks with dim color     |
      | rungs              | Horizontal bars in #3a3a60         |
      | revealed-paths     | Player paths at opacity 0.6        |
      | animating-path     | Current path at opacity 1.0        |
      | winner-glow        | Gold shadow on winner path         |
      | ball-marker        | Animated ball with glow            |
      | endpoints          | Bottom circles                     |
      | player-names       | Names at top of rails              |
      | winner-star        | Gold ★ at bottom of winner         |

  # --- 佈局與響應式 ---

  Scenario: Desktop layout shows sidebar on the right
    Given my viewport is 1024px or wider
    When I view the game view
    Then I should see the canvas on the left taking majority of width
    And the sidebar should be on the right at 280px width

  Scenario: Mobile layout stacks canvas and sidebar vertically
    Given my viewport is narrower than 768px
    When I view the game view
    Then the canvas should take full width
    And the sidebar results list should appear below the canvas
    And the host controls should be fixed at the bottom

  Scenario: Header shows room name, status badge, and connection dot
    When I view the game view header
    Then I should see the room name as the game title
    And I should see a status badge matching the current game state
    And I should see the connection status dot on the right

  # --- 結果側欄(揭曉中即時更新)---

  Scenario: Result list shows winner entry with crown
    Given "Alice" has been revealed as a winner
    When I view the sidebar result list
    Then I should see "Alice" with a ♛ crown in gold
    And the entry background should be #1c1600 with border #b8960a

  Scenario: Result list shows non-winner entry
    Given "Bob" has been revealed as a non-winner
    When I view the sidebar result list
    Then I should see "Bob" without a crown
    And the entry background should be var(--bg) with dim text

  Scenario: My own result entry is highlighted with purple border
    Given my path has been revealed
    When I view my entry in the result list
    Then my entry should have a purple left border of 3px solid #6c63ff
    And I should see "(你)" label next to my name

  Scenario: Result items animate in sequentially
    Given multiple players have been revealed
    When I view the result list
    Then each result item should fade in with translateY animation of 300ms
    And each subsequent item should be delayed by 50ms

client/lobby.feature

Feature: Lobby UI
  As a player or host, I want to create or join a game from the lobby

  Background:
    Given I am on the lobby page

  # --- 建立房間 ---

  Scenario: Create room from lobby as host
    When I enter nickname "Alice" in the create-room form
    And I click "建立房間"
    Then I should see the waiting room
    And my nickname "Alice" should be displayed in the player list
    And I should see the host badge next to my name

  Scenario: Create room with optional room title
    When I enter nickname "Alice" in the create-room form
    And I enter room title "年終抽獎 2026"
    And I click "建立房間"
    Then I should see the waiting room
    And the room title "年終抽獎 2026" should be displayed

  Scenario: Create room button shows loading state
    When I enter nickname "Alice" in the create-room form
    And I click "建立房間"
    Then the button should display "建立中…" and be disabled during the request

  Scenario: Create room failure shows error toast
    Given the server returns a create-room error
    When I enter nickname "Alice" in the create-room form
    And I click "建立房間"
    Then I should see an error toast "建立失敗,請重試"
    And the nickname input should still contain "Alice"

  # --- 加入房間 ---

  Scenario: Join room from lobby
    When I enter nickname "Bob" in the join-room form
    And I enter room code "ABC123"
    And I click "加入房間"
    Then I should be in the waiting room with other players

  Scenario: Join room with URL room parameter pre-filled
    Given I navigate to the lobby with URL parameter "?room=K7NP3Q"
    Then the room code input should be pre-filled with "K7NP3Q"

  Scenario: Join button auto-enabled when both nickname and room code are filled
    Given the room code input is pre-filled via URL parameter "K7NP3Q"
    And the nickname input is pre-filled from localStorage "ladder_last_nickname" as "Carol"
    Then the "加入房間" button should be enabled without further interaction

  # --- 表單驗證 ---

  Scenario: Empty nickname validation on create-room
    When I click "建立房間" without entering a nickname
    Then I should see an inline error message for the nickname field
    And the "建立房間" button should be disabled

  Scenario: Empty nickname validation on join-room
    When I enter room code "ABC123"
    And I click "加入房間" without entering a nickname
    Then I should see an inline error message for the nickname field
    And the "加入房間" button should be disabled

  Scenario: Nickname exceeding 20 characters shows validation error
    When I enter a nickname of 21 characters in the create-room form
    Then I should see an inline error message "暱稱須為 1–20 個字元"
    And the "建立房間" button should be disabled

  Scenario: Room code field forces uppercase input
    When I type "abc123" into the room code input
    Then the room code input should display "ABC123"

  Scenario: Duplicate nickname error from server
    Given the server returns error "NICKNAME_TAKEN"
    When I enter nickname "Alice" in the join-room form
    And I enter room code "ABC123"
    And I click "加入房間"
    Then I should see an error toast "此暱稱已被使用,請換一個"

  Scenario: Room not found error from server
    Given the server returns error "ROOM_NOT_FOUND"
    When I enter nickname "Bob" in the join-room form
    And I enter room code "XXXXXX"
    And I click "加入房間"
    Then I should see an error toast "找不到此房間,請確認房間碼"

  Scenario: Room full error from server
    Given the server returns error "ROOM_FULL"
    When I enter nickname "Bob" in the join-room form
    And I enter room code "ABC123"
    And I click "加入房間"
    Then I should see an error toast "房間已滿,無法加入"

  Scenario: Room not joinable when game already started
    Given the server returns error "ROOM_NOT_JOINABLE"
    When I enter nickname "Bob" in the join-room form
    And I enter room code "ABC123"
    And I click "加入房間"
    Then I should see an error toast "此房間遊戲已開始,無法加入"

  # --- 已有暱稱自動填入 ---

  Scenario: Nickname auto-filled from localStorage
    Given localStorage key "ladder_last_nickname" is set to "Carol"
    When I visit the lobby page
    Then the nickname input in the join-room form should be pre-filled with "Carol"

  Scenario: Nickname is saved to localStorage after successful join
    When I enter nickname "Dave" in the join-room form
    And I enter room code "ABC123"
    And I click "加入房間"
    And the join is successful
    Then localStorage key "ladder_last_nickname" should be set to "Dave"

client/play_again_ui.feature

# features/client/play_again_ui.feature
#
# BDD Feature: 再玩一場 — Client 端 UI 流程
#
# 對應規格:
#   PDD §5.3 Game View — finished 狀態主持人控制面板(「再玩一局」按鈕 .btn-ghost)
#   PDD §5.4 Result View — finished 全螢幕結果頁主持人限定「再玩一局」按鈕
#   API §2.10 POST /api/rooms/:code/game/play-again
#   API §3.6 WS PLAY_AGAIN / ROOM_STATE(waiting) 廣播
#
# 摘要:
#   遊戲結束(status=finished)後,房主看到「再玩一場」按鈕(非房主不顯示/disabled)。
#   房主點擊後 → 呼叫 POST /api/rooms/:code/game/play-again → 成功則 UI 切換至 waiting room。
#   所有玩家收到 WS PLAY_AGAIN 或 ROOM_STATE(waiting) 後同步切換至 waiting room UI。
#   含載入狀態防重複送出、以及 API 錯誤時 Toast 回饋。

Feature: Play Again UI Flow
  As a host or player
  I want the UI to correctly render and handle the "Play Again" action
  So that the host can start a new round and all players transition back to the waiting room

  Background:
    # 前置條件:已有進行完畢的房間,所有玩家已透過 WS 連線
    Given room "ROOM01" exists with status "finished"
    And player "Alice" is the host of room "ROOM01"
    And players "Bob" and "Carol" are non-host members of room "ROOM01"
    And all three players have active WebSocket connections

  # ---------------------------------------------------------------------------
  # 1. Finished 頁面按鈕可見性
  # 主持人看到「再玩一場」按鈕;非主持人不顯示(或為 disabled)
  # 來源:PDD §5.3 finished 狀態控制面板、PDD §5.4 Result View
  # ---------------------------------------------------------------------------

  # 房主在 finished 頁面應看到「再玩一場」按鈕
  @play-again-ui @visibility @host @P0
  Scenario: Host sees "Play Again" button in the finished result view
    # finished 狀態結果頁 Sidebar 底部,主持人限定顯示「再玩一局」.btn-ghost 按鈕
    Given the game has finished and all players are viewing the result page
    When Alice (the host) views the finished result page
    Then Alice sees a "Play Again" button with class "btn-ghost"
    And the "Play Again" button is enabled and clickable

  # 非房主在 finished 頁面不應看到「再玩一場」按鈕
  @play-again-ui @visibility @non-host @P0
  Scenario: Non-host player does not see "Play Again" button in the finished result view
    # 普通玩家的結果頁 Sidebar 底部不渲染「再玩一局」按鈕
    Given the game has finished and all players are viewing the result page
    When Bob (a non-host player) views the finished result page
    Then Bob does not see a "Play Again" button
    And no "Play Again" element is present in Bob's DOM

  # ---------------------------------------------------------------------------
  # 2. 房主點擊「再玩一場」→ 呼叫 POST /api/rooms/:code/game/play-again
  # 來源:API §2.10
  # ---------------------------------------------------------------------------

  # 房主點擊按鈕後前端呼叫正確的 HTTP endpoint
  @play-again-ui @http-call @P0
  Scenario: Host clicks "Play Again" and the client sends POST play-again request
    # 點擊後前端送出 POST /api/rooms/ROOM01/game/play-again,帶上 Authorization Bearer token
    Given Alice is viewing the finished result page
    And the "Play Again" button is visible and enabled
    When Alice clicks the "Play Again" button
    Then the client sends a POST request to "/api/rooms/ROOM01/game/play-again"
    And the request includes "Authorization: Bearer <hostToken>" header
    And the request body is empty or "{}"

  # ---------------------------------------------------------------------------
  # 3. 成功回應 → 切換至 waiting room UI
  # 來源:API §2.10 200 OK、PDD §5.2 Waiting Room
  # ---------------------------------------------------------------------------

  # 伺服器回傳 200 OK,房主 UI 立即切換至等待室
  @play-again-ui @success @navigation @P0
  Scenario: Host UI transitions to waiting room after successful play-again response
    # HTTP 200 → 前端應立即切換至等待室視圖,顯示等待大廳元素
    Given Alice has clicked the "Play Again" button
    When the server responds with HTTP 200 and status "waiting"
    Then Alice's UI transitions to the waiting room view
    And Alice sees the room code "ROOM01" displayed
    And Alice sees the player list in waiting state
    And Alice sees the "Start Game" button (host control)
    And the "Play Again" button is no longer visible

  # ---------------------------------------------------------------------------
  # 4. 收到 WS PLAY_AGAIN 或 ROOM_STATE(waiting) → 所有玩家 UI 切換至 waiting room
  # 來源:API §3.6 WS PLAY_AGAIN、ROOM_STATE broadcast
  # ---------------------------------------------------------------------------

  # 非房主玩家收到 WS ROOM_STATE(waiting) 廣播後切換至等待室
  @play-again-ui @websocket @broadcast @P0
  Scenario: All players transition to waiting room upon receiving ROOM_STATE(waiting) broadcast
    # 伺服器廣播 ROOM_STATE(status="waiting") 後,所有連線玩家 UI 同步切換
    Given the host has triggered play-again and the server broadcasts ROOM_STATE with status "waiting"
    When Bob receives the ROOM_STATE WebSocket message with status "waiting"
    Then Bob's UI transitions from the finished result page to the waiting room view
    And Bob sees the waiting room with the player list
    And Bob sees "Waiting for host to start…" message

  # 收到 WS PLAY_AGAIN 事件(若前端實作 WS 路徑)也應觸發切換
  @play-again-ui @websocket @play-again-event @P1
  Scenario: All players transition to waiting room upon receiving PLAY_AGAIN WebSocket event
    # 前端若監聽 PLAY_AGAIN WS 事件,收到後同樣切換至等待室
    Given all players are viewing the finished result page
    When the server broadcasts a PLAY_AGAIN WebSocket event to all connected clients
    Then Alice's UI transitions to the waiting room view
    And Bob's UI transitions to the waiting room view
    And Carol's UI transitions to the waiting room view

  # 多人同步:所有在線玩家在同一廣播後同時切換
  @play-again-ui @websocket @sync @P1
  Scenario: All online players synchronously switch to waiting room view
    # 確保所有在線玩家(含主持人)在收到 ROOM_STATE(waiting) 後同步切換
    Given the host triggers play-again and the broadcast is sent
    When all connected clients receive the ROOM_STATE broadcast with status "waiting"
    Then Alice's view shows the waiting room
    And Bob's view shows the waiting room
    And Carol's view shows the waiting room
    And no player remains on the finished result page

  # ---------------------------------------------------------------------------
  # 5. 載入狀態:點擊後按鈕 disabled 防止重複送出
  # 來源:PDD §5.4 behaviour、PDD §8.4 Loading States
  # ---------------------------------------------------------------------------

  # 點擊後按鈕立即 disabled,防止重複請求
  @play-again-ui @loading @P0
  Scenario: "Play Again" button becomes disabled immediately after click to prevent duplicate submission
    # 點擊後立即設為 disabled(opacity: 0.4; cursor: not-allowed),不允許第二次點擊
    Given Alice is viewing the finished result page
    And the "Play Again" button is enabled
    When Alice clicks the "Play Again" button
    Then the "Play Again" button becomes disabled immediately
    And the button has attribute "disabled" or CSS class that prevents pointer events
    And no duplicate POST requests are sent if Alice clicks again while the request is pending

  # 請求進行中按鈕顯示載入中文字或樣式
  @play-again-ui @loading @in-progress @P1
  Scenario: "Play Again" button shows loading state while the request is in flight
    # 可選:按鈕文字改為「載入中…」或加上 spinner,提示使用者請求進行中
    Given Alice has clicked the "Play Again" button
    And the POST request to play-again is in progress
    Then the "Play Again" button is in a loading/pending state
    # (按鈕應顯示禁用樣式;文字可為「再玩一場」或「載入中…」依實作決定)

  # 請求完成(成功或失敗)後按鈕恢復可互動
  @play-again-ui @loading @recovery @P1
  Scenario: "Play Again" button returns to enabled state if the request fails
    # 請求失敗後按鈕應恢復啟用,讓房主可以重試
    Given Alice has clicked the "Play Again" button
    And the "Play Again" button is in disabled/loading state
    When the server responds with HTTP 403 "PLAYER_NOT_HOST"
    Then the "Play Again" button returns to enabled state
    And Alice can click the button again

  # ---------------------------------------------------------------------------
  # 6. 錯誤處理:API 回傳 400/403 → 顯示 Toast 錯誤訊息
  # 來源:API §2.10 Error Responses、PDD §4.5 Toast、PDD §8.3 錯誤 UI 層次
  # ---------------------------------------------------------------------------

  # HTTP 403 PLAYER_NOT_HOST → 顯示 Toast 錯誤,UI 不切換
  @play-again-ui @error @P0
  Scenario: API returns 403 PLAYER_NOT_HOST and the client shows an error toast
    # 若操作者 token 不是房主(理論上不應發生,但仍需防禦),顯示 error toast
    Given Alice's token unexpectedly returns PLAYER_NOT_HOST
    When Alice clicks the "Play Again" button and the server returns HTTP 403
    Then an error Toast is displayed with message containing "操作失敗" or "不是房主"
    And the Toast has class "toast-error"
    And the UI remains on the finished result page
    And the "Play Again" button returns to enabled state

  # HTTP 400 INSUFFICIENT_ONLINE_PLAYERS → 顯示 Toast 錯誤,UI 不切換
  @play-again-ui @error @P0
  Scenario: API returns 400 INSUFFICIENT_ONLINE_PLAYERS and the client shows an error toast
    # 在線玩家 < 2 時點擊「再玩一場」,server 回 400,前端顯示 toast 提示
    Given only Alice is online in room "ROOM01" at the moment
    When Alice clicks the "Play Again" button and the server returns HTTP 400 "INSUFFICIENT_ONLINE_PLAYERS"
    Then an error Toast is displayed with message containing "在線玩家不足" or "至少需要 2 位"
    And the Toast has class "toast-error"
    And the UI remains on the finished result page
    And the "Play Again" button returns to enabled state

  # HTTP 409 INVALID_STATE → 顯示 Toast 錯誤,UI 不切換(防禦性)
  @play-again-ui @error @P1
  Scenario: API returns 409 INVALID_STATE and the client shows an error toast
    # 若房間狀態已因競態條件改變,server 回 409,前端顯示 toast 提示
    Given room "ROOM01" status has changed unexpectedly before the request arrives
    When Alice clicks the "Play Again" button and the server returns HTTP 409 "INVALID_STATE"
    Then an error Toast is displayed with message containing "狀態錯誤" or "請重新整理"
    And the Toast has class "toast-error"
    And the UI remains on the current page
    And the "Play Again" button returns to enabled state

  # Toast 自動消失(3 秒後)且不堆疊
  @play-again-ui @error @toast-behavior @P1
  Scenario: Error toast from play-again failure auto-dismisses after 3 seconds
    # play-again 錯誤 toast 遵守全域 Toast 行為規範(PDD §4.5)
    Given an error toast was shown due to a play-again API failure
    When 3 seconds have elapsed
    Then the error Toast automatically fades out and is removed from the DOM

  # ---------------------------------------------------------------------------
  # 7. 手機版(< 768px)佈局:「再玩一場」按鈕位於底部固定控制欄
  # 來源:PDD §5.4 手機版 Result View layout
  # ---------------------------------------------------------------------------

  @play-again-ui @responsive @mobile @P1
  Scenario: On mobile viewport "Play Again" button appears in the bottom control bar
    # 手機版(< 768px)finished 頁面:Canvas 上方、得獎名單下方、底部顯示「再玩一局」
    Given the viewport width is 375px (mobile)
    And the game has finished
    When Alice (the host) views the finished result page on mobile
    Then the "Play Again" button is rendered in the bottom control bar area
    And the button is fully visible without requiring horizontal scroll

  # ---------------------------------------------------------------------------
  # 8. 整合流程:finished → play-again → waiting → 下一局可開始
  # 完整 UI 狀態機轉換驗證
  # ---------------------------------------------------------------------------

  @play-again-ui @integration @full-flow @P1
  Scenario: Full UI flow from finished result page through play-again back to waiting room
    # 完整流程:結果頁 → 點擊「再玩一場」→ 等待室 → 主持人可設定並開始新局
    Given the game has finished with 3 online players
    And all players are on the finished result page

    # Step 1: 按鈕點擊 → 載入狀態
    When Alice clicks the "Play Again" button
    Then the "Play Again" button becomes disabled
    And the client sends POST to "/api/rooms/ROOM01/game/play-again"

    # Step 2: 成功回應 → UI 切換
    When the server responds with HTTP 200 and ROOM_STATE(status="waiting")
    Then Alice's UI transitions to the waiting room view
    And Bob's UI transitions to the waiting room view
    And Carol's UI transitions to the waiting room view

    # Step 3: 等待室功能可正常使用
    And Alice sees the room code "ROOM01"
    And Alice sees "Bob" and "Carol" in the player list
    And Alice can set winnerCount and click "Start Game" to begin a new round

client/result-view.feature

Feature: Result View UI
  As a player or host, I want to see the final results clearly so I can confirm the winners and decide next steps

  Background:
    Given the game state is "finished"
    And I am viewing the result view

  # --- 全部路徑揭曉後顯示完整結果 ---

  Scenario: Show all paths after reveal-all
    Given all paths were revealed via REVEAL_ALL
    When I view the canvas on the result page
    Then all player paths should be visible on the canvas
    And each path should render with globalAlpha of 0.6
    And winner paths should have gold glow (shadowColor: #ffd700, shadowBlur: 10)

  Scenario: Full-screen result layout shows canvas and winner list
    When I view the result page
    Then I should see the canvas on the left (or top on mobile)
    And I should see the winner list sidebar on the right
    And the header should show the room name with "FINISHED" status badge

  Scenario: Winner list shows ranked results with crowns
    Given the game finished with winners "Alice" and "Bob", non-winner "Carol"
    When I view the winner list
    Then I should see "Alice" ranked 1st with ♛ crown in gold
    And I should see "Bob" ranked 2nd with ♛ crown in gold
    And I should see "Carol" ranked 3rd without a crown in dim text

  Scenario: Winner entries have gold background styling
    Given "Alice" is a winner
    When I view "Alice"'s result entry
    Then the entry background should be #1c1600
    And the entry border should be #b8960a

  Scenario: Non-winner entries have neutral styling
    Given "Carol" is not a winner
    When I view "Carol"'s result entry
    Then the entry background should be var(--bg)
    And the text should use var(--text-dim)

  Scenario: My own result entry is highlighted
    Given I am player "Bob" and I am a winner
    When I view the result list
    Then my entry should have border-left: 3px solid #6c63ff
    And I should see "(你)" label next to my name in color #6c63ff

  Scenario: Result list is scrollable for large player count
    Given 20 players participated in the game
    When I view the result list
    Then the result list should be scrollable
    And all players should be listed with correct rank order

  Scenario: Gold glow continuously displayed on winner paths in canvas
    Given the game is finished
    When I view the canvas
    Then winner paths should continuously show shadowColor #ffd700 and shadowBlur 10
    And winner endpoints should show gold glow circles

  Scenario: Winner star markers displayed at bottom of winner paths
    Given "Alice" is a winner
    When I view the canvas
    Then a gold ★ should be displayed at the bottom of "Alice"'s rail
    And the star should be rendered at 11px system-ui font

  Scenario: Mobile result view stacks canvas above result list
    Given my viewport is narrower than 768px
    When I view the result page
    Then the canvas should appear at the top with max-height of 50vh
    And the result list should appear below the canvas with max-height of 200px and horizontal scroll

  # --- 再玩一局 ---

  Scenario: Host sees replay button in finished state
    Given I am the host
    When I view the result page
    Then I should see the "再玩一局" button with ghost styling

  Scenario: Replay game resets to waiting room
    Given I am the host
    When I click "再玩一局"
    Then the room should reset to "waiting" state
    And I should be redirected to the waiting room
    And offline players should be removed from the player list

  Scenario: Replay game clears kicked player list
    Given player "Dave" was kicked in the previous round
    When I click "再玩一局" as the host
    Then "Dave" should no longer be in the kicked players list
    And "Dave" can rejoin the room with a fresh nickname

  Scenario: Replay with insufficient online players shows error
    Given only 1 player is currently online
    When I click "再玩一局" as the host
    Then I should see an error toast "在線玩家不足(至少需要 2 位),無法開始新局"
    And the room state should remain "finished"

  Scenario: Non-host does not see replay button
    Given I am not the host
    When I view the result page
    Then I should not see the "再玩一局" button

  Scenario: Winners count resets if it exceeds new player count after replay
    Given winners count was 3 and only 2 players are online after replay
    When the host initiates replay
    Then the winners count should be reset to null
    And I should see a toast "中獎名額已重設,請重新設定"

  # --- 回大廳 ---

  Scenario: Return to lobby navigates to home page
    When I click "回首頁" or navigate to the lobby
    Then I should be on the lobby page
    And the lobby page should show the create-room and join-room forms

  Scenario: Kicked player sees return-to-lobby button
    Given I have been kicked by the host
    When I receive the kick notification
    Then I should see the message "你已被主持人移出房間"
    And I should see a "回首頁" button
    And clicking "回首頁" should navigate me to the lobby

  # --- 結果項目動畫 ---

  Scenario: Result items animate in with fadeSlideIn
    When the result list renders for the first time
    Then each result item should animate with opacity 0 to 1 and translateY 8px to 0
    And animation duration should be 300ms ease
    And each item should be delayed by an additional 50ms per rank position

client/waiting-room.feature

Feature: Waiting Room UI
  As a player or host, I want to see the waiting room so I can track who has joined and start the game

  Background:
    Given I am in the waiting room of room "K7NP3Q"

  # --- 顯示房間碼供分享 ---

  Scenario: Display room code for sharing
    Then I should see the room code "K7NP3Q" displayed prominently with accent color
    And the room code should have letter-spacing of 0.25em

  Scenario: Copy invite link via clipboard API
    When I click the room code box or "複製邀請連結" button
    Then the invite URL "{origin}/?room=K7NP3Q" should be copied to clipboard
    And I should see a toast "已複製!"
    And the button should revert to its original text after 1.5 seconds

  Scenario: Fallback input shown when clipboard API is unavailable
    Given the Clipboard API is not available
    When I click "複製邀請連結"
    Then I should see a text input pre-filled with "{origin}/?room=K7NP3Q"
    And the input text should be fully selected for manual copy

  # --- 顯示已加入的所有玩家 ---

  Scenario: Show all joined players
    Given players "Alice", "Bob", and "Carol" have joined the room
    Then I should see 3 player entries in the player list
    And each player should show their colored player dot
    And "Alice" the host should have a host badge

  Scenario: Offline player shows offline label
    Given player "Carol" is disconnected
    Then I should see "Carol" in the player list with an "離線" label
    And the player dot for "Carol" should have opacity 0.35

  Scenario: Host sees kick button on hover for other players
    Given I am the host
    And player "Bob" is in the player list
    When I hover over "Bob"'s player entry
    Then I should see a red kick button for "Bob"

  Scenario: Host cannot see kick button for themselves
    Given I am the host
    When I view my own player entry
    Then I should not see a kick button next to my name

  Scenario: Non-host does not see kick buttons
    Given I am not the host
    When I view the player list
    Then I should not see any kick buttons

  # --- 主持人開始遊戲(2+ 玩家就緒)---

  Scenario: Host can start game when 2 or more players are present
    Given I am the host
    And 2 players are in the room
    And the winners count is set to 1
    When I click "開始遊戲"
    Then the game should start and transition to the game view

  Scenario: Start game button disabled when fewer than 2 players
    Given I am the host
    And only 1 player is in the room
    Then the "開始遊戲" button should be disabled

  Scenario: Start game button disabled when winners count is not set
    Given I am the host
    And 3 players are in the room
    And the winners count input is empty
    Then the "開始遊戲" button should be disabled

  Scenario: Host sets winners count
    Given I am the host
    And 3 players are in the room
    When I enter 1 in the winners count input
    Then the "開始遊戲" button should become enabled

  Scenario: Winners count validation rejects out-of-range values
    Given I am the host
    And 3 players are in the room
    When I enter 3 in the winners count input
    And I click "開始遊戲"
    Then I should see an error "中獎名額須介於 1 到玩家數減 1 之間"

  # --- 非主持人無法開始遊戲 ---

  Scenario: Non-host cannot start game - button not visible
    Given I am not the host
    Then I should not see the "開始遊戲" button
    And I should see the message "等待主持人開始…"

  Scenario: Non-host does not see game settings panel
    Given I am not the host
    Then I should not see the winners count input
    And I should not see the game settings card

  # --- 玩家人數即時更新 ---

  Scenario: Player count updates in real-time when new player joins
    Given I am in the waiting room with 2 players
    When a new player "Dave" joins the room
    Then the player list should update within 2 seconds to show 3 players
    And "Dave" should appear in the player list

  Scenario: Player count updates in real-time when player disconnects
    Given I am in the waiting room with 3 players
    When player "Carol" disconnects
    Then "Carol" should show the "離線" label within 2 seconds
    And the player count should reflect the updated online status

  Scenario: Connection status dot reflects WebSocket state
    Given my WebSocket connection is active
    Then the connection dot should be green
    When my WebSocket connection drops
    Then the connection dot should be red
    When the WebSocket is reconnecting
    Then the connection dot should be gold with a pulse animation

client-canvas.feature

# features/client-canvas.feature
Feature: 梯子 Canvas 渲染行為
  As a 玩家
  I want to 在各個遊戲階段看到符合狀態的 Canvas 畫面
  So that 我能清楚掌握遊戲進行狀況並正確辨識自己的結果

  Background:
    Given 我已加入房間 "EPS6"
    And Canvas 元素已掛載於頁面上

  # ---------------------------------------------------------------------------
  # waiting 狀態 — 佔位顯示
  # ---------------------------------------------------------------------------
  @canvas @waiting @P1
  Scenario: 房間狀態為 waiting 時 Canvas 顯示佔位畫面
    Given 房間狀態為 "waiting"
    And 尚未生成梯子資料
    When Canvas 完成初次渲染
    Then Canvas 顯示灰色佔位軌道,對應每位已加入的玩家
    And Canvas 上方顯示每位玩家的暱稱標籤
    And Canvas 不顯示任何橫槓(rung)或路徑
    And Canvas 底部不顯示結果標籤

  @canvas @waiting @P2
  Scenario: waiting 狀態下新玩家加入時 Canvas 即時新增軌道
    Given 房間狀態為 "waiting",已有 2 名玩家
    When 第 3 名玩家加入房間
    Then Canvas 在不重新載入頁面的情況下新增第 3 條軌道
    And 新軌道對應第 3 名玩家的暱稱與顏色

  # ---------------------------------------------------------------------------
  # running 狀態 — 顯示完整梯子框架
  # ---------------------------------------------------------------------------
  @canvas @running @P0
  Scenario: 房間狀態切換為 running 時 Canvas 渲染完整梯子結構
    Given 房間狀態從 "waiting" 切換為 "running"
    And 伺服器廣播包含 ladderData 的 ROOM_STATE
    When Canvas 接收到新的梯子資料
    Then Canvas 顯示所有垂直軌道,數量等於玩家人數
    And Canvas 顯示所有水平橫槓(rung),位置與伺服器資料一致
    And 每條軌道以對應玩家的顏色繪製
    And 尚未揭曉的路徑不顯示彩色高亮

  @canvas @running @P1
  Scenario: running 狀態下 Canvas 隨視窗大小調整仍維持正確比例
    Given 房間狀態為 "running",梯子已完整渲染
    When 使用者縮放瀏覽器視窗寬度
    Then Canvas 重新計算列間距與行間距
    And 梯子結構保持正確比例,不出現截斷或溢出

  # ---------------------------------------------------------------------------
  # revealing 狀態 — 揭曉動畫
  # ---------------------------------------------------------------------------
  @canvas @revealing @P0
  Scenario: 觸發揭曉時 Canvas 播放路徑動畫
    Given 房間狀態為 "running",梯子已渲染
    When 主持人觸發揭曉,Canvas 接收到 REVEAL_INDEX 事件
    Then Canvas 沿對應玩家的行進路徑播放移動動畫
    And 動畫期間路徑以彩色高亮繪製
    And 動畫完成後路徑保持彩色高亮靜止顯示
    And 動畫時間約為 1.5 秒

  @canvas @revealing @P0
  Scenario: 我的路徑揭曉時顯示金色高亮以示區分
    Given 房間狀態為 "running"
    When 我的路徑(自身玩家)被揭曉
    Then Canvas 以金色(#ffd700)繪製我的路徑
    And 其他玩家的路徑以各自的指定顏色繪製

  @canvas @revealing @P1
  Scenario: 揭曉動畫進行中不允許重疊觸發第二條路徑
    Given 第一條路徑正在播放揭曉動畫
    When 第二個 REVEAL_INDEX 事件到達
    Then 第一條動畫繼續播放至完成
    And 第二條路徑於第一條動畫結束後開始播放

  @canvas @revealing @P1
  Scenario: 重連時房間已揭曉部分路徑,Canvas 直接顯示靜態結果不重播動畫
    Given 房間狀態為 "revealing",已揭曉 3 條路徑
    When 我重新連線並接收 ROOM_STATE_FULL
    Then Canvas 直接以靜態方式繪製已揭曉的 3 條路徑
    And Canvas 不播放任何動畫

  # ---------------------------------------------------------------------------
  # finished 狀態 — 結果顯示
  # ---------------------------------------------------------------------------
  @canvas @finished @P0
  Scenario: 所有路徑揭曉完成後 Canvas 顯示最終結果標籤
    Given 所有玩家的路徑均已揭曉
    When Canvas 完成最後一條路徑的渲染
    Then Canvas 底部每條軌道終點顯示「中獎」或「未中獎」標籤
    And 中獎標籤以金色顯示
    And 未中獎標籤以灰色顯示

  @canvas @finished @P1
  Scenario: finished 狀態下我的結果欄位以明顯樣式標示
    Given 所有路徑均已揭曉,房間狀態為 "finished"
    When 畫面渲染完成
    Then 我的路徑終點標籤比其他玩家更加突出(粗框或額外標記)

client-error.feature

# features/client-error.feature
Feature: 錯誤狀態的 UI 回饋行為
  As a 玩家或主持人
  I want to 在操作失敗時立刻看到清楚的錯誤提示
  So that 我能理解發生了什麼問題並採取正確的後續行動

  # ---------------------------------------------------------------------------
  # 加入房間 — 輸入驗證錯誤
  # ---------------------------------------------------------------------------
  @error @join @P0
  Scenario: 暱稱為空時送出加入請求,顯示 inline 錯誤提示
    Given 我在首頁的加入房間表單
    When 我未填寫暱稱直接點擊「加入房間」
    Then 暱稱欄位旁顯示「請輸入暱稱」錯誤提示
    And 不送出任何 HTTP 請求
    And 焦點移至暱稱輸入欄位

  @error @join @P0
  Scenario: 房間碼為空時送出加入請求,顯示 inline 錯誤提示
    Given 我在首頁的加入房間表單,已輸入暱稱 "Alice"
    When 我未填寫房間碼直接點擊「加入房間」
    Then 房間碼欄位旁顯示「請輸入房間碼」錯誤提示
    And 不送出任何 HTTP 請求

  @error @join @P0
  Scenario: 房間不存在時顯示 Toast 錯誤訊息
    Given 我在首頁輸入暱稱 "Bob" 與房間碼 "XXXX"
    When 我點擊「加入房間」,伺服器回傳錯誤碼 "ROOM_NOT_FOUND"
    Then 頁面顯示 error Toast「找不到此房間」
    And 頁面維持在首頁視圖

  @error @join @P0
  Scenario: 暱稱已被使用時顯示 Toast 錯誤訊息
    Given 房間 "EPS6" 中已有暱稱為 "Carol" 的玩家
    When 我以暱稱 "Carol" 嘗試加入房間 "EPS6"
    And 伺服器回傳錯誤碼 "NICKNAME_TAKEN"
    Then 頁面顯示 error Toast「此暱稱已被使用,請換一個」
    And 頁面維持在首頁視圖

  @error @join @P1
  Scenario: 房間已滿時顯示 Toast 錯誤訊息
    Given 房間 "EPS6" 已達玩家上限
    When 我嘗試加入房間 "EPS6"
    And 伺服器回傳錯誤碼 "ROOM_FULL"
    Then 頁面顯示 error Toast「房間已滿,無法加入」
    And 頁面維持在首頁視圖

  @error @join @P1
  Scenario: 房間狀態非 waiting 時玩家無法加入並看到提示
    Given 房間 "EPS6" 狀態為 "running"
    When 我嘗試加入房間 "EPS6"
    And 伺服器回傳錯誤碼 "ROOM_NOT_JOINABLE"
    Then 頁面顯示 error Toast「遊戲已開始,無法加入」
    And 頁面維持在首頁視圖

  # ---------------------------------------------------------------------------
  # 建立房間 — 輸入驗證錯誤
  # ---------------------------------------------------------------------------
  @error @create @P0
  Scenario: 暱稱為空時建立房間,顯示 inline 錯誤提示
    Given 我在首頁的建立房間表單
    When 我未填寫暱稱直接點擊「建立房間」
    Then 暱稱欄位旁顯示「請輸入暱稱」錯誤提示
    And 不送出任何 HTTP 請求

  # ---------------------------------------------------------------------------
  # 遊戲操作錯誤(主持人)
  # ---------------------------------------------------------------------------
  @error @host @P0
  Scenario: 人數不足時主持人開始遊戲,顯示 Toast 錯誤
    Given 等待大廳只有主持人一人
    When 主持人點擊「開始遊戲」
    And 伺服器回傳錯誤碼 "INSUFFICIENT_PLAYERS"
    Then 頁面顯示 error Toast「人數不足(至少需要 2 位玩家)」
    And 房間視圖維持在等待大廳

  @error @host @P0
  Scenario: 中獎名額未設定時主持人開始遊戲,顯示 Toast 錯誤
    Given 等待大廳有 3 名玩家但中獎名額尚未設定
    When 主持人點擊「開始遊戲」
    And 伺服器回傳錯誤碼 "PRIZES_NOT_SET"
    Then 頁面顯示 error Toast「請先設定中獎名額」
    And 房間視圖維持在等待大廳

  @error @host @P1
  Scenario: 主持人設定非法中獎名額,顯示 Toast 錯誤
    Given 等待大廳有 5 名玩家
    When 主持人設定中獎名額為 0
    And 伺服器回傳錯誤碼 "INVALID_PRIZES_COUNT"
    Then 頁面顯示 error Toast「中獎名額須介於 1 到玩家數減 1 之間」
    And 中獎名額欄位恢復可編輯狀態

  @error @host @P1
  Scenario: 非主持人嘗試操作主持人功能,顯示 Toast 錯誤
    Given 我是普通玩家
    When 我嘗試透過 WebSocket 發送主持人專屬指令
    And 伺服器回傳錯誤碼 "FORBIDDEN"
    Then 頁面顯示 error Toast,說明操作不被允許

  # ---------------------------------------------------------------------------
  # 被踢出房間
  # ---------------------------------------------------------------------------
  @error @kicked @P0
  Scenario: 被主持人踢出房間時顯示提示並返回首頁
    Given 我在房間 "EPS6" 的等待大廳
    When 主持人將我踢出,伺服器發送 PLAYER_KICKED 事件
    Then 頁面顯示 error Toast「你已被踢出房間」
    And 畫面切換至首頁(lobby 視圖)
    And localStorage 中的 token 被清除

  # ---------------------------------------------------------------------------
  # WebSocket 傳輸層錯誤
  # ---------------------------------------------------------------------------
  @error @ws @P1
  Scenario: WebSocket 未連線時嘗試送出訊息,顯示 Toast 錯誤
    Given 我的 WebSocket 連線已中斷
    When 我嘗試觸發任何需要 WebSocket 的操作
    Then 頁面顯示 error Toast「WebSocket not connected」或類似提示
    And 操作不被執行

  @error @ws @P1
  Scenario: 伺服器回傳通用 ERROR 事件時顯示錯誤訊息
    Given 我在遊戲頁面
    When 伺服器發送 ERROR 事件,其 payload 包含 message="Unexpected error occurred"
    Then 頁面顯示 error Toast 顯示 "Unexpected error occurred"

  # ---------------------------------------------------------------------------
  # Toast 行為規格
  # ---------------------------------------------------------------------------
  @error @toast @P1
  Scenario: Toast 錯誤訊息在 3.5 秒後自動消失
    Given 頁面顯示一條 error Toast
    When 3.5 秒後
    Then Toast 自動淡出並從畫面移除

  @error @toast @P2
  Scenario: 新 Toast 出現時取代舊 Toast(不堆疊)
    Given 頁面正在顯示第一條 Toast 訊息
    When 第二條 Toast 觸發
    Then 第一條 Toast 立即被第二條取代
    And 畫面上同時只顯示一條 Toast

client-navigation.feature

# features/client-navigation.feature
Feature: 頁面流程與視圖切換
  As a 玩家或主持人
  I want to 在不同遊戲階段被引導到對應的頁面
  So that 我能看到符合當前狀態的 UI,並依直覺完成每個操作步驟

  # ---------------------------------------------------------------------------
  # 首頁(Lobby)
  # ---------------------------------------------------------------------------
  @navigation @lobby @P0
  Scenario: 首次進入應用程式時顯示首頁
    Given 使用者開啟應用程式首頁
    When 頁面載入完成
    Then 顯示「建立房間」與「加入房間」兩個操作入口
    And 不顯示梯子 Canvas、玩家列表或結果列表

  @navigation @lobby @P0
  Scenario: 主持人填寫暱稱後建立房間,進入等待大廳
    Given 使用者在首頁輸入暱稱 "Alice"
    When 使用者點擊「建立房間」
    And 伺服器回傳房間碼 "EPS6" 與 hostToken
    Then 畫面切換至等待大廳視圖
    And 等待大廳顯示房間碼 "EPS6"
    And 玩家列表中顯示 "Alice"(主持人標記)

  @navigation @lobby @P0
  Scenario: 玩家輸入房間碼與暱稱後加入房間,進入等待大廳
    Given 使用者在首頁輸入暱稱 "Bob" 與房間碼 "EPS6"
    When 使用者點擊「加入房間」
    And 伺服器回傳 playerToken
    Then 畫面切換至等待大廳視圖
    And 等待大廳顯示房間碼 "EPS6"
    And 玩家列表中顯示 "Bob"

  # ---------------------------------------------------------------------------
  # 等待大廳(Waiting Room)
  # ---------------------------------------------------------------------------
  @navigation @waiting @P0
  Scenario: 等待大廳顯示目前已加入的玩家清單
    Given 我已進入房間 "EPS6" 的等待大廳
    When 等待大廳頁面渲染完成
    Then 顯示所有已加入玩家的暱稱與上線狀態
    And 顯示房間碼供玩家分享

  @navigation @waiting @P1
  Scenario: 等待大廳即時更新當其他玩家加入
    Given 我在等待大廳,目前房間有 2 名玩家
    When 第 3 名玩家加入房間,伺服器廣播 ROOM_STATE
    Then 玩家列表即時更新,顯示第 3 名玩家
    And 不需手動重新整理頁面

  @navigation @waiting @P0
  Scenario: 主持人點擊「開始遊戲」後所有人切換至遊戲頁面
    Given 等待大廳有 3 名玩家且中獎名額已設定
    When 主持人點擊「開始遊戲」
    And 伺服器廣播 ROOM_STATE(status="running",含 ladderData)
    Then 主持人畫面切換至遊戲頁面
    And 所有玩家畫面同步切換至遊戲頁面
    And 遊戲頁面顯示完整梯子 Canvas

  # ---------------------------------------------------------------------------
  # 遊戲頁面(Game View)
  # ---------------------------------------------------------------------------
  @navigation @game @P0
  Scenario: 遊戲頁面顯示房間碼、狀態標籤、玩家列表與 Canvas
    Given 房間狀態為 "running"
    When 遊戲頁面渲染完成
    Then 頁面標題區域顯示房間碼
    And 顯示目前狀態標籤(例如「遊戲進行中」)
    And 右側或下方顯示玩家列表
    And 中央顯示梯子 Canvas

  @navigation @game @P1
  Scenario: 主持人在遊戲頁面看到揭曉控制按鈕,玩家不顯示
    Given 房間狀態為 "running"
    When 主持人的遊戲頁面渲染完成
    Then 主持人看到「揭曉下一個」等操作按鈕
    When 普通玩家的遊戲頁面渲染完成
    Then 普通玩家不看到任何揭曉控制按鈕

  # ---------------------------------------------------------------------------
  # 結果頁面 / 揭曉完成
  # ---------------------------------------------------------------------------
  @navigation @results @P0
  Scenario: 所有路徑揭曉完成後頁面顯示最終結果
    Given 房間狀態為 "revealing",所有路徑逐一揭曉中
    When 最後一條路徑揭曉完成
    Then Canvas 顯示所有路徑的最終靜態結果
    And 結果列表顯示每位玩家的中獎或未中獎狀態
    And 我的結果以明顯方式標示

  @navigation @results @P1
  Scenario: 主持人可在結果頁面選擇返回首頁或結束房間
    Given 所有路徑已揭曉完成
    When 主持人查看結果頁面
    Then 顯示「返回首頁」或「結束房間」操作按鈕

  # ---------------------------------------------------------------------------
  # 頁面刷新與重新進入
  # ---------------------------------------------------------------------------
  @navigation @refresh @P1
  Scenario: 玩家刷新頁面後根據 localStorage 中的 token 恢復至正確視圖
    Given 我已加入房間 "EPS6" 且 localStorage 存有 myToken 與 myPlayerId
    When 我刷新瀏覽器頁面
    Then 前端以 localStorage 中的 token 重新建立 WebSocket 連線
    And 根據伺服器回傳的房間狀態自動切換至對應視圖(waiting 或 game)

  @navigation @refresh @P1
  Scenario: localStorage 無 token 時刷新頁面顯示首頁
    Given 瀏覽器 localStorage 中無任何 token 資料
    When 使用者開啟或刷新頁面
    Then 直接顯示首頁視圖
    And 不顯示任何載入中或錯誤訊息

client-reconnect.feature

# features/client-reconnect.feature
Feature: 前端斷線重連 UI 行為
  As a 玩家
  I want to 在網路中斷時看到明確提示,並在重連後恢復遊戲畫面
  So that 我不會因短暫網路波動而感到困惑或失去遊戲進度

  Background:
    Given 我已加入房間 "EPS6" 並處於遊戲頁面

  # ---------------------------------------------------------------------------
  # 斷線時的 UI 提示
  # ---------------------------------------------------------------------------
  @reconnect @offline @P0
  Scenario: WebSocket 斷線時顯示連線中斷 overlay
    Given 我的 WebSocket 連線正常
    When WebSocket 連線意外中斷
    Then 頁面顯示連線中斷提示(例如灰色 overlay 或 Toast 訊息)
    And 提示文字說明「連線中斷,嘗試重連中…」
    And 連線狀態指示點變為灰色或紅色

  @reconnect @offline @P1
  Scenario: 斷線期間遊戲畫面保持顯示,不清空 Canvas
    Given 我在房間 "EPS6" 的遊戲頁面,梯子已渲染
    When WebSocket 連線中斷
    Then Canvas 繼續顯示已渲染的梯子畫面
    And 不切換至其他頁面或顯示空白畫面

  @reconnect @offline @P1
  Scenario: 斷線時其他玩家的上線狀態指示器更新為離線
    Given 玩家 "Bob" 在玩家列表中顯示為上線
    When 伺服器廣播 ROOM_STATE,其中 "Bob" 的 isOnline 為 false
    Then 玩家列表中 "Bob" 的上線指示器變為灰色或顯示離線標記

  # ---------------------------------------------------------------------------
  # 自動重連進度提示
  # ---------------------------------------------------------------------------
  @reconnect @retry @P0
  Scenario: 前端自動嘗試重連並顯示倒數提示
    Given WebSocket 連線已中斷
    When 前端開始自動重連流程(第 1 次嘗試)
    Then 頁面提示「正在嘗試重新連線(第 1 / 3 次)…」或類似文字
    And 使用者無需手動操作

  @reconnect @retry @P1
  Scenario: 達到最大重連次數後提示使用者手動重新整理
    Given WebSocket 連線已中斷且自動重連已嘗試 3 次
    When 第 3 次重連仍然失敗
    Then 頁面提示「連線失敗,請重新整理頁面」
    And 顯示「重新整理」按鈕或連結

  # ---------------------------------------------------------------------------
  # 重連成功後的 UI 恢復
  # ---------------------------------------------------------------------------
  @reconnect @restore @P0
  Scenario: 重連成功後 overlay 消失,UI 恢復正常
    Given 連線中斷 overlay 正在顯示
    When WebSocket 重新連線成功
    Then 連線中斷 overlay 自動消失
    And 連線狀態指示點變回綠色
    And 遊戲頁面恢復正常操作狀態

  @reconnect @restore @P0
  Scenario: 重連後接收 ROOM_STATE_FULL 並恢復至正確視圖
    Given 我斷線時房間狀態為 "running"
    When 重連成功,伺服器發送 ROOM_STATE_FULL(status="running")
    Then 頁面維持在遊戲視圖
    And Canvas 重新渲染包含最新狀態的梯子畫面
    And 玩家列表更新為最新的上線狀態

  @reconnect @restore @P1
  Scenario: 重連時房間已進入 revealing 狀態,直接顯示靜態已揭曉結果
    Given 我斷線時房間狀態為 "running"
    When 重連成功,伺服器發送 ROOM_STATE_FULL(status="revealing",revealedCount=3)
    Then Canvas 直接顯示 3 條已揭曉路徑的靜態畫面
    And 不播放已完成路徑的動畫
    And 結果列表顯示 3 筆已揭曉結果

  @reconnect @restore @P1
  Scenario: 重連後我的 isOnline 狀態更新,其他玩家可見
    Given 我已成功重連至房間 "EPS6"
    When 伺服器廣播 ROOM_STATE(我的 isOnline=true)
    Then 其他玩家的畫面中,我的玩家標籤顯示為上線

  # ---------------------------------------------------------------------------
  # Session 被替換時的 UI 提示
  # ---------------------------------------------------------------------------
  @reconnect @session-replaced @P0
  Scenario: 在同一 playerId 從其他裝置登入時,目前頁面顯示 SESSION_REPLACED 提示
    Given 我在裝置 A 上正在進行遊戲
    When 我在裝置 B 以相同帳號登入同一房間
    Then 裝置 A 的頁面顯示 Toast 訊息「你的帳號已在其他地方登入」
    And 裝置 A 的 WebSocket 連線被關閉
    And 裝置 A 的畫面切換回首頁(lobby 視圖)
    And 裝置 A 的 localStorage token 被清除或失效

  @reconnect @session-replaced @P1
  Scenario: SESSION_REPLACED 後使用者可重新輸入暱稱加入房間
    Given 裝置 A 已因 SESSION_REPLACED 被踢回首頁
    When 使用者在裝置 A 重新輸入暱稱並加入房間
    Then 使用者可正常加入或重新連接房間

  # ---------------------------------------------------------------------------
  # 主持人斷線時的 UI 提示
  # ---------------------------------------------------------------------------
  @reconnect @host-offline @P1
  Scenario: 主持人斷線時玩家頁面顯示主持人離線提示
    Given 我是普通玩家,主持人目前上線
    When 伺服器廣播 ROOM_STATE,主持人的 isOnline 為 false
    Then 頁面顯示提示「主持人暫時離線,等待重連中」
    And 揭曉操作按鈕(若存在)變為停用狀態

  @reconnect @host-offline @P1
  Scenario: 主持人重連後提示消失,操作恢復正常
    Given 主持人離線提示正在顯示
    When 伺服器廣播 ROOM_STATE,主持人的 isOnline 恢復為 true
    Then 主持人離線提示自動消失
    And 揭曉操作按鈕恢復啟用狀態

game-flow.feature

# features/game-flow.feature
Feature: 遊戲流程控制
  As a 主持人
  I want to 設定中獎名額並在玩家就緒後開始遊戲
  So that 伺服器生成確定性樓梯結構並讓所有客戶端同步進入揭曉流程

  Background:
    Given 房間碼 "BETA23" 已存在且狀態為 "waiting"
    And 主持人已透過 hostToken 驗證身份

  # ---------------------------------------------------------------------------
  # AC-H02-1 — 設定合法 W,伺服器接受並廣播 ROOM_STATE
  # ---------------------------------------------------------------------------
  @AC-GAME-WIN-001 @P0
  Scenario: 主持人設定合法中獎名額
    Given 房間有 5 名玩家(N=5,含主持人)
    When 主持人設定中獎名額 W=2(1 ≤ W ≤ N-1=4)
    Then 伺服器接受設定
    And 所有在線玩家收到廣播的 ROOM_STATE
    And ROOM_STATE payload 中 winnerCount 為 2

  # ---------------------------------------------------------------------------
  # AC-H02-2 — W 超出範圍 → INVALID_PRIZES_COUNT
  # ---------------------------------------------------------------------------
  @AC-GAME-WIN-002 @P0
  Scenario Outline: 主持人設定非法中獎名額時收到 INVALID_PRIZES_COUNT 錯誤
    Given 房間有 <N> 名玩家(含主持人)
    When 主持人設定中獎名額 W=<W>
    Then 伺服器回傳錯誤碼 "INVALID_PRIZES_COUNT"
    And 錯誤訊息包含「中獎名額須介於 1 到玩家數減 1 之間」
    And 房間 winnerCount 維持不變

    Examples:
      | N  | W  | 說明              |
      | 5  | 0  | W=0,低於下限      |
      | 5  | -1 | W<0,負數          |
      | 5  | 5  | W=N,等於上限違規  |
      | 5  | 6  | W>N,超過玩家數    |

  # ---------------------------------------------------------------------------
  # AC-H03-1 — 成功開始遊戲:狀態轉為 running,廣播樓梯資料
  # ---------------------------------------------------------------------------
  @AC-GAME-START-001 @P0
  Scenario: 主持人滿足條件後成功開始遊戲
    Given 房間有 3 名玩家(N=3)且 winnerCount=1 已設定
    When 主持人點擊「開始遊戲」
    Then HTTP 回應狀態碼為 200
    And 回應包含 LadderData(含 seed、seedSource、rowCount、segments)
    And 伺服器廣播 ROOM_STATE,status 為 "running"
    And 所有客戶端收到完全一致的 ladderMap

  # ---------------------------------------------------------------------------
  # AC-H03-2 — W 未設定 → PRIZES_NOT_SET
  # ---------------------------------------------------------------------------
  @AC-GAME-START-002 @P0
  Scenario: W 尚未設定時主持人開始遊戲被拒絕
    Given 房間有 3 名玩家(N=3)且 winnerCount 尚未設定(為 null)
    When 主持人點擊「開始遊戲」
    Then 伺服器回傳錯誤碼 "PRIZES_NOT_SET"
    And 錯誤訊息包含「請先設定中獎名額」
    And 房間狀態維持 "waiting"

  # ---------------------------------------------------------------------------
  # AC-H03-4 — N < 2 → INSUFFICIENT_PLAYERS
  # ---------------------------------------------------------------------------
  @AC-GAME-START-003 @P0
  Scenario: 房間只有主持人一人時開始遊戲被拒絕
    Given 房間只有主持人一人(N=1)
    When 主持人點擊「開始遊戲」
    Then 伺服器回傳錯誤碼 "INSUFFICIENT_PLAYERS"
    And 前端顯示「人數不足(至少需要 2 位玩家)」
    And 房間狀態維持 "waiting"

  # ---------------------------------------------------------------------------
  # AC-H03-5 — rowCount 邊界值驗證(N=3→20, N=10→30, N=21→60)
  # ---------------------------------------------------------------------------
  @AC-GAME-START-004 @P0
  Scenario Outline: 遊戲開始後 rowCount 符合 clamp(N*3, 20, 60) 公式
    Given 房間有 <N> 名玩家且 winnerCount 已合法設定
    When 主持人成功開始遊戲
    Then 伺服器廣播的 ROOM_STATE 中 ladder.rowCount 為 <expected_rowCount>

    Examples:
      | N  | expected_rowCount | 說明                     |
      | 2  | 20                | clamp(6, 20, 60)=20      |
      | 3  | 20                | clamp(9, 20, 60)=20      |
      | 10 | 30                | clamp(30, 20, 60)=30     |
      | 20 | 60                | clamp(60, 20, 60)=60     |
      | 21 | 60                | clamp(63, 20, 60)=60     |
      | 50 | 60                | clamp(150, 20, 60)=60    |

game-lifecycle.feature

Feature: Game Lifecycle
  As a host
  I want to manage the full game lifecycle from start to finish
  So that all players can participate in a fair and synchronized lottery

  Background:
    Given a room with code "GAME01" exists and status is "waiting"
    And the host is authenticated with a valid hostToken

  # ---------------------------------------------------------------------------
  # AC-H03-1 — Start game with valid state transitions to running
  # ---------------------------------------------------------------------------
  Scenario: Start game with 2 or more players
    # AC-H03-1
    Given room has 3 players and winnerCount is set to 1
    When host sends { type: "START_GAME" } via WebSocket
    Then all players receive ROOM_STATE with status "running"
    And the broadcast contains rowCount equal to clamp(N*3, 20, 60)
    And seed and ladderData are NOT included in the broadcast

  # ---------------------------------------------------------------------------
  # AC-H03-1 — Ladder data is generated at BEGIN_REVEAL, not START_GAME
  # ---------------------------------------------------------------------------
  Scenario: Ladder data is generated when host triggers BEGIN_REVEAL
    # AC-H03-1, FR-04-1
    Given room status is "running" with 3 players and winnerCount 1
    When host sends { type: "BEGIN_REVEAL" } via WebSocket
    Then room status transitions to "revealing"
    And all players receive ROOM_STATE_FULL with ladderData (without seed)
    And all players see identical ladder structure and bar positions

  # ---------------------------------------------------------------------------
  # AC-H03-2 — Start game fails when winnerCount is not set
  # ---------------------------------------------------------------------------
  Scenario: Start game rejected when winnerCount is not set
    # AC-H03-2
    Given room has 3 players and winnerCount is null
    When host sends { type: "START_GAME" } via WebSocket
    Then host receives ERROR event with code "PRIZES_NOT_SET"
    And room status remains "waiting"

  # ---------------------------------------------------------------------------
  # AC-H03-4 — Start game rejected when player count is insufficient
  # ---------------------------------------------------------------------------
  Scenario: Start game rejected when only host is in the room
    # AC-H03-4
    Given room has only the host (N=1) and winnerCount is set to 1
    When host sends { type: "START_GAME" } via WebSocket
    Then host receives ERROR event with code "INSUFFICIENT_PLAYERS"
    And the message indicates at least 2 players are required
    And room status remains "waiting"

  # ---------------------------------------------------------------------------
  # AC-H03-5 — rowCount formula clamp(N*3, 20, 60) boundary values
  # ---------------------------------------------------------------------------
  Scenario Outline: rowCount follows clamp(N*3, 20, 60) formula
    # AC-H03-5
    Given room has <N> players and winnerCount is validly set
    When host starts the game successfully
    Then broadcast rowCount equals <expected_rowCount>

    Examples:
      | N  | expected_rowCount | note                    |
      | 2  | 20                | clamp(6, 20, 60)=20     |
      | 3  | 20                | clamp(9, 20, 60)=20     |
      | 10 | 30                | clamp(30, 20, 60)=30    |
      | 21 | 60                | clamp(63, 20, 60)=60    |

  # ---------------------------------------------------------------------------
  # AC-H04-1 — BEGIN_REVEAL transitions state to revealing
  # ---------------------------------------------------------------------------
  Scenario: Begin reveal transitions room from running to revealing
    # AC-H04-1
    Given room status is "running" with 3 players and winnerCount 1
    When host sends { type: "BEGIN_REVEAL" } via WebSocket
    Then room status transitions to "revealing"
    And all players receive ROOM_STATE broadcast with status "revealing"
    And the operation completes within 1 second

  # ---------------------------------------------------------------------------
  # AC-H04-2 — Reveal one player path manually
  # ---------------------------------------------------------------------------
  Scenario: Reveal one player's path manually
    # AC-H04-2
    Given room status is "revealing" with 3 players and revealedCount is 0
    When host sends { type: "REVEAL_NEXT" } via WebSocket
    Then all players receive REVEAL_INDEX event
    And REVEAL_INDEX payload contains playerIndex, result, and revealedCount=1
    And all clients render the player's path animation synchronously

  # ---------------------------------------------------------------------------
  # AC-H06-1, AC-H06-2 — Reveal all paths simultaneously
  # ---------------------------------------------------------------------------
  Scenario: Reveal all remaining paths simultaneously with REVEAL_ALL_TRIGGER
    # AC-H06-1, AC-H06-2
    Given room status is "revealing" with 4 players and revealedCount is 1
    When host sends { type: "REVEAL_ALL_TRIGGER" } via WebSocket
    Then all players receive REVEAL_ALL event
    And REVEAL_ALL payload contains all remaining 3 unrevealed results
    And all clients render the animations within 2 seconds
    And host then sends END_GAME to finalize the game
    And room status transitions to "finished"
    And all players receive ROOM_STATE with status "finished" containing complete results and seed

  # ---------------------------------------------------------------------------
  # AC-H04-4 — END_GAME after all paths revealed transitions to finished
  # ---------------------------------------------------------------------------
  Scenario: Game completion with winner announcement after all paths revealed
    # AC-H04-4, US-P04
    Given room status is "revealing" with 3 players and all paths revealed (revealedCount=3)
    When host sends { type: "END_GAME" } via WebSocket
    Then room status transitions to "finished"
    And all players receive ROOM_STATE with status "finished"
    And the broadcast includes complete winner list and seed
    And each player's screen correctly shows their own result (winner or loser)

  # ---------------------------------------------------------------------------
  # AC-H04-4b — END_GAME rejected when paths not fully revealed
  # ---------------------------------------------------------------------------
  Scenario: END_GAME rejected when not all paths are revealed
    # AC-H04-4b
    Given room status is "revealing" with 3 players and revealedCount is 1
    When host sends { type: "END_GAME" } via WebSocket
    Then host receives ERROR event with code "END_GAME_REQUIRES_ALL_REVEALED"
    And room status remains "revealing"

  # ---------------------------------------------------------------------------
  # AC-H08-1 — Play again resets room to waiting after finished
  # ---------------------------------------------------------------------------
  Scenario: Play again resets room to waiting with online players only
    # AC-H08-1
    Given room status is "finished" with 3 players (1 offline)
    When host sends { type: "PLAY_AGAIN" } via WebSocket
    Then offline players are removed from the player list
    And room status resets to "waiting" with the remaining online players
    And all remaining players receive ROOM_STATE with status "waiting"
    And kickedPlayerIds is cleared

  # ---------------------------------------------------------------------------
  # AC-H08-3 — Play again rejected when insufficient online players
  # ---------------------------------------------------------------------------
  Scenario: Play again rejected when fewer than 2 online players remain
    # AC-H08-3
    Given room status is "finished" with only 1 online player remaining
    When host sends { type: "PLAY_AGAIN" } via WebSocket
    Then host receives ERROR event with code "INSUFFICIENT_ONLINE_PLAYERS"
    And room status remains "finished"

  # ---------------------------------------------------------------------------
  # AC-H03-6 — Duplicate START_GAME rejected after game has started
  # ---------------------------------------------------------------------------
  Scenario: Duplicate START_GAME is rejected without resetting state
    # AC-H03-6
    Given room status is already "running"
    When host sends { type: "START_GAME" } again via WebSocket
    Then host receives ERROR event with code "INVALID_STATE"
    And room status remains "running"
    And seed and ladderMap are not regenerated

host-actions.feature

# features/host-actions.feature
Feature: 主持人管理操作
  As a 主持人
  I want to 踢除特定玩家並在一局結束後發起再玩一局
  So that 能移除不當參與者並用同一房間繼續下一輪抽獎

  Background:
    Given 主持人已透過 hostToken 驗證身份

  # ---------------------------------------------------------------------------
  # AC-H07-1 — waiting 狀態踢除玩家 → 廣播 PLAYER_KICKED
  # ---------------------------------------------------------------------------
  @AC-HOST-KICK-001 @P1
  Scenario: 主持人在 waiting 狀態踢除玩家後廣播 PLAYER_KICKED
    Given 房間碼 "DELTA5" 存在且狀態為 "waiting"
    And 房間內有玩家 "Tom"(playerId="player-tom-uuid")
    When 主持人送出 KICK_PLAYER(targetPlayerId="player-tom-uuid")
    Then 伺服器廣播 PLAYER_KICKED 事件給所有連線客戶端
    And PLAYER_KICKED payload 包含 kickedPlayerId="player-tom-uuid"
    And "Tom" 的客戶端收到後跳轉至「你已被主持人移出房間」頁面
    And 玩家列表即時更新,不再顯示 "Tom"

  # ---------------------------------------------------------------------------
  # AC-H07-2 — 被踢玩家嘗試重連 → PLAYER_KICKED 錯誤
  # ---------------------------------------------------------------------------
  @AC-HOST-KICK-002 @P1
  Scenario: 被踢玩家嘗試以相同 playerId 重連時被拒絕
    Given 房間碼 "DELTA5" 中 playerId="player-tom-uuid" 已存在於 kickedPlayerIds
    When "Tom" 嘗試以相同 playerId="player-tom-uuid" 重新建立 WebSocket 連線
    Then WS Upgrade 被伺服器以 close code 4003 拒絕
    And 前端顯示「你已被移出此房間,無法重新加入」

  # ---------------------------------------------------------------------------
  # AC-H07-3 — running 或之後狀態踢除玩家 → INVALID_STATE
  # ---------------------------------------------------------------------------
  @AC-HOST-KICK-003 @P1
  Scenario Outline: 非 waiting 狀態踢除玩家時系統拒絕並回傳 INVALID_STATE
    Given 房間碼 "DELTA5" 存在且狀態為 "<room_status>"
    And 房間內有玩家 "Jerry"(playerId="player-jerry-uuid")
    When 主持人送出 KICK_PLAYER(targetPlayerId="player-jerry-uuid")
    Then 伺服器回傳錯誤碼 "INVALID_STATE"
    And 房間狀態維持 "<room_status>"
    And 玩家 "Jerry" 的連線不受影響

    Examples:
      | room_status |
      | running     |
      | revealing   |
      | finished    |

  # ---------------------------------------------------------------------------
  # AC-HOST-KICK-SELF — 主持人嘗試踢除自己時系統拒絕並回傳 CANNOT_KICK_HOST
  # ---------------------------------------------------------------------------
  @AC-HOST-KICK-SELF @P1
  Scenario: 主持人嘗試踢除自己時收到 CANNOT_KICK_HOST 錯誤
    Given 房間碼 "DELTA5" 存在且狀態為 "waiting"
    And 主持人的 playerId 為 "host-player-uuid"
    When 主持人送出 KICK_PLAYER(targetPlayerId="host-player-uuid",即自己)
    Then 伺服器回傳錯誤碼 "CANNOT_KICK_SELF"
    And 主持人仍在房間玩家列表中
    And 房間狀態維持 "waiting"

  # ---------------------------------------------------------------------------
  # AC-H07-4 — 踢除後 playerId 持久化存入 Redis kickedPlayerIds
  # ---------------------------------------------------------------------------
  @AC-HOST-KICK-004 @P1
  Scenario: 踢除操作完成後被踢玩家的 playerId 存入 Redis kickedPlayerIds
    Given 房間碼 "DELTA5" 存在且狀態為 "waiting"
    And 房間內有玩家 "Sam"(playerId="player-sam-uuid")
    When 主持人成功踢除 "Sam"
    Then Redis 中房間資料的 kickedPlayerIds 包含 "player-sam-uuid"
    And 任何以 playerId="player-sam-uuid" 嘗試重連的請求均回傳 PLAYER_KICKED 錯誤

  # ---------------------------------------------------------------------------
  # AC-H07-5 — RESET_ROOM 後 kickedPlayerIds 清空
  # ---------------------------------------------------------------------------
  @AC-HOST-KICK-005 @P1
  Scenario: 再玩一局後 kickedPlayerIds 被清空
    Given 房間碼 "DELTA5" 狀態為 "finished"
    And kickedPlayerIds 包含 ["player-sam-uuid", "player-tom-uuid"]
    When 主持人送出 RESET_ROOM
    Then 新局初始化完成後 Redis 中 kickedPlayerIds 為空陣列 []
    And 前一局被踢玩家可使用新暱稱加入新局
    When the host resets the game with "RESET_ROOM"
    And the previously kicked player joins with a new nickname "NewPlayer"
    Then the join response status should be 201

  # ---------------------------------------------------------------------------
  # AC-H08-1 — 再玩一局:剔除離線玩家後重置為 waiting
  # ---------------------------------------------------------------------------
  @AC-HOST-RESET-001 @P1
  Scenario: 主持人發起再玩一局後系統剔除離線玩家並重置房間
    Given 房間碼 "DELTA5" 狀態為 "finished"
    And 房間有 5 名玩家,其中 "Offline-User"(isOnline=false)
    When 主持人送出 RESET_ROOM
    Then 系統從玩家列表移除 isOnline=false 的 "Offline-User"
    And 伺服器廣播 ROOM_STATE(status="waiting",含剩餘 4 名在線玩家)

  # ---------------------------------------------------------------------------
  # AC-H08-2 — 再玩一局後 W 越界則重設為 null
  # ---------------------------------------------------------------------------
  @AC-HOST-RESET-002 @P1
  Scenario: 再玩一局後 winnerCount 越界時自動重設為 null
    Given 房間碼 "DELTA5" 狀態為 "finished",winnerCount=4
    And 剔除離線玩家後剩餘在線玩家數量為 4(新 N=4,W=4 >= 新 N)
    When 主持人送出 RESET_ROOM
    Then 伺服器將 winnerCount 設為 null
    And 主持人收到「中獎名額已重設,請重新設定」提示
    And 新局 ROOM_STATE 中 winnerCount 為 null

  # ---------------------------------------------------------------------------
  # AC-H08-3 — 再玩一局時在線玩家數 < 2 → INSUFFICIENT_ONLINE_PLAYERS
  # ---------------------------------------------------------------------------
  @AC-HOST-RESET-003 @P1
  Scenario: 再玩一局時在線玩家不足兩人時系統拒絕
    Given 房間碼 "DELTA5" 狀態為 "finished"
    And 目前僅有 1 名玩家在線(isOnline=true)
    When 主持人送出 RESET_ROOM
    Then 伺服器回傳錯誤碼 "INSUFFICIENT_ONLINE_PLAYERS"
    And 前端顯示「在線玩家不足,無法開始新局」
    And 房間狀態維持 "finished"

  # ---------------------------------------------------------------------------
  # AC-PLAY-AGAIN-001 — PLAY_AGAIN(新事件):finished 狀態下成功重置為 waiting
  # ---------------------------------------------------------------------------
  @AC-PLAY-AGAIN-001 @P1
  Scenario: 主持人在 finished 狀態送出 PLAY_AGAIN 成功重置房間
    Given 房間碼 "DELTA5" 狀態為 "finished"
    And 房間有 5 名玩家,其中 4 名在線(isOnline=true),1 名離線(isOnline=false)
    When 主持人送出 WS 訊息 PLAY_AGAIN
    Then 系統從玩家列表移除所有 isOnline=false 的玩家
    And 伺服器廣播 ROOM_STATE(status="waiting",含剩餘 4 名在線玩家)
    And ladder 資料清空(ladder=null)
    And results 資料清空(results=[])

  # ---------------------------------------------------------------------------
  # AC-PLAY-AGAIN-002 — PLAY_AGAIN 後 kickedPlayerIds 清空,前次被踢玩家可重新加入
  # ---------------------------------------------------------------------------
  @AC-PLAY-AGAIN-002 @P1
  Scenario: PLAY_AGAIN 後 kickedPlayerIds 被清空,前次被踢玩家可以新暱稱加入
    Given 房間碼 "DELTA5" 狀態為 "finished"
    And kickedPlayerIds 包含 ["player-sam-uuid", "player-tom-uuid"]
    When 主持人送出 WS 訊息 PLAY_AGAIN
    Then 新局初始化完成後 Redis 中 kickedPlayerIds 為空陣列 []
    And 前一局被踢玩家可使用新暱稱加入新局

  # ---------------------------------------------------------------------------
  # AC-PLAY-AGAIN-003 — PLAY_AGAIN 後 winnerCount 越界時自動重設為 null
  # ---------------------------------------------------------------------------
  @AC-PLAY-AGAIN-003 @P1
  Scenario: PLAY_AGAIN 後在線玩家減少導致 winnerCount 越界,系統自動重設為 null
    Given 房間碼 "DELTA5" 狀態為 "finished",winnerCount=4
    And 剔除離線玩家後剩餘在線玩家數量為 4(新 N=4,W=4 >= 新 N)
    When 主持人送出 WS 訊息 PLAY_AGAIN
    Then 伺服器將 winnerCount 設為 null
    And 主持人收到「中獎名額已重設,請重新設定」提示
    And 新局 ROOM_STATE 中 winnerCount 為 null

  # ---------------------------------------------------------------------------
  # AC-PLAY-AGAIN-004 — PLAY_AGAIN 在非 finished 狀態被拒絕(錯誤路徑)
  # ---------------------------------------------------------------------------
  @AC-PLAY-AGAIN-004 @P1
  Scenario Outline: 非 finished 狀態送出 PLAY_AGAIN 時收到 INVALID_STATE 錯誤
    Given 房間碼 "DELTA5" 存在且狀態為 "<room_status>"
    When 主持人送出 WS 訊息 PLAY_AGAIN
    Then 伺服器回傳 ERROR 事件,code 為 "INVALID_STATE"
    And 房間狀態維持 "<room_status>"

    Examples:
      | room_status |
      | waiting     |
      | running     |
      | revealing   |

  # ---------------------------------------------------------------------------
  # AC-PLAY-AGAIN-005 — PLAY_AGAIN 在線玩家不足兩人時被拒絕(錯誤路徑)
  # ---------------------------------------------------------------------------
  @AC-PLAY-AGAIN-005 @P1
  Scenario: PLAY_AGAIN 時在線玩家不足兩人系統拒絕並回傳 INSUFFICIENT_ONLINE_PLAYERS
    Given 房間碼 "DELTA5" 狀態為 "finished"
    And 目前僅有主持人 1 名玩家在線(isOnline=true)
    When 主持人送出 WS 訊息 PLAY_AGAIN
    Then 伺服器回傳錯誤碼 "INSUFFICIENT_ONLINE_PLAYERS"
    And 前端顯示「在線玩家不足,無法開始新局」
    And 房間狀態維持 "finished"

  # ---------------------------------------------------------------------------
  # AC-P06-1/2 — 玩家被踢除後收到 PLAYER_KICKED 通知並跳轉首頁
  # ---------------------------------------------------------------------------
  @AC-P06-001 @P1
  Scenario: 玩家被踢除後收到 PLAYER_KICKED unicast 並跳轉首頁
    Given 房間碼 "DELTA5" 存在且狀態為 "waiting"
    And 玩家 "Alice"(playerId="player-alice-uuid")已連線至房間
    When 主持人送出 KICK_PLAYER(targetPlayerId="player-alice-uuid")
    Then 伺服器 unicast PLAYER_KICKED 給 "Alice" 的 WebSocket 連線
    And "Alice" 的客戶端顯示「你已被主持人移出房間」提示
    And "Alice" 的 WebSocket 連線關閉
    And 頁面顯示「回首頁」按鈕

  @AC-P06-002 @P1
  Scenario: 被踢除的玩家嘗試重連時收到 PLAYER_KICKED 錯誤
    Given playerId="player-alice-uuid" 已存在於 kickedPlayerIds
    When "Alice" 嘗試以相同 playerId 重新建立 WebSocket 連線
    Then WS Upgrade 被以 close code 4003 拒絕
    And 前端顯示「你已被移出此房間,無法重新加入」

  # ---------------------------------------------------------------------------
  # NFR-05 — 無效或過期 JWT 嘗試 Host 操作時回傳 401
  # ---------------------------------------------------------------------------
  @AC-AUTH-001 @P0
  Scenario: 使用無效 JWT 執行 Host 操作時收到 401 AUTH_INVALID_TOKEN
    Given 主持人使用格式錯誤或簽名不符的 JWT token
    When 主持人嘗試送出 DELETE /api/rooms/DELTA5/players/some-player-id
    Then HTTP 回應狀態碼為 401
    And 回應錯誤碼為 "AUTH_INVALID_TOKEN"

  @AC-AUTH-002 @P0
  Scenario: 使用過期 JWT 執行 Host 操作時收到 401 AUTH_TOKEN_EXPIRED
    Given 主持人的 JWT token 已超過 6 小時過期
    When 主持人嘗試送出 POST /api/rooms/DELTA5/game/start
    Then HTTP 回應狀態碼為 401
    And 回應錯誤碼為 "AUTH_TOKEN_EXPIRED"

ladder-generation.feature

Feature: Ladder Generation
  As a system
  I want to generate deterministic and valid ladder structures using Mulberry32 PRNG
  So that all clients get identical results and outcomes cannot be predicted before reveal

  # ---------------------------------------------------------------------------
  # FR-04-1, FR-04-2 — Deterministic generation with same seed produces same ladder
  # ---------------------------------------------------------------------------
  Scenario: Deterministic generation with same seed produces identical ladders
    # FR-04-1, AC-H03-1
    Given seedSource "550e8400-e29b-41d4-a716-446655440000" and 5 players
    When ladder is generated twice using the same seedSource and N
    Then both ladders are identical (deep equality on segments, rowCount, colCount)
    And both ladders have the same rowCount
    And both ladders have the same colCount equal to the player count

  # ---------------------------------------------------------------------------
  # FR-04-3 — rowCount clamping to [20, 60]
  # ---------------------------------------------------------------------------
  Scenario Outline: rowCount is clamped to [20, 60] for all player counts
    # AC-H03-5, FR-04-3
    Given seedSource "test-seed-xyz" and <N> players
    When ladder is generated
    Then ladder rowCount equals <expected_rowCount>

    Examples:
      | N  | expected_rowCount | note                    |
      | 2  | 20                | clamp(6, 20, 60)=20     |
      | 3  | 20                | clamp(9, 20, 60)=20     |
      | 7  | 21                | clamp(21, 20, 60)=21    |
      | 10 | 30                | clamp(30, 20, 60)=30    |
      | 20 | 60                | clamp(60, 20, 60)=60    |
      | 21 | 60                | clamp(63, 20, 60)=60    |
      | 50 | 60                | clamp(150, 20, 60)=60   |

  # ---------------------------------------------------------------------------
  # FR-04-2 — Segment validity: no overlap within the same row
  # ---------------------------------------------------------------------------
  Scenario: No segments overlap within the same row
    # FR-04-2
    Given seedSource "550e8400-e29b-41d4-a716-446655440000" and 10 players
    When ladder is generated
    Then for every pair of segments in the same row (col_a, col_b)
    And |col_a - col_b| > 1 (no adjacent overlap)

  Scenario Outline: No overlapping segments for various player counts
    # FR-04-2
    Given seedSource "test-seed-abc123" and <N> players
    When ladder is generated
    Then no row contains overlapping segments

    Examples:
      | N  |
      | 2  |
      | 5  |
      | 20 |
      | 50 |

  # ---------------------------------------------------------------------------
  # FR-04-2 — Bar density: target max(1, round(N/4)) bars per row
  # ---------------------------------------------------------------------------
  Scenario Outline: Bar density target is max(1, round(N/4)) per row
    # FR-04-2
    Given seedSource "deterministic-seed-xyz" and <N> players
    When ladder is generated
    Then the average bars per row approaches <target_density>
    And each bar attempt does not exceed N*10 retries

    Examples:
      | N  | target_density |
      | 2  | 1              |
      | 4  | 1              |
      | 8  | 2              |
      | 12 | 3              |
      | 20 | 5              |

  # ---------------------------------------------------------------------------
  # FR-04-4 — Bijection: N players produce N unique endCol values
  # ---------------------------------------------------------------------------
  Scenario: Result bijection ensures N unique endCol values for N players
    # FR-04-4
    Given seedSource "550e8400-e29b-41d4-a716-446655440000" and 8 players with winnerCount 3
    When ladder is generated and results are computed
    Then resultSlots array length equals 8
    And all endCol values are unique (no duplicates, covering 0 to 7)
    And the number of isWinner=true results equals 3
    And the number of isWinner=false results equals 5

  Scenario Outline: Bijection holds for various N and winnerCount combinations
    # FR-04-4
    Given seedSource "deterministic-seed-xyz" and <N> players with winnerCount <W>
    When ladder is generated and results are computed
    Then resultSlots array length equals <N>
    And all endCol values are unique
    And isWinner=true count equals <W>

    Examples:
      | N  | W  |
      | 2  | 1  |
      | 5  | 2  |
      | 10 | 3  |
      | 50 | 10 |

  # ---------------------------------------------------------------------------
  # FR-04-4 — 1000-seed automated bijection validation
  # ---------------------------------------------------------------------------
  Scenario: 1000 random seeds all satisfy bijection property
    # FR-04-4, NFR-03
    Given 1000 randomly generated seedSource values with N=10 and winnerCount=3
    When ladder and results are computed for each seed
    Then all 1000 results satisfy bijection (all endCol values unique)
    And no result has more or fewer than N entries

  # ---------------------------------------------------------------------------
  # FR-04-6 — Seed not exposed to clients before finished state
  # ---------------------------------------------------------------------------
  Scenario: Seed is not transmitted to clients before room status is finished
    # FR-04-6
    Given a room is in "revealing" status with ladder generated
    When a client receives ROOM_STATE or ROOM_STATE_FULL
    Then the payload does NOT contain the seed field
    And the payload does NOT contain the seedSource field

  Scenario: Seed is transmitted to clients when room status becomes finished
    # FR-04-6, AC-H04-4
    Given a room transitions to "finished" status
    When all clients receive ROOM_STATE broadcast
    Then the payload contains the seed field
    And the payload contains the complete results array

play_again.feature

# features/play_again.feature
#
# BDD Feature: 再玩一場(Play Again After Game Ends)
#
# 對應規格:
#   PRD §3 US-H08 (AC-H08-1 ~ AC-H08-5)
#   PRD §4 FR-12-1 ~ FR-12-3
#   API §2.10 POST /api/rooms/:code/game/play-again
#   API §3.6 WS PLAY_AGAIN
#
# 摘要:
#   遊戲結束(status=finished)後,房主可呼叫「再玩一場」重置房間,
#   所有在線玩家保留在房間並重新進入 waiting 狀態。

Feature: Play Again After Game Ends
  As a host
  I want to trigger "play again" after a game finishes
  So that I can start the next round using the same room without asking players to rejoin

  Background:
    # 前置條件:主持人已取得有效 hostToken,並於 finished 狀態下操作
    Given the host has a valid JWT token with role "host"
    And room "ROOM01" exists with status "finished"

  # ---------------------------------------------------------------------------
  # AC-H08-1:finished 狀態下房主呼叫 play-again → 剔除離線玩家 → 重置為 waiting
  # HTTP 路徑:POST /api/rooms/:code/game/play-again
  # WS 路徑:PLAY_AGAIN 訊息
  # ---------------------------------------------------------------------------

  # 測試 Happy Path:透過 HTTP REST 呼叫再玩一場
  @AC-H08-1 @P1 @http
  Scenario: Host calls play-again via HTTP and room resets to waiting with online players only
    # 房間有 5 名玩家,其中 1 名離線(isOnline=false)
    Given room "ROOM01" has 5 players:
      | nickname  | isOnline |
      | Alice     | true     |
      | Bob       | true     |
      | Carol     | true     |
      | Dave      | true     |
      | Offline1  | false    |
    When the host sends POST /api/rooms/ROOM01/game/play-again with valid token
    Then the HTTP response status is 200
    And the response body contains status "waiting"
    And the response body players list has 4 entries
    And "Offline1" is not in the response players list
    # 廣播 ROOM_STATE 給所有仍在線的玩家
    And a ROOM_STATE broadcast is sent to all connected clients with status "waiting"
    And the ROOM_STATE payload players count is 4

  # 測試 Happy Path:透過 WebSocket 訊息 PLAY_AGAIN 呼叫再玩一場
  @AC-H08-1 @P1 @websocket
  Scenario: Host sends PLAY_AGAIN via WebSocket and room resets to waiting with online players only
    # 房間有 5 名玩家,其中 1 名離線(isOnline=false)
    Given room "ROOM01" has 5 players:
      | nickname  | isOnline |
      | Alice     | true     |
      | Bob       | true     |
      | Carol     | true     |
      | Dave      | true     |
      | Offline1  | false    |
    When the host sends WebSocket message PLAY_AGAIN with payload {}
    Then a ROOM_STATE broadcast is sent to all connected clients
    And the ROOM_STATE payload status is "waiting"
    And the ROOM_STATE payload players list contains exactly 4 entries
    And "Offline1" is not included in the broadcast players list
    # Redis 中確認離線玩家已被剔除
    And the Redis room record for "ROOM01" has 4 players

  # ---------------------------------------------------------------------------
  # AC-H08-1 + FR-12-1:ladder、results 及 revealedCount 在新局中被清空
  # ---------------------------------------------------------------------------
  @AC-H08-1-RESET-DATA @P1
  Scenario: After play-again, game data is cleared and room is in clean waiting state
    # 確認梯子與結果資料在再玩一場後被清空
    Given room "ROOM01" has ladder data and results from the previous game
    And room "ROOM01" has revealedCount = 5
    When the host sends WebSocket message PLAY_AGAIN with payload {}
    Then the ROOM_STATE broadcast contains ladder = null
    And the ROOM_STATE broadcast contains results = null
    And the ROOM_STATE broadcast contains revealedCount = 0
    And the ROOM_STATE broadcast contains rowCount = null

  # ---------------------------------------------------------------------------
  # AC-H08-2 / FR-12-2:再玩一場後 winnerCount 越界時自動重設為 null
  # 當新 N <= 原 winnerCount 時,winnerCount 被重設並通知主持人
  # ---------------------------------------------------------------------------
  @AC-H08-2 @P1
  Scenario: winnerCount is reset to null when it becomes out of bounds after play-again
    # 原 winnerCount=4,剔除離線玩家後剩餘在線玩家 N=4,W >= N 故越界
    Given room "ROOM01" has winnerCount = 4
    And room "ROOM01" has 5 players:
      | nickname  | isOnline |
      | Alice     | true     |
      | Bob       | true     |
      | Carol     | true     |
      | Dave      | true     |
      | Offline1  | false    |
    # 剔除後新 N=4,winnerCount=4 >= N=4,故需重設
    When the host sends WebSocket message PLAY_AGAIN with payload {}
    Then the ROOM_STATE broadcast contains winnerCount = null
    # 主持人應收到需重新設定的提示
    And the host receives an ERROR event with code "WINNER_COUNT_RESET"
    And the error message contains "請重新設定中獎名額"

  # ---------------------------------------------------------------------------
  # AC-H08-3 / FR-12-1:在線玩家 < 2 時拒絕再玩一場
  # HTTP 路徑回傳 400 INSUFFICIENT_ONLINE_PLAYERS
  # WS 路徑回傳 ERROR 事件
  # ---------------------------------------------------------------------------

  # 透過 HTTP 呼叫時的拒絕
  @AC-H08-3 @P1 @http
  Scenario: play-again via HTTP is rejected when online players count is less than 2
    # 房間僅剩主持人 1 名在線
    Given room "ROOM01" has 3 players:
      | nickname  | isOnline |
      | HostUser  | true     |
      | Offline2  | false    |
      | Offline3  | false    |
    When the host sends POST /api/rooms/ROOM01/game/play-again with valid token
    Then the HTTP response status is 400
    And the response error code is "INSUFFICIENT_ONLINE_PLAYERS"
    And the room status remains "finished"

  # 透過 WebSocket 呼叫時的拒絕
  @AC-H08-3 @P1 @websocket
  Scenario: PLAY_AGAIN via WebSocket is rejected when online players count is less than 2
    # 房間僅剩主持人 1 名在線,無法滿足 N >= 2 的條件
    Given room "ROOM01" has 3 players:
      | nickname  | isOnline |
      | HostUser  | true     |
      | Offline2  | false    |
      | Offline3  | false    |
    When the host sends WebSocket message PLAY_AGAIN with payload {}
    Then the host receives an ERROR event with code "INSUFFICIENT_ONLINE_PLAYERS"
    And the error message contains "在線玩家不足(至少需要 2 位),無法開始新局"
    And no ROOM_STATE broadcast is sent
    And the room status remains "finished"

  # ---------------------------------------------------------------------------
  # AC-H08-5 / FR-12-2:再玩一場後 kickedPlayerIds 被清空
  # 前一局被踢者可在新局以全新 playerId 加入
  # ---------------------------------------------------------------------------
  @AC-H08-5 @P1
  Scenario: kickedPlayerIds is cleared after play-again and previously kicked player can rejoin
    # kickedPlayerIds 在再玩一場後必須清空
    Given room "ROOM01" has kickedPlayerIds = ["kicked-player-uuid-1", "kicked-player-uuid-2"]
    And room "ROOM01" has 3 players with 2 online
    When the host sends WebSocket message PLAY_AGAIN with payload {}
    Then the ROOM_STATE broadcast contains kickedPlayerIds = []
    And the Redis room record for "ROOM01" has kickedPlayerIds = []
    # 前次被踢的玩家可以用新暱稱重新加入
    And a new player with nickname "PreviouslyKicked" can join room "ROOM01"

  # ---------------------------------------------------------------------------
  # 非房主呼叫 play-again → 403 Forbidden(HTTP 與 WS 均適用)
  # API §2.10 PLAYER_NOT_HOST / WS §3.6 驗證:host only
  # ---------------------------------------------------------------------------
  @AC-PLAY-AGAIN-NON-HOST @P1 @http
  Scenario: Non-host player calling play-again via HTTP receives 403 Forbidden
    # 非房主使用一般玩家 token 呼叫時應收到 403
    Given room "ROOM01" exists with status "finished"
    And a player "Bob" has a valid JWT token with role "player"
    When "Bob" sends POST /api/rooms/ROOM01/game/play-again with player token
    Then the HTTP response status is 403
    And the response error code is "PLAYER_NOT_HOST"
    And the room status remains "finished"

  @AC-PLAY-AGAIN-NON-HOST @P1 @websocket
  Scenario: Non-host player sending PLAY_AGAIN via WebSocket receives PLAYER_NOT_HOST error
    # 非房主透過 WS 發送 PLAY_AGAIN 時應收到錯誤
    Given room "ROOM01" exists with status "finished"
    And a player "Bob" is connected via WebSocket with role "player"
    When "Bob" sends WebSocket message PLAY_AGAIN with payload {}
    Then "Bob" receives an ERROR event with code "PLAYER_NOT_HOST"
    And no ROOM_STATE broadcast is sent
    And the room status remains "finished"

  # ---------------------------------------------------------------------------
  # AC-H08-5:房間不在 finished 狀態下呼叫 → 400 / INVALID_STATE
  # API §2.10 INVALID_STATE / WS §3.6 驗證:status 必須為 finished
  # ---------------------------------------------------------------------------
  @AC-H08-5 @P1 @http
  Scenario Outline: play-again via HTTP is rejected with INVALID_STATE when room is not finished
    # 只有 finished 狀態允許再玩一場,其他狀態應被拒絕
    Given room "ROOM01" exists with status "<room_status>"
    When the host sends POST /api/rooms/ROOM01/game/play-again with valid token
    Then the HTTP response status is 409
    And the response error code is "INVALID_STATE"
    And the room status remains "<room_status>"

    Examples:
      | room_status |
      | waiting     |
      | running     |
      | revealing   |

  @AC-H08-5 @P1 @websocket
  Scenario Outline: PLAY_AGAIN via WebSocket is rejected with INVALID_STATE when room is not finished
    # WS 路徑同樣拒絕非 finished 狀態的再玩一場請求
    Given room "ROOM01" exists with status "<room_status>"
    When the host sends WebSocket message PLAY_AGAIN with payload {}
    Then the host receives an ERROR event with code "INVALID_STATE"
    And no ROOM_STATE broadcast is sent
    And the room status remains "<room_status>"

    Examples:
      | room_status |
      | waiting     |
      | running     |
      | revealing   |

  # ---------------------------------------------------------------------------
  # FR-12-3 / §3.5 ROOM_STATE:廣播包含所有在線玩家(WS broadcast 驗證)
  # 確保再玩一場後 ROOM_STATE 廣播給所有仍在線玩家(含重連玩家)
  # ---------------------------------------------------------------------------
  @AC-WS-BROADCAST @P1 @websocket
  Scenario: ROOM_STATE broadcast after PLAY_AGAIN reaches all online connected clients
    # 確認 ROOM_STATE 廣播給所有在線客戶端,包含重連回來的玩家
    Given room "ROOM01" exists with status "finished"
    And 4 players are connected via WebSocket to room "ROOM01"
    And 1 player "LostConn" is offline (isOnline=false, no active WebSocket)
    When the host sends WebSocket message PLAY_AGAIN with payload {}
    Then every active WebSocket connection in room "ROOM01" receives a ROOM_STATE message
    And the ROOM_STATE payload contains status = "waiting"
    And the ROOM_STATE payload players list does not contain "LostConn"
    # ROOM_STATE_FULL 為每位玩家 unicast 的完整狀態快照,含 selfPlayerId
    And each connected client receives their own ROOM_STATE_FULL with selfPlayerId set correctly

  # ---------------------------------------------------------------------------
  # 完整流程整合:finished → play-again → waiting → start next game
  # 驗證整個「再玩一場」到「下一局開始」的狀態機轉換
  # ---------------------------------------------------------------------------
  @AC-FULL-FLOW @P1 @integration
  Scenario: Full play-again flow from finished state to next game start
    # 完整流程:遊戲結束 → 再玩一場 → 等待 → 設定中獎名額 → 開始下一局
    Given room "ROOM01" exists with status "finished"
    And room "ROOM01" has 4 online players and 1 offline player
    And room "ROOM01" has kickedPlayerIds = ["kicked-uuid"]
    And room "ROOM01" has winnerCount = 2

    # Step 1: 主持人發起再玩一場
    When the host sends WebSocket message PLAY_AGAIN with payload {}
    Then a ROOM_STATE broadcast is sent with status "waiting"
    And the broadcast players count is 4
    And the broadcast kickedPlayerIds is []
    And the broadcast winnerCount is 2
    # winnerCount=2 < newN=4,保留不重設

    # Step 2: 主持人設定中獎名額並開始新一局
    When the host sets winnerCount to 1 for the new round
    And the host sends WebSocket message START_GAME with payload {}
    Then a ROOM_STATE broadcast is sent with status "running"
    And the broadcast rowCount equals clamp(4 * 3, 20, 60) which is 20
    # seed 與梯子資料在 finished 前不對客戶端公開
    And the broadcast does not contain seed or ladder data

player-management.feature

# features/player-management.feature
Feature: 玩家加入與管理
  As a 玩家
  I want to 輸入暱稱並透過 6 碼房間碼加入房間
  So that 能參與抽獎活動並即時看到玩家列表更新

  Background:
    Given 房間碼 "ALPHA2" 已存在且狀態為 "waiting"

  # ---------------------------------------------------------------------------
  # AC-P01-1 — 有效暱稱與正確房間碼,1.5 秒內加入成功
  # ---------------------------------------------------------------------------
  @AC-PLAYER-001 @P0
  Scenario: 玩家以有效暱稱成功加入房間
    Given 玩家輸入有效暱稱 "Alice"(長度 1~20 Unicode 字元)
    When 玩家送出 POST /api/v1/rooms/ALPHA2/players 並 WebSocket 握手成功
    Then HTTP 回應狀態碼為 201
    And 回應包含 playerId(UUID v4 格式)及 sessionToken
    And WebSocket 連線於 1.5 秒內建立完成
    And 主持人及所有在線玩家收到更新後的 ROOM_STATE(玩家列表含 "Alice")

  # ---------------------------------------------------------------------------
  # AC-P01-2 — 暱稱與同房間已有玩家重複 → NICKNAME_TAKEN
  # ---------------------------------------------------------------------------
  @AC-PLAYER-002 @P0
  Scenario: 玩家輸入重複暱稱時收到 NICKNAME_TAKEN 錯誤
    Given 房間內已有暱稱為 "Bob" 的玩家
    When 新玩家嘗試以暱稱 "Bob" 加入同一房間
    Then HTTP 回應狀態碼為 409
    And 回應錯誤碼為 "NICKNAME_TAKEN"
    And 錯誤訊息包含「此暱稱已被使用,請換一個」

  # ---------------------------------------------------------------------------
  # AC-P01-3 — 前端暱稱驗證(空白 / 超過 20 字元)
  # ---------------------------------------------------------------------------
  @AC-PLAYER-003 @P0
  Scenario Outline: 前端即時驗證暱稱格式不合規時禁用送出
    Given 玩家在暱稱輸入框填入 "<nickname>"
    When 前端執行即時驗證
    Then 輸入框顯示欄位錯誤訊息
    And 送出按鈕保持 disabled 狀態
    And 不發送任何 HTTP 請求到伺服器

    Examples:
      | nickname                     | 說明              |
      |                              | 空白暱稱          |
      | ABCDEFGHIJKLMNOPQRSTU        | 超過 20 字元(21)|
      | 一二三四五六七八九十一二三四五六七八九十甲 | 超過 20 Unicode 字元 |

  # ---------------------------------------------------------------------------
  # AC-P01-4 — 房間人數達 50 人上限 → ROOM_FULL
  # ---------------------------------------------------------------------------
  @AC-PLAYER-004 @P0
  Scenario: 房間已達 50 人上限時新玩家收到 ROOM_FULL 錯誤
    Given 房間已有 50 名玩家(含主持人)
    When 第 51 名玩家嘗試加入房間
    Then HTTP 回應狀態碼為 409
    And 回應錯誤碼為 "ROOM_FULL"
    And 玩家看到「房間已滿,無法加入」提示

  # ---------------------------------------------------------------------------
  # AC-P02-1 — 新玩家加入後所有在線玩家 2 秒內看到更新的玩家列表
  # ---------------------------------------------------------------------------
  @AC-PLAYER-005 @P0
  Scenario: 新玩家加入後所有在線玩家即時收到玩家列表更新
    Given 房間內已有玩家 "Carol" 和 "Dave" 在線
    When 新玩家 "Eve" 成功加入房間
    Then 所有在線玩家於 2 秒內收到 ROOM_STATE 廣播
    And ROOM_STATE payload 中玩家列表包含 "Carol"、"Dave"、"Eve" 三人

  # ---------------------------------------------------------------------------
  # AC-P02-2 — 玩家離線後其他玩家看到「離線」標記,不立即移除
  # ---------------------------------------------------------------------------
  @AC-PLAYER-006 @P0
  Scenario: 玩家斷線後其他玩家列表顯示離線標記
    Given 房間內有玩家 "Frank" 在線(isOnline=true)
    When 伺服器偵測到 "Frank" 的 WebSocket 連線中斷
    Then 伺服器廣播 ROOM_STATE
    And ROOM_STATE 中 "Frank" 的 isOnline 為 false
    And 其他玩家的等待畫面在 2 秒內更新並顯示 "Frank" 為「離線」
    And "Frank" 的名額與位置保留,不從玩家列表移除

  # ---------------------------------------------------------------------------
  # EDD §12.1 ROOM_NOT_ACCEPTING — 玩家嘗試加入非 waiting 狀態的房間
  # ---------------------------------------------------------------------------
  @AC-PLAYER-007 @P0
  Scenario Outline: 玩家嘗試加入非 waiting 狀態房間時收到 ROOM_NOT_ACCEPTING
    Given 房間碼 "ALPHA2" 存在且狀態為 "<room_status>"
    When 玩家嘗試以暱稱 "NewPlayer" 加入房間
    Then HTTP 回應狀態碼為 409
    And 回應錯誤碼為 "ROOM_NOT_ACCEPTING"
    And 前端顯示「房間已在進行中,無法加入」提示

    Examples:
      | room_status |
      | running     |
      | revealing   |
      | finished    |

prng.feature

# features/prng.feature
Feature: PRNG 確定性與正確性
  As a 系統
  I want to 以 Mulberry32+djb2 確定性算法生成樓梯結構
  So that 相同輸入恆得相同輸出,確保所有客戶端 100% 結果一致且無法舞弊

  # ---------------------------------------------------------------------------
  # 確定性(Determinism)— 相同 seed+N 兩次呼叫完全一致
  # ---------------------------------------------------------------------------
  @AC-PRNG-001 @P0
  Scenario: 相同 seed 和 N 生成完全相同的樓梯結構(快照測試)
    Given seedSource 為固定字串 "550e8400-e29b-41d4-a716-446655440000"
    And N=5(玩家數)
    When 以相同 seedSource 和 N 兩次呼叫 generateLadder
    Then 第一次和第二次呼叫的 LadderData.segments 完全相等(深度比較)
    And 兩次呼叫的 rowCount 相同
    And 兩次呼叫的 colCount 相同

  # ---------------------------------------------------------------------------
  # 無重疊橫槓(Non-overlapping segments)
  # ---------------------------------------------------------------------------
  @AC-PRNG-002 @P0
  Scenario: 同一 row 中不存在重疊的橫槓
    Given seedSource 為 "550e8400-e29b-41d4-a716-446655440000",N=10
    When 呼叫 generateLadder 生成 LadderData
    Then 對於 LadderData.segments 中所有 row 相同的橫槓對(col_a, col_b)
    And 不存在任何兩條橫槓使得 |col_a - col_b| <= 1(即不衝突)

  @AC-PRNG-003 @P0
  Scenario Outline: 多種 N 值下同一 row 均無重疊橫槓
    Given seedSource 為 "test-seed-abc123",N=<N>
    When 呼叫 generateLadder 生成 LadderData
    Then 所有 row 中均不存在重疊橫槓

    Examples:
      | N  |
      | 2  |
      | 5  |
      | 20 |
      | 50 |

  # ---------------------------------------------------------------------------
  # rowCount 精確符合 clamp(N*3, 20, 60)
  # ---------------------------------------------------------------------------
  @AC-PRNG-004 @P0
  Scenario Outline: rowCount 精確符合 clamp(N*3, 20, 60) 公式
    Given seedSource 為任意有效字串,N=<N>
    When 呼叫 generateLadder 生成 LadderData
    Then LadderData.rowCount 恰好等於 <expected_rowCount>

    Examples:
      | N  | expected_rowCount | 說明                  |
      | 2  | 20                | clamp(6,20,60)=20    |
      | 3  | 20                | clamp(9,20,60)=20    |
      | 10 | 30                | clamp(30,20,60)=30   |
      | 20 | 60                | clamp(60,20,60)=60   |
      | 21 | 60                | clamp(63,20,60)=60   |
      | 50 | 60                | clamp(150,20,60)=60  |

  # ---------------------------------------------------------------------------
  # 結果雙射(Bijection)— N 個起點對應 N 個唯一終點
  # ---------------------------------------------------------------------------
  @AC-PRNG-005 @P0
  Scenario: 結果雙射:N 個玩家對應 N 個唯一 endCol 值
    Given seedSource 為 "550e8400-e29b-41d4-a716-446655440000",N=8,winnerCount=3
    When 呼叫 generateLadder 後再呼叫 computeResults 生成 ResultSlot[]
    Then ResultSlot 陣列長度恰好為 8
    And 所有 ResultSlot 的 endCol 值兩兩不相同(無重複,完整覆蓋 0~7)

  @AC-PRNG-006 @P0
  Scenario: 結果雙射:isWinner=true 的數量恰好等於 winnerCount
    Given seedSource 為 "550e8400-e29b-41d4-a716-446655440000",N=8,winnerCount=3
    When 呼叫 generateLadder 後再呼叫 computeResults 生成 ResultSlot[]
    Then ResultSlot 中 isWinner=true 的數量恰好為 3
    And ResultSlot 中 isWinner=false 的數量恰好為 5

  @AC-PRNG-007 @P0
  Scenario Outline: 多種 N 和 winnerCount 組合均滿足雙射特性
    Given seedSource 為 "deterministic-seed-xyz",N=<N>,winnerCount=<W>
    When 呼叫 generateLadder 後再呼叫 computeResults
    Then ResultSlot 陣列長度為 <N>
    And 所有 endCol 值唯一(無重複)
    And isWinner=true 的數量等於 <W>

    Examples:
      | N  | W  |
      | 2  | 1  |
      | 5  | 2  |
      | 10 | 3  |
      | 50 | 10 |

reconnect.feature

# features/reconnect.feature
Feature: 斷線與重連行為
  As a 玩家
  I want to 在斷線後能重新連上房間並恢復完整狀態
  So that 不因網路波動而遺失參與資格,並了解跨裝置登入時的 Session 替換行為

  Background:
    Given 房間碼 "EPS6" 已存在

  # ---------------------------------------------------------------------------
  # AC-P05-1 — 玩家帶 playerId 重連,取得完整房間狀態
  # ---------------------------------------------------------------------------
  @AC-RECONNECT-001 @P1
  Scenario: 玩家斷線後以 localStorage 中的 playerId 重連並取得完整房間狀態
    Given 玩家 "Alice" 的 localStorage 中存有 playerId="player-alice-uuid"
    And 房間碼 "EPS6" 狀態為 "waiting"
    When "Alice" 重新訪問房間連結並以 playerId="player-alice-uuid" 建立 WebSocket 連線
    Then 伺服器驗證 playerId 後發送 ROOM_STATE_FULL 給 "Alice"
    And ROOM_STATE_FULL payload 包含完整房間狀態(status、players、ladder、results、selfPlayerId)
    And 重連完成時間在 3 秒以內

  # ---------------------------------------------------------------------------
  # AC-P05-2 — 重連時房間已在 revealing 狀態,直接呈現靜態結果,不重播動畫
  # ---------------------------------------------------------------------------
  @AC-RECONNECT-002 @P1
  Scenario: 玩家重連時房間已在揭曉進行中,顯示靜態結果不重播動畫
    Given 玩家 "Bob" 的 localStorage 中存有 playerId="player-bob-uuid"
    And 房間碼 "EPS6" 狀態為 "revealing",已揭曉 3 條路徑(revealedCount=3)
    When "Bob" 重新連線至房間
    Then 伺服器發送 ROOM_STATE_FULL,其中包含已揭曉的 3 筆 ResultSlot 資料
    And 前端直接渲染已揭曉路徑的靜態結果,不重播已完成的動畫

  # ---------------------------------------------------------------------------
  # AC-P05-3 — Ghost Player(localStorage 遺失)視為新玩家
  # ---------------------------------------------------------------------------
  @AC-RECONNECT-003 @P1
  Scenario: localStorage 中無 playerId 的 Ghost Player 被視為新玩家處理
    Given 玩家的瀏覽器 localStorage 中不存在任何 playerId
    And 房間碼 "EPS6" 狀態為 "waiting",房間內已有暱稱 "Carol" 的玩家
    When 玩家嘗試以暱稱 "Carol" 加入房間
    Then 伺服器視為新玩家,但因暱稱重複回傳錯誤碼 "NICKNAME_TAKEN"
    And 提示「此暱稱已被使用,請換一個」

  @AC-RECONNECT-004 @P1
  Scenario: Ghost Player 使用全新暱稱成功加入房間
    Given 玩家的瀏覽器 localStorage 中不存在任何 playerId
    And 房間碼 "EPS6" 狀態為 "waiting"
    When 玩家以全新暱稱 "NewComer" 加入房間
    Then 伺服器視為新玩家,HTTP 回應狀態碼為 201
    And 回應包含新的 playerId(UUID v4 格式)

  # ---------------------------------------------------------------------------
  # FR-07-3 — 同一 playerId 從新裝置連線:舊 session 收到 SESSION_REPLACED,新裝置取得 ROOM_STATE_FULL
  # ---------------------------------------------------------------------------
  @AC-RECONNECT-005 @P1
  Scenario: 同一 playerId 在新裝置登入時舊 session 收到 SESSION_REPLACED
    Given 玩家 "Dave"(playerId="player-dave-uuid")已在裝置 A 建立 WebSocket 連線(session-A)
    And 房間碼 "EPS6" 狀態為 "waiting"
    When "Dave" 在裝置 B 以相同 playerId="player-dave-uuid" 建立新的 WebSocket 連線
    Then 伺服器更新 "Dave" 的 isOnline=true 關聯至裝置 B 的連線
    And 裝置 A 的 session-A 收到 SESSION_REPLACED 事件
    And 裝置 A 的 WebSocket 連線被關閉
    And 裝置 B 收到 ROOM_STATE_FULL 事件(含完整房間狀態及 selfPlayerId)

  # ---------------------------------------------------------------------------
  # AC-RECONNECT-006 — 舊 session 在新裝置登入時收到 SESSION_REPLACED 通知
  # ---------------------------------------------------------------------------
  @AC-RECONNECT-006 @P1
  Scenario: 舊 session 在收到 SESSION_REPLACED 後連線關閉
    Given 玩家 "Eve"(playerId="player-eve-uuid")已在裝置 X 建立 WebSocket 連線(session-X)
    And 房間碼 "EPS6" 狀態為 "waiting"
    When "Eve" 在裝置 Y 以相同 playerId="player-eve-uuid" 建立新 WebSocket 連線
    Then 裝置 X 的 session-X 收到 SESSION_REPLACED 事件,其 payload 包含 replacedAt 時間戳
    And 裝置 X 的 WebSocket 連線以 close code 4001 關閉

  # ---------------------------------------------------------------------------
  # AC-RECONNECT-007 — 重連後玩家收到包含完整狀態的 ROOM_STATE_FULL
  # ---------------------------------------------------------------------------
  @AC-RECONNECT-007 @P1
  Scenario: 重連後玩家收到包含完整狀態的 ROOM_STATE_FULL
    Given 玩家 "Frank"(playerId="player-frank-uuid")已成功加入房間 "EPS6"
    And 玩家 "Frank" 的 WebSocket 連線中斷
    When "Frank" 以相同 playerId="player-frank-uuid" 重新建立 WebSocket 連線
    Then 伺服器發送 ROOM_STATE_FULL 給 "Frank"
    And ROOM_STATE_FULL payload 包含完整房間狀態(status、players、ladder、results、selfPlayerId)
    And "Frank" 在 players 清單中的 isOnline 為 true

  # ---------------------------------------------------------------------------
  # AC-RECONNECT-008 — 玩家斷線時 isOnline 變為 false,重連時恢復 true
  # ---------------------------------------------------------------------------
  @AC-RECONNECT-008 @P1
  Scenario: 玩家斷線後 isOnline 變為 false,重連後恢復 true
    Given 玩家 "Grace"(playerId="player-grace-uuid")已連線至房間 "EPS6"
    When "Grace" 的 WebSocket 連線意外中斷
    Then 伺服器廣播 ROOM_STATE,其中 "Grace" 的 isOnline 為 false
    When "Grace" 重新建立 WebSocket 連線
    Then 伺服器廣播 ROOM_STATE,其中 "Grace" 的 isOnline 為 true

  # ---------------------------------------------------------------------------
  # AC-RECONNECT-009 — 主持人斷線後房間繼續,進入 grace period
  # ---------------------------------------------------------------------------
  @AC-RECONNECT-009 @P1
  Scenario: 主持人斷線後房間繼續並等待主持人重連(grace period)
    Given 主持人(playerId="host-uuid")已連線至房間 "EPS6",房間狀態為 "running"
    When 主持人的 WebSocket 連線意外中斷
    Then 伺服器廣播 ROOM_STATE,主持人的 isOnline 為 false
    And 房間狀態維持 "running"(不中止遊戲)
    And 其他玩家收到提示「主持人暫時離線,等待重連中」
    When 主持人在 grace period 內重新建立 WebSocket 連線
    Then 主持人收到 ROOM_STATE_FULL(含完整遊戲狀態)
    And 房間狀態維持 "running",遊戲繼續

  # ---------------------------------------------------------------------------
  # AC-RECONNECT-010 — 斷線重連後玩家資料保持不變
  # ---------------------------------------------------------------------------
  @AC-RECONNECT-010 @P1
  Scenario: 斷線重連後玩家暱稱與 colorIndex 等資料保持不變
    Given 玩家 "Henry"(playerId="player-henry-uuid",nickname="Henry",colorIndex=3)已連線至房間 "EPS6"
    When "Henry" 的 WebSocket 連線中斷後重新連線
    Then ROOM_STATE_FULL 中 "Henry" 的 nickname 仍為 "Henry"
    And "Henry" 的 colorIndex 仍為 3
    And "Henry" 的 playerId 仍為 "player-henry-uuid"

reveal-flow.feature

# features/reveal-flow.feature
Feature: 路徑揭曉流程
  As a 主持人
  I want to 手動逐步、自動定時或一鍵揭曉所有玩家的路徑
  So that 能配合現場節奏製造懸念,或在時間緊迫時快速完成抽獎

  Background:
    Given 房間碼 "GAMMA4" 已存在且狀態為 "revealing"
    And 房間有 4 名玩家(N=4),winnerCount=1
    And 主持人已透過 hostToken 驗證身份
    And 尚有未揭曉的路徑

  # ---------------------------------------------------------------------------
  # AC-H04-1 — 手動模式:REVEAL_NEXT → 廣播 REVEAL_INDEX 給所有客戶端
  # ---------------------------------------------------------------------------
  @AC-REVEAL-001 @P0
  Scenario: 主持人點擊「下一位」後所有客戶端同步播放揭曉動畫
    Given 目前已揭曉 0 條路徑(revealedCount=0)
    When 主持人送出 WS 訊息 REVEAL_NEXT
    Then 伺服器廣播 REVEAL_INDEX 事件給房間內所有連線客戶端
    And REVEAL_INDEX payload 包含 playerIndex、result 及 revealedCount=1

  # ---------------------------------------------------------------------------
  # AC-H04-2 — 所有路徑已揭曉後再點擊「下一位」,系統不回應且按鈕 disabled
  # ---------------------------------------------------------------------------
  @AC-REVEAL-002 @P0
  Scenario: 所有路徑揭曉完畢後主持人再點擊「下一位」系統不回應
    Given 所有 4 條路徑已全數揭曉(revealedCount=4)
    When 主持人送出 WS 訊息 REVEAL_NEXT
    Then 伺服器不廣播任何 REVEAL_INDEX 事件
    And 伺服器回傳 ERROR 事件,code 為 "INVALID_STATE"
    And 前端「下一位」按鈕保持 disabled 狀態

  # ---------------------------------------------------------------------------
  # AC-H05-1 — 自動模式:每隔 T 秒廣播下一個 REVEAL_INDEX
  # ---------------------------------------------------------------------------
  @AC-REVEAL-003 @P1
  Scenario: 主持人設定自動揭曉間隔後系統定時廣播
    Given 目前已揭曉 0 條路徑
    When 主持人送出 SET_REVEAL_MODE(mode="auto",intervalSec=3)
    Then 每隔 3 秒伺服器自動廣播下一個 REVEAL_INDEX
    And 重複直到所有 4 條路徑全部揭曉(revealedCount=4)

  # ---------------------------------------------------------------------------
  # AC-H05-2 — 自動模式進行中切換回手動模式,計時停止
  # ---------------------------------------------------------------------------
  @AC-REVEAL-004 @P1
  Scenario: 自動揭曉進行中切換回手動模式後計時停止
    Given 自動揭曉模式進行中(intervalSec=5),已揭曉 2 條路徑
    When 主持人送出 SET_REVEAL_MODE(mode="manual")
    Then 伺服器停止自動計時,不再自動廣播 REVEAL_INDEX
    And 已揭曉的 2 條路徑結果不受影響
    And 後續揭曉需由主持人手動觸發 REVEAL_NEXT

  # ---------------------------------------------------------------------------
  # AC-H06-1 — 一鍵全揭:REVEAL_ALL_TRIGGER → 廣播 REVEAL_ALL
  # ---------------------------------------------------------------------------
  @AC-REVEAL-005 @P0
  Scenario: 主持人點擊「全部揭曉」後伺服器廣播 REVEAL_ALL
    Given 目前已揭曉 1 條路徑,剩餘 3 條未揭曉
    When 主持人送出 WS 訊息 REVEAL_ALL_TRIGGER
    Then 伺服器廣播 REVEAL_ALL 事件給所有客戶端
    And REVEAL_ALL payload 包含所有剩餘 3 條路徑的完整 ResultSlot 資料
    And 所有客戶端於 2 秒內完成剩餘路徑動畫渲染

  # ---------------------------------------------------------------------------
  # AC-H06-2 — REVEAL_ALL 廣播後狀態自動轉為 finished
  # ---------------------------------------------------------------------------
  @AC-REVEAL-006 @P0
  Scenario: REVEAL_ALL 廣播後房間狀態自動轉為 finished 並顯示完整得獎名單
    Given 主持人已觸發 REVEAL_ALL_TRIGGER
    And 伺服器已廣播 REVEAL_ALL
    When 所有客戶端完成動畫播放
    Then 伺服器廣播 ROOM_STATE,status 為 "finished"
    And 所有客戶端顯示完整得獎名單

  # ---------------------------------------------------------------------------
  # AC-H05 — auto-reveal intervalSec 邊界值驗證(T=1 valid, T=30 valid, T=0 reject, T=31 reject)
  # ---------------------------------------------------------------------------
  @AC-REVEAL-AUTO-BOUNDARY @P1
  Scenario Outline: 自動揭曉間隔 T 邊界值驗證
    When 主持人送出 SET_REVEAL_MODE(mode="auto",intervalSec=<T>)
    Then <expected_result>

    Examples:
      | T  | expected_result                                         |
      | 1  | 伺服器接受並開始每隔 1 秒廣播 REVEAL_INDEX(合法下限)    |
      | 30 | 伺服器接受並開始每隔 30 秒廣播 REVEAL_INDEX(合法上限)   |
      | 0  | 伺服器回傳錯誤碼 "INVALID_AUTO_REVEAL_INTERVAL"(T=0 非法)|
      | 31 | 伺服器回傳錯誤碼 "INVALID_AUTO_REVEAL_INTERVAL"(T=31 非法)|

  # ---------------------------------------------------------------------------
  # AC-BEGIN-REVEAL-001 — BEGIN_REVEAL:running → revealing,廣播 ROOM_STATE
  # ---------------------------------------------------------------------------
  @AC-BEGIN-REVEAL-001 @P0
  Scenario: 主持人在 running 狀態觸發 BEGIN_REVEAL 後房間進入 revealing 狀態
    Given 房間碼 "GAMMA4" 存在且狀態為 "running"
    And 主持人已透過 hostToken 驗證身份
    When 主持人送出 WS 訊息 BEGIN_REVEAL
    Then 伺服器廣播 ROOM_STATE 給所有客戶端
    And ROOM_STATE payload 中 status 為 "revealing"
    And ROOM_STATE payload 中 revealedCount 為 0
    And 所有客戶端進入揭曉等待介面

  # ---------------------------------------------------------------------------
  # AC-BEGIN-REVEAL-002 — BEGIN_REVEAL 在非 running 狀態時被拒絕
  # ---------------------------------------------------------------------------
  @AC-BEGIN-REVEAL-002 @P0
  Scenario Outline: 非 running 狀態送出 BEGIN_REVEAL 時收到 INVALID_STATE 錯誤
    Given 房間碼 "GAMMA4" 存在且狀態為 "<room_status>"
    And 主持人已透過 hostToken 驗證身份
    When 主持人送出 WS 訊息 BEGIN_REVEAL
    Then 伺服器回傳 ERROR 事件,code 為 "INVALID_STATE"
    And 房間狀態維持 "<room_status>"

    Examples:
      | room_status |
      | waiting     |
      | revealing   |
      | finished    |

  # ---------------------------------------------------------------------------
  # AC-END-GAME-001 — END_GAME:全部路徑已揭曉後主持人觸發,狀態轉為 finished
  # ---------------------------------------------------------------------------
  @AC-END-GAME-001 @P0
  Scenario: 所有路徑揭曉後主持人送出 END_GAME 使房間進入 finished 狀態
    Given 房間碼 "GAMMA4" 存在且狀態為 "revealing"
    And 主持人已透過 hostToken 驗證身份
    And 所有 4 條路徑已全數揭曉(revealedCount=4)
    When 主持人送出 WS 訊息 END_GAME
    Then 伺服器廣播 ROOM_STATE 給所有客戶端
    And ROOM_STATE payload 中 status 為 "finished"
    And 所有客戶端顯示完整得獎名單

  # ---------------------------------------------------------------------------
  # AC-END-GAME-002 — END_GAME 在路徑尚未全部揭曉時被拒絕
  # ---------------------------------------------------------------------------
  @AC-END-GAME-002 @P0
  Scenario: 尚有未揭曉路徑時送出 END_GAME 收到 REVEALS_INCOMPLETE 錯誤
    Given 房間碼 "GAMMA4" 存在且狀態為 "revealing"
    And 主持人已透過 hostToken 驗證身份
    And 目前已揭曉 2 條路徑,剩餘 2 條未揭曉(revealedCount=2,totalCount=4)
    When 主持人送出 WS 訊息 END_GAME
    Then 伺服器回傳 ERROR 事件,code 為 "REVEALS_INCOMPLETE"
    And 錯誤訊息包含「尚有未揭曉的路徑,請先完成揭曉」
    And 房間狀態維持 "revealing"

  # ---------------------------------------------------------------------------
  # AC-END-GAME-003 — END_GAME 在非 revealing 狀態時被拒絕
  # ---------------------------------------------------------------------------
  @AC-END-GAME-003 @P0
  Scenario Outline: 非 revealing 狀態送出 END_GAME 時收到 INVALID_STATE 錯誤
    Given 房間碼 "GAMMA4" 存在且狀態為 "<room_status>"
    And 主持人已透過 hostToken 驗證身份
    When 主持人送出 WS 訊息 END_GAME
    Then 伺服器回傳 ERROR 事件,code 為 "INVALID_STATE"
    And 房間狀態維持 "<room_status>"

    Examples:
      | room_status |
      | waiting     |
      | running     |
      | finished    |

  # ---------------------------------------------------------------------------
  # AC-END-GAME-004 — REVEAL_ALL 後系統自動觸發 END_GAME(無需主持人手動)
  # ---------------------------------------------------------------------------
  @AC-END-GAME-004 @P0
  Scenario: REVEAL_ALL_TRIGGER 後伺服器自動完成揭曉並廣播 finished 狀態
    Given 房間碼 "GAMMA4" 存在且狀態為 "revealing"
    And 目前已揭曉 1 條路徑,剩餘 3 條未揭曉
    When 主持人送出 WS 訊息 REVEAL_ALL_TRIGGER
    Then 伺服器廣播 REVEAL_ALL 事件,payload 包含所有剩餘 3 條路徑的完整 ResultSlot 資料
    And 伺服器隨後廣播 ROOM_STATE,status 為 "finished"
    And 所有客戶端顯示完整得獎名單,無需主持人再送出 END_GAME

room-lifecycle.feature

# features/room-lifecycle.feature
Feature: 房間生命週期
  As a 主持人
  I want to 建立並管理一個帶有唯一房間碼的房間
  So that 玩家能快速找到並加入我的抽獎活動

  # ---------------------------------------------------------------------------
  # AC-H01-1 — 建立房間:回傳合法 6 碼 Room Code
  # ---------------------------------------------------------------------------
  @AC-ROOM-001 @P0
  Scenario: 成功建立房間並取得合法 6 碼 Room Code
    Given 主持人送出 POST /api/v1/rooms 請求(含 hostNickname 及 winnerCount)
    When 系統處理請求成功
    Then HTTP 回應狀態碼為 201
    And 回應包含 6 碼 roomCode,字元集為大寫英數字(排除 O、0、I、1),符合正規表達式 [A-HJ-NP-Z2-9]{6}
    And Redis 中存在以 "room:{roomCode}" 為 key 的資料
    And 回應時間在 2 秒以內

  # ---------------------------------------------------------------------------
  # AC-H01-2 — 建立成功後主持人進入等待畫面
  # ---------------------------------------------------------------------------
  @AC-ROOM-002 @P0
  Scenario: 主持人建立房間後進入等待畫面
    Given 主持人已成功建立房間,取得 roomCode 及 hostToken
    When 主持人透過 WebSocket 連線至房間
    Then 主持人收到 ROOM_STATE_FULL 事件
    And 事件 payload 中 status 為 "waiting"
    And 玩家列表為空(不含主持人以外的玩家)
    And 頁面包含設定中獎名額的輸入框

  # ---------------------------------------------------------------------------
  # AC-H01-3 — 建立房間失敗時不產生孤立房間
  # ---------------------------------------------------------------------------
  @AC-ROOM-003 @P0
  Scenario: 建立房間因網路異常失敗時不產生孤立房間
    Given 後端模擬回應 HTTP 500 網路異常
    When 主持人送出 POST /api/v1/rooms 請求
    Then HTTP 回應狀態碼為 5xx
    And Redis 中不存在任何對應此請求的 "room:{*}" 孤立 key
    And 前端顯示「建立失敗,請重試」提示

  # ---------------------------------------------------------------------------
  # FR-01-2 — Room Code TTL 與碰撞重試
  # ---------------------------------------------------------------------------
  @AC-ROOM-004 @P0
  Scenario: Room Code 在 Redis 中設有 TTL 且碰撞時最多重試 10 次
    Given 系統正常運作中
    When 主持人成功建立房間
    Then Redis 中 "room:{roomCode}" 的 TTL 大於 0 且不超過 14400 秒(4 小時)

  @AC-ROOM-005 @P0
  Scenario Outline: Room Code 碰撞時系統重試並最終成功
    Given Redis 中已存在 <collision_count> 個佔用的 Room Code
    When 主持人送出建立房間請求
    Then 系統在重試不超過 10 次後成功回傳唯一 roomCode
    And HTTP 回應狀態碼為 201

    Examples:
      | collision_count |
      | 1               |
      | 5               |
      | 9               |

  # ---------------------------------------------------------------------------
  # AC-H01-4 — Room Code 碰撞超過 10 次 → ROOM_CODE_GENERATION_FAILED
  # ---------------------------------------------------------------------------
  @AC-ROOM-006 @P0
  Scenario: Room Code 碰撞超過 10 次後系統回傳 ROOM_CODE_GENERATION_FAILED
    Given Redis 中已存在超過 10 個碰撞的 Room Code,導致每次嘗試均重複
    When 主持人送出建立房間請求
    Then HTTP 回應狀態碼為 500
    And 回應錯誤碼為 "ROOM_CODE_GENERATION_FAILED"
    And 前端顯示「建立失敗,請重試」提示
    And Redis 中不存在任何孤立房間 key

  # ---------------------------------------------------------------------------
  # AC-H02-1 — waiting 狀態下 Host 可更新中獎名額(UPDATE_WINNER_COUNT)
  # ---------------------------------------------------------------------------
  @AC-WIN-001 @P0
  Scenario: 主持人在 waiting 狀態成功更新中獎名額
    Given 房間碼 "ALPHA2" 存在且狀態為 "waiting"
    And 房間有 5 名玩家(N=5)且 winnerCount 尚未設定
    When 主持人送出 UPDATE_WINNER_COUNT(winnerCount=2)
    Then 伺服器接受並廣播 ROOM_STATE 給所有在線玩家
    And ROOM_STATE payload 中 winnerCount 為 2

  @AC-WIN-002 @P0
  Scenario Outline: 主持人設定非法中獎名額時收到 INVALID_PRIZES_COUNT
    Given 房間碼 "ALPHA2" 存在且狀態為 "waiting"
    And 房間有 <N> 名玩家(N=<N>)
    When 主持人送出 UPDATE_WINNER_COUNT(winnerCount=<W>)
    Then 伺服器回傳錯誤碼 "INVALID_PRIZES_COUNT"
    And 房間 winnerCount 維持不變

    Examples:
      | N | W | 說明             |
      | 5 | 0 | W=0,低於下限     |
      | 5 | 5 | W=N,等於上限違規 |

  @AC-WIN-003 @P0
  Scenario: 主持人在非 waiting 狀態更新中獎名額時被拒絕
    Given 房間碼 "ALPHA2" 存在且狀態為 "running"
    When 主持人送出 UPDATE_WINNER_COUNT(winnerCount=1)
    Then 伺服器回傳錯誤碼 "UPDATE_WINNER_COUNT_NOT_ALLOWED_IN_STATE"
    And 房間狀態維持 "running"

room-management.feature

Feature: Room Management
  As a player
  I want to create and join rooms
  So that I can participate in lottery activities

  # ---------------------------------------------------------------------------
  # AC-H01-1 — Create a new room successfully
  # ---------------------------------------------------------------------------
  Scenario: Create a new room with valid host nickname
    # AC-H01-1
    Given I provide a valid nickname "Alice"
    When I POST to /api/rooms with { hostNickname: "Alice", winnerCount: 1 }
    Then response status is 201
    And response contains roomCode, playerId, and token
    And roomCode matches pattern [A-HJ-NP-Z2-9]{6}
    And the room exists in Redis with TTL up to 14400 seconds

  # ---------------------------------------------------------------------------
  # AC-H01-2 — Host enters waiting lobby after room creation
  # ---------------------------------------------------------------------------
  Scenario: Host enters waiting lobby after room creation
    # AC-H01-2
    Given I have successfully created a room and received roomCode and token
    When I connect WebSocket to /ws with the host token
    Then I receive a ROOM_STATE_FULL event
    And the payload status is "waiting"
    And the player list contains only the host
    And the page includes a winner count input field

  # ---------------------------------------------------------------------------
  # AC-H01-3 — Room creation failure does not produce orphan rooms
  # ---------------------------------------------------------------------------
  Scenario: Room creation failure does not produce orphan rooms
    # AC-H01-3
    Given the backend is configured to simulate HTTP 500 error
    When I POST to /api/rooms with { hostNickname: "Alice", winnerCount: 1 }
    Then response status is 500
    And no orphan "room:{*}" key exists in Redis
    And the error message indicates creation failure

  # ---------------------------------------------------------------------------
  # AC-H01-4 — Room code collision exceeds 10 retries returns error
  # ---------------------------------------------------------------------------
  Scenario: Room code collision exceeds 10 retries returns ROOM_CODE_GENERATION_FAILED
    # AC-H01-4
    Given Redis already contains more than 10 colliding Room Codes
    When I POST to /api/rooms with { hostNickname: "Alice", winnerCount: 1 }
    Then response status is 500
    And response error code is "ROOM_CODE_GENERATION_FAILED"

  # ---------------------------------------------------------------------------
  # AC-P01-1 — Player joins existing room via WebSocket
  # ---------------------------------------------------------------------------
  Scenario: Join existing room via WebSocket
    # AC-P01-1, AC-P02-1
    Given a room with code "ABC123" exists and status is "waiting"
    When I POST to /api/rooms/ABC123/players with { nickname: "Bob" }
    Then response status is 201
    And response contains playerId and token
    When I connect WebSocket to /ws?room=ABC123 with the player token
    Then I send { type: "JOIN_ROOM", nickname: "Bob" }
    And all players receive ROOM_UPDATE message
    And the player list includes "Bob"

  # ---------------------------------------------------------------------------
  # AC-P01-7 — Room not found returns 404
  # ---------------------------------------------------------------------------
  Scenario: Room not found
    # AC-P01-7
    When I GET /api/rooms/INVALID
    Then response status is 404
    And response error code is "ROOM_NOT_FOUND"

  # ---------------------------------------------------------------------------
  # AC-P01-4 — Room is full returns ROOM_FULL error
  # ---------------------------------------------------------------------------
  Scenario: Room is full when 50 players already joined
    # AC-P01-4
    Given a room with code "ABC123" exists and already has 50 players
    When I POST to /api/rooms/ABC123/players with { nickname: "LateComer" }
    Then response status is 409
    And response error code is "ROOM_FULL"
    And the message indicates the room is full

  # ---------------------------------------------------------------------------
  # AC-P01-2 — Duplicate nickname returns NICKNAME_TAKEN error
  # ---------------------------------------------------------------------------
  Scenario: Duplicate nickname in the same room returns NICKNAME_TAKEN
    # AC-P01-2
    Given a room with code "ABC123" exists and has a player with nickname "Bob"
    When I POST to /api/rooms/ABC123/players with { nickname: "Bob" }
    Then response status is 409
    And response error code is "NICKNAME_TAKEN"
    And the message says "此暱稱已被使用,請換一個"

  # ---------------------------------------------------------------------------
  # AC-P01-8 — Cannot join room that has already started
  # ---------------------------------------------------------------------------
  Scenario: Cannot join a room that has already started
    # AC-P01-8
    Given a room with code "ABC123" exists and status is "running"
    When I POST to /api/rooms/ABC123/players with { nickname: "LateComer" }
    Then response status is 409
    And response error code is "ROOM_NOT_ACCEPTING"
    And the message indicates the game has already started

websocket-protocol.feature

Feature: WebSocket Protocol
  As a player or host
  I want reliable WebSocket communication with the server
  So that I can participate in real-time room events without losing state

  # ---------------------------------------------------------------------------
  # FR-03-1, FR-03-2 — Connection and join flow
  # ---------------------------------------------------------------------------
  Scenario: Player connects and receives initial room state
    # FR-03-1, AC-P01-1
    Given a room with code "WS0001" exists and status is "waiting"
    And a player has obtained a token via POST /api/rooms/WS0001/players
    When the player connects WebSocket to /ws?room=WS0001&token={token}
    Then the server sends ROOM_STATE_FULL unicast to the player
    And ROOM_STATE_FULL payload contains status, players, selfPlayerId, ladder (null), and results (null)
    And the WebSocket connection is established within 1.5 seconds

  Scenario: Host connects and receives host-specific room state
    # FR-03-1, AC-H01-2
    Given a host has created a room and obtained a host token
    When the host connects WebSocket to /ws?room={roomCode}&token={hostToken}
    Then the server sends ROOM_STATE_FULL unicast to the host
    And ROOM_STATE_FULL payload status is "waiting"
    And the host can issue host-only messages (START_GAME, BEGIN_REVEAL, etc.)

  # ---------------------------------------------------------------------------
  # FR-03-3 — WebSocket message envelope format validation
  # ---------------------------------------------------------------------------
  Scenario: Server sends messages in correct envelope format
    # FR-03-3
    Given a player is connected to a room
    When any server event is broadcast
    Then the message JSON contains "type", "ts", and "payload" fields
    And "ts" is a Unix milliseconds timestamp

  Scenario: Client sends messages in correct envelope format
    # FR-03-3
    Given a host is connected to a room in "waiting" status
    When the host sends { type: "START_GAME", ts: 1745050000000, payload: {} }
    Then the server processes the message without error
    And the server responds or broadcasts within 1 second

  # ---------------------------------------------------------------------------
  # FR-03-4 — Error events include machine-readable code and human-readable message
  # ---------------------------------------------------------------------------
  Scenario: Error events include code and message fields
    # FR-03-4
    Given a player (not host) is connected to a room
    When the player sends a host-only message { type: "START_GAME", ts: 1745050000000, payload: {} }
    Then the server sends ERROR unicast to the player
    And the ERROR payload contains "code" field (e.g. "PLAYER_NOT_HOST")
    And the ERROR payload contains "message" field with human-readable description
    And the WebSocket connection remains open

  # ---------------------------------------------------------------------------
  # FR-03-5 — WebSocket message size limit 64KB
  # ---------------------------------------------------------------------------
  Scenario: Server rejects oversized WebSocket messages
    # FR-03-5
    Given a player is connected to a room
    When the player sends a WebSocket message exceeding 64KB
    Then the server rejects the message and closes the connection

  # ---------------------------------------------------------------------------
  # AC-P05-2, FR-10-4 — Disconnect handling: player offline status broadcast
  # ---------------------------------------------------------------------------
  Scenario: Disconnect handling updates player online status
    # AC-P02-2, FR-10-1
    Given a room "WS0001" has players "Alice" and "Bob" both online
    When "Bob"'s WebSocket connection is closed (TCP close or ping timeout)
    Then the server detects the disconnect within 30 seconds
    And the server broadcasts ROOM_STATE to all remaining players
    And ROOM_STATE payload shows "Bob" with isOnline=false
    And "Bob"'s slot and path are preserved (not removed from player list)

  # ---------------------------------------------------------------------------
  # FR-10-3 — Session replacement when same playerId reconnects from new device
  # ---------------------------------------------------------------------------
  Scenario: Session is replaced when same playerId connects from a new device
    # FR-10-3, AC-RECONNECT-005
    Given player "Dave" (playerId="player-dave-uuid") is connected via device A
    And room "WS0001" status is "waiting"
    When "Dave" connects from device B using the same playerId
    Then device A receives SESSION_REPLACED unicast event
    And device A's WebSocket connection is closed
    And device B receives ROOM_STATE_FULL with complete room state
    And "Dave"'s isOnline is updated to true for device B's connection

  # ---------------------------------------------------------------------------
  # AC-P05-1 — Reconnect restores complete state
  # ---------------------------------------------------------------------------
  Scenario: Reconnect restores player state with full room snapshot
    # AC-P05-1, FR-10-1
    Given player "Carol" (playerId="player-carol-uuid") has disconnected from room "WS0001"
    And "Carol"'s localStorage contains the playerId
    When "Carol" reconnects using the same token
    Then the server sends ROOM_STATE_FULL unicast to "Carol"
    And ROOM_STATE_FULL contains complete room state (status, players, ladder, results, selfPlayerId)
    And "Carol"'s isOnline is restored to true in the broadcast ROOM_STATE
    And reconnect completes within 3 seconds

  Scenario: Reconnect during revealing state returns static snapshot without replaying animations
    # AC-P05-2, FR-10-4
    Given room "WS0001" is in "revealing" status with revealedCount=3
    And player "Eve" has disconnected during revealing
    When "Eve" reconnects to the room
    Then the server sends ROOM_STATE_FULL with revealedCount=3 and current results
    And the client renders already-revealed paths as static results
    And no animations are replayed for previously revealed paths

  # ---------------------------------------------------------------------------
  # FR-03-4 — Heartbeat: server sends PING every 30 seconds
  # ---------------------------------------------------------------------------
  Scenario: Heartbeat keeps connection alive with PING/PONG exchange
    # FR-03-1 (Heartbeat)
    Given a player is connected to a room
    When 30 seconds pass without activity
    Then the server sends a WebSocket protocol-level PING frame
    And the client responds with a PONG frame
    And the connection remains open

  Scenario: Server closes connection when client does not respond to PING
    # FR-03-1 (Heartbeat timeout)
    Given a player is connected to a room
    When the server sends a PING frame and the client does NOT respond within 30 seconds
    Then the server closes the connection with close code 1001

  # ---------------------------------------------------------------------------
  # FR-03-4 — Application-level PING/PONG for RTT measurement
  # ---------------------------------------------------------------------------
  Scenario: Application-level PING returns PONG with echo timestamp
    # FR-03-3, §3.4
    Given a player is connected to a room
    When the player sends { type: "PING", ts: 1745049600000, payload: {} }
    Then the server responds with { type: "PONG", payload: { ts: 1745049600000 } }
    And the response ts echoes the original PING ts for RTT measurement

  # ---------------------------------------------------------------------------
  # FR-11-1, FR-11-4 — Player kicked event flow
  # ---------------------------------------------------------------------------
  Scenario: Kicked player receives PLAYER_KICKED and connection closes
    # AC-H07-1, FR-11-4
    Given room "WS0001" is in "waiting" status
    And player "Frank" (playerId="player-frank-uuid") is connected
    When the host sends KICK_PLAYER targeting "player-frank-uuid"
    Then the server sends PLAYER_KICKED unicast to "Frank"
    And "Frank"'s WebSocket connection is closed with close code 4003
    And all other players receive updated ROOM_STATE without "Frank"

  Scenario: Kicked player is blocked from reconnecting with same playerId
    # AC-H07-2, FR-11-2
    Given player "Grace" has been kicked from room "WS0001"
    When "Grace" attempts to reconnect using the same playerId
    Then the server sends PLAYER_KICKED and closes connection with close code 4003
    And "Grace" cannot rejoin the same game session

  # ---------------------------------------------------------------------------
  # §3.2 — WebSocket upgrade validation rejects unauthorized connections
  # ---------------------------------------------------------------------------
  Scenario: WebSocket upgrade is rejected with invalid token
    # §3.2, NFR-05
    Given a room with code "WS0001" exists
    When a client attempts to connect WebSocket with an invalid JWT token
    Then the WebSocket upgrade is rejected with HTTP 401
    And no WebSocket connection is established

  Scenario: WebSocket upgrade is rejected for kicked player
    # §3.2, FR-11-2
    Given a player has been kicked from room "WS0001" (playerId in kickedPlayerIds)
    When the kicked player attempts to connect WebSocket to the room
    Then the server sends PLAYER_KICKED and closes the connection with close code 4003