adventure-table

P2 — 實作規格

Phase:P2 — Room / Campaign / Session / Seat
本文件只定義 P2 各 Subphase 完成後什麼必須成立。具體 DB schema、API、module、migration、route wiring、scope enforcement 與 standalone boundary 放在同目錄的 開發設計方針.md;自動/人工驗收流程與證據要求放在 測試指南.md

最後更新:2026-09-06


1. P2 定位

P0 / P1 已完成 Character Core、Character Sheet、Character Builder、Level Up、immutable Build Version 與 mutable Current State;M01 建立可長期擴充的 Multi-Source Character Content;M02 建立 zh-TW / en 雙語基礎;M03 把同一套 Character Core 包成 Windows standalone,並建立 Character JSON Import / Export 與 standalone import boundary。

P2 是第一個真正的多人桌面基礎 Phase。P2 不做 Exploration / Chat / Roll / AI Event Queue / Combat / Adventure Runtime;它先把「一群朋友在哪一張桌、有哪些角色、哪個 Campaign、今天哪些 Seat 出席、這場 Session 何時開始/結束」做成可靠的 Server State 與 Web UX。

P2 關門時必須做到:

  1. Web 版改成 Room-first。 使用者必須先 Create / Enter Room,才可建立、匯入、管理 Character 或 Builder Draft;Web 不再有跨 Room 的 global Character Workshop。
  2. Standalone 維持 Character-first。 Standalone 完全沒有 Room / Campaign / Session / Seat;仍可 Create / Import / Manage / Level Up / Export Character。
  3. Room 是 Web workspace / namespace,不是 Character Core 的依賴。 Room 可以 reference Character / Draft;Character domain、Character persistence 與 standalone entry 不得反向 import Room / Campaign / Session / Seat。
  4. Web 的 Character 與未完成 Draft 都必須被某個 Room workspace 管理。 新 Web Draft 從建立那一刻就屬於 Room;Confirm 後產生的 Character 留在同一 Room。
  5. 同一 Character instance 不跨 Room 共用。 要把角色帶去另一 Room,走 Character JSON Export / Import 或等價 Copy-as-new;目標 Room 產生新的 Character identity。
  6. 同一 Room 內,同一 Character 可被多個 Campaign Roster reference,而且 Build / Current State 仍只有一份。 Campaign Roster 不複製 Character State。
  7. 同一 Character 不得同時成為兩個 active Session 的 Active Character。 這是避免同一 live Current State 被兩場同時寫入的 hard invariant。
  8. Campaign、Party Roster、Seat、Controller、Session lifecycle 按 規格企劃.md 的已拍板行為實作。 不重新發明 Character Ownership / Transfer workflow。
  9. P2 開始鎖 Character JSON schema。 P2 接受 M03 已產出的 unstable legacy envelope,normalize 成 locked v1;從 v1 起維持 backward-compatible import contract。
  10. Standalone boundary 仍是永久 gate。 P2 新增多人模組後,standalone frozen build、migration target 與 import graph 必須證明 Character distribution 沒被 Room dependency 汙染。

2. P2 固定 Subphase

P2 固定拆成六個可獨立實作、驗證與 commit 的 Subphases:

P2-A — Room Foundation & Web Entry
↓
P2-B — Room Character Workspace
↓
P2-C — Campaign & Party Roster
↓
P2-D — Seat, Controller & Lobby
↓
P2-E — Session Lifecycle & Late Join
↓
P2-F — Full P2 Integration & Closeout

三份 P2 文件必須使用完全一致的名稱與順序。

不得在 P2-A 先做 P2-E 的 Session feature;也不得因 P2 已有 Room 就順手加入 P3 Chat / Roll / AI Tool、P4 Combat 或 P7 full Snapshot / Restore。

每個 Subphase closeout 都必須維持已完成產品可用。特別是 P2-A 與 P2-B 的過渡:P2-A 先讓首頁與新多人入口變成 Room-first,但在 P2-B 正式完成 Room Character Workspace 前,既有 Web global Character route / API 可暫時保留為 migration compatibility path;P2-A 不再從首頁導流到它,也不得把它稱為最終 Web UX。P2-B closeout 才是 global Web Character Workshop / global Web Character API 正式收口的 gate。


