├── .env ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── app ├── Annotation │ ├── Mapping │ │ └── AlphaDash.php │ └── Parser │ │ └── AlphaDashParser.php ├── Application.php ├── Aspect │ └── AnnotationAspect.php ├── AutoLoader.php ├── Common │ └── TcpReceiveListener.php ├── Console │ ├── .gitkeep │ └── Command │ │ └── .gitkeep ├── Exception │ ├── ApiException.php │ └── Handler │ │ ├── ApiExceptionHandler.php │ │ ├── HttpExceptionHandler.php │ │ ├── RpcExceptionHandler.php │ │ ├── WsHandshakeExceptionHandler.php │ │ └── WsMessageExceptionHandler.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 │ │ ├── GameStart.php │ │ └── HeartAsk.php ├── Helper │ ├── .gitkeep │ └── Functions.php ├── Http │ ├── Controller │ │ └── GameController.php │ └── Middleware │ │ └── SomeMiddleware.php ├── Listener │ ├── .gitkeep │ └── BeforeStartListener.php ├── Migration │ └── .gitkeep ├── Model │ ├── Dao │ │ └── .gitkeep │ ├── Data │ │ └── .gitkeep │ ├── Entity │ │ └── .gitkeep │ └── Logic │ │ └── .gitkeep ├── Process │ └── .gitkeep ├── Rpc │ ├── Lib │ │ ├── DbproxyInterface.php │ │ └── UserInterface.php │ ├── Middleware │ │ └── ServiceMiddleware.php │ └── Service │ │ ├── DbproxyService.php │ │ ├── UserService.php │ │ ├── UserServiceV2.php │ │ └── big.data ├── Task │ ├── Listener │ │ └── FinishListener.php │ └── Task │ │ ├── GameSyncTask.php │ │ ├── SyncTask.php │ │ └── TestTask.php ├── Validator │ └── .gitkeep ├── WebSocket │ ├── GameModule.php │ └── VedioModule.php └── bean.php ├── bin ├── bootstrap.php └── swoft ├── composer.json ├── config ├── base.php └── game.php ├── images ├── ai.jpg ├── demo1.png └── demo2.png ├── public ├── .keep └── client │ ├── Const.js │ ├── Init.js │ ├── Packet.js │ ├── Req.js │ ├── Resp.js │ ├── index.html │ └── msgpack.js ├── resource └── views │ ├── game │ ├── game.php │ ├── login.php │ └── test.php │ └── vedio │ ├── camera.php │ └── show.php └── test ├── ai.php ├── tcp_client.php ├── test_redis.php └── test_rpc.php /.env: -------------------------------------------------------------------------------- 1 | APP_DEBUG = 0 2 | SWOFT_DEBUG = 0 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .git/ 3 | runtime/ 4 | vendor/ 5 | *.lock 6 | public/devtool/ 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # @description php image base on the debian 9.x 2 | # 3 | # Some Information 4 | # ------------------------------------------------------------------------------------ 5 | # @link https://hub.docker.com/_/debian/ alpine image 6 | # @link https://hub.docker.com/_/php/ php image 7 | # @link https://github.com/docker-library/php php dockerfiles 8 | # @see https://github.com/docker-library/php/tree/master/7.2/stretch/cli/Dockerfile 9 | # ------------------------------------------------------------------------------------ 10 | # @build-example docker build . -f Dockerfile -t swoft/swoft 11 | # 12 | FROM php:7.2 13 | 14 | LABEL maintainer="jxy918 <251413215@qq.com>" version="2.3" 15 | 16 | # --build-arg timezone=Asia/Shanghai 17 | ARG timezone 18 | # app env: prod pre test dev 19 | ARG app_env=prod 20 | # default use www-data user 21 | ARG work_user=www-data 22 | 23 | ENV APP_ENV=${app_env:-"prod"} \ 24 | TIMEZONE=${timezone:-"Asia/Shanghai"} \ 25 | PHPREDIS_VERSION=4.3.0 \ 26 | SWOOLE_VERSION=4.4.17 \ 27 | COMPOSER_ALLOW_SUPERUSER=1 28 | 29 | # Libs -y --no-install-recommends 30 | RUN apt-get update \ 31 | && apt-get install -y \ 32 | curl wget git zip unzip less vim openssl \ 33 | libz-dev \ 34 | libssl-dev \ 35 | libnghttp2-dev \ 36 | libpcre3-dev \ 37 | libjpeg-dev \ 38 | libpng-dev \ 39 | libfreetype6-dev \ 40 | # Install composer 41 | && curl -sS https://getcomposer.org/installer | php \ 42 | && mv composer.phar /usr/local/bin/composer \ 43 | && composer self-update --clean-backups \ 44 | # Install PHP extensions 45 | && docker-php-ext-install \ 46 | bcmath gd pdo_mysql mbstring sockets zip sysvmsg sysvsem sysvshm \ 47 | # Install redis extension 48 | && wget http://pecl.php.net/get/redis-${PHPREDIS_VERSION}.tgz -O /tmp/redis.tar.tgz \ 49 | && pecl install /tmp/redis.tar.tgz \ 50 | && rm -rf /tmp/redis.tar.tgz \ 51 | && docker-php-ext-enable redis \ 52 | # Install swoole extension 53 | && wget https://github.com/swoole/swoole-src/archive/v${SWOOLE_VERSION}.tar.gz -O swoole.tar.gz \ 54 | && mkdir -p swoole \ 55 | && tar -xf swoole.tar.gz -C swoole --strip-components=1 \ 56 | && rm swoole.tar.gz \ 57 | && ( \ 58 | cd swoole \ 59 | && phpize \ 60 | && ./configure --enable-mysqlnd --enable-sockets --enable-openssl --enable-http2 \ 61 | && make -j$(nproc) \ 62 | && make install \ 63 | ) \ 64 | && rm -r swoole \ 65 | && docker-php-ext-enable swoole \ 66 | 67 | #Install msgpack extension 68 | && wget http://pecl.php.net/get/msgpack-2.0.3.tgz -O msgpack-2.0.3.tgz \ 69 | && mkdir -p msgpack \ 70 | && tar -xf msgpack-2.0.3.tgz -C msgpack --strip-components=1 \ 71 | && ( \ 72 | cd msgpack \ 73 | && phpize \ 74 | && ./configure \ 75 | && make \ 76 | && make install \ 77 | ) \ 78 | && rm -r msgpack \ 79 | && docker-php-ext-enable msgpack \ 80 | 81 | # Clear dev deps 82 | && apt-get clean \ 83 | && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \ 84 | # Timezone 85 | && cp /usr/share/zoneinfo/${TIMEZONE} /etc/localtime \ 86 | && echo "${TIMEZONE}" > /etc/timezone \ 87 | && echo "[Date]\ndate.timezone=${TIMEZONE}" > /usr/local/etc/php/conf.d/timezone.ini 88 | 89 | # Install composer deps 90 | #ADD . /var/www/swoft 91 | #RUN cd /var/www/swoft \ 92 | # && composer install \ 93 | # && composer clearcache 94 | # 95 | WORKDIR /var/www/swoft 96 | EXPOSE 18306 18307 18308 97 | 98 | # ENTRYPOINT ["php", "/var/www/swoft/bin/swoft", "http:start"] 99 | # CMD ["php", "/var/www/swoft/bin/swoft", "http:start"] 100 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # swoft-ddz(斗地主) 2 | 3 | * 基于swoft2.0框架开发游戏斗地主 4 | 5 | * 使用swoft2.0框架基本实现斗地主游戏逻辑, 可以简单玩斗地主 6 | * 客户端采用纯原生js编写, 实现简单的测试客户端, 没有任何动画效果 7 | * 实现斗地主AI机器人简单逻辑,现在只能是过牌,比较弱智,可以简单陪打,机器人逻辑会慢慢完善 8 | 9 | * 想关注更多游戏开发可以关注hyperf-ddz斗地主:**[hyperf-ddz](https://github.com/jxy918/hyperf-ddz)** 10 | 11 | ### 一,概述 12 | 13 | * 使用swoft2.0开发实现斗地主游戏服务器逻辑, 并采用原生js实现简单的客户端打牌逻辑,基本可以做到简单玩斗地主游戏了, 并实现机器人AI逻辑编写, 全方面实现斗地主游戏逻辑。 14 | * 整个斗地主逻辑实现是在redis里存储的, 并没有接入数据库, 数据库接入, 可以在游戏登录或者斗地主打牌完成, 接入。 15 | 16 | ### 二,示例图 17 | 1, 登录, 简单实现登录, 请随便输入英文或这数字账号, 现在直接是采用账号当uid使用存入redis的,如果接入数据库, 请自行通过账号替换成uid,登录如下图: 18 | ![游戏demo1](images/demo1.png) 19 | 20 | 2, 打牌逻辑, 根据按钮来操作打牌逻辑, 消息框里会提示打牌逻辑过程,打牌逻辑如下图: 21 | ![游戏demo2](images/demo2.png) 22 | 23 | 24 | ### 三,特性 25 | 26 | * 实现前后端二进制封包解包,采用的是msgpack扩展,msgpack对数据进行了压缩,并实现粘包处理 27 | * 数据采用固定包头,包头4个字节存包体长度,包体前2个字节分别为cmd(主命令字),scmd(子命令字),后面为包体内容 28 | * 采用策略模式解耦游戏中的每个协议逻辑 29 | * 实现定义游戏开发cmd(主命令字)和scmd(子命令字)定义,cmd和scmd主要用来路由到游戏协议逻辑处理 30 | * 代码里有个DdzPoker类是一个款斗地主算法类. 31 | * 多地主逻辑是单独目录是实现的, 可以方便迁移框架。 32 | 33 | ### 四,环境依赖 34 | 35 | >依赖swoft环境,请安装php扩展msgpack 36 | 37 | * php vesion > 7.0 38 | * swoole version > 4.4.1 39 | * msgpack 40 | * swoft vesion > 2.0 41 | 42 | 43 | ### 五,开始使用 44 | * 1,安装 45 | 46 | ``` 47 | composer install 48 | 49 | ``` 50 | 51 | * 2,目录说明(swoft目录不具体说明): 52 | 53 | ``` 54 | ./app/Http/Controller/GameController.php 游戏http控制器逻辑 55 | ./app/Game 是这个整体游戏服务器逻辑 56 | ./app/Game/Conf 逻辑配置目录, 比如:命令字, 子名字, 路由转发 57 | ./app/Game/Core 游戏路由转发,算法,解包核心类 58 | ./app/Game/Logic 游戏路由转发逻辑协议包处理目录 59 | ./public/client 客户端view的资源文件 60 | ./resources/views/game 斗地主客户端 61 | ./app/Task/Task/GameSyncTask.php 用户进入房间异步匹配处理逻辑 62 | ./app/Common/TcpReceiveListener.php 重新覆盖tcp服务器路由到游戏处理逻辑, 此文件开tcp协议, 让斗地主逻辑支持tcp协议 63 | 64 | ``` 65 | 66 | * 3,进入根目录目录,启动服务器(swoft启动websocket启动法) : 67 | 68 | 游戏服务器命令操作: 69 | 70 | ``` 71 | // 启动服务,根据 72 | php bin/swoft ws:start 73 | 74 | // 守护进程启动,覆盖 75 | php bin/swoft ws:start -d 76 | 77 | // 重启 78 | php bin/swoft ws:restart 79 | 80 | // 重新加载 81 | php bin/swoft ws:reload 82 | 83 | // 关闭服务 84 | php bin/swoft ws:stop 85 | 86 | ``` 87 | 88 | 机器人AI执行命令操作, 进入根目录下test目录下 89 | 90 | ``` 91 | //xxx表示用户账号 92 | php ai.php xxx 93 | 94 | ``` 95 | > xxx 表示用户账号, 账号最好用英文加数字, 账号唯一就可以 96 | 97 | > ai功能需要swoole开启短名称功能, 去php.ini添加:swoole.use_shortname=on 98 | 99 | ![AI示例图](images/ai.jpg) 100 | 101 | * 4,访问url: 102 | 103 | ``` 104 | //斗地主客户端入口 105 | http://[ip]:[port]/game 106 | 107 | ``` 108 | 109 | 110 | ### 六,联系方式 111 | 112 | * qq:251413215,加qq请输入验证消息:swoft-ddz 113 | qq群:100754069 114 | 115 | ### 七,备注 116 | 117 | * 可以使用根目录增加docker运行环境(Dockerfile), 可以直接执行下面的命令,创建镜像php_swoole, 环境增加php-protobuf,php-msgpack支持。 118 | 119 | ``` 120 | docker build -t php_swoole . 121 | 122 | ``` 123 | * 注意如果程序不能自动加载,请去除环境中opcache扩展。 124 | * 服务器增加支持TCP服务器,服务器启动就会监控TCP游戏服务器, 可以通过/test/tcp_client.php测试。 125 | 126 | ``` 127 | php ./test/tcp_client 128 | 129 | ``` 130 | 131 | * 请使用根目录下的Dockerfile可以直接跑, 如果使用swoft的Dockerfile需要自行安装msgpack扩展, 132 | 133 | ### 八,增加录像直播功能, 此功能只是测试, 效果比较差 134 | 135 | * 1, 视频录制url,如果电脑没有摄像头, 请用手机测试录制视频: 136 | 137 | ```` 138 | http://[ip]:[port]/camera 139 | ```` 140 | 141 | * 2,浏览器上播放视频url如下: 142 | 143 | ``` 144 | http://[ip]:[port]/show 145 | ``` 146 | 147 | 148 | 149 | -------------------------------------------------------------------------------- /app/Annotation/Mapping/AlphaDash.php: -------------------------------------------------------------------------------- 1 | message = $values['value']; 39 | } 40 | if (isset($values['message'])) { 41 | $this->message = $values['message']; 42 | } 43 | if (isset($values['name'])) { 44 | $this->name = $values['name']; 45 | } 46 | } 47 | 48 | /** 49 | * @return string 50 | */ 51 | public function getMessage(): string 52 | { 53 | return $this->message; 54 | } 55 | 56 | /** 57 | * @return string 58 | */ 59 | public function getName(): string 60 | { 61 | return $this->name; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/Annotation/Parser/AlphaDashParser.php: -------------------------------------------------------------------------------- 1 | className, $this->propertyName, $annotationObject); 33 | return []; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/Application.php: -------------------------------------------------------------------------------- 1 | __DIR__, 23 | ]; 24 | } 25 | 26 | /** 27 | * @return array 28 | */ 29 | public function metadata(): array 30 | { 31 | return []; 32 | } 33 | } -------------------------------------------------------------------------------- /app/Common/TcpReceiveListener.php: -------------------------------------------------------------------------------- 1 | log("Receive: conn#{$fd} received client request, begin init context", [], 'debug'); 50 | $sid = (string)$fd; 51 | $ctx = TcpReceiveContext::new($fd, $request, $response); 52 | 53 | // Storage context 54 | Context::set($ctx); 55 | Session::bindCo($sid); 56 | 57 | try { 58 | Log::show(" Receive: client #{$fd} send success Mete: \n{"); 59 | if(!empty($data)) { 60 | $data = Packet::packDecode($data); 61 | $back = ''; 62 | if (isset($data['code']) && $data['code'] == 0 && isset($data['msg']) && $data['msg'] == 'OK') { 63 | Log::show('Recv <<< cmd=' . $data['cmd'] . ' scmd=' . $data['scmd'] . ' len=' . $data['len'] . ' data=' . json_encode($data['data'])); 64 | //转发请求,代理模式处理,websocket路由到相关逻辑 65 | $data['serv'] = $server; 66 | $back = $this->dispatch($data); 67 | Log::show('Tcp Strategy <<< data=' . $back); 68 | } else { 69 | Log::show($data['msg']); 70 | } 71 | if (!empty($back)) { 72 | $server->send($fd, $back); 73 | } 74 | } 75 | Log::split('}'); 76 | } catch (Throwable $e) { 77 | server()->log("Receive: conn#{$fd} error: " . $e->getMessage(), [], 'error'); 78 | Swoft::trigger(TcpServerEvent::RECEIVE_ERROR, $e, $fd); 79 | 80 | /** @var TcpErrorDispatcher $errDispatcher */ 81 | $errDispatcher = Swoft::getSingleton(TcpErrorDispatcher::class); 82 | 83 | // Dispatching error handle 84 | $response = $errDispatcher->receiveError($e, $response); 85 | $response->send($server); 86 | } finally { 87 | // Defer 88 | Swoft::trigger(SwoftEvent::COROUTINE_DEFER); 89 | 90 | // Destroy 91 | Swoft::trigger(SwoftEvent::COROUTINE_COMPLETE); 92 | 93 | // Unbind cid => sid(fd) 94 | Session::unbindCo(); 95 | } 96 | } 97 | 98 | /** 99 | * 根据路由策略处理逻辑,并返回数据 100 | * @param $data 101 | * @return string 102 | */ 103 | public function dispatch($data) { 104 | $obj = new Dispatch($data); 105 | $back = "

404 Not Found


