├── .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' 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 |
    19 |
  • 创建房间
  • 20 |
  • 进入房间
  • 21 |
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 |
38 | 准备 39 | 40 | 41 |
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 | 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 | --------------------------------------------------------------------------------