3. 已拍板的 Web / Standalone 產品邊界

3.1 Web:Room-first

Web 首頁第一版入口:

Adventure Table

[ Enter Room ]
[ Create Room ]

Recent Rooms   # 可有;只作 client convenience

P2 最終 Web 首頁不提供 Character Workshop

流程固定是:

Web Homepage
↓
Create Room / Enter Room
↓
Room Workspace
├─ Characters
├─ Campaigns
└─ 後續 Phase 的 Room assets

角色工作流:

Enter Room
↓
Room Character Workshop
↓
Create / Import Character
↓
Builder Draft(已屬於該 Room)
↓
Confirm
↓
Character(仍屬於該 Room workspace)

不能建立一個「之後再決定放哪個 Room」的 Web Draft。

3.2 Standalone:Character-first

Standalone 仍是:

Standalone Landing
↓
Character Workshop
├─ Create
├─ Import
├─ Manage / Sheet
├─ Level Up / Build Edit / Version History
└─ Export

Standalone:

3.3 Room scope 是產品 workspace,不是 Character schema 必填欄位

P2 的產品保證是:

Web Character / Draft → 一定被一個 Room workspace 管理

不是:

Character Core → 必須知道 Room

因此 standalone 可以繼續使用同一個 Character domain / Builder domain / JSON schema,而不建立假的 Room。


4. Room 行為

4.1 Room 是 Web 資料隔離單位

Room 是朋友共用的一張持久桌面/workspace。

第一版 Room 至少承載:

P2 不因為未來會有 Monster Templates / Maps / Adventures,就提前建立這些 domain。

4.2 Room access

MVP 不做完整帳號系統。沿用已拍板的:

Room Password
DM Key
Owner Key

產品語意:

P2 可建立未來 AI 所需的 scope / role shape,但不得宣稱 AI 已能正式加入桌內。

Room code是 public locator,不是 secret;具體格式、entropy、Password KDF與失敗登入 throttle由 開發設計方針.md 鎖定。P2-A不能把這些安全邊界留成未定 implementation detail。

4.3 不建立 Character Ownership

Room 內沒有 My Characters / Other Characters / Ownership Transfer 制度。

一般成員可以依朋友間協調使用 Room Character Workshop。Server 仍要防止:

不要用 owner_user_id 類欄位把 Character 變成商業平台資產。

4.4 Room Hard Delete

Owner 可 Hard Delete Room,必須強確認。

Room Hard Delete 的產品語意是整個 workspace 永久刪除,包括當下由該 Room 管理的:

不留下 orphan Web Characters。

重要 Character 要保留時,先 Character Export;P2 不做 recycle bin。


5. Character / Draft 與 Room 的規則

5.1 Web Character 一次只屬於一個 Room workspace

合法:

Room A
└─ Mira #character-A

若 Room B 也要一隻 Mira:

Room A Mira
↓ Export / Copy-as-new
Room B Import
↓
Room B Mira #character-B

兩者之後完全獨立。

禁止同一 character_id 同時掛到 Room A 與 Room B。

5.2 Draft 同樣是 Room-scoped

Room Character Workshop 要能看:

Drafts
Characters
Archived Characters

新 Create Draft / imported-repair Draft 從落地時就必須有 Room scope。

Confirm Create Draft 時:

Level Up / Build Edit / Correction Draft 必須繼承其 Character 的 Room scope,不可搬到另一 Room。

5.3 Character JSON 是 Room-neutral

Character JSON:

5.4 Same Room / Multi-Campaign Character State

同一 Character 可出現在同 Room 的多個 Campaign Roster:

Room A
├─ Campaign 1 → Mira
└─ Campaign 2 → Mira

這仍是同一隻 Mira:

若使用者要「同一起始角色但兩條世界線各自發展」,必須 Duplicate / Export+Import 成另一 Character identity。


6. Character JSON v1 lock

P2-A 開始把 M03 Character exchange contract 從 unstable 帶到 locked v1。