swoft
\n"; 106 | if(!empty($obj->getStrategy())) { 107 | $back = $obj->exec(); 108 | } 109 | return $back; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /app/Console/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jxy918/swoft-ddz/07c42b2437691fa502e6891384f9bd4ce7d79801/app/Console/.gitkeep -------------------------------------------------------------------------------- /app/Console/Command/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jxy918/swoft-ddz/07c42b2437691fa502e6891384f9bd4ce7d79801/app/Console/Command/.gitkeep -------------------------------------------------------------------------------- /app/Exception/ApiException.php: -------------------------------------------------------------------------------- 1 | $except->getCode(), 32 | 'error' => sprintf('(%s) %s', get_class($except), $except->getMessage()), 33 | 'file' => sprintf('At %s line %d', $except->getFile(), $except->getLine()), 34 | 'trace' => $except->getTraceAsString(), 35 | ]; 36 | return $response->withData($data); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/Exception/Handler/HttpExceptionHandler.php: -------------------------------------------------------------------------------- 1 | getMessage()); 35 | 36 | // Debug is false 37 | if (!APP_DEBUG) { 38 | return $response->withStatus(500)->withContent( 39 | sprintf(' %s At %s line %d', $e->getMessage(), $e->getFile(), $e->getLine()) 40 | ); 41 | } 42 | 43 | $data = [ 44 | 'code' => $e->getCode(), 45 | 'error' => sprintf('(%s) %s', get_class($e), $e->getMessage()), 46 | 'file' => sprintf('At %s line %d', $e->getFile(), $e->getLine()), 47 | 'trace' => $e->getTraceAsString(), 48 | ]; 49 | 50 | // Debug is true 51 | return $response->withData($data); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/Exception/Handler/RpcExceptionHandler.php: -------------------------------------------------------------------------------- 1 | getMessage(), $e->getFile(), $e->getLine()); 39 | $error = Error::new($e->getCode(), $message, null); 40 | } else { 41 | $error = Error::new($e->getCode(), $e->getMessage(), null); 42 | } 43 | 44 | Debug::log('Rpc server error(%s)', $e->getMessage()); 45 | 46 | $response->setError($error); 47 | 48 | // Debug is true 49 | return $response; 50 | } 51 | } -------------------------------------------------------------------------------- /app/Exception/Handler/WsHandshakeExceptionHandler.php: -------------------------------------------------------------------------------- 1 | withStatus(500)->withContent(sprintf( 35 | '%s At %s line %d', $e->getMessage(), $e->getFile(), $e->getLine() 36 | )); 37 | } 38 | 39 | $data = [ 40 | 'code' => $e->getCode(), 41 | 'error' => sprintf('(%s) %s', get_class($e), $e->getMessage()), 42 | 'file' => sprintf('At %s line %d', $e->getFile(), $e->getLine()), 43 | 'trace' => $e->getTraceAsString(), 44 | ]; 45 | 46 | // Debug is true 47 | return $response->withData($data); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/Exception/Handler/WsMessageExceptionHandler.php: -------------------------------------------------------------------------------- 1 | getMessage(), $e->getFile(), $e->getLine()); 34 | 35 | Log::error('Ws server error(%s)', $message); 36 | 37 | // Debug is false 38 | if (!APP_DEBUG) { 39 | server()->push($frame->fd, $e->getMessage()); 40 | return; 41 | } 42 | 43 | server()->push($frame->fd, $message); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/Game/Conf/MainCmd.php: -------------------------------------------------------------------------------- 1 | array( 20 | SubCmd::HEART_ASK_REQ =>'HeartAsk', 21 | ), 22 | //游戏请求 23 | MainCmd::CMD_GAME => array( 24 | SubCmd::SUB_GAME_START_REQ =>'GameStart', 25 | SubCmd::SUB_GAME_CALL_REQ =>'GameCall', 26 | SubCmd::SUB_GAME_DOUBLE_REQ =>'GameDouble', 27 | SubCmd::SUB_GAME_OUT_CARD_REQ =>'GameOutCard', 28 | SubCmd::CHAT_MSG_REQ =>'ChatMsg', 29 | ), 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /app/Game/Conf/SubCmd.php: -------------------------------------------------------------------------------- 1 | CGameStart 22 | const SUB_GAME_START_RESP = 2; //游戏场景---> CGameScence 23 | const SUB_USER_INFO_RESP = 3; //用户信息 ------> CUserInfo 24 | const SUB_GAME_SEND_CARD_RESP = 4; //发牌 ------> CGameSendCard 25 | const SUB_GAME_CALL_TIPS_RESP = 5; //叫地主提示(广播) --> CGameCall 26 | const SUB_GAME_CALL_REQ = 6; //叫地主请求 --> CGameCallReq 27 | const SUB_GAME_CALL_RESP = 7; //叫地主请求返回--CGameCallResp 28 | const SUB_GAME_DOUBLE_TIPS_RESP = 8; //加倍提示(广播) --> CGameDouble 29 | const SUB_GAME_DOUBLE_REQ = 9; //加倍请求--> CGameDoubleReq 30 | const SUB_GAME_DOUBLE_RESP = 10; //加倍请求返回----> CGameDoubleResp 31 | const SUB_GAME_CATCH_BASECARD_RESP = 11; //摸底牌 ---> CGameCatchBaseCard 32 | const SUB_GAME_OUT_CARD = 12; //出牌提示 --> CGameOutCard 33 | const SUB_GAME_OUT_CARD_REQ = 13; //出牌请求 --> CGameOutCardReq 34 | const SUB_GAME_OUT_CARD_RESP = 14; //出牌返回 --> CGameOutCardResp 35 | const CHAT_MSG_REQ = 213; //聊天消息请求,客户端使用 36 | const CHAT_MSG_RESP = 214; //聊天消息响应,服务端使用 37 | } -------------------------------------------------------------------------------- /app/Game/Core/AStrategy.php: -------------------------------------------------------------------------------- 1 | _params = $params; 36 | $this->obj_ddz = new DdzPoker(); 37 | } 38 | 39 | /** 40 | * 执行方法,每条游戏协议,实现这个方法就行 41 | */ 42 | abstract public function exec(); 43 | 44 | /** 45 | * 服务器广播消息, 此方法是给所有的连接客户端, 广播消息 46 | * @param $serv 47 | * @param $data 48 | */ 49 | protected function Broadcast($serv, $data) 50 | { 51 | foreach($serv->connections as $fd) { 52 | $serv->push($fd, $data, WEBSOCKET_OPCODE_BINARY); 53 | } 54 | } 55 | 56 | /** 57 | * 当connetions属性无效时可以使用此方法,服务器广播消息, 此方法是给所有的连接客户端, 广播消息,通过方法getClientList广播 58 | * @param $serv 59 | * @param $data 60 | */ 61 | protected function BroadCast2($serv, $data) 62 | { 63 | $start_fd = 0; 64 | while(true) { 65 | $conn_list = $serv->getClientList($start_fd, 10); 66 | if ($conn_list===false or count($conn_list) === 0) { 67 | Log::show("BroadCast finish"); 68 | break; 69 | } 70 | $start_fd = end($conn_list); 71 | foreach($conn_list as $fd) { 72 | //获取客户端信息 73 | $client_info = $serv->getClientInfo($fd); 74 | if(isset($client_info['websocket_status']) && $client_info['websocket_status'] == 3) { 75 | $serv->push($fd, $data, WEBSOCKET_OPCODE_BINARY); 76 | } 77 | } 78 | } 79 | } 80 | 81 | /** 82 | * 对多用发送信息 83 | * @param $serv 84 | * @param $users 85 | * @param $data 86 | */ 87 | protected function pushToUsers($serv, $users, $data) 88 | { 89 | foreach($users as $fd) { 90 | //获取客户端信息 91 | $client_info = $serv->getClientInfo($fd); 92 | $client[$fd] = $client_info; 93 | if(isset($client_info['websocket_status']) && $client_info['websocket_status'] == 3) { 94 | $serv->push($fd, $data, WEBSOCKET_OPCODE_BINARY); 95 | } 96 | } 97 | } 98 | 99 | /** 100 | * 获取房间信息 101 | * @param $account 102 | * @return array 103 | */ 104 | protected function getRoomData($account) 105 | { 106 | $user_room_data = array(); 107 | //获取用户房间号 108 | $room_no = $this->getRoomNo($account); 109 | //房间信息 110 | $game_key = $this->getGameConf('user_room_data'); 111 | if($game_key) { 112 | $user_room_key = sprintf($game_key, $room_no); 113 | $user_room_data = Redis::hGetAll($user_room_key); 114 | } 115 | return $user_room_data; 116 | } 117 | 118 | /** 119 | * 获取房间信息通过key 120 | * @param $account 121 | * @param $key 122 | * @return mixed 123 | */ 124 | protected function getRoomDataByKey($account, $key) 125 | { 126 | $data = array(); 127 | $no = $this->getRoomNo($account); 128 | $game_key = $this->getGameConf('user_room_data'); 129 | if($no && $game_key) { 130 | $user_room_key = sprintf($game_key, $no); 131 | $user_room_data = Redis::hGet($user_room_key, $key); 132 | $data = json_decode($user_room_data, true); 133 | if (is_null($data)) { 134 | $data = $user_room_data; 135 | } 136 | } 137 | return $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 | * 获取房间信息通过key 156 | * @param $account 157 | * @return mixed 158 | */ 159 | protected function getRoomUserInfoDataByKey($account) 160 | { 161 | $user_data = array(); 162 | $no = $this->getRoomNo($account); 163 | $game_key = $this->getGameConf('user_room_data'); 164 | if($no && $game_key) { 165 | //房间信息 166 | $user_room_key = sprintf($game_key, $no); 167 | $user_data = Redis::hGet($user_room_key, $account); 168 | $user_data = json_decode($user_data, true); 169 | } 170 | return $user_data; 171 | } 172 | 173 | /** 174 | * 设置房间用户玩牌信息 175 | * @param $account 176 | * @param $key 177 | * @param $value 178 | */ 179 | protected function setRoomData($account, $key, $value) 180 | { 181 | $no = $this->getRoomNo($account); 182 | $game_key = $this->getGameConf('user_room_data'); 183 | if($no && $game_key) { 184 | $user_room_key = sprintf($game_key, $no); 185 | Redis::hSet($user_room_key, $key, $value); 186 | } 187 | } 188 | 189 | /** 190 | * 批量设置房间信息 191 | * @param $account 192 | * @param $params 193 | */ 194 | protected function muitSetRoomData($account, $params) 195 | { 196 | $no = $this->getRoomNo($account); 197 | $game_key = $this->getGameConf('user_room_data'); 198 | if($no && $game_key) { 199 | $user_room_key = sprintf($game_key, $no); 200 | Redis::hMSet($user_room_key, $params); 201 | } 202 | } 203 | 204 | /** 205 | * 设置房间信息 206 | * @param $room_user_data 207 | * @param $account 208 | * @param $key 209 | * @param $value 210 | */ 211 | protected function setRoomUserInfoDataByKey($room_user_data, $account, $key, $value) 212 | { 213 | $no = $this->getRoomNo($account); 214 | $game_key = $this->getGameConf('user_room_data'); 215 | if($no && $game_key) { 216 | $room_user_data[$key] = $value; 217 | $user_room_key = sprintf($game_key, $no); 218 | Redis::hSet($user_room_key, $account, json_encode($room_user_data)); 219 | } 220 | } 221 | 222 | /** 223 | * 通过accounts获取fds 224 | * @param $account 225 | * @return array 226 | */ 227 | protected function getRoomFds($account) 228 | { 229 | $accs = $this->getRoomDataByKey($account, 'uinfo'); 230 | $game_key = $this->getGameConf('user_bind_key'); 231 | $binds = $fds = array(); 232 | if(!empty($accs) && $game_key) { 233 | foreach($accs as $v) { 234 | $binds[] = sprintf($game_key, $v); 235 | } 236 | $fds = Redis::mget($binds); 237 | } 238 | return $fds; 239 | } 240 | 241 | /** 242 | * 批量清除用户房间号 243 | * @param $users 244 | */ 245 | protected function clearRoomNo($users) { 246 | $game_key = $this->getGameConf('user_room'); 247 | if(is_array($users)) { 248 | foreach($users as $v) { 249 | $key = sprintf($game_key, $v); 250 | Redis::del($key); 251 | 252 | } 253 | } 254 | } 255 | 256 | /** 257 | * 把php数组存入redis的hash表中 258 | * @param $arr 259 | * @param $hash_key 260 | */ 261 | protected function arrToHashInRedis($arr, $hash_key) { 262 | foreach($arr as $key=>$val) { 263 | Redis::hSet($hash_key,$key, json_encode($val)); 264 | } 265 | } 266 | 267 | /** 268 | * 返回游戏配置 269 | * @param string $key 270 | * @return string 271 | */ 272 | protected function getGameConf($key = '') { 273 | $conf = config('game'); 274 | if(isset($conf[$key])) { 275 | return $conf[$key]; 276 | } else { 277 | return ''; 278 | } 279 | } 280 | 281 | /** 282 | * 设置游戏房间玩牌步骤信息, 方便后面录像回放 283 | * @param $account 284 | * @param $key 285 | * @param $value 286 | */ 287 | protected function setRoomPlayCardStep($account, $key, $value) 288 | { 289 | $no = $this->getRoomNo($account); 290 | $game_key = $this->getGameConf('user_room_play'); 291 | if($no && $game_key) { 292 | $play_key = sprintf($game_key, $no);; 293 | Redis::hSet($play_key, $key, $value); 294 | } 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /app/Game/Core/Dispatch.php: -------------------------------------------------------------------------------- 1 | _params = $params; 31 | $this->Strategy(); 32 | } 33 | 34 | /** 35 | * 逻辑处理策略路由转发, 游戏逻辑策略转发, 根据主命令字和子命令字来转发 36 | */ 37 | public function Strategy() 38 | { 39 | //获取路由策略 40 | $route = Route::$cmd_map; 41 | //获取策略类名 42 | $classname = isset($route[$this->_params['cmd']][$this->_params['scmd']]) ? $route[$this->_params['cmd']][$this->_params['scmd']] : ''; 43 | //转发到对应目录处理逻辑 44 | $classname = 'App\Game\Logic\\'.$classname; 45 | if (class_exists($classname)) { 46 | $this->_strategy = new $classname($this->_params); 47 | Log::show("Class: $classname"); 48 | } else { 49 | Log::show("Websockt Error: class is not support,cmd is {$this->_params['cmd']},scmd is {$this->_params['scmd']}"); 50 | } 51 | } 52 | 53 | /** 54 | * 获取策略 55 | */ 56 | public function getStrategy() 57 | { 58 | return $this->_strategy; 59 | } 60 | 61 | /** 62 | * 执行策略 63 | */ 64 | public function exec() 65 | { 66 | return $this->_strategy->exec(); 67 | } 68 | } -------------------------------------------------------------------------------- /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', 17 | 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', 18 | 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', 19 | 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', 20 | 79=>'JOKER' 21 | ); 22 | 23 | /** 24 | * 赖子的key值,和牌的key值对应 25 | * @var int 26 | */ 27 | public static $laizi_value = 79; 28 | 29 | /** 30 | * 花色 31 | */ 32 | public static $card_color = array( 33 | 0=>'方块', 34 | 1=>'黑桃', 35 | 2=>'红桃', 36 | 3=>'梅花' 37 | ); 38 | 39 | /** 40 | * 牌型 41 | * @var array 42 | */ 43 | public static $card_type = array( 44 | 0=>'非赢牌', 45 | 1=>'对K或者以上', 46 | 2=>'两对', 47 | 3=>'三条', 48 | 4=>'顺子', 49 | 5=>'同花', 50 | 6=>'葫芦', 51 | 7=>'四条', 52 | 8=>'同花顺', 53 | 9=>'五条', 54 | 10=>'带赖子皇家同花顺', 55 | 11=>'皇家同花顺' 56 | ); 57 | 58 | /** 59 | * 牌型赔付的倍率 60 | * @var array 61 | */ 62 | public static $card_rate = array( 63 | 0=>0, 64 | 1=>1, 65 | 2=>1, 66 | 3=>2, 67 | 4=>3, 68 | 5=>5, 69 | 6=>7, 70 | 7=>17, 71 | 8=>50, 72 | 9=>100, 73 | 10=>200, 74 | 11=>250 75 | ); 76 | 77 | /** 78 | * 是否翻倍的概率配置:1表示不翻倍回收奖励,2.表示再来一次 3,表示奖励翻倍 79 | */ 80 | public static $is_double_rate = array( 81 | 1=>5000, 82 | 2=>1000, 83 | 3=>4000 84 | ); 85 | 86 | /** 87 | * 是否翻倍提示语 88 | */ 89 | public static $is_double_msg = array( 90 | 1=>'不翻倍回收奖励', 91 | 2=>'再来一次,不回收奖励', 92 | 3=>'奖励翻倍' 93 | ); 94 | 95 | /** 96 | * 是否有赖子牌, 如果有赖子牌,这个值就是true, 默认false 97 | */ 98 | public static $is_laizi = false; 99 | 100 | /** 101 | * 是否为顺子,是true,否false 102 | */ 103 | public static $is_shunzi = false; 104 | 105 | /** 106 | * 是否为最大顺子,是true,否false 107 | */ 108 | public static $is_big_shunzi = false; 109 | 110 | /** 111 | * 是否为同花,是true,否false 112 | */ 113 | public static $is_tonghua = false; 114 | 115 | /** 116 | * 随机获取5张牌,如果参数指定n张牌, 就补齐5-n张牌 117 | */ 118 | public static function getFiveCard($arr = array()) 119 | { 120 | $card = self::$card_value_list; 121 | $num = 5 - count($arr); 122 | if($num == 0) { 123 | $card_key = $arr; 124 | } else { 125 | //去除上面的牌, 防止重复出现 126 | foreach($arr as $v) { 127 | unset($card[$v]); 128 | } 129 | $card_key = array_rand($card, $num); 130 | if(!is_array($card_key)) { 131 | $card_key = array($card_key); 132 | } 133 | $card_key = array_merge($card_key,$arr); 134 | } 135 | return $card_key; 136 | } 137 | /** 138 | * 随机获取1张牌,不包括王 139 | */ 140 | public static function getOneCard() 141 | { 142 | $card = self::$card_value_list; 143 | unset($card[79]); 144 | $card_key = array_rand($card, 1); 145 | if(!is_array($card_key)) { 146 | $card_key = array($card_key); 147 | } 148 | return $card_key; 149 | } 150 | 151 | /** 152 | * 去除赖子,并且排序 153 | */ 154 | private static function exceptLaizi($arr) 155 | { 156 | $key = array_search(self::$laizi_value, $arr); //键值有可能0 157 | if($key !== false) { 158 | unset($arr[$key]); 159 | self::$is_laizi = true; 160 | } else { 161 | self::$is_laizi = false; 162 | } 163 | sort($arr); 164 | return $arr; 165 | } 166 | 167 | /** 168 | * 获取牌内容,根据牌的key,获取牌的内容 169 | */ 170 | private static function getCard($arr) 171 | { 172 | $card = array(); 173 | foreach($arr as $v) { 174 | $card[$v]= self::$card_value_list[$v]; 175 | } 176 | return $card; 177 | } 178 | 179 | /** 180 | * 获取牌内容,并显示花色, 方便直观查看 181 | */ 182 | public static function showCard($arr) 183 | { 184 | $show = array(); 185 | $card = self::getCard($arr); 186 | foreach($card as $k=>$v) { 187 | if($k != self::$laizi_value) { 188 | $key = floor($k / 16); 189 | $show[] = self::$card_color[$key].'_'.$v; 190 | } else { 191 | $show[] = $v; 192 | } 193 | 194 | } 195 | return implode(',', $show); 196 | } 197 | 198 | /** 199 | * 不带赖子皇家同花顺 200 | */ 201 | public static function isBigTongHuaShun() 202 | { 203 | return (self::$is_tonghua && self::$is_shunzi && self::$is_big_shunzi && !self::$is_laizi) ? true : false; 204 | } 205 | 206 | /** 207 | * 带来赖子皇家同花顺 208 | */ 209 | public static function isBigTongHuaShunByLaizi() 210 | { 211 | return (self::$is_tonghua && self::$is_shunzi && self::$is_big_shunzi && self::$is_laizi) ? true : false; 212 | } 213 | 214 | /** 215 | * 是否为同花顺 216 | */ 217 | public static function isTongHuaShun() 218 | { 219 | return (self::$is_tonghua && self::$is_shunzi) ? true : false; 220 | } 221 | 222 | /** 223 | * 是否为同花牌,判断同花的算法 224 | */ 225 | public static function isTongHua($arr) 226 | { 227 | $sub = array(); 228 | foreach($arr as $v) { 229 | $sub[] = floor($v / 16); 230 | } 231 | $u = array_unique($sub); 232 | if(count($u) == 1) { 233 | self::$is_tonghua = true; 234 | } else { 235 | self::$is_tonghua = false; 236 | } 237 | return self::$is_tonghua; 238 | } 239 | 240 | /** 241 | * 是否为顺子牌,判断顺子的算法 242 | */ 243 | public static function isShunZi($arr) 244 | { 245 | $flag = 0; 246 | $card = self::getCard($arr); 247 | asort($card); 248 | $min = key($card) % 16; 249 | if($min >= 2 && $min <= 10) { 250 | //最小或者最大顺子,需要特殊处理 251 | /* if(($min == 2 || $min == 10) && array_search('A', $card) !== false) { 252 | $flag++; 253 | } */ 254 | if( array_search('A', $card) !== false) { 255 | if($min == 2 ){ 256 | $min = 1; 257 | }elseif ( $min == 10){ 258 | $flag++; 259 | } 260 | } 261 | $cnt = count($arr); 262 | for($i = 1; $i < 5; $i++) { 263 | $next = $min + $i; 264 | if(in_array($next, $arr) || in_array(($next + 16), $arr) || in_array(($next + 32), $arr) || in_array(($next + 48), $arr)) { 265 | $flag++; 266 | } 267 | } 268 | } 269 | if($flag == $cnt - 1) { 270 | self::$is_shunzi = true; 271 | } else { 272 | self::$is_shunzi = false; 273 | } 274 | //是否为最大顺子,是true,否false 275 | if($min == 10) { 276 | self::$is_big_shunzi = true; 277 | } else { 278 | self::$is_big_shunzi = false; 279 | } 280 | return self::$is_shunzi; 281 | } 282 | 283 | /** 284 | * 取模值,算对子,两对,三张,四条,5条的算法 285 | */ 286 | public static function _getModValue($arr) 287 | { 288 | $flag = $type = 0; 289 | $mod = array(); 290 | foreach($arr as $k=>$v) { 291 | $mod[] = $v % 16; 292 | } 293 | $v = array_count_values($mod); 294 | $cnt = count($v); 295 | if(self::$is_laizi) { 296 | if(in_array(1, $v) && $cnt == 4) { 297 | //对子 298 | $card = self::getCard($arr); 299 | if(array_search('A', $card) !== false || array_search('K', $card) !== false){ 300 | $type = 1; //对K或更大 301 | } 302 | } elseif(in_array(2, $v) && $cnt == 3) { 303 | $type = 3; //三张 304 | } elseif(in_array(2, $v) && $cnt == 2) { 305 | $type = 4; //葫芦 306 | } elseif(in_array(3, $v)) { 307 | $type = 5; //四条 308 | } elseif(in_array(4, $v)) { 309 | $type = 6; //五条 310 | } 311 | } else { 312 | if(in_array(2, $v) && $cnt == 4) { 313 | //对子 314 | $card = self::getCard($arr); 315 | $card_key = array_count_values($card); 316 | arsort($card_key); 317 | $kw = key($card_key); 318 | if($kw == 'A' || $kw == 'K'){ 319 | $type = 1; //对K或更大 320 | } 321 | } elseif(in_array(2, $v) && $cnt == 3) { 322 | $type = 2; //两对 323 | } elseif(in_array(3, $v) && $cnt == 3) { 324 | $type = 3; //三张 325 | } elseif(in_array(3, $v) && $cnt == 2) { 326 | $type = 4; //葫芦 327 | } elseif(in_array(4, $v)) { 328 | $type = 5; //四条 329 | } 330 | } 331 | return $type; 332 | } 333 | 334 | /** 335 | * 五张 336 | */ 337 | public static function isWuZhang($type) 338 | { 339 | return $type == 6 ? true : false; 340 | } 341 | 342 | /** 343 | * 四张 344 | */ 345 | public static function isSiZhang($type) 346 | { 347 | return $type == 5 ? true : false; 348 | } 349 | 350 | /** 351 | * 葫芦 352 | */ 353 | public static function isHulu($type) 354 | { 355 | return $type == 4 ? true : false; 356 | } 357 | 358 | /** 359 | * 三张 360 | */ 361 | public static function isSanZhang($type) 362 | { 363 | return $type == 3 ? true : false; 364 | } 365 | 366 | /** 367 | * 两对 368 | */ 369 | public static function isLiangDui($type) 370 | { 371 | return $type == 2 ? true : false; 372 | } 373 | 374 | /** 375 | * 大于对K或更大 376 | */ 377 | public static function isDaYuQDui($type) 378 | { 379 | return $type == 1 ? true : false; 380 | } 381 | 382 | /** 383 | * 检查牌型,判断用户所翻的牌为那种牌型 384 | */ 385 | public static function checkCardType($arr) 386 | { 387 | //去除赖子牌 388 | $arr_card = self::exceptLaizi($arr); 389 | $type = self::_getModValue($arr_card); 390 | if(self::isWuZhang($type)) { 391 | return 9; //五条 392 | } elseif(self::isSiZhang($type)) { 393 | return 7; //四条 394 | } elseif(self::isHulu($type)) { 395 | return 6; //葫芦,三张两对 396 | } elseif(self::isSanZhang($type)) { 397 | return 3; //三张 398 | } elseif(self::isLiangDui($type)) { 399 | return 2; //两对 400 | } else { 401 | $back = 0; 402 | if(self::isDaYuQDui($type)) { 403 | $back = 1; //对K或者大于 404 | } 405 | if(self::isShunZi($arr_card)) { 406 | $back = 4; //是否为顺子 407 | } 408 | if(self::isTongHua($arr_card)) { 409 | $back = 5; //是否为同花 410 | } 411 | if(self::isTongHuaShun()) { 412 | $back = 8; //是否为同花顺 413 | } 414 | if(self::isBigTongHuaShunByLaizi()) { 415 | $back = 10; //带赖子皇家同花顺 416 | } 417 | if(self::isBigTongHuaShun()) { 418 | $back = 11; //皇家同花顺 419 | } 420 | return $back; 421 | } 422 | } 423 | 424 | /** 425 | * 找出牌型里那些牌需要高亮显示 426 | */ 427 | public static function highLight($arr, $type) 428 | { 429 | $card_key = array(); 430 | $card = self::getCard($arr); 431 | $val = array_count_values($card); 432 | if($type > 3) { 433 | $card_key = $arr; 434 | } elseif($type == 3) { 435 | //三条 436 | arsort($val); 437 | $kw = key($val); 438 | $card_key = array(); 439 | foreach($card as $k=>$v) { 440 | if($v == $kw || $k == self::$laizi_value) { 441 | $card_key[] = $k; 442 | } 443 | } 444 | } elseif($type == 2) { 445 | //两对 446 | $kw = $card_key = array(); 447 | foreach($val as $k=>$v) { 448 | if($v == 2) { 449 | $kw[] = $k; 450 | } 451 | } 452 | foreach($card as $k=>$v) { 453 | if(in_array($v, $kw)) { 454 | $card_key[] = $k; 455 | } 456 | } 457 | } elseif($type == 1) { 458 | //对A后者对K 459 | foreach($card as $k=>$v) { 460 | if(in_array($v, array('A','K')) || $k == self::$laizi_value) { 461 | $card_val[$k] = $v; 462 | } 463 | } 464 | $t_val = array_count_values($card_val); 465 | arsort($t_val); 466 | $kw = key($t_val); 467 | if(!self::$is_laizi) { 468 | if(count($t_val) > 1) { 469 | foreach($card_val as $k=>$v) { 470 | if($kw != $v) { 471 | unset($card_val[$k]); 472 | } 473 | } 474 | } 475 | } else { 476 | //去除k 477 | if(count($t_val) > 2) { 478 | foreach($card_val as $k=>$v) { 479 | if($v == 'K') { 480 | unset($card_val[$k]); 481 | } 482 | } 483 | } 484 | } 485 | $card_key = array_keys($card_val); 486 | } 487 | return $card_key; 488 | } 489 | 490 | /** 491 | * 是否翻倍, 玩家翻倍处理 492 | */ 493 | public static function getIsDoubleCard($m_card = 2, $pos = 2, $arr = array()) 494 | { 495 | $list = self::$card_value_list; 496 | unset($list[self::$laizi_value]); //去除赖子大王 497 | $card_list = array_rand($list, 4); 498 | //概率运算 499 | if(!empty($arr)) { 500 | $rate = self::_getRate($arr); 501 | }else{ 502 | $rate = self::_getRate(self::$is_double_rate); 503 | } 504 | 505 | $min = $m_card % 16; 506 | //拿到最大牌A和最小牌2的概率需要特殊处理一下 507 | if($min == 1 && $rate == 3) { 508 | //最大牌A出现, 对方肯定是平手或者输 509 | $rate = rand(1, 2); 510 | } elseif($min == 2 && $rate == 1) { 511 | //最小牌2,出现对方肯定是平手或者赢 // $rate = rand(2,3); 512 | $rate = rand(2,3); 513 | } 514 | //最小牌 515 | if($rate == 2) { 516 | //不翻倍,奖励不扣除 517 | $key = $min; 518 | } elseif($rate == 3) { 519 | //翻倍,奖励累加, 系统数, 发大牌 520 | if($min == 13) { 521 | $key = 1; 522 | } else { 523 | $key = rand($min+1, 13); 524 | } 525 | } else { 526 | //不翻倍,丢失全部奖励,系统赢发小牌 527 | if($min == 1) { 528 | $key = rand(2, 13); 529 | } else { 530 | $key = rand(2, $min-1); 531 | } 532 | } 533 | //根据key组牌 534 | $card_val = array($key, $key+16, $key+32, $key+48); 535 | //去除相同的值 536 | $card_val = array_diff($card_val, $card_list); 537 | $card_key = array_rand($card_val, 1); 538 | $card_list[$pos] = $card_val[$card_key]; 539 | return array('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)); 540 | } 541 | 542 | /** 543 | * 计算概率算法 544 | * @param array $prizes 奖品概率数组 545 | * 格式:array(奖品id => array( 'rate'=>概率),奖品id => array('rate'=>概率)) 546 | * @return int 547 | */ 548 | private static function _getRate($arr = array()) 549 | { 550 | $key = 0; 551 | //首先生成一个1W内的数 552 | $rid = rand(1, 10000); 553 | //概率值(按设置累加) 554 | $rate = 0; 555 | foreach ($arr as $k => $v) { 556 | //根据设置的概率向上累加 557 | $rate += $v; 558 | //如果生成的概率数小于或等于此数据,表示当前道具ID即是,退出查找 559 | if($rid <= $rate) { 560 | $key = $k; 561 | break; 562 | } 563 | } 564 | return $key; 565 | } 566 | 567 | /** 568 | * 获取牌型结果 569 | */ 570 | public static function getCardType($arr) 571 | { 572 | $type = self::checkCardType($arr); 573 | $highlight = self::highLight($arr, $type); 574 | return array('card'=>$arr, 'type'=>$type, 'typenote'=>self::$card_type[$type], 'rate'=>self::$card_rate[$type], 'highlight'=>$highlight); 575 | } 576 | 577 | /** 578 | * 设置翻倍的概率 579 | */ 580 | public static function setRate($rate = array()) 581 | { 582 | if(empty($rate)) { 583 | self::$is_double_rate = $rate; 584 | } 585 | } 586 | } 587 | 588 | /* 589 | 590 | header("Content-type: text/html; charset=utf-8"); 591 | 592 | $act = isset($_REQUEST['act']) ? trim($_REQUEST['act']) : ''; 593 | //类调用 594 | $obj = new JokerPoker(); 595 | 596 | if($act == 'getcard') { 597 | //获取5张牌 598 | $key = $obj->getFiveCard(); 599 | //$key = array(17,37,39,40,42); 600 | exit(json_encode($key)); 601 | } elseif($act == 'turncard') { 602 | //翻牌 603 | $tmp = isset($_REQUEST['card']) ? trim($_REQUEST['card']) : ''; 604 | if(!empty($tmp)) { 605 | $key = explode('|',$tmp); 606 | } else { 607 | $key = array(); 608 | } 609 | $key = array_map('intval', $key); 610 | $card = $obj->getFiveCard($key); 611 | $res = $obj->getCardType($card); 612 | exit(json_encode($res)); 613 | } elseif($act == 'isdouble') { 614 | //翻倍处理 615 | $card = isset($_REQUEST['card']) && !empty($_REQUEST['card']) ? intval($_REQUEST['card']) : 2; 616 | $pos = (isset($_REQUEST['pos']) && $_REQUEST['pos'] < 4) ? intval($_REQUEST['pos']) : 2; 617 | $res = $obj->getIsDoubleCard($card, $pos); 618 | exit(json_encode($res)); 619 | } 620 | 621 | //测试牌型结果 622 | $tmp = isset($_REQUEST['test']) ? trim($_REQUEST['test']) : ''; 623 | if(!empty($tmp)) { 624 | $key = explode('|',$tmp); 625 | } else { 626 | $key = array(); 627 | } 628 | 629 | //类调用 630 | $obj = new JokerPoker(); 631 | $key = $obj->getFiveCard(); 632 | $key = array(13,18,24,27,43); 633 | $card = $obj->showCard($key); 634 | 635 | var_dump($key, $card, $obj->getCardType($key),$obj->getIsDoubleCard()); 636 | 637 | */ 638 | -------------------------------------------------------------------------------- /app/Game/Core/Log.php: -------------------------------------------------------------------------------- 1 | 'INFO', 12 | 2 => 'DEBUG', 13 | 3=>'ERROR' 14 | ); 15 | 16 | /** 17 | * 日志等级,1表示大于等于1的等级的日志,都会显示,依次类推 18 | * @var int 19 | */ 20 | protected static $level = 1; 21 | 22 | /** 23 | * 显示日志 24 | * @param string $centent 25 | * @param int $level 26 | */ 27 | public static function show($centent = '', $level = 1, $str = '') 28 | { 29 | if($level >= self::$level) { 30 | echo $str.date('Y/m/d H:i:s') . " [\033[0;36m" . self::$level_info[$level] . "\033[0m] " . $centent . "\n"; 31 | } 32 | } 33 | 34 | /** 35 | * 显示日志 36 | * @param string $centent 37 | * @param int $level 38 | */ 39 | public static function split($split = '', $level = 1) 40 | { 41 | if($level >= self::$level) { 42 | echo $split . "\n"; 43 | } 44 | } 45 | } 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /app/Game/Core/Packet.php: -------------------------------------------------------------------------------- 1 | $code, 16 | "msg" => $msg, 17 | "data" => $data, 18 | ); 19 | return $pack; 20 | } 21 | 22 | /** 23 | * 打包数据,固定包头,4个字节为包头(里面存了包体长度),包体前2个字节为 24 | */ 25 | public static function packEncode($data, $cmd = 1, $scmd = 1, $format='msgpack', $type = "tcp") 26 | { 27 | if ($type == "tcp") { 28 | if($format == 'msgpack') { 29 | $sendStr = msgpack_pack($data); 30 | } else { 31 | $sendStr = $data; 32 | } 33 | $sendStr = pack('N', strlen($sendStr) + 2) . pack("C2", $cmd, $scmd). $sendStr; 34 | return $sendStr; 35 | } else { 36 | return self::packFormat("packet type wrong", 100006); 37 | } 38 | } 39 | 40 | /** 41 | * 解包数据 42 | */ 43 | public static function packDecode($str, $format='msgpack') 44 | { 45 | $header = substr($str, 0, 4); 46 | if(strlen($header) != 4) { 47 | return self::packFormat("packet length invalid", 100007); 48 | } else { 49 | $len = unpack("Nlen", $header); 50 | $len = $len["len"]; 51 | $result = substr($str, 6); 52 | if ($len != strlen($result) + 2) { 53 | //结果长度不对 54 | return self::packFormat("packet length invalid", 100007); 55 | } 56 | 57 | if($format == 'msgpack') { 58 | $result = msgpack_unpack($result); 59 | } 60 | if(empty($result)) { 61 | //结果长度不对 62 | return self::packFormat("packet data is empty", 100008); 63 | } 64 | $cmd = unpack("Ccmd/Cscmd", substr($str, 4, 6)); 65 | $result = self::packFormat("OK", 0, $result); 66 | $result['cmd'] = $cmd['cmd']; 67 | $result['scmd'] = $cmd['scmd']; 68 | $result['len'] = $len + 4; 69 | return $result; 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /app/Game/Logic/ChatMsg.php: -------------------------------------------------------------------------------- 1 | _params['data']); 18 | $data = Packet::packEncode($data, MainCmd::CMD_GAME, SubCmd::CHAT_MSG_RESP); 19 | return $data; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/Game/Logic/GameCall.php: -------------------------------------------------------------------------------- 1 | _params['userinfo']['account']; 21 | $calltype = $this->_params['data']['type']; 22 | $user_room_data = $this->getRoomData($account); 23 | $room_user_data = json_decode($user_room_data[$account], true); 24 | //如果已经地主产生了, 直接下发叫地主信息 25 | if(isset($user_room_data['master']) && $user_room_data['last_chair_id']) { 26 | $this->callGameResp($room_user_data['chair_id'], $room_user_data['calltype'], $user_room_data['master'], $user_room_data['last_chair_id']); 27 | } else { 28 | if (!empty($room_user_data)) { 29 | if (!isset($room_user_data['calltype'])) { 30 | $this->setRoomUserInfoDataByKey($room_user_data, $account, 'calltype', $calltype); 31 | } else { 32 | $calltype = $room_user_data['calltype']; 33 | } 34 | $chair_id = $room_user_data['chair_id']; 35 | //广播叫地主消息 36 | $this->gameCallBroadcastResp($account, $calltype, $chair_id); 37 | //返回 38 | $this->callGameResp($chair_id, $calltype); 39 | //摸底牌操作 40 | $this->catchGameCardResp($account); 41 | } 42 | } 43 | return 0; 44 | } 45 | 46 | /** 47 | * 广播叫地主 48 | * @param $account 49 | * @param $calltype 50 | * @param $chair_id 51 | */ 52 | public function gameCallBroadcastResp($account, $calltype, $chair_id) 53 | { 54 | $fds = $this->getRoomFds($account); 55 | //匹配失败, 请继续等待 56 | $msg = array('account'=>$account, 'calltype'=>$calltype, 'chair_id'=>$chair_id, 'calltime'=>time()); 57 | $data = Packet::packFormat('OK', 0, $msg); 58 | $data = Packet::packEncode($data, MainCmd::CMD_GAME, SubCmd::SUB_GAME_CALL_TIPS_RESP); 59 | $this->pushToUsers($this->_params['serv'], $fds, $data); 60 | } 61 | 62 | /** 63 | * 组装抢地主返回数据 64 | * @param $chair_id 65 | * @param $calltype 66 | * @param $master 67 | * @param $last_chair_id 68 | * @return array|string 69 | */ 70 | protected function callGameResp($chair_id, $calltype, $master = '', $last_chair_id = 0) 71 | { 72 | $msg = array('chair_id'=>$chair_id,'calltype'=>$calltype); 73 | if($master != '' && $last_chair_id > 0) { 74 | $msg['master'] = $master; 75 | $msg['last_chair_id'] = $last_chair_id; 76 | } 77 | $data = Packet::packFormat('OK', 0, $msg); 78 | $data = Packet::packEncode($data, MainCmd::CMD_GAME, SubCmd::SUB_GAME_CALL_RESP); 79 | $this->_params['serv']->push($this->_params['userinfo']['fd'], $data, WEBSOCKET_OPCODE_BINARY); 80 | } 81 | 82 | /** 83 | * 摸手牌操作 84 | * @param $account 85 | */ 86 | protected function catchGameCardResp($account) 87 | { 88 | $room_data = $this->getRoomData($account); 89 | $infos = json_decode($room_data['uinfo'], true); 90 | if(!isset($room_data['master'])) { 91 | //加入游戏房间队列里面 92 | $calls = $accouts = array(); 93 | $flag = 0; 94 | foreach ($infos as $v) { 95 | $u = json_decode($room_data[$v], true); 96 | if (isset($u['calltype'])) { 97 | $flag++; 98 | if ($u['calltype'] == 1) { 99 | $calls[] = $v; 100 | } 101 | } 102 | } 103 | if ($flag == 3) { 104 | //抢地主里随机一个人出来 105 | if (empty($calls)) { 106 | $calls = $infos; 107 | } 108 | $key = array_rand($calls, 1); 109 | $user = $calls[$key]; 110 | //抓牌,合并手牌数据 111 | $user_data = json_decode($room_data[$user], true); 112 | $hand = json_decode($room_data['hand'], true); 113 | $card = array_values(array_merge($user_data['card'], $hand)); 114 | $card = $this->obj_ddz->_sortCardByGrade($card); 115 | $user_data['card'] = $card; 116 | //设置地主和用户手牌数据 117 | $param = array( 118 | 'master'=>$user, 119 | $user=>json_encode($user_data) 120 | ); 121 | $this->muitSetRoomData($account, $param); 122 | $this->catchGameCard($room_data, $user); 123 | } 124 | } 125 | } 126 | 127 | /** 128 | * 抓牌返回数据 129 | * @param $room_data 130 | * @param $user 131 | * @param $infos 132 | */ 133 | protected function catchGameCard($room_data, $user) 134 | { 135 | $info = json_decode($room_data[$user], true); 136 | $msg = array( 137 | 'user'=>$user, 138 | 'chair_id' => $info['chair_id'], 139 | 'hand_card' => $room_data['hand'] 140 | ); 141 | $data = Packet::packFormat('OK', 0, $msg); 142 | $data = Packet::packEncode($data, MainCmd::CMD_GAME, SubCmd::SUB_GAME_CATCH_BASECARD_RESP); 143 | $this->pushToUsers($this->_params['serv'], $this->getRoomFds($user), $data); 144 | } 145 | } -------------------------------------------------------------------------------- /app/Game/Logic/GameOutCard.php: -------------------------------------------------------------------------------- 1 | _params['userinfo']['account']; 23 | $user_room_data = $this->getRoomData($account); 24 | $out_cards = $this->_params['data']; 25 | $ret = $this->playCard($user_room_data, $out_cards, $account); 26 | return $ret; 27 | } 28 | 29 | /** 30 | * 用户打牌逻辑处理 31 | * @param $user_room_data 32 | * @param $out_cards 33 | * @param $account 34 | * @return int 35 | */ 36 | protected function playCard($user_room_data, $out_cards, $account) 37 | { 38 | //轮次 39 | $round = isset($user_room_data['round']) ? $user_room_data['round'] + 1 : 0; 40 | //手次 41 | $hand = isset($user_room_data['hand_num']) ? $user_room_data['hand_num'] + 1 : 1; 42 | //本轮次上一次牌型 43 | $last_chair_id = isset($user_room_data['last_chair_id']) ? $user_room_data['last_chair_id'] : 0; 44 | //本轮次上一次牌型 45 | $last_card_type = isset($user_room_data['last_card_type']) ? $user_room_data['last_card_type'] : 0; 46 | //本轮次上一次牌值 47 | $last_card = isset($user_room_data['last_card']) ? $user_room_data['last_card'] : ''; 48 | //下一个出牌人椅子id 49 | $next_chair_id = $out_cards['chair_id'] + 1; 50 | $next_chair_id = ($next_chair_id > 3) ? $next_chair_id - 3 : $next_chair_id; 51 | 52 | //根据椅子查询手牌信息 53 | $my_card = json_decode($user_room_data[$account], true); 54 | 55 | //出牌牌型 56 | $card_type = '无'; 57 | 58 | //验证出牌数据 59 | if($out_cards['status'] == 1) { 60 | if(count($out_cards['card']) == 0) { 61 | return $this->gameOutCard(array('status'=>1, 'msg'=>'出牌非法, 请出手牌')); 62 | } else { 63 | //判断手牌是否存在, 手牌存在继续往下执行 64 | if (!$out_cards['card'] == array_intersect($out_cards['card'], $my_card['card'])) { 65 | return $this->gameOutCard(array('status'=>2, 'msg'=>'出牌非法, 出牌数据有问题')); 66 | } 67 | //检查牌型 68 | $arr = $this->obj_ddz->checkCardType($out_cards['card']); 69 | if($arr['type'] == 0) { 70 | return $this->gameOutCard(array('status'=>3, 'msg'=>'出牌非法, 牌型有误')); 71 | } else { 72 | $card_type = $arr['type_msg']; 73 | } 74 | //如果非首轮牌, 请验证牌型, 并判断牌型是否一直, 如果打出的牌型是, 炸弹和飞机, 跳过验证, 13表示炸弹,14表示飞机 75 | if($last_card_type > 0 && !in_array($arr['type'], array(13,14)) && $last_card_type != $arr['type']) { 76 | return $this->gameOutCard(array('status'=>3, 'msg'=>'出牌非法, 和上一把牌型不符合')); 77 | } 78 | $out_cards['card_type'] = $arr['type']; 79 | //比牌大小 80 | if(!$this->obj_ddz->checkCardSize($out_cards['card'], json_decode($last_card, true))) { 81 | return $this->gameOutCard(array('status'=>4, 'msg'=>'出牌非法, 牌没有大过上家牌')); 82 | } 83 | } 84 | } else { 85 | //过牌要验证是否为首次出牌, 如果是首次出牌是不能过牌的 86 | if($hand == 1 || $last_chair_id == $out_cards['chair_id']) { 87 | return $this->gameOutCard(array('status'=>4, 'msg'=>'出牌非法, 首次出牌不能过牌操作')); 88 | } 89 | } 90 | if($out_cards['chair_id'] < 1) { 91 | return $this->gameOutCard(array('status'=>5, 'msg'=>'出牌非法, 椅子ID非法')); 92 | } 93 | //判断游戏是否结束 94 | if(count($my_card['card']) < 1) { 95 | return $this->gameOutCard(array('status'=>6, 'msg'=>'游戏结束, 所有手牌已经出完')); 96 | } 97 | 98 | //出牌逻辑 99 | if($last_card_type == 0) { 100 | //如果上一次牌型为0, 证明没有牌型, 这次手牌为开始手牌 101 | $ret = $this->roundStart($user_room_data, $out_cards, $account, $hand, $next_chair_id); 102 | \App\Game\Core\Log::show($account.":第".$ret['round'].'回合-开始'); 103 | } elseif($out_cards['status'] == 0 && $last_chair_id == $next_chair_id) { 104 | //上一轮过牌, 并上一次椅子id和这一次相等, 轮次结束 105 | $this->roundEnd($account, $last_chair_id, $hand, $next_chair_id); 106 | \App\Game\Core\Log::show($account.":第".$round.'回合-结束'); 107 | } else { 108 | //跟牌逻辑 109 | $this->roundFollow($out_cards, $account, $hand, $next_chair_id); 110 | $last_chair_id = $out_cards['chair_id']; 111 | \App\Game\Core\Log::show($account.":第".$round.'回合-跟牌'); 112 | } 113 | 114 | //判断下个用户, 是首次出牌还是跟牌操作 115 | $is_first_round = $last_chair_id == $next_chair_id ? true : false; 116 | //设置减少手牌数据 117 | $my_card = $this->setMyCard($user_room_data, $out_cards, $account); 118 | //判断游戏是否结束 119 | $is_game_over = (count($my_card['card']) < 1) ? true : false; 120 | //计算下家牌是否能大过上一手牌 121 | // $next_card = $this->findCardsByChairId($user_room_data, $next_chair_id); 122 | // $prv_card = (isset($out_cards['card']) && count($out_cards['card']) > 0) ? $out_cards['card'] : json_decode($last_card, true); 123 | // $is_out_card = $this->obj_ddz->isPlayCard($next_card, $prv_card); 124 | // var_dump($next_card, $prv_card, $is_out_card); 125 | 126 | //并下发出牌提示 127 | $step = array( 128 | 'round'=>$round, //轮次 129 | 'hand_num'=>$hand, //首次 130 | 'chair_id'=>$out_cards['chair_id'], //出牌椅子 131 | 'account'=>$account, //出牌账号 132 | 'show_type'=>$out_cards['status'], //1,跟牌, 2, 过牌 133 | 'next_chair_id'=>$next_chair_id, //下一个出牌的椅子id 134 | 'is_first_round'=>$is_first_round, //是否为首轮, 下一个出牌人的情况 135 | 'card'=>$out_cards['card'], //本次出牌 136 | 'card_type' => $card_type, //显示牌型 137 | 'last_card'=>json_decode($last_card, true), //上次最大牌 138 | 'is_game_over' => $is_game_over //游戏是否结束 139 | ); 140 | 141 | //记录一下出牌数据, 记录没步骤录像数据 142 | $this->setRoomPlayCardStep($account, 'step_'.$hand, json_encode($step)); 143 | //广播打牌结果 144 | $ret = $this->gameOutCardResp($this->_params['serv'], $account, $step); 145 | //游戏结束, 重置游戏数据 146 | $this->gameOver($account, json_decode($user_room_data['uinfo'], true), $is_game_over); 147 | //记录步骤信息 148 | Log::info(json_encode($step), 'info'); 149 | return $ret; 150 | } 151 | 152 | /** 153 | * 轮次开始 154 | * @param $user_room_data 155 | * @param $out_cards 156 | * @param $account 157 | * @param $hand 158 | * @param $next_chair_id 159 | * @return array 160 | */ 161 | protected function roundStart($user_room_data, $out_cards, $account, $hand, $next_chair_id) 162 | { 163 | //当前轮次 164 | $round = isset($user_room_data['round']) ? $user_room_data['round'] + 1 : 1; 165 | //本轮次开始时椅子id 166 | $start_chair_id = $out_cards['chair_id']; 167 | //本轮次最大牌椅子id 168 | $last_chair_id = $out_cards['chair_id']; 169 | //本轮次最大牌椅子i牌型 170 | $last_card_type = $out_cards['card_type']; 171 | //本轮次最大牌椅子牌值 172 | $last_card = $out_cards['card']; 173 | 174 | //结果存入redis 175 | $param = array( 176 | 'round'=>$round, 177 | 'hand_num'=>$hand, 178 | 'start_chair_id'=>$start_chair_id, 179 | 'last_chair_id'=>$last_chair_id, 180 | 'last_card_type'=>$last_card_type, 181 | 'last_card'=>json_encode($last_card), 182 | 'next_chair_id'=>$next_chair_id 183 | ); 184 | $this->muitSetRoomData($account, $param); 185 | return $param; 186 | } 187 | 188 | /** 189 | * 轮次结束 190 | * @param $account 191 | * @param $last_chair_id 192 | * @param $next_chair_id 193 | * @param $hand 194 | */ 195 | protected function roundEnd($account, $last_chair_id, $hand, $next_chair_id) 196 | { 197 | //结果存入redis 198 | $param = array( 199 | 'start_chair_id'=>$last_chair_id, 200 | 'last_card_type'=>0, 201 | 'last_card'=>json_encode(array()), 202 | 'hand_num'=>$hand, 203 | 'next_chair_id'=>$next_chair_id 204 | ); 205 | $this->muitSetRoomData($account, $param); 206 | } 207 | 208 | /** 209 | * 跟牌 210 | * @param $out_cards 211 | * @param $account 212 | * @param $next_chair_id 213 | * @param $hand 214 | */ 215 | protected function roundFollow($out_cards, $account, $hand, $next_chair_id) 216 | { 217 | //跟牌 218 | $param = array(); 219 | if($out_cards['status'] == 1) { 220 | //本轮次上一次最大牌椅子id 221 | $param = array( 222 | 'last_chair_id'=>$out_cards['chair_id'], 223 | 'last_card'=>json_encode($out_cards['card']), 224 | ); 225 | } 226 | $param['next_chair_id'] = $next_chair_id; 227 | $param['hand_num'] = $hand; 228 | //结果存入redis 229 | $this->muitSetRoomData($account, $param); 230 | } 231 | 232 | /** 233 | * 游戏结束 234 | * @param $account 235 | * @param $uinfo 236 | * @param $is_game_over 237 | */ 238 | protected function gameOver($account, $uinfo, $is_game_over) : void { 239 | if($is_game_over) { 240 | //设置游戏结束标识 241 | $this->setRoomData($account, 'is_game_over', $is_game_over); 242 | //清除数据, 进行下一轮玩牌, 随机分配 243 | $this->clearRoomNo($uinfo); 244 | } 245 | } 246 | 247 | /** 248 | * 设置我的手牌 249 | * @param $user_room_data 250 | * @param $cards 251 | * @param $account 252 | * @return mixed 253 | */ 254 | protected function setMyCard($user_room_data, $cards, $account) 255 | { 256 | //根据椅子查询手牌信息 257 | $my_card = json_decode($user_room_data[$account], true); 258 | $hand_card = array_unique(array_values(array_diff($my_card['card'], $cards['card']))); 259 | if(isset($my_card['out_card'])) { 260 | $out_card = array_unique(array_values(array_merge($my_card['out_card'], $cards['card']))); 261 | } else { 262 | $out_card = $cards['card']; 263 | } 264 | $my_card['card'] = $hand_card; 265 | $my_card['out_card'] = $out_card; 266 | //写会redis 267 | $this->setRoomData($account, $account, json_encode($my_card)); 268 | return $my_card; 269 | } 270 | 271 | /** 272 | * 根据椅子id找出这个一直用户的手牌 273 | * @param $user_room_data 274 | * @param $chair_id 275 | * @return array 276 | */ 277 | protected function findCardsByChairId($user_room_data, $chair_id) { 278 | $uinfo = json_decode($user_room_data['uinfo'], true); 279 | $cards = array(); 280 | foreach($uinfo as $v) { 281 | $d = json_decode($user_room_data[$v], true); 282 | if(isset($d['chair_id']) && $d['chair_id'] == $chair_id) { 283 | $cards = $d['card']; 284 | break; 285 | } 286 | } 287 | return $cards; 288 | } 289 | 290 | /** 291 | * 向客户端发送出牌提示响应, 单发 292 | * @param $param 293 | * @return array|string 294 | */ 295 | protected function gameOutCard($param) 296 | { 297 | $data = Packet::packFormat('OK', 0, $param); 298 | $data = Packet::packEncode($data, MainCmd::CMD_GAME, SubCmd::SUB_GAME_OUT_CARD); 299 | return $data; 300 | } 301 | 302 | /** 303 | * 向客户端广播出牌响应, 群发 304 | * @param $serv 305 | * @param $account 306 | * @param $param 307 | * @return int 308 | */ 309 | protected function gameOutCardResp($serv, $account, $param) 310 | { 311 | $data = Packet::packFormat('OK', 0, $param); 312 | $data = Packet::packEncode($data, MainCmd::CMD_GAME, SubCmd::SUB_GAME_OUT_CARD_RESP); 313 | $this->pushToUsers($serv, $this->getRoomFds($account), $data); 314 | //并提示成功 315 | return $this->gameOutCard(array('status'=>0, 'msg'=>'出牌成功', 'data'=>$param)); 316 | } 317 | } -------------------------------------------------------------------------------- /app/Game/Logic/GameStart.php: -------------------------------------------------------------------------------- 1 | _params['userinfo']['account']; 23 | $room_data = $this->getRoomData($account); 24 | $user_room_data = isset($room_data[$account]) ? json_decode($room_data[$account], true) : array(); 25 | if($user_room_data) { 26 | //是否产生地主 27 | $master = isset($room_data['master']) ? $room_data['master'] : ''; 28 | if($master) { 29 | $user_room_data['is_master'] = 1; 30 | if($master == $account) { 31 | //此人是地主 32 | $user_room_data['master'] = 1; 33 | } 34 | } else { 35 | $user_room_data['is_master'] = 0; 36 | } 37 | 38 | //轮到谁出牌了 39 | $last_chair_id = isset($room_data['last_chair_id']) ? $room_data['last_chair_id'] : 0; 40 | $next_chair_id = isset($room_data['next_chair_id']) ? $room_data['next_chair_id'] : 0; 41 | $user_room_data['is_first_round'] = false; 42 | if($next_chair_id > 0) { 43 | $user_room_data['index_chair_id'] = $next_chair_id; 44 | if($next_chair_id == $last_chair_id) { 45 | //首轮出牌 46 | $user_room_data['is_first_round'] = true; 47 | } 48 | } else { 49 | //地主首次出牌 50 | if(isset($room_data[$master])) { 51 | $master_info = json_decode($room_data[$master], true); 52 | $user_room_data['index_chair_id'] = $master_info['chair_id']; 53 | //首轮出牌 54 | $user_room_data['is_first_round'] = true; 55 | } 56 | } 57 | 58 | //判断游戏是否结束 59 | $user_room_data['is_game_over'] = isset($room_data['is_game_over']) ? $room_data['is_game_over'] : false; 60 | //进入房间成功 61 | $msg = $user_room_data; 62 | $room_data = Packet::packFormat('OK', 0, $msg); 63 | $room_data = Packet::packEncode($room_data, MainCmd::CMD_SYS, SubCmd::ENTER_ROOM_SUCC_RESP); 64 | return $room_data; 65 | } else { 66 | $room_list = $this->getGameConf('room_list'); 67 | if($room_list) { 68 | //判断是否在队列里面 69 | Redis::sAdd($room_list, $this->_params['userinfo']['account']); 70 | //投递异步任务 71 | $data = Task::async('GameSyncTask', 'gameRoomMatch', [$this->_params['userinfo']['fd']]); 72 | } 73 | return 0; 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /app/Game/Logic/HeartAsk.php: -------------------------------------------------------------------------------- 1 | _params['data']['time']) ? $this->_params['data']['time'] : 0; 21 | $end_time = $this->getMillisecond(); 22 | $time = $end_time - $begin_time; 23 | $data = Packet::packFormat('OK', 0, array('time'=>$time)); 24 | $data = Packet::packEncode($data, MainCmd::CMD_SYS, SubCmd::HEART_ASK_RESP); 25 | return $data; 26 | } 27 | 28 | function getMillisecond() 29 | { 30 | list($t1, $t2) = explode(' ', microtime()); 31 | return (float)sprintf('%.0f',(floatval($t1)+floatval($t2))*1000); 32 | } 33 | } -------------------------------------------------------------------------------- /app/Helper/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jxy918/swoft-ddz/07c42b2437691fa502e6891384f9bd4ce7d79801/app/Helper/.gitkeep -------------------------------------------------------------------------------- /app/Helper/Functions.php: -------------------------------------------------------------------------------- 1 | _isLogin($request)) { 63 | return $response->redirect('/login'); 64 | } 65 | //用户信息传递到客户端 66 | $info = $request->getCookieParams(); 67 | $u = json_decode($info['USER_INFO'], true); 68 | return view('game/game', $u); 69 | } 70 | 71 | /** 72 | * login 73 | * @RequestMapping(route="/login") 74 | * @param Request $request 75 | * @param Response $response 76 | * @return Swoft\Http\Message\Concern\CookiesTrait|Response 77 | */ 78 | public function login(Request $request, Response $response) 79 | { 80 | $action = $request->post('action'); 81 | $account = $request->post('account'); 82 | $tips = ''; 83 | if($action == 'login') { 84 | if(!empty($account)) { 85 | //注册登录 86 | $uinfo = array('account'=>$account); 87 | $response = $response->withCookie('USER_INFO', json_encode($uinfo)); 88 | return $response->redirect('/game'); 89 | } else { 90 | $tips = '温馨提示:用户账号不能为空!'; 91 | } 92 | } 93 | return view('game/login', ['tips'=>$tips]); 94 | } 95 | 96 | /** 97 | * @RequestMapping(route="/test") 98 | * @throws Throwable 99 | */ 100 | public function test() 101 | { 102 | return view('game/test'); 103 | } 104 | 105 | /** 106 | * 是否登录 107 | * @param Request $request 108 | * @return bool 109 | */ 110 | private function _isLogin(Request $request) 111 | { 112 | $cookie_info = $request->getCookieParams(); 113 | if(isset($cookie_info['USER_INFO'])) { 114 | $this->userinfo = json_decode($cookie_info['USER_INFO']); 115 | return true; 116 | } else { 117 | return false; 118 | } 119 | } 120 | 121 | /** 122 | * 广播当前服务器消息 123 | * @RequestMapping(route="/broadcast") 124 | * @return array 125 | */ 126 | public function broadcast(Request $request) 127 | { 128 | $msg = $request->query('msg'); 129 | //原封不动发回去 130 | if(!empty($msg)) { 131 | $msg = array('data' => $msg); 132 | } else { 133 | $msg = array('data' => 'this is a system msg'); 134 | } 135 | $data = Packet::packFormat('OK', 0, $msg); 136 | $data = Packet::packEncode($data, MainCmd::CMD_SYS, SubCmd::BROADCAST_MSG_RESP); 137 | $cnt = server()->sendToAll($data, 0, 50, WEBSOCKET_OPCODE_BINARY); 138 | return ['status'=>0, 'msg'=>'给'.$cnt.'广播了消息']; 139 | } 140 | 141 | /** 142 | * 广播全服, 分布式广播, 需要安装consul注册发现服务器 143 | * @RequestMapping(route="/broadcast_to_all") 144 | * @return array 145 | */ 146 | public function broadcastToAll(Request $request) 147 | { 148 | $msg = $request->query('msg'); 149 | $msg = !empty($msg) ? $msg : "this is a system msg"; 150 | //走consul注册发现服务器来广播消息,获取服务器列表 151 | $serviceList = $this->getServiceList(); 152 | $result = []; 153 | //采用http循环发送消息 154 | foreach($serviceList as $v) { 155 | $cli = new Client($v['ip'], $v['port']); 156 | $cli->get("broadcast?msg={$msg}"); 157 | $result = $cli->body; 158 | $cli->close(); 159 | } 160 | return $result; 161 | } 162 | 163 | /** 164 | * get service list, 默认就是游戏网关服务器的consul服务器name 165 | * 166 | * @param string $serviceName 167 | * @return array 168 | */ 169 | public function getServiceList($serviceName = 'gateway') 170 | { 171 | $cli = new Client(self::DISCOVERY_IP, self::DISCOVERY_PORT); 172 | $cli->get(sprintf(self::DISCOVERY_URI, $serviceName)); 173 | $result = $cli->body; 174 | $cli->close(); 175 | $services = json_decode($result, true); 176 | 177 | // 数据格式化 178 | $nodes = []; 179 | foreach ($services as $service) { 180 | if (!isset($service['Service'])) { 181 | App::warning("consul[Service] 服务健康节点集合,数据格式不不正确,Data=" . $result); 182 | continue; 183 | } 184 | $serviceInfo = $service['Service']; 185 | if (!isset($serviceInfo['Address'], $serviceInfo['Port'])) { 186 | App::warning("consul[Address] Or consul[Port] 服务健康节点集合,数据格式不不正确,Data=" . $result); 187 | continue; 188 | } 189 | $address = $serviceInfo['Address']; 190 | $port = $serviceInfo['Port']; 191 | $uri = ['ip'=>$address, 'port'=>$port]; 192 | $nodes[] = $uri; 193 | } 194 | return $nodes; 195 | } 196 | 197 | /** 198 | * 录像页面 199 | * @RequestMapping(route="/camera") 200 | * @throws Throwable 201 | */ 202 | public function camera() 203 | { 204 | return view('vedio/camera'); 205 | } 206 | 207 | /** 208 | * 直播页面 209 | * @RequestMapping(route="/show") 210 | * @throws Throwable 211 | */ 212 | public function show() 213 | { 214 | return view('vedio/show'); 215 | } 216 | } -------------------------------------------------------------------------------- /app/Http/Middleware/SomeMiddleware.php: -------------------------------------------------------------------------------- 1 | getUri()->getPath(); 32 | if ($path === '/favicon.ico') { 33 | $response = Context::mustGet()->getResponse(); 34 | return $response->withStatus(404); 35 | } 36 | return $handler->handle($request); 37 | } 38 | } -------------------------------------------------------------------------------- /app/Listener/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jxy918/swoft-ddz/07c42b2437691fa502e6891384f9bd4ce7d79801/app/Listener/.gitkeep -------------------------------------------------------------------------------- /app/Listener/BeforeStartListener.php: -------------------------------------------------------------------------------- 1 | handle($request); 31 | } 32 | } -------------------------------------------------------------------------------- /app/Rpc/Service/DbproxyService.php: -------------------------------------------------------------------------------- 1 | execProc('account_mj','sp_account_get_by_uid',array($uid)); 51 | // } 52 | // 53 | // /** 54 | // * 根据uname获取用户信息 55 | // * @param string $uname 56 | // * @return array 57 | // * @throws \Swoft\Db\Exception\DbException 58 | // */ 59 | // public function getLobbyInfoByName(string $uname) 60 | // { 61 | // return $this->execProc('account_mj','sp_account_get_by_username',array($uname)); 62 | // } 63 | // 64 | // /** 65 | // * @param int $uid 66 | // */ 67 | // public function getGameInfoByUid(int $uid) 68 | // { 69 | // return $this->execProc('game_mj','sp_user_get_by_uid',array($uid)); 70 | // } 71 | // 72 | // /** 73 | // * 根据uid获取用户信息 74 | // * @param int $uid 75 | // * @return array 76 | // * @throws \Swoft\Db\Exception\DbException 77 | // */ 78 | // public function getYuanbao(int $uid) 79 | // { 80 | // return $this->execProc('account_mj','sp_yuanbao_get',array($uid)); 81 | // } 82 | // 83 | // /** 84 | // * 添加元宝 85 | // * @param int $uid 86 | // * @param int $quantity 87 | // * @param int $type_id 88 | // * @param int $type_id_sub 89 | // * @param int $gid 90 | // * @param int $kind_id 91 | // * @param string $summary 92 | // * @return array 93 | // * @throws \Swoft\Db\Exception\DbException 94 | // */ 95 | // public function addYuanbao(int $uid, int $quantity, int $type_id = 10002, int $type_id_sub = 0 , int $gid = 0, int $kind_id = 0, string $summary = 'web exec') 96 | // { 97 | // $params = array($uid, $quantity); 98 | // $result = $this->execProc('account_mj','sp_yuanbao_add',$params); 99 | // if(isset($result[0]['quantity']) && isset($result[0]['quantity1'])) { 100 | // //写入流水 101 | // $params2 = array( 102 | // time(), 103 | // $uid, 104 | // $gid, 105 | // $type_id, 106 | // $type_id_sub, 107 | // $result[0]['quantity1'], 108 | // $result[0]['quantity'], 109 | // $quantity, 110 | // 0, 111 | // 0, 112 | // $kind_id, 113 | // $summary 114 | // ); 115 | // $res = $this->execProc('log_comm_mj', 'sp_yuanbao_i', $params2); 116 | // if(!$res) { 117 | // App::error('addYuanbao_sp_yuanbao_i:' . $res); 118 | // } 119 | // } 120 | // return $result; 121 | // } 122 | // 123 | // /** 124 | // * 删除元宝 125 | // * @param int $uid 126 | // * @param int $quantity 127 | // * @param int $type_id 128 | // * @param int $type_id_sub 129 | // * @param int $gid 130 | // * @param int $kind_id 131 | // * @param string $summary 132 | // * @return array 133 | // * @throws \Swoft\Db\Exception\DbException 134 | // */ 135 | // public function delYuanbao(int $uid, int $quantity, int $type_id = 10002, int $type_id_sub = 0, int $gid = 0, int $kind_id = 0, string $summary = 'web exec') 136 | // { 137 | // $params = array($uid, $quantity); 138 | // $result = $this->execProc('account_mj','sp_yuanbao_del2',$params); 139 | // if(isset($result[0]['quantity']) && isset($result[0]['quantity1'])) { 140 | // //写入流水 141 | // $params2 = array( 142 | // time(), 143 | // $uid, 144 | // $gid, 145 | // $type_id, 146 | // $type_id_sub, 147 | // $result[0]['quantity1'], 148 | // $result[0]['quantity'], 149 | // $quantity, 150 | // 0, 151 | // 0, 152 | // $kind_id, 153 | // $summary 154 | // ); 155 | // $res = $this->execProc('log_comm_mj', 'sp_yuanbao_i', $params2); 156 | // if(!$res) { 157 | // App::error('delYuanbao_sp_yuanbao_i:' . $res); 158 | // } 159 | // } 160 | // return $result; 161 | // } 162 | // 163 | // /** 164 | // * 修改用户金币 165 | // * @param int $uid 166 | // * @param int $quantity 167 | // * @param int $type_id 168 | // * @param int $type_id_sub 169 | // * @param int $gid 170 | // * @param int $kind_id 171 | // * @param string $summary 172 | // * @return array 173 | // * @throws \Swoft\Db\Exception\DbException 174 | // */ 175 | // public function modifyGold(int $uid, int $quantity, int $type_id = 10002, int $type_id_sub = 0, int $gid = 0, int $kind_id = 0, string $summary = 'web exec') 176 | // { 177 | // $params = array($uid, $quantity); 178 | // $result = $this->execProc('account_mj','sp_account_gold_upd2',$params); 179 | // if(isset($result[0]['quantity']) && isset($result[0]['quantity1'])) { 180 | // //写入流水 181 | // $params2 = array( 182 | // time(), 183 | // $uid, 184 | // $gid, 185 | // $type_id, 186 | // $type_id_sub, 187 | // $result[0]['quantity1'], 188 | // $result[0]['quantity'], 189 | // $quantity, 190 | // 0, 191 | // 0, 192 | // $kind_id, 193 | // $summary 194 | // ); 195 | // $res = $this->execProc('log_comm_mj', 'sp_gold_i', $params2); 196 | // if(!$res) { 197 | // App::error('modifyGold_sp_gold_i:' . $res); 198 | // } 199 | // } 200 | // return $result; 201 | // } 202 | // 203 | // public function addProp(int $uid, int $prop_id , int $quantity, int $expiry_time = 2145888000, int $type_id = 10002, int $type_id_sub = 0, int $gid = 0, $cid = 0, $kind_id = 0, $version = '', $summary = 'web add') 204 | // { 205 | // $params = array($uid, $prop_id, $expiry_time, $quantity); 206 | // $result = $this->execProc('account_mj','sp_account_bag_add',$params); 207 | // if(isset($result[0]['quantity1']) && isset($result[0]['quantity2'])) { 208 | // //写入流水 209 | // $params2 = array( 210 | // $uid, 211 | // $type_id, 212 | // $type_id_sub, 213 | // $kind_id, 214 | // $gid, 215 | // $prop_id, 216 | // $quantity, 217 | // $result[0]['quantity1'], 218 | // $result[0]['quantity2'], 219 | // time(), 220 | // $cid, 221 | // $version, 222 | // 0, 223 | // $summary 224 | // ); 225 | // $res = $this->execProc('log_comm_mj', 'sp_bag_i', $params2); 226 | // if(!$res) { 227 | // App::error('addProp_sp_bag_i:' . $res); 228 | // } 229 | // } 230 | // return $result; 231 | // } 232 | // 233 | // public function delProp(int $uid, int $prop_id , int $quantity, $type_id = 10002, int $type_id_sub = 0, int $gid = 0, $cid = 0, $kind_id = 0, $version = '', $summary = 'web add') 234 | // { 235 | // $params = array($uid, $prop_id, $quantity); 236 | // $result = $this->execProc('account_mj','sp_account_bag_del2',$params); 237 | // var_dump($result); 238 | // if(isset($result[0]['quantity1']) && isset($result[0]['quantity2'])) { 239 | // //写入流水 240 | // $params2 = array( 241 | // $uid, 242 | // $type_id, 243 | // $type_id_sub, 244 | // $kind_id, 245 | // $gid, 246 | // $prop_id, 247 | // $quantity, 248 | // $result[0]['quantity1'], 249 | // $result[0]['quantity2'], 250 | // time(), 251 | // $cid, 252 | // $version, 253 | // 0, 254 | // $summary 255 | // ); 256 | // $res = $this->execProc('log_comm_mj', 'sp_bag_i', $params2); 257 | // if(!$res) { 258 | // App::error('addProp_sp_bag_i:' . $res); 259 | // } 260 | // } 261 | // return $result; 262 | // 263 | // } 264 | } -------------------------------------------------------------------------------- /app/Rpc/Service/UserService.php: -------------------------------------------------------------------------------- 1 | ['list']]; 31 | } 32 | 33 | /** 34 | * @param int $id 35 | * 36 | * @return bool 37 | */ 38 | public function delete(int $id): bool 39 | { 40 | return false; 41 | } 42 | 43 | /** 44 | * @return void 45 | */ 46 | public function returnNull(): void 47 | { 48 | return; 49 | } 50 | 51 | /** 52 | * @return string 53 | */ 54 | public function getBigContent(): string 55 | { 56 | $content = Co::readFile(__DIR__ . '/big.data'); 57 | return $content; 58 | } 59 | 60 | /** 61 | * Exception 62 | * @throws Exception 63 | */ 64 | public function exception(): void 65 | { 66 | throw new Exception('exception version'); 67 | } 68 | 69 | /** 70 | * @param string $content 71 | * 72 | * @return int 73 | */ 74 | public function sendBigContent(string $content): int 75 | { 76 | return strlen($content); 77 | } 78 | } -------------------------------------------------------------------------------- /app/Rpc/Service/UserServiceV2.php: -------------------------------------------------------------------------------- 1 | ['list'], 32 | 'v' => '1.2' 33 | ]; 34 | } 35 | 36 | /** 37 | * @return void 38 | */ 39 | public function returnNull(): void 40 | { 41 | return; 42 | } 43 | 44 | /** 45 | * @param int $id 46 | * 47 | * @return bool 48 | */ 49 | public function delete(int $id): bool 50 | { 51 | return false; 52 | } 53 | 54 | /** 55 | * @return string 56 | */ 57 | public function getBigContent(): string 58 | { 59 | $content = Co::readFile(__DIR__ . '/big.data'); 60 | return $content; 61 | } 62 | 63 | /** 64 | * Exception 65 | * @throws Exception 66 | */ 67 | public function exception(): void 68 | { 69 | throw new Exception('exception version2'); 70 | } 71 | 72 | /** 73 | * @param string $content 74 | * 75 | * @return int 76 | */ 77 | public function sendBigContent(string $content): int 78 | { 79 | return strlen($content); 80 | } 81 | } -------------------------------------------------------------------------------- /app/Task/Listener/FinishListener.php: -------------------------------------------------------------------------------- 1 | getTaskUniqid()); 28 | } 29 | } -------------------------------------------------------------------------------- /app/Task/Task/GameSyncTask.php: -------------------------------------------------------------------------------- 1 | sCard($game_conf['room_list']); 42 | $serv = server(); 43 | if($len >= 3) { 44 | //匹配成功, 下发手牌数据, 并进入房间数据 45 | $users = $users_key = $fds = array(); 46 | for($i = 0; $i < 3; $i++) { 47 | $account = $redis->sPop($game_conf['room_list']); 48 | $key = sprintf($game_conf['user_bind_key'], $account); 49 | //根据账号获取fd 50 | $fds[$account] = $redis->get($key); 51 | //获取账号数 52 | $users[] = $account; 53 | } 54 | //获取房间号 55 | $room_no_key = $game_conf['user_room_no']; 56 | if($redis->exists($room_no_key)) { 57 | $room_no = $redis->get($room_no_key); 58 | $room_no++; 59 | $redis->set($room_no_key, $room_no); 60 | } else { 61 | $room_no = intval(1000001); 62 | $redis->set($room_no_key, $room_no); 63 | } 64 | //存入房间号和用户对应关系 65 | foreach($users as $v) { 66 | $user_key = sprintf($game_conf['user_room'], $v); 67 | $user_room[$user_key] = $room_no; 68 | } 69 | 70 | if(!empty($user_room)) { 71 | $redis->mset($user_room); 72 | } 73 | 74 | //随机发牌 75 | $obj = new DdzPoker(); 76 | $card = $obj->dealCards($users); 77 | 78 | //存入用户信息 79 | $room_data = array( 80 | 'room_no'=>$room_no, 81 | 'hand'=>$card['card']['hand'] 82 | ); 83 | foreach($users as $k=>$v) { 84 | $room_data['uinfo'][] = $v; 85 | $room_data[$v] = array( 86 | 'card'=>$card['card'][$v] , 87 | 'chair_id'=>($k+1) 88 | ); 89 | } 90 | $user_room_data_key = sprintf($game_conf['user_room_data'], $room_no); 91 | $this->arrToHashInRedis($room_data, $user_room_data_key); 92 | //分别发消息给三个人 93 | foreach($users as $k=>$v) { 94 | if(isset($fds[$v])) { 95 | $data = Packet::packFormat('OK', 0, $room_data[$v]); 96 | $data = Packet::packEncode($data, MainCmd::CMD_SYS, SubCmd::ENTER_ROOM_SUCC_RESP); 97 | $serv->push($fds[$v], $data, WEBSOCKET_OPCODE_BINARY); 98 | } 99 | } 100 | } else { 101 | //匹配失败, 请继续等待 102 | $msg = array( 103 | 'status'=>'fail', 104 | 'msg'=>'人数不够3人,请耐心等待!' 105 | ); 106 | $data = Packet::packFormat('OK', 0, $msg); 107 | $data = Packet::packEncode($data, MainCmd::CMD_SYS, SubCmd::ENTER_ROOM_FAIL_RESP); 108 | $serv->push($fd, $data, WEBSOCKET_OPCODE_BINARY); 109 | } 110 | } 111 | 112 | /** 113 | * 广播叫地主 114 | * @param $account 115 | * @param $calltype 116 | * @param $chair_id 117 | */ 118 | public function gameCall($account, $calltype, $chair_id) 119 | { 120 | $fds = $this->_getRoomFds($account); 121 | //匹配失败, 请继续等待 122 | $msg = array( 123 | 'account'=>$account, 124 | 'calltype'=>$calltype, 125 | 'chair_id'=>$chair_id, 126 | 'calltime'=>time() 127 | ); 128 | $data = Packet::packFormat('OK', 0, $msg); 129 | $data = Packet::packEncode($data, MainCmd::CMD_GAME, SubCmd::SUB_GAME_CALL_TIPS_RESP); 130 | $serv = server(); 131 | $this->pushToUsers($serv, $fds, $data); 132 | } 133 | 134 | /** 135 | * 当connetions属性无效时可以使用此方法,服务器广播消息, 此方法是给所有的连接客户端, 广播消息,通过方法getClientList广播 136 | * @param $serv 137 | * @param $data 138 | * @return array 139 | */ 140 | protected function pushToAll($serv, $data) 141 | { 142 | $client = array(); 143 | $start_fd = 0; 144 | while(true) { 145 | $conn_list = $serv->getClientList($start_fd, 10); 146 | if ($conn_list===false or count($conn_list) === 0) { 147 | echo "BroadCast finish\n"; 148 | break; 149 | } 150 | $start_fd = end($conn_list); 151 | foreach($conn_list as $fd) { 152 | //获取客户端信息 153 | $client_info = $serv->getClientInfo($fd); 154 | $client[$fd] = $client_info; 155 | if(isset($client_info['websocket_status']) && $client_info['websocket_status'] == 3) { 156 | $serv->push($fd, $data, WEBSOCKET_OPCODE_BINARY); 157 | } 158 | } 159 | } 160 | return $client; 161 | } 162 | 163 | /** 164 | * 对多用发送信息 165 | * @param $serv 166 | * @param $users 167 | * @param $data 168 | */ 169 | protected function pushToUsers($serv, $users, $data) 170 | { 171 | foreach($users as $fd) { 172 | //获取客户端信息 173 | $client_info = $serv->getClientInfo($fd); 174 | $client[$fd] = $client_info; 175 | if(isset($client_info['websocket_status']) && $client_info['websocket_status'] == 3) { 176 | $serv->push($fd, $data, WEBSOCKET_OPCODE_BINARY); 177 | } 178 | } 179 | } 180 | 181 | /** 182 | * 通过accounts获取fds 183 | * @param $account 184 | * @return array 185 | */ 186 | private function _getRoomFds($account) 187 | { 188 | $game_conf = config('game'); 189 | $user_room_data = $game_conf['user_room_data']; 190 | $uinfo = Redis::hGet($user_room_data, $account); 191 | $uinfo = json_decode($uinfo, true); 192 | $accs = isset($uinfo['account']) ? $uinfo['account'] : array(); 193 | $binds = $fds = array(); 194 | if(!empty($accs)) { 195 | foreach($accs as $v) { 196 | $binds[] = sprintf($game_conf['user_bind_key'], $v); 197 | } 198 | $fds = Redis::mget($binds); 199 | } 200 | return $fds; 201 | } 202 | 203 | /** 204 | * 把php数组存入redis的hash表中 205 | * @param $arr 206 | * @param $hash_key 207 | */ 208 | protected function arrToHashInRedis($arr, $hash_key) 209 | { 210 | foreach($arr as $key=>$val) { 211 | Redis::hSet($hash_key,$key, json_encode($val)); 212 | } 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /app/Task/Task/SyncTask.php: -------------------------------------------------------------------------------- 1 | [1, 3, 3], 30 | 'id' => $id, 31 | 'default' => $default 32 | ]; 33 | } 34 | 35 | /** 36 | * @TaskMapping() 37 | * 38 | * @param int $id 39 | * 40 | * @return bool 41 | */ 42 | public function delete(int $id): bool 43 | { 44 | if ($id > 10) { 45 | return true; 46 | } 47 | 48 | return false; 49 | } 50 | 51 | /** 52 | * @TaskMapping() 53 | * 54 | * @param string $name 55 | * 56 | * @return null 57 | */ 58 | public function returnNull(string $name) 59 | { 60 | return null; 61 | } 62 | 63 | /** 64 | * @TaskMapping() 65 | * 66 | * @param string $name 67 | */ 68 | public function returnVoid(string $name): void 69 | { 70 | return; 71 | } 72 | } -------------------------------------------------------------------------------- /app/Validator/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jxy918/swoft-ddz/07c42b2437691fa502e6891384f9bd4ce7d79801/app/Validator/.gitkeep -------------------------------------------------------------------------------- /app/WebSocket/GameModule.php: -------------------------------------------------------------------------------- 1 | getQueryParams(); 52 | $cookie = $request->getCookieParams(); 53 | $token = ''; 54 | if(isset($cookie['USER_INFO'])) { 55 | $token = $cookie['USER_INFO']; 56 | } elseif(isset($query['token'])) { 57 | $token = $query['token']; 58 | } 59 | $server = server(); 60 | if($token) { 61 | $uinfo = json_decode($token, true); 62 | //允许连接, 并记录用户信息 63 | $uinfo['fd'] = $fd; 64 | $user_bind_key = sprintf($game_conf['user_bind_key'], $uinfo['account']); 65 | $last_fd = (int)Redis::get($user_bind_key); 66 | //之前信息存在, 清除之前的连接 67 | if($last_fd) { 68 | //处理双开的情况 69 | $this->loginFail($last_fd, '1'); 70 | $server->disconnect($last_fd); 71 | //清理redis. 72 | Redis::del($user_bind_key); //清除上一个绑定关系 73 | Redis::del(sprintf($game_conf['user_info_key'], $last_fd)); //清除上一个用户信息 74 | } 75 | //保存登陆信息 76 | Redis::set($user_bind_key, $fd, $game_conf['expire']); 77 | //设置绑定关系 78 | Redis::set(sprintf($game_conf['user_info_key'], $fd), json_encode($uinfo), $game_conf['expire']); 79 | $this->loginSuccess($server, $fd, $uinfo['account']); //登陆成功 80 | } else { 81 | $this->loginFail($fd, '2'); 82 | $server->disconnect($fd); 83 | } 84 | } 85 | 86 | /** 87 | * @OnMessage() 88 | * @param Server $server 89 | * @param Frame $frame 90 | */ 91 | public function onMessage(Server $server, Frame $frame) 92 | { 93 | Log::show(" Message: client #{$frame->fd} push success Mete: \n{"); 94 | $data = Packet::packDecode($frame->data); 95 | if(isset($data['code']) && $data['code'] == 0 && isset($data['msg']) && $data['msg'] == 'OK') { 96 | Log::show('Recv <<< cmd='.$data['cmd'].' scmd='.$data['scmd'].' len='.$data['len'].' data='.json_encode($data['data'])); 97 | //转发请求,代理模式处理,websocket路由到相关逻辑 98 | $data['serv'] = $server; 99 | //用户登陆信息 100 | $game_conf = config('game'); 101 | $user_info_key = sprintf($game_conf['user_info_key'], $frame->fd); 102 | $uinfo = Redis::get($user_info_key); 103 | if($uinfo) { 104 | $data['userinfo'] = json_decode($uinfo, true); 105 | } else { 106 | $data['userinfo'] = array(); 107 | } 108 | $obj = new Dispatch($data); 109 | $back = "

404 Not Found


swoft
\n"; 110 | if(!empty($obj->getStrategy())) { 111 | $back = $obj->exec(); 112 | if($back) { 113 | $server->push($frame->fd, $back, WEBSOCKET_OPCODE_BINARY); 114 | } 115 | } 116 | Log::show('Tcp Strategy <<< data='.$back); 117 | } else { 118 | Log::show($data['msg']); 119 | } 120 | Log::split('}'); 121 | } 122 | 123 | /** 124 | * On connection closed 125 | * - you can do something. eg. record log 126 | * 127 | * @OnClose() 128 | * @param Server $server 129 | * @param int $fd 130 | */ 131 | public function onClose(Server $server, int $fd): void 132 | { 133 | //清除登陆信息变量 134 | $this->loginFail($fd, '3'); 135 | } 136 | 137 | /** 138 | * 登陆成功下发协议 139 | * @param $server 140 | * @param $fd 141 | * @param $account 142 | */ 143 | private function loginSuccess($server, $fd, $account) 144 | { 145 | //原封不动发回去 146 | if($server->getClientInfo($fd) !== false) { 147 | //查询用户是否在房间里面 148 | $info = $this->getRoomData($account); 149 | $data = array('status'=>'success'); 150 | if(!empty($info)) { 151 | $data['is_room'] = 1; 152 | } else { 153 | $data['is_room'] = 0; 154 | } 155 | $data = Packet::packFormat('OK', 0, $data); 156 | $back = Packet::packEncode($data, MainCmd::CMD_SYS, SubCmd::LOGIN_SUCCESS_RESP); 157 | $server->push($fd, $back, WEBSOCKET_OPCODE_BINARY); 158 | } 159 | } 160 | 161 | /** 162 | * 发送登陆失败请求到客户端 163 | * @param $fd 164 | * @param string $msg 165 | */ 166 | private function loginFail($fd, $msg = '') 167 | { 168 | //原封不动发回去 169 | $server = server(); 170 | if($server->getClientInfo($fd) !== false) { 171 | $data = Packet::packFormat('OK', 0, array('data' => 'login fail'.$msg)); 172 | $back = Packet::packEncode($data, MainCmd::CMD_SYS, SubCmd::LOGIN_FAIL_RESP); 173 | $server->push($fd, $back, WEBSOCKET_OPCODE_BINARY); 174 | } 175 | } 176 | 177 | /** 178 | * 获取房间信息 179 | * @param $account 180 | * @return array 181 | */ 182 | protected function getRoomData($account) 183 | { 184 | $user_room_data = array(); 185 | //获取用户房间号 186 | $room_no = $this->getRoomNo($account); 187 | //房间信息 188 | $game_key = $this->getGameConf('user_room_data'); 189 | if($game_key) { 190 | $user_room_key = sprintf($game_key, $room_no); 191 | $user_room_data = Redis::hGetAll($user_room_key); 192 | } 193 | return $user_room_data; 194 | } 195 | 196 | /** 197 | * 获取用户房间号 198 | * @param $account 199 | * @return mixed 200 | */ 201 | protected function getRoomNo($account) 202 | { 203 | $game_key = $this->getGameConf('user_room'); 204 | //获取用户房间号 205 | $room_key = sprintf($game_key, $account); 206 | $room_no = Redis::get($room_key); 207 | return $room_no ? $room_no : 0; 208 | } 209 | 210 | /** 211 | * 返回游戏配置 212 | * @param string $key 213 | * @return string 214 | */ 215 | protected function getGameConf($key = '') { 216 | $conf = config('game'); 217 | if(isset($conf[$key])) { 218 | return $conf[$key]; 219 | } else { 220 | return ''; 221 | } 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /app/WebSocket/VedioModule.php: -------------------------------------------------------------------------------- 1 | fd}...data:".$frame->data."\n"; 68 | if(!is_numeric($frame->data)) { 69 | //如果是录像数据, 发送二进制数据 70 | $ret = server()->sendToAll($frame->data, 0, 50); 71 | var_dump($ret); 72 | } 73 | } 74 | 75 | 76 | /** 77 | * On connection closed 78 | * - you can do something. eg. record log 79 | * 80 | * @OnClose() 81 | * @param Server $server 82 | * @param int $fd 83 | */ 84 | public function onClose(Server $server, int $fd): void 85 | { 86 | //清除登陆信息变量 87 | echo "vedio close #{$fd}..."; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /app/bean.php: -------------------------------------------------------------------------------- 1 | [ 21 | 'path' => __DIR__ . '/../config', 22 | // 'env' => 'dev', 23 | ], 24 | 'httpServer' => [ 25 | 'class' => HttpServer::class, 26 | 'port' => 18306, 27 | 'listener' => [ 28 | 'rpc' => bean('rpcServer') 29 | ], 30 | 'process' => [ 31 | // 'monitor' => bean(MonitorProcess::class) 32 | ], 33 | 'on' => [ 34 | SwooleEvent::TASK => bean(SyncTaskListener::class), // Enable sync task 35 | SwooleEvent::TASK => bean(TaskListener::class), // Enable task must task and finish event 36 | SwooleEvent::FINISH => bean(FinishListener::class) 37 | ], 38 | /* @see HttpServer::$setting */ 39 | 'setting' => [ 40 | 'task_worker_num' => 12, 41 | 'task_enable_coroutine' => true 42 | ] 43 | ], 44 | 'httpDispatcher' => [ 45 | // Add global http middleware 46 | 'middlewares' => [ 47 | // Allow use @View tag 48 | // \Swoft\View\Middleware\ViewMiddleware::class, 49 | \App\Http\Middleware\SomeMiddleware::class, 50 | // \Swoft\Smarty\Middleware\SmartyMiddleware::class 51 | ], 52 | ], 53 | 'db' => [ 54 | 'class' => Database::class, 55 | 'dsn' => 'mysql:dbname=accounts_mj;host=192.168.22.34:3307', 56 | 'username' => 'web', 57 | 'password' => '111www', 58 | 'charset' => 'utf8', 59 | ], 60 | 'db.pool' => [ 61 | 'class' => Pool::class, 62 | 'database' => bean('db'), 63 | 'minActive' => 10, 64 | 'maxActive' => 20, 65 | 'maxWait' => 0, 66 | 'maxWaitTime' => 0, 67 | 'maxIdleTime' => 60, 68 | ], 69 | 'migrationManager' => [ 70 | 'migrationPath' => '@app/Migration', 71 | ], 72 | 'redis' => [ 73 | 'class' => RedisDb::class, 74 | 'host' => 'redis', 75 | 'port' => 6379, 76 | 'database' => 0, 77 | 'option' => [ 78 | 'prefix' => '' 79 | ] 80 | ], 81 | 'redis.pool' => [ 82 | 'class' => \Swoft\Redis\Pool::class, 83 | 'redisDb' => \bean('redis'), 84 | 'minActive' => 10, 85 | 'maxActive' => 20, 86 | 'maxWait' => 0, 87 | 'maxWaitTime' => 0, 88 | 'maxIdleTime' => 60, 89 | ], 90 | 'user' => [ 91 | 'class' => ServiceClient::class, 92 | 'host' => '127.0.0.1', 93 | 'port' => 18307, 94 | 'setting' => [ 95 | 'timeout' => 0.5, 96 | 'connect_timeout' => 1.0, 97 | 'write_timeout' => 10.0, 98 | 'read_timeout' => 0.5, 99 | ], 100 | 'packet' => bean('rpcClientPacket') 101 | ], 102 | 'user.pool' => [ 103 | 'class' => ServicePool::class, 104 | 'client' => bean('user') 105 | ], 106 | 'rpcServer' => [ 107 | 'class' => ServiceServer::class, 108 | ], 109 | 'wsServer' => [ 110 | 'class' => WebSocketServer::class, 111 | 'port' => 18308, 112 | 'on' => [ 113 | // 开启处理http请求支持 114 | SwooleEvent::REQUEST => bean(RequestListener::class), 115 | // 启用任务必须添加 task, finish 事件处理 116 | SwooleEvent::TASK => bean(TaskListener::class), 117 | SwooleEvent::FINISH => bean(FinishListener::class) 118 | ], 119 | 'listener' => [ 120 | // 引入 tcpServer 121 | 'tcp' => \bean('tcpServer') 122 | ], 123 | 'debug' => env('SWOFT_DEBUG', 0), 124 | /* @see WebSocketServer::$setting */ 125 | 'setting' => [ 126 | 'log_file' => alias('@runtime/swoole.log'), 127 | 'document_root' => dirname(__DIR__) . '/public/', 128 | 'enable_static_handler' => true, 129 | 'worker_num' => 2, 130 | // 任务需要配置 task worker 131 | 'task_worker_num' => 4, 132 | 'task_enable_coroutine' => true, 133 | 'max_request' => 10000, 134 | 'package_max_length' => 20480 135 | ], 136 | ], 137 | 'tcpServer' => [ 138 | 'port' => 18309, 139 | 'debug' => 1, 140 | 'on' => [ 141 | SwooleEvent::RECEIVE => bean(App\Common\TcpReceiveListener::class) 142 | ], 143 | 'setting' => [ 144 | 'log_file' => alias('@runtime/swoole.log'), 145 | 'worker_num' => 2, 146 | // 任务需要配置 task worker 147 | 'task_worker_num' => 4, 148 | 'task_enable_coroutine' => true, 149 | 'max_request' => 10000, 150 | 'package_max_length' => 20480 151 | ], 152 | ], 153 | 'cliRouter' => [ 154 | // 'disabledGroups' => ['demo', 'test'], 155 | ], 156 | 'processPool' => [ 157 | 'class' => \Swoft\Process\ProcessPool::class, 158 | 'workerNum' => 3 159 | ], 160 | 'lineFormatter' => [ 161 | 'format' => '%datetime% [%level_name%] [%channel%] [%event%] [tid:%tid%] [cid:%cid%] [traceid:%traceid%] [spanid:%spanid%] [parentid:%parentid%] %messages%', 162 | 'dateFormat' => 'Y-m-d H:i:s', 163 | ], 164 | 'noticeHandler' => [ 165 | 'class' => \Swoft\Log\Handler\FileHandler::class, 166 | 'logFile' => '@runtime/logs/notice.log', 167 | 'formatter' => \bean('lineFormatter'), 168 | 'levels' => 'notice,info,debug,trace', 169 | ], 170 | 'applicationHandler' => [ 171 | 'class' => \Swoft\Log\Handler\FileHandler::class, 172 | 'logFile' => '@runtime/logs/error.log', 173 | 'formatter' => \bean('lineFormatter'), 174 | 'levels' => 'error,warning', 175 | ], 176 | 'logger' => [ 177 | 'flushRequest' => false, 178 | 'enable' => true, 179 | 'handlers' => [ 180 | 'application' => \bean('applicationHandler'), 181 | 'notice' => \bean('noticeHandler'), 182 | ], 183 | ] 184 | ]; 185 | -------------------------------------------------------------------------------- /bin/bootstrap.php: -------------------------------------------------------------------------------- 1 | 300000, 9 | ]); 10 | 11 | // Run application 12 | (new \App\Application())->run(); 13 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "swoft/swoft", 3 | "type": "project", 4 | "keywords": [ 5 | "php", 6 | "swoole", 7 | "swoft", 8 | "swoft-game" 9 | ], 10 | "description": "Modern High performance AOP and Coroutine PHP Framework", 11 | "license": "Apache-2.0", 12 | "require": { 13 | "php": ">7.1", 14 | "ext-pdo": "*", 15 | "ext-redis": "*", 16 | "ext-json": "*", 17 | "ext-mbstring": "*", 18 | "swoft/db": "~2.0.0", 19 | "swoft/i18n": "~2.0.0", 20 | "swoft/view": "~2.0.0", 21 | "swoft/task": "~2.0.0", 22 | "swoft/redis": "~2.0.0", 23 | "swoft/framework": "~2.0.0", 24 | "swoft/http-server": "~2.0.0", 25 | "swoft/rpc-client": "~2.0.0", 26 | "swoft/rpc-server": "~2.0.0", 27 | "swoft/websocket-server": "~2.0.0", 28 | "swoft/tcp-server": "~2.0.0", 29 | "swoft/process": "^2.0", 30 | "swoft/apollo": "~2.0.0", 31 | "swoft/consul": "~2.0.0", 32 | "swoft/limiter": "~2.0.0", 33 | "swoft/breaker": "~2.0.0", 34 | "swoft/devtool": "~2.0.0", 35 | "php-amqplib/php-amqplib": "^2.9", 36 | "rybakit/msgpack": "^0.5.4" 37 | }, 38 | "require-dev": { 39 | "swoft/swoole-ide-helper": "dev-master", 40 | "phpunit/phpunit": "^7.5" 41 | }, 42 | "autoload": { 43 | "psr-4": { 44 | "App\\": "app/" 45 | }, 46 | "files": [ 47 | "app/Helper/Functions.php" 48 | ] 49 | }, 50 | "autoload-dev": { 51 | "psr-4": { 52 | "AppTest\\Testing\\": "test/testing", 53 | "AppTest\\Unit\\": "test/unit" 54 | } 55 | }, 56 | "scripts": { 57 | "post-root-package-install": [ 58 | "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" 59 | ], 60 | "test": "./vendor/bin/phpunit -c phpunit.xml", 61 | "cs-fix": "./vendor/bin/php-cs-fixer fix $1" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /config/base.php: -------------------------------------------------------------------------------- 1 | 'Swoft framework 2.0', 4 | 'debug' => env('SWOFT_DEBUG', 0), 5 | ]; 6 | -------------------------------------------------------------------------------- /config/game.php: -------------------------------------------------------------------------------- 1 | 'user:info:%s', //用户信息redis的key,fd对应用户信息 8 | 'user_bind_key' => 'user:bind:%s', //用户绑定信息和fd绑定key,里面存是根据fd存入account和fd绑定关系 9 | 'expire' => 1 * 24 * 60 * 60, //设置key过期时间, 设置为1天 10 | 'room_list'=> 'user:room:list', //用户进入房间队列 11 | 'user_room_no' => 'user:room:no', //用户自增房间号 12 | 'user_room' => 'user:room:map:%s', //用户和房间映射关系 13 | 'user_room_data' => 'user:room:data:%s', //用户游戏房间数据 14 | 'user_room_play' => 'user:room:play:%s', //用户游戏房间打牌步骤数据 15 | ]; -------------------------------------------------------------------------------- /images/ai.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jxy918/swoft-ddz/07c42b2437691fa502e6891384f9bd4ce7d79801/images/ai.jpg -------------------------------------------------------------------------------- /images/demo1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jxy918/swoft-ddz/07c42b2437691fa502e6891384f9bd4ce7d79801/images/demo1.png -------------------------------------------------------------------------------- /images/demo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jxy918/swoft-ddz/07c42b2437691fa502e6891384f9bd4ce7d79801/images/demo2.png -------------------------------------------------------------------------------- /public/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jxy918/swoft-ddz/07c42b2437691fa502e6891384f9bd4ce7d79801/public/.keep -------------------------------------------------------------------------------- /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 | CHAT_MSG_REQ : 213, //聊天消息请求,客户端使用 33 | CHAT_MSG_RESP : 214, //聊天消息响应,服务端使用 34 | } 35 | 36 | 37 | /** 38 | * 路由规则,key主要命令字=》array(子命令字对应策略类名) 39 | * 每条客户端对应的请求,路由到对应的逻辑处理类上处理 40 | * 41 | */ 42 | var Route = { 43 | 1 : { 44 | 100 : 'loginFail', //登陆失败 45 | 105 : 'loginSuccess', //登陆成功 46 | 102 : 'heartAsk', //心跳处理 47 | 104 : 'broadcast', //广播消息 48 | 106 : 'enterRoomFail', //进入房间失败 49 | 107 : 'enterRoomSucc', //进入房间成功 50 | }, 51 | 2 : { 52 | 2 : 'gameStart', //获取卡牌 53 | 214 : 'chatMsg', 54 | 3 : 'userInfo', //显示用户信息 55 | 5 : 'gameCallTips', //叫地主广播 56 | 7 : 'gameCall', //叫地主返回 57 | 11 : 'gameCatchCardTips', //摸底牌 58 | 12 : 'gameOutCard', //出牌广播 59 | 14 : 'gameOutCardResp', //出牌响应 60 | }, 61 | } 62 | 63 | /** 64 | * 花色类型 65 | */ 66 | var CardType = { 67 | HEITAO : 0, //黑桃 68 | HONGTAO : 1, //红桃 69 | MEIHUA : 2, //梅花 70 | FANGKUAI : 3, //方块 71 | XIAOWANG : 4, //小王 72 | DAWANG : 5, //大王 73 | } 74 | /** 75 | * 牌显示出来的值 76 | */ 77 | var CardVal = { 78 | CARD_SAN : '3', //牌值3 79 | CARD_SI : '4', //牌值4 80 | CARD_WU : '5', //牌值5 81 | CARD_LIU : '6', //牌值6 82 | CARD_QI : '7', //牌值7 83 | CARD_BA : '8', //牌值8 84 | CARD_JIU : '9', //牌值9 85 | CARD_SHI : '10', //牌值10 86 | CARD_J : 'J', //牌值J 87 | CARD_Q : 'Q', //牌值Q 88 | CARD_K : 'K', //牌值K 89 | CARD_A : 'A', //牌值A 90 | CARD_ER : '2', //牌值2 91 | CARD_XIAOWANG : 'w', //牌值小王 92 | CARD_DAWANG : 'W', //牌值大王 93 | } -------------------------------------------------------------------------------- /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 | GameStart: function(obj,data) { 22 | var data = {}; 23 | obj.send(data, MainCmd.CMD_GAME, SubCmd.SUB_GAME_START_REQ); 24 | }, 25 | 26 | //抢地主 27 | GameCall: function(obj,status) { 28 | var data = {"type": status}; 29 | obj.send(data, MainCmd.CMD_GAME, SubCmd.SUB_GAME_CALL_REQ); 30 | }, 31 | 32 | //玩游戏 33 | PlayGame: function(obj,data) { 34 | obj.send(data, MainCmd.CMD_GAME, SubCmd.SUB_GAME_OUT_CARD_REQ); 35 | }, 36 | 37 | //聊天消息 38 | ChatMsg: function(obj, data) { 39 | var data = {data}; 40 | obj.send(data, MainCmd.CMD_GAME, SubCmd.CHAT_MSG_REQ); 41 | }, 42 | } -------------------------------------------------------------------------------- /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 | }, 74 | 75 | //进入房间失败 76 | enterRoomFail: function(data) { 77 | this.log(data); 78 | this.showTips('进入房间失败:'+data.msg); 79 | }, 80 | 81 | //进入房间成功,解锁按钮 82 | enterRoomSucc: function(data) { 83 | this.log(data); 84 | this.showTips('进入房间成功:'+JSON.stringify(data)); 85 | var card = data.card; 86 | var chair_id = data.chair_id; 87 | var is_master = data.is_master; 88 | var is_game_over = data.is_game_over; 89 | info = data 90 | if(typeof(data.calltype) == 'undefined') { 91 | document.getElementById('call').disabled = false; 92 | document.getElementById('nocall').disabled = false; 93 | } else { 94 | document.getElementById('call').disabled = true; 95 | document.getElementById('nocall').disabled = true; 96 | } 97 | 98 | //显示牌 99 | if(card && chair_id) { 100 | //循环展现牌 101 | var show_card = ''; 102 | for(var k in card) { 103 | show_card += ''+this.getCard(card[k])+''; 104 | } 105 | var id = 'chair_'+chair_id; 106 | document.getElementById(id).innerHTML = show_card; 107 | } 108 | 109 | //是否为地主 110 | if(is_master == 1) { 111 | if(typeof(data.master) != 'undefined') { 112 | document.getElementById('master').innerHTML = '(地主)-'+chair_id+'号位置'; 113 | } else { 114 | document.getElementById('master').innerHTML = '(农民)-'+chair_id+'号位置'; 115 | } 116 | } 117 | 118 | //判断游戏是否结束 119 | if(is_game_over) { 120 | this.showTips('游戏结束'); 121 | } else { 122 | //轮到谁出来, 就解锁谁的按钮 123 | if(typeof(data.index_chair_id) != 'undefined' && info.chair_id == data.index_chair_id) { 124 | //解锁打牌按钮 125 | document.getElementById('play').disabled = false; 126 | document.getElementById('pass').disabled = false; 127 | var tips = data.is_first_round ? '请首次出牌' : '请跟牌'; 128 | this.showTips(tips); 129 | } else { 130 | document.getElementById('play').disabled = true; 131 | document.getElementById('pass').disabled = true; 132 | } 133 | } 134 | }, 135 | 136 | //出牌提示 137 | gameOutCard: function(data) { 138 | this.log(data); 139 | this.showTips('出牌提示:'+data.msg); 140 | if(data.status == 0) { 141 | //移除当前牌元素 142 | var obj_box = document.getElementsByName("handcard"); 143 | var obj_item = []; 144 | for(k in obj_box){ 145 | if(obj_box[k].checked){ 146 | obj_item[k] = obj_box[k].parentNode; 147 | } 148 | } 149 | //循环删除 150 | for(k in obj_item){ 151 | obj_item[k].remove(this); 152 | } 153 | } 154 | }, 155 | 156 | //出牌广播响应 157 | gameOutCardResp: function(data) { 158 | //判断游戏是否结束 159 | if(data.is_game_over) { 160 | this.showTips('广播:游戏结束,'+data.account+'胜利, 请点击"开始游戏",进行下一轮游戏'); 161 | //手牌重置 162 | document.getElementById('chair_1').innerHTML = ''; 163 | document.getElementById('chair_2').innerHTML = ''; 164 | document.getElementById('chair_3').innerHTML = ''; 165 | document.getElementById('last_card').innerHTML = ''; 166 | document.getElementById('out_card').innerHTML = ''; 167 | document.getElementById('play').disabled = true; 168 | document.getElementById('pass').disabled = true; 169 | } else { 170 | this.log(data); 171 | var play = data.show_type == 1 ? '跟牌' : '过牌'; 172 | if(data.last_card == null || data.last_card.length < 1) { 173 | play = '出牌'; 174 | } 175 | this.showTips('广播: 第'+data.round+'回合,第'+data.hand_num+'手出牌, '+data.account+play+', 上次牌值是'+data.last_card+', 本次出牌值是'+data.card+', 本次出牌型是'+data.card_type); 176 | this.showPlayCard(data.last_card,data.card); 177 | 178 | //自己出牌按钮变灰 179 | if(info.chair_id == data.next_chair_id) { 180 | document.getElementById('play').disabled = false; 181 | document.getElementById('pass').disabled = false; 182 | //提示下一个跟牌操作 183 | var tips = data.is_first_round ? '请首次出牌' : '请跟牌'; 184 | this.showTips(tips); 185 | } else { 186 | document.getElementById('play').disabled = true; 187 | document.getElementById('pass').disabled = true; 188 | } 189 | } 190 | 191 | }, 192 | 193 | //广播消息响应 194 | broadcast: function(data) { 195 | this.log(data); 196 | this.showTips("广播:消息,"+JSON.stringify(data)); 197 | }, 198 | 199 | //显示打牌过程 200 | showPlayCard: function(last_card, out_card) { 201 | document.getElementById('last_card').innerHTML = ''; 202 | document.getElementById('out_card').innerHTML = ''; 203 | if(last_card != null && typeof(last_card) == 'object' && last_card.length > 0) { 204 | var l = ''; 205 | for(k in last_card) { 206 | l += ''+this.getCard(last_card[k])+''; 207 | } 208 | document.getElementById('last_card').innerHTML = l; 209 | } 210 | if(out_card != null && typeof(out_card) == 'object' && out_card.length > 0) { 211 | var n = ''; 212 | for(j in out_card) { 213 | n += ''+this.getCard(out_card[j])+''; 214 | } 215 | document.getElementById('out_card').innerHTML = n; 216 | } 217 | 218 | }, 219 | 220 | //构造牌 221 | getCard: function(card_val) { 222 | var card = ''; 223 | var color = parseInt(card_val / 16); 224 | if(color == CardType.HEITAO) { 225 | card += '♠'; 226 | } else if(color == CardType.HONGTAO) { 227 | card += ''; 228 | } else if(color == CardType.MEIHUA) { 229 | card += '♣'; 230 | } else if(color == CardType.FANGKUAI) { 231 | card += ''; 232 | } else if(color == CardType.XIAOWANG) { 233 | if(card_val == 78) { 234 | card += 's'; 235 | } else if(card_val == 79) { 236 | card += 'B'; 237 | } 238 | } 239 | 240 | if(card_val == 78) { 241 | card +='_'+CardVal.CARD_XIAOWANG; 242 | } else if(card_val == 79) { 243 | card +='_'+CardVal.CARD_DAWANG; 244 | } else { 245 | //牌值渲染 246 | var value = parseInt(card_val % 16); 247 | switch(value) { 248 | case 1: 249 | card +='_'+CardVal.CARD_SAN; 250 | break; 251 | case 2: 252 | card +='_'+CardVal.CARD_SI; 253 | break; 254 | case 3: 255 | card +='_'+CardVal.CARD_WU; 256 | break; 257 | case 4: 258 | card +='_'+CardVal.CARD_LIU; 259 | break; 260 | case 5: 261 | card +='_'+CardVal.CARD_QI; 262 | break; 263 | case 6: 264 | card +='_'+CardVal.CARD_BA; 265 | break; 266 | case 7: 267 | card +='_'+CardVal.CARD_JIU; 268 | break; 269 | case 8: 270 | card +='_'+CardVal.CARD_SHI; 271 | break; 272 | case 9: 273 | card +='_'+CardVal.CARD_J; 274 | break; 275 | case 10: 276 | card +='_'+CardVal.CARD_Q; 277 | break; 278 | case 11: 279 | card +='_'+CardVal.CARD_K; 280 | break; 281 | case 12: 282 | card +='_'+CardVal.CARD_A; 283 | break; 284 | case 13: 285 | card +='_'+CardVal.CARD_ER; 286 | break; 287 | } 288 | } 289 | return card; 290 | }, 291 | 292 | //日志显示协议返回数据 293 | log: function(data) { 294 | //document.getElementById('msgText').innerHTML += JSON.stringify(data) + '\n'; 295 | console.log(data); 296 | }, 297 | 298 | //显示提示语句 299 | showTips: function(tips) { 300 | document.getElementById('msgText').innerHTML += tips + '\n'; 301 | } 302 | } -------------------------------------------------------------------------------- /public/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Testing swoole-h5game 6 | 7 | 8 |
9 |

swoole-h5game Test工具

10 |
11 | 12 | 服务器: 13 | 14 | 15 | 16 |
17 |
18 |
19 | 20 |
21 |
22 |
23 | 24 | 昵称:    25 |        26 |     27 | 28 |
29 |
30 |
31 | 请求类型: 40 | 41 | 请求数据:  42 | 43 |
44 |
45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 91 | -------------------------------------------------------------------------------- /resource/views/game/game.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 这是一个斗地主打牌测试工具 6 | 7 | 28 | 29 |
30 |

昵称:

31 |
32 | 服务器链接: 33 | 34 | 35 |
36 |
37 |
38 | 39 | 40 | 41 | 42 | 43 |
44 |
45 | 我的手牌: 46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | 上次出牌: 56 |
57 |
58 |
59 |
60 |
61 | 本次出牌: 62 |
63 |
64 |
65 |
66 |
67 |
68 | 69 |
70 |
71 |
72 | 发送消息: 73 | 74 | 75 | 76 |
77 |
78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 153 | 154 | -------------------------------------------------------------------------------- /resource/views/game/login.php: -------------------------------------------------------------------------------- 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 | 46 | -------------------------------------------------------------------------------- /resource/views/game/test.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Testing swoole-h5game 6 | 7 | 8 |
9 |

swoole-h5game Test工具

10 |
11 | 12 | 服务器: 13 | 14 | 15 | 16 |
17 |
18 |
19 | 20 |
21 |
22 |
23 | 24 | 昵称:    25 |        26 |     27 | 28 |
29 |
30 |
31 | 请求类型: 36 | 37 | 请求数据:  38 | 39 |
40 |
41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 87 | 88 | -------------------------------------------------------------------------------- /resource/views/vedio/camera.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 录像页面 5 | 6 | 7 | 8 | 9 | 10 |
11 | 提示:最好用火狐测试,谷歌浏览器升级了安全策略,谷歌浏览器只能在https下才能利用html5打开摄像头。 12 | 13 | 106 | 107 | -------------------------------------------------------------------------------- /resource/views/vedio/show.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 录像显示页面 5 | 6 | 7 | 8 | 9 | 10 |

如果显示空白,说明当前没有人在直播,点击这里直播 11 | 77 | 78 | -------------------------------------------------------------------------------- /test/ai.php: -------------------------------------------------------------------------------- 1 | true, 24 | ); 25 | 26 | /** 27 | * 客户端头部设置 28 | * @var array 29 | */ 30 | private $_header = array( 31 | 'UserAgent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36', 32 | ); 33 | 34 | /** 35 | * 用户登陆账号 36 | * @var string 37 | */ 38 | public $account = ''; 39 | 40 | /** 41 | * 心跳定时器 42 | * @var int 43 | */ 44 | public $heart_timer = 0; 45 | 46 | /** 47 | * 心跳定时器间隔时间(毫秒) 48 | * @var int 49 | */ 50 | public $heart_interval = 60000; 51 | 52 | /** 53 | * 断线重连定时器 54 | * @var int 55 | */ 56 | public $reback_timer = 0; 57 | 58 | /** 59 | * 断线重连次数 60 | * @var int 61 | */ 62 | public $reback_times = 10; 63 | 64 | /** 65 | * 断线重连计数器 66 | * @var int 67 | */ 68 | public $reback_count = 0; 69 | 70 | /** 71 | * 断线重连间隔时间(毫秒) 72 | * @var int 73 | */ 74 | public $reback_interval = 2000; 75 | 76 | /** 77 | * 椅子id 78 | * @var array 79 | */ 80 | public $chair_id = 0; 81 | 82 | /** 83 | * 手牌数据 84 | * @var array 85 | */ 86 | public $hand_card = array(); 87 | 88 | /** 89 | * 房间信息 90 | * @var array 91 | */ 92 | public $my_room_info = array(); 93 | 94 | /** 95 | * 手牌对象 96 | * @var null 97 | */ 98 | public $ddz = null; 99 | 100 | /** 101 | * 路由规则 102 | * @var array 103 | */ 104 | public $route = array( 105 | //系统请求响应 106 | App\Game\Conf\MainCmd::CMD_SYS => array( 107 | \App\Game\Conf\SubCmd::LOGIN_FAIL_RESP =>'loginFailResp', //登录失败响应 108 | \App\Game\Conf\SubCmd::LOGIN_SUCCESS_RESP =>'loginSucessResp', //登录成功响应 109 | \App\Game\Conf\SubCmd::HEART_ASK_RESP =>'heartAskResp', //心跳响应 110 | \App\Game\Conf\SubCmd::ENTER_ROOM_FAIL_RESP =>'enterRoomFailResp', //进入房间失败响应 111 | \App\Game\Conf\SubCmd::ENTER_ROOM_SUCC_RESP =>'enterRoomSuccResp', //进入房间成功响应 112 | ), 113 | //游戏请求响应 114 | App\Game\Conf\MainCmd::CMD_GAME => array( 115 | \App\Game\Conf\SubCmd::SUB_GAME_START_RESP =>'gameStartResp', //游戏开始响应 116 | \App\Game\Conf\SubCmd::SUB_USER_INFO_RESP =>'userInfoResp', //用户信息响应 117 | \App\Game\Conf\SubCmd::CHAT_MSG_RESP =>'chatMsgResp', //聊天,消息响应 118 | \App\Game\Conf\SubCmd::SUB_GAME_CALL_TIPS_RESP =>'gameCallTipsResp', //叫地主广播响应 119 | \App\Game\Conf\SubCmd::SUB_GAME_CALL_RESP =>'gameCallResp', //叫地主响应 120 | \App\Game\Conf\SubCmd::SUB_GAME_CATCH_BASECARD_RESP =>'catchGameCardResp', //摸牌广播响应 121 | \App\Game\Conf\SubCmd::SUB_GAME_OUT_CARD =>'gameOutCard', //出牌广播 122 | \App\Game\Conf\SubCmd::SUB_GAME_OUT_CARD_RESP =>'gameOutCardResp', //出牌响应 123 | ), 124 | ); 125 | 126 | /** 127 | * 构造函数 128 | * Ai constructor. 129 | * @param string $account 130 | */ 131 | public function __construct($account = ''){ 132 | if($account) { 133 | $this->account = $account; 134 | } 135 | } 136 | 137 | /** 138 | * 运行服务器 139 | */ 140 | public function run(){ 141 | if($this->account) { 142 | $this->createConnection(); 143 | } else { 144 | \App\Game\Core\Log::show("账号错误!"); 145 | } 146 | } 147 | 148 | /** 149 | * 创建链接 150 | */ 151 | protected function createConnection() { 152 | go(function () { 153 | $cli = new \Swoole\Coroutine\Http\Client(self::IP, self::PORT); 154 | $cli->set($this->_setconfig); 155 | $cli->setHeaders($this->_header); 156 | $cli->setMethod("GET"); 157 | $self = $this; 158 | $data = array('account' => $this->account); 159 | $token = json_encode($data); 160 | $ret = $cli->upgrade('/game?token=' . $token); 161 | if($ret && $cli->connected) { 162 | //清除断线重连定时器, 断线重连次数重置为0 163 | Swoole\Timer::clear($this->reback_timer); 164 | $this->reback_count = 0; 165 | // $self->chatMsgReq($cli); //测试聊天请求 166 | $self->heartAskReq($cli); //发送心跳 167 | while (true) { 168 | $ret = $self::onMessage($cli, $cli->recv()); 169 | if(!$ret) { 170 | break; 171 | } 172 | } 173 | 174 | } 175 | }); 176 | } 177 | 178 | /** 179 | * websocket 消息处理 180 | * @param $cli 181 | * @param $frame 182 | * @return bool 183 | */ 184 | public function onMessage($cli, $frame) { 185 | \App\Game\Core\Log::show('原数据:'.$frame->data); 186 | $ret = false; 187 | if($cli->connected && $frame) { 188 | $total_data = $frame->data; 189 | $total_len = strlen($total_data); 190 | if ($total_len < 4) { 191 | //清除定时器 192 | Swoole\Timer::clear($this->timer); 193 | //断开链接 194 | $cli->close(); 195 | \App\Game\Core\Log::show('数据包格式有误!'); 196 | } else { 197 | //需要进行粘包处理 198 | $off = 0; //结束时指针 199 | while ($total_len > $off) { 200 | $header = substr($total_data, $off, 4); 201 | $arr = unpack("Nlen", $header); 202 | $len = isset($arr['len']) ? $arr['len'] : 0; 203 | if ($len) { 204 | $data = substr($total_data, $off, $off + $len + 4); 205 | $body = \App\Game\Core\Packet::packDecode($data); 206 | $this->dispatch($cli, $body); 207 | $off += $len + 4; 208 | } else { 209 | break; 210 | } 211 | } 212 | } 213 | $ret = true; 214 | } else { 215 | //清除定时器 216 | Swoole\Timer::clear($this->heart_timer); 217 | Swoole\Timer::clear($this->reback_timer); 218 | //链接断开, 可以尝试断线重连逻辑 219 | $cli->close(); 220 | \App\Game\Core\Log::show('链接断开: 清除定时器, 断开链接!'); 221 | //断线重连逻辑 222 | $this->rebackConnection(); 223 | } 224 | return $ret; 225 | } 226 | 227 | /** 228 | * 断线重连 229 | */ 230 | protected function rebackConnection() { 231 | \App\Game\Core\Log::show('断线重连开始'); 232 | //定时器发送数据,发送心跳数据 233 | $this->reback_timer = Swoole\Timer::tick($this->reback_interval, function () { 234 | if($this->reback_count < $this->reback_times) { 235 | $this->reback_count++; 236 | $this->createConnection(); 237 | \App\Game\Core\Log::show('断线重连' . $this->reback_count . '次'); 238 | } else { 239 | Swoole\Timer::clear($this->reback_timer); 240 | Swoole\Timer::clear($this->heart_timer); 241 | } 242 | }); 243 | } 244 | 245 | /** 246 | * 转发到不同的逻辑处理 247 | * @param $cli 248 | * @param $cmd 249 | * @param $scmd 250 | * @param $data 251 | */ 252 | protected function dispatch($cli, $data) { 253 | $cmd = isset($data['cmd']) ? intval($data['cmd']) : 0; 254 | $scmd = isset($data['scmd']) ? intval($data['scmd']) : 0; 255 | $len = isset($data['len']) ? intval($data['len']) : 0; 256 | $method = isset($this->route[$cmd][$scmd]) ? $this->route[$cmd][$scmd] : ''; 257 | if($method) { 258 | if($method != 'heartAskResp') { 259 | \App\Game\Core\Log::show('----------------------------------------------------------------------------------------------'); 260 | \App\Game\Core\Log::show('cmd = ' . $cmd . ' scmd =' . $scmd . ' len=' . $len . ' method=' . $method); 261 | } 262 | $this->$method($cli, $data['data']['data']); 263 | } else { 264 | \App\Game\Core\Log::show('cmd = '.$cmd . ' scmd =' .$scmd .' ,method is not exists'); 265 | } 266 | } 267 | 268 | /** 269 | * 聊天请求 270 | * @param $cli 271 | */ 272 | protected function chatMsgReq($cli) { 273 | if($cli->connected) { 274 | $msg = array('data' => 'this is a test msg'); 275 | $data = \App\Game\Core\Packet::packEncode($msg, \App\Game\Conf\MainCmd::CMD_GAME, \App\Game\Conf\SubCmd::CHAT_MSG_REQ); 276 | $cli->push($data, WEBSOCKET_OPCODE_BINARY); 277 | } 278 | } 279 | 280 | /** 281 | * 触发心跳 282 | * @param $cli 283 | */ 284 | protected function heartAskReq($cli) { 285 | //定时器发送数据,发送心跳数据 286 | $this->heart_timer = Swoole\Timer::tick($this->heart_interval, function () use ($cli) { 287 | list($t1, $t2) = explode(' ', microtime()); 288 | $time = (float)sprintf('%.0f', (floatval($t1) + floatval($t2)) * 1000); 289 | $msg = array('time' => $time); 290 | $data = \App\Game\Core\Packet::packEncode($msg, \App\Game\Conf\MainCmd::CMD_SYS, \App\Game\Conf\SubCmd::HEART_ASK_REQ); 291 | $ret = $cli->push($data, WEBSOCKET_OPCODE_BINARY); 292 | if(!$ret) { 293 | $this->loginFail($cli); 294 | } 295 | }); 296 | } 297 | 298 | /** 299 | * 触发游戏开始 300 | * @param $cli 301 | */ 302 | protected function gameStartReq($cli) { 303 | if($cli->connected) { 304 | $msg = array('data'=>'game start'); 305 | $data = \App\Game\Core\Packet::packEncode($msg, \App\Game\Conf\MainCmd::CMD_GAME, \App\Game\Conf\SubCmd::SUB_GAME_START_REQ); 306 | $cli->push($data, WEBSOCKET_OPCODE_BINARY); 307 | } 308 | } 309 | 310 | /** 311 | * 发送叫地主请求 312 | * @param $cli 313 | * @param int $status 0表示不叫地主, 1表示叫地主 314 | */ 315 | protected function gameCallReq($cli, $status = 0) { 316 | if($cli->connected) { 317 | $data = array('type'=>$status); 318 | $data = \App\Game\Core\Packet::packEncode($data, \App\Game\Conf\MainCmd::CMD_GAME, \App\Game\Conf\SubCmd::SUB_GAME_CALL_REQ); 319 | $cli->push($data, WEBSOCKET_OPCODE_BINARY); 320 | } 321 | } 322 | 323 | /** 324 | * 出牌请求 325 | * @param $cli 326 | * @param bool $is_first_round 是否为首轮, 首轮必须出牌 327 | */ 328 | protected function outCardReq($cli, $is_first_round = false) { 329 | if($cli->connected) { 330 | \App\Game\Core\Log::show("开始出牌:"); 331 | if($is_first_round) { 332 | $status = 1; 333 | $card = array(array_shift($this->hand_card)); //第一张牌, 打出去 334 | } else { 335 | //跟牌默认过牌, TODO:需要实现跟牌逻辑, 需要从自己手牌中找出打过上次牌的牌, 根据情况决定是否跟牌 336 | $status = 0; //出牌状态随机 337 | $card = array(); 338 | } 339 | $msg = array( 340 | 'status' => $status, //打牌还是过牌, 1跟牌, 0是过牌 341 | 'chair_id' => $this->chair_id, 342 | 'card' => $card, 343 | ); 344 | $data = \App\Game\Core\Packet::packEncode($msg, \App\Game\Conf\MainCmd::CMD_GAME, \App\Game\Conf\SubCmd::SUB_GAME_OUT_CARD_REQ); 345 | $cli->push($data, WEBSOCKET_OPCODE_BINARY); 346 | } 347 | } 348 | 349 | 350 | 351 | /** 352 | * 响应登录失败 353 | * @param $cli 354 | */ 355 | protected function loginFailResp($cli, $data) { 356 | $cli->close(); 357 | Swoole\Timer::clear($this->heart_timer); 358 | Swoole\Timer::clear($this->reback_timer); 359 | \App\Game\Core\Log::show("关闭客户端, 清除定时器"); 360 | } 361 | 362 | /** 363 | * 响应登录成功 364 | * @param $cli 365 | */ 366 | protected function loginSucessResp($cli, $data) { 367 | //登录成功, 开始游戏逻辑 368 | \App\Game\Core\Log::show("登录成功, 开始游戏请求"); 369 | $this->gameStartReq($cli); 370 | } 371 | 372 | /** 373 | * 响应处理心跳 374 | * @param $cli 375 | */ 376 | protected function heartAskResp($cli, $data) { 377 | //定时器发送数据,发送心跳数据 378 | \App\Game\Core\Log::show('心跳(毫秒):' .$data['time']); 379 | } 380 | 381 | /** 382 | * 响应处理聊天 383 | * @param $cli 384 | */ 385 | protected function chatMsgResp($cli, $data) { 386 | //定时器发送数据,发送心跳数据 387 | \App\Game\Core\Log::show('聊天内容:'.json_encode($data)); 388 | } 389 | 390 | /** 391 | * 触发游戏开始 392 | * @param $cli 393 | */ 394 | protected function gameStartResp($cli, $data) { 395 | \App\Game\Core\Log::show('游戏场景数据:'.json_encode($data)); 396 | } 397 | 398 | /** 399 | * 解说用户信息协议 400 | * @param $cli 401 | */ 402 | protected function userInfoResp($cli, $data) { 403 | \App\Game\Core\Log::show('用户数据数据:'.json_encode($data)); 404 | } 405 | 406 | /** 407 | * 进入房间后, 开始抢地主 408 | * @param $cli 409 | */ 410 | protected function enterRoomSuccResp($cli, $data) { 411 | if($data['is_game_over']) { 412 | \App\Game\Core\Log::show('游戏结束'); 413 | //触发开始游戏 414 | $this->gameStartReq($cli); 415 | } else { 416 | \App\Game\Core\Log::show('进入房间成功数据:'.json_encode($data)); 417 | //保存用户信息和手牌信息 418 | $this->chair_id = $data['chair_id']; 419 | $this->hand_card = $data['card']; 420 | $this->my_room_info = $data; 421 | //如果没有叫地主, 触发叫地主动作 422 | if(!isset($data['calltype'])) { 423 | //根据自己的牌是否可以发送是否叫地主, 0,不叫, 1,叫地主, 2, 抢地主 424 | $obj = new \App\Game\Core\DdzPoker(); 425 | $ret = $obj->isGoodCard($this->hand_card); 426 | $status = $ret ? 1 : 0; 427 | //发送是否叫地主操作 428 | $this->gameCallReq($cli, $status); 429 | } 430 | //是否轮到自己出牌, 如果是, 请出牌 431 | if(isset($data['index_chair_id']) && $data['index_chair_id'] == $this->chair_id) { 432 | if (isset($data['is_first_round']) && $data['is_first_round']) { 433 | //首轮出牌 434 | \App\Game\Core\Log::show('请出牌'); 435 | } else { 436 | //跟牌操作 437 | \App\Game\Core\Log::show('请跟牌'); 438 | } 439 | $this->outCardReq($cli, $data['is_first_round']); 440 | } 441 | } 442 | } 443 | 444 | /** 445 | * 自己叫完地主提示响应 446 | * @param $cli 447 | */ 448 | protected function gameCallResp($cli, $data) { 449 | \App\Game\Core\Log::show('叫地主成功提示:'.json_encode($data)); 450 | } 451 | 452 | /** 453 | * 叫完地主广播提示 454 | * @param $cli 455 | */ 456 | protected function gameCallTipsResp($cli, $data) { 457 | $tips = $data['calltype'] ? $data['account'].'叫地主' : $data['account'].'不叫'; 458 | \App\Game\Core\Log::show('广播叫地主提示:'.$tips); 459 | } 460 | 461 | /** 462 | * 触发游戏开始 463 | * @param $cli 464 | */ 465 | protected function catchGameCardResp($cli, $data) { 466 | $tips = $data['user'].'摸底牌'.$data['hand_card']; 467 | \App\Game\Core\Log::show('摸底牌广播:'.$tips); 468 | if(isset($data['chair_id']) && $data['chair_id'] == $this->chair_id) { 469 | //合并手牌 470 | $hand_card = json_decode($data['hand_card'], true); 471 | $this->hand_card = $this->getDdzObj()->_sortCardByGrade(array_merge($this->hand_card, $hand_card)); 472 | \App\Game\Core\Log::show('地主['.$this->account.']出牌:'.json_encode($this->hand_card)); 473 | //地主首次出牌 474 | $this->outCardReq($cli, true); 475 | } 476 | } 477 | 478 | /** 479 | * 出牌提示 480 | * @param $cli 481 | */ 482 | protected function gameOutCard($cli, $data) { 483 | \App\Game\Core\Log::show('出牌提示:'.json_encode($data)); 484 | //移除手牌 485 | if(isset($data['status']) == 0 && isset($data['data']['card'])) { 486 | $this->hand_card = array_unique(array_values(array_diff($this->hand_card, $data['data']['card']))); 487 | } 488 | } 489 | 490 | /** 491 | * 出牌广播 492 | * @param $cli 493 | * @param $data 494 | */ 495 | protected function gameOutCardResp($cli, $data) { 496 | \App\Game\Core\Log::show('出牌广播提示:'.json_encode($data)); 497 | if(isset($data['is_game_over']) && $data['is_game_over']) { 498 | $tips = '广播:游戏结束,'.$data['account'].'胜利, 请点击"开始游戏",进行下一轮游戏'; 499 | \App\Game\Core\Log::show($tips); 500 | //触发开始游戏 501 | $this->gameStartReq($cli); 502 | } else { 503 | $play = (isset($data['show_type']) && $data['show_type'] == 1) ? '跟牌': '过牌'; 504 | $play = (isset($data['last_card']) && empty($data['last_card'])) ? '出牌' : $play; 505 | $last_card = !empty($data['last_card']) ? json_encode($data['last_card']) : '无'; 506 | $out_card = !empty($data['card']) ? json_encode($data['card']) : '无'; 507 | $tips = '广播: 第'.$data['round'].'回合,第'.$data['hand_num'].'手出牌, '.$data['account'].$play.', 上次牌值是'.$last_card.', 本次出牌值是'.$out_card .', 本次出牌牌型'.$data['card_type']; 508 | \App\Game\Core\Log::show($tips); 509 | //下次出牌是否轮到自己, 轮到自己, 请出牌 510 | if(isset($data['next_chair_id']) && $data['next_chair_id'] == $this->chair_id) { 511 | //出牌请求, 默认过牌操作 512 | if (isset($data['is_first_round']) && $data['is_first_round']) { 513 | //首轮出牌 514 | \App\Game\Core\Log::show('请出牌'); 515 | //地主首次出牌 516 | } else { 517 | //跟牌操作 518 | \App\Game\Core\Log::show('请跟牌'); 519 | //地主首次出牌 520 | } 521 | $this->outCardReq($cli, $data['is_first_round']); 522 | } 523 | } 524 | } 525 | 526 | /** 527 | * 说有没处理的方法, 输出 528 | * @param $name 529 | * @param $arguments 530 | */ 531 | public function __call($name, $arguments) 532 | { 533 | \App\Game\Core\Log::show($name.':'.json_encode($arguments[1])); 534 | } 535 | 536 | /** 537 | * 获取手牌对象 538 | */ 539 | public function getDdzObj() { 540 | if($this->ddz === null) { 541 | $this->ddz = new \App\Game\Core\DdzPoker(); 542 | } 543 | return $this->ddz; 544 | } 545 | } 546 | 547 | $ai = new Ai($argv[1]); 548 | $ai->run(); 549 | -------------------------------------------------------------------------------- /test/tcp_client.php: -------------------------------------------------------------------------------- 1 | $code, 14 | "msg" => $msg, 15 | "data" => $data, 16 | ); 17 | return $pack; 18 | } 19 | 20 | /** 21 | * 打包数据,固定包头,4个字节为包头(里面存了包体长度),包体前2个字节为 22 | */ 23 | public static function packEncode($data, $cmd = 1, $scmd = 1, $format='msgpack', $type = "tcp") { 24 | if ($type == "tcp") { 25 | if($format == 'msgpack') { 26 | $sendStr = msgpack_pack($data); 27 | } else { 28 | $sendStr = $data; 29 | } 30 | $sendStr = pack('N', strlen($sendStr) + 2) . pack("C2", $cmd, $scmd). $sendStr; 31 | return $sendStr; 32 | } else { 33 | return self::packFormat("packet type wrong", 100006); 34 | } 35 | } 36 | 37 | /** 38 | * 解包数据 39 | */ 40 | public static function packDecode($str, $format='msgpack') { 41 | $header = substr($str, 0, 4); 42 | if(strlen($header) != 4) { 43 | return self::packFormat("packet length invalid", 100007); 44 | } else { 45 | $len = unpack("Nlen", $header); 46 | $len = $len["len"]; 47 | $cmd = unpack("Ccmd/Cscmd", substr($str, 4, 6)); 48 | $result = substr($str, 6); 49 | if ($len != strlen($result) + 2) { 50 | //结果长度不对 51 | return self::packFormat("packet length invalid", 100007); 52 | } 53 | 54 | if($format == 'msgpack') { 55 | $result = msgpack_unpack($result); 56 | } 57 | 58 | if(empty($result)) { 59 | //结果长度不对 60 | return self::packFormat("packet data is empty", 100008); 61 | } 62 | 63 | // $result = self::packFormat("OK", 0, $result); 64 | $result['cmd'] = $cmd['cmd']; 65 | $result['scmd'] = $cmd['scmd']; 66 | $result['len'] = $len + 4; 67 | return $result; 68 | } 69 | } 70 | } 71 | 72 | 73 | //测试发送protobuf 发送请求 74 | $client = new swoole_client(SWOOLE_SOCK_TCP); 75 | if (!$client->connect('127.0.0.1', 18309, -1)) 76 | { 77 | exit("connect failed. Error: {$client->errCode}\n"); 78 | } 79 | $data = 'this is a system msg'; 80 | $back = Packet::packEncode($data, 2, 213); 81 | $client->send($back); 82 | $res = $client->recv(); 83 | echo '返回加密数据:'.$res."\n"; 84 | //解开数据 85 | $res = Packet::packDecode($res); 86 | echo "解开返回数据\n"; 87 | print_r($res); 88 | $client->close(); 89 | -------------------------------------------------------------------------------- /test/test_redis.php: -------------------------------------------------------------------------------- 1 | connect('192.168.1.155', 6379); 12 | $redis->setOptions(['compatibility_mode' => true]); 13 | $room_no_key = "user:room:no"; 14 | if($redis->exists($room_no_key)) { 15 | echo '+++++++++++++++++++++++++++++++++++'; 16 | $room_no = $redis->incr($room_no_key); 17 | echo $room_no; 18 | } else { 19 | $room_no = 1000001; 20 | $redis->set($room_no_key, $room_no); 21 | } 22 | var_dump($room_no); 23 | }); -------------------------------------------------------------------------------- /test/test_rpc.php: -------------------------------------------------------------------------------- 1 | '2.0', 24 | "method" => sprintf("%s::%s::%s", $version, $class, $method), 25 | 'params' => $param, 26 | 'id' => '', 27 | 'ext' => $ext, 28 | ]; 29 | $data = json_encode($req) . RPC_EOL; 30 | fwrite($fp, $data); 31 | 32 | $result = ''; 33 | while (!feof($fp)) { 34 | $tmp = stream_socket_recvfrom($fp, 1024); 35 | 36 | if ($pos = strpos($tmp, RPC_EOL)) { 37 | $result .= substr($tmp, 0, $pos); 38 | break; 39 | } else { 40 | $result .= $tmp; 41 | } 42 | } 43 | 44 | fclose($fp); 45 | return json_decode($result, true); 46 | } 47 | 48 | $ret = request('tcp://127.0.0.1:18307', \App\Rpc\Lib\UserInterface::class, 'getList', [1, 2], "1.0"); 49 | var_dump($ret); 50 | 51 | $uid = 10001; 52 | $url = 'tcp://127.0.0.1:18307'; 53 | $result = request($url, \App\Rpc\Lib\DbproxyInterface::class, 'execProc', ['accounts_mj', 'sp_account_get_by_uid', [$uid]], "1.0"); 54 | var_dump($result); 55 | 56 | 57 | //$result = call('App\Lib\DbproxyInterface', '1.0.0', 'execProc', ['accounts_mj', 'sp_account_get_by_uid', [$uid]]); 58 | //$result1 = call('App\Lib\DemoInterface', '1.0.1', 'getUsers', [[$uid]]); 59 | //$result2 = call('App\Lib\DbproxyInterface', '1.0.0', 'execProc', ['activity_mj', 'sp_winlist_s', [1,10032,'',0]]); 60 | // 61 | // 62 | //$result3 = call('App\Lib\DbproxyInterface', '1.0.0', 'getYuanbao', [$uid]); 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | --------------------------------------------------------------------------------