用 GamePlugin 契约接入新玩法
平台 Shell 负责房间、身份、实时同步、计时与错误边界;玩法全部封装在插件里。新增一款游戏 = 实现下面的契约 + 在 registry 注册,无需改动任何页面。
已注册插件
🎲 骰王争霸
五颗骰子,全场比大小
ID:dice-king
版本:1.0.0
人数:1-50
作者:聚场 PlayHub
阶段:LOBBY → COUNTDOWN → ROLLING → ROUND_RESULT → FINAL_RESULT
第一步:实现契约
export interface GamePlugin<TState = Record<string, unknown>> {
metadata: PluginMetadata; // id / 名称 / 人数范围 / 版本
defaultConfig: PluginConfig; // totalRounds、roundSeconds 等
validateConfig(c): ValidationResult;
initialState: TState;
phases: Phase[]; // LOBBY / COUNTDOWN / ROLLING / ...
handleCommand(cmd, room): Promise<void>; // 只发意图,裁决在服务端
reduceEvent(state, event): TState; // 事件信封 -> 本地视图状态
score(snapshot): LeaderboardEntry[]; // 从权威快照派生排行榜
ScreenComponent: ComponentType<PluginViewProps>; // 大屏
PlayerComponent: ComponentType<PluginViewProps>; // 手机
HostControlPanel: ComponentType<PluginViewProps>; // 主持
RulesComponent: ComponentType; // 规则
}第二步:最小插件示例并注册
// src/lib/game/quick-tap/index.tsx
import type { GamePlugin } from "@/lib/game/contract";
export const quickTapPlugin: GamePlugin = {
metadata: {
id: "quick-tap", name: "极速点击", tagline: "10 秒手速对决",
description: "限时内点击次数最多者胜。", icon: "⚡", version: "1.0.0",
minPlayers: 2, maxPlayers: 50, author: "PlayHub",
},
defaultConfig: { totalRounds: 3, roundSeconds: 10 },
validateConfig: (c) => ({
ok: (c.totalRounds ?? 3) >= 1 && (c.roundSeconds ?? 10) >= 10,
errors: [],
}),
initialState: {},
phases: ["LOBBY", "COUNTDOWN", "ROLLING", "ROUND_RESULT", "FINAL_RESULT"],
async handleCommand(cmd, room) {
if (cmd.type === "TAP") await room.actions.roll();
},
reduceEvent: (s) => s,
score: () => [],
ScreenComponent: () => <div>大屏视图</div>,
PlayerComponent: () => <div>手机视图</div>,
HostControlPanel: () => <div>主持面板</div>,
RulesComponent: () => <div>规则</div>,
};
// src/lib/game/registry.ts
registerPlugin(quickTapPlugin);约定
- · 客户端只发送意图命令,随机数、阶段推进与计分一律由服务端函数裁决。
- · 玩家动作必须带 commandId 幂等号,重复提交返回同一结果。
- · 所有视图都从权威快照 RoomSnapshot 派生,禁止本地私自改分。
- · 事件信封统一为 { type, version, serverTimestamp, actorId, payload }。