P2 完成後必須為真:

  1. 新 Export 使用 locked v1 envelope。
  2. P2 importer 仍接受 M03 最後正式產出的 unstable envelope,並先 normalize 到 v1 internal representation。
  3. Legacy unstable import 成功後,再 export 必須得到 v1。
  4. v1 之後可以 additive 擴充,但不得任意改既有欄位語意。
  5. 未來版本 importer 必須持續接受 v1。
  6. Character exchange payload 保持 Room-neutral。
  7. Room / Campaign / Session package export 仍屬後續 Phase,不塞進 Character JSON。
  8. 最後 M03 unstable compatibility fixture必須在P2-A修改 exporter前先由真實舊 exporter產生並commit,不得等切成v1後手刻舊格式。

P2 不承諾「舊版 standalone binary 可以讀未來新 JSON」;承諾方向是新的 Adventure Table 版本可讀既有 locked schema


P2-A — Room Foundation & Web Entry

目標

建立第一個多人 workspace boundary,讓 Web 首頁正式從 global Character-first 改成 Room-first,同時鎖定 Character JSON v1,並先把 standalone boundary 保護好。

完成後必須為真

  1. Web 可以 Create Room。
  2. Web 可以用 Room Password Enter Room。
  3. Room code格式/entropy、Password KDF與failed-attempt throttle已有固定契約與測試,不留下未定 security blank。
  4. Owner / DM elevated access 有可用的 MVP credential flow;secret 不以明文保存。
  5. P2-A已交付最小 RoomAccess heartbeat;P2-D Presence只消費這份 liveness contract,不再另造第二套。
  6. Web 首頁只提供 Create / Enter Room 與 optional Recent Rooms,不再從首頁提供 Character Workshop。
  7. Room 有最小可用 workspace page / shell,後續 P2-B~E 可逐步加入功能。
  8. Web capability 宣告 room=true;Standalone 仍 room=false
  9. Standalone 不 mount Room router;手動打開 Room route 仍得到 capability-disabled UX。
  10. 在修改 exporter前,最後 M03 realistic unstable fixture已先由舊 exporter產生、驗證並commit;之後 Character JSON 新 Export才切到 locked v1。
  11. Importer 可讀該真實 M03 unstable fixture並 normalize。
  12. M03 import-boundary gate更新到 P2 真正採用的 module names,且能證明 standalone reachable import graph 不碰 Room / Campaign / Session / Seat。
  13. Alembic shared-character / web-multiplayer migration tracks 已分離;Standalone只升 shared Character head,Web 同時升所有 heads。
  14. P2-A 導入 multiple heads 後,Web開發啟動、Docker、CI / Actions、正式文件、launcher、既有可執行 migration tests與 standalone frozen smoke全部改用正確 branch-aware target;repo 中不得留下任何會因 multiple heads 直接失敗的 repo-global single-head 假設,包含裸 head target、get_current_head()len(heads)==1、自行推導唯一 repository head,以及把 Web alembic_version 固定當單列 scalar。
  15. M03 SQLite schema parity已收斂成explicit Character table allowlist;即使同pytest process先import Room tables也不受影響。
  16. P2 Non-E2E專屬 workflow已存在並提供真 PostgreSQL migration gate;其他歷史 workflow綠燈不可替代。
  17. 既有 Web Playwright suite 與 app.main Character / Builder HTTP regression 在 Room-first 首頁上線後仍可完整執行,且仍作為 Web channel regression;不得改綁 standalone、不得整批 skip / fixme 來閃避 route 遷移。
  18. P2-A 不建立 Campaign、Seat、Session business logic。

P2-A 過渡相容

P2-A closeout 時,舊 Web /characters / /api/characters 等 global Character path 可以暫時存在到 P2-B,目的是讓每個 Subphase完成後既有 Character產品仍可操作;但:

P2-A 不做


P2-B — Room Character Workspace

目標

把既有 Web Character Workshop、Character、Builder Draft、Import / Export 正式收進 Room workspace;Standalone 保留原本 Room-less workflow。

