├── .env
├── .env.example
├── .gitignore
├── .gitlab-ci.yml
├── .php_cs
├── .phpstorm.meta.php
├── LICENSE
├── README.md
├── app
├── Controller
│ ├── AbstractController.php
│ ├── IndexController.php
│ └── WebSocketController.php
├── Exception
│ └── Handler
│ │ └── AppExceptionHandler.php
├── Game
│ ├── Conf
│ │ ├── MainCmd.php
│ │ ├── Route.php
│ │ └── SubCmd.php
│ ├── Core
│ │ ├── AStrategy.php
│ │ ├── DdzPoker.php
│ │ ├── Dispatch.php
│ │ ├── JokerPoker.php
│ │ ├── Log.php
│ │ └── Packet.php
│ └── Logic
│ │ ├── ChatMsg.php
│ │ ├── GameCall.php
│ │ ├── GameOutCard.php
│ │ ├── GameRoomCreate.php
│ │ ├── GameRoomJoin.php
│ │ ├── GameStart.php
│ │ └── HeartAsk.php
├── Helper.php
├── Listener
│ └── DbQueryExecutedListener.php
├── Log.php
├── Model
│ └── Model.php
└── Task
│ └── GameSyncTask.php
├── bin
└── hyperf.php
├── composer.json
├── config
├── autoload
│ ├── annotations.php
│ ├── aspects.php
│ ├── cache.php
│ ├── commands.php
│ ├── databases.php
│ ├── dependencies.php
│ ├── devtool.php
│ ├── exceptions.php
│ ├── game.php
│ ├── listeners.php
│ ├── logger.php
│ ├── middlewares.php
│ ├── processes.php
│ ├── redis.php
│ ├── server.php
│ └── view.php
├── config.php
├── container.php
└── routes.php
├── deploy.test.yml
├── phpstan.neon
├── phpunit.xml
├── public
├── client
│ ├── Const.js
│ ├── Init.js
│ ├── Packet.js
│ ├── Req.js
│ ├── Resp.js
│ └── msgpack.js
└── example
│ ├── 1.png
│ └── 2.png
├── storage
└── view
│ ├── index.html
│ └── login.html
└── test
├── Cases
└── ExampleTest.php
├── HttpTestCase.php
└── bootstrap.php
/.env:
--------------------------------------------------------------------------------
1 | APP_NAME=skeleton
2 |
3 | DB_DRIVER=mysql
4 | DB_HOST=localhost
5 | DB_PORT=3306
6 | DB_DATABASE=hyperf
7 | DB_USERNAME=root
8 | DB_PASSWORD=
9 | DB_CHARSET=utf8mb4
10 | DB_COLLATION=utf8mb4_unicode_ci
11 | DB_PREFIX=
12 |
13 | REDIS_HOST=localhost
14 | REDIS_AUTH=(null)
15 | REDIS_PORT=6379
16 | REDIS_DB=0
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | APP_NAME=skeleton
2 |
3 | DB_DRIVER=mysql
4 | DB_HOST=localhost
5 | DB_PORT=3306
6 | DB_DATABASE=hyperf
7 | DB_USERNAME=root
8 | DB_PASSWORD=
9 | DB_CHARSET=utf8mb4
10 | DB_COLLATION=utf8mb4_unicode_ci
11 | DB_PREFIX=
12 |
13 | REDIS_HOST=localhost
14 | REDIS_AUTH=(null)
15 | REDIS_PORT=6379
16 | REDIS_DB=0
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .buildpath
2 | .settings/
3 | .project
4 | *.patch
5 | .idea/
6 | .git/
7 | runtime/
8 | vendor/
9 | .phpintel/
10 | .DS_Store
11 | *.lock
12 | .phpunit*
--------------------------------------------------------------------------------
/.gitlab-ci.yml:
--------------------------------------------------------------------------------
1 | # usermod -aG docker gitlab-runner
2 |
3 | stages:
4 | - build
5 | - deploy
6 |
7 | variables:
8 | PROJECT_NAME: hyperf
9 | REGISTRY_URL: registry-docker.org
10 |
11 | build_test_docker:
12 | stage: build
13 | before_script:
14 | # - git submodule sync --recursive
15 | # - git submodule update --init --recursive
16 | script:
17 | - docker build . -t $PROJECT_NAME
18 | - docker tag $PROJECT_NAME $REGISTRY_URL/$PROJECT_NAME:test
19 | - docker push $REGISTRY_URL/$PROJECT_NAME:test
20 | only:
21 | - test
22 | tags:
23 | - builder
24 |
25 | deploy_test_docker:
26 | stage: deploy
27 | script:
28 | - docker stack deploy -c deploy.test.yml --with-registry-auth $PROJECT_NAME
29 | only:
30 | - test
31 | tags:
32 | - test
33 |
34 | build_docker:
35 | stage: build
36 | before_script:
37 | # - git submodule sync --recursive
38 | # - git submodule update --init --recursive
39 | script:
40 | - docker build . -t $PROJECT_NAME
41 | - docker tag $PROJECT_NAME $REGISTRY_URL/$PROJECT_NAME:$CI_COMMIT_REF_NAME
42 | - docker tag $PROJECT_NAME $REGISTRY_URL/$PROJECT_NAME:latest
43 | - docker push $REGISTRY_URL/$PROJECT_NAME:$CI_COMMIT_REF_NAME
44 | - docker push $REGISTRY_URL/$PROJECT_NAME:latest
45 | only:
46 | - tags
47 | tags:
48 | - builder
49 |
50 | deploy_docker:
51 | stage: deploy
52 | script:
53 | - echo SUCCESS
54 | only:
55 | - tags
56 | tags:
57 | - builder
58 |
--------------------------------------------------------------------------------
/.php_cs:
--------------------------------------------------------------------------------
1 | setRiskyAllowed(true)
14 | ->setRules([
15 | '@PSR2' => true,
16 | '@Symfony' => true,
17 | '@DoctrineAnnotation' => true,
18 | '@PhpCsFixer' => true,
19 | 'header_comment' => [
20 | 'commentType' => 'PHPDoc',
21 | 'header' => $header,
22 | 'separate' => 'none',
23 | 'location' => 'after_declare_strict',
24 | ],
25 | 'array_syntax' => [
26 | 'syntax' => 'short'
27 | ],
28 | 'list_syntax' => [
29 | 'syntax' => 'short'
30 | ],
31 | 'concat_space' => [
32 | 'spacing' => 'one'
33 | ],
34 | 'blank_line_before_statement' => [
35 | 'statements' => [
36 | 'declare',
37 | ],
38 | ],
39 | 'general_phpdoc_annotation_remove' => [
40 | 'annotations' => [
41 | 'author'
42 | ],
43 | ],
44 | 'ordered_imports' => [
45 | 'imports_order' => [
46 | 'class', 'function', 'const',
47 | ],
48 | 'sort_algorithm' => 'alpha',
49 | ],
50 | 'single_line_comment_style' => [
51 | 'comment_types' => [
52 | ],
53 | ],
54 | 'yoda_style' => [
55 | 'always_move_variable' => false,
56 | 'equal' => false,
57 | 'identical' => false,
58 | ],
59 | 'phpdoc_align' => [
60 | 'align' => 'left',
61 | ],
62 | 'multiline_whitespace_before_semicolons' => [
63 | 'strategy' => 'no_multi_line',
64 | ],
65 | 'class_attributes_separation' => true,
66 | 'combine_consecutive_unsets' => true,
67 | 'declare_strict_types' => true,
68 | 'linebreak_after_opening_tag' => true,
69 | 'lowercase_constants' => true,
70 | 'lowercase_static_reference' => true,
71 | 'no_useless_else' => true,
72 | 'no_unused_imports' => true,
73 | 'not_operator_with_successor_space' => true,
74 | 'not_operator_with_space' => false,
75 | 'ordered_class_elements' => true,
76 | 'php_unit_strict' => false,
77 | 'phpdoc_separation' => false,
78 | 'single_quote' => true,
79 | 'standardize_not_equals' => true,
80 | 'multiline_comment_opening_closing' => true,
81 | ])
82 | ->setFinder(
83 | PhpCsFixer\Finder::create()
84 | ->exclude('public')
85 | ->exclude('runtime')
86 | ->exclude('vendor')
87 | ->in(__DIR__)
88 | )
89 | ->setUsingCache(false);
90 |
--------------------------------------------------------------------------------
/.phpstorm.meta.php:
--------------------------------------------------------------------------------
1 |
47 |
--------------------------------------------------------------------------------
/app/Controller/AbstractController.php:
--------------------------------------------------------------------------------
1 | _isLogin($this->request)) {
24 | return $this->response->redirect('/login');
25 | }
26 | //用户信息传递到客户端
27 | $info = $this->request->getCookieParams();
28 | $u = json_decode($info['USER_INFO'], true);
29 | $host = $this->request->getHeaderLine('host');
30 | $data = array_merge($u, ['host' => $host]);
31 | return $render->render('index', $data);
32 | }
33 |
34 | /**
35 | * @return \Psr\Http\Message\ResponseInterface
36 | */
37 | public function login(RequestInterface $request, ResponseInterface $response, RenderInterface $render)
38 | {
39 | $action = $request->post('action');
40 | $account = $request->post('account');
41 | $tips = '';
42 | if ($action == 'login') {
43 | if (! empty($account)) {
44 | //注册登录
45 | $uinfo = ['account' => $account];
46 | $cookie = new Cookie('USER_INFO', json_encode($uinfo));
47 | $response = $response->withCookie($cookie);
48 | return $response->redirect('/');
49 | }
50 | $tips = '温馨提示:用户账号不能为空!';
51 | }
52 | return $render->render('login', ['tips' => $tips]);
53 | }
54 |
55 | /**
56 | * 是否登录.
57 | * @return bool
58 | */
59 | private function _isLogin(RequestInterface $request)
60 | {
61 | $cookie_info = $request->getCookieParams();
62 | if (isset($cookie_info['USER_INFO'])) {
63 | $this->userinfo = json_decode($cookie_info['USER_INFO']);
64 | return true;
65 | }
66 | return false;
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/app/Controller/WebSocketController.php:
--------------------------------------------------------------------------------
1 | container = $container;
38 | }
39 |
40 | public function onMessage($server, Frame $frame): void
41 | {
42 | Log::show(" Message: client #{$frame->fd} push success Mete: \n{");
43 | $data = Packet::packDecode($frame->data);
44 | if (isset($data['code']) && $data['code'] == 0 && isset($data['msg']) && $data['msg'] == 'OK') {
45 | Log::show('Recv <<< cmd=' . $data['cmd'] . ' scmd=' . $data['scmd'] . ' len=' . $data['len'] . ' data=' . json_encode($data['data']));
46 | //转发请求,代理模式处理,websocket路由到相关逻辑
47 | $data['serv'] = $server;
48 | //用户登陆信息
49 | $game_conf = config('game');
50 | $user_info_key = sprintf($game_conf['user_info_key'], $frame->fd);
51 | $uinfo = redis()->get($user_info_key);
52 | if ($uinfo) {
53 | $data['userinfo'] = json_decode($uinfo, true);
54 | } else {
55 | $data['userinfo'] = [];
56 | }
57 | $obj = new Dispatch($data, $this->container);
58 | $back = "
404 Not Found
Swoole\n";
59 | if (! empty($obj->getStrategy())) {
60 | $back = $obj->exec();
61 | if ($back) {
62 | $server->push((int) $frame->fd, $back, WEBSOCKET_OPCODE_BINARY);
63 | }
64 | }
65 | Log::show('Tcp Strategy <<< data=' . $back);
66 | } else {
67 | Log::show($data['msg']);
68 | }
69 | Log::split('}');
70 | }
71 |
72 | public function onClose($server, int $fd, int $reactorId): void
73 | {
74 | //清除登陆信息变量
75 | $this->loginFail($fd, '3');
76 | }
77 |
78 | public function onOpen($server, Request $request): void
79 | {
80 | $fd = $request->fd;
81 | $game_conf = config('game');
82 | $query = $request->get;
83 | $cookie = $request->cookie;
84 | $token = '';
85 | if (isset($cookie['USER_INFO'])) {
86 | $token = $cookie['USER_INFO'];
87 | } elseif (isset($query['token'])) {
88 | $token = $query['token'];
89 | }
90 | if ($token) {
91 | $uinfo = json_decode($token, true);
92 | //允许连接, 并记录用户信息
93 | $uinfo['fd'] = $fd;
94 | $redis = redis();
95 | $user_bind_key = sprintf($game_conf['user_bind_key'], $uinfo['account']);
96 | $last_fd = (int) $redis->get($user_bind_key);
97 | //之前信息存在,清除之前的连接
98 | if ($last_fd) {
99 | if ($server->exist($last_fd) && $server->isEstablished($last_fd)) {
100 | //处理双开的情况
101 | $this->loginFail($last_fd, '1');
102 | $server->disconnect($last_fd);
103 | }
104 | //清理redis
105 | $redis->del($user_bind_key); //清除上一个绑定关系
106 | $redis->del(sprintf($game_conf['user_info_key'], $last_fd)); //清除上一个用户信息
107 | }
108 | //保存登陆信息
109 | $redis->set($user_bind_key, $fd, $game_conf['expire']);
110 | //设置绑定关系
111 | $redis->set(sprintf($game_conf['user_info_key'], $fd), json_encode($uinfo), $game_conf['expire']);
112 | $this->loginSuccess($server, $fd, $uinfo['account']); //登陆成功
113 | } else {
114 | if ($server->exist($fd) && $server->isEstablished($fd)) {
115 | $this->loginFail($fd, '2');
116 | $server->disconnect($fd);
117 | }
118 | }
119 | }
120 |
121 | /**
122 | * 获取房间信息.
123 | * @param $account
124 | * @return array
125 | */
126 | protected function getRoomData($account)
127 | {
128 | $user_room_data = [];
129 | //获取用户房间号
130 | $room_no = $this->getRoomNo($account);
131 | //房间信息
132 | $game_key = $this->getGameConf('user_room_data');
133 | if ($game_key) {
134 | $user_room_key = sprintf($game_key, $room_no);
135 | $user_room_data = redis()->hGetAll($user_room_key);
136 | }
137 | return $user_room_data;
138 | }
139 |
140 | /**
141 | * 获取用户房间号.
142 | * @param $account
143 | * @return mixed
144 | */
145 | protected function getRoomNo($account)
146 | {
147 | $game_key = $this->getGameConf('user_room');
148 | //获取用户房间号
149 | $room_key = sprintf($game_key, $account);
150 | $room_no = redis()->get($room_key);
151 | return $room_no ? $room_no : 0;
152 | }
153 |
154 | /**
155 | * 返回游戏配置.
156 | * @param string $key
157 | * @return string
158 | */
159 | protected function getGameConf($key = '')
160 | {
161 | $conf = config('game');
162 | if (isset($conf[$key])) {
163 | return $conf[$key];
164 | }
165 | return '';
166 | }
167 |
168 | /**
169 | * 登陆成功下发协议.
170 | * @param $server
171 | * @param $fd
172 | * @param $account
173 | */
174 | private function loginSuccess($server, $fd, $account)
175 | {
176 | //原封不动发回去
177 | if ($server->getClientInfo((int) $fd) !== false) {
178 | //查询用户是否在房间里面
179 | $info = $this->getRoomData($account);
180 | $data = ['status' => 'success'];
181 | if (! empty($info)) {
182 | $data['is_room'] = 1;
183 | } else {
184 | $data['is_room'] = 0;
185 | }
186 | $data = Packet::packFormat('OK', 0, $data);
187 | $back = Packet::packEncode($data, MainCmd::CMD_SYS, SubCmd::LOGIN_SUCCESS_RESP);
188 | $server->push((int) $fd, $back, WEBSOCKET_OPCODE_BINARY);
189 | }
190 | }
191 |
192 | /**
193 | * 发送登陆失败请求到客户端.
194 | * @param $server
195 | * @param $fd
196 | * @param string $msg
197 | */
198 | private function loginFail($fd, $msg = '')
199 | {
200 | //原封不动发回去
201 | $server = server();
202 | if ($server->getClientInfo((int) $fd) !== false) {
203 | $data = Packet::packFormat('OK', 0, ['data' => 'login fail' . $msg]);
204 | $back = Packet::packEncode($data, MainCmd::CMD_SYS, SubCmd::LOGIN_FAIL_RESP);
205 | if ($server->exist($fd) && $server->isEstablished($fd)) {
206 | $server->push((int) $fd, $back, WEBSOCKET_OPCODE_BINARY);
207 | }
208 | }
209 | }
210 | }
211 |
--------------------------------------------------------------------------------
/app/Exception/Handler/AppExceptionHandler.php:
--------------------------------------------------------------------------------
1 | logger = $logger;
30 | }
31 |
32 | public function handle(Throwable $throwable, ResponseInterface $response)
33 | {
34 | $this->logger->error(sprintf('%s[%s] in %s', $throwable->getMessage(), $throwable->getLine(), $throwable->getFile()));
35 | $this->logger->error($throwable->getTraceAsString());
36 | return $response->withStatus(500)->withBody(new SwooleStream('Internal Server Error.'));
37 | }
38 |
39 | public function isValid(Throwable $throwable): bool
40 | {
41 | return true;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/app/Game/Conf/MainCmd.php:
--------------------------------------------------------------------------------
1 | [
28 | SubCmd::HEART_ASK_REQ => 'HeartAsk',
29 | ],
30 | //游戏请求
31 | MainCmd::CMD_GAME => [
32 | SubCmd::SUB_GAME_START_REQ => 'GameStart',
33 | SubCmd::SUB_GAME_CALL_REQ => 'GameCall',
34 | SubCmd::SUB_GAME_DOUBLE_REQ => 'GameDouble',
35 | SubCmd::SUB_GAME_OUT_CARD_REQ => 'GameOutCard',
36 | SubCmd::CHAT_MSG_REQ => 'ChatMsg',
37 | SubCmd::SUB_GAME_ROOM_CREATE => 'GameRoomCreate',
38 | SubCmd::SUB_GAME_ROOM_JOIN => 'GameRoomJoin',
39 | ],
40 | ];
41 | }
42 |
--------------------------------------------------------------------------------
/app/Game/Conf/SubCmd.php:
--------------------------------------------------------------------------------
1 | CGameStart
42 |
43 | const SUB_GAME_START_RESP = 2; //游戏场景---> CGameScence
44 |
45 | const SUB_USER_INFO_RESP = 3; //用户信息 ------> CUserInfo
46 |
47 | const SUB_GAME_SEND_CARD_RESP = 4; //发牌 ------> CGameSendCard
48 |
49 | const SUB_GAME_CALL_TIPS_RESP = 5; //叫地主提示(广播) --> CGameCall
50 |
51 | const SUB_GAME_CALL_REQ = 6; //叫地主请求 --> CGameCallReq
52 |
53 | const SUB_GAME_CALL_RESP = 7; //叫地主请求返回--CGameCallResp
54 |
55 | const SUB_GAME_DOUBLE_TIPS_RESP = 8; //加倍提示(广播) --> CGameDouble
56 |
57 | const SUB_GAME_DOUBLE_REQ = 9; //加倍请求--> CGameDoubleReq
58 |
59 | const SUB_GAME_DOUBLE_RESP = 10; //加倍请求返回----> CGameDoubleResp
60 |
61 | const SUB_GAME_CATCH_BASECARD_RESP = 11; //摸底牌 ---> CGameCatchBaseCard
62 |
63 | const SUB_GAME_OUT_CARD = 12; //出牌提示 --> CGameOutCard
64 |
65 | const SUB_GAME_OUT_CARD_REQ = 13; //出牌请求 --> CGameOutCardReq
66 |
67 | const SUB_GAME_OUT_CARD_RESP = 14; //出牌返回 --> CGameOutCardResp
68 |
69 | const SUB_GAME_ROOM_CREATE = 31; //创建房间
70 |
71 | const SUB_GAME_ROOM_JOIN = 32; //加入房间
72 |
73 | const CHAT_MSG_REQ = 213; //聊天消息请求,客户端使用
74 |
75 | const CHAT_MSG_RESP = 214; //聊天消息响应,服务端使用
76 | }
77 |
--------------------------------------------------------------------------------
/app/Game/Core/AStrategy.php:
--------------------------------------------------------------------------------
1 | _params = $params;
41 | $this->obj_ddz = new DdzPoker();
42 | }
43 |
44 | /**
45 | * 执行方法,每条游戏协议,实现这个方法就行.
46 | */
47 | abstract public function exec();
48 |
49 | /**
50 | * 服务器广播消息, 此方法是给所有的连接客户端, 广播消息.
51 | * @param $serv
52 | * @param $data
53 | */
54 | protected function Broadcast($serv, $data)
55 | {
56 | foreach ($serv->connections as $fd) {
57 | $serv->push((int) $fd, $data, WEBSOCKET_OPCODE_BINARY);
58 | }
59 | }
60 |
61 | /**
62 | * 当connetions属性无效时可以使用此方法,服务器广播消息, 此方法是给所有的连接客户端, 广播消息,通过方法getClientList广播.
63 | * @param $serv
64 | * @param $data
65 | */
66 | protected function BroadCast2($serv, $data)
67 | {
68 | $start_fd = 0;
69 | while (true) {
70 | $conn_list = $serv->getClientList($start_fd, 10);
71 | if ($conn_list === false or count($conn_list) === 0) {
72 | Log::show('BroadCast finish');
73 | break;
74 | }
75 | $start_fd = end($conn_list);
76 | foreach ($conn_list as $fd) {
77 | //获取客户端信息
78 | $client_info = $serv->getClientInfo((int) $fd);
79 | if (isset($client_info['websocket_status']) && $client_info['websocket_status'] == 3) {
80 | $serv->push((int) $fd, $data, WEBSOCKET_OPCODE_BINARY);
81 | }
82 | }
83 | }
84 | }
85 |
86 | /**
87 | * 对多用发送信息.
88 | * @param $serv
89 | * @param $users
90 | * @param $data
91 | */
92 | protected function pushToUsers($serv, $users, $data)
93 | {
94 | foreach ($users as $fd) {
95 | //获取客户端信息
96 | $client_info = $serv->getClientInfo((int) $fd);
97 | $client[$fd] = $client_info;
98 | if (isset($client_info['websocket_status']) && $client_info['websocket_status'] == 3) {
99 | $serv->push((int) $fd, $data, WEBSOCKET_OPCODE_BINARY);
100 | }
101 | }
102 | }
103 |
104 | /**
105 | * 获取房间信息.
106 | * @param $account
107 | * @return array
108 | */
109 | protected function getRoomData($account)
110 | {
111 | $user_room_data = [];
112 | //获取用户房间号
113 | $room_no = $this->getRoomNo($account);
114 | //房间信息
115 | $game_key = $this->getGameConf('user_room_data');
116 | if ($game_key) {
117 | $user_room_key = sprintf($game_key, $room_no);
118 | $user_room_data = redis()->hGetAll($user_room_key);
119 | }
120 | return $user_room_data;
121 | }
122 |
123 | /**
124 | * 获取房间信息通过key.
125 | * @param $account
126 | * @param $key
127 | * @return mixed
128 | */
129 | protected function getRoomDataByKey($account, $key)
130 | {
131 | $data = [];
132 | $no = $this->getRoomNo($account);
133 | $game_key = $this->getGameConf('user_room_data');
134 | if ($no && $game_key) {
135 | $user_room_key = sprintf($game_key, $no);
136 | $user_room_data = redis()->hGet($user_room_key, $key);
137 | $data = json_decode($user_room_data, true);
138 | if (is_null($data)) {
139 | $data = $user_room_data;
140 | }
141 | }
142 | return $data;
143 | }
144 |
145 | /**
146 | * 获取用户房间号.
147 | * @param $account
148 | * @return mixed
149 | */
150 | protected function getRoomNo($account)
151 | {
152 | $game_key = $this->getGameConf('user_room');
153 | //获取用户房间号
154 | $room_key = sprintf($game_key, $account);
155 | $room_no = redis()->get($room_key);
156 | return $room_no ? $room_no : 0;
157 | }
158 |
159 | /**
160 | * 获取房间信息通过key.
161 | * @param $account
162 | * @return mixed
163 | */
164 | protected function getRoomUserInfoDataByKey($account)
165 | {
166 | $user_data = [];
167 | $no = $this->getRoomNo($account);
168 | $game_key = $this->getGameConf('user_room_data');
169 | if ($no && $game_key) {
170 | //房间信息
171 | $user_room_key = sprintf($game_key, $no);
172 | $user_data = redis()->hGet($user_room_key, $account);
173 | $user_data = json_decode($user_data, true);
174 | }
175 | return $user_data;
176 | }
177 |
178 | /**
179 | * 设置房间用户玩牌信息.
180 | * @param $account
181 | * @param $key
182 | * @param $value
183 | */
184 | protected function setRoomData($account, $key, $value)
185 | {
186 | $no = $this->getRoomNo($account);
187 | $game_key = $this->getGameConf('user_room_data');
188 | if ($no && $game_key) {
189 | $user_room_key = sprintf($game_key, $no);
190 | redis()->hSet((string) $user_room_key, (string) $key, $value);
191 | }
192 | }
193 |
194 | /**
195 | * 批量设置房间信息.
196 | * @param $account
197 | * @param $params
198 | */
199 | protected function muitSetRoomData($account, $params)
200 | {
201 | $no = $this->getRoomNo($account);
202 | $game_key = $this->getGameConf('user_room_data');
203 | if ($no && $game_key) {
204 | $user_room_key = sprintf($game_key, $no);
205 | redis()->hMSet($user_room_key, $params);
206 | }
207 | }
208 |
209 | /**
210 | * 设置房间信息.
211 | * @param $room_user_data
212 | * @param $account
213 | * @param $key
214 | * @param $value
215 | */
216 | protected function setRoomUserInfoDataByKey($room_user_data, $account, $key, $value)
217 | {
218 | $no = $this->getRoomNo($account);
219 | $game_key = $this->getGameConf('user_room_data');
220 | if ($no && $game_key) {
221 | $room_user_data[$key] = $value;
222 | $user_room_key = sprintf($game_key, $no);
223 | redis()->hSet((string) $user_room_key, (string) $account, json_encode($room_user_data));
224 | }
225 | }
226 |
227 | /**
228 | * 通过accounts获取fds.
229 | * @param $account
230 | * @return array
231 | */
232 | protected function getRoomFds($account)
233 | {
234 | $accs = $this->getRoomDataByKey($account, 'uinfo');
235 | $game_key = $this->getGameConf('user_bind_key');
236 | $binds = $fds = [];
237 | if (! empty($accs) && $game_key) {
238 | foreach ($accs as $v) {
239 | $binds[] = sprintf($game_key, $v);
240 | }
241 | $fds = redis()->mget($binds);
242 | }
243 | return $fds;
244 | }
245 |
246 | /**
247 | * 批量清除用户房间号.
248 | * @param $users
249 | */
250 | protected function clearRoomNo($users)
251 | {
252 | $game_key = $this->getGameConf('user_room');
253 | if (is_array($users)) {
254 | foreach ($users as $v) {
255 | $key = sprintf($game_key, $v);
256 | redis()->del($key);
257 | }
258 | }
259 | }
260 |
261 | /**
262 | * 把php数组存入redis的hash表中.
263 | * @param $arr
264 | * @param $hash_key
265 | */
266 | protected function arrToHashInRedis($arr, $hash_key)
267 | {
268 | foreach ($arr as $key => $val) {
269 | redis()->hSet((string) $hash_key, (string) $key, json_encode($val));
270 | }
271 | }
272 |
273 | /**
274 | * 返回游戏配置.
275 | * @param string $key
276 | * @return string
277 | */
278 | protected function getGameConf($key = '')
279 | {
280 | $conf = config('game');
281 | if (isset($conf[$key])) {
282 | return $conf[$key];
283 | }
284 | return '';
285 | }
286 |
287 | /**
288 | * 设置游戏房间玩牌步骤信息, 方便后面录像回放.
289 | * @param $account
290 | * @param $key
291 | * @param $value
292 | */
293 | protected function setRoomPlayCardStep($account, $key, $value)
294 | {
295 | $no = $this->getRoomNo($account);
296 | $game_key = $this->getGameConf('user_room_play');
297 | if ($no && $game_key) {
298 | $play_key = sprintf($game_key, $no);
299 | redis()->hSet((string) $play_key, (string) $key, $value);
300 | }
301 | }
302 | }
303 |
--------------------------------------------------------------------------------
/app/Game/Core/Dispatch.php:
--------------------------------------------------------------------------------
1 | _params = $params;
41 | $this->Strategy();
42 | }
43 |
44 | /**
45 | * 逻辑处理策略路由转发, 游戏逻辑策略转发, 根据主命令字和子命令字来转发.
46 | */
47 | public function Strategy()
48 | {
49 | //获取路由策略
50 | $route = Route::$cmd_map;
51 | if (isset($this->_params['cmd'], $this->_params['scmd'])) {
52 | //获取策略类名
53 | $classname = $route[$this->_params['cmd']][$this->_params['scmd']] ?? '';
54 | //转发到对应目录处理逻辑
55 | $classname = 'App\Game\Logic\\' . $classname;
56 | if (class_exists($classname)) {
57 | $this->_strategy = new $classname($this->_params);
58 | Log::show("Class: {$classname}");
59 | } else {
60 | Log::show("Websockt Error: class is not support,cmd is {$this->_params['cmd']},scmd is {$this->_params['scmd']}");
61 | }
62 | }
63 | }
64 |
65 | /**
66 | * 获取策略.
67 | */
68 | public function getStrategy()
69 | {
70 | return $this->_strategy;
71 | }
72 |
73 | /**
74 | * 执行策略.
75 | */
76 | public function exec()
77 | {
78 | return $this->_strategy->exec();
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/app/Game/Core/JokerPoker.php:
--------------------------------------------------------------------------------
1 | 'A', 2 => '2', 3 => '3', 4 => '4', 5 => '5', 6 => '6', 7 => '7', 8 => '8', 9 => '9', 10 => '10', 11 => 'J', 12 => 'Q', 13 => 'K',
26 | 17 => 'A', 18 => '2', 19 => '3', 20 => '4', 21 => '5', 22 => '6', 23 => '7', 24 => '8', 25 => '9', 26 => '10', 27 => 'J', 28 => 'Q', 29 => 'K',
27 | 33 => 'A', 34 => '2', 35 => '3', 36 => '4', 37 => '5', 38 => '6', 39 => '7', 40 => '8', 41 => '9', 42 => '10', 43 => 'J', 44 => 'Q', 45 => 'K',
28 | 49 => 'A', 50 => '2', 51 => '3', 52 => '4', 53 => '5', 54 => '6', 55 => '7', 56 => '8', 57 => '9', 58 => '10', 59 => 'J', 60 => 'Q', 61 => 'K',
29 | 79 => 'JOKER',
30 | ];
31 |
32 | /**
33 | * 赖子的key值,和牌的key值对应.
34 | * @var int
35 | */
36 | public static $laizi_value = 79;
37 |
38 | /**
39 | * 花色.
40 | */
41 | public static $card_color = [
42 | 0 => '方块',
43 | 1 => '黑桃',
44 | 2 => '红桃',
45 | 3 => '梅花',
46 | ];
47 |
48 | /**
49 | * 牌型.
50 | * @var array
51 | */
52 | public static $card_type = [
53 | 0 => '非赢牌',
54 | 1 => '对K或者以上',
55 | 2 => '两对',
56 | 3 => '三条',
57 | 4 => '顺子',
58 | 5 => '同花',
59 | 6 => '葫芦',
60 | 7 => '四条',
61 | 8 => '同花顺',
62 | 9 => '五条',
63 | 10 => '带赖子皇家同花顺',
64 | 11 => '皇家同花顺',
65 | ];
66 |
67 | /**
68 | * 牌型赔付的倍率.
69 | * @var array
70 | */
71 | public static $card_rate = [
72 | 0 => 0,
73 | 1 => 1,
74 | 2 => 1,
75 | 3 => 2,
76 | 4 => 3,
77 | 5 => 5,
78 | 6 => 7,
79 | 7 => 17,
80 | 8 => 50,
81 | 9 => 100,
82 | 10 => 200,
83 | 11 => 250,
84 | ];
85 |
86 | /**
87 | * 是否翻倍的概率配置:1表示不翻倍回收奖励,2.表示再来一次 3,表示奖励翻倍.
88 | */
89 | public static $is_double_rate = [
90 | 1 => 5000,
91 | 2 => 1000,
92 | 3 => 4000,
93 | ];
94 |
95 | /**
96 | * 是否翻倍提示语.
97 | */
98 | public static $is_double_msg = [
99 | 1 => '不翻倍回收奖励',
100 | 2 => '再来一次,不回收奖励',
101 | 3 => '奖励翻倍',
102 | ];
103 |
104 | /**
105 | * 是否有赖子牌, 如果有赖子牌,这个值就是true, 默认false.
106 | */
107 | public static $is_laizi = false;
108 |
109 | /**
110 | * 是否为顺子,是true,否false.
111 | */
112 | public static $is_shunzi = false;
113 |
114 | /**
115 | * 是否为最大顺子,是true,否false.
116 | */
117 | public static $is_big_shunzi = false;
118 |
119 | /**
120 | * 是否为同花,是true,否false.
121 | */
122 | public static $is_tonghua = false;
123 |
124 | /**
125 | * 随机获取5张牌,如果参数指定n张牌, 就补齐5-n张牌.
126 | * @param mixed $arr
127 | */
128 | public static function getFiveCard($arr = [])
129 | {
130 | $card = self::$card_value_list;
131 | $num = 5 - count($arr);
132 | if ($num == 0) {
133 | $card_key = $arr;
134 | } else {
135 | //去除上面的牌, 防止重复出现
136 | foreach ($arr as $v) {
137 | unset($card[$v]);
138 | }
139 | $card_key = array_rand($card, $num);
140 | if (! is_array($card_key)) {
141 | $card_key = [$card_key];
142 | }
143 | $card_key = array_merge($card_key, $arr);
144 | }
145 | return $card_key;
146 | }
147 |
148 | /**
149 | * 随机获取1张牌,不包括王.
150 | */
151 | public static function getOneCard()
152 | {
153 | $card = self::$card_value_list;
154 | unset($card[79]);
155 | $card_key = array_rand($card, 1);
156 | if (! is_array($card_key)) {
157 | $card_key = [$card_key];
158 | }
159 | return $card_key;
160 | }
161 |
162 | /**
163 | * 获取牌内容,并显示花色, 方便直观查看.
164 | * @param mixed $arr
165 | */
166 | public static function showCard($arr)
167 | {
168 | $show = [];
169 | $card = self::getCard($arr);
170 | foreach ($card as $k => $v) {
171 | if ($k != self::$laizi_value) {
172 | $key = floor($k / 16);
173 | $show[] = self::$card_color[$key] . '_' . $v;
174 | } else {
175 | $show[] = $v;
176 | }
177 | }
178 | return implode(',', $show);
179 | }
180 |
181 | /**
182 | * 不带赖子皇家同花顺.
183 | */
184 | public static function isBigTongHuaShun()
185 | {
186 | return (self::$is_tonghua && self::$is_shunzi && self::$is_big_shunzi && ! self::$is_laizi) ? true : false;
187 | }
188 |
189 | /**
190 | * 带来赖子皇家同花顺.
191 | */
192 | public static function isBigTongHuaShunByLaizi()
193 | {
194 | return (self::$is_tonghua && self::$is_shunzi && self::$is_big_shunzi && self::$is_laizi) ? true : false;
195 | }
196 |
197 | /**
198 | * 是否为同花顺.
199 | */
200 | public static function isTongHuaShun()
201 | {
202 | return (self::$is_tonghua && self::$is_shunzi) ? true : false;
203 | }
204 |
205 | /**
206 | * 是否为同花牌,判断同花的算法.
207 | * @param mixed $arr
208 | */
209 | public static function isTongHua($arr)
210 | {
211 | $sub = [];
212 | foreach ($arr as $v) {
213 | $sub[] = floor($v / 16);
214 | }
215 | $u = array_unique($sub);
216 | if (count($u) == 1) {
217 | self::$is_tonghua = true;
218 | } else {
219 | self::$is_tonghua = false;
220 | }
221 | return self::$is_tonghua;
222 | }
223 |
224 | /**
225 | * 是否为顺子牌,判断顺子的算法.
226 | * @param mixed $arr
227 | */
228 | public static function isShunZi($arr)
229 | {
230 | $flag = 0;
231 | $card = self::getCard($arr);
232 | asort($card);
233 | $min = key($card) % 16;
234 | if ($min >= 2 && $min <= 10) {
235 | //最小或者最大顺子,需要特殊处理
236 | /* if(($min == 2 || $min == 10) && array_search('A', $card) !== false) {
237 | $flag++;
238 | } */
239 | if (array_search('A', $card) !== false) {
240 | if ($min == 2) {
241 | $min = 1;
242 | } elseif ($min == 10) {
243 | ++$flag;
244 | }
245 | }
246 | $cnt = count($arr);
247 | for ($i = 1; $i < 5; ++$i) {
248 | $next = $min + $i;
249 | if (in_array($next, $arr) || in_array(($next + 16), $arr) || in_array(($next + 32), $arr) || in_array(($next + 48), $arr)) {
250 | ++$flag;
251 | }
252 | }
253 | }
254 | if ($flag == $cnt - 1) {
255 | self::$is_shunzi = true;
256 | } else {
257 | self::$is_shunzi = false;
258 | }
259 | //是否为最大顺子,是true,否false
260 | if ($min == 10) {
261 | self::$is_big_shunzi = true;
262 | } else {
263 | self::$is_big_shunzi = false;
264 | }
265 | return self::$is_shunzi;
266 | }
267 |
268 | /**
269 | * 取模值,算对子,两对,三张,四条,5条的算法.
270 | * @param mixed $arr
271 | */
272 | public static function _getModValue($arr)
273 | {
274 | $flag = $type = 0;
275 | $mod = [];
276 | foreach ($arr as $k => $v) {
277 | $mod[] = $v % 16;
278 | }
279 | $v = array_count_values($mod);
280 | $cnt = count($v);
281 | if (self::$is_laizi) {
282 | if (in_array(1, $v) && $cnt == 4) {
283 | //对子
284 | $card = self::getCard($arr);
285 | if (array_search('A', $card) !== false || array_search('K', $card) !== false) {
286 | $type = 1; //对K或更大
287 | }
288 | } elseif (in_array(2, $v) && $cnt == 3) {
289 | $type = 3; //三张
290 | } elseif (in_array(2, $v) && $cnt == 2) {
291 | $type = 4; //葫芦
292 | } elseif (in_array(3, $v)) {
293 | $type = 5; //四条
294 | } elseif (in_array(4, $v)) {
295 | $type = 6; //五条
296 | }
297 | } else {
298 | if (in_array(2, $v) && $cnt == 4) {
299 | //对子
300 | $card = self::getCard($arr);
301 | $card_key = array_count_values($card);
302 | arsort($card_key);
303 | $kw = key($card_key);
304 | if ($kw == 'A' || $kw == 'K') {
305 | $type = 1; //对K或更大
306 | }
307 | } elseif (in_array(2, $v) && $cnt == 3) {
308 | $type = 2; //两对
309 | } elseif (in_array(3, $v) && $cnt == 3) {
310 | $type = 3; //三张
311 | } elseif (in_array(3, $v) && $cnt == 2) {
312 | $type = 4; //葫芦
313 | } elseif (in_array(4, $v)) {
314 | $type = 5; //四条
315 | }
316 | }
317 | return $type;
318 | }
319 |
320 | /**
321 | * 五张.
322 | * @param mixed $type
323 | */
324 | public static function isWuZhang($type)
325 | {
326 | return $type == 6 ? true : false;
327 | }
328 |
329 | /**
330 | * 四张.
331 | * @param mixed $type
332 | */
333 | public static function isSiZhang($type)
334 | {
335 | return $type == 5 ? true : false;
336 | }
337 |
338 | /**
339 | * 葫芦.
340 | * @param mixed $type
341 | */
342 | public static function isHulu($type)
343 | {
344 | return $type == 4 ? true : false;
345 | }
346 |
347 | /**
348 | * 三张.
349 | * @param mixed $type
350 | */
351 | public static function isSanZhang($type)
352 | {
353 | return $type == 3 ? true : false;
354 | }
355 |
356 | /**
357 | * 两对.
358 | * @param mixed $type
359 | */
360 | public static function isLiangDui($type)
361 | {
362 | return $type == 2 ? true : false;
363 | }
364 |
365 | /**
366 | * 大于对K或更大.
367 | * @param mixed $type
368 | */
369 | public static function isDaYuQDui($type)
370 | {
371 | return $type == 1 ? true : false;
372 | }
373 |
374 | /**
375 | * 检查牌型,判断用户所翻的牌为那种牌型.
376 | * @param mixed $arr
377 | */
378 | public static function checkCardType($arr)
379 | {
380 | //去除赖子牌
381 | $arr_card = self::exceptLaizi($arr);
382 | $type = self::_getModValue($arr_card);
383 | if (self::isWuZhang($type)) {
384 | return 9; //五条
385 | }
386 | if (self::isSiZhang($type)) {
387 | return 7; //四条
388 | }
389 | if (self::isHulu($type)) {
390 | return 6; //葫芦,三张两对
391 | }
392 | if (self::isSanZhang($type)) {
393 | return 3; //三张
394 | }
395 | if (self::isLiangDui($type)) {
396 | return 2; //两对
397 | }
398 | $back = 0;
399 | if (self::isDaYuQDui($type)) {
400 | $back = 1; //对K或者大于
401 | }
402 | if (self::isShunZi($arr_card)) {
403 | $back = 4; //是否为顺子
404 | }
405 | if (self::isTongHua($arr_card)) {
406 | $back = 5; //是否为同花
407 | }
408 | if (self::isTongHuaShun()) {
409 | $back = 8; //是否为同花顺
410 | }
411 | if (self::isBigTongHuaShunByLaizi()) {
412 | $back = 10; //带赖子皇家同花顺
413 | }
414 | if (self::isBigTongHuaShun()) {
415 | $back = 11; //皇家同花顺
416 | }
417 | return $back;
418 | }
419 |
420 | /**
421 | * 找出牌型里那些牌需要高亮显示.
422 | * @param mixed $arr
423 | * @param mixed $type
424 | */
425 | public static function highLight($arr, $type)
426 | {
427 | $card_key = [];
428 | $card = self::getCard($arr);
429 | $val = array_count_values($card);
430 | if ($type > 3) {
431 | $card_key = $arr;
432 | } elseif ($type == 3) {
433 | //三条
434 | arsort($val);
435 | $kw = key($val);
436 | $card_key = [];
437 | foreach ($card as $k => $v) {
438 | if ($v == $kw || $k == self::$laizi_value) {
439 | $card_key[] = $k;
440 | }
441 | }
442 | } elseif ($type == 2) {
443 | //两对
444 | $kw = $card_key = [];
445 | foreach ($val as $k => $v) {
446 | if ($v == 2) {
447 | $kw[] = $k;
448 | }
449 | }
450 | foreach ($card as $k => $v) {
451 | if (in_array($v, $kw)) {
452 | $card_key[] = $k;
453 | }
454 | }
455 | } elseif ($type == 1) {
456 | //对A后者对K
457 | foreach ($card as $k => $v) {
458 | if (in_array($v, ['A', 'K']) || $k == self::$laizi_value) {
459 | $card_val[$k] = $v;
460 | }
461 | }
462 | $t_val = array_count_values($card_val);
463 | arsort($t_val);
464 | $kw = key($t_val);
465 | if (! self::$is_laizi) {
466 | if (count($t_val) > 1) {
467 | foreach ($card_val as $k => $v) {
468 | if ($kw != $v) {
469 | unset($card_val[$k]);
470 | }
471 | }
472 | }
473 | } else {
474 | //去除k
475 | if (count($t_val) > 2) {
476 | foreach ($card_val as $k => $v) {
477 | if ($v == 'K') {
478 | unset($card_val[$k]);
479 | }
480 | }
481 | }
482 | }
483 | $card_key = array_keys($card_val);
484 | }
485 | return $card_key;
486 | }
487 |
488 | /**
489 | * 是否翻倍, 玩家翻倍处理.
490 | * @param mixed $m_card
491 | * @param mixed $pos
492 | * @param mixed $arr
493 | */
494 | public static function getIsDoubleCard($m_card = 2, $pos = 2, $arr = [])
495 | {
496 | $list = self::$card_value_list;
497 | unset($list[self::$laizi_value]); //去除赖子大王
498 | $card_list = array_rand($list, 4);
499 | //概率运算
500 | if (! empty($arr)) {
501 | $rate = self::_getRate($arr);
502 | } else {
503 | $rate = self::_getRate(self::$is_double_rate);
504 | }
505 |
506 | $min = $m_card % 16;
507 | //拿到最大牌A和最小牌2的概率需要特殊处理一下
508 | if ($min == 1 && $rate == 3) {
509 | //最大牌A出现, 对方肯定是平手或者输
510 | $rate = rand(1, 2);
511 | } elseif ($min == 2 && $rate == 1) {
512 | //最小牌2,出现对方肯定是平手或者赢 // $rate = rand(2,3);
513 | $rate = rand(2, 3);
514 | }
515 | //最小牌
516 | if ($rate == 2) {
517 | //不翻倍,奖励不扣除
518 | $key = $min;
519 | } elseif ($rate == 3) {
520 | //翻倍,奖励累加, 系统数, 发大牌
521 | if ($min == 13) {
522 | $key = 1;
523 | } else {
524 | $key = rand($min + 1, 13);
525 | }
526 | } else {
527 | //不翻倍,丢失全部奖励,系统赢发小牌
528 | if ($min == 1) {
529 | $key = rand(2, 13);
530 | } else {
531 | $key = rand(2, $min - 1);
532 | }
533 | }
534 | //根据key组牌
535 | $card_val = [$key, $key + 16, $key + 32, $key + 48];
536 | //去除相同的值
537 | $card_val = array_diff($card_val, $card_list);
538 | $card_key = array_rand($card_val, 1);
539 | $card_list[$pos] = $card_val[$card_key];
540 | return ['result' => $rate, 'msg' => self::$is_double_msg[$rate], 'm_card' => self::$card_value_list[$m_card], 'pos_card' => self::$card_value_list[$card_list[$pos]], 'pos' => $pos, 'card' => $card_list, 'show' => self::showCard($card_list)];
541 | }
542 |
543 | /**
544 | * 获取牌型结果.
545 | * @param mixed $arr
546 | */
547 | public static function getCardType($arr)
548 | {
549 | $type = self::checkCardType($arr);
550 | $highlight = self::highLight($arr, $type);
551 | return ['card' => $arr, 'type' => $type, 'typenote' => self::$card_type[$type], 'rate' => self::$card_rate[$type], 'highlight' => $highlight];
552 | }
553 |
554 | /**
555 | * 设置翻倍的概率.
556 | * @param mixed $rate
557 | */
558 | public static function setRate($rate = [])
559 | {
560 | if (empty($rate)) {
561 | self::$is_double_rate = $rate;
562 | }
563 | }
564 |
565 | /**
566 | * 去除赖子,并且排序.
567 | * @param mixed $arr
568 | */
569 | private static function exceptLaizi($arr)
570 | {
571 | $key = array_search(self::$laizi_value, $arr); //键值有可能0
572 | if ($key !== false) {
573 | unset($arr[$key]);
574 | self::$is_laizi = true;
575 | } else {
576 | self::$is_laizi = false;
577 | }
578 | sort($arr);
579 | return $arr;
580 | }
581 |
582 | /**
583 | * 获取牌内容,根据牌的key,获取牌的内容.
584 | * @param mixed $arr
585 | */
586 | private static function getCard($arr)
587 | {
588 | $card = [];
589 | foreach ($arr as $v) {
590 | $card[$v] = self::$card_value_list[$v];
591 | }
592 | return $card;
593 | }
594 |
595 | /**
596 | * 计算概率算法.
597 | * @param array $prizes 奖品概率数组
598 | * 格式:array(奖品id => array( 'rate'=>概率),奖品id => array('rate'=>概率))
599 | * @param mixed $arr
600 | * @return int
601 | */
602 | private static function _getRate($arr = [])
603 | {
604 | $key = 0;
605 | //首先生成一个1W内的数
606 | $rid = rand(1, 10000);
607 | //概率值(按设置累加)
608 | $rate = 0;
609 | foreach ($arr as $k => $v) {
610 | //根据设置的概率向上累加
611 | $rate += $v;
612 | //如果生成的概率数小于或等于此数据,表示当前道具ID即是,退出查找
613 | if ($rid <= $rate) {
614 | $key = $k;
615 | break;
616 | }
617 | }
618 | return $key;
619 | }
620 | }
621 |
622 | /*
623 |
624 | header("Content-type: text/html; charset=utf-8");
625 |
626 | $act = isset($_REQUEST['act']) ? trim($_REQUEST['act']) : '';
627 | //类调用
628 | $obj = new JokerPoker();
629 |
630 | if($act == 'getcard') {
631 | //获取5张牌
632 | $key = $obj->getFiveCard();
633 | //$key = array(17,37,39,40,42);
634 | exit(json_encode($key));
635 | } elseif($act == 'turncard') {
636 | //翻牌
637 | $tmp = isset($_REQUEST['card']) ? trim($_REQUEST['card']) : '';
638 | if(!empty($tmp)) {
639 | $key = explode('|',$tmp);
640 | } else {
641 | $key = array();
642 | }
643 | $key = array_map('intval', $key);
644 | $card = $obj->getFiveCard($key);
645 | $res = $obj->getCardType($card);
646 | exit(json_encode($res));
647 | } elseif($act == 'isdouble') {
648 | //翻倍处理
649 | $card = isset($_REQUEST['card']) && !empty($_REQUEST['card']) ? intval($_REQUEST['card']) : 2;
650 | $pos = (isset($_REQUEST['pos']) && $_REQUEST['pos'] < 4) ? intval($_REQUEST['pos']) : 2;
651 | $res = $obj->getIsDoubleCard($card, $pos);
652 | exit(json_encode($res));
653 | }
654 |
655 | //测试牌型结果
656 | $tmp = isset($_REQUEST['test']) ? trim($_REQUEST['test']) : '';
657 | if(!empty($tmp)) {
658 | $key = explode('|',$tmp);
659 | } else {
660 | $key = array();
661 | }
662 |
663 | //类调用
664 | $obj = new JokerPoker();
665 | $key = $obj->getFiveCard();
666 | $key = array(13,18,24,27,43);
667 | $card = $obj->showCard($key);
668 |
669 | var_dump($key, $card, $obj->getCardType($key),$obj->getIsDoubleCard());
670 |
671 | */
672 |
--------------------------------------------------------------------------------
/app/Game/Core/Log.php:
--------------------------------------------------------------------------------
1 | 'INFO',
22 | 2 => 'DEBUG',
23 | 3 => 'ERROR',
24 | ];
25 |
26 | /**
27 | * 日志等级,1表示大于等于1的等级的日志,都会显示,依次类推.
28 | * @var int
29 | */
30 | protected static $level = 1;
31 |
32 | /**
33 | * 显示日志.
34 | * @param string $centent
35 | * @param int $level
36 | * @param mixed $str
37 | */
38 | public static function show($centent = '', $level = 1, $str = '')
39 | {
40 | if ($level >= self::$level) {
41 | echo $str . date('Y/m/d H:i:s') . " [\033[0;36m" . self::$level_info[$level] . "\033[0m] " . $centent . "\n";
42 | }
43 | }
44 |
45 | /**
46 | * 显示日志.
47 | * @param string $centent
48 | * @param int $level
49 | * @param mixed $split
50 | */
51 | public static function split($split = '', $level = 1)
52 | {
53 | if ($level >= self::$level) {
54 | echo $split . "\n";
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/app/Game/Core/Packet.php:
--------------------------------------------------------------------------------
1 | $code,
29 | 'msg' => $msg,
30 | 'data' => $data,
31 | ];
32 | }
33 |
34 | /**
35 | * 打包数据,固定包头,4个字节为包头(里面存了包体长度),包体前2个字节为.
36 | * @param mixed $data
37 | * @param mixed $cmd
38 | * @param mixed $scmd
39 | * @param mixed $format
40 | * @param mixed $type
41 | */
42 | public static function packEncode($data, $cmd = 1, $scmd = 1, $format = 'msgpack', $type = 'tcp')
43 | {
44 | if ($type == 'tcp') {
45 | if ($format == 'msgpack') {
46 | $sendStr = msgpack_pack($data);
47 | } else {
48 | $sendStr = $data;
49 | }
50 | return pack('N', strlen($sendStr) + 2) . pack('C2', $cmd, $scmd) . $sendStr;
51 | }
52 | return self::packFormat('packet type wrong', 100006);
53 | }
54 |
55 | /**
56 | * 解包数据.
57 | * @param mixed $str
58 | * @param mixed $format
59 | */
60 | public static function packDecode($str, $format = 'msgpack')
61 | {
62 | $header = substr($str, 0, 4);
63 | if (strlen($header) != 4) {
64 | return self::packFormat('packet length invalid', 100007);
65 | }
66 | $len = unpack('Nlen', $header);
67 | $len = $len['len'];
68 | $result = substr($str, 6);
69 | if ($len != strlen($result) + 2) {
70 | //结果长度不对
71 | return self::packFormat('packet length invalid', 100007);
72 | }
73 |
74 | if ($format == 'msgpack') {
75 | $result = msgpack_unpack($result);
76 | }
77 | if (empty($result)) {
78 | //结果长度不对
79 | return self::packFormat('packet data is empty', 100008);
80 | }
81 | $cmd = unpack('Ccmd/Cscmd', substr($str, 4, 6));
82 | $result = self::packFormat('OK', 0, $result);
83 | $result['cmd'] = $cmd['cmd'];
84 | $result['scmd'] = $cmd['scmd'];
85 | $result['len'] = $len + 4;
86 | return $result;
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/app/Game/Logic/ChatMsg.php:
--------------------------------------------------------------------------------
1 | _params['data']);
28 | // $data = Packet::packEncode($data, MainCmd::CMD_GAME, SubCmd::CHAT_MSG_RESP);
29 | // return $data;
30 |
31 | $game_conf = config('game');
32 | /** @var \Redis $redis */
33 | $redis = redis();
34 | $user_room_key = sprintf($game_conf['user_room'], $this->_params['userinfo']['account']);
35 | $room_no = $redis->get($user_room_key);
36 | $user_room_data_key = sprintf($game_conf['user_room_data'], $room_no);
37 | $uinfo = $redis->hGet($user_room_data_key, 'uinfo');
38 | $uinfo = json_decode($uinfo, true);
39 | $binds = $fds = [];
40 | if (! empty($uinfo)) {
41 | foreach ($uinfo as $u) {
42 | $binds[] = sprintf($game_conf['user_bind_key'], $u);
43 | }
44 | $fds = $redis->mget($binds);
45 | }
46 | $user = ['user' => $this->_params['userinfo']['account']];
47 | $msg_data = array_merge($user, $this->_params['data']);
48 |
49 | $fds[] = $fd = $this->_params['userinfo']['fd'];
50 | //分别发消息给三个人
51 | foreach ($fds as $fd) {
52 | $data = Packet::packFormat('OK', 0, $msg_data);
53 | $data = Packet::packEncode($data, MainCmd::CMD_GAME, SubCmd::CHAT_MSG_RESP);
54 | server()->push((int) $fd, $data, WEBSOCKET_OPCODE_BINARY);
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/app/Game/Logic/GameCall.php:
--------------------------------------------------------------------------------
1 | _params['userinfo']['account'];
30 | $calltype = $this->_params['data']['type'];
31 | $user_room_data = $this->getRoomData($account);
32 | $room_user_data = json_decode($user_room_data[$account], true);
33 | //如果已经地主产生了, 直接下发叫地主信息
34 | if (isset($user_room_data['master']) && $user_room_data['last_chair_id']) {
35 | $this->callGameResp($room_user_data['chair_id'], $room_user_data['calltype'], $user_room_data['master'], $user_room_data['last_chair_id']);
36 | } else {
37 | if (! empty($room_user_data)) {
38 | if (! isset($room_user_data['calltype'])) {
39 | $this->setRoomUserInfoDataByKey($room_user_data, $account, 'calltype', $calltype);
40 | } else {
41 | $calltype = $room_user_data['calltype'];
42 | }
43 | $chair_id = $room_user_data['chair_id'];
44 | //广播叫地主消息
45 | $this->gameCallBroadcastResp($account, $calltype, $chair_id);
46 | //返回
47 | $this->callGameResp($chair_id, $calltype);
48 | //摸底牌操作
49 | $this->catchGameCardResp($account);
50 | }
51 | }
52 | return 0;
53 | }
54 |
55 | /**
56 | * 广播叫地主.
57 | * @param $account
58 | * @param $calltype
59 | * @param $chair_id
60 | */
61 | public function gameCallBroadcastResp($account, $calltype, $chair_id)
62 | {
63 | $fds = $this->getRoomFds($account);
64 | //匹配失败, 请继续等待
65 | $msg = ['account' => $account, 'calltype' => $calltype, 'chair_id' => $chair_id, 'calltime' => time()];
66 | $data = Packet::packFormat('OK', 0, $msg);
67 | $data = Packet::packEncode($data, MainCmd::CMD_GAME, SubCmd::SUB_GAME_CALL_TIPS_RESP);
68 | $this->pushToUsers($this->_params['serv'], $fds, $data);
69 | }
70 |
71 | /**
72 | * 组装抢地主返回数据.
73 | * @param $chair_id
74 | * @param $calltype
75 | * @param $master
76 | * @param $last_chair_id
77 | * @return array|string
78 | */
79 | protected function callGameResp($chair_id, $calltype, $master = '', $last_chair_id = 0)
80 | {
81 | $msg = ['chair_id' => $chair_id, 'calltype' => $calltype];
82 | if ($master != '' && $last_chair_id > 0) {
83 | $msg['master'] = $master;
84 | $msg['last_chair_id'] = $last_chair_id;
85 | }
86 | $data = Packet::packFormat('OK', 0, $msg);
87 | $data = Packet::packEncode($data, MainCmd::CMD_GAME, SubCmd::SUB_GAME_CALL_RESP);
88 | $this->_params['serv']->push((int) $this->_params['userinfo']['fd'], $data, WEBSOCKET_OPCODE_BINARY);
89 | }
90 |
91 | /**
92 | * 摸手牌操作.
93 | * @param $account
94 | */
95 | protected function catchGameCardResp($account)
96 | {
97 | $room_data = $this->getRoomData($account);
98 | $infos = json_decode($room_data['uinfo'], true);
99 | if (! isset($room_data['master'])) {
100 | //加入游戏房间队列里面
101 | $calls = $accouts = [];
102 | $flag = 0;
103 | foreach ($infos as $v) {
104 | $u = json_decode($room_data[$v], true);
105 | if (isset($u['calltype'])) {
106 | ++$flag;
107 | if ($u['calltype'] == 1) {
108 | $calls[] = $v;
109 | }
110 | }
111 | }
112 | if ($flag == 3) {
113 | //抢地主里随机一个人出来
114 | if (empty($calls)) {
115 | $calls = $infos;
116 | }
117 | $key = array_rand($calls, 1);
118 | $user = $calls[$key];
119 | //抓牌,合并手牌数据
120 | $user_data = json_decode($room_data[$user], true);
121 | $hand = json_decode($room_data['hand'], true);
122 | $card = array_values(array_merge($user_data['card'], $hand));
123 | $card = $this->obj_ddz->_sortCardByGrade($card);
124 | $user_data['card'] = $card;
125 | //设置地主和用户手牌数据
126 | $param = [
127 | 'master' => $user,
128 | $user => json_encode($user_data),
129 | ];
130 | $this->muitSetRoomData($account, $param);
131 | $this->catchGameCard($room_data, $user);
132 | }
133 | }
134 | }
135 |
136 | /**
137 | * 抓牌返回数据.
138 | * @param $room_data
139 | * @param $user
140 | * @param $infos
141 | */
142 | protected function catchGameCard($room_data, $user)
143 | {
144 | $info = json_decode($room_data[$user], true);
145 | $msg = [
146 | 'user' => $user,
147 | 'chair_id' => $info['chair_id'],
148 | 'hand_card' => $room_data['hand'],
149 | ];
150 | $data = Packet::packFormat('OK', 0, $msg);
151 | $data = Packet::packEncode($data, MainCmd::CMD_GAME, SubCmd::SUB_GAME_CATCH_BASECARD_RESP);
152 | $this->pushToUsers($this->_params['serv'], $this->getRoomFds($user), $data);
153 | }
154 | }
155 |
--------------------------------------------------------------------------------
/app/Game/Logic/GameOutCard.php:
--------------------------------------------------------------------------------
1 | _params['userinfo']['account'];
31 | $user_room_data = $this->getRoomData($account);
32 | $out_cards = $this->_params['data'];
33 | return $this->playCard($user_room_data, $out_cards, $account);
34 | }
35 |
36 | /**
37 | * 用户打牌逻辑处理.
38 | * @param $user_room_data
39 | * @param $out_cards
40 | * @param $account
41 | * @return int
42 | */
43 | protected function playCard($user_room_data, $out_cards, $account)
44 | {
45 | //轮次
46 | $round = isset($user_room_data['round']) ? $user_room_data['round'] + 1 : 0;
47 | //手次
48 | $hand = isset($user_room_data['hand_num']) ? $user_room_data['hand_num'] + 1 : 1;
49 | //本轮次上一次牌型
50 | $last_chair_id = isset($user_room_data['last_chair_id']) ? $user_room_data['last_chair_id'] : 0;
51 | //本轮次上一次牌型
52 | $last_card_type = isset($user_room_data['last_card_type']) ? $user_room_data['last_card_type'] : 0;
53 | //本轮次上一次牌值
54 | $last_card = isset($user_room_data['last_card']) ? $user_room_data['last_card'] : '';
55 | //下一个出牌人椅子id
56 | $next_chair_id = $out_cards['chair_id'] + 1;
57 | $next_chair_id = ($next_chair_id > 3) ? $next_chair_id - 3 : $next_chair_id;
58 |
59 | //根据椅子查询手牌信息
60 | $my_card = json_decode($user_room_data[$account], true);
61 |
62 | //出牌牌型
63 | $card_type = '无';
64 |
65 | //验证出牌数据
66 | if ($out_cards['status'] == 1) {
67 | if (count($out_cards['card']) == 0) {
68 | return $this->gameOutCard(['status' => 1, 'msg' => '出牌非法, 请出手牌']);
69 | }
70 | //判断手牌是否存在, 手牌存在继续往下执行
71 | if (! $out_cards['card'] == array_intersect($out_cards['card'], $my_card['card'])) {
72 | return $this->gameOutCard(['status' => 2, 'msg' => '出牌非法, 出牌数据有问题']);
73 | }
74 | //检查牌型
75 | $arr = $this->obj_ddz->checkCardType($out_cards['card']);
76 | if ($arr['type'] == 0) {
77 | return $this->gameOutCard(['status' => 3, 'msg' => '出牌非法, 牌型有误']);
78 | }
79 | $card_type = $arr['type_msg'];
80 |
81 | //如果非首轮牌, 请验证牌型, 并判断牌型是否一直, 如果打出的牌型是, 炸弹和飞机, 跳过验证, 13表示炸弹,14表示飞机
82 | if ($last_card_type > 0 && ! in_array($arr['type'], [13, 14]) && $last_card_type != $arr['type']) {
83 | return $this->gameOutCard(['status' => 3, 'msg' => '出牌非法, 和上一把牌型不符合']);
84 | }
85 | $out_cards['card_type'] = $arr['type'];
86 | //比牌大小
87 | if (! $this->obj_ddz->checkCardSize($out_cards['card'], json_decode($last_card, true))) {
88 | return $this->gameOutCard(['status' => 4, 'msg' => '出牌非法, 牌没有大过上家牌']);
89 | }
90 | } else {
91 | //过牌要验证是否为首次出牌, 如果是首次出牌是不能过牌的
92 | if ($hand == 1 || $last_chair_id == $out_cards['chair_id']) {
93 | return $this->gameOutCard(['status' => 4, 'msg' => '出牌非法, 首次出牌不能过牌操作']);
94 | }
95 | }
96 | if ($out_cards['chair_id'] < 1) {
97 | return $this->gameOutCard(['status' => 5, 'msg' => '出牌非法, 椅子ID非法']);
98 | }
99 | //判断游戏是否结束
100 | if (count($my_card['card']) < 1) {
101 | return $this->gameOutCard(['status' => 6, 'msg' => '游戏结束, 所有手牌已经出完']);
102 | }
103 |
104 | //出牌逻辑
105 | if ($last_card_type == 0) {
106 | //如果上一次牌型为0, 证明没有牌型, 这次手牌为开始手牌
107 | $ret = $this->roundStart($user_room_data, $out_cards, $account, $hand, $next_chair_id);
108 | \App\Game\Core\Log::show($account . ':第' . $ret['round'] . '回合-开始');
109 | } elseif ($out_cards['status'] == 0 && $last_chair_id == $next_chair_id) {
110 | //上一轮过牌, 并上一次椅子id和这一次相等, 轮次结束
111 | $this->roundEnd($account, $last_chair_id, $hand, $next_chair_id);
112 | \App\Game\Core\Log::show($account . ':第' . $round . '回合-结束');
113 | } else {
114 | //跟牌逻辑
115 | $this->roundFollow($out_cards, $account, $hand, $next_chair_id);
116 | $last_chair_id = $out_cards['chair_id'];
117 | \App\Game\Core\Log::show($account . ':第' . $round . '回合-跟牌');
118 | }
119 |
120 | //判断下个用户, 是首次出牌还是跟牌操作
121 | $is_first_round = $last_chair_id == $next_chair_id ? true : false;
122 | //设置减少手牌数据
123 | $my_card = $this->setMyCard($user_room_data, $out_cards, $account);
124 | //判断游戏是否结束
125 | $is_game_over = (count($my_card['card']) < 1) ? true : false;
126 | //计算下家牌是否能大过上一手牌
127 | // $next_card = $this->findCardsByChairId($user_room_data, $next_chair_id);
128 | // $prv_card = (isset($out_cards['card']) && count($out_cards['card']) > 0) ? $out_cards['card'] : json_decode($last_card, true);
129 | // $is_out_card = $this->obj_ddz->isPlayCard($next_card, $prv_card);
130 | // var_dump($next_card, $prv_card, $is_out_card);
131 |
132 | //并下发出牌提示
133 | $step = [
134 | 'round' => $round, //轮次
135 | 'hand_num' => $hand, //首次
136 | 'chair_id' => $out_cards['chair_id'], //出牌椅子
137 | 'account' => $account, //出牌账号
138 | 'show_type' => $out_cards['status'], //1,跟牌, 2, 过牌
139 | 'next_chair_id' => $next_chair_id, //下一个出牌的椅子id
140 | 'is_first_round' => $is_first_round, //是否为首轮, 下一个出牌人的情况
141 | 'card' => $out_cards['card'], //本次出牌
142 | 'card_type' => $card_type, //显示牌型
143 | 'last_card' => json_decode($last_card, true), //上次最大牌
144 | 'is_game_over' => $is_game_over, //游戏是否结束
145 | ];
146 |
147 | // 如果游戏结束,构造游戏结果
148 | if ($is_game_over) {
149 | // 获取当前玩家身份
150 | $master = $user_room_data['master'] ?? '';
151 | $step['result'] = $account . ':1';
152 | $is_master = ($master == $account) ? 1 : 0;
153 | $user_info = json_decode($user_room_data['uinfo'], true);
154 |
155 | // 获取所有玩家
156 | foreach ($user_info as $user_account) {
157 | if ($account != $user_account) {
158 | if ($master == $user_account) {
159 | $step['result'] .= " {$user_account}:0";
160 | } else {
161 | $step['result'] .= " {$user_account}:" . ($is_master ? 0 : 1);
162 | }
163 | }
164 | }
165 | }
166 |
167 | //记录一下出牌数据, 记录没步骤录像数据
168 | $this->setRoomPlayCardStep($account, 'step_' . $hand, json_encode($step));
169 | //广播打牌结果
170 | $ret = $this->gameOutCardResp($this->_params['serv'], $account, $step);
171 | //游戏结束, 重置游戏数据
172 | $this->gameOver($account, json_decode($user_room_data['uinfo'], true), $is_game_over);
173 | //记录步骤信息
174 | Log::get()->info(json_encode($step));
175 | return $ret;
176 | }
177 |
178 | /**
179 | * 轮次开始.
180 | * @param $user_room_data
181 | * @param $out_cards
182 | * @param $account
183 | * @param $hand
184 | * @param $next_chair_id
185 | * @return array
186 | */
187 | protected function roundStart($user_room_data, $out_cards, $account, $hand, $next_chair_id)
188 | {
189 | //当前轮次
190 | $round = isset($user_room_data['round']) ? $user_room_data['round'] + 1 : 1;
191 | //本轮次开始时椅子id
192 | $start_chair_id = $out_cards['chair_id'];
193 | //本轮次最大牌椅子id
194 | $last_chair_id = $out_cards['chair_id'];
195 | //本轮次最大牌椅子i牌型
196 | $last_card_type = $out_cards['card_type'];
197 | //本轮次最大牌椅子牌值
198 | $last_card = $out_cards['card'];
199 |
200 | //结果存入redis
201 | $param = [
202 | 'round' => $round,
203 | 'hand_num' => $hand,
204 | 'start_chair_id' => $start_chair_id,
205 | 'last_chair_id' => $last_chair_id,
206 | 'last_card_type' => $last_card_type,
207 | 'last_card' => json_encode($last_card),
208 | 'next_chair_id' => $next_chair_id,
209 | ];
210 | $this->muitSetRoomData($account, $param);
211 | return $param;
212 | }
213 |
214 | /**
215 | * 轮次结束
216 | * @param $account
217 | * @param $last_chair_id
218 | * @param $next_chair_id
219 | * @param $hand
220 | */
221 | protected function roundEnd($account, $last_chair_id, $hand, $next_chair_id)
222 | {
223 | //结果存入redis
224 | $param = [
225 | 'start_chair_id' => $last_chair_id,
226 | 'last_card_type' => 0,
227 | 'last_card' => json_encode([]),
228 | 'hand_num' => $hand,
229 | 'next_chair_id' => $next_chair_id,
230 | ];
231 | $this->muitSetRoomData($account, $param);
232 | }
233 |
234 | /**
235 | * 跟牌.
236 | * @param $out_cards
237 | * @param $account
238 | * @param $next_chair_id
239 | * @param $hand
240 | */
241 | protected function roundFollow($out_cards, $account, $hand, $next_chair_id)
242 | {
243 | //跟牌
244 | $param = [];
245 | if ($out_cards['status'] == 1) {
246 | //本轮次上一次最大牌椅子id
247 | $param = [
248 | 'last_chair_id' => $out_cards['chair_id'],
249 | 'last_card' => json_encode($out_cards['card']),
250 | ];
251 | }
252 | $param['next_chair_id'] = $next_chair_id;
253 | $param['hand_num'] = $hand;
254 | //结果存入redis
255 | $this->muitSetRoomData($account, $param);
256 | }
257 |
258 | /**
259 | * 游戏结束
260 | * @param $account
261 | * @param $uinfo
262 | * @param $is_game_over
263 | */
264 | protected function gameOver($account, $uinfo, $is_game_over): void
265 | {
266 | if ($is_game_over) {
267 | //设置游戏结束标识
268 | $this->setRoomData($account, 'is_game_over', $is_game_over);
269 |
270 | // 清除房间队列
271 | $room_no = $this->getRoomNo($account);
272 | $key = sprintf($this->getGameConf('room_user_list'), $room_no);
273 | redis()->del($key);
274 |
275 | //清除数据, 进行下一轮玩牌, 随机分配
276 | $this->clearRoomNo($uinfo);
277 | }
278 | }
279 |
280 | /**
281 | * 设置我的手牌.
282 | * @param $user_room_data
283 | * @param $cards
284 | * @param $account
285 | * @return mixed
286 | */
287 | protected function setMyCard($user_room_data, $cards, $account)
288 | {
289 | //根据椅子查询手牌信息
290 | $my_card = json_decode($user_room_data[$account], true);
291 | $hand_card = array_unique(array_values(array_diff($my_card['card'], $cards['card'])));
292 | if (isset($my_card['out_card'])) {
293 | $out_card = array_unique(array_values(array_merge($my_card['out_card'], $cards['card'])));
294 | } else {
295 | $out_card = $cards['card'];
296 | }
297 | $my_card['card'] = $hand_card;
298 | $my_card['out_card'] = $out_card;
299 | //写会redis
300 | $this->setRoomData($account, $account, json_encode($my_card));
301 | return $my_card;
302 | }
303 |
304 | /**
305 | * 根据椅子id找出这个一直用户的手牌.
306 | * @param $user_room_data
307 | * @param $chair_id
308 | * @return array
309 | */
310 | protected function findCardsByChairId($user_room_data, $chair_id)
311 | {
312 | $uinfo = json_decode($user_room_data['uinfo'], true);
313 | $cards = [];
314 | foreach ($uinfo as $v) {
315 | $d = json_decode($user_room_data[$v], true);
316 | if (isset($d['chair_id']) && $d['chair_id'] == $chair_id) {
317 | $cards = $d['card'];
318 | break;
319 | }
320 | }
321 | return $cards;
322 | }
323 |
324 | /**
325 | * 向客户端发送出牌提示响应, 单发.
326 | * @param $param
327 | * @return array|string
328 | */
329 | protected function gameOutCard($param)
330 | {
331 | $data = Packet::packFormat('OK', 0, $param);
332 | return Packet::packEncode($data, MainCmd::CMD_GAME, SubCmd::SUB_GAME_OUT_CARD);
333 | }
334 |
335 | /**
336 | * 向客户端广播出牌响应, 群发.
337 | * @param $serv
338 | * @param $account
339 | * @param $param
340 | * @return int
341 | */
342 | protected function gameOutCardResp($serv, $account, $param)
343 | {
344 | $data = Packet::packFormat('OK', 0, $param);
345 | $data = Packet::packEncode($data, MainCmd::CMD_GAME, SubCmd::SUB_GAME_OUT_CARD_RESP);
346 | $this->pushToUsers($serv, $this->getRoomFds($account), $data);
347 | //并提示成功
348 | return $this->gameOutCard(['status' => 0, 'msg' => '出牌成功', 'data' => $param]);
349 | }
350 | }
351 |
--------------------------------------------------------------------------------
/app/Game/Logic/GameRoomCreate.php:
--------------------------------------------------------------------------------
1 | _params['userinfo']['account'];
31 | $room_no = $this->getRoomNo($account);
32 |
33 | $serv = server();
34 | $redis = redis();
35 | $fd = $this->_params['userinfo']['fd'];
36 | if ($room_no) {
37 | // 有房间了
38 | $msg = [
39 | 'status' => 'fail',
40 | 'msg' => '已经有房间了,请耐心等待!',
41 | ];
42 |
43 | $data = Packet::packFormat('OK', 0, $msg);
44 | return Packet::packEncode($data, MainCmd::CMD_SYS, SubCmd::CREATE_ROOM_FAIL_RESP);
45 | }
46 | // 没有房间,创建房间
47 | $room_no_key = $this->getGameConf('user_room_no');
48 | if ($redis->exists($room_no_key)) {
49 | $room_no = $redis->get($room_no_key);
50 | ++$room_no;
51 | $redis->set($room_no_key, $room_no);
52 | } else {
53 | $room_no = intval(1000001);
54 | $redis->set($room_no_key, $room_no);
55 | }
56 |
57 | // 保存用户和房间的关系
58 | $redis->set(sprintf($this->getGameConf('user_room'), $account), $room_no);
59 |
60 | // 保存房间队列
61 | $redis->sadd(sprintf($this->getGameConf('room_user_list'), $room_no), $account);
62 |
63 | //发消息给随机10个用户建立了新房间
64 | $msg_data = [
65 | 'user' => $account,
66 | 'data' => "我创建了新的房间[{$room_no}]",
67 | ];
68 | $users = $redis->sRandMember($this->getGameConf('room_list'), 10);
69 | foreach ($users as $account) {
70 | $key = sprintf($this->getGameConf('user_bind_key'), $account);
71 |
72 | //根据账号获取fd
73 | $tmpFd = $redis->get($key);
74 | if ($tmpFd) {
75 | $data = Packet::packFormat('OK', 0, $msg_data);
76 | $data = Packet::packEncode($data, MainCmd::CMD_GAME, SubCmd::CHAT_MSG_RESP);
77 | server()->push((int) $tmpFd, $data, WEBSOCKET_OPCODE_BINARY);
78 | }
79 | }
80 |
81 | // 返回创建成功信息
82 | $msg = [
83 | 'status' => 'succ',
84 | 'msg' => "创建成功,房间号[{$room_no}]",
85 | ];
86 | $retData = Packet::packFormat('OK', 0, $msg);
87 | return Packet::packEncode($retData, MainCmd::CMD_SYS, SubCmd::CREATE_ROOM_SUCC_RESP);
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/app/Game/Logic/GameRoomJoin.php:
--------------------------------------------------------------------------------
1 | _params['userinfo']['account'];
31 | $room_no = $this->getRoomNo($account);
32 |
33 | $serv = server();
34 | $redis = redis();
35 | $fd = $this->_params['userinfo']['fd'];
36 | if ($room_no) {
37 | // 有房间了
38 | $msg = [
39 | 'status' => 'fail',
40 | 'msg' => '已经有房间了,请耐心等待!',
41 | ];
42 |
43 | $data = Packet::packFormat('OK', 0, $msg);
44 | return Packet::packEncode($data, MainCmd::CMD_SYS, SubCmd::ENTER_ROOM_FAIL_RESP);
45 | }
46 | // 绑定房间对应关系
47 | $result = true;
48 | $room_no = $this->_params['data'];
49 |
50 | // 检查房间人数
51 | $room_user_list_key = sprintf($this->getGameConf('room_user_list'), $room_no);
52 | $room_length = $redis->scard($room_user_list_key);
53 | if ($room_length == 0 || $room_length >= 3) {
54 | // 人数超3人,不允许加入
55 | $result = false;
56 | } else {
57 | $res = $redis->sadd($room_user_list_key, $account);
58 |
59 | // 保存用户和房间的关系
60 | $user_key = sprintf($this->getGameConf('user_room'), $account);
61 | $redis->set($user_key, $room_no);
62 | }
63 |
64 | if (! $result) {
65 | // 房间人数已满3人
66 | $msg = [
67 | 'status' => 'fail',
68 | 'msg' => '房间不存在或者人数已满3人,请进入其他房间!',
69 | ];
70 |
71 | $data = Packet::packFormat('OK', 0, $msg);
72 | return Packet::packEncode($data, MainCmd::CMD_SYS, SubCmd::ENTER_ROOM_FAIL_RESP);
73 | }
74 | // 发消息给房间用户
75 | $game_conf = config('game');
76 | $user = ['user' => $account];
77 | $msg_data = [
78 | 'user' => $account,
79 | 'data' => '我进入了房间',
80 | ];
81 | $room_users = $redis->sRandMember($room_user_list_key, 3);
82 | $serv = server();
83 | foreach ($room_users as $roomUser) {
84 | $key = sprintf($game_conf['user_bind_key'], $roomUser);
85 | $tmpFd = $redis->get($key);
86 | if ($tmpFd) {
87 | $data = Packet::packFormat('OK', 0, $msg_data);
88 | $data = Packet::packEncode($data, MainCmd::CMD_GAME, SubCmd::CHAT_MSG_RESP);
89 | server()->push((int) $tmpFd, $data, WEBSOCKET_OPCODE_BINARY);
90 | }
91 | }
92 | return 0;
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/app/Game/Logic/GameStart.php:
--------------------------------------------------------------------------------
1 | _params['userinfo']['account'];
32 | $room_data = $this->getRoomData($account);
33 | $user_room_data = isset($room_data[$account]) ? json_decode($room_data[$account], true) : [];
34 | if ($user_room_data) {
35 | //是否产生地主
36 | $master = isset($room_data['master']) ? $room_data['master'] : '';
37 | if ($master) {
38 | $user_room_data['is_master'] = 1;
39 | if ($master == $account) {
40 | //此人是地主
41 | $user_room_data['master'] = 1;
42 | }
43 | } else {
44 | $user_room_data['is_master'] = 0;
45 | }
46 |
47 | //轮到谁出牌了
48 | $last_chair_id = isset($room_data['last_chair_id']) ? $room_data['last_chair_id'] : 0;
49 | $next_chair_id = isset($room_data['next_chair_id']) ? $room_data['next_chair_id'] : 0;
50 | $user_room_data['is_first_round'] = false;
51 | if ($next_chair_id > 0) {
52 | $user_room_data['index_chair_id'] = $next_chair_id;
53 | if ($next_chair_id == $last_chair_id) {
54 | //首轮出牌
55 | $user_room_data['is_first_round'] = true;
56 | }
57 | } else {
58 | //地主首次出牌
59 | if (isset($room_data[$master])) {
60 | $master_info = json_decode($room_data[$master], true);
61 | $user_room_data['index_chair_id'] = $master_info['chair_id'];
62 | //首轮出牌
63 | $user_room_data['is_first_round'] = true;
64 | }
65 | }
66 |
67 | //判断游戏是否结束
68 | $user_room_data['is_game_over'] = isset($room_data['is_game_over']) ? $room_data['is_game_over'] : false;
69 | //进入房间成功
70 | $msg = $user_room_data;
71 | $room_data = Packet::packFormat('OK', 0, $msg);
72 | return Packet::packEncode($room_data, MainCmd::CMD_SYS, SubCmd::ENTER_ROOM_SUCC_RESP);
73 | }
74 | //$room_list = $this->getGameConf('room_list');
75 | //if($room_list) {
76 | //判断是否在队列里面
77 | //redis()->sAdd($room_list, $this->_params['userinfo']['account']);
78 | //投递异步任务
79 | //$task = container()->get(GameSyncTask::class);
80 | //$task->gameRoomMatch($this->_params['userinfo']['fd']);
81 | //}
82 |
83 | $room_no = $this->getRoomNo($account);
84 | if ($room_no) {
85 | $task = container()->get(GameSyncTask::class);
86 | $task->gameRoomMatch($this->_params['userinfo']['fd'], $room_no);
87 | } else {
88 | // 没有进入房间的放到公共队列里
89 | $room_list = $this->getGameConf('room_list');
90 | if ($room_list) {
91 | redis()->sAdd($room_list, $this->_params['userinfo']['account']);
92 | }
93 |
94 | //未加入房间
95 | $msg = [
96 | 'status' => 'fail',
97 | 'msg' => '您还未加入房间,请创建房间或者输入房间号进入房间!',
98 | ];
99 | $data = Packet::packFormat('OK', 0, $msg);
100 | $data = Packet::packEncode($data, MainCmd::CMD_SYS, SubCmd::ENTER_ROOM_FAIL_RESP);
101 | $serv = server();
102 | $serv->push((int) $this->_params['userinfo']['fd'], $data, WEBSOCKET_OPCODE_BINARY);
103 | }
104 | return 0;
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/app/Game/Logic/HeartAsk.php:
--------------------------------------------------------------------------------
1 | _params['data']['time']) ? $this->_params['data']['time'] : 0;
30 | $end_time = $this->getMillisecond();
31 | $time = $end_time - $begin_time;
32 | $data = Packet::packFormat('OK', 0, ['time' => $time]);
33 | return Packet::packEncode($data, MainCmd::CMD_SYS, SubCmd::HEART_ASK_RESP);
34 | }
35 |
36 | public function getMillisecond()
37 | {
38 | [$t1, $t2] = explode(' ', microtime());
39 | return (float) sprintf('%.0f', (floatval($t1) + floatval($t2)) * 1000);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/app/Helper.php:
--------------------------------------------------------------------------------
1 | get(\Redis::class);
27 | }
28 | }
29 | if (! function_exists('server')) {
30 | function server()
31 | {
32 | return container()->get(ServerFactory::class)->getServer()->getServer();
33 | }
34 | }
35 | if (! function_exists('frame')) {
36 | function frame()
37 | {
38 | return container()->get(Frame::class);
39 | }
40 | }
41 | if (! function_exists('websocket')) {
42 | function websocket()
43 | {
44 | return container()->get(WebSocketServer::class);
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/app/Listener/DbQueryExecutedListener.php:
--------------------------------------------------------------------------------
1 | logger = $container->get(LoggerFactory::class)->get('sql');
36 | }
37 |
38 | public function listen(): array
39 | {
40 | return [
41 | QueryExecuted::class,
42 | ];
43 | }
44 |
45 | /**
46 | * @param QueryExecuted $event
47 | */
48 | public function process(object $event)
49 | {
50 | if ($event instanceof QueryExecuted) {
51 | $sql = $event->sql;
52 | if (! Arr::isAssoc($event->bindings)) {
53 | foreach ($event->bindings as $key => $value) {
54 | $sql = Str::replaceFirst('?', "'{$value}'", $sql);
55 | }
56 | }
57 |
58 | $this->logger->info(sprintf('[%s] %s', $event->time, $sql));
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/app/Log.php:
--------------------------------------------------------------------------------
1 | get(\Hyperf\Logger\LoggerFactory::class)->get($name);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/app/Model/Model.php:
--------------------------------------------------------------------------------
1 | container = $container;
31 | }
32 |
33 | /**
34 | * 游戏房间匹配.
35 | * @Task
36 | * @param mixed $fd
37 | * @param mixed $room_no
38 | */
39 | public function gameRoomMatch($fd, $room_no): void
40 | {
41 | $game_conf = config('game');
42 | $redis = redis();
43 | $room_user_list_key = sprintf($game_conf['room_user_list'], $room_no);
44 | $len = $redis->sCard($room_user_list_key);
45 | $serv = server();
46 | if (! empty($room_no) && $len == 3) {
47 | //匹配成功, 下发手牌数据, 并进入房间数据
48 | $users = $redis->sRandMember($room_user_list_key, 3);
49 | $users_key = $fds = [];
50 | foreach ($users as $account) {
51 | $key = sprintf($game_conf['user_bind_key'], $account);
52 | //根据账号获取fd
53 | $fds[$account] = $redis->get($key);
54 | }
55 |
56 | //随机发牌
57 | $obj = new DdzPoker();
58 | $card = $obj->dealCards($users);
59 |
60 | //存入用户信息
61 | $room_data = [
62 | 'room_no' => $room_no,
63 | 'hand' => $card['card']['hand'],
64 | ];
65 | foreach ($users as $k => $v) {
66 | $room_data['uinfo'][] = $v;
67 | $room_data[$v] = [
68 | 'card' => $card['card'][$v],
69 | 'chair_id' => ($k + 1),
70 | ];
71 | }
72 | $user_room_data_key = sprintf($game_conf['user_room_data'], $room_no);
73 | $this->arrToHashInRedis($room_data, $user_room_data_key);
74 | //分别发消息给三个人
75 | foreach ($users as $k => $v) {
76 | if (isset($fds[$v])) {
77 | $data = Packet::packFormat('OK', 0, $room_data[$v]);
78 | $data = Packet::packEncode($data, MainCmd::CMD_SYS, SubCmd::ENTER_ROOM_SUCC_RESP);
79 | $serv->push((int) $fds[$v], $data, WEBSOCKET_OPCODE_BINARY);
80 | }
81 | }
82 | } else {
83 | //匹配失败, 请继续等待
84 | $msg = [
85 | 'status' => 'fail',
86 | 'msg' => '人数不够3人,请耐心等待!',
87 | ];
88 | $data = Packet::packFormat('OK', 0, $msg);
89 | $data = Packet::packEncode($data, MainCmd::CMD_SYS, SubCmd::ENTER_ROOM_FAIL_RESP);
90 | $serv->push((int) $fd, $data, WEBSOCKET_OPCODE_BINARY);
91 | }
92 | }
93 |
94 | /**
95 | * 广播叫地主.
96 | * @param $account
97 | * @param $calltype
98 | * @param $chair_id
99 | */
100 | public function gameCall($account, $calltype, $chair_id)
101 | {
102 | $fds = $this->_getRoomFds($account);
103 | //匹配失败, 请继续等待
104 | $msg = [
105 | 'account' => $account,
106 | 'calltype' => $calltype,
107 | 'chair_id' => $chair_id,
108 | 'calltime' => time(),
109 | ];
110 | $data = Packet::packFormat('OK', 0, $msg);
111 | $data = Packet::packEncode($data, MainCmd::CMD_GAME, SubCmd::SUB_GAME_CALL_TIPS_RESP);
112 | $serv = server();
113 | $this->pushToUsers($serv, $fds, $data);
114 | }
115 |
116 | /**
117 | * 当connetions属性无效时可以使用此方法,服务器广播消息, 此方法是给所有的连接客户端, 广播消息,通过方法getClientList广播.
118 | * @param $serv
119 | * @param $data
120 | * @return array
121 | */
122 | protected function pushToAll($serv, $data)
123 | {
124 | $client = [];
125 | $start_fd = 0;
126 | while (true) {
127 | $conn_list = $serv->getClientList($start_fd, 10);
128 | if ($conn_list === false or count($conn_list) === 0) {
129 | echo "BroadCast finish\n";
130 | break;
131 | }
132 | $start_fd = end($conn_list);
133 | foreach ($conn_list as $fd) {
134 | //获取客户端信息
135 | $client_info = $serv->getClientInfo((int) $fd);
136 | $client[$fd] = $client_info;
137 | if (isset($client_info['websocket_status']) && $client_info['websocket_status'] == 3) {
138 | $serv->push((int) $fd, $data, WEBSOCKET_OPCODE_BINARY);
139 | }
140 | }
141 | }
142 | return $client;
143 | }
144 |
145 | /**
146 | * 对多用发送信息.
147 | * @param $serv
148 | * @param $users
149 | * @param $data
150 | */
151 | protected function pushToUsers($serv, $users, $data)
152 | {
153 | foreach ($users as $fd) {
154 | //获取客户端信息
155 | $client_info = $serv->getClientInfo((int) $fd);
156 | $client[$fd] = $client_info;
157 | if (isset($client_info['websocket_status']) && $client_info['websocket_status'] == 3) {
158 | $serv->push((int) $fd, $data, WEBSOCKET_OPCODE_BINARY);
159 | }
160 | }
161 | }
162 |
163 | /**
164 | * 把php数组存入redis的hash表中.
165 | * @param $arr
166 | * @param $hash_key
167 | */
168 | protected function arrToHashInRedis($arr, $hash_key)
169 | {
170 | foreach ($arr as $key => $val) {
171 | redis()->hSet((string) $hash_key, (string) $key, json_encode($val));
172 | }
173 | }
174 |
175 | /**
176 | * 通过accounts获取fds.
177 | * @param $account
178 | * @return array
179 | */
180 | private function _getRoomFds($account)
181 | {
182 | $game_conf = config('game');
183 | $user_room_data = $game_conf['user_room_data'];
184 | $redis = redis();
185 | $uinfo = $redis->hGet($user_room_data, $account);
186 | $uinfo = json_decode($uinfo, true);
187 | $accs = isset($uinfo['account']) ? $uinfo['account'] : [];
188 | $binds = $fds = [];
189 | if (! empty($accs)) {
190 | foreach ($accs as $v) {
191 | $binds[] = sprintf($game_conf['user_bind_key'], $v);
192 | }
193 | $fds = $redis->mget($binds);
194 | }
195 | return $fds;
196 | }
197 | }
198 |
--------------------------------------------------------------------------------
/bin/hyperf.php:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | get(\Hyperf\Contract\ApplicationInterface::class);
23 | $application->run();
24 | })();
25 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hyperf/hyperf-skeleton",
3 | "type": "project",
4 | "keywords": [
5 | "php",
6 | "swoole",
7 | "framework",
8 | "hyperf",
9 | "microservice",
10 | "middleware"
11 | ],
12 | "description": "A coroutine framework that focuses on hyperspeed and flexible, specifically use for build microservices and middlewares.",
13 | "license": "Apache-2.0",
14 | "require": {
15 | "php": ">=7.3",
16 | "ext-swoole": ">=4.4",
17 | "ext-msgpack": "*",
18 | "hyperf/cache": "2.1.*",
19 | "hyperf/command": "2.1.*",
20 | "hyperf/config": "2.1.*",
21 | "hyperf/contract": "2.1.*",
22 | "hyperf/database": "2.1.*",
23 | "hyperf/db-connection": "2.1.*",
24 | "hyperf/devtool": "2.1.*",
25 | "hyperf/di": "2.1.*",
26 | "hyperf/dispatcher": "2.1.*",
27 | "hyperf/event": "2.1.*",
28 | "hyperf/exception-handler": "2.1.*",
29 | "hyperf/framework": "2.1.*",
30 | "hyperf/guzzle": "2.1.*",
31 | "hyperf/http-server": "2.1.*",
32 | "hyperf/logger": "2.1.*",
33 | "hyperf/memory": "2.1.*",
34 | "hyperf/paginator": "2.1.*",
35 | "hyperf/pool": "2.1.*",
36 | "hyperf/process": "2.1.*",
37 | "hyperf/redis": "2.1.*",
38 | "hyperf/utils": "2.1.*",
39 | "hyperf/view": "2.1.*",
40 | "hyperf/websocket-server": "2.1.*",
41 | "sy-records/think-template": "^2.0",
42 | "hyperf/task": "2.1.*"
43 | },
44 | "require-dev": {
45 | "swoole/ide-helper": "^4.6",
46 | "phpmd/phpmd": "^2.6",
47 | "friendsofphp/php-cs-fixer": "^2.14",
48 | "mockery/mockery": "^1.0",
49 | "phpstan/phpstan": "^0.12",
50 | "hyperf/testing": "2.1.*",
51 | "doctrine/inflector": "2.0"
52 | },
53 | "suggest": {
54 | "ext-openssl": "Required to use HTTPS.",
55 | "ext-json": "Required to use JSON.",
56 | "ext-pdo": "Required to use MySQL Client.",
57 | "ext-pdo_mysql": "Required to use MySQL Client.",
58 | "ext-redis": "Required to use Redis Client."
59 | },
60 | "autoload": {
61 | "psr-4": {
62 | "App\\": "app/"
63 | },
64 | "files": [
65 | "app/Helper.php"
66 | ]
67 | },
68 | "autoload-dev": {
69 | "psr-4": {
70 | "HyperfTest\\": "./test/"
71 | }
72 | },
73 | "minimum-stability": "dev",
74 | "prefer-stable": true,
75 | "extra": [],
76 | "scripts": {
77 | "post-root-package-install": [
78 | "@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
79 | ],
80 | "post-autoload-dump": [
81 | "rm -rf runtime/container"
82 | ],
83 | "test": "co-phpunit -c phpunit.xml --colors=always",
84 | "cs-fix": "php-cs-fixer fix $1",
85 | "analyse": "phpstan analyse --memory-limit 300M -l 0 -c phpstan.neon ./app ./config",
86 | "start": "php ./bin/hyperf.php start"
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/config/autoload/annotations.php:
--------------------------------------------------------------------------------
1 | [
14 | 'paths' => [
15 | BASE_PATH . '/app',
16 | ],
17 | 'ignore_annotations' => [
18 | 'mixin',
19 | ],
20 | ],
21 | ];
22 |
--------------------------------------------------------------------------------
/config/autoload/aspects.php:
--------------------------------------------------------------------------------
1 | [
14 | 'driver' => Hyperf\Cache\Driver\RedisDriver::class,
15 | 'packer' => Hyperf\Utils\Packer\PhpSerializerPacker::class,
16 | 'prefix' => 'c:',
17 | ],
18 | ];
19 |
--------------------------------------------------------------------------------
/config/autoload/commands.php:
--------------------------------------------------------------------------------
1 | [
14 | 'driver' => env('DB_DRIVER', 'mysql'),
15 | 'host' => env('DB_HOST', 'localhost'),
16 | 'database' => env('DB_DATABASE', 'hyperf'),
17 | 'port' => env('DB_PORT', 3306),
18 | 'username' => env('DB_USERNAME', 'root'),
19 | 'password' => env('DB_PASSWORD', ''),
20 | 'charset' => env('DB_CHARSET', 'utf8'),
21 | 'collation' => env('DB_COLLATION', 'utf8_unicode_ci'),
22 | 'prefix' => env('DB_PREFIX', ''),
23 | 'pool' => [
24 | 'min_connections' => 1,
25 | 'max_connections' => 10,
26 | 'connect_timeout' => 10.0,
27 | 'wait_timeout' => 3.0,
28 | 'heartbeat' => -1,
29 | 'max_idle_time' => (float) env('DB_MAX_IDLE_TIME', 60),
30 | ],
31 | 'commands' => [
32 | 'gen:model' => [
33 | 'path' => 'app/Model',
34 | 'force_casts' => true,
35 | 'inheritance' => 'Model',
36 | ],
37 | ],
38 | ],
39 | ];
40 |
--------------------------------------------------------------------------------
/config/autoload/dependencies.php:
--------------------------------------------------------------------------------
1 | [
14 | 'amqp' => [
15 | 'consumer' => [
16 | 'namespace' => 'App\\Amqp\\Consumer',
17 | ],
18 | 'producer' => [
19 | 'namespace' => 'App\\Amqp\\Producer',
20 | ],
21 | ],
22 | 'aspect' => [
23 | 'namespace' => 'App\\Aspect',
24 | ],
25 | 'command' => [
26 | 'namespace' => 'App\\Command',
27 | ],
28 | 'controller' => [
29 | 'namespace' => 'App\\Controller',
30 | ],
31 | 'job' => [
32 | 'namespace' => 'App\\Job',
33 | ],
34 | 'listener' => [
35 | 'namespace' => 'App\\Listener',
36 | ],
37 | 'middleware' => [
38 | 'namespace' => 'App\\Middleware',
39 | ],
40 | 'Process' => [
41 | 'namespace' => 'App\\Processes',
42 | ],
43 | ],
44 | ];
45 |
--------------------------------------------------------------------------------
/config/autoload/exceptions.php:
--------------------------------------------------------------------------------
1 | [
14 | 'http' => [
15 | Hyperf\HttpServer\Exception\Handler\HttpExceptionHandler::class,
16 | ],
17 | ],
18 | ];
19 |
--------------------------------------------------------------------------------
/config/autoload/game.php:
--------------------------------------------------------------------------------
1 | 'user:info:%s', //用户信息redis的key,fd对应用户信息
14 | 'user_bind_key' => 'user:bind:%s', //用户绑定信息和fd绑定key,里面存是根据fd存入account和fd绑定关系
15 | 'expire' => 1 * 24 * 60 * 60, //设置key过期时间, 设置为1天
16 | 'room_list' => 'user:room:list', //用户进入房间队列
17 | 'room_user_list' => 'room:user:list:%s', //用户进入房间号队列
18 | 'user_room_no' => 'user:room:no', //用户自增房间号
19 | 'user_room' => 'user:room:map:%s', //用户和房间映射关系
20 | 'user_room_data' => 'user:room:data:%s', //用户游戏房间数据
21 | 'user_room_play' => 'user:room:play:%s', //用户游戏房间打牌步骤数据
22 | ];
23 |
--------------------------------------------------------------------------------
/config/autoload/listeners.php:
--------------------------------------------------------------------------------
1 | [
14 | 'handler' => [
15 | 'class' => Monolog\Handler\StreamHandler::class,
16 | 'constructor' => [
17 | 'stream' => BASE_PATH . '/runtime/logs/hyperf.log',
18 | 'level' => Monolog\Logger::DEBUG,
19 | ],
20 | ],
21 | 'formatter' => [
22 | 'class' => Monolog\Formatter\LineFormatter::class,
23 | 'constructor' => [
24 | 'format' => null,
25 | 'dateFormat' => null,
26 | 'allowInlineLineBreaks' => true,
27 | ],
28 | ],
29 | 'processors' => [
30 | ],
31 | ],
32 | ];
33 |
--------------------------------------------------------------------------------
/config/autoload/middlewares.php:
--------------------------------------------------------------------------------
1 | [
14 | ],
15 | ];
16 |
--------------------------------------------------------------------------------
/config/autoload/processes.php:
--------------------------------------------------------------------------------
1 | [
14 | 'host' => env('REDIS_HOST', 'localhost'),
15 | 'auth' => env('REDIS_AUTH', null),
16 | 'port' => (int) env('REDIS_PORT', 6379),
17 | 'db' => (int) env('REDIS_DB', 0),
18 | 'pool' => [
19 | 'min_connections' => 1,
20 | 'max_connections' => 10,
21 | 'connect_timeout' => 10.0,
22 | 'wait_timeout' => 3.0,
23 | 'heartbeat' => -1,
24 | 'max_idle_time' => (float) env('REDIS_MAX_IDLE_TIME', 60),
25 | ],
26 | ],
27 | ];
28 |
--------------------------------------------------------------------------------
/config/autoload/server.php:
--------------------------------------------------------------------------------
1 | SWOOLE_PROCESS,
17 | 'servers' => [
18 | [
19 | 'name' => 'http',
20 | 'type' => Server::SERVER_HTTP,
21 | 'host' => '0.0.0.0',
22 | 'port' => 9501,
23 | 'sock_type' => SWOOLE_SOCK_TCP,
24 | 'callbacks' => [
25 | Event::ON_REQUEST => [Hyperf\HttpServer\Server::class, 'onRequest'],
26 | ],
27 | ],
28 | [
29 | 'name' => 'ws',
30 | 'type' => Server::SERVER_WEBSOCKET,
31 | 'host' => '0.0.0.0',
32 | 'port' => 9502,
33 | 'sock_type' => SWOOLE_SOCK_TCP,
34 | 'callbacks' => [
35 | Event::ON_HAND_SHAKE => [Hyperf\WebSocketServer\Server::class, 'onHandShake'],
36 | Event::ON_MESSAGE => [Hyperf\WebSocketServer\Server::class, 'onMessage'],
37 | Event::ON_CLOSE => [Hyperf\WebSocketServer\Server::class, 'onClose'],
38 | ],
39 | ],
40 | ],
41 | 'settings' => [
42 | 'enable_coroutine' => true,
43 | 'worker_num' => 4,
44 | 'pid_file' => BASE_PATH . '/runtime/hyperf.pid',
45 | 'open_tcp_nodelay' => true,
46 | 'max_coroutine' => 100000,
47 | 'open_http2_protocol' => true,
48 | 'max_request' => 100000,
49 | 'socket_buffer_size' => 2 * 1024 * 1024,
50 |
51 | 'document_root' => BASE_PATH . '/public',
52 | 'enable_static_handler' => true,
53 |
54 | 'task_worker_num' => 2,
55 | 'task_enable_coroutine' => false,
56 | ],
57 | 'callbacks' => [
58 | Event::ON_BEFORE_START => [Hyperf\Framework\Bootstrap\ServerStartCallback::class, 'beforeStart'],
59 | Event::ON_WORKER_START => [Hyperf\Framework\Bootstrap\WorkerStartCallback::class, 'onWorkerStart'],
60 | Event::ON_PIPE_MESSAGE => [Hyperf\Framework\Bootstrap\PipeMessageCallback::class, 'onPipeMessage'],
61 |
62 | // Task callbacks
63 | Event::ON_TASK => [Hyperf\Framework\Bootstrap\TaskCallback::class, 'onTask'],
64 | Event::ON_FINISH => [Hyperf\Framework\Bootstrap\FinishCallback::class, 'onFinish'],
65 | ],
66 | ];
67 |
--------------------------------------------------------------------------------
/config/autoload/view.php:
--------------------------------------------------------------------------------
1 | ThinkEngine::class,
17 | 'mode' => Mode::TASK,
18 | 'config' => [
19 | 'view_path' => BASE_PATH . '/storage/view/',
20 | 'cache_path' => BASE_PATH . '/runtime/view/',
21 | ],
22 | ];
23 |
--------------------------------------------------------------------------------
/config/config.php:
--------------------------------------------------------------------------------
1 | env('APP_NAME', 'skeleton'),
17 | 'app_env' => env('APP_ENV', 'dev'),
18 | 'scan_cacheable' => env('SCAN_CACHEABLE', false),
19 | StdoutLoggerInterface::class => [
20 | 'log_level' => [
21 | LogLevel::ALERT,
22 | LogLevel::CRITICAL,
23 | // LogLevel::DEBUG,
24 | LogLevel::EMERGENCY,
25 | LogLevel::ERROR,
26 | LogLevel::INFO,
27 | LogLevel::NOTICE,
28 | LogLevel::WARNING,
29 | ],
30 | ],
31 | ];
32 |
--------------------------------------------------------------------------------
/config/container.php:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 | ./test
14 |
15 |
16 |
17 |
18 | ./app
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/public/client/Const.js:
--------------------------------------------------------------------------------
1 | /** 主命令字定义 **/
2 | var MainCmd = {
3 | CMD_SYS : 1, /** 系统类(主命令字)- 客户端使用 **/
4 | CMD_GAME : 2, /** 游戏类(主命令字)- 客户端使用 **/
5 | }
6 |
7 | /** 子命令字定义 **/
8 | var SubCmd = {
9 | //系统子命令字,对应MainCmd.CMD_SYS
10 | LOGIN_FAIL_RESP : 100,
11 | HEART_ASK_REQ : 101,
12 | HEART_ASK_RESP : 102,
13 | BROADCAST_MSG_REQ : 103,
14 | BROADCAST_MSG_RESP : 104,
15 |
16 | //游戏逻辑子命令字,对应MainCmd.CMD_GAME
17 | SUB_GAME_START_REQ : 1, //游戏开始---> CGameStart
18 | SUB_GAME_START_RESP : 2, //游戏开始---> CGameStart
19 | SUB_USER_INFO_RESP : 3, //用户信息 ------> CUserInfo
20 | SUB_GAME_SEND_CARD_RESP : 4, //发牌 ------> CGameSendCard
21 | SUB_GAME_CALL_TIPS_RESP : 5, //叫地主提示(广播) --> CGameCall
22 | SUB_GAME_CALL_REQ : 6, //叫地主请求 --> CGameCallReq
23 | SUB_GAME_CALL_RESP : 7, //叫地主请求返回--CGameCallResp
24 | SUB_GAME_DOUBLE_TIPS_RESP : 8, //加倍提示(广播) --> CGameDouble
25 | SUB_GAME_DOUBLE_REQ : 9, //加倍请求--> CGameDoubleReq
26 | SUB_GAME_DOUBLE_RESP : 10, //加倍请求返回----> CGameDoubleResp
27 | SUB_GAME_CATCH_BASECARD_RESP : 11, //摸底牌 ---> CGameCatchBaseCard
28 | SUB_GAME_OUT_CARD : 12, //出牌提示 --> CGameOutCard
29 | SUB_GAME_OUT_CARD_REQ : 13, //出牌请求 --> CGameOutCardReq
30 | SUB_GAME_OUT_CARD_RESP : 14, //出牌返回 --> CGameOutCardResp
31 |
32 | SUB_GAME_ROOM_CREATE : 31,
33 | SUB_GAME_ROOM_JOIN : 32,
34 |
35 | CHAT_MSG_REQ : 213, //聊天消息请求,客户端使用
36 | CHAT_MSG_RESP : 214, //聊天消息响应,服务端使用
37 | }
38 |
39 |
40 | /**
41 | * 路由规则,key主要命令字=》array(子命令字对应策略类名)
42 | * 每条客户端对应的请求,路由到对应的逻辑处理类上处理
43 | *
44 | */
45 | var Route = {
46 | 1 : {
47 | 100 : 'loginFail', //登陆失败
48 | 105 : 'loginSuccess', //登陆成功
49 | 102 : 'heartAsk', //心跳处理
50 | 104 : 'broadcast', //广播消息
51 | 106 : 'enterRoomFail', //进入房间失败
52 | 107 : 'enterRoomSucc', //进入房间成功
53 | 108 : 'createRoomSucc', //创建房间成功
54 | 109 : 'createRoomFail', //创建房间失败
55 | },
56 | 2 : {
57 | 2 : 'gameStart', //获取卡牌
58 | 214 : 'chatMsg',
59 | 3 : 'userInfo', //显示用户信息
60 | 5 : 'gameCallTips', //叫地主广播
61 | 7 : 'gameCall', //叫地主返回
62 | 11 : 'gameCatchCardTips', //摸底牌
63 | 12 : 'gameOutCard', //出牌广播
64 | 14 : 'gameOutCardResp', //出牌响应
65 | },
66 | }
67 |
68 | /**
69 | * 花色类型
70 | */
71 | var CardType = {
72 | HEITAO : 0, //黑桃
73 | HONGTAO : 1, //红桃
74 | MEIHUA : 2, //梅花
75 | FANGKUAI : 3, //方块
76 | XIAOWANG : 4, //小王
77 | DAWANG : 5, //大王
78 | }
79 | /**
80 | * 牌显示出来的值
81 | */
82 | var CardVal = {
83 | CARD_SAN : '3', //牌值3
84 | CARD_SI : '4', //牌值4
85 | CARD_WU : '5', //牌值5
86 | CARD_LIU : '6', //牌值6
87 | CARD_QI : '7', //牌值7
88 | CARD_BA : '8', //牌值8
89 | CARD_JIU : '9', //牌值9
90 | CARD_SHI : '10', //牌值10
91 | CARD_J : 'J', //牌值J
92 | CARD_Q : 'Q', //牌值Q
93 | CARD_K : 'K', //牌值K
94 | CARD_A : 'A', //牌值A
95 | CARD_ER : '2', //牌值2
96 | CARD_XIAOWANG : 'w', //牌值小王
97 | CARD_DAWANG : 'W', //牌值大王
98 | }
99 |
--------------------------------------------------------------------------------
/public/client/Init.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 初始化类,websock服务器类
3 | */
4 |
5 | var Init = {
6 | ws : null,
7 | url : "",
8 | timer : 0,
9 | reback_times : 100, //断线重连次数
10 | dubug : true,
11 |
12 | //启动websocket
13 | webSock: function (url) {
14 | this.url = url;
15 | ws = new WebSocket(url);
16 | ws.binaryType = "arraybuffer"; //设置为2进制类型 webSocket.binaryType = "blob" ;
17 | var obj = this;
18 | //连接回调
19 | ws.onopen = function(evt) {
20 | Req.heartBeat(obj);
21 | //清除定时器
22 | clearInterval(obj.timer);
23 | //获取用户状态
24 | obj.log('系统提示: 连接服务器成功');
25 | };
26 |
27 | //消息回调
28 | ws.onmessage = function(evt) {
29 | if(!evt.data) return ;
30 | var total_data = new DataView(evt.data);
31 | var total_len = total_data.byteLength;
32 | if(total_data.byteLength < 4){
33 | obj.log('系统提示: 数据格式有问题');
34 | ws.close();
35 | return ;
36 | }
37 |
38 | //进行粘包处理
39 | var off = 0;
40 | var guid = body = '';
41 | while(total_len > off) {
42 | var len = total_data.getUint32(off);
43 | var data = evt.data.slice(off, off + len + 4);
44 | //解析body
45 | body = Packet.msgunpack(data);
46 | //转发响应的请求
47 | obj.recvCmd(body);
48 | off += len + 4;
49 | }
50 |
51 | };
52 | //关闭回调
53 | ws.onclose = function(evt) {
54 | //断线重新连接
55 | obj.timer = setInterval(function () {
56 | if(obj.reback_times == 0) {
57 | clearInterval(obj.timer);
58 | clearInterval(Req.timer);
59 | } else {
60 | obj.reback_times--;
61 | obj.webSock(obj.url);
62 | }
63 | },5000);
64 | obj.log('系统提示: 连接断开');
65 | };
66 | //socket错误回调
67 | ws.onerror = function(evt) {
68 | obj.log('系统提示: 服务器错误'+evt.type);
69 | };
70 | this.ws = ws;
71 | return this;
72 | },
73 |
74 | //处理消息回调命令字
75 | recvCmd: function(body) {
76 | console.log('debub data'+body);
77 | console.log(body);
78 | var len = body['len'];
79 | var cmd = body['cmd'];
80 | var scmd = body['scmd'];
81 | var data = body['data'];
82 | this.log('websocket Recv <<< len='+len+" cmd="+cmd+" scmd="+scmd);
83 | //路由到处理地方
84 | var func = Route[cmd][scmd];
85 | var str = 'Resp.'+func;
86 | if(func) {
87 | if(typeof(eval(str)) == 'function') {
88 | eval("Resp."+func+"(data)");
89 | } else {
90 | document.getElementById('msgText').innerHTML += func+':'+JSON.stringify(data) + '\n';
91 | }
92 | } else {
93 | this.log('func is valid');
94 | }
95 |
96 | this.log("websocket Recv body <<< func="+func);
97 | this.log(body);
98 | },
99 |
100 | //打印日志方法
101 | log: function(msg) {
102 | if(this.dubug) {
103 | console.log(msg);
104 | }
105 | },
106 |
107 | //发送数据
108 | send: function(data, cmd, scmd) {
109 | //this.ws.close();
110 | this.log("websocket Send >>> cmd="+cmd+" scmd="+scmd+" data=");
111 | this.log(data);
112 | var pack_data = Packet.msgpack(data, cmd, scmd);
113 | //组装数据
114 | if(this.ws.readyState == this.ws.OPEN) {
115 | this.ws.send(pack_data);
116 | }
117 | }
118 | }
--------------------------------------------------------------------------------
/public/client/Packet.js:
--------------------------------------------------------------------------------
1 | var Packet = {
2 | //jons数据打包成二进制数据
3 | encode : function(data, cmd, scmd) {
4 | var data = JSON.stringify(data);
5 | var len = data.length + 6;
6 | var buf = new ArrayBuffer(len); // 每个字符占用1个字节
7 | var buff_data = new DataView(buf, 0, len);
8 | var str_len = data.length;
9 | buff_data.setUint32(0, str_len+2);
10 | buff_data.setUint8(4, cmd);
11 | buff_data.setUint8(5, scmd);
12 | for (var i = 0; i < str_len; i++) {
13 | buff_data.setInt8(i+6, data.charCodeAt(i));
14 | }
15 | return buf;
16 | },
17 |
18 | //二进制数据解包成二进制数据
19 | decode : function(buff) {
20 | var body = '';
21 | var len = new DataView(buff, 0, 4).getUint32(0);
22 | var body_data = new DataView(buff, 4, len);
23 | //解析cmd
24 | var cmd = body_data.getUint8(0);
25 | var scmd = body_data.getUint8(1);
26 |
27 | //解析body
28 | for(var i = 2; i < body_data.byteLength; i++) {
29 | body += String.fromCharCode(body_data.getUint8(i));
30 | }
31 | //console.log("data decode >>> len="+len+" cmd="+cmd+" scmd="+scmd+" data="+body);
32 | var body = JSON.parse(body);
33 | body["cmd"] = cmd;
34 | body["scmd"] = scmd;
35 | return body;
36 | },
37 |
38 | encodeUTF8: function(str){
39 | var temp = "",rs = "";
40 | for( var i=0 , len = str.length; i < len; i++ ){
41 | temp = str.charCodeAt(i).toString(16);
42 | rs += "\\u"+ new Array(5-temp.length).join("0") + temp;
43 | }
44 | return rs;
45 | },
46 |
47 | decodeUTF8: function(str){
48 | return str.replace(/(\\u)(\w{4}|\w{2})/gi, function($0,$1,$2){
49 | return String.fromCharCode(parseInt($2,16));
50 | });
51 | },
52 |
53 | //使用msgpack解包arraybuf数据
54 | msgunpack: function(buff) {
55 | var body = '';
56 | var len = new DataView(buff, 0, 4).getUint32(0);
57 | var body_data = new DataView(buff, 4, len);
58 | //解析cmd
59 | var cmd = body_data.getUint8(0);
60 | var scmd = body_data.getUint8(1);
61 |
62 | //解析body
63 | for(var i = 2; i < body_data.byteLength; i++) {
64 | body += String.fromCharCode(body_data.getUint8(i));
65 | }
66 | //console.log("data msgpack decode >>> cmd="+cmd+" scmd="+scmd+" len="+len+" data="+body);
67 | var body = msgpack.unpack(body);
68 | body["cmd"] = cmd;
69 | body["scmd"] = scmd;
70 | body["len"] = len;
71 | return body;
72 | },
73 |
74 | //使用packmsg打包object数据对象
75 | msgpack: function(data, cmd, scmd) {
76 | //var dt = {};
77 | //dt.data = data;
78 | var data_buff = msgpack.pack(data);
79 | var str_buff = String.fromCharCode.apply(null, new Uint8Array(data_buff));
80 | var len = str_buff.length + 6;
81 | var buf = new ArrayBuffer(len); // 每个字符占用1个字节
82 | var buff_data = new DataView(buf, 0, len);
83 | var str_len = str_buff.length;
84 | buff_data.setUint32(0, str_len + 2);
85 | buff_data.setUint8(4, cmd);
86 | buff_data.setUint8(5, scmd);
87 |
88 | for (var i = 0; i < str_len; i++) {
89 | buff_data.setInt8(i+6, str_buff.charCodeAt(i));
90 | }
91 | //console.log("data msgpack encode >>> cmd="+cmd+" scmd="+scmd+" len="+len+" data=");
92 | //console.log(data);
93 | return buf;
94 | }
95 | }
--------------------------------------------------------------------------------
/public/client/Req.js:
--------------------------------------------------------------------------------
1 | /**发请求命令字处理类*/
2 |
3 | var Req = {
4 | //定时器
5 | timer : 0,
6 |
7 | //发送心跳
8 | heartBeat:function(obj) {
9 | this.timer = setInterval(function () {
10 | if(obj.ws.readyState == obj.ws.OPEN) {
11 | var data = {};
12 | data['time'] = (new Date()).valueOf()
13 | obj.send(data, MainCmd.CMD_SYS, SubCmd.HEART_ASK_REQ);
14 | } else {
15 | clearInterval(this.timer);
16 | }
17 | }, 600000);
18 | },
19 |
20 |
21 | //创建房间
22 | RoomCreate: function(obj,data) {
23 | var data = {};
24 | obj.send(data, MainCmd.CMD_GAME, SubCmd.SUB_GAME_ROOM_CREATE);
25 | },
26 |
27 | //加入房间
28 | RoomJoin: function(obj,data) {
29 | obj.send(data, MainCmd.CMD_GAME, SubCmd.SUB_GAME_ROOM_JOIN);
30 | },
31 |
32 | //游戏开始
33 | GameStart: function(obj,data) {
34 | var data = {};
35 | obj.send(data, MainCmd.CMD_GAME, SubCmd.SUB_GAME_START_REQ);
36 | },
37 |
38 | //抢地主
39 | GameCall: function(obj,status) {
40 | var data = {"type": status};
41 | obj.send(data, MainCmd.CMD_GAME, SubCmd.SUB_GAME_CALL_REQ);
42 | },
43 |
44 | //玩游戏
45 | PlayGame: function(obj,data) {
46 | obj.send(data, MainCmd.CMD_GAME, SubCmd.SUB_GAME_OUT_CARD_REQ);
47 | },
48 |
49 | //聊天消息
50 | ChatMsg: function(obj, data) {
51 | var data = {data};
52 | obj.send(data, MainCmd.CMD_GAME, SubCmd.CHAT_MSG_REQ);
53 | },
54 | }
--------------------------------------------------------------------------------
/public/client/Resp.js:
--------------------------------------------------------------------------------
1 | /**响应服务器命令字处理类*/
2 |
3 | var Resp = {
4 | //心跳响应
5 | heartAsk: function(data) {
6 | this.log(data);
7 | this.showTips('心跳数据:');
8 | },
9 |
10 | //登录失败
11 | loginFail: function(data) {
12 | this.log(data);
13 | this.showTips('登录失败:');
14 | //跳转登陆页面
15 | document.location.href = "login";
16 | },
17 |
18 | //登录成功响应
19 | loginSuccess: function(data) {
20 | this.log(data);
21 | this.showTips('登录成功');
22 | //如果用户在房间里, 直接进入房间
23 | if(data.is_room == 1) {
24 | //进入房间请求
25 | Req.GameStart(obj,{});
26 | }
27 | },
28 |
29 | //游戏开始响应
30 | gameStart: function(data) {
31 | this.log(data);
32 | this.showTips('游戏开始');
33 | },
34 |
35 | //用户信息
36 | userInfo: function(data) {
37 | this.log(data);
38 | this.showTips('用户信息');
39 | },
40 |
41 | //叫地主
42 | gameCall: function(data) {
43 | this.log(data);
44 | this.showTips('我叫地主成功');
45 | document.getElementById('call').disabled = true;
46 | document.getElementById('nocall').disabled = true;
47 | },
48 |
49 | //叫地主广播
50 | gameCallTips: function(data) {
51 | this.log(data);
52 | if(data.calltype == 1) {
53 | var tips = data.account+'叫地主';
54 | } else {
55 | var tips = data.account+'不叫';
56 | }
57 | this.showTips('广播:'+tips);
58 | },
59 |
60 | //摸底牌
61 | gameCatchCardTips: function(data) {
62 | this.log(data);
63 | this.showTips('广播:'+data.user+'摸底牌'+data.hand_card);
64 | //摸完底牌, 重新构造牌, 这里偷懒, 重新触发开始游戏
65 | Req.GameStart(obj,{});
66 | },
67 |
68 |
69 | //聊天数据
70 | chatMsg: function(data) {
71 | this.log(data);
72 | // this.showTips('聊天内容是:'+JSON.stringify(data));
73 | this.showTips(data.user + '发送的聊天内容是:' + data.data);
74 | },
75 |
76 | //进入房间失败
77 | enterRoomFail: function(data) {
78 | this.log(data);
79 | this.showTips('进入房间失败:'+data.msg);
80 | },
81 |
82 |
83 | //创建房间
84 | RoomCreate: function(obj,data) {
85 | var data = {};
86 | obj.send(data, MainCmd.CMD_GAME, SubCmd.SUB_GAME_ROOM_CREATE);
87 | },
88 |
89 | //加入房间
90 | RoomJoin: function(obj,data) {
91 | obj.send(data, MainCmd.CMD_GAME, SubCmd.SUB_GAME_ROOM_JOIN);
92 | },
93 |
94 | //进入房间成功,解锁按钮
95 | enterRoomSucc: function(data) {
96 | this.log(data);
97 | this.showTips('进入房间成功:'+JSON.stringify(data));
98 | var card = data.card;
99 | var chair_id = data.chair_id;
100 | var is_master = data.is_master;
101 | var is_game_over = data.is_game_over;
102 | info = data
103 | if(typeof(data.calltype) == 'undefined') {
104 | document.getElementById('call').disabled = false;
105 | document.getElementById('nocall').disabled = false;
106 | } else {
107 | document.getElementById('call').disabled = true;
108 | document.getElementById('nocall').disabled = true;
109 | }
110 |
111 | //显示牌
112 | if(card && chair_id) {
113 | //循环展现牌
114 | var show_card = '';
115 | for(var k in card) {
116 | show_card += ''+this.getCard(card[k])+'';
117 | }
118 | var id = 'chair_'+chair_id;
119 | document.getElementById(id).innerHTML = show_card;
120 | }
121 |
122 | //是否为地主
123 | if(is_master == 1) {
124 | if(typeof(data.master) != 'undefined') {
125 | document.getElementById('master').innerHTML = '(地主)-'+chair_id+'号位置';
126 | } else {
127 | document.getElementById('master').innerHTML = '(农民)-'+chair_id+'号位置';
128 | }
129 | }
130 |
131 | //判断游戏是否结束
132 | if(is_game_over) {
133 | this.showTips('游戏结束');
134 | } else {
135 | //轮到谁出来, 就解锁谁的按钮
136 | if(typeof(data.index_chair_id) != 'undefined' && info.chair_id == data.index_chair_id) {
137 | //解锁打牌按钮
138 | document.getElementById('play').disabled = false;
139 | document.getElementById('pass').disabled = false;
140 | var tips = data.is_first_round ? '请首次出牌' : '请跟牌';
141 | this.showTips(tips);
142 | } else {
143 | document.getElementById('play').disabled = true;
144 | document.getElementById('pass').disabled = true;
145 | }
146 | }
147 | },
148 |
149 | //出牌提示
150 | gameOutCard: function(data) {
151 | this.log(data);
152 | this.showTips('出牌提示:'+data.msg);
153 | if(data.status == 0) {
154 | //移除当前牌元素
155 | var obj_box = document.getElementsByName("handcard");
156 | var obj_item = [];
157 | for(k in obj_box){
158 | if(obj_box[k].checked){
159 | obj_item[k] = obj_box[k].parentNode;
160 | }
161 | }
162 | //循环删除
163 | for(k in obj_item){
164 | obj_item[k].remove(this);
165 | }
166 | }
167 | },
168 |
169 | //出牌广播响应
170 | gameOutCardResp: function(data) {
171 | //判断游戏是否结束
172 | if(data.is_game_over) {
173 | this.showTips('广播:游戏结束,' + data.result + ', 请点击"开始游戏",进行下一轮游戏');
174 | //手牌重置
175 | document.getElementById('chair_1').innerHTML = '';
176 | document.getElementById('chair_2').innerHTML = '';
177 | document.getElementById('chair_3').innerHTML = '';
178 | document.getElementById('last_card').innerHTML = '';
179 | document.getElementById('out_card').innerHTML = '';
180 | document.getElementById('play').disabled = true;
181 | document.getElementById('pass').disabled = true;
182 | } else {
183 | this.log(data);
184 | var play = data.show_type == 1 ? '跟牌' : '过牌';
185 | if(data.last_card == null || data.last_card.length < 1) {
186 | play = '出牌';
187 | }
188 | this.showTips('广播: 第'+data.round+'回合,第'+data.hand_num+'手出牌, '+data.account+play+', 上次牌值是'+data.last_card+', 本次出牌值是'+data.card+', 本次出牌型是'+data.card_type);
189 | this.showPlayCard(data.last_card,data.card);
190 |
191 | //自己出牌按钮变灰
192 | if(info.chair_id == data.next_chair_id) {
193 | document.getElementById('play').disabled = false;
194 | document.getElementById('pass').disabled = false;
195 | //提示下一个跟牌操作
196 | var tips = data.is_first_round ? '请首次出牌' : '请跟牌';
197 | this.showTips(tips);
198 | } else {
199 | document.getElementById('play').disabled = true;
200 | document.getElementById('pass').disabled = true;
201 | }
202 | }
203 |
204 | },
205 |
206 | //广播消息响应
207 | broadcast: function(data) {
208 | this.log(data);
209 | this.showTips("广播:消息,"+JSON.stringify(data));
210 | },
211 |
212 | //显示打牌过程
213 | showPlayCard: function(last_card, out_card) {
214 | document.getElementById('last_card').innerHTML = '';
215 | document.getElementById('out_card').innerHTML = '';
216 | if(last_card != null && typeof(last_card) == 'object' && last_card.length > 0) {
217 | var l = '';
218 | for(k in last_card) {
219 | l += ''+this.getCard(last_card[k])+'';
220 | }
221 | document.getElementById('last_card').innerHTML = l;
222 | }
223 | if(out_card != null && typeof(out_card) == 'object' && out_card.length > 0) {
224 | var n = '';
225 | for(j in out_card) {
226 | n += ''+this.getCard(out_card[j])+'';
227 | }
228 | document.getElementById('out_card').innerHTML = n;
229 | }
230 |
231 | },
232 |
233 | //构造牌
234 | getCard: function(card_val) {
235 | var card = '';
236 | var color = parseInt(card_val / 16);
237 | if(color == CardType.HEITAO) {
238 | card += '♠';
239 | } else if(color == CardType.HONGTAO) {
240 | card += '♥';
241 | } else if(color == CardType.MEIHUA) {
242 | card += '♣';
243 | } else if(color == CardType.FANGKUAI) {
244 | card += '♦';
245 | } else if(color == CardType.XIAOWANG) {
246 | if(card_val == 78) {
247 | card += 's';
248 | } else if(card_val == 79) {
249 | card += 'B';
250 | }
251 | }
252 |
253 | if(card_val == 78) {
254 | card +='_'+CardVal.CARD_XIAOWANG;
255 | } else if(card_val == 79) {
256 | card +='_'+CardVal.CARD_DAWANG;
257 | } else {
258 | //牌值渲染
259 | var value = parseInt(card_val % 16);
260 | switch(value) {
261 | case 1:
262 | card +='_'+CardVal.CARD_SAN;
263 | break;
264 | case 2:
265 | card +='_'+CardVal.CARD_SI;
266 | break;
267 | case 3:
268 | card +='_'+CardVal.CARD_WU;
269 | break;
270 | case 4:
271 | card +='_'+CardVal.CARD_LIU;
272 | break;
273 | case 5:
274 | card +='_'+CardVal.CARD_QI;
275 | break;
276 | case 6:
277 | card +='_'+CardVal.CARD_BA;
278 | break;
279 | case 7:
280 | card +='_'+CardVal.CARD_JIU;
281 | break;
282 | case 8:
283 | card +='_'+CardVal.CARD_SHI;
284 | break;
285 | case 9:
286 | card +='_'+CardVal.CARD_J;
287 | break;
288 | case 10:
289 | card +='_'+CardVal.CARD_Q;
290 | break;
291 | case 11:
292 | card +='_'+CardVal.CARD_K;
293 | break;
294 | case 12:
295 | card +='_'+CardVal.CARD_A;
296 | break;
297 | case 13:
298 | card +='_'+CardVal.CARD_ER;
299 | break;
300 | }
301 | }
302 | return card;
303 | },
304 |
305 | //日志显示协议返回数据
306 | log: function(data) {
307 | //document.getElementById('msgText').innerHTML += JSON.stringify(data) + '\n';
308 | console.log(data);
309 | },
310 |
311 | //显示提示语句
312 | showTips: function(tips) {
313 | document.getElementById('msgText').innerHTML += tips + '\n';
314 | }
315 | }
--------------------------------------------------------------------------------
/public/client/msgpack.js:
--------------------------------------------------------------------------------
1 | /*!{id:msgpack.js,ver:1.05,license:"MIT",author:"uupaa.js@gmail.com"}*/
2 |
3 | // === msgpack ===
4 | // MessagePack -> http://msgpack.sourceforge.net/
5 |
6 | this.msgpack || (function(globalScope) {
7 |
8 | globalScope.msgpack = {
9 | pack: msgpackpack, // msgpack.pack(data:Mix,
10 | // toString:Boolean = false):ByteArray/ByteString/false
11 | // [1][mix to String] msgpack.pack({}, true) -> "..."
12 | // [2][mix to ByteArray] msgpack.pack({}) -> [...]
13 | unpack: msgpackunpack, // msgpack.unpack(data:BinaryString/ByteArray):Mix
14 | // [1][String to mix] msgpack.unpack("...") -> {}
15 | // [2][ByteArray to mix] msgpack.unpack([...]) -> {}
16 | worker: "msgpack.js", // msgpack.worker - WebWorkers script filename
17 | upload: msgpackupload, // msgpack.upload(url:String, option:Hash, callback:Function)
18 | download: msgpackdownload // msgpack.download(url:String, option:Hash, callback:Function)
19 | };
20 |
21 | var _ie = /MSIE/.test(navigator.userAgent),
22 | _bin2num = {}, // BinaryStringToNumber { "\00": 0, ... "\ff": 255 }
23 | _num2bin = {}, // NumberToBinaryString { 0: "\00", ... 255: "\ff" }
24 | _num2b64 = ("ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
25 | "abcdefghijklmnopqrstuvwxyz0123456789+/").split(""),
26 | _buf = [], // decode buffer
27 | _idx = 0, // decode buffer[index]
28 | _error = 0, // msgpack.pack() error code. 1 = CYCLIC_REFERENCE_ERROR
29 | _isArray = Array.isArray || (function(mix) {
30 | return Object.prototype.toString.call(mix) === "[object Array]";
31 | }),
32 | _isUint8Array = function(mix) {
33 | return Object.prototype.toString.call(mix) === "[object Uint8Array]";
34 | },
35 | _toString = String.fromCharCode, // CharCode/ByteArray to String
36 | _MAX_DEPTH = 512;
37 |
38 | // for WebWorkers Code Block
39 | self.importScripts && (onmessage = function(event) {
40 | if (event.data.method === "pack") {
41 | postMessage(base64encode(msgpackpack(event.data.data)));
42 | } else {
43 | postMessage(msgpackunpack(event.data.data));
44 | }
45 | });
46 |
47 | // msgpack.pack
48 | function msgpackpack(data, // @param Mix:
49 | toString) { // @param Boolean(= false):
50 | // @return ByteArray/BinaryString/false:
51 | // false is error return
52 | // [1][mix to String] msgpack.pack({}, true) -> "..."
53 | // [2][mix to ByteArray] msgpack.pack({}) -> [...]
54 |
55 | _error = 0;
56 |
57 | var byteArray = encode([], data, 0);
58 |
59 | return _error ? false
60 | : toString ? byteArrayToByteString(byteArray)
61 | : byteArray;
62 | }
63 |
64 | // msgpack.unpack
65 | function msgpackunpack(data) { // @param BinaryString/ByteArray:
66 | // @return Mix/undefined:
67 | // undefined is error return
68 | // [1][String to mix] msgpack.unpack("...") -> {}
69 | // [2][ByteArray to mix] msgpack.unpack([...]) -> {}
70 |
71 | _buf = typeof data === "string" ? toByteArray(data) : data;
72 | _idx = -1;
73 | return decode(); // mix or undefined
74 | }
75 |
76 | // inner - encoder
77 | function encode(rv, // @param ByteArray: result
78 | mix, // @param Mix: source data
79 | depth) { // @param Number: depth
80 | var size, i, iz, c, pos, // for UTF8.encode, Array.encode, Hash.encode
81 | high, low, sign, exp, frac; // for IEEE754
82 |
83 | if (mix == null) { // null or undefined -> 0xc0 ( null )
84 | rv.push(0xc0);
85 | } else if (mix === false) { // false -> 0xc2 ( false )
86 | rv.push(0xc2);
87 | } else if (mix === true) { // true -> 0xc3 ( true )
88 | rv.push(0xc3);
89 | } else {
90 | switch (typeof mix) {
91 | case "number":
92 | if (mix !== mix) { // isNaN
93 | rv.push(0xcb, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff); // quiet NaN
94 | } else if (mix === Infinity) {
95 | rv.push(0xcb, 0x7f, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00); // positive infinity
96 | } else if (Math.floor(mix) === mix) { // int or uint
97 | if (mix < 0) {
98 | // int
99 | if (mix >= -32) { // negative fixnum
100 | rv.push(0xe0 + mix + 32);
101 | } else if (mix > -0x80) {
102 | rv.push(0xd0, mix + 0x100);
103 | } else if (mix > -0x8000) {
104 | mix += 0x10000;
105 | rv.push(0xd1, mix >> 8, mix & 0xff);
106 | } else if (mix > -0x80000000) {
107 | mix += 0x100000000;
108 | rv.push(0xd2, mix >>> 24, (mix >> 16) & 0xff,
109 | (mix >> 8) & 0xff, mix & 0xff);
110 | } else {
111 | high = Math.floor(mix / 0x100000000);
112 | low = mix & 0xffffffff;
113 | rv.push(0xd3, (high >> 24) & 0xff, (high >> 16) & 0xff,
114 | (high >> 8) & 0xff, high & 0xff,
115 | (low >> 24) & 0xff, (low >> 16) & 0xff,
116 | (low >> 8) & 0xff, low & 0xff);
117 | }
118 | } else {
119 | // uint
120 | if (mix < 0x80) {
121 | rv.push(mix); // positive fixnum
122 | } else if (mix < 0x100) { // uint 8
123 | rv.push(0xcc, mix);
124 | } else if (mix < 0x10000) { // uint 16
125 | rv.push(0xcd, mix >> 8, mix & 0xff);
126 | } else if (mix < 0x100000000) { // uint 32
127 | rv.push(0xce, mix >>> 24, (mix >> 16) & 0xff,
128 | (mix >> 8) & 0xff, mix & 0xff);
129 | } else {
130 | high = Math.floor(mix / 0x100000000);
131 | low = mix & 0xffffffff;
132 | rv.push(0xcf, (high >> 24) & 0xff, (high >> 16) & 0xff,
133 | (high >> 8) & 0xff, high & 0xff,
134 | (low >> 24) & 0xff, (low >> 16) & 0xff,
135 | (low >> 8) & 0xff, low & 0xff);
136 | }
137 | }
138 | } else { // double
139 | // THX!! @edvakf
140 | // http://javascript.g.hatena.ne.jp/edvakf/20101128/1291000731
141 | sign = mix < 0;
142 | sign && (mix *= -1);
143 |
144 | // add offset 1023 to ensure positive
145 | // 0.6931471805599453 = Math.LN2;
146 | exp = ((Math.log(mix) / 0.6931471805599453) + 1023) | 0;
147 |
148 | // shift 52 - (exp - 1023) bits to make integer part exactly 53 bits,
149 | // then throw away trash less than decimal point
150 | frac = mix * Math.pow(2, 52 + 1023 - exp);
151 |
152 | // S+-Exp(11)--++-----------------Fraction(52bits)-----------------------+
153 | // || || |
154 | // v+----------++--------------------------------------------------------+
155 | // 00000000|00000000|00000000|00000000|00000000|00000000|00000000|00000000
156 | // 6 5 55 4 4 3 2 1 8 0
157 | // 3 6 21 8 0 2 4 6
158 | //
159 | // +----------high(32bits)-----------+ +----------low(32bits)------------+
160 | // | | | |
161 | // +---------------------------------+ +---------------------------------+
162 | // 3 2 21 1 8 0
163 | // 1 4 09 6
164 | low = frac & 0xffffffff;
165 | sign && (exp |= 0x800);
166 | high = ((frac / 0x100000000) & 0xfffff) | (exp << 20);
167 |
168 | rv.push(0xcb, (high >> 24) & 0xff, (high >> 16) & 0xff,
169 | (high >> 8) & 0xff, high & 0xff,
170 | (low >> 24) & 0xff, (low >> 16) & 0xff,
171 | (low >> 8) & 0xff, low & 0xff);
172 | }
173 | break;
174 | case "string":
175 | // http://d.hatena.ne.jp/uupaa/20101128
176 | iz = mix.length;
177 | pos = rv.length; // keep rewrite position
178 |
179 | rv.push(0); // placeholder
180 |
181 | // utf8.encode
182 | for (i = 0; i < iz; ++i) {
183 | c = mix.charCodeAt(i);
184 | if (c < 0x80) { // ASCII(0x00 ~ 0x7f)
185 | rv.push(c & 0x7f);
186 | } else if (c < 0x0800) {
187 | rv.push(((c >>> 6) & 0x1f) | 0xc0, (c & 0x3f) | 0x80);
188 | } else if (c < 0x10000) {
189 | rv.push(((c >>> 12) & 0x0f) | 0xe0,
190 | ((c >>> 6) & 0x3f) | 0x80, (c & 0x3f) | 0x80);
191 | }
192 | }
193 | size = rv.length - pos - 1;
194 |
195 | if (size < 32) {
196 | rv[pos] = 0xa0 + size; // rewrite
197 | } else if (size < 0x100) { // 8
198 | rv.splice(pos, 1, 0xd9, size);
199 | } else if (size < 0x10000) { // 16
200 | rv.splice(pos, 1, 0xda, size >> 8, size & 0xff);
201 | } else if (size < 0x100000000) { // 32
202 | rv.splice(pos, 1, 0xdb,
203 | size >>> 24, (size >> 16) & 0xff,
204 | (size >> 8) & 0xff, size & 0xff);
205 | }
206 | break;
207 | default: // array, hash, or Uint8Array
208 | if (_isUint8Array(mix)) {
209 | size = mix.length;
210 |
211 | if (size < 0x100) { // 8
212 | rv.push(0xc4, size);
213 | } else if (size < 0x10000) { // 16
214 | rv.push(0xc5, size >> 8, size & 0xff);
215 | } else if (size < 0x100000000) { // 32
216 | rv.push(0xc6, size >>> 24, (size >> 16) & 0xff,
217 | (size >> 8) & 0xff, size & 0xff);
218 | }
219 | Array.prototype.push.apply(rv, mix);
220 | break;
221 | }
222 | if (++depth >= _MAX_DEPTH) {
223 | _error = 1; // CYCLIC_REFERENCE_ERROR
224 | return rv = []; // clear
225 | }
226 | if (_isArray(mix)) {
227 | size = mix.length;
228 | if (size < 16) {
229 | rv.push(0x90 + size);
230 | } else if (size < 0x10000) { // 16
231 | rv.push(0xdc, size >> 8, size & 0xff);
232 | } else if (size < 0x100000000) { // 32
233 | rv.push(0xdd, size >>> 24, (size >> 16) & 0xff,
234 | (size >> 8) & 0xff, size & 0xff);
235 | }
236 | for (i = 0; i < size; ++i) {
237 | encode(rv, mix[i], depth);
238 | }
239 | } else { // hash
240 | // http://d.hatena.ne.jp/uupaa/20101129
241 | pos = rv.length; // keep rewrite position
242 | rv.push(0); // placeholder
243 | size = 0;
244 | for (i in mix) {
245 | ++size;
246 | encode(rv, i, depth);
247 | encode(rv, mix[i], depth);
248 | }
249 | if (size < 16) {
250 | rv[pos] = 0x80 + size; // rewrite
251 | } else if (size < 0x10000) { // 16
252 | rv.splice(pos, 1, 0xde, size >> 8, size & 0xff);
253 | } else if (size < 0x100000000) { // 32
254 | rv.splice(pos, 1, 0xdf,
255 | size >>> 24, (size >> 16) & 0xff,
256 | (size >> 8) & 0xff, size & 0xff);
257 | }
258 | }
259 | }
260 | }
261 | return rv;
262 | }
263 |
264 | // inner - decoder
265 | function decode() { // @return Mix:
266 | var size, i, iz, c, num = 0,
267 | sign, exp, frac, ary, hash,
268 | buf = _buf, type = buf[++_idx];
269 |
270 | if (type >= 0xe0) { // Negative FixNum (111x xxxx) (-32 ~ -1)
271 | return type - 0x100;
272 | }
273 | if (type < 0xc0) {
274 | if (type < 0x80) { // Positive FixNum (0xxx xxxx) (0 ~ 127)
275 | return type;
276 | }
277 | if (type < 0x90) { // FixMap (1000 xxxx)
278 | num = type - 0x80;
279 | type = 0x80;
280 | } else if (type < 0xa0) { // FixArray (1001 xxxx)
281 | num = type - 0x90;
282 | type = 0x90;
283 | } else { // if (type < 0xc0) { // FixRaw (101x xxxx)
284 | num = type - 0xa0;
285 | type = 0xa0;
286 | }
287 | }
288 | switch (type) {
289 | case 0xc0: return null;
290 | case 0xc2: return false;
291 | case 0xc3: return true;
292 | case 0xca: // float
293 | num = buf[++_idx] * 0x1000000 + (buf[++_idx] << 16) +
294 | (buf[++_idx] << 8) + buf[++_idx];
295 | sign = num & 0x80000000; // 1bit
296 | exp = (num >> 23) & 0xff; // 8bits
297 | frac = num & 0x7fffff; // 23bits
298 | if (!num || num === 0x80000000) { // 0.0 or -0.0
299 | return 0;
300 | }
301 | if (exp === 0xff) { // NaN or Infinity
302 | return frac ? NaN : Infinity;
303 | }
304 | return (sign ? -1 : 1) *
305 | (frac | 0x800000) * Math.pow(2, exp - 127 - 23); // 127: bias
306 | case 0xcb: // double
307 | num = buf[++_idx] * 0x1000000 + (buf[++_idx] << 16) +
308 | (buf[++_idx] << 8) + buf[++_idx];
309 | sign = num & 0x80000000; // 1bit
310 | exp = (num >> 20) & 0x7ff; // 11bits
311 | frac = num & 0xfffff; // 52bits - 32bits (high word)
312 | if (!num || num === 0x80000000) { // 0.0 or -0.0
313 | _idx += 4;
314 | return 0;
315 | }
316 | if (exp === 0x7ff) { // NaN or Infinity
317 | _idx += 4;
318 | return frac ? NaN : Infinity;
319 | }
320 | num = buf[++_idx] * 0x1000000 + (buf[++_idx] << 16) +
321 | (buf[++_idx] << 8) + buf[++_idx];
322 | return (sign ? -1 : 1) *
323 | ((frac | 0x100000) * Math.pow(2, exp - 1023 - 20) // 1023: bias
324 | + num * Math.pow(2, exp - 1023 - 52));
325 | // 0xcf: uint64, 0xce: uint32, 0xcd: uint16
326 | case 0xcf: num = buf[++_idx] * 0x1000000 + (buf[++_idx] << 16) +
327 | (buf[++_idx] << 8) + buf[++_idx];
328 | return num * 0x100000000 +
329 | buf[++_idx] * 0x1000000 + (buf[++_idx] << 16) +
330 | (buf[++_idx] << 8) + buf[++_idx];
331 | case 0xce: num += buf[++_idx] * 0x1000000 + (buf[++_idx] << 16);
332 | case 0xcd: num += buf[++_idx] << 8;
333 | case 0xcc: return num + buf[++_idx];
334 | // 0xd3: int64, 0xd2: int32, 0xd1: int16, 0xd0: int8
335 | case 0xd3: num = buf[++_idx];
336 | if (num & 0x80) { // sign -> avoid overflow
337 | return ((num ^ 0xff) * 0x100000000000000 +
338 | (buf[++_idx] ^ 0xff) * 0x1000000000000 +
339 | (buf[++_idx] ^ 0xff) * 0x10000000000 +
340 | (buf[++_idx] ^ 0xff) * 0x100000000 +
341 | (buf[++_idx] ^ 0xff) * 0x1000000 +
342 | (buf[++_idx] ^ 0xff) * 0x10000 +
343 | (buf[++_idx] ^ 0xff) * 0x100 +
344 | (buf[++_idx] ^ 0xff) + 1) * -1;
345 | }
346 | return num * 0x100000000000000 +
347 | buf[++_idx] * 0x1000000000000 +
348 | buf[++_idx] * 0x10000000000 +
349 | buf[++_idx] * 0x100000000 +
350 | buf[++_idx] * 0x1000000 +
351 | buf[++_idx] * 0x10000 +
352 | buf[++_idx] * 0x100 +
353 | buf[++_idx];
354 | case 0xd2: num = buf[++_idx] * 0x1000000 + (buf[++_idx] << 16) +
355 | (buf[++_idx] << 8) + buf[++_idx];
356 | return num < 0x80000000 ? num : num - 0x100000000; // 0x80000000 * 2
357 | case 0xd1: num = (buf[++_idx] << 8) + buf[++_idx];
358 | return num < 0x8000 ? num : num - 0x10000; // 0x8000 * 2
359 | case 0xd0: num = buf[++_idx];
360 | return num < 0x80 ? num : num - 0x100; // 0x80 * 2
361 | // 0xdb: str32, 0xda: str16, 0xd9: str8, 0xa0: fixstr
362 | case 0xdb: num += buf[++_idx] * 0x1000000 + (buf[++_idx] << 16);
363 | case 0xda: num += buf[++_idx] << 8;
364 | case 0xd9: num += buf[++_idx];
365 | case 0xa0: // utf8.decode
366 | for (ary = [], i = _idx, iz = i + num; i < iz; ) {
367 | c = buf[++i]; // lead byte
368 | ary.push(c < 0x80 ? c : // ASCII(0x00 ~ 0x7f)
369 | c < 0xe0 ? ((c & 0x1f) << 6 | (buf[++i] & 0x3f)) :
370 | ((c & 0x0f) << 12 | (buf[++i] & 0x3f) << 6
371 | | (buf[++i] & 0x3f)));
372 | }
373 | _idx = i;
374 | return ary.length < 10240 ? _toString.apply(null, ary)
375 | : byteArrayToByteString(ary);
376 | // 0xc6: bin32, 0xc5: bin16, 0xc4: bin8
377 | case 0xc6: num += buf[++_idx] * 0x1000000 + (buf[++_idx] << 16);
378 | case 0xc5: num += buf[++_idx] << 8;
379 | case 0xc4: num += buf[++_idx];
380 | var end = ++_idx + num
381 | var ret = buf.slice(_idx, end);
382 | _idx += num;
383 | return ret;
384 | // 0xdf: map32, 0xde: map16, 0x80: map
385 | case 0xdf: num += buf[++_idx] * 0x1000000 + (buf[++_idx] << 16);
386 | case 0xde: num += (buf[++_idx] << 8) + buf[++_idx];
387 | case 0x80: hash = {};
388 | while (num--) {
389 | // make key/value pair
390 | size = buf[++_idx] - 0xa0;
391 |
392 | for (ary = [], i = _idx, iz = i + size; i < iz; ) {
393 | c = buf[++i]; // lead byte
394 | ary.push(c < 0x80 ? c : // ASCII(0x00 ~ 0x7f)
395 | c < 0xe0 ? ((c & 0x1f) << 6 | (buf[++i] & 0x3f)) :
396 | ((c & 0x0f) << 12 | (buf[++i] & 0x3f) << 6
397 | | (buf[++i] & 0x3f)));
398 | }
399 | _idx = i;
400 | hash[_toString.apply(null, ary)] = decode();
401 | }
402 | return hash;
403 | // 0xdd: array32, 0xdc: array16, 0x90: array
404 | case 0xdd: num += buf[++_idx] * 0x1000000 + (buf[++_idx] << 16);
405 | case 0xdc: num += (buf[++_idx] << 8) + buf[++_idx];
406 | case 0x90: ary = [];
407 | while (num--) {
408 | ary.push(decode());
409 | }
410 | return ary;
411 | }
412 | return;
413 | }
414 |
415 | // inner - byteArray To ByteString
416 | function byteArrayToByteString(byteArray) { // @param ByteArray
417 | // @return String
418 | // http://d.hatena.ne.jp/uupaa/20101128
419 | try {
420 | return _toString.apply(this, byteArray); // toString
421 | } catch(err) {
422 | ; // avoid "Maximum call stack size exceeded"
423 | }
424 | var rv = [], i = 0, iz = byteArray.length, num2bin = _num2bin;
425 |
426 | for (; i < iz; ++i) {
427 | rv[i] = num2bin[byteArray[i]];
428 | }
429 | return rv.join("");
430 | }
431 |
432 | // msgpack.download - load from server
433 | function msgpackdownload(url, // @param String:
434 | option, // @param Hash: { worker, timeout, before, after }
435 | // option.worker - Boolean(= false): true is use WebWorkers
436 | // option.timeout - Number(= 10): timeout sec
437 | // option.before - Function: before(xhr, option)
438 | // option.after - Function: after(xhr, option, { status, ok })
439 | callback) { // @param Function: callback(data, option, { status, ok })
440 | // data - Mix/null:
441 | // option - Hash:
442 | // status - Number: HTTP status code
443 | // ok - Boolean:
444 | option.method = "GET";
445 | option.binary = true;
446 | ajax(url, option, callback);
447 | }
448 |
449 | // msgpack.upload - save to server
450 | function msgpackupload(url, // @param String:
451 | option, // @param Hash: { data, worker, timeout, before, after }
452 | // option.data - Mix:
453 | // option.worker - Boolean(= false): true is use WebWorkers
454 | // option.timeout - Number(= 10): timeout sec
455 | // option.before - Function: before(xhr, option)
456 | // option.after - Function: after(xhr, option, { status, ok })
457 | callback) { // @param Function: callback(data, option, { status, ok })
458 | // data - String: responseText
459 | // option - Hash:
460 | // status - Number: HTTP status code
461 | // ok - Boolean:
462 | option.method = "PUT";
463 | option.binary = true;
464 |
465 | if (option.worker && globalScope.Worker) {
466 | var worker = new Worker(msgpack.worker);
467 |
468 | worker.onmessage = function(event) {
469 | option.data = event.data;
470 | ajax(url, option, callback);
471 | };
472 | worker.postMessage({ method: "pack", data: option.data });
473 | } else {
474 | // pack and base64 encode
475 | option.data = base64encode(msgpackpack(option.data));
476 | ajax(url, option, callback);
477 | }
478 | }
479 |
480 | // inner -
481 | function ajax(url, // @param String:
482 | option, // @param Hash: { data, ifmod, method, timeout,
483 | // header, binary, before, after, worker }
484 | // option.data - Mix: upload data
485 | // option.ifmod - Boolean: true is "If-Modified-Since" header
486 | // option.method - String: "GET", "POST", "PUT"
487 | // option.timeout - Number(= 10): timeout sec
488 | // option.header - Hash(= {}): { key: "value", ... }
489 | // option.binary - Boolean(= false): true is binary data
490 | // option.before - Function: before(xhr, option)
491 | // option.after - Function: after(xhr, option, { status, ok })
492 | // option.worker - Boolean(= false): true is use WebWorkers
493 | callback) { // @param Function: callback(data, option, { status, ok })
494 | // data - String/Mix/null:
495 | // option - Hash:
496 | // status - Number: HTTP status code
497 | // ok - Boolean:
498 | function readyStateChange() {
499 | if (xhr.readyState === 4) {
500 | var data, status = xhr.status, worker, byteArray,
501 | rv = { status: status, ok: status >= 200 && status < 300 };
502 |
503 | if (!run++) {
504 | if (method === "PUT") {
505 | data = rv.ok ? xhr.responseText : "";
506 | } else {
507 | if (rv.ok) {
508 | if (option.worker && globalScope.Worker) {
509 | worker = new Worker(msgpack.worker);
510 | worker.onmessage = function(event) {
511 | callback(event.data, option, rv);
512 | };
513 | worker.postMessage({ method: "unpack",
514 | data: xhr.responseText });
515 | gc();
516 | return;
517 | } else {
518 | byteArray = _ie ? toByteArrayIE(xhr)
519 | : toByteArray(xhr.responseText);
520 | data = msgpackunpack(byteArray);
521 | }
522 | }
523 | }
524 | after && after(xhr, option, rv);
525 | callback(data, option, rv);
526 | gc();
527 | }
528 | }
529 | }
530 |
531 | function ng(abort, status) {
532 | if (!run++) {
533 | var rv = { status: status || 400, ok: false };
534 |
535 | after && after(xhr, option, rv);
536 | callback(null, option, rv);
537 | gc(abort);
538 | }
539 | }
540 |
541 | function gc(abort) {
542 | abort && xhr && xhr.abort && xhr.abort();
543 | watchdog && (clearTimeout(watchdog), watchdog = 0);
544 | xhr = null;
545 | globalScope.addEventListener &&
546 | globalScope.removeEventListener("beforeunload", ng, false);
547 | }
548 |
549 | var watchdog = 0,
550 | method = option.method || "GET",
551 | header = option.header || {},
552 | before = option.before,
553 | after = option.after,
554 | data = option.data || null,
555 | xhr = globalScope.XMLHttpRequest ? new XMLHttpRequest() :
556 | globalScope.ActiveXObject ? new ActiveXObject("Microsoft.XMLHTTP") :
557 | null,
558 | run = 0, i,
559 | overrideMimeType = "overrideMimeType",
560 | setRequestHeader = "setRequestHeader",
561 | getbinary = method === "GET" && option.binary;
562 |
563 | try {
564 | xhr.onreadystatechange = readyStateChange;
565 | xhr.open(method, url, true); // ASync
566 |
567 | before && before(xhr, option);
568 |
569 | getbinary && xhr[overrideMimeType] &&
570 | xhr[overrideMimeType]("text/plain; charset=x-user-defined");
571 | data &&
572 | xhr[setRequestHeader]("Content-Type",
573 | "application/x-www-form-urlencoded");
574 |
575 | for (i in header) {
576 | xhr[setRequestHeader](i, header[i]);
577 | }
578 |
579 | globalScope.addEventListener &&
580 | globalScope.addEventListener("beforeunload", ng, false); // 400: Bad Request
581 |
582 | xhr.send(data);
583 | watchdog = setTimeout(function() {
584 | ng(1, 408); // 408: Request Time-out
585 | }, (option.timeout || 10) * 1000);
586 | } catch (err) {
587 | ng(0, 400); // 400: Bad Request
588 | }
589 | }
590 |
591 | // inner - BinaryString To ByteArray
592 | function toByteArray(data) { // @param BinaryString: "\00\01"
593 | // @return ByteArray: [0x00, 0x01]
594 | var rv = [], bin2num = _bin2num, remain,
595 | ary = data.split(""),
596 | i = -1, iz;
597 |
598 | iz = ary.length;
599 | remain = iz % 8;
600 |
601 | while (remain--) {
602 | ++i;
603 | rv[i] = bin2num[ary[i]];
604 | }
605 | remain = iz >> 3;
606 | while (remain--) {
607 | rv.push(bin2num[ary[++i]], bin2num[ary[++i]],
608 | bin2num[ary[++i]], bin2num[ary[++i]],
609 | bin2num[ary[++i]], bin2num[ary[++i]],
610 | bin2num[ary[++i]], bin2num[ary[++i]]);
611 | }
612 | return rv;
613 | }
614 |
615 | // inner - BinaryString to ByteArray
616 | function toByteArrayIE(xhr) {
617 | var rv = [], data, remain,
618 | charCodeAt = "charCodeAt",
619 | loop, v0, v1, v2, v3, v4, v5, v6, v7,
620 | i = -1, iz;
621 |
622 | iz = vblen(xhr);
623 | data = vbstr(xhr);
624 | loop = Math.ceil(iz / 2);
625 | remain = loop % 8;
626 |
627 | while (remain--) {
628 | v0 = data[charCodeAt](++i); // 0x00,0x01 -> 0x0100
629 | rv.push(v0 & 0xff, v0 >> 8);
630 | }
631 | remain = loop >> 3;
632 | while (remain--) {
633 | v0 = data[charCodeAt](++i);
634 | v1 = data[charCodeAt](++i);
635 | v2 = data[charCodeAt](++i);
636 | v3 = data[charCodeAt](++i);
637 | v4 = data[charCodeAt](++i);
638 | v5 = data[charCodeAt](++i);
639 | v6 = data[charCodeAt](++i);
640 | v7 = data[charCodeAt](++i);
641 | rv.push(v0 & 0xff, v0 >> 8, v1 & 0xff, v1 >> 8,
642 | v2 & 0xff, v2 >> 8, v3 & 0xff, v3 >> 8,
643 | v4 & 0xff, v4 >> 8, v5 & 0xff, v5 >> 8,
644 | v6 & 0xff, v6 >> 8, v7 & 0xff, v7 >> 8);
645 | }
646 | iz % 2 && rv.pop();
647 |
648 | return rv;
649 | }
650 |
651 | // inner - base64.encode
652 | function base64encode(data) { // @param ByteArray:
653 | // @return Base64String:
654 | var rv = [],
655 | c = 0, i = -1, iz = data.length,
656 | pad = [0, 2, 1][data.length % 3],
657 | num2bin = _num2bin,
658 | num2b64 = _num2b64;
659 |
660 | if (globalScope.btoa) {
661 | while (i < iz) {
662 | rv.push(num2bin[data[++i]]);
663 | }
664 | return btoa(rv.join(""));
665 | }
666 | --iz;
667 | while (i < iz) {
668 | c = (data[++i] << 16) | (data[++i] << 8) | (data[++i]); // 24bit
669 | rv.push(num2b64[(c >> 18) & 0x3f],
670 | num2b64[(c >> 12) & 0x3f],
671 | num2b64[(c >> 6) & 0x3f],
672 | num2b64[ c & 0x3f]);
673 | }
674 | pad > 1 && (rv[rv.length - 2] = "=");
675 | pad > 0 && (rv[rv.length - 1] = "=");
676 | return rv.join("");
677 | }
678 |
679 | // --- init ---
680 | (function() {
681 | var i = 0, v;
682 |
683 | for (; i < 0x100; ++i) {
684 | v = _toString(i);
685 | _bin2num[v] = i; // "\00" -> 0x00
686 | _num2bin[i] = v; // 0 -> "\00"
687 | }
688 | // http://twitter.com/edvakf/statuses/15576483807
689 | for (i = 0x80; i < 0x100; ++i) { // [Webkit][Gecko]
690 | _bin2num[_toString(0xf700 + i)] = i; // "\f780" -> 0x80
691 | }
692 | })();
693 |
694 | _ie && document.write('
83 |
84 |
85 |
86 |
87 |
88 |
168 |