├── .gitmodules
├── design
├── interface.md
├── lobbyapi.md
├── roomapi.md
├── rules.md
└── url.md
├── etc
└── config
├── lualib
├── json.lua
├── log.lua
├── objproxy.lua
├── rule.lua
└── staticfile.lua
├── service
├── broker.lua
├── lobby.lua
├── main.lua
├── room.lua
├── roomkeeper.lua
└── userid.lua
├── start.sh
└── static
├── avalon.css
├── game.js
├── index.html
├── lob.js
├── room.html
├── room.js
└── simple_lib.js
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "skynet"]
2 | path = skynet
3 | url = https://github.com/cloudwu/skynet.git
4 |
--------------------------------------------------------------------------------
/design/interface.md:
--------------------------------------------------------------------------------
1 | 大厅界面只有两个按钮,创建新房间和进入一个房间。
2 | 选择创建房间后,就跳转到房间创建界面。
3 |
4 | 进入房间其实就是访问一个 http://xxx/123456 的地址。
5 | 选中进入房间后,显露出一行输入房间号的位置,点继续跳转。
6 |
7 |
8 | +-------------------------------------+ +-------------------------------------+
9 | | 用户名字 | | |
10 | | | | |
11 | | | | |
12 | | | | |
13 | | 我是标题 | | 我是标题 |
14 | | | | |
15 | | | | |
16 | | +----------------------+ | | +......................+ |
17 | | | | | | | | |
18 | | | 创建房间 | | | | 创建房间(淡色) | |
19 | | | | | | | | |
20 | | +----------------------+ | ------+ | +......................+ |
21 | | | | |
22 | | +----------------------+ | | +----------------------+ |
23 | | |......................| | | | | |
24 | | |.....进入房间..(按下).| | | | 进入房间 | |
25 | | |......................| | | | | |
26 | | +----------------------+ | | +----------------------+ |
27 | | | | |
28 | | | | http://xxx/[房间号] [继续] |
29 | | | | |
30 | | | | |
31 | | | | |
32 | | | | |
33 | | | | |
34 | +-------------------------------------+ +-------------------------------------+
35 | | |
36 | | 选创建房间 |
37 | v |
38 | +-------------------------------------+ |
39 | | | 选进入房间(继续) |
40 | | 123456 号 6 人房间 | +------------------------+
41 | | | 或直接用房间 url 访问
42 | | * 某某(颜色) * 某某(颜色) |
43 | | o 某某(颜色) * 某某(颜色) |
44 | | * 某某(颜色) * 某某(颜色) |
45 | | X 某某(颜色) |
46 | | |
47 | | 5 人准备好 |
48 | | 1 人正在准备 |
49 | | 1 人旁观 |
50 | | |
51 | | |
52 | | |
53 | | +--------------------------+ |
54 | | | 房间可选规则 (x) | |
55 | | | 房间可选规则 ( ) | |
56 | | | 房间可选规则 (x) | |
57 | | | | |
58 | | | | |
59 | | | | |
60 | | +--------------------------+ |
61 | | [名字 ] [开始] |
62 | | |
63 | | |
64 | +-------------------------------------+
65 |
66 |
67 | 进入房间后,会有一个默认名字(或上次用过记在 cookie 里的)。点开始表示准备好了。
68 | 界面的上方显示房间的号码。
69 |
70 | 上半屏分两列显示房间里的人,每个人名前有一个标记表示是否准备好。
71 | 任何人都可以把其他人屏蔽掉,也就是在名字前打 X ,表示这个人不必计算在游戏人数内。
72 | 被 X 的人自动变成未准备好状态,重新选开始可以去掉这个标记变成准备好的状态。
73 |
74 | 下半屏有可以更改的可选规则。所有人,不只创建者都可以修改。创建者只是第一个进入房间的人。
75 |
76 | 开始按钮前有输入框可以修改名字。
77 |
78 | 颜色是系统分配好的固定颜色。修改名字不影响颜色以及准备状态。但修改规则会导致所有用户的
79 | 准备状态复位。
80 |
81 | 开始开始
82 | ========
83 | 顶部下拉一个框显示自己的身份,以及这个身份可以看见的人.
84 |
85 | +---------------------------------------+
86 | | |
87 | | 身份名 |
88 | | |
89 | | 若干行的说明 |
90 | | |
91 | | |
92 | | |
93 | | 你看见了 |
94 | | |
95 | | 用户 1 |
96 | | 用户 2 |
97 | | |
98 | +-----------+----------+----------------+
99 | | | 身份 | |
100 | | +----------+ |
101 | | |
102 | | |
103 | | |
104 | | |
105 | | |
106 | | |
107 | | |
108 | | |
109 | | |
110 | | |
111 | | |
112 | +---------------------------------------+
113 |
114 | 点屏幕任何位置,身份卡向上收起,只在顶部留下一个小标签。点击标签会再次下拉展示身份信息。
115 |
116 | 提案过程
117 | ========
118 |
119 | +-----------+----------+----------------+ +-----------+----------+----------------+
120 | | | 身份 | | | | 身份 | |
121 | | +----------+ | | +----------+ |
122 | | == 提案阶段 == | | == 提案阶段 == |
123 | | | | |
124 | | 任务 X 第 2 个提案: [提案] | | 任务 X : 请选出 3 人 |
125 | | | | |
126 | | +-----------------------------+ | | +-----------------------------+ |
127 | | | 用户1 用户2 | | | | 用户1 用户2 | |
128 | | | 用户3 用户4 | | | | x 用户3 用户4 | |
129 | | | 用户5 用户6 | | | | 用户5 用户6 | |
130 | | | 用户7 用户8 | | | | x 用户7 x 用户8 | |
131 | | | 用户9 用户10 | | | | 用户9 用户10 | |
132 | | +-----------------------------+ | | +-----------------------------+ |
133 | | | | +-------+ +-------+ |
134 | | | | | 取消 | | 提议 | |
135 | | | --+| +-------+ +-------+ |
136 | | | | |
137 | | | | |
138 | | | | |
139 | | | | |
140 | | | | |
141 | | | | |
142 | | | | |
143 | | | | |
144 | | | | |
145 | | | | |
146 | | | | |
147 | | | | |
148 | | | | |
149 | | | | |
150 | | | | |
151 | | | | |
152 | | | | |
153 | +---------------------------------------+ +---------------------------------------+
154 | |
155 | v
156 | +-----------+----------+----------------+ +-----------+----------+----------------+
157 | | | 身份 | | | | 身份 | |
158 | | +----------+ | | +----------+ |
159 | | == 投票阶段 == | | == 任务阶段 == |
160 | | | | |
161 | | 任务 X : 用户3 队长 [修改] | | 任务 X : 进行中 |
162 | | | | |
163 | | +-----------------------------+ | | +-----------------------------+ |
164 | | | * 用户1 用户2 | | | | | |
165 | | | [用户3] * 用户4 | | | | [用户3] | |
166 | | | * 用户5 用户6 | | | | | |
167 | | | [用户7] [用户8] | | | | [用户7] [用户8] | |
168 | | | * 用户9 * 用户10 | | | | | |
169 | | +-----------------------------+ | | +-----------------------------+ |
170 | | +-------+ +-------+ | | +-------+ +-------+ |
171 | | | 赞成 | | 反对 | | | | 成功 | | 失败 | |
172 | | +-------+ +-------+ | | +-------+ +-------+ |
173 | | | | |
174 | | | | |
175 | | | | |
176 | | | | |
177 | | | | |
178 | | | | |
179 | | | | |
180 | | | | |
181 | | | | |
182 | | | | |
183 | | | | |
184 | | | | |
185 | | | | |
186 | | | | |
187 | | | | |
188 | | | | |
189 | | | | |
190 | +---------------------------------------+ +---------------------------------------+
191 |
192 | 在没有任何人的提案阶段,界面显示当前是第几个任务。
193 | 提示条的右侧有一个按钮。如果选择,则可以由自己提案。
194 |
195 | 下方是最多 10 人两列的选择框,可以选出人选参加任务。
196 |
197 | 选择的人选在前面打勾。同时,应该监视服务器是否有别的玩家的提案。如果有,
198 | 应该刷新当前队伍状态。别人的提案将用用户所在区块的背景改变来区分。在提案过程中,
199 | 可以在有其他人提案的时候,了解情况。
200 |
201 | 如果点取消,则退出提案状态。如果点提议,则将方案提交服务器。
202 |
203 | 一旦有一个合法提案,就进入投票阶段。这个时候,名单中没有选择勾, 用背景色区分那些人被选中。
204 | 上方提示条会提示方案是由谁提出的。同样在右侧有一个修改提议的按钮。
205 |
206 | 用户可以通过名单框下方的赞同反对投票。所有对当前提案投过票的用户的名字前面都会有一个标记。
207 |
208 | 一旦议案通过, 进入任务阶段。参于任务的玩家可以看见任务成功/失败的选择。其他玩家等待。
209 | 所有玩家都可以看见参与玩家是否有选择。
210 |
211 | 历史记录
212 | =========
213 |
214 | 界面的下方是游戏历程的历史记录。最后发生的事情显示在最上方,用倒序显示历史。
215 |
216 | +-----------+----------+----------------+
217 | | | 身份 | |
218 | | +----------+ |
219 | | == 提案阶段 == |
220 | | |
221 | | 任务 4 第 2 个提案: [提案] |
222 | | |
223 | | +-----------------------------+ |
224 | | | 用户1 用户2 | |
225 | | | 用户3 用户4 | |
226 | | | 用户5 用户6 | |
227 | | | 用户7 用户8 | |
228 | | | 用户9 用户10 | |
229 | | +-----------------------------+ |
230 | | |
231 | | ---------------------------------- |
232 | | 4.1 用户3 的提案[用户列表]未通过, |
233 | | 赞同者:用户1 |
234 | | 3.3 任务成功,参与者:用户1, 用户2 |
235 | | 用户2 的提案成功,反对者:无 |
236 | | ... |
237 | | 2.1 任务失败,参与者:用户1, 用户2 |
238 | | 出现 2 张失败票 |
239 | | 用户1 的提案[用户列表]通过, |
240 | | 反对者:用户 5 |
241 | | |
242 | | |
243 | | |
244 | | |
245 | | |
246 | | |
247 | | |
248 | | |
249 | | |
250 | | |
251 | +---------------------------------------+
252 |
253 |
--------------------------------------------------------------------------------
/design/lobbyapi.md:
--------------------------------------------------------------------------------
1 | URL
2 | ====
3 |
4 | 用户通过 / 访问大厅。
5 |
6 | 所有和房间通讯的 xmlhttprequest 请求都发到 /lobby 这个 url 上。
7 |
8 | Cookie
9 | ====
10 |
11 | 每个用户都带有一个 unique userid 。cookie 名为 userid 。
12 | 如果用户没有 userid 的 cookie ,服务器主动生成一个设为 cookie 。
13 |
14 | API
15 | ====
16 | 通过向 /lobby 发送请求。请求必须使用 x-www-form-urlencoded 格式,Content-Type 可以不填(会被忽略)。
17 |
18 | 必须有一个 action 字段,表示发起的动作,它可以是:
19 |
20 | * getname : 获取名字。可以用来获取自己的用户名。
21 | * setname : 设置名字。可以用来修改自己的用户名。需要额外字段 username 。
22 | * create : 创建一个新房间。
23 | * join : 进入一个房间。需要额外字段 roomid 。
24 |
25 | 服务器返回 json 格式。status 字段表示状态,可以是
26 |
27 | * ok : 表示操作成功。另外有字段 username 表示当前用户名。
28 | * error : 表示操作失败,还会有一个额外的 error 字段描述具体信息。
29 | * join : 表示应该进入一个房间, 还会有一个额外的字段 room 表示房间号(一个整数)。
30 |
--------------------------------------------------------------------------------
/design/roomapi.md:
--------------------------------------------------------------------------------
1 | URL
2 | ====
3 |
4 | 用户访问的房间 URL 统一是 /123456 的形式,就是 / 加数字。
5 |
6 | 所有和房间通讯的 xmlhttprequest 请求都发到 /room 这个 url 上。
7 |
8 | Cookie
9 | ====
10 |
11 | 每个用户都带有一个 unique userid 。cookie 名为 userid 。
12 | 如果用户没有 userid 的 cookie ,服务器主动生成一个设为 cookie 。
13 |
14 | 状态
15 | ====
16 | 房间有多个状态,不同的状态对应不同的页面。但都是通过 /123456 这样形式的 URL 访问到的。
17 |
18 | * 准备状态
19 | * 游戏状态
20 |
21 | 下面先讨论准备状态
22 |
23 | 准备状态
24 | ====
25 | 通过向 /room 发送 json 请求,可以做准备状态的交互。每个请求都必须有几个必备项。
26 |
27 | * roomid: 123456
28 | * status: "prepare"
29 | * action: "操作名"
30 | * version: 当前已知的状态版本号
31 |
32 | 如果 roomid 无效,会返回错误信息。应该引导用户回到大厅界面。
33 | 如果 roomid 有效,但 status 无效或 action 无效,返回状态信息。
34 | 每次请求提交,都应该给出一个当前已知的状态版本号。0 表示获取全新版本。
35 |
36 | action 可以是这些:
37 |
38 | * setname : 更换名字。username : 名字。
39 | * ready : 准备确认。enable : true/false 。
40 | * kick : 将某个用户设为旁观。id : 用户id 。
41 | * set : 修改规则。rule : 规则编号。enable : ture/false 。注:修改规则会导致 ready 状态变化。
42 | * request : 请求当前房间状态。
43 |
44 | 服务器对请求的返回就是当前房间的状态。
45 |
46 | * status : "error" / "ok" / "prepare" / "game"
47 | * version : 当前状态的版本号
48 |
49 | * error : 出错了. error : 出错信息。
50 | * ok : 用于请求返回。表示没有状态变化。
51 | * prepare : 目前处于准备状态, 并刷新成新版本。
52 | * game : 目前处于游戏状态, 并刷新成新版本。
53 |
54 | 关于 prepare 的细节:
55 |
56 | 首先是玩家列表
57 | player : {
58 | { userid : xxxx, username : xxxx , color: xxx, status : 0/1/2 准备好,正在准备, 旁观 }
59 | { ... }
60 | }
61 |
62 | 然后是规则列表
63 | rule : [ 2,3,4,5 ]
64 | 每个数字表示一条 enable 的规则.
65 |
66 | 最后是不可以开始游戏的原因
67 | needs : "为什么还不能开始游戏"
68 |
69 | 游戏状态
70 | ======
71 |
72 | 当所有人的状态都为 0 或 2 时, (旁观或准备好). 房间有可能进入游戏状态.
73 |
74 | 进入游戏状态还需要满足几个条件:
75 |
76 | 游戏人数在 5~10 人. 规则需要匹配对应的人数. 如果规则不匹配, 或其它条件不满足.
77 | 返回的状态表中, 有一项 needs 表示了原因, 供 client 提示.
78 |
79 | 一旦条件满足, 房间状态自动进入游戏状态. status = "game"
80 |
81 | 游戏分五个阶段, 每个大阶段分多个小阶段.
82 |
83 | 小阶段有四个: 提案 , 投票 和任务 以及结束. 由于这个工具定义为辅助线下游戏,所以并不严格校验游戏规则.
84 |
85 | 任何人都可以提案,或修改提案. 任何人都可以对提案投票. 一旦投票通过, 提案中参与玩家必须进行任务. 依次循环.
86 |
87 | 在查询游戏中的房间状态时, 用一个数组下发游戏的历史进程.
88 |
89 | "history" : [
90 | {
91 | "leader" : userid, // 谁在提案
92 | "plan" : [ userid , userid , userid, ... ] ,
93 | "vote" : [userid, userid, userid, ... ] ,
94 | "result" : [true, false, true, ...] , // 可选
95 | "quest" : [userid, userid, userid, ...] ,
96 | "ending" : 几张失败票 },
97 | ...
98 | ],
99 |
100 | 每个阶段都从 plan 开始, 一旦有一个人提交了提案, 其他玩家可以继续修改提案或对提案投票.
101 | 一个 plan 需要几个 id , 由规则决定. 规则是预先写在 client 的.
102 |
103 | * 5 人局: 2/3/2/3/3
104 | * 6 人时,任务人数分别是 2/3/4/3/4
105 | * 7 人时,任务人数分别是 2/3/3/4/4
106 | * 8-10 人时,任务人数分别是 3/4/4/5/5
107 |
108 | 对于 7 人+, 第四轮是特殊轮, 需要两张失败票,任务才失败.
109 |
110 | 客户端可以校验提交的提案人数是否符合要求, 服务器会再校验.
111 |
112 | 一旦有一个人提交了提案, 在 history 里就会有体现. 只要当前阶段没有进入 quest 环节,
113 | 所有人都在 client 显示 "修改提案", 同时显示投票.
114 |
115 | 一旦有人修改提案, 未完成的 vote 将被复位, 所有人必须重新投票.
116 |
117 | 如果所有玩家都提交了投票(赞成或反对), 就会进入投票环节,不再接受修改提案和投票.
118 | 并有 result 字段表示投票结果 (对应上面的投票 userid).
119 |
120 | 如果提案通过, 则有 quest 阶段. 除了参于玩家外, 其他玩家都不能提交任务.
121 | 但可以看到那些玩家做了任务.
122 |
123 | 任务完毕后,会到 ending 阶段, 显示当前任务中有几票失败票.
124 |
125 | 一个房间状态的例子:
126 |
127 |
128 | "history" : [
129 | {
130 | "leader" : id1, // 提案由 id1 提出
131 | "vote" : [id1, id2 ,id3 , ...],
132 | "result", [true, false, false, ...], // 议案被否决
133 | },
134 | { "leader" : id2, // 提案由 id2 提出
135 | "vote" : [id1, id2 ,id3 , ...],
136 | "result" : [true, true, true, ...],
137 | "quest" : [id2, id3, ...],
138 | "ending" : 0, // 成功
139 | },
140 | { "leader" : id3,
141 | "plan" :[id1, id2, id3, id4],
142 | "vote" : [id5, ...], // 还在投票中, 尚未达成一致.
143 | }
144 | ]
145 |
146 |
147 | 在游戏过程中,玩家可以提的操作有:
148 |
149 | * plan : 提出一个提案, "list" : [id1, id2, ...]
150 | * vote : 对当前提案投票. "ticket" : true/false
151 | * quest : 进行一个任务, "ticket" : true/false
152 | * list : 请求用户列表, 以及个人的身份, 还有其它额外的信息.
153 |
154 | 如果是发送 list, 那么将返回一个当前用户列表.
155 |
156 | {
157 | "player" : [ { "userid" : id , "username" : "name", "color" : "#ffffff" }, ... ],
158 | "identity" : { "name" : 名称 , "desc" : "描述串" },
159 | "information" : [ { "userid" : "身份" } , ... ] // 可以看见的 userid 的身份.
160 | }
--------------------------------------------------------------------------------
/design/rules.md:
--------------------------------------------------------------------------------
1 | 游戏可选规则
2 | ============
3 |
4 | * 梅林(正)/刺客(邪) : 此条是根规则. 默认必须选 true , 一旦不选, 所有其它规则为 false 。梅林可以看见邪恶阵营里除莫德雷德外所有人。
5 | * 派西维尔(正) : 一旦选上,他可以看见梅林和莫甘娜。
6 | * 莫甘娜(邪): 可以看见除奥伯伦之外的所有邪恶阵营。
7 | * 莫德雷德(邪): 可以看见除奥伯伦之外的所有邪恶阵营。
8 | * 奥伯伦(邪): 谁也看不见。
9 | * 兰斯洛特(正邪)1. 第三任务转变:随机 5 张卡,3 张为空白,2 张为转变。在第三个任务的每个阶段做一次揭示。
10 | * 兰斯洛特(正邪)2 .每个任务转变:随机 7 张卡,其中 5 张为空白,2 张为转变。选前 5 张,一开始全部揭示。
11 | * 兰斯洛特(正邪)3.:相互可见。
12 | * 王者之剑。
13 |
14 | 兰斯洛特规则只能三选一,如果选了 3 ,那么正邪兰斯洛特相互可见。
15 |
16 | 所有角色如下:
17 |
18 | 正义:梅林、派西维尔、兰斯洛特(正)、其他圆桌骑士。
19 |
20 | 邪恶:莫德雷德、莫甘娜、奥伯伦、刺客、兰斯洛特(邪)、其他走狗。
21 |
22 | 邪恶方,奥伯伦和兰斯洛特看不见任何同伴。其他人相互看见,(在前两个选项中)并可以看见兰斯洛特(邪);
23 | 或在第三个选项中,兰斯洛特(邪)可以看见奥伯伦之外的所有邪恶阵营。
24 |
25 | 分配角色时,先按总人数查下表:
26 |
27 | 人数 | 邪恶 | 正义
28 | -----|------|-----
29 | 5 | 2 | 3
30 | 6 | 2 | 4
31 | 7 | 3 | 4
32 | 8 | 3 | 5
33 | 9 | 4 | 5
34 | 10 | 4 | 6
35 |
36 | 如果邪恶方选的特殊角色数量超过了人数,则游戏不能开始。
37 |
38 | 在 兰斯洛特 2 规则中,兰斯洛特(正)必须在任务中投成功票;
39 | 兰斯洛特(邪)必须在任务中投失败票。
40 |
41 | 除兰斯洛特外的正义角色,参加任务都必须投成功票。
42 |
43 |
44 | ------
45 |
46 | 角色分配
47 | ========
48 |
49 | 当所有玩家选定规则以及参加人数后, 进入角色分配阶段.
50 |
51 | 梅林和刺客是一起上场的,属于同一条规则。且如果没有这条规则,其它规则都不可选。
52 | 其它规则一共有 4 加 3 选 1 条(王者之剑暂时不实现)。
53 |
54 | 兰斯洛特(正)(邪) 是一起上场的, 属于同一条规则。有三个变体,只可以选其中之一,或不选择。
55 |
56 | 当正方特殊角色不够时,应加入若干正派无特殊身份角色,直到数量达到需要的人数。如果特殊角色数量超过人数,应该提示失败,重新选规则。
57 |
58 | 当反方特殊角色不够时,应该加入若干邪恶无特殊身份角色,直到数量达到需要的人数。如果特殊角色数量超过人数,应该提示失败,重新选规则。
59 |
60 | 身份随机分配完毕后,可以查下表来决定每个角色可以看到其它哪些角色。表中“正”和“邪”两类角色都有可能不只一人。查到自己所属角色行时,
61 | 同一行 x 表示不可见,V 表示可见。? 表示只在兰斯洛特3号可选规则时才可见,否则不可见。
62 |
63 |
64 | | 梅林 | 派西维尔 | 兰斯洛特(正) | 正 | 刺客 | 莫德雷德 | 莫甘娜 | 兰斯洛特(邪) | 奥伯伦 | 邪
65 | -------------|------|----------|--------------|----|------|----------|--------|--------------|--------|----
66 | 梅林 | . | x | x | x | V | x | V | V | V | V
67 | 派西维尔 | V | . | x | x | x | x | V | x | x | x
68 | 兰斯洛特(正) | x | x | . | x | x | x | x | ? | x | x
69 | 正 | x | x | x | . | x | x | x | x | x | x
70 | 刺客 | x | x | x | x | . | V | V | V | x | V
71 | 莫德雷德 | x | x | x | x | V | . | V | V | x | V
72 | 莫甘娜 | x | x | x | x | V | V | . | V | x | V
73 | 兰斯洛特(邪) | x | x | ? | x | ? | ? | ? | ? | x | ?
74 | 奥伯伦 | x | x | x | x | x | x | x | x | . | x
75 | 邪 | x | x | x | x | V | V | V | V | x | V
76 |
77 |
78 | ```lua
79 | --[[ 规则列表
80 | "梅林", -- 1
81 | "派西维尔", -- 2
82 | "莫甘娜", -- 3
83 | "莫德雷德", -- 4
84 | "奥伯伦", -- 5
85 | "兰斯洛特1", -- 6
86 | "兰斯洛特2", -- 7
87 | "兰斯洛特3", -- 8
88 | ]]
89 |
90 | local role = {
91 | "梅林(正)", -- 1
92 | "派西维尔(正)", -- 2
93 | "兰斯洛特(正)", --3
94 | "圆桌骑士(正)", --4
95 | "刺客(邪)", -- 5
96 | "莫德雷德(邪)", -- 6
97 | "莫甘娜(邪)", -- 7
98 | "兰斯洛特(邪)", --8
99 | "奥伯伦(邪)", -- 9
100 | "爪牙(邪)", -- 10
101 | }
102 |
103 | local visible = {
104 | -- 梅林 派西维尔 兰(正) 正 刺客 莫德雷德 莫甘娜 兰(邪) 奥伯伦 邪
105 | { false, false, false, false, true, false, true, true, true, true }, --梅林
106 | { true, false, false, false, true, false, true, false, false, false }, --派西维尔
107 | { false, false, false, false, false, false, false, 3, false, false }, --兰(正)
108 | { false, false, false, false, false, false, false, false, false, false }, --正
109 | { false, false, false, false, false, true, true, true, false, true }, --刺客
110 | { false, false, false, false, true, false, true, true, false, true }, --莫德雷德
111 | { false, false, false, false, true, true, false, true, false, true }, --莫甘娜
112 | { false, false, 3, false, 3, 3, 3, false, false, 3 }, --兰(邪)
113 | { false, false, false, false, false, false, false, false, false, false }, --奥伯伦
114 | { false, false, false, false, false, true, true, true, false, true }, --刺客
115 | }
116 |
117 | local camp_good = {
118 | 0,0,0,0, -- can't below 4
119 | 2, -- 5
120 | 2, -- 6
121 | 3, -- 7
122 | 3, -- 8
123 | 4, -- 9
124 | 4, -- 10
125 | }
126 |
127 | local function randomrole(roles)
128 | local n = #roles
129 | for i=1, n-1 do
130 | local c = math.random(i,n)
131 | roles[i],roles[c] = roles[c],roles[i]
132 | end
133 | return roles
134 | end
135 |
136 | -- 本函数会返回一个 table , 包含有所有参于的角色;或返回出错信息。
137 | function checkrules(rules, n)
138 | if n <5 or n>10 then
139 | return false, "游戏人数必须在 5 到 10 人之间"
140 | end
141 | if not rules[1] then
142 | for i=2,8 do
143 | if rules[i] then
144 | return false, "当去掉梅林时,不可以选择其他角色"
145 | end
146 | end
147 | local ret = {}
148 | for i=1,camp_good[n] do
149 | table.insert(ret, 4)
150 | end
151 | for i=1,n-camp_good[n] do
152 | table.insert(ret, 10)
153 | end
154 | return randomrole(ret)
155 | end
156 | local lancelot = 0
157 | for i=6,8 do
158 | if rules[i] then
159 | lancelot = lancelot + 1
160 | end
161 | end
162 | if lancelot > 1 then
163 | return false,"请从兰斯洛特规则里选择其中一个,或则不选"
164 | end
165 | local ret = {1,3,5,8}
166 |
167 | local good = 1 -- 梅林
168 | local evil = 1 -- 刺客
169 | if rules[2] then
170 | good = good + 1 --派西维尔
171 | table.insert(ret,2)
172 | end
173 | if lancelot == 1 then
174 | good = good + 1
175 | evil = evil + 1
176 | table.insert(ret,3)
177 | table.insert(ret,8)
178 | end
179 | if rules[3] then -- 莫甘娜
180 | evil = evil + 1
181 | table.insert(ret, 7)
182 | end
183 | if rules[4] then
184 | evil = evil + 1 -- 莫德雷德
185 | table.insert(ret, 6)
186 | end
187 | if rules[5] then
188 | evil = evil + 1 -- 奥伯伦
189 | table.insert(ret, 9)
190 | end
191 | if good > camp_good[n] then
192 | return false, "好人身份太多"
193 | end
194 | if evil > n-camp_good[n] then
195 | return false, "坏人身份太多"
196 | end
197 | for i = 1,camp_good[n] - good do
198 | table.insert(ret, 4)
199 | end
200 | for i = 1,n-camp_good[n] - evil do
201 | table.insert(ret, 10)
202 | end
203 | return randomrole(ret)
204 | end
205 | ```
206 |
--------------------------------------------------------------------------------
/design/url.md:
--------------------------------------------------------------------------------
1 | * / 大厅。显示主界面。
2 | * /123456 数字全部是房间。
3 | * /lobby 用于大厅的 ajax 通讯。
4 | * /room 用于房间的 ajax 通讯。
5 |
6 |
--------------------------------------------------------------------------------
/etc/config:
--------------------------------------------------------------------------------
1 | thread = 8
2 | --logger = "log/main.log"
3 | logpath = "log"
4 | harbor = 0
5 | start = "main"
6 | bootstrap = "snlua bootstrap" -- The service for bootstrap
7 | luaservice = "skynet/service/?.lua;service/?.lua"
8 | lualoader = "skynet/lualib/loader.lua"
9 | cpath = "skynet/cservice/?.so"
10 | -- daemon = "etc/skynet.pid"
11 | lua_path = "lualib/?.lua;skynet/lualib/?.lua"
12 | lua_cpath = "skynet/luaclib/?.so"
13 |
14 | listen = "$AVALON_LISTEN"
15 | broker = $AVALON_BROKER
16 | static_path = "$AVALON_STATIC"
17 |
--------------------------------------------------------------------------------
/lualib/json.lua:
--------------------------------------------------------------------------------
1 | -- Module options:
2 | local always_try_using_lpeg = true
3 | local register_global_module_table = false
4 | local global_module_name = 'json'
5 |
6 | --[==[
7 |
8 | David Kolf's JSON module for Lua 5.1/5.2
9 | ========================================
10 |
11 | *Version 2.4*
12 |
13 | In the default configuration this module writes no global values, not even
14 | the module table. Import it using
15 |
16 | json = require ("dkjson")
17 |
18 | In environments where `require` or a similiar function are not available
19 | and you cannot receive the return value of the module, you can set the
20 | option `register_global_module_table` to `true`. The module table will
21 | then be saved in the global variable with the name given by the option
22 | `global_module_name`.
23 |
24 | Exported functions and values:
25 |
26 | `json.encode (object [, state])`
27 | --------------------------------
28 |
29 | Create a string representing the object. `Object` can be a table,
30 | a string, a number, a boolean, `nil`, `json.null` or any object with
31 | a function `__tojson` in its metatable. A table can only use strings
32 | and numbers as keys and its values have to be valid objects as
33 | well. It raises an error for any invalid data types or reference
34 | cycles.
35 |
36 | `state` is an optional table with the following fields:
37 |
38 | - `indent`
39 | When `indent` (a boolean) is set, the created string will contain
40 | newlines and indentations. Otherwise it will be one long line.
41 | - `keyorder`
42 | `keyorder` is an array to specify the ordering of keys in the
43 | encoded output. If an object has keys which are not in this array
44 | they are written after the sorted keys.
45 | - `level`
46 | This is the initial level of indentation used when `indent` is
47 | set. For each level two spaces are added. When absent it is set
48 | to 0.
49 | - `buffer`
50 | `buffer` is an array to store the strings for the result so they
51 | can be concatenated at once. When it isn't given, the encode
52 | function will create it temporary and will return the
53 | concatenated result.
54 | - `bufferlen`
55 | When `bufferlen` is set, it has to be the index of the last
56 | element of `buffer`.
57 | - `tables`
58 | `tables` is a set to detect reference cycles. It is created
59 | temporary when absent. Every table that is currently processed
60 | is used as key, the value is `true`.
61 |
62 | When `state.buffer` was set, the return value will be `true` on
63 | success. Without `state.buffer` the return value will be a string.
64 |
65 | `json.decode (string [, position [, null]])`
66 | --------------------------------------------
67 |
68 | Decode `string` starting at `position` or at 1 if `position` was
69 | omitted.
70 |
71 | `null` is an optional value to be returned for null values. The
72 | default is `nil`, but you could set it to `json.null` or any other
73 | value.
74 |
75 | The return values are the object or `nil`, the position of the next
76 | character that doesn't belong to the object, and in case of errors
77 | an error message.
78 |
79 | Two metatables are created. Every array or object that is decoded gets
80 | a metatable with the `__jsontype` field set to either `array` or
81 | `object`. If you want to provide your own metatables use the syntax
82 |
83 | json.decode (string, position, null, objectmeta, arraymeta)
84 |
85 | To prevent the assigning of metatables pass `nil`:
86 |
87 | json.decode (string, position, null, nil)
88 |
89 | `.__jsonorder`
90 | -------------------------
91 |
92 | `__jsonorder` can overwrite the `keyorder` for a specific table.
93 |
94 | `.__jsontype`
95 | ------------------------
96 |
97 | `__jsontype` can be either `"array"` or `"object"`. This value is only
98 | checked for empty tables. (The default for empty tables is `"array"`).
99 |
100 | `.__tojson (self, state)`
101 | ------------------------------------
102 |
103 | You can provide your own `__tojson` function in a metatable. In this
104 | function you can either add directly to the buffer and return true,
105 | or you can return a string. On errors nil and a message should be
106 | returned.
107 |
108 | `json.null`
109 | -----------
110 |
111 | You can use this value for setting explicit `null` values.
112 |
113 | `json.version`
114 | --------------
115 |
116 | Set to `"dkjson 2.4"`.
117 |
118 | `json.quotestring (string)`
119 | ---------------------------
120 |
121 | Quote a UTF-8 string and escape critical characters using JSON
122 | escape sequences. This function is only necessary when you build
123 | your own `__tojson` functions.
124 |
125 | `json.addnewline (state)`
126 | -------------------------
127 |
128 | When `state.indent` is set, add a newline to `state.buffer` and spaces
129 | according to `state.level`.
130 |
131 | LPeg support
132 | ------------
133 |
134 | When the local configuration variable `always_try_using_lpeg` is set,
135 | this module tries to load LPeg to replace the `decode` function. The
136 | speed increase is significant. You can get the LPeg module at
137 | .
138 | When LPeg couldn't be loaded, the pure Lua functions stay active.
139 |
140 | In case you don't want this module to require LPeg on its own,
141 | disable the option `always_try_using_lpeg` in the options section at
142 | the top of the module.
143 |
144 | In this case you can later load LPeg support using
145 |
146 | ### `json.use_lpeg ()`
147 |
148 | Require the LPeg module and replace the functions `quotestring` and
149 | and `decode` with functions that use LPeg patterns.
150 | This function returns the module table, so you can load the module
151 | using:
152 |
153 | json = require "dkjson".use_lpeg()
154 |
155 | Alternatively you can use `pcall` so the JSON module still works when
156 | LPeg isn't found.
157 |
158 | json = require "dkjson"
159 | pcall (json.use_lpeg)
160 |
161 | ### `json.using_lpeg`
162 |
163 | This variable is set to `true` when LPeg was loaded successfully.
164 |
165 | ---------------------------------------------------------------------
166 |
167 | Contact
168 | -------
169 |
170 | You can contact the author by sending an e-mail to 'david' at the
171 | domain 'dkolf.de'.
172 |
173 | ---------------------------------------------------------------------
174 |
175 | *Copyright (C) 2010-2013 David Heiko Kolf*
176 |
177 | Permission is hereby granted, free of charge, to any person obtaining
178 | a copy of this software and associated documentation files (the
179 | "Software"), to deal in the Software without restriction, including
180 | without limitation the rights to use, copy, modify, merge, publish,
181 | distribute, sublicense, and/or sell copies of the Software, and to
182 | permit persons to whom the Software is furnished to do so, subject to
183 | the following conditions:
184 |
185 | The above copyright notice and this permission notice shall be
186 | included in all copies or substantial portions of the Software.
187 |
188 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
189 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
190 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
191 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
192 | BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
193 | ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
194 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
195 | SOFTWARE.
196 |
197 |
202 |
203 |
844 |
--------------------------------------------------------------------------------
/lualib/log.lua:
--------------------------------------------------------------------------------
1 | local skynet = require "skynet"
2 | local os = os
3 | local string = string
4 | local math = math
5 |
6 | local log = {}
7 |
8 | local cache_ti
9 | local cache_str
10 | local function fmttime()
11 | local ti = math.floor(skynet.time())
12 | if ti ~= cache_ti then
13 | cache_ti = ti
14 | cache_str = os.date("%F %T",ti)
15 | end
16 | return cache_str
17 | end
18 |
19 | function log.printf(...)
20 | skynet.error(string.format("[%s] %s",fmttime(),string.format(...)))
21 | end
22 |
23 | return log
24 |
--------------------------------------------------------------------------------
/lualib/objproxy.lua:
--------------------------------------------------------------------------------
1 |
2 | local proxy = {}
3 |
4 | local function new_proxy(v, flag)
5 | if type(v) ~= "table" or getmetatable(v) == proxy then
6 | return v
7 | end
8 | local p = setmetatable({_proxyobj = {}, _proxyflag = flag or {}}, proxy)
9 | for k,v in pairs(v) do p[k] = v end
10 |
11 | return p
12 | end
13 |
14 | function proxy.__len(t)
15 | return #t._proxyobj
16 | end
17 |
18 | function proxy.__index(t, k)
19 | return t._proxyobj[k]
20 | end
21 |
22 | function proxy.__newindex(t, k, v)
23 | if t._proxyobj[k] == v then
24 | return
25 | end
26 |
27 | v = new_proxy(v, t._proxyflag)
28 | t._proxyobj[k] = v
29 |
30 | t._proxyflag[1] = true
31 | end
32 |
33 | function proxy.__ipairs(t)
34 | return ipairs(t._proxyobj)
35 | end
36 |
37 | function proxy.__pairs(t)
38 | return pairs(t._proxyobj)
39 | end
40 |
41 | local M = {}
42 |
43 | function M.new (t)
44 | return new_proxy(t)
45 | end
46 |
47 | function M.is_dirty(t)
48 | return t._proxyflag[1] == true
49 | end
50 |
51 | function M.clean(t)
52 | t._proxyflag[1] = nil
53 | end
54 |
55 | return M
56 |
--------------------------------------------------------------------------------
/lualib/rule.lua:
--------------------------------------------------------------------------------
1 | --[[ 规则列表
2 | "梅林", -- 1
3 | "派西维尔", -- 2
4 | "莫甘娜", -- 3
5 | "莫德雷德", -- 4
6 | "奥伯伦", -- 5
7 | "兰斯洛特1", -- 6
8 | "兰斯洛特2", -- 7
9 | "兰斯洛特3", -- 8
10 | ]]
11 |
12 | local M = {}
13 |
14 | math.randomseed(os.time())
15 |
16 | M.role = {
17 | "梅林(正)", -- 1
18 | "派西维尔(正)", -- 2
19 | "兰斯洛特(正)", --3
20 | "圆桌骑士(正)", --4
21 | "刺客(邪)", -- 5
22 | "莫德雷德(邪)", -- 6
23 | "莫甘娜(邪)", -- 7
24 | "兰斯洛特(邪)", --8
25 | "奥伯伦(邪)", -- 9
26 | "爪牙(邪)", -- 10
27 | }
28 |
29 | M.camp_name = {
30 | "正", "正", "正", "正", "邪", "邪", "邪", "邪","邪", "邪"
31 | }
32 |
33 | -- 4: 表示只能看见派别,不能看见身份
34 | M.visible = {
35 | -- 梅林 派西维尔 兰(正) 骑士 刺客 莫德雷德 莫甘娜 兰(邪) 奥伯伦 爪牙
36 | { false, false, false, false, true, false, true, true, true, true }, --梅林
37 | { 4, false, false, false, true, false, 4, false, false, false }, --派西维尔
38 | { false, false, false, false, false, false, false, 3, false, false }, --兰(正)
39 | { false, false, false, false, false, false, false, false, false, false }, --正
40 | { false, false, false, false, false, 4, 4, true, false, 4 }, --刺客
41 | { false, false, false, false, 4, false, 4, true, false, 4 }, --莫德雷德
42 | { false, false, false, false, 4, 4, false, true, false, 4 }, --莫甘娜
43 | { false, false, 3, false, 3, 3, 3, false, false, 3 }, --兰(邪)
44 | { false, false, false, false, false, false, false, false, false, false }, --奥伯伦
45 | { false, false, false, false, false, true, true, true, false, true }, --刺客
46 | }
47 |
48 | local camp_good = {
49 | 0,0,0,0, -- can't below 4
50 | 3, -- 5
51 | 4, -- 6
52 | 4, -- 7
53 | 5, -- 8
54 | 6, -- 9
55 | 6, -- 10
56 | }
57 |
58 | local function randomrole(roles)
59 | local n = #roles
60 | for i=1, n-1 do
61 | local c = math.random(i,n)
62 | roles[i],roles[c] = roles[c],roles[i]
63 | end
64 | return roles
65 | end
66 |
67 | -- 本函数会返回一个 table , 包含有所有参于的角色;或返回出错信息。
68 | function M.checkrules(rules, n)
69 | if n <5 or n>10 then
70 | return false, "游戏人数必须在 5 到 10 人之间"
71 | end
72 | if not rules[1] then
73 | for i=2,8 do
74 | if rules[i] then
75 | return false, "当去掉梅林时,不可以选择其他角色"
76 | end
77 | end
78 | local ret = {}
79 | for i=1,camp_good[n] do
80 | table.insert(ret, 4)
81 | end
82 | for i=1,n-camp_good[n] do
83 | table.insert(ret, 10)
84 | end
85 | return true, randomrole(ret)
86 | end
87 | local lancelot = 0
88 | for i=6,8 do
89 | if rules[i] then
90 | lancelot = lancelot + 1
91 | end
92 | end
93 | if lancelot > 1 then
94 | return false,"请从兰斯洛特规则里选择其中一个,或则不选"
95 | end
96 | local ret = {1,5}
97 |
98 | local good = 1 -- 梅林
99 | local evil = 1 -- 刺客
100 | if rules[2] then
101 | good = good + 1 --派西维尔
102 | table.insert(ret,2)
103 | end
104 | if lancelot == 1 then
105 | good = good + 1
106 | evil = evil + 1
107 | table.insert(ret,3)
108 | table.insert(ret,8)
109 | end
110 | if rules[3] then -- 莫甘娜
111 | evil = evil + 1
112 | table.insert(ret, 7)
113 | end
114 | if rules[4] then
115 | evil = evil + 1 -- 莫德雷德
116 | table.insert(ret, 6)
117 | end
118 | if rules[5] then
119 | evil = evil + 1 -- 奥伯伦
120 | table.insert(ret, 9)
121 | end
122 | if good > camp_good[n] then
123 | return false, "好人身份太多"
124 | end
125 | if evil > n-camp_good[n] then
126 | return false, "坏人身份太多"
127 | end
128 | for i = 1,camp_good[n] - good do
129 | table.insert(ret, 4)
130 | end
131 | for i = 1,n-camp_good[n] - evil do
132 | table.insert(ret, 10)
133 | end
134 |
135 | return true, randomrole(ret)
136 | end
137 |
138 | M.pass_limit = 5
139 |
140 | M.stage_per_round = {
141 | [5] = {2,3,2,3,3},
142 | [6] = {2,3,4,3,4},
143 | [7] = {2,3,3,-4,4},
144 | [8] = {3,4,4,-5,5},
145 | [9] = {3,4,4,-5,5},
146 | [10] = {3,4,4,-5,5},
147 | }
148 |
149 | M.camp_good = camp_good
150 | return M
151 |
--------------------------------------------------------------------------------
/lualib/staticfile.lua:
--------------------------------------------------------------------------------
1 | local skynet = require "skynet"
2 | local io = io
3 | local root = skynet.getenv "static_path" or "./static/"
4 |
5 | local cache = setmetatable({}, { __mode = "kv" })
6 |
7 | local function cachefile(_, filename)
8 | local v = cache[filename]
9 | if v then
10 | return v[1]
11 | end
12 | local f = io.open (root .. filename)
13 | if f then
14 | local content = f:read "a"
15 | f:close()
16 | cache[filename] = { content }
17 | return content
18 | else
19 | cache[filename] = {}
20 | end
21 | end
22 |
23 | local staticfile = setmetatable({}, {__index = cachefile })
24 |
25 | return staticfile
26 |
--------------------------------------------------------------------------------
/service/broker.lua:
--------------------------------------------------------------------------------
1 | local skynet = require "skynet"
2 | local socket = require "socket"
3 | local httpd = require "http.httpd"
4 | local sockethelper = require "http.sockethelper"
5 | local urllib = require "http.url"
6 | local log = require "log"
7 | local staticfile = require "staticfile"
8 | local json = require"json"
9 |
10 | local roomkeeper
11 | local userservice
12 | local lobby
13 | local address_table = {}
14 | local action_method = {}
15 |
16 | local function response(id, ...)
17 | local ok, err = httpd.write_response(sockethelper.writefunc(id), ...)
18 | if not ok then
19 | if err ~= sockethelper.socket_error then
20 | log.printf("%s error : %s", address_table[id], err)
21 | end
22 | end
23 | end
24 |
25 | -- get userid from cookies, and query username from userservice
26 | local function get_userid(header)
27 | local cookie = header.cookie
28 | local userid
29 | if cookie then
30 | for k,v in cookie:gmatch " *(.-)=([^;]*);?" do
31 | if k == "userid" then
32 | userid = v
33 | break
34 | end
35 | end
36 | end
37 | return skynet.call(userservice, "lua", userid)
38 | end
39 |
40 | local userid_header = setmetatable({} , { __mode = "kv",
41 | __index = function(t,k)
42 | local v = {
43 | ["Set-Cookie"] = string.format(
44 | "userid=%d; Path=/; Max-Age=2592000", k),
45 | ["Content-Type"] = "text/html; charset=utf-8"
46 | }
47 | t[k] = v
48 | return v
49 | end,
50 | })
51 |
52 | action_method["/"] = function(body, userid, username)
53 | return skynet.call(lobby, "lua", "web", userid, username)
54 | end
55 |
56 | action_method["/lobby"] = function(body, userid, username)
57 | return skynet.call(lobby, "lua", "api", userid, username, body)
58 | end
59 |
60 | action_method["/room"] = function(body, userid, username)
61 | local args = body
62 | if not args then
63 | return '{"status":"error","error":"Invalid Action"}'
64 | end
65 | local roomid = args.roomid
66 | if not roomid then
67 | return '{"status":"error","error":"Invalid Room id"}'
68 | end
69 | local r = skynet.call(roomkeeper, "lua", "query", roomid)
70 | if not r then
71 | return '{"status":"error","error":"Room not open"}'
72 | end
73 | args.userid = userid
74 | return skynet.call(r, "lua", "api", args)
75 | end
76 |
77 | local function enter_room(room, userid, username, action)
78 | local room = tonumber(action:sub(2))
79 | local r = room and skynet.call(roomkeeper, "lua", "query", room)
80 | if not r then
81 | return "Invalid or closed room.", 404
82 | end
83 | return skynet.call(r, "lua", "web", userid, username), 200
84 | end
85 |
86 | local function handle_socket(id)
87 | -- limit request body size to 8192 (you can pass nil to unlimit)
88 | local code, url, method, header, body = httpd.read_request(sockethelper.readfunc(id), 8192)
89 | if body and body ~= "" then
90 | body = json.decode(body)
91 | end
92 | if code then
93 | if code ~= 200 then
94 | response(id, code)
95 | else
96 | local action = urllib.parse(url)
97 | local offset = action:find("/",2,true)
98 | if offset then
99 | local path = action:sub(1,offset-1)
100 | local filename = action:sub(offset+1)
101 | if path == "/static" then
102 | local content = staticfile[filename]
103 | if content then
104 | response(id, 200, content)
105 | else
106 | response(id, 404, "404 Not found")
107 | end
108 | else
109 | response(id, 404, "404 Not found")
110 | end
111 | else
112 | local userid, username = get_userid(header)
113 | local f = action_method[action] or enter_room
114 |
115 | local ret, c = f(body, userid, username, action)
116 | if type(ret) ~= "string" then
117 | ret = json.encode(ret or {})
118 | end
119 | c = c or 200
120 | response(id, c, ret, userid_header[userid])
121 | end
122 | end
123 | else
124 | if url ~= sockethelper.socket_error then
125 | log.printf("%s error: %s", address_table[id], url)
126 | end
127 | end
128 | end
129 |
130 | skynet.start(function()
131 | roomkeeper = assert(skynet.uniqueservice "roomkeeper")
132 | userservice = assert(skynet.uniqueservice "userid")
133 | lobby = assert(skynet.uniqueservice "lobby")
134 | skynet.dispatch("lua", function(_,_,id, ipaddr)
135 | address_table[id] = ipaddr
136 | socket.start(id)
137 | local ok,reason = xpcall(handle_socket, debug.traceback, id)
138 | if not ok then print(reason) end
139 | socket.close(id)
140 | end)
141 | end)
142 |
--------------------------------------------------------------------------------
/service/lobby.lua:
--------------------------------------------------------------------------------
1 | local skynet = require "skynet"
2 | local urllib = require "http.url"
3 | local staticfile = require "staticfile"
4 | local string = string
5 |
6 | local userservice
7 | local roomkeeper
8 |
9 | local content = staticfile["index.html"]
10 |
11 | local function main()
12 | return content
13 | end
14 |
15 | local action = {}
16 |
17 | function action.getname(userid, username, args)
18 | return {username = username}
19 | end
20 |
21 | function action.setname(userid, username, args)
22 | local userid, username = skynet.call(userservice, "lua", userid, args.username)
23 | return {username = username}
24 | end
25 |
26 | function action.create(userid, username, args)
27 | local ok, roomid = skynet.call(roomkeeper, "lua", "open")
28 | return ok and {room = roomid} or {error = roomid}
29 | end
30 |
31 | function action.join(userid, username, args)
32 | local roomid = args.roomid
33 | if roomid and skynet.call(roomkeeper, "lua", "query", roomid) then
34 | return {roomid = roomid}
35 | else
36 | return {error = "invalid room id"}
37 | end
38 | end
39 |
40 | skynet.start(function()
41 | userservice = assert(skynet.uniqueservice "userid")
42 | roomkeeper = assert(skynet.uniqueservice "roomkeeper")
43 | skynet.dispatch("lua", function(_,_, cmd, userid, username, body)
44 | if cmd == "web" then
45 | skynet.ret(skynet.pack(main(httpheader)))
46 | elseif cmd == "api" then
47 | -- lobby api
48 | local args = body
49 | local ret = {username = username}
50 | if args then
51 | local f = action[args.action]
52 | if f then
53 | ret = f(userid, username, args)
54 | else
55 | ret = {error = "Invalid Action"}
56 | end
57 | end
58 |
59 | skynet.ret(skynet.pack(ret))
60 | end
61 | end)
62 | end)
63 |
--------------------------------------------------------------------------------
/service/main.lua:
--------------------------------------------------------------------------------
1 | local skynet = require "skynet"
2 | local socket = require "socket"
3 |
4 | skynet.start(function()
5 | skynet.newservice("debug_console",8000)
6 | assert(skynet.uniqueservice "roomkeeper")
7 | local broker_n = tonumber(skynet.getenv "broker")
8 | local broker = {}
9 | for i= 1, broker_n do
10 | broker[i] = assert(skynet.newservice("broker"))
11 | end
12 |
13 | local roomkeeper = skynet.uniqueservice "roomkeeper"
14 |
15 | local address = skynet.getenv "listen"
16 | skynet.error("Listening "..address)
17 | local id = assert(socket.listen(address))
18 | local balance = 1
19 | socket.start(id , function(id, addr)
20 | skynet.send(broker[balance], "lua", id, addr)
21 | balance = balance + 1
22 | if balance > #broker then
23 | balance = 1
24 | end
25 | end)
26 | end)
27 |
--------------------------------------------------------------------------------
/service/room.lua:
--------------------------------------------------------------------------------
1 | local skynet = require "skynet"
2 | local log = require "log"
3 | local table = table
4 | local staticfile = require "staticfile"
5 | local rule = require "rule"
6 | local json = require"json"
7 | local objproxy = require"objproxy"
8 |
9 | local content = staticfile["room.html"]
10 |
11 | local ALIVETIME = 100 * 60 * 10 -- 10 minutes
12 | local PUSH_TIME = 100 * 60
13 |
14 | local R = {
15 | version = 1,
16 | push_tbl={},
17 | info = objproxy.new{user_tbl = {}, rules = {}}
18 | }
19 |
20 | local READY = 0
21 | local NOTREADY = 1
22 | local BLOCK = 2
23 |
24 | local roomkeeper
25 | local alive
26 | local userservice
27 | local room = {}
28 |
29 | local function exit()
30 | if R.roomid then
31 | local id = R.roomid
32 | R.roomid = nil
33 | skynet.call(roomkeeper, "lua", "close", id)
34 | end
35 | skynet.exit()
36 | end
37 |
38 | local function heartbeat()
39 | alive = skynet.now()
40 | while true do
41 | skynet.sleep(ALIVETIME//2)
42 | if skynet.now() - alive > ALIVETIME then
43 | exit()
44 | end
45 | end
46 | end
47 |
48 | local function enter_room(userid, username)
49 | local u = R.info.user_tbl[userid]
50 | if u then
51 | u.timestamp = skynet.now()
52 | if u.username == username then
53 | return
54 | end
55 | u.username = username
56 | else
57 | R.info.user_tbl[userid] = {
58 | userid = userid,
59 | username = username,
60 | timestamp = skynet.now(),
61 | status = NOTREADY, -- not ready
62 | }
63 | end
64 | end
65 |
66 | local function checkrule()
67 | local ready_num = 0
68 | for _, u in pairs(R.info.user_tbl) do
69 | if u.status == READY then
70 | ready_num = ready_num + 1
71 | end
72 | end
73 |
74 | return rule.checkrules(R.info.rules, ready_num)
75 | end
76 |
77 | local function prepareinfo(_)
78 | local info = {status = "prepare", player = {}, rule = {}, version = R.version}
79 | info.can_game, result = checkrule()
80 | info.reason = info.can_game and "" or result
81 |
82 | for _, v in pairs(R.info.user_tbl) do
83 | table.insert(info.player, {
84 | userid = v.userid,
85 | username = v.username,
86 | status = v.status})
87 | end
88 | for rule in pairs(R.info.rules) do
89 | table.insert(info.rule, rule)
90 | end
91 |
92 | return info
93 | end
94 |
95 | local function gameinfo(userid)
96 | assert (R.status == "game")
97 | local u = R.info.user_tbl[userid]
98 | if not u.identity then
99 | return {error = "您未分配角色"}
100 | end
101 | local identity_name = rule.role[u.identity]
102 | local role_visible = rule.visible[u.identity]
103 | local tmp_information = {}
104 | for _, u in pairs(R.info.user_tbl) do
105 | local visible = role_visible[u.identity]
106 | if visible then
107 | local identity_name
108 | if visible == true then
109 | identity_name = rule.role[u.identity]
110 | elseif visible == 4 then
111 | identity_name = rule.camp_name[u.identity]
112 | elseif visible == 3 and R.info.rules[8] then
113 | identity_name = rule.role[u.identity]
114 | end
115 | if identity_name then
116 | table.insert(tmp_information, {username = u.username, identity = identity_name})
117 | end
118 | end
119 | end
120 |
121 | local players = {}
122 | for _, uid in ipairs(R.info.uidlist) do
123 | local u = R.info.user_tbl[uid]
124 | local p = {userid = u.userid, username=u.username, color = "#ffffff"}
125 | if R.info.mode == "end" then
126 | p.identity = rule.role[u.identity]
127 | end
128 | table.insert(players, p)
129 | end
130 |
131 | local stagel = {}
132 | for _,uid in ipairs(R.info.stage) do
133 | table.insert(stagel, uid)
134 | end
135 | local hist = {}
136 | for i,s in ipairs(R.info.history) do hist[i] = s end
137 |
138 | return {
139 | players = players,
140 | identity = {name = identity_name, desc = ""},
141 | information = tmp_information,
142 | evil_count = #R.info.uidlist - rule.camp_good[#R.info.uidlist],
143 | gameinfo = {round = R.info.round, pass = R.info.pass, leader = R.info.leader,
144 | stage = stagel, mode = R.info.mode, history = hist,
145 | need = R.stage_per_round[R.info.round], round_success = R.info.round_success},
146 | status = "game",
147 | version = R.version,
148 | }
149 | end
150 |
151 | local function roominfo(userid)
152 | local info = R.status == "prepare" and prepareinfo(userid) or gameinfo(userid)
153 | R.cache = json.encode(info)
154 | return R.cache
155 | end
156 |
157 | local function _seri(...)
158 | local cache = {}
159 | local function _seri(el, path)
160 | if type(el) ~= "table" then
161 | if type(el) == "string" then
162 | return el
163 | else
164 | return tostring(el)
165 | end
166 | end
167 |
168 | if cache[el] then return cache[el] end
169 | cache[el] = path == "" and "." or path
170 |
171 | local tmp = {}
172 | for i,v in ipairs(el) do
173 | table.insert(tmp, _seri(v, path.."."..i))
174 | end
175 | return string.format("[%s]", table.concat(tmp, ", "))
176 | end
177 |
178 | local output = {}
179 | for i=1,select("#", ...) do
180 | table.insert(output, _seri(select(i, ...), ""))
181 | end
182 | return table.concat(output, " ")
183 | end
184 |
185 | local handles = {
186 | pass_limit = function () return rule.pass_limit end,
187 | leader = function () return R.info.user_tbl[R.info.leader].username end,
188 | stage = function ()
189 | local l = {}
190 | for _,uid in ipairs(R.info.stage) do
191 | table.insert(l, R.info.user_tbl[uid].username)
192 | end
193 | return _seri(l)
194 | end,
195 |
196 | vote_yes = function ()
197 | local l = {}
198 | for uid,flag in pairs(R.vote) do
199 | if flag then table.insert(l, R.info.user_tbl[uid].username) end
200 | end
201 | return _seri(l)
202 | end,
203 |
204 | vote_no = function ()
205 | local l = {}
206 | for uid,flag in pairs(R.vote) do
207 | if not flag then table.insert(l, R.info.user_tbl[uid].username) end
208 | end
209 | return _seri(l)
210 | end
211 | }
212 |
213 | local function add_history(...)
214 | local l = {...}
215 | for i,s in ipairs(l) do
216 | l[i] = string.gsub(s, "{([%w_]+)}", function (w) return handles[w]() end)
217 | end
218 |
219 | local hist = ("%d.%d "):format(R.info.round, R.info.pass) .. table.concat(l, "\n\t")
220 | table.insert(R.info.history, hist)
221 | end
222 |
223 | function room.web(userid, username)
224 | enter_room(userid, username)
225 | return content
226 | end
227 |
228 | local function enter_quest()
229 | R.info.mode = "quest"
230 | R.vote = {}
231 | end
232 |
233 | local function new_pass()
234 | R.info.mode = "plan"
235 | R.info.stage = {}
236 | R.vote = {}
237 | R.info.pass = R.info.pass + 1
238 |
239 | for i,uid in ipairs(R.info.uidlist) do
240 | if uid == R.info.leader then
241 | local j = i == #R.info.uidlist and 1 or i+1
242 | R.info.leader = R.info.uidlist[j]
243 | break
244 | end
245 | end
246 | end
247 |
248 | local function new_round(success)
249 | if R.info.round == #R.stage_per_round then
250 | R.info.mode = "end"
251 | return
252 | end
253 |
254 | R.info.round = R.info.round + 1
255 | R.info.pass = 0
256 | if success then
257 | R.info.round_success = R.info.round_success + 1
258 | end
259 | new_pass()
260 | end
261 |
262 | local function next_pass()
263 | if R.info.pass >= rule.pass_limit then
264 | add_history("任务失败! 提案连续{pass_limit}次没有通过")
265 | return new_round(false)
266 | end
267 |
268 | new_pass()
269 | end
270 |
271 | local api = {}
272 |
273 | function api.setname(args)
274 | local userid = args.userid
275 | local username = args.username
276 | local u = R.info.user_tbl[userid]
277 | if u.username ~= username then
278 | skynet.call(userservice, "lua", userid, username)
279 | u.username = username
280 | end
281 | end
282 |
283 | function api.begin_game(args)
284 | local userid = args.userid
285 | local ok,result = checkrule()
286 | if ok then
287 | local i = 1
288 | local user_tbl = R.info.user_tbl
289 | local uidlist = {}
290 | for uid, u in pairs(user_tbl) do
291 | if u.status == READY then
292 | u.identity = result[i]
293 | i = i + 1
294 | table.insert(uidlist, uid)
295 | end
296 | end
297 | table.sort(uidlist)
298 | R.status = "game"
299 | R.stage_per_round = rule.stage_per_round[#uidlist]
300 | R.vote = {}
301 | R.info = objproxy.new{
302 | user_tbl = user_tbl,
303 | round = 1, -- 第n轮
304 | pass =1, -- 第n次提案
305 | round_success = 0, -- 成功任务数
306 | rules = R.info.rules,
307 | leader = uidlist[math.random(#uidlist)],
308 | uidlist = uidlist,
309 | stage = {}, -- 被提名的人
310 | history = {},
311 | mode = "plan" -- plan/audit/quest
312 | }
313 | end
314 |
315 | for k,v in pairs(R.info.rules) do
316 | print(":::",k,v)
317 | end
318 | return roominfo(userid)
319 | end
320 |
321 | function api.vote(args)
322 | local userid = args.userid
323 | local approve = args.approve
324 |
325 | local function in_stage()
326 | for _,uid in ipairs(R.info.stage) do
327 | if uid == userid then return true end
328 | end
329 | return false
330 | end
331 |
332 | local function _total()
333 | local total,yes = 0,0
334 | for _,flag in pairs(R.vote) do
335 | total = total + 1
336 | if flag then yes = yes+1 end
337 | end
338 | return total, yes
339 | end
340 |
341 | if R.info.mode == "audit" then
342 | R.vote[userid] = approve
343 | local total, yes = _total()
344 | if total == #R.info.uidlist then
345 | if yes > total/2 then
346 | add_history("提议通过. {leader} 提议 {stage}", "赞同者: {vote_yes}", "反对者: {vote_no}")
347 | enter_quest()
348 | else
349 | add_history("提议否决! {leader} 提议 {stage}", "赞同者: {vote_yes}", "反对者: {vote_no}")
350 | next_pass()
351 | end
352 | end
353 | elseif R.info.mode == "quest" and in_stage() then
354 | R.vote[userid] = approve
355 | local total, yes = _total()
356 | if total == #R.info.stage then
357 | local needtwo = R.stage_per_round[R.info.round] < 0
358 | if yes == total or needtwo and yes+1 == total then
359 | add_history("任务成功. 参与者: {stage}", ("出现%d张失败票"):format(total-yes))
360 | new_round(true)
361 | else
362 | add_history("任务失败! 参与者: {stage}", ("出现%d张失败票"):format(total-yes))
363 | new_round(false)
364 | end
365 | end
366 | else
367 | return {error = "您不能表态"}
368 | end
369 | end
370 |
371 | function api.stage(args)
372 | if R.info.mode ~= "plan" or R.info.leader ~= args.userid then
373 | return {error = "您不能提名"}
374 | end
375 |
376 | -- todo: check stagelist
377 | R.info.stage = args.stagelist
378 | R.info.mode = "audit"
379 | end
380 |
381 | function api.ready(args)
382 | local userid = args.userid
383 | local enable = args.enable
384 | local u = R.info.user_tbl[userid]
385 | u.status = enable and READY or NOTREADY
386 | end
387 |
388 | function api.kick(args)
389 | local id = tonumber(args.id)
390 | local u = R.info.user_tbl[id]
391 | if not u then
392 | return
393 | end
394 | u.status = BLOCK
395 | end
396 |
397 | function api.set(args)
398 | local rule = tonumber(args.rule)
399 | local enable = args.enable
400 |
401 | R.info.rules[rule] = enable and true or nil
402 | end
403 |
404 | function api.request(args)
405 | local userid = args.userid
406 | local version = tonumber(args.version)
407 | local co = R.push_tbl[userid]
408 | if co then
409 | skynet.wakeup(co)
410 | R.push_tbl[userid] = nil
411 | end
412 | if version ~= 0 and version == R.version then
413 | local co = coroutine.running()
414 | R.push_tbl[userid] = co
415 | skynet.sleep(PUSH_TIME)
416 | R.push_tbl[userid] = nil
417 | if version == R.version then
418 | return {version = version}
419 | end
420 | end
421 | return roominfo(userid)
422 | end
423 |
424 | function api.list(args)
425 | local userid = args.userid
426 | local u = R.info.user_tbl[userid]
427 | if not u.identity then
428 | return {error = "您未分配角色"}
429 | end
430 | local identity_name = rule.role[u.identity]
431 | local role_visible = rule.visible[u.identity]
432 | local tmp_information = {}
433 | for _, u in pairs(R.info.user_tbl) do
434 | local visible = role_visible[u.identity]
435 | if visible then
436 | local identity_name
437 | if visible == true then
438 | identity_name = rule.role[u.identity]
439 | elseif visible == 4 then
440 | identity_name = rule.camp_name[u.identity]
441 | elseif visible == 3 and not R.info.rules[6] and not R.info.rules[7] then
442 | identity_name = rule.role[u.identity]
443 | end
444 | if identity_name then
445 | table.insert(tmp_information, {username = u.username, identity = identity_name})
446 | end
447 | end
448 | end
449 |
450 | local players = {}
451 | for _, uid in ipairs(R.info.uidlist) do
452 | local u = R.info.user_tbl[uid]
453 | table.insert(players, {userid = u.userid, username=u.username, color = "#ffffff"})
454 | end
455 |
456 | return {
457 | players = players,
458 | identity = {name = identity_name, desc = ""},
459 | information = tmp_information,
460 | round = R.info.round,
461 | pass = R.info.pass,
462 | leader = R.info.leader
463 | }
464 | end
465 |
466 | function room.api(args)
467 | local f = args.action and api[args.action]
468 | if not f then
469 | return {error = "Invalid Action"}
470 | end
471 | print("request", args.action)
472 | if args.status ~= R.status then
473 | -- todo push status
474 | return roominfo(args.userid)
475 | end
476 | return f(args)
477 | end
478 |
479 | function room.init(id)
480 | assert(R.roomid == nil, "Already Init")
481 | R.roomid = id
482 | R.status = "prepare"
483 | R.needs = ""
484 | log.printf("[Room:%d] open", id)
485 | end
486 |
487 | local function update_status()
488 | local idx, co = next(R.push_tbl)
489 | while(co) do
490 | skynet.wakeup(co)
491 | idx, co = next(R.push_tbl, idx)
492 | end
493 | end
494 |
495 | skynet.start(function()
496 | roomkeeper = assert(skynet.uniqueservice "roomkeeper")
497 | userservice = assert(skynet.uniqueservice "userid")
498 | skynet.fork(heartbeat)
499 | skynet.dispatch("lua", function (_,_,cmd,...)
500 | alive = skynet.now()
501 | local f = room[cmd]
502 | if f then
503 | local ok, ret = xpcall(f, debug.traceback, ...)
504 | if not ok then
505 | print(ret)
506 | ret = {error = "server error"}
507 | end
508 | skynet.ret(skynet.pack(ret))
509 |
510 | if objproxy.is_dirty(R.info) then
511 | R.version = R.version + 1
512 | R.cache = nil
513 | objproxy.clean(R.info)
514 | update_status()
515 | end
516 | end
517 | end)
518 | end)
519 |
--------------------------------------------------------------------------------
/service/roomkeeper.lua:
--------------------------------------------------------------------------------
1 | local skynet = require "skynet"
2 | local log = require "log"
3 |
4 | local MAXROOM = 4096
5 | local MAXROOMID = 999999
6 | local house = { n = 0 }
7 | local roomkeeper = {}
8 |
9 | function roomkeeper.query(room)
10 | if house[room] then
11 | return house[room]
12 | end
13 | end
14 |
15 | function roomkeeper.open()
16 | if house.n >= MAXROOM then
17 | return false, "Not enough empty rooms"
18 | end
19 | local room
20 | repeat
21 | room = math.random(MAXROOMID)
22 | until house[room] == nil
23 | local r = assert(skynet.newservice "room")
24 | skynet.call(r, "lua", "init", room)
25 | house.n = house.n + 1
26 | house[room] = r
27 | return true, room
28 | end
29 |
30 | function roomkeeper.close(room)
31 | if house[room] then
32 | house[room] = nil
33 | house.n = house.n - 1
34 | log.printf("[Room:%d] closed", room)
35 | end
36 | end
37 |
38 | skynet.start(function()
39 | math.randomseed(skynet.time())
40 | skynet.dispatch("lua", function(_,_,cmd,...)
41 | local f = roomkeeper[cmd]
42 | if f then
43 | skynet.ret(skynet.pack(f(...)))
44 | end
45 | end)
46 | end)
47 |
--------------------------------------------------------------------------------
/service/userid.lua:
--------------------------------------------------------------------------------
1 | local skynet = require "skynet"
2 | local math = math
3 | local utf8 = utf8
4 | local table = table
5 |
6 | local users = {}
7 | local lastid = 0
8 |
9 | local function new_username(userid)
10 | local username = "u"..tostring(userid)
11 | users[userid] = username
12 | return username
13 | end
14 |
15 | local function create_userid()
16 | local ti = math.floor(skynet.time()) - 1420000000
17 | if ti > lastid then
18 | lastid = ti
19 | else
20 | lastid = lastid + 1
21 | end
22 | return tostring(lastid), new_username(lastid)
23 | end
24 |
25 | local function get_username(userid)
26 | local username = users[userid]
27 | if not username then
28 | username = new_username(userid)
29 | end
30 | return username
31 | end
32 |
33 | local function set_username(userid, username)
34 | -- verify username
35 | if #username > 16 or username:find "[^%w%.\128-\255]" then
36 | local temp = {}
37 | for p,c in utf8.codes(username) do
38 | if #temp > 8 then
39 | break
40 | end
41 | if c > 128 or string.char(c):find "[%w%.]" then
42 | table.insert(temp, c)
43 | end
44 | end
45 | if #temp > 1 then
46 | users[userid] = utf8.char(table.unpack(temp))
47 | else
48 | new_username(userid)
49 | end
50 | else
51 | users[userid] = username
52 | end
53 | end
54 |
55 | skynet.start(function()
56 | skynet.dispatch("lua", function (_,_,userid,username)
57 | if not userid then
58 | userid, username = create_userid()
59 | else
60 | if not username or username == "" then
61 | username = get_username(userid)
62 | else
63 | set_username(userid, username)
64 | username = users[userid]
65 | end
66 | end
67 | skynet.ret(skynet.pack(userid, username))
68 | end)
69 | end)
70 |
--------------------------------------------------------------------------------
/start.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | export AVALON_LISTEN=0.0.0.0:8001
3 | export AVALON_BROKER=8
4 | export AVALON_STATIC=./static/
5 | skynet/skynet etc/config
6 |
--------------------------------------------------------------------------------
/static/avalon.css:
--------------------------------------------------------------------------------
1 | html {
2 | background-color: black;
3 | color: #15b940;
4 | font-family: monospace;
5 | font-size: 15px;
6 | }
7 |
8 | ul {
9 | list-style: none;
10 | margin: 0;
11 | padding: 0;
12 | }
13 |
14 | .wrapper{
15 | border: 1px dashed #39da08;
16 | margin: 15px auto;
17 | padding: 10px;
18 | }
19 |
20 | .inner {
21 | width: 85%;
22 | margin: 0 auto;
23 | min-height: 300px;
24 | }
25 |
26 | .room_title{
27 | text-align: center;
28 | }
29 |
30 | .people{
31 | overflow: hidden;
32 | margin: 15px 0;
33 | /*min-height: 48px;*/
34 | }
35 |
36 | .people div{
37 | float: left;
38 | width: 50%;
39 | }
40 |
41 | .people .status_mark{
42 | margin-right: 5px;
43 | }
44 |
45 | .people_status{
46 | min-height: 61px;
47 | }
48 |
49 | .people_status div{
50 | margin: 5px;
51 | }
52 |
53 | .people_status span{
54 | margin-right: 5px;
55 | }
56 |
57 | .room_rule span{
58 | margin-left: 10px;
59 | }
60 |
61 | .room_rule p{
62 | margin: 8px 0;
63 | }
64 |
65 | .action {
66 | margin-bottom: 10px;
67 | }
68 | .action input{
69 | background-color: black;
70 | border: 1px dashed #39da08;
71 | color: #39da08;
72 | line-height: 15px;
73 | padding: 5px;
74 | outline: 0 none;
75 | }
76 |
77 | .action a:hover{
78 | cursor: pointer;
79 | }
80 |
81 | ::-webkit-input-placeholder {
82 | color: #39da08;
83 | }
84 |
85 | .status_mark{
86 | height: 20px;
87 | display: inline-block;
88 | padding: 2px;
89 | }
90 |
91 | .status_mark:before {
92 | line-height: 15px;
93 | display: inline-block;
94 | vertical-align: middle;
95 | height: 15px;
96 | margin-right: 5px;
97 | width: 10px;
98 | text-align: center;
99 | }
100 | .status_0:before{
101 | content: "✔";
102 | }
103 | .status_1:before{
104 | content: "✘";
105 | }
106 | .status_2:before{
107 | content: "☺";
108 | }
109 |
110 | .room_rule p:after{
111 | line-height: 15px;
112 | display: inline-block;
113 | vertical-align: middle;
114 | height: 15px;
115 | margin-right: 5px;
116 | width: 10px;
117 | text-align: center;
118 | content: "(✘)"
119 | }
120 |
121 | .room_rule .rule_enabled:after{
122 | content: "(✔)";
123 | }
124 |
125 | .lobby-buttons li{
126 | text-align: center;
127 | height: 50px;
128 | line-height: 50px;
129 | margin: 30px 0;
130 | cursor: pointer;
131 | }
132 |
133 | .lobby-title {
134 | text-align: center;
135 | }
136 |
137 | input.room_number{
138 | width: 50px;
139 | height: 10px;
140 | line-height: 10px;
141 | }
142 |
143 | .outter {
144 | overflow: scroll;
145 | height: 550px;
146 | }
147 |
148 | .prepare, .game{
149 | float: left;
150 | height: 100%;
151 | width: 100%;
152 | }
153 |
154 | .game{
155 | position: relative;
156 | }
157 |
158 | .game .role{
159 | position: absolute;
160 | top: -173px;
161 | width: 100%;
162 | height: 191px;
163 | }
164 |
165 | .game .role h5{
166 | text-align: center;
167 | margin-top: 10px;
168 | font-size: 15px;
169 | }
170 |
171 | .game .role .role-desc{
172 | text-align: center;
173 | height: 30px;
174 | }
175 | .game .role .role-visible{
176 | overflow: hidden;
177 | height: 50px;
178 | }
179 | .game .role .role-visible span{
180 | display: block;
181 | float: left;
182 | width: 45%;
183 | text-align: center;
184 | padding: 4px;
185 | }
186 |
187 | .game .role .role-button{
188 | margin: 0 auto;
189 | margin-top: 15px;
190 | width: 80px;
191 | text-align: center;
192 | padding: 4px;
193 | }
194 |
195 | .game .role.show{
196 | top: 0px;
197 | }
198 |
199 | .game .ground{
200 | margin-top: 45px;
201 | text-align: center;
202 | }
203 |
204 | .game .ground h4{}
205 |
206 | .game{display: none}
207 |
--------------------------------------------------------------------------------
/static/game.js:
--------------------------------------------------------------------------------
1 | var AvalonGame = function(){
2 | this.status = "game";
3 | this.mission = 1; // 第几个任务, 总共5轮,也就最多5个任务
4 | this.plan = 1; // 第几个提案
5 | this.rules = {
6 | 5: [2, 3, 2, 3, 3],
7 | 6: [2, 3, 4, 3, 4],
8 | 7: [2, 3, 3, 4, 4],
9 | 8: [3, 4, 4, 5, 5],
10 | }
11 | }
12 |
13 | AvalonGame.fn = AvalonGame.prototype = {constructor: AvalonGame};
14 |
15 | AvalonGame.fn.begin = function(resp){
16 | this.update_info(resp)
17 | this.render_players(resp.players, resp.gameinfo.stage)
18 | this.game_bind_action()
19 | this.render_role_info(resp)
20 | this.update_game(0)
21 | }
22 |
23 | AvalonGame.fn.update_info = function(resp){
24 | this.mode = resp.gameinfo.mode
25 | this.is_leader = resp.gameinfo.leader == userid
26 | this.stage = resp.gameinfo.stage
27 | }
28 |
29 | AvalonGame.fn.set_game_history = function(v, poll_begin){
30 | var req = {
31 | roomid: room_number,
32 | status: 'game',
33 | action: 'request',
34 | version: v ? v : 0
35 | };
36 |
37 | Ejoy.postJSON('/room', req, function(resp){
38 |
39 | })
40 | }
41 |
42 | AvalonGame.fn.update_game = function (v) {
43 | var self = this
44 | var req = {
45 | roomid: room_number,
46 | status: 'game',
47 | action: 'request',
48 | version: v ? v : 0
49 | }
50 | Ejoy.postJSON("/room", req, function(resp){
51 | console.log(resp)
52 | if (resp.error){
53 | return
54 | }
55 |
56 | if (version != 0 && version == resp.version) {
57 | console.log(">>> continue update_game")
58 | return self.update_game(version)
59 | }
60 |
61 | gameinfo = resp.gameinfo
62 | version = resp.version
63 | self.update_info(resp)
64 |
65 | Ejoy("stage_title").html("共" + resp.evil_count + "个反方")
66 | Ejoy("stage_desc").html("第 "+ resp.gameinfo.round + " 个任务, 第 " + resp.gameinfo.pass + " 次提案" )
67 |
68 | if (resp.gameinfo.history) {
69 | var hist = ""
70 | for (var i=resp.gameinfo.history.length-1;i>=0;i--) {
71 | hist += "" + resp.gameinfo.history[i] + "
"
72 | }
73 | Ejoy("game-history").html(hist)
74 | }
75 |
76 | if (resp.gameinfo.mode == "plan") {
77 | self.render_players(resp.players, resp.gameinfo.stage)
78 | if (userid == resp.gameinfo.leader) {
79 | var info = resp.gameinfo
80 | var prompt = "请选出 " + Math.abs(info.need) + " 人";
81 | if (info.need < 0) {
82 | prompt += "(本次任务失败需至少两次反对票)"
83 | }
84 | Ejoy("stage_prompt").html(prompt)
85 | document.getElementsByClassName('stage-action')[0].style.display = "block"
86 | document.getElementsByClassName('vote-action')[0].style.display = "none"
87 | return
88 | } else {
89 | var info = resp.gameinfo
90 | document.getElementsByClassName('stage-action')[0].style.display = "none"
91 | document.getElementsByClassName('vote-action')[0].style.display = "none"
92 |
93 | var leader
94 | for (var i=0;i= 3)
144 | prompt += "正方胜利"
145 | else
146 | prompt += "反方胜利"
147 | Ejoy("stage_prompt").html(prompt)
148 |
149 | self.render_players(resp.players, resp.gameinfo.stage)
150 | }
151 | })
152 | }
153 |
154 | AvalonGame.fn.wait = function () {
155 | this.update_game(version)
156 | }
157 |
158 | AvalonGame.fn.leader_plan = function(resp) {
159 | }
160 |
161 | AvalonGame.fn.wait_leader_plan = function (resp) {
162 | }
163 |
164 | AvalonGame.fn.game_bind_action = function(){
165 | self = this;
166 | Ejoy('role-button').on('click', function(e){
167 | var target = document.getElementsByClassName('role')[0]
168 | var ground = document.getElementsByClassName('ground')[0]
169 | if(target.className.indexOf('show') > -1){
170 | target.className = target.className.replace('show', '')
171 | ground.style.display = "block"
172 |
173 | }else{
174 | ground.style.display = "none"
175 | target.className += " show"
176 | }
177 | });
178 |
179 | var stage_list = []
180 | Ejoy("game-people").on("click", "people_item", function(select_dom){
181 | var user_id = select_dom.id;
182 | if (self.mode == "plan" && self.is_leader) {
183 | if (stage_list.indexOf(user_id) == -1) {
184 | if (stage_list.length >= Math.abs(gameinfo.need)) {
185 | return
186 | }
187 | stage_list.push(user_id);
188 | }
189 | else {
190 | Ejoy.array_remove(stage_list, user_id)
191 | }
192 |
193 | var status_mark = "status_0"
194 | if (stage_list.indexOf(user_id) == -1)
195 | status_mark = "status_1"
196 | var el = select_dom.children[0]
197 | el.className = el.className.replace(/status_\d/, status_mark)
198 | }
199 | }
200 | );
201 |
202 | Ejoy('stage-commit').on('click', function(){
203 | if (self.mode == "plan" && self.is_leader && stage_list.length == Math.abs(gameinfo.need)) {
204 | var req = {
205 | roomid: room_number,
206 | status: 'game',
207 | action: 'stage',
208 | version: version,
209 | stagelist: stage_list,
210 | }
211 |
212 | Ejoy.postJSON('/room', req, function(resp){
213 | console.log(resp)
214 | if(!resp.error){
215 | self.wait()
216 | }
217 | })
218 | }
219 | });
220 |
221 | var genvote = function (flag){
222 | return function () {
223 | if (self.mode == "audit" || (self.mode == "quest" && self.stage.indexOf(userid) != -1)) {
224 | var req = {
225 | roomid: room_number,
226 | status: 'game',
227 | action: 'vote',
228 | version: version,
229 | approve: flag,
230 | }
231 |
232 | Ejoy.postJSON('/room', req, function(resp){
233 | console.log(resp)
234 | if(!resp.error){
235 | document.getElementsByClassName("vote-action")[0].style.display = "none"
236 | self.wait()
237 | }
238 | })
239 | }
240 | }
241 | }
242 | Ejoy('vote-yes').on('click', genvote(true));
243 | Ejoy('vote-no').on('click', genvote(false));
244 | }
245 |
246 | AvalonGame.fn.render_role_info = function(resp){
247 | self = this
248 | if (!resp.error) {
249 | var identity = resp.identity
250 | Ejoy('role_name').html(identity.name)
251 | Ejoy('role-desc').html(identity.desc)
252 | var friends = resp.information
253 | var friends_html = ""
254 | for(var i=0; i' + v.username + " : " + v.identity + ''
257 | }
258 |
259 | Ejoy("role-visible").html(friends_html)
260 | }
261 | }
262 |
263 | AvalonGame.fn.render_players = function(players, stage){
264 | var players_str = ""
265 | for(var i=0; i < players.length; i++){
266 | var player = players[i]
267 | var mark = 1
268 | if (stage.indexOf(player.userid) > -1)
269 | mark = 0
270 | var content = player.username
271 | if (player.identity)
272 | content += "[" + player.identity + "]"
273 | players_str += '' +
280 | content +
281 | '
';
282 | }
283 | Ejoy('game-people').html(players_str);
284 | }
285 |
--------------------------------------------------------------------------------
/static/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | avalon--lobby
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
avalon-游戏大厅
17 |
18 |
22 |
23 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/static/lob.js:
--------------------------------------------------------------------------------
1 | document.addEventListener("DOMContentLoaded", function(){
2 | Ejoy('enter-room').on('click', function(){
3 | Ejoy('lobby-action').css("display: block;")
4 | });
5 |
6 | Ejoy('create-room').on('click', function(){
7 | create_room()
8 | });
9 |
10 | Ejoy('contine').on('click', function(e){
11 | var room_number = e.target.previousElementSibling.value
12 | //check room_number
13 | join_room(room_number)
14 | });
15 |
16 | function get_name(){
17 | var req = {
18 | action: "getname"
19 | }
20 | Ejoy.postJSON('/lobby', req, function(resp){
21 | Ejoy('user-name').html(resp.username)
22 | })
23 |
24 | };
25 |
26 | function create_room(){
27 | var req = {
28 | action: "create",
29 | }
30 | Ejoy.postJSON('/lobby', req, function(resp){
31 | //resp {status: 'join', room: 787878}
32 | location.href = "/" + resp.room
33 | })
34 |
35 | }
36 | function join_room(roomid){
37 | location.href = "/" + roomid
38 | }
39 | get_name()
40 | });
41 |
--------------------------------------------------------------------------------
/static/room.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | avalon
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
号 人房间
16 |
提示: 游戏准备中...
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
增加[梅林]
28 |
增加[派西维尔]
29 |
增加[莫甘娜]
30 |
增加[莫德雷德]
31 |
增加[奥伯伦]
32 |
兰斯洛特[规则1]
33 |
兰斯洛特[规则2]
34 |
兰斯洛特[规则3]
35 |
36 |
37 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
亚瑟的骑士
49 |
50 | 正义的一方, 保护梅林,对抗爪牙。。。
51 |
52 |
53 |
54 | 用户1
55 | 用户2
56 | 用户3
57 |
58 |
59 |
身份
60 |
61 |
62 |
63 |
###提案阶段###
64 |
任务x 第x个提案
65 |
请选出x人
66 |
67 |
68 |
u374837
69 |
u374837
70 |
u374837
71 |
u374837
72 |
u374837
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
--------------------------------------------------------------------------------
/static/room.js:
--------------------------------------------------------------------------------
1 | // 请求房间状态 ajax long pulling update 状态
2 | // 返回结果 更新 页面内容
3 | // cookie 获取当前用户的user_id
4 | // click 操作的 绑定
5 |
6 | var room_number, version, userid, game_status;
7 | var user_ready = false;
8 |
9 | document.addEventListener("DOMContentLoaded", function(){
10 | userid = Ejoy.getCookie('userid');
11 | set_room_number();
12 | set_room_content(0, true);
13 | bind_action()
14 |
15 | function set_room_number(){
16 | var pathname = location.pathname.split('/')
17 | room_number = parseInt(pathname[pathname.length - 1], 10)
18 | Ejoy('room_number').html(room_number)
19 | }
20 |
21 | function set_room_content(v, poll_begin){
22 | var req = {
23 | roomid: room_number,
24 | status: 'prepare',
25 | action: 'request',
26 | version: v ? v : 0
27 | }
28 | Ejoy.postJSON('/room', req, function(resp){
29 | console.log('request', resp)
30 | if(game_status == "game"){return}
31 | if (resp.error) {return}
32 |
33 | version = resp.version;
34 | if(resp.status){
35 | if(resp.status == "game"){
36 |
37 | game_status = "game"
38 | prepare_clear()
39 | var avalon = new AvalonGame(userid)
40 | version = 0
41 | return avalon.begin(resp)
42 | }
43 | }
44 | if(resp.player){
45 | Ejoy('people_num').html(resp.player.length)
46 |
47 | render_players(resp.player)
48 | render_people_status(resp.player)
49 | render_prepare_action(resp.player)
50 | }
51 | if(resp.rule){
52 | render_rules(resp.rule)
53 | }
54 |
55 | render_game_status(resp.reason)
56 | render_begin_button(resp.can_game)
57 |
58 |
59 | if(poll_begin){
60 | console.log("polling")
61 | set_room_content(version, true)
62 | }
63 | })
64 | }
65 |
66 | function render_game_status(reason) {
67 | if (reason) {
68 | Ejoy("game-status").html(reason)
69 | } else {
70 | Ejoy("game-status").html("")
71 | }
72 | }
73 |
74 | function render_begin_button(can_game) {
75 | if (can_game) {
76 | Ejoy("begin_button").html("开始")
77 | } else {
78 | Ejoy("begin_button").html("")
79 | }
80 | }
81 |
82 | function prepare_clear(){
83 | var prepare = document.getElementsByClassName('prepare')[0]
84 | var game = document.getElementsByClassName('game')[0]
85 | prepare.style.display = "none"
86 | game.style.display = "block"
87 | }
88 |
89 | function render_players(players){
90 | var players_str = ""
91 | for(var i=0; i < players.length; i++){
92 | var player = players[i]
93 | var player_str = '' +
100 | player.username +
101 | '
';
102 | players_str += player_str;
103 | }
104 | Ejoy('people').html(players_str);
105 | }
106 |
107 | function render_rules(rules){
108 | if(!rules){
109 | rules = []
110 | }
111 | var rules_str = ""
112 | rules_dom = document.getElementsByClassName("room_rule")[0].children
113 | for(var i = 0; i < rules.length; i++){
114 | rules_dom[rules[i]-1].className += " rule_enabled"
115 | }
116 | }
117 |
118 | function render_people_status(players){
119 | var prepare=0, watch=0, ready=0;
120 | for(var i=0; i< players.length; i++){
121 | switch(players[i].status){
122 | case 0:
123 | ready++;
124 | break;
125 | case 1:
126 | prepare++;
127 | break;
128 | case 2:
129 | watch ++;
130 | break
131 | }
132 | }
133 |
134 | var status = "";
135 | status += '' + ready + '人准备好
' +
136 | '' + prepare + '人正在准备
' +
137 | '' + watch + '人旁观
'
138 | Ejoy('people_status').html(status)
139 | }
140 |
141 | function render_prepare_action(players){
142 | var username="";
143 | for(var i=0; i< players.length; i++){
144 | var player = players[i]
145 | if(player.userid == userid){
146 | user_ready = player.status
147 | username = player.username
148 | break;
149 | }
150 | }
151 | var action = user_ready == 0 ? "取消准备" :"准备";
152 | Ejoy('action_button').html(action)
153 | document.getElementsByClassName('action_value')[0].value = username
154 | }
155 |
156 | function bind_action(){
157 | Ejoy('people').on("click", "people_item", function(select_dom){
158 | var userid = select_dom.id;
159 | console.log(userid)
160 | kick_user(userid, select_dom.children[0])
161 | });
162 |
163 | Ejoy('room_rule').on('click', 'rule_item', function(select_dom){
164 | var rule_num = select_dom.dataset.rule
165 | var enabled = !(select_dom.className.indexOf("rule_enabled") > -1)
166 |
167 | if(enabled){
168 | select_dom.className += " rule_enabled"
169 | }else{
170 | select_dom.className = "rule_item"
171 | }
172 | set_rule(rule_num, enabled)
173 | })
174 |
175 | Ejoy('action_button').on('click', function(e){
176 | var name = e.target.previousElementSibling.value
177 | if(!name){
178 | return alert("请输入名字");
179 | }
180 | set_user_name(name)
181 | })
182 |
183 | Ejoy("begin_button").on("click", function (e){
184 | begin_game()
185 | })
186 | }
187 |
188 |
189 | function kick_user(userid, select_dom){
190 | var req = {
191 | roomid: room_number,
192 | status: 'prepare',
193 | action: 'kick',
194 | version: version,
195 |
196 | id: userid
197 | }
198 | Ejoy.postJSON('/room', req, function(resp){
199 | if(!resp.error){
200 | select_dom.className = select_dom.className.replace(/status_\d/, 'status_2')
201 | set_room_content()
202 | }
203 | });
204 |
205 | }
206 |
207 | function set_user_name(username){
208 | var req = {
209 | roomid: room_number,
210 | status: 'prepare',
211 | action: 'setname',
212 | version: version,
213 | username: username
214 | }
215 | Ejoy.postJSON('/room', req, function(resp){
216 | if(!resp.error){
217 | name_span = document.getElementById(userid).children[0]
218 | name_span.innerHTML = username
219 | set_ready()
220 | }
221 | });
222 |
223 | }
224 |
225 | function set_rule(rule_num, enable){
226 | var req = {
227 | roomid: room_number,
228 | status: 'prepare',
229 | action: 'set',
230 | version: version,
231 |
232 | rule: rule_num,
233 | enable: enable
234 | }
235 | Ejoy.postJSON('/room', req, function(resp){
236 | set_room_content()
237 | });
238 |
239 | }
240 |
241 | function set_ready(){
242 | var req = {
243 | roomid: room_number,
244 | status: 'prepare',
245 | action: 'ready',
246 | version: version,
247 | enable: !!user_ready,
248 | }
249 | Ejoy.postJSON('/room', req, function(resp){
250 | if(!resp.error){
251 | set_room_content();
252 | }
253 | })
254 | }
255 |
256 | function begin_game() {
257 | var req = {
258 | roomid: room_number,
259 | status: "prepare",
260 | action: "begin_game",
261 | version:version,
262 | }
263 | Ejoy.postJSON('/room', req, function(resp){
264 | if(!resp.error){
265 | set_room_content();
266 | }
267 | })
268 | }
269 | });
270 |
--------------------------------------------------------------------------------
/static/simple_lib.js:
--------------------------------------------------------------------------------
1 | // 简单的selector + manipulation + evnet bind 类似jquery 的 链式写法
2 | // ajax
3 |
4 | var Ejoy = function(selector){
5 | return new Ejoy.fn.init(selector);
6 | }
7 |
8 | Ejoy.array_remove = function(array, target){
9 | var index = array.indexOf(target);
10 | if (index > -1) {
11 | array.splice(index, 1);
12 | }
13 | }
14 |
15 | Ejoy.url_params = function(dict){
16 | var str = ""
17 | for( k in dict){
18 | str += k + "=" + dict[k] + "&";
19 | }
20 | return str.slice(0, -1)
21 | }
22 | Ejoy.postJSON = function(url, req, callback){
23 | var xmlhttp = new XMLHttpRequest(); // new HttpRequest instance
24 | xmlhttp.open("POST", url);
25 | xmlhttp.timeout = 1000 * 120;
26 | //xmlhttp.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
27 | xmlhttp.onreadystatechange = function() {
28 | if (xmlhttp.readyState == 4) {
29 | try{
30 | var data = JSON.parse(xmlhttp.responseText)
31 | }
32 | catch(e){
33 | return console.log(e)
34 | }
35 | callback(data)
36 | }
37 | }
38 | xmlhttp.send(JSON.stringify(req));
39 | //xmlhttp.send(Ejoy.url_params(req));
40 |
41 | }
42 |
43 | Ejoy.getCookie = function(sKey){
44 | if (!sKey) { return null; }
45 | return decodeURIComponent(
46 | document.cookie.replace(new RegExp("(?:(?:^|.*;)\\s*" + encodeURIComponent(sKey).replace(/[\-\.\+\*]/g, "\\$&") + "\\s*\\=\\s*([^;]*).*$)|^.*$"), "$1")
47 | ) || null;
48 | }
49 |
50 | Ejoy.fn = Ejoy.prototype = {constructor: Ejoy};
51 |
52 |
53 | var init = Ejoy.fn.init = function(selector){
54 | this.dom = document.getElementsByClassName(selector)
55 | this.len = this.dom.length
56 | this.selector = selector
57 | }
58 |
59 | init.prototype = Ejoy.fn;
60 |
61 |
62 | Ejoy.fn.html = function(val){
63 | for(var i = 0; i< this.len; i++){
64 | this.dom[i].innerHTML = val
65 | }
66 | return this
67 | };
68 |
69 | Ejoy.fn.css = function(css){
70 | for(var i = 0; i< this.len; i++){
71 | this.dom[i].style.cssText = css
72 | }
73 | return this
74 |
75 | }
76 |
77 | Ejoy.fn.on = function(event, selector, data, fn){
78 | var self = this;
79 | if ( data == null && fn == null ) {
80 | // ( types, fn )
81 | fn = selector;
82 | data = selector = undefined;
83 | } else if ( fn == null ) {
84 | if ( typeof selector === "string" ) {
85 | // ( types, selector, fn )
86 | fn = data;
87 | data = undefined;
88 | } else {
89 | // ( types, data, fn )
90 | fn = data;
91 | data = selector;
92 | selector = undefined;
93 | }
94 | }
95 | for(var i = 0; i < this.len; i++){
96 | var dom = this.dom[i]
97 | dom.addEventListener(event, function(e) {
98 | if(selector){
99 | var select_dom = filter_selector(selector, e.target, dom)
100 | if(select_dom){
101 | e.preventDefault();
102 | fn(select_dom, data)
103 | }
104 | }else{
105 | e.preventDefault();
106 | fn(e, data)
107 | }
108 | });
109 | }
110 | return this
111 | };
112 |
113 | function filter_selector(selector, dom, end){
114 | if(dom.className.indexOf(selector) > -1){
115 | return dom;
116 | }else if(dom == end){
117 | return false
118 | }else if(dom.parentElement == end){
119 | return false;
120 | }else{
121 | return filter_selector(selector, dom.parentElement, end)
122 | }
123 |
124 | }
125 |
--------------------------------------------------------------------------------