完成後必須為真

  1. Web 新 Character Draft 必須在 Room → Character Workshop 建立。
  2. Web Create Draft 從第一筆 persistence 起就有 Room workspace association。
  3. Web list Character / Draft / archived Character 只回傳目前 Room 的資料。
  4. 跨 Room 猜 UUID 不能讀、改、confirm、archive、export、level up、build edit 或刪除另一 Room 的 Character / Draft。
  5. Create Draft Confirm 與 Room Character association 是同一個 authoritative transaction boundary;不能留下 successful Character + missing Room association。
  6. Level Up / Build Edit / Correction Draft 自動繼承原 Character Room,不提供搬 Room 選項。
  7. Web Import 必須由 target Room 發起;成功後 character / repair draft 屬於 target Room。
  8. Web Character routes / UI 都帶 Room context;不再從 global /characters UX 列出整台 Server 的 Character。
  9. Web global Character / Builder API 不再提供可繞過 Room scope 的 list / mutation path;Standalone 原 Room-less endpoints不受影響。
  10. Standalone 仍可使用 Room-less Character Workshop、Draft、Sheet、Version History、Import / Export。
  11. Room Hard Delete 可完整刪掉它管理的 Character / Draft,不留下 orphan。
  12. P2 升級前既有 global Character / Draft 不得 silent delete;migration / first-room claim 必須有 deterministic upgrade path。
  13. 個別 Character 若已被 Campaign / Session history reference,後續不得永久刪除破壞 reference;使用 Archive 保留。Room Hard Delete 是 Owner 對整個 workspace 的 destructive exception。
  14. Web global Character / Builder path 收口後,既有 Web browser 與 HTTP regression 已改走 Room-scoped path 並全數通過,仍以 Web channel 驗證;Standalone regression 責任不同,不得用來替代。

Legacy Web data

P2-B 必須處理 P2 之前已存在、沒有 Room association 的 Web Character / Draft。

要求:

具體 migration / bootstrap 技術契約見 開發設計方針.md


P2-C — Campaign & Party Roster

目標

讓 Room 可以建立多個持久 Campaign,並從同一 Room 的 Characters建立 Party Roster;Roster 管 Campaign participation,不複製 Character Build / State。

Campaign 完成條件

  1. Owner 可在 Room 建立 / archive / select Campaign;DM authority不因持有 DM Key就取得 Room Campaign lifecycle權限。
  2. Campaign 有已拍板狀態:
draft
active
completed
archived
  1. Campaign 最低建立資料只要求 Name + Ruleset;Adventure 未實作時不強迫建立或假裝存在 Adventure integration。
  2. Room 可有多個仍在進行的 Campaign,但只有一個 active_campaign_id 作為 Room 目前選中的 Campaign。
  3. Campaign Archive / lifecycle 不會刪 Character。
  4. 規格企劃.mdCampaign Level / Rules 仍是產品方向,但 P2 不新增 generic Campaign Rules blob,也不持久化 Leveling / Diagonal 欄位Leveling = Milestone 在目前只有單一升級模式時維持全產品基線;Diagonal = 5/10 alternating 到 P5 Tactical Combat 真正需要空間規則時交付。未來若出現第二種 leveling mode,再由對應 Phase 把它提升成 Campaign setting。

Party Roster 完成條件

Roster entry:

Campaign
→ Room Character
→ Campaign status

status:

active
inactive
retired
dead

必須成立:

State ownership

P2-C 必須明確證明:

Campaign Roster ≠ Campaign copy of CharacterState

Roster reference 同一 Room Character;HP / Inventory / Prepared / resources 等仍讀寫該 Character 的唯一 Current State。


P2-D — Seat, Controller & Lobby

目標

建立「本場桌上的操作位置」與最小 Lobby,讓下一步 Session 可以固定每個 Player Seat 本場使用哪隻 Character。

Seat 概念

產品概念保持:

Seat
├─ Role
└─ Controller

P2 必須清楚區分:

Owner 是 Room 管理 authority,不要求「Owner」成為獨立的 gameplay seat type;Owner 若實際玩桌,仍使用 DM / Player Seat。

Controller

資料 shape 必須能表示:

human
ai
none

P2 只正式交付 Human / None 的可操作控制流程

