├── .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 | -------------------------------------------------------------------------------- /storage/view/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | login 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 31 | 32 | 33 |
34 |
35 | 36 | 用户帐号: 37 | 38 |
39 |
40 |
41 | 42 |
43 | 44 | 45 | -------------------------------------------------------------------------------- /test/Cases/ExampleTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(true); 25 | $this->assertTrue(is_array($this->get('/'))); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/HttpTestCase.php: -------------------------------------------------------------------------------- 1 | client = make(Client::class); 35 | } 36 | 37 | public function __call($name, $arguments) 38 | { 39 | return $this->client->{$name}(...$arguments); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test/bootstrap.php: -------------------------------------------------------------------------------- 1 | get(Hyperf\Contract\ApplicationInterface::class); 30 | --------------------------------------------------------------------------------