mafia-studio-protocol 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 The Resonance Lab
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,20 @@
1
+ # mafia-studio-protocol
2
+
3
+ Shared wire protocol types for [MafiaStudio](https://github.com/The-Resonance-Lab/MafiaStudio) realtime.
4
+
5
+ Types-only — no runtime. You almost never import this directly; it's re-exported through [`mafia-studio-adapter`](https://www.npmjs.com/package/mafia-studio-adapter).
6
+
7
+ ## What's in here
8
+
9
+ - Frame envelope (`UniversalFrame`, `ServerFrame`)
10
+ - Phase names, action names, message channels
11
+ - Seat roles, participant types, factions
12
+ - Room welcome / message / phase-change / move-result / game-over payloads
13
+
14
+ ## Version
15
+
16
+ `0.1.x` — the protocol is still evolving. Adapter and realtime both pin the same minor.
17
+
18
+ ## License
19
+
20
+ MIT
@@ -0,0 +1,204 @@
1
+ export declare const PROTOCOL_VERSION: 1;
2
+ export type PhaseName = "LOBBY" | "NIGHT_0" | "DAY_DISCUSSION" | "DAY_VOTE" | "NIGHT" | "REVEAL" | "GAME_OVER";
3
+ export type SeatRole = "town" | "mafia" | "detective";
4
+ export type ParticipantType = "human" | "persona" | "empty";
5
+ export type MessageChannel = "public" | "mafia_whisper" | "system";
6
+ export type Faction = "town" | "mafia";
7
+ export type MafiaActionName = "post_message" | "stay_quiet" | "vote_for" | "abstain" | "whisper_to_mafia" | "nominate_kill" | "investigate" | "accuse";
8
+ export type MoveKind = "vote_for" | "abstain" | "nominate_kill" | "investigate" | "timeout";
9
+ export type MoveResultKind = "vote_tally" | "kill_result" | "investigation_result" | "timeout" | "merged";
10
+ export type ErrorCode = "TURN_DEADLINE_MISSED" | "PHASE_ADVANCED" | "PHASE_DISALLOWS_ACTION" | "UNKNOWN_ACTION" | "SEAT_TAKEN" | "TABLE_CLOSED" | "AUTH_FAILED" | "RATE_LIMITED" | "INTERNAL";
11
+ export interface PhaseConfig {
12
+ night_0_ms: number;
13
+ day_discussion_ms: number;
14
+ day_vote_ms: number;
15
+ night_ms: number;
16
+ reveal_ms: number;
17
+ }
18
+ export declare const DEFAULT_PHASE_CONFIG: PhaseConfig;
19
+ export declare const ALLOWED_ACTIONS_BY_PHASE: Record<PhaseName, Partial<Record<SeatRole, MafiaActionName[]>>>;
20
+ /** Minimal structural mirror of emocentric's ContextEvent. Adapter casts between the two. */
21
+ export interface WireContextEvent<TRaw = unknown> {
22
+ id: string;
23
+ occurredAt: string;
24
+ platform: string;
25
+ channelId?: string;
26
+ actor: {
27
+ id: string;
28
+ displayName?: string;
29
+ metadata?: Record<string, unknown>;
30
+ };
31
+ payload: WirePayload;
32
+ raw?: TRaw;
33
+ metadata?: Record<string, unknown>;
34
+ }
35
+ /** Aligned with emocentric's Payload shapes so the adapter can pass through
36
+ * the ContextEvent directly. We use `text` for chat and `action` for game
37
+ * events (phase changes, move results); teams' personas naturally consume
38
+ * both. */
39
+ export type WirePayload = {
40
+ kind: "text";
41
+ text: string;
42
+ } | {
43
+ kind: "action";
44
+ name: string;
45
+ description?: string;
46
+ data?: Record<string, unknown>;
47
+ };
48
+ export interface UniversalFrame<TData = unknown> {
49
+ v: typeof PROTOCOL_VERSION;
50
+ type: string;
51
+ seq: number;
52
+ at: string;
53
+ table_id: string;
54
+ data: TData;
55
+ }
56
+ export interface RoomWelcomeSeat {
57
+ seat_id: string;
58
+ seat_index: number;
59
+ label: string;
60
+ kind: ParticipantType;
61
+ alive: boolean;
62
+ }
63
+ export interface RoomWelcomeData {
64
+ seat_id: string;
65
+ seat_index: number;
66
+ seat_label: string;
67
+ /** Only revealed to the owning seat. Mafia see mafia; town see generic "town". */
68
+ seat_role: SeatRole;
69
+ /** Other-seat roles visible to this seat (mafia see fellow mafia). */
70
+ visible_roles?: Record<string, SeatRole>;
71
+ table: {
72
+ id: string;
73
+ name: string;
74
+ phase_config: PhaseConfig;
75
+ seats: RoomWelcomeSeat[];
76
+ };
77
+ phase: {
78
+ name: PhaseName;
79
+ started_at: string;
80
+ ends_at: string | null;
81
+ round: number;
82
+ };
83
+ resume?: {
84
+ last_seq: number;
85
+ };
86
+ }
87
+ export interface RoomMessageData {
88
+ message_id: string;
89
+ seat_id: string;
90
+ seat_label: string;
91
+ channel: MessageChannel;
92
+ text: string;
93
+ phase: PhaseName;
94
+ round: number;
95
+ }
96
+ export interface AgentTurnRequestData {
97
+ request_id: string;
98
+ context_event: WireContextEvent;
99
+ deadline_ms: number;
100
+ allowed_actions: MafiaActionName[];
101
+ }
102
+ export interface PhaseChangeData {
103
+ from: PhaseName | null;
104
+ to: PhaseName;
105
+ round: number;
106
+ started_at: string;
107
+ ends_at: string | null;
108
+ allowed_actions_by_role: Partial<Record<SeatRole, MafiaActionName[]>>;
109
+ }
110
+ export interface MoveResultData {
111
+ move_id: string;
112
+ kind: MoveResultKind;
113
+ phase: PhaseName;
114
+ round: number;
115
+ payload: Record<string, unknown>;
116
+ }
117
+ export interface RoundEndData {
118
+ round: number;
119
+ dead_seats: Array<{
120
+ seat_id: string;
121
+ seat_label: string;
122
+ }>;
123
+ next_phase: PhaseName;
124
+ }
125
+ export interface GameOverData {
126
+ winning_faction: Faction | null;
127
+ per_seat_outcomes: Array<{
128
+ seat_id: string;
129
+ seat_label: string;
130
+ role: SeatRole;
131
+ alive: boolean;
132
+ won: boolean;
133
+ }>;
134
+ scoring: Array<{
135
+ seat_id: string;
136
+ score: number;
137
+ }>;
138
+ }
139
+ export interface ErrorData {
140
+ code: ErrorCode;
141
+ request_id?: string;
142
+ message: string;
143
+ }
144
+ /** Runtime shape of every server → client frame. We keep `data` as `unknown`
145
+ * in the base type so the engine can construct frames without contorting to
146
+ * a discriminated union; consumers narrow via `frame.type` and cast `data`
147
+ * to the matching interface below. */
148
+ export type ServerFrame = UniversalFrame<unknown> & {
149
+ type: string;
150
+ };
151
+ export type ServerFrameData = RoomWelcomeData | RoomMessageData | AgentTurnRequestData | PhaseChangeData | MoveResultData | RoundEndData | GameOverData | ErrorData | {
152
+ ack_seq: number;
153
+ };
154
+ export interface AgentActionData {
155
+ request_id: string;
156
+ action: {
157
+ name: MafiaActionName;
158
+ params: Record<string, unknown>;
159
+ };
160
+ }
161
+ /** Wire-safe subset of emocentric's AgentResult — we accept anything JSON. */
162
+ export interface AgentTelemetryData {
163
+ request_id: string;
164
+ context_event_id: string;
165
+ agent_result: Record<string, unknown>;
166
+ }
167
+ export interface AgentHeartbeatData {
168
+ process_ms_since_start: number;
169
+ queue_depth: number;
170
+ }
171
+ export interface ChatSendData {
172
+ text: string;
173
+ }
174
+ export interface MoveSubmitData {
175
+ kind: MoveKind;
176
+ params: Record<string, unknown>;
177
+ }
178
+ export interface ClientFrame<TData = unknown> {
179
+ v: typeof PROTOCOL_VERSION;
180
+ type: string;
181
+ seq: number;
182
+ at?: string;
183
+ data: TData;
184
+ }
185
+ export type PersonaClientFrame = (ClientFrame<AgentActionData> & {
186
+ type: "agent.action";
187
+ }) | (ClientFrame<AgentTelemetryData> & {
188
+ type: "agent.telemetry";
189
+ }) | (ClientFrame<AgentHeartbeatData> & {
190
+ type: "agent.heartbeat";
191
+ });
192
+ export type HumanClientFrame = (ClientFrame<ChatSendData> & {
193
+ type: "chat.send";
194
+ }) | (ClientFrame<MoveSubmitData> & {
195
+ type: "move.submit";
196
+ });
197
+ export type AnyClientFrame = PersonaClientFrame | HumanClientFrame;
198
+ /** Which S→C frame types the SSE monitor stream forwards. */
199
+ export declare const MONITOR_STREAM_EVENTS: readonly ["room.message", "phase.change", "move.result", "round.end", "game.over", "agent.telemetry"];
200
+ export declare function allowedActionsFor(phase: PhaseName, role: SeatRole): MafiaActionName[];
201
+ export declare function isDeadlineForPhase(phase: PhaseName, config: PhaseConfig): number;
202
+ export declare function nextPhaseName(current: PhaseName): PhaseName;
203
+ export declare function phaseDurationMs(phase: PhaseName, config: PhaseConfig): number;
204
+ export declare function defaultFallbackAction(allowed: readonly MafiaActionName[]): MafiaActionName;
package/dist/index.js ADDED
@@ -0,0 +1,107 @@
1
+ export const PROTOCOL_VERSION = 1;
2
+ export const DEFAULT_PHASE_CONFIG = {
3
+ night_0_ms: 20_000,
4
+ day_discussion_ms: 180_000,
5
+ day_vote_ms: 60_000,
6
+ night_ms: 45_000,
7
+ reveal_ms: 15_000,
8
+ };
9
+ export const ALLOWED_ACTIONS_BY_PHASE = {
10
+ LOBBY: { town: [], mafia: [], detective: [] },
11
+ NIGHT_0: { town: [], mafia: ["whisper_to_mafia"], detective: [] },
12
+ DAY_DISCUSSION: {
13
+ town: ["post_message", "stay_quiet"],
14
+ mafia: ["post_message", "stay_quiet"],
15
+ detective: ["post_message", "stay_quiet"],
16
+ },
17
+ DAY_VOTE: {
18
+ town: ["vote_for", "abstain"],
19
+ mafia: ["vote_for", "abstain"],
20
+ detective: ["vote_for", "abstain"],
21
+ },
22
+ NIGHT: {
23
+ town: ["stay_quiet"],
24
+ mafia: ["whisper_to_mafia", "nominate_kill"],
25
+ detective: ["investigate"],
26
+ },
27
+ REVEAL: {
28
+ town: ["post_message", "stay_quiet"],
29
+ mafia: ["post_message", "stay_quiet"],
30
+ detective: ["post_message", "stay_quiet"],
31
+ },
32
+ GAME_OVER: { town: [], mafia: [], detective: [] },
33
+ };
34
+ // ============================================================================
35
+ // Helpers
36
+ // ============================================================================
37
+ /** Which S→C frame types the SSE monitor stream forwards. */
38
+ export const MONITOR_STREAM_EVENTS = [
39
+ "room.message",
40
+ "phase.change",
41
+ "move.result",
42
+ "round.end",
43
+ "game.over",
44
+ "agent.telemetry",
45
+ ];
46
+ export function allowedActionsFor(phase, role) {
47
+ return ALLOWED_ACTIONS_BY_PHASE[phase][role] ?? [];
48
+ }
49
+ export function isDeadlineForPhase(phase, config) {
50
+ switch (phase) {
51
+ case "DAY_DISCUSSION":
52
+ return 15_000;
53
+ case "DAY_VOTE":
54
+ return 10_000;
55
+ case "NIGHT":
56
+ return 8_000;
57
+ case "REVEAL":
58
+ return 8_000;
59
+ case "NIGHT_0":
60
+ return 8_000;
61
+ default:
62
+ return 5_000;
63
+ }
64
+ }
65
+ export function nextPhaseName(current) {
66
+ switch (current) {
67
+ case "LOBBY":
68
+ return "NIGHT_0";
69
+ case "NIGHT_0":
70
+ return "DAY_DISCUSSION";
71
+ case "DAY_DISCUSSION":
72
+ return "DAY_VOTE";
73
+ case "DAY_VOTE":
74
+ return "REVEAL";
75
+ case "REVEAL":
76
+ return "NIGHT";
77
+ case "NIGHT":
78
+ return "DAY_DISCUSSION";
79
+ case "GAME_OVER":
80
+ return "GAME_OVER";
81
+ }
82
+ }
83
+ export function phaseDurationMs(phase, config) {
84
+ switch (phase) {
85
+ case "NIGHT_0":
86
+ return config.night_0_ms;
87
+ case "DAY_DISCUSSION":
88
+ return config.day_discussion_ms;
89
+ case "DAY_VOTE":
90
+ return config.day_vote_ms;
91
+ case "NIGHT":
92
+ return config.night_ms;
93
+ case "REVEAL":
94
+ return config.reveal_ms;
95
+ case "LOBBY":
96
+ case "GAME_OVER":
97
+ return 0;
98
+ }
99
+ }
100
+ export function defaultFallbackAction(allowed) {
101
+ if (allowed.includes("stay_quiet"))
102
+ return "stay_quiet";
103
+ if (allowed.includes("abstain"))
104
+ return "abstain";
105
+ return allowed[0] ?? "stay_quiet";
106
+ }
107
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "mafia-studio-protocol",
3
+ "version": "0.1.0",
4
+ "description": "Shared wire protocol types (frames, phases, action names) for MafiaStudio realtime and its team adapters.",
5
+ "keywords": ["mafia", "mafiastudio", "protocol", "websocket", "types"],
6
+ "license": "MIT",
7
+ "homepage": "https://github.com/The-Resonance-Lab/MafiaStudio",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/The-Resonance-Lab/MafiaStudio.git",
11
+ "directory": "packages/protocol"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/The-Resonance-Lab/MafiaStudio/issues"
15
+ },
16
+ "type": "module",
17
+ "main": "./dist/index.js",
18
+ "types": "./dist/index.d.ts",
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/index.d.ts",
22
+ "import": "./dist/index.js"
23
+ }
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "README.md",
28
+ "LICENSE"
29
+ ],
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "scripts": {
34
+ "build": "tsc -p tsconfig.json",
35
+ "typecheck": "tsc -p tsconfig.json --noEmit",
36
+ "prepublishOnly": "npm run build"
37
+ },
38
+ "devDependencies": {
39
+ "typescript": "5.6.3"
40
+ }
41
+ }