ai 是為 P3 正式 AI actor 保留的合法 domain shape;P2 不建立 AI Join Token、MCP、event queue、wait_for_event 或 Connection workflow,也不得在 UI 假裝 AI 已經上線可玩。

Lobby / Seat 行為

P2-D 完成後:

  1. Active Campaign 可進 Lobby。
  2. 不強制 Ready。
  3. 不要求所有 Roster Character 都出席。
  4. 一個 Human controller 可以同場控制多個 Player Seat。
  5. 一個 Seat 同時只對應一隻本場 Active Character。
  6. Start 前 Player Seat 只能從目前 Campaign Roster 選 Character。
  7. 只有 Roster status active / inactive 可選;retired / dead 不出現在 Active Character 選單。
  8. DM Seat 不需要綁 Player Character。
  9. Human presence 使用P2-A既有heartbeat資料顯示 Connected / Offline,不另建 P3 realtime event system。
  10. Owner / DM authority可配置目前 Campaign的 Roster與Player / Spectator Seats;一般 member只能操作分配給自己的 Human Player Seat,不得改他人 Seat。
  11. DM Seat的Human controller assignment / reassignment只有Owner authority可做。 持DM Key者不能自行把自己綁上DM Seat;Owner若自己當DM,也必須明確把自己的access session assign到DM Seat。
  12. Room authority與 Seat role / controller boundary由 Server enforce,不能只靠 frontend 隱藏按鈕。
  13. 從未被任何Session引用的Seat可以刪;一旦被Session history引用,Seat只能archive,舊Session reference必須保留。Room Hard Delete為整個workspace destructive exception。

P2-E — Session Lifecycle & Late Join

目標

完成最小 Session lifecycle,讓一群人可以在一個 Campaign 開始今天的桌、固定本場角色、late join、結束/abandon,並在下次重新進入時看到可恢復的 structured context。

Session 狀態

active
ended
abandoned

Start Session

Start 前:

Room
↓
Active Campaign
↓
Lobby / Seats
↓
Owner已完成DM assignment
↓
被指定的DM Start Session

Start 必須:

P2 不提前實作 P7 完整 Snapshot payload / Restore subsystem規格企劃.md 所稱 Session Start / End Snapshot,在 P2 先保留 deterministic boundary / future hook;P7 Snapshot subsystem 到位後再接真正整包 snapshot。

Active Character freeze

Session 一旦開始:

Concurrent Session invariant

同一 Character 如果已是任何 active Session 的 Active Character,另一 active Session 不得再取得它。

這個限制跨同 Room 多 Campaign 生效。

Late Join

Human late join由本場 current DM Controller管理:

  1. 加入新的 Player Seat / session participation。
  2. 從同 Campaign Roster 選合法 Character。
  3. Character 不能已被另一 active Session 使用。
  4. Exploration / Quick Combat 不要求 spawn position;Tactical placement 留給 P5。

敘事如何進場由 DM 決定。

End / Abandon Session

End Session:只有本場 current DM Controller可執行;Owner / 其他 DM authority不能因權限較高就取代本場 DM。

Abandon Session:本場 current DM Controller或 Owner可執行,用於誤開、DM確定不回來等情況;這不是 DM succession。

End / Abandon 都必須:

Resume

P2 的 Resume UX 只顯示目前 P2 已有的 structured state,例如:

Combat Round / Pending Roll / Reaction 等資料要等對應 P3/P4/P5 subsystem 真正存在才顯示,不在 P2 建 placeholder truth。


P2-F — Full P2 Integration & Closeout

目標

證明 Room-first Web、Room Character Workspace、Campaign / Roster、Seat / Lobby、Session lifecycle、Character JSON v1 與 Standalone boundary 能作為一個完整產品基線工作。

P2-F 關門條件

至少必須證明:

  1. Fresh Web PostgreSQL migration 到 P2 heads 成功;Standalone fresh / legacy SQLite只升 character@head 成功。
  2. 從現行 0008_m03c_import_records legacy Web DB upgrade 到 P2 heads 成功,既有 Character / Draft 不 silent loss。
  3. 既有M03 migration tests已改為multi-head-aware,Standalone schema parity使用Character allowlist且不受test import順序影響。
  4. P2專屬 P2 Non-E2E workflow在final SHA提供PostgreSQL migration / persistence / concurrency evidence;其他workflow不能替代。
  5. Web:Homepage → Create Room → Enter → Character Workshop → Create Draft → reload → Confirm → Sheet / Version History 全程 Room-scoped。
  6. Web:target Room Import在P2-A exporter改版前已封存的真實legacy unstable fixture → Preview → Import → Export v1。
  7. Standalone:Create / Import / Sheet / Level Up / Export 仍可用,而且沒有 Room UI / router / dependency / multiplayer tables。
  8. Multi-Room isolation:Room A caller 無法用 UUID 操作 Room B Character / Draft / Campaign / Seat / Session。
  9. Same-Room multi-Campaign:同一 Character 可在兩個 Roster,且 Current State 是同一份。
  10. Room access code/password/throttle與heartbeat契約有自動證據。
  11. Owner-controlled DM assignment成立;DM Key holder不能self-assign DM Seat。
  12. 被Session history reference的Seat只能archive,Session Seat FK history不破壞。
  13. Active Session collision:同一 Character 不得同時在兩個 active Session。
  14. Lobby / Start / Late Join / End / Abandon lifecycle 符合產品契約。
  15. Session End 不改 Character gameplay state。
  16. Room Hard Delete 強確認後能清掉整個 workspace,沒有 P2 scope orphan。
  17. zh-TW / en P2 user-facing copy 完整,不出 raw key / raw server code。
  18. M03 standalone import-boundary tests green。
  19. Windows standalone build workflow仍能成功產出 artifact;P2 不把 multiplayer package 拉進 standalone runtime graph。
  20. P0 / P1 / M01 cumulative Character baseline沒有因 Room scope wrapper regression。
  21. P2 real-backend E2E、restart persistence、authorization matrix與 human smoke皆有 closeout evidence。

7. P2 權限最低產品矩陣

P2 不做帳號 ownership,但需有可解釋的 Room scope。Room authority與本場 Session Controller是兩個不同維度。

Room / Character / Campaign authority

操作 Room Member DM authority Owner authority
Enter Room / view allowed Room data
Create / Import Character / Draft
Export Room Character
使用 Builder / Level Up(無 active Seat衝突時)
Reversible Character archive / restore(無 active Session lease)
Permanent individual Character delete(archived 且無 history reference)
Create / archive / select Campaign
Manage current Campaign Roster
Configure Player / Spectator Seats / Lobby
Assign / reassign DM Seat Human controller
Room settings / rotate keys / Hard Delete Room

這不是 Character ownership。一般朋友仍自行協調誰建立/升級哪隻 Character;Server 只對 destructive / table-authority action收緊。

Session Start / Active Session authority

操作 一般 Room authority DM authority但未被Owner assign Owner但未被assign Owner-assigned DM Seat Controller / current DM Controller
Start Session ✅(Start時成為固定DM Controller)
Configure Late Join ✅(active後須current DM)
End Session ✅(current DM only)
Abandon stuck / mistaken Session
修改自己 Seat 的 Current State 需同時是該 Human Player Seat Controller 同左 同左,Owner身分不額外放寬 ✅ 可 DM 修正

Session active 後,Player 對 live Character State 的實際操作權必須受 Seat / Controller 約束;完整 GameAction permission會在 P3+ 擴充,但 P2 不可留下「知道另一角色 UUID就能 patch state」的 Web bypass。

Human DM 對 Player Character 的既有高修改權限,在 active Session中由本場 current DM Controller行使;不能讓另一個只持有 DM Key的人旁路修改本場角色。


8. P2 明確不做

P2 不做:


9. P2 closeout 後交給 P3 的地基

P2 關門後,P3 可以假設下列正式基礎已存在:

Web Request
→ Room Access Context
→ Room
→ Active Campaign
→ Active Session
→ Seat / Controller
→ Active Character

以及:

P3 在此基礎上加入 Exploration、Chat / Action / Check / Roll、正式 Human / AI table actor integration;P2 不提前替 P3 實作那些行為。