├── images ├── ai.jpg ├── demo1.png └── demo2.png ├── .gitignore ├── config ├── autoload │ ├── aspects.php │ ├── commands.php │ ├── dependencies.php │ ├── listeners.php │ ├── processes.php │ ├── middlewares.php │ ├── view.php │ ├── exceptions.php │ ├── annotations.php │ ├── cache.php │ ├── game.php │ ├── async_queue.php │ ├── apollo.php │ ├── redis.php │ ├── logger.php │ ├── amqp.php │ ├── devtool.php │ ├── databases.php │ ├── opentracing.php │ └── server.php ├── routes.php ├── config.php └── container.php ├── app ├── Game │ ├── Conf │ │ ├── MainCmd.php │ │ ├── Route.php │ │ └── SubCmd.php │ ├── Logic │ │ ├── ChatMsg.php │ │ ├── HeartAsk.php │ │ ├── GameStart.php │ │ ├── GameCall.php │ │ └── GameOutCard.php │ └── Core │ │ ├── Log.php │ │ ├── Dispatch.php │ │ ├── Packet.php │ │ ├── AStrategy.php │ │ └── JokerPoker.php ├── Process │ └── AsyncQueueConsumer.php ├── Model │ └── Model.php ├── Constants │ └── ErrorCode.php ├── Exception │ ├── BusinessException.php │ └── Handler │ │ └── AppExceptionHandler.php ├── Helper.php ├── Controller │ ├── AbstractController.php │ ├── IndexController.php │ └── GameController.php ├── Listener │ ├── DbQueryExecutedListener.php │ └── QueueHandleListener.php └── Task │ └── GameSyncTask.php ├── phpstan.neon ├── test ├── Cases │ └── ExampleTest.php ├── bootstrap.php └── HttpTestCase.php ├── ai ├── test_redis.php ├── test_rpc.php ├── tcp_client.php └── ai.php ├── deploy.test.yml ├── phpunit.xml ├── bin └── hyperf.php ├── public └── client │ ├── Req.js │ ├── Const.js │ ├── index.html │ ├── Init.js │ ├── Packet.js │ ├── Resp.js │ └── msgpack.js ├── storage └── view │ ├── login.html │ └── index.html ├── README.md ├── composer.json ├── Dockerfile └── LICENSE /images/ai.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jxy918/hyperf-ddz/HEAD/images/ai.jpg -------------------------------------------------------------------------------- /images/demo1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jxy918/hyperf-ddz/HEAD/images/demo1.png -------------------------------------------------------------------------------- /images/demo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jxy918/hyperf-ddz/HEAD/images/demo2.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.patch 2 | .idea/ 3 | .git/ 4 | runtime/ 5 | vendor/ 6 | .env 7 | .DS_Store 8 | *.lock 9 | .phpunit* 10 | -------------------------------------------------------------------------------- /config/autoload/aspects.php: -------------------------------------------------------------------------------- 1 | [ 15 | ], 16 | ]; 17 | -------------------------------------------------------------------------------- /config/autoload/view.php: -------------------------------------------------------------------------------- 1 | SmartyEngine::class, 10 | // 不填写则默认为 Task 模式,推荐使用 Task 模式 11 | 'mode' => Mode::TASK, 12 | 'config' => [ 13 | // 若下列文件夹不存在请自行创建 14 | 'view_path' => BASE_PATH . '/storage/view/', 15 | 'cache_path' => BASE_PATH . '/runtime/view/', 16 | ], 17 | ]; -------------------------------------------------------------------------------- /config/autoload/exceptions.php: -------------------------------------------------------------------------------- 1 | [ 15 | 'http' => [ 16 | App\Exception\Handler\AppExceptionHandler::class, 17 | ], 18 | ], 19 | ]; 20 | -------------------------------------------------------------------------------- /config/autoload/annotations.php: -------------------------------------------------------------------------------- 1 | [ 15 | 'paths' => [ 16 | BASE_PATH . '/app', 17 | ], 18 | 'ignore_annotations' => [ 19 | 'mixin', 20 | ], 21 | ], 22 | ]; 23 | -------------------------------------------------------------------------------- /config/autoload/cache.php: -------------------------------------------------------------------------------- 1 | [ 15 | 'driver' => Hyperf\Cache\Driver\RedisDriver::class, 16 | 'packer' => Hyperf\Utils\Packer\PhpSerializerPacker::class, 17 | 'prefix' => 'c:', 18 | ], 19 | ]; 20 | -------------------------------------------------------------------------------- /app/Process/AsyncQueueConsumer.php: -------------------------------------------------------------------------------- 1 | _params['data']); 19 | $data = Packet::packEncode($data, MainCmd::CMD_GAME, SubCmd::CHAT_MSG_RESP); 20 | return $data; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/Model/Model.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 | ]; -------------------------------------------------------------------------------- /config/autoload/async_queue.php: -------------------------------------------------------------------------------- 1 | [ 15 | 'driver' => Hyperf\AsyncQueue\Driver\RedisDriver::class, 16 | 'channel' => 'queue', 17 | 'timeout' => 2, 18 | 'retry_seconds' => 5, 19 | 'handle_timeout' => 10, 20 | 'processes' => 1, 21 | ], 22 | ]; 23 | -------------------------------------------------------------------------------- /config/autoload/apollo.php: -------------------------------------------------------------------------------- 1 | false, 15 | 'server' => env('APOLLO_SERVER', 'http://127.0.0.1:8080'), 16 | 'appid' => 'Your APP ID', 17 | 'cluster' => 'default', 18 | 'namespaces' => [ 19 | 'application', 20 | ], 21 | 'interval' => 5, 22 | 'strict_mode' => false, 23 | ]; 24 | -------------------------------------------------------------------------------- /app/Constants/ErrorCode.php: -------------------------------------------------------------------------------- 1 | assertTrue(true); 26 | $this->assertTrue(is_array($this->get('/'))); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /ai/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 | }); -------------------------------------------------------------------------------- /deploy.test.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | services: 3 | hyperf: 4 | image: $REGISTRY_URL/$PROJECT_NAME:test 5 | environment: 6 | - "APP_PROJECT=hyperf" 7 | - "APP_ENV=test" 8 | ports: 9 | - 9501:9501 10 | deploy: 11 | replicas: 1 12 | restart_policy: 13 | condition: on-failure 14 | delay: 5s 15 | max_attempts: 5 16 | update_config: 17 | parallelism: 2 18 | delay: 5s 19 | order: start-first 20 | networks: 21 | - hyperf_net 22 | configs: 23 | - source: hyperf_v1.0 24 | target: /opt/www/.env 25 | configs: 26 | hyperf_v1.0: 27 | external: true 28 | networks: 29 | hyperf_net: 30 | external: true 31 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | ./test 14 | 15 | 16 | 17 | 18 | ./app 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /bin/hyperf.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | get(\Hyperf\Contract\ApplicationInterface::class); 21 | $application->run(); 22 | })(); 23 | -------------------------------------------------------------------------------- /app/Exception/BusinessException.php: -------------------------------------------------------------------------------- 1 | env('APP_NAME', 'skeleton'), 18 | StdoutLoggerInterface::class => [ 19 | 'log_level' => [ 20 | LogLevel::ALERT, 21 | LogLevel::CRITICAL, 22 | LogLevel::DEBUG, 23 | LogLevel::EMERGENCY, 24 | LogLevel::ERROR, 25 | LogLevel::INFO, 26 | LogLevel::NOTICE, 27 | LogLevel::WARNING, 28 | ], 29 | ], 30 | ]; 31 | -------------------------------------------------------------------------------- /config/container.php: -------------------------------------------------------------------------------- 1 | get(Hyperf\Contract\ApplicationInterface::class); 29 | -------------------------------------------------------------------------------- /config/autoload/redis.php: -------------------------------------------------------------------------------- 1 | [ 15 | 'host' => env('REDIS_HOST', 'redis'), 16 | 'auth' => env('REDIS_AUTH', null), 17 | 'port' => (int) env('REDIS_PORT', 6379), 18 | 'db' => (int) env('REDIS_DB', 0), 19 | 'pool' => [ 20 | 'min_connections' => 1, 21 | 'max_connections' => 10, 22 | 'connect_timeout' => 10.0, 23 | 'wait_timeout' => 3.0, 24 | 'heartbeat' => -1, 25 | 'max_idle_time' => (float) env('REDIS_MAX_IDLE_TIME', 60), 26 | ], 27 | ], 28 | ]; 29 | -------------------------------------------------------------------------------- /app/Helper.php: -------------------------------------------------------------------------------- 1 | get(\Redis::class); 12 | } 13 | } 14 | if (!function_exists('server')) { 15 | function server() 16 | { 17 | return ApplicationContext::getContainer()->get(ServerFactory::class)->getServer()->getServer(); 18 | } 19 | } 20 | if (!function_exists('frame')) { 21 | function frame() 22 | { 23 | return ApplicationContext::getContainer()->get(Frame::class); 24 | } 25 | } 26 | if (!function_exists('websocket')) { 27 | function websocket() 28 | { 29 | return ApplicationContext::getContainer()->get(WebSocketServer::class); 30 | } 31 | } -------------------------------------------------------------------------------- /app/Controller/AbstractController.php: -------------------------------------------------------------------------------- 1 | array( 21 | SubCmd::HEART_ASK_REQ => 'HeartAsk', 22 | ), 23 | //游戏请求 24 | MainCmd::CMD_GAME => array( 25 | SubCmd::SUB_GAME_START_REQ => 'GameStart', 26 | SubCmd::SUB_GAME_CALL_REQ => 'GameCall', 27 | SubCmd::SUB_GAME_DOUBLE_REQ => 'GameDouble', 28 | SubCmd::SUB_GAME_OUT_CARD_REQ => 'GameOutCard', 29 | SubCmd::CHAT_MSG_REQ => 'ChatMsg', 30 | ), 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /config/autoload/logger.php: -------------------------------------------------------------------------------- 1 | [ 15 | 'handler' => [ 16 | 'class' => Monolog\Handler\StreamHandler::class, 17 | 'constructor' => [ 18 | 'stream' => BASE_PATH . '/runtime/logs/hyperf.log', 19 | 'level' => Monolog\Logger::DEBUG, 20 | ], 21 | ], 22 | 'formatter' => [ 23 | 'class' => Monolog\Formatter\LineFormatter::class, 24 | 'constructor' => [ 25 | 'format' => null, 26 | 'dateFormat' => null, 27 | 'allowInlineLineBreaks' => true, 28 | ], 29 | ], 30 | ], 31 | ]; 32 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /test/HttpTestCase.php: -------------------------------------------------------------------------------- 1 | client = make(Client::class); 36 | } 37 | 38 | public function __call($name, $arguments) 39 | { 40 | return $this->client->{$name}(...$arguments); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /config/autoload/amqp.php: -------------------------------------------------------------------------------- 1 | [ 15 | 'host' => 'localhost', 16 | 'port' => 5672, 17 | 'user' => 'guest', 18 | 'password' => 'guest', 19 | 'vhost' => '/', 20 | 'pool' => [ 21 | 'min_connections' => 1, 22 | 'max_connections' => 10, 23 | 'connect_timeout' => 10.0, 24 | 'wait_timeout' => 3.0, 25 | 'heartbeat' => -1, 26 | ], 27 | 'params' => [ 28 | 'insist' => false, 29 | 'login_method' => 'AMQPLAIN', 30 | 'login_response' => null, 31 | 'locale' => 'en_US', 32 | 'connection_timeout' => 3.0, 33 | 'read_write_timeout' => 6.0, 34 | 'context' => null, 35 | 'keepalive' => false, 36 | 'heartbeat' => 3, 37 | ], 38 | ], 39 | ]; 40 | -------------------------------------------------------------------------------- /app/Game/Core/Log.php: -------------------------------------------------------------------------------- 1 | 'INFO', 13 | 2 => 'DEBUG', 14 | 3 => 'ERROR' 15 | ); 16 | 17 | /** 18 | * 日志等级,1表示大于等于1的等级的日志,都会显示,依次类推 19 | * @var int 20 | */ 21 | protected static $level = 1; 22 | 23 | /** 24 | * 显示日志 25 | * @param string $centent 26 | * @param int $level 27 | */ 28 | public static function show($centent = '', $level = 1, $str = '') 29 | { 30 | if ($level >= self::$level) { 31 | echo $str . date('Y/m/d H:i:s') . " [\033[0;36m" . self::$level_info[$level] . "\033[0m] " . $centent . "\n"; 32 | } 33 | } 34 | 35 | /** 36 | * 显示日志 37 | * @param string $centent 38 | * @param int $level 39 | */ 40 | public static function split($split = '', $level = 1) 41 | { 42 | if ($level >= self::$level) { 43 | echo $split . "\n"; 44 | } 45 | } 46 | } 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /config/autoload/devtool.php: -------------------------------------------------------------------------------- 1 | [ 15 | 'amqp' => [ 16 | 'consumer' => [ 17 | 'namespace' => 'App\\Amqp\\Consumer', 18 | ], 19 | 'producer' => [ 20 | 'namespace' => 'App\\Amqp\\Producer', 21 | ], 22 | ], 23 | 'aspect' => [ 24 | 'namespace' => 'App\\Aspect', 25 | ], 26 | 'command' => [ 27 | 'namespace' => 'App\\Command', 28 | ], 29 | 'controller' => [ 30 | 'namespace' => 'App\\Controller', 31 | ], 32 | 'job' => [ 33 | 'namespace' => 'App\\Job', 34 | ], 35 | 'listener' => [ 36 | 'namespace' => 'App\\Listener', 37 | ], 38 | 'middleware' => [ 39 | 'namespace' => 'App\\Middleware', 40 | ], 41 | 'Process' => [ 42 | 'namespace' => 'App\\Processes', 43 | ], 44 | ], 45 | ]; 46 | -------------------------------------------------------------------------------- /app/Exception/Handler/AppExceptionHandler.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 31 | } 32 | 33 | public function handle(Throwable $throwable, ResponseInterface $response) 34 | { 35 | $this->logger->error(sprintf('%s[%s] in %s', $throwable->getMessage(), $throwable->getLine(), $throwable->getFile())); 36 | $this->logger->error($throwable->getTraceAsString()); 37 | return $response->withHeader("Server", "Hyperf")->withStatus(500)->withBody(new SwooleStream('Internal Server Error.')); 38 | } 39 | 40 | public function isValid(Throwable $throwable): bool 41 | { 42 | return true; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /storage/view/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | login 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 31 | 32 | 33 |
34 |
35 | 36 | 用户帐号: 37 | 38 |
39 |
40 |
41 | {$tips} 42 |
43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /config/autoload/databases.php: -------------------------------------------------------------------------------- 1 | [ 15 | 'driver' => env('DB_DRIVER', 'mysql'), 16 | 'host' => env('DB_HOST', 'localhost'), 17 | 'port' => env('DB_PORT', 3306), 18 | 'database' => env('DB_DATABASE', 'hyperf'), 19 | 'username' => env('DB_USERNAME', 'root'), 20 | 'password' => env('DB_PASSWORD', ''), 21 | 'charset' => env('DB_CHARSET', 'utf8'), 22 | 'collation' => env('DB_COLLATION', 'utf8_unicode_ci'), 23 | 'prefix' => env('DB_PREFIX', ''), 24 | 'pool' => [ 25 | 'min_connections' => 1, 26 | 'max_connections' => 10, 27 | 'connect_timeout' => 10.0, 28 | 'wait_timeout' => 3.0, 29 | 'heartbeat' => -1, 30 | 'max_idle_time' => (float) env('DB_MAX_IDLE_TIME', 60), 31 | ], 32 | 'cache' => [ 33 | 'handler' => Hyperf\ModelCache\Handler\RedisHandler::class, 34 | 'cache_key' => 'mc:%s:m:%s:%s:%s', 35 | 'prefix' => 'default', 36 | 'ttl' => 3600 * 24, 37 | 'empty_model_ttl' => 600, 38 | 'load_script' => true, 39 | ], 40 | 'commands' => [ 41 | 'gen:model' => [ 42 | 'path' => 'app/Model', 43 | 'force_casts' => true, 44 | 'inheritance' => 'Model', 45 | ], 46 | ], 47 | ], 48 | ]; 49 | -------------------------------------------------------------------------------- /app/Listener/DbQueryExecutedListener.php: -------------------------------------------------------------------------------- 1 | logger = $container->get(LoggerFactory::class)->get('sql'); 37 | } 38 | 39 | public function listen(): array 40 | { 41 | return [ 42 | QueryExecuted::class, 43 | ]; 44 | } 45 | 46 | /** 47 | * @param QueryExecuted $event 48 | */ 49 | public function process(object $event) 50 | { 51 | if ($event instanceof QueryExecuted) { 52 | $sql = $event->sql; 53 | if (! Arr::isAssoc($event->bindings)) { 54 | foreach ($event->bindings as $key => $value) { 55 | $sql = Str::replaceFirst('?', "'{$value}'", $sql); 56 | } 57 | } 58 | 59 | $this->logger->info(sprintf('[%s] %s', $event->time, $sql)); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/Game/Core/Dispatch.php: -------------------------------------------------------------------------------- 1 | _params = $params; 32 | $this->Strategy(); 33 | } 34 | 35 | /** 36 | * 逻辑处理策略路由转发, 游戏逻辑策略转发, 根据主命令字和子命令字来转发 37 | */ 38 | public function Strategy() 39 | { 40 | //获取路由策略 41 | $route = Route::$cmd_map; 42 | //获取策略类名 43 | $classname = isset($route[$this->_params['cmd']][$this->_params['scmd']]) ? $route[$this->_params['cmd']][$this->_params['scmd']] : ''; 44 | //转发到对应目录处理逻辑 45 | $classname = 'App\Game\Logic\\' . $classname; 46 | if (class_exists($classname)) { 47 | $this->_strategy = new $classname($this->_params); 48 | Log::show("Class: $classname"); 49 | } else { 50 | Log::show("Websockt Error: class is not support,cmd is {$this->_params['cmd']},scmd is {$this->_params['scmd']}"); 51 | } 52 | } 53 | 54 | /** 55 | * 获取策略 56 | */ 57 | public function getStrategy() 58 | { 59 | return $this->_strategy; 60 | } 61 | 62 | /** 63 | * 执行策略 64 | */ 65 | public function exec() 66 | { 67 | return $this->_strategy->exec(); 68 | } 69 | } -------------------------------------------------------------------------------- /config/autoload/opentracing.php: -------------------------------------------------------------------------------- 1 | env('TRACER_DRIVER', 'zipkin'), 17 | 'enable' => [ 18 | 'guzzle' => env('TRACER_ENABLE_GUZZLE', false), 19 | 'redis' => env('TRACER_ENABLE_REDIS', false), 20 | 'db' => env('TRACER_ENABLE_DB', false), 21 | 'method' => env('TRACER_ENABLE_METHOD', false), 22 | ], 23 | 'tracer' => [ 24 | 'zipkin' => [ 25 | 'driver' => \Hyperf\Tracer\Adapter\ZipkinTracerFactory::class, 26 | 'app' => [ 27 | 'name' => env('APP_NAME', 'skeleton'), 28 | // Hyperf will detect the system info automatically as the value if ipv4, ipv6, port is null 29 | 'ipv4' => '127.0.0.1', 30 | 'ipv6' => null, 31 | 'port' => 9501, 32 | ], 33 | 'options' => [ 34 | 'endpoint_url' => env('ZIPKIN_ENDPOINT_URL', 'http://localhost:9411/api/v2/spans'), 35 | 'timeout' => env('ZIPKIN_TIMEOUT', 1), 36 | ], 37 | 'sampler' => BinarySampler::createAsAlwaysSample(), 38 | ], 39 | 'jaeger' => [ 40 | 'driver' => \Hyperf\Tracer\Adapter\JaegerTracerFactory::class, 41 | 'name' => env('APP_NAME', 'skeleton'), 42 | 'options' => [ 43 | 'local_agent' => [ 44 | 'reporting_host' => env('JAEGER_REPORTING_HOST', 'localhost'), 45 | 'reporting_port' => env('JAEGER_REPORTING_PORT', 5775), 46 | ], 47 | ], 48 | ], 49 | ], 50 | ]; 51 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /ai/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 | -------------------------------------------------------------------------------- /app/Controller/IndexController.php: -------------------------------------------------------------------------------- 1 | _isLogin()) { 31 | return $this->response->redirect('/login'); 32 | } 33 | //用户信息传递到客户端 34 | $info = $this->request->getCookieParams(); 35 | $u = json_decode($info['USER_INFO'], true); 36 | return $render->render('index.html', $u); 37 | } 38 | 39 | public function login(RequestInterface $request, ResponseInterface $response, RenderInterface $render) 40 | { 41 | $action = $request->post('action'); 42 | $account = $request->post('account'); 43 | $tips = ''; 44 | if ($action == 'login') { 45 | if (!empty($account)) { 46 | //注册登录 47 | $uinfo = array('account' => $account); 48 | $cookie = new Cookie('USER_INFO', json_encode($uinfo)); 49 | $response = $response->withCookie($cookie); 50 | return $response->redirect('/'); 51 | } else { 52 | $tips = '温馨提示:用户账号不能为空!'; 53 | } 54 | } 55 | return $render->render('login.html', ['tips' => $tips]); 56 | } 57 | 58 | private function _isLogin() 59 | { 60 | $cookie_info = $this->request->getCookieParams(); 61 | if (isset($cookie_info['USER_INFO'])) { 62 | $this->userinfo = json_decode($cookie_info['USER_INFO']); 63 | return true; 64 | } else { 65 | return false; 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /app/Game/Core/Packet.php: -------------------------------------------------------------------------------- 1 | $code, 17 | "msg" => $msg, 18 | "data" => $data, 19 | ); 20 | return $pack; 21 | } 22 | 23 | /** 24 | * 打包数据,固定包头,4个字节为包头(里面存了包体长度),包体前2个字节为 25 | */ 26 | public static function packEncode($data, $cmd = 1, $scmd = 1, $format = 'msgpack', $type = "tcp") 27 | { 28 | if ($type == "tcp") { 29 | if ($format == 'msgpack') { 30 | $sendStr = msgpack_pack($data); 31 | } else { 32 | $sendStr = $data; 33 | } 34 | $sendStr = pack('N', strlen($sendStr) + 2) . pack("C2", $cmd, $scmd) . $sendStr; 35 | return $sendStr; 36 | } else { 37 | return self::packFormat("packet type wrong", 100006); 38 | } 39 | } 40 | 41 | /** 42 | * 解包数据 43 | */ 44 | public static function packDecode($str, $format = 'msgpack') 45 | { 46 | $header = substr($str, 0, 4); 47 | if (strlen($header) != 4) { 48 | return self::packFormat("packet length invalid", 100007); 49 | } else { 50 | $len = unpack("Nlen", $header); 51 | $len = $len["len"]; 52 | $result = substr($str, 6); 53 | if ($len != strlen($result) + 2) { 54 | //结果长度不对 55 | return self::packFormat("packet length invalid", 100007); 56 | } 57 | 58 | if ($format == 'msgpack') { 59 | $result = msgpack_unpack($result); 60 | } 61 | if (empty($result)) { 62 | //结果长度不对 63 | return self::packFormat("packet data is empty", 100008); 64 | } 65 | $cmd = unpack("Ccmd/Cscmd", substr($str, 4, 6)); 66 | $result = self::packFormat("OK", 0, $result); 67 | $result['cmd'] = $cmd['cmd']; 68 | $result['scmd'] = $cmd['scmd']; 69 | $result['len'] = $len + 4; 70 | return $result; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hyperf-ddz(斗地主) 2 | 3 | * 基于hyperf框架开发游戏斗地主 4 | 5 | * 使用hyperf框架基本实现斗地主游戏逻辑, 可以简单玩斗地主 6 | * 客户端采用纯原生js编写, 实现简单的测试客户端, 没有任何动画效果 7 | * 实现斗地主AI机器人简单逻辑, 现在只能是过牌, 比较弱智,可以简单陪打,机器人逻辑会慢慢完善 8 | 9 | * 想关注更多游戏开发可以关注swoft-ddz斗地主:**[swoft-ddz](https://github.com/jxy918/swoft-ddz)** 10 | ### 一,概述 11 | 12 | * 使用hyperf开发实现斗地主游戏服务器逻辑, 并采用原生js实现简单的客户端打牌逻辑,基本可以做到简单玩斗地主游戏了, 并实现机器人AI逻辑编写, 全方面实现斗地主游戏逻辑。 13 | * 整个斗地主逻辑实现是在redis里存储的, 并没有接入数据库, 数据库接入, 可以在游戏登录或者斗地主打牌完成, 接入。 14 | 15 | ### 二,示例图 16 | 1, 登录, 简单实现登录, 请随便输入英文或这数字账号, 现在直接是采用账号当uid使用存入redis的,如果接入数据库, 请自行通过账号替换成uid,登录如下图: 17 | ![游戏demo1](images/demo1.png) 18 | 19 | 2, 打牌逻辑, 根据按钮来操作打牌逻辑, 消息框里会提示打牌逻辑过程,打牌逻辑如下图: 20 | ![游戏demo2](images/demo2.png) 21 | 22 | 23 | ### 三,特性 24 | 25 | * 实现前后端二进制封包解包,采用的是msgpack扩展,msgpack对数据进行了压缩,并实现粘包处理 26 | * 数据采用固定包头,包头4个字节存包体长度,包体前2个字节分别为cmd(主命令字),scmd(子命令字),后面为包体内容 27 | * 采用策略模式解耦游戏中的每个协议逻辑 28 | * 实现定义游戏开发cmd(主命令字)和scmd(子命令字)定义,cmd和scmd主要用来路由到游戏协议逻辑处理 29 | * 代码里有个DdzPoker类是一个款斗地主算法类. 30 | * 多地主逻辑是单独目录是实现的, 可以方便迁移框架。 31 | 32 | ### 四,环境依赖 33 | 34 | >依赖swoft环境,请安装php扩展msgpack 35 | 36 | * php vesion > 7.0 37 | * swoole version > 4.4.1 38 | * msgpack 39 | * hyperf vesion > 1.0.0 40 | 41 | 42 | ### 五,开始使用 43 | * 1,安装 44 | 45 | ``` 46 | composer install 47 | 48 | ``` 49 | 50 | * 2,目录说明(swoft目录不具体说明): 51 | 52 | ``` 53 | ./app/Controller/GameController.php 游戏http控制器逻辑 54 | ./app/Game 是这个整体游戏服务器逻辑 55 | ./app/Game/Conf 逻辑配置目录, 比如:命令字, 子名字, 路由转发 56 | ./app/Game/Core 游戏路由转发,算法,解包核心类 57 | ./app/Game/Logic 游戏路由转发逻辑协议包处理目录 58 | ./public/client 客户端view的资源文件 59 | ./storage/views/ 斗地主客户端 60 | ./app/Task/GameSyncTask.php 用户进入房间异步匹配处理逻辑 61 | ``` 62 | 63 | * 3,进入根目录目录,启动服务器(hyperf启动websocket启动法) : 64 | 65 | 游戏服务器命令操作: 66 | 67 | ``` 68 | // 启动服务,根据 69 | php bin/hyperf.php start 70 | 71 | ``` 72 | 73 | 机器人AI执行命令操作, 进入根目录下ai目录下 74 | 75 | ``` 76 | //xxx表示用户账号 77 | php ai.php xxx 78 | 79 | ``` 80 | > xxx 表示用户账号, 账号最好用英文加数字, 账号唯一就可以 81 | 82 | > ai功能需要关闭swoole短名称功能,php.ini 添加 swoole.use_shortname=off 83 | 84 | ![AI示例图](images/ai.jpg) 85 | 86 | * 4,访问url: 87 | 88 | ``` 89 | //斗地主客户端入口 90 | http://[ip]:[port]/ 91 | 92 | ``` 93 | 94 | 95 | ### 六,联系方式 96 | 97 | * qq:251413215,加qq请输入验证消息:hyperf-ddz 98 | 99 | qq群:100754069 100 | 101 | ### 七,备注 102 | 103 | * 可以使用根目录增加docker运行环境(Dockerfile), 可以直接执行下面的命令,创建镜像php_swoole, 环境增加php-protobuf,php-msgpack支持。 104 | 105 | ``` 106 | docker build -t php_swoole . 107 | 108 | ``` 109 | * 注意如果程序不能自动加载,请去除环境中opcache扩展。 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /app/Listener/QueueHandleListener.php: -------------------------------------------------------------------------------- 1 | logger = $loggerFactory->get('queue'); 43 | $this->formatter = $formatter; 44 | } 45 | 46 | public function listen(): array 47 | { 48 | return [ 49 | AfterHandle::class, 50 | BeforeHandle::class, 51 | FailedHandle::class, 52 | RetryHandle::class, 53 | ]; 54 | } 55 | 56 | public function process(object $event) 57 | { 58 | if ($event instanceof Event && $event->message->job()) { 59 | $job = $event->message->job(); 60 | $jobClass = get_class($job); 61 | $date = date('Y-m-d H:i:s'); 62 | 63 | switch (true) { 64 | case $event instanceof BeforeHandle: 65 | $this->logger->info(sprintf('[%s] Processing %s.', $date, $jobClass)); 66 | break; 67 | case $event instanceof AfterHandle: 68 | $this->logger->info(sprintf('[%s] Processed %s.', $date, $jobClass)); 69 | break; 70 | case $event instanceof FailedHandle: 71 | $this->logger->error(sprintf('[%s] Failed %s.', $date, $jobClass)); 72 | $this->logger->error($this->formatter->format($event->getThrowable())); 73 | break; 74 | case $event instanceof RetryHandle: 75 | $this->logger->warning(sprintf('[%s] Retried %s.', $date, $jobClass)); 76 | break; 77 | } 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /config/autoload/server.php: -------------------------------------------------------------------------------- 1 | SWOOLE_PROCESS, 18 | 'servers' => [ 19 | [ 20 | 'name' => 'http', 21 | 'type' => Server::SERVER_HTTP, 22 | 'host' => '0.0.0.0', 23 | 'port' => 18308, 24 | 'sock_type' => SWOOLE_SOCK_TCP, 25 | 'callbacks' => [ 26 | SwooleEvent::ON_REQUEST => [Hyperf\HttpServer\Server::class, 'onRequest'], 27 | ], 28 | ], 29 | [ 30 | 'name' => 'ws', 31 | 'type' => Server::SERVER_WEBSOCKET, 32 | 'host' => '0.0.0.0', 33 | 'port' => 18306, 34 | 'sock_type' => SWOOLE_SOCK_TCP, 35 | 'callbacks' => [ 36 | SwooleEvent::ON_HAND_SHAKE => [Hyperf\WebSocketServer\Server::class, 'onHandShake'], 37 | SwooleEvent::ON_MESSAGE => [Hyperf\WebSocketServer\Server::class, 'onMessage'], 38 | SwooleEvent::ON_CLOSE => [Hyperf\WebSocketServer\Server::class, 'onClose'], 39 | ], 40 | ], 41 | ], 42 | 'settings' => [ 43 | 'enable_coroutine' => true, 44 | 'worker_num' => swoole_cpu_num(), 45 | 'pid_file' => BASE_PATH . '/runtime/hyperf.pid', 46 | 'open_tcp_nodelay' => true, 47 | 'max_coroutine' => 100000, 48 | 'open_http2_protocol' => true, 49 | 'max_request' => 100000, 50 | 'socket_buffer_size' => 2 * 1024 * 1024, 51 | 'buffer_output_size' => 2 * 1024 * 1024, 52 | 53 | 'task_worker_num' => 1, 54 | 'task_enable_coroutine' => false, 55 | 56 | 'document_root' => BASE_PATH . '/public', 57 | 'static_handler_locations' => ['/'], 58 | 'enable_static_handler' => true, 59 | ], 60 | 'callbacks' => [ 61 | SwooleEvent::ON_BEFORE_START => [Hyperf\Framework\Bootstrap\ServerStartCallback::class, 'beforeStart'], 62 | SwooleEvent::ON_WORKER_START => [Hyperf\Framework\Bootstrap\WorkerStartCallback::class, 'onWorkerStart'], 63 | SwooleEvent::ON_PIPE_MESSAGE => [Hyperf\Framework\Bootstrap\PipeMessageCallback::class, 'onPipeMessage'], 64 | 65 | // Task callbacks 66 | SwooleEvent::ON_TASK => [Hyperf\Framework\Bootstrap\TaskCallback::class, 'onTask'], 67 | SwooleEvent::ON_FINISH => [Hyperf\Framework\Bootstrap\FinishCallback::class, 'onFinish'], 68 | ], 69 | ]; 70 | -------------------------------------------------------------------------------- /ai/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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /app/Game/Logic/GameStart.php: -------------------------------------------------------------------------------- 1 | _params['userinfo']['account']; 25 | $room_data = $this->getRoomData($account); 26 | $user_room_data = isset($room_data[$account]) ? json_decode($room_data[$account], true) : array(); 27 | if ($user_room_data) { 28 | //是否产生地主 29 | $master = isset($room_data['master']) ? $room_data['master'] : ''; 30 | if ($master) { 31 | $user_room_data['is_master'] = 1; 32 | if ($master == $account) { 33 | //此人是地主 34 | $user_room_data['master'] = 1; 35 | } 36 | } else { 37 | $user_room_data['is_master'] = 0; 38 | } 39 | 40 | //轮到谁出牌了 41 | $last_chair_id = isset($room_data['last_chair_id']) ? $room_data['last_chair_id'] : 0; 42 | $next_chair_id = isset($room_data['next_chair_id']) ? $room_data['next_chair_id'] : 0; 43 | $user_room_data['is_first_round'] = false; 44 | if ($next_chair_id > 0) { 45 | $user_room_data['index_chair_id'] = $next_chair_id; 46 | if ($next_chair_id == $last_chair_id) { 47 | //首轮出牌 48 | $user_room_data['is_first_round'] = true; 49 | } 50 | } else { 51 | //地主首次出牌 52 | if (isset($room_data[$master])) { 53 | $master_info = json_decode($room_data[$master], true); 54 | $user_room_data['index_chair_id'] = $master_info['chair_id']; 55 | //首轮出牌 56 | $user_room_data['is_first_round'] = true; 57 | } 58 | } 59 | 60 | //判断游戏是否结束 61 | $user_room_data['is_game_over'] = isset($room_data['is_game_over']) ? $room_data['is_game_over'] : false; 62 | //进入房间成功 63 | $msg = $user_room_data; 64 | $room_data = Packet::packFormat('OK', 0, $msg); 65 | $room_data = Packet::packEncode($room_data, MainCmd::CMD_SYS, SubCmd::ENTER_ROOM_SUCC_RESP); 66 | return $room_data; 67 | } else { 68 | $room_list = $this->getGameConf('room_list'); 69 | if ($room_list) { 70 | //判断是否在队列里面 71 | redis()->sAdd($room_list, $this->_params['userinfo']['account']); 72 | //投递异步任务 73 | $client = ApplicationContext::getContainer()->get(GameSyncTask::class); 74 | $client->gameRoomMatch([$this->_params['userinfo']['fd']]); 75 | } 76 | return 0; 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hyperf/hyperf-skeleton", 3 | "type": "project", 4 | "keywords": [ 5 | "php", 6 | "swoole", 7 | "framework", 8 | "hyperf", 9 | "microservice", 10 | "middleware", 11 | "hyperf-ddz" 12 | ], 13 | "description": "A coroutine framework that focuses on hyperspeed and flexible, specifically use for build microservices and middlewares.", 14 | "license": "Apache-2.0", 15 | "require": { 16 | "php": ">=7.2", 17 | "ext-swoole": ">=4.4", 18 | "hyperf/cache": "~1.1.0", 19 | "hyperf/command": "~1.1.0", 20 | "hyperf/config": "~1.1.0", 21 | "hyperf/db-connection": "~1.1.0", 22 | "hyperf/framework": "~1.1.0", 23 | "hyperf/guzzle": "~1.1.0", 24 | "hyperf/http-server": "~1.1.0", 25 | "hyperf/logger": "~1.1.0", 26 | "hyperf/memory": "~1.1.0", 27 | "hyperf/process": "~1.1.0", 28 | "hyperf/redis": "~1.1.0", 29 | "hyperf/json-rpc": "~1.1.0", 30 | "hyperf/rpc": "~1.1.0", 31 | "hyperf/rpc-client": "~1.1.0", 32 | "hyperf/rpc-server": "~1.1.0", 33 | "hyperf/service-governance": "~1.1.0", 34 | "hyperf/config-apollo": "~1.1.0", 35 | "hyperf/constants": "~1.1.0", 36 | "hyperf/async-queue": "~1.1.0", 37 | "hyperf/amqp": "~1.1.0", 38 | "hyperf/model-cache": "~1.1.0", 39 | "hyperf/elasticsearch": "~1.1.0", 40 | "hyperf/tracer": "~1.1.0", 41 | "hyperf/websocket-server": "^1.1", 42 | "hyperf/task": "^1.1", 43 | "hyperf/view": "^1.1", 44 | "smarty/smarty": "^3.1" 45 | }, 46 | "require-dev": { 47 | "swoft/swoole-ide-helper": "^4.2", 48 | "phpmd/phpmd": "^2.6", 49 | "friendsofphp/php-cs-fixer": "^2.14", 50 | "mockery/mockery": "^1.0", 51 | "doctrine/common": "^2.9", 52 | "phpstan/phpstan": "^0.11.2", 53 | "hyperf/devtool": "~1.1.0", 54 | "hyperf/testing": "~1.1.0" 55 | }, 56 | "suggest": { 57 | "ext-openssl": "Required to use HTTPS.", 58 | "ext-json": "Required to use JSON.", 59 | "ext-pdo": "Required to use MySQL Client.", 60 | "ext-pdo_mysql": "Required to use MySQL Client.", 61 | "ext-redis": "Required to use Redis Client." 62 | }, 63 | "autoload": { 64 | "psr-4": { 65 | "App\\": "app/" 66 | }, 67 | "files": [ 68 | "app/Helper.php" 69 | ] 70 | }, 71 | "autoload-dev": { 72 | "psr-4": { 73 | "HyperfTest\\": "./test/" 74 | } 75 | }, 76 | "minimum-stability": "dev", 77 | "prefer-stable": true, 78 | "extra": [], 79 | "scripts": { 80 | "post-root-package-install": [ 81 | "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" 82 | ], 83 | "post-autoload-dump": [ 84 | "init-proxy.sh" 85 | ], 86 | "test": "co-phpunit -c phpunit.xml --colors=always", 87 | "cs-fix": "php-cs-fixer fix $1", 88 | "analyse": "phpstan analyse --memory-limit 300M -l 0 -c phpstan.neon ./app ./config", 89 | "start": "php ./bin/hyperf.php start", 90 | "init-proxy": "init-proxy.sh" 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /storage/view/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 这是一个斗地主打牌测试工具 6 | 7 | 28 | 29 |
30 |

昵称:{$account}

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 | -------------------------------------------------------------------------------- /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/Controller/GameController.php: -------------------------------------------------------------------------------- 1 | fd} push success Mete: \n{"); 25 | $data = Packet::packDecode($frame->data); 26 | if (isset($data['code']) && $data['code'] == 0 && isset($data['msg']) && $data['msg'] == 'OK') { 27 | Log::show('Recv <<< cmd=' . $data['cmd'] . ' scmd=' . $data['scmd'] . ' len=' . $data['len'] . ' data=' . json_encode($data['data'])); 28 | //转发请求,代理模式处理,websocket路由到相关逻辑 29 | $data['serv'] = $server; 30 | //用户登陆信息 31 | $game_conf = config('game'); 32 | $user_info_key = sprintf($game_conf['user_info_key'], $frame->fd); 33 | $uinfo = redis()->get($user_info_key); 34 | if ($uinfo) { 35 | $data['userinfo'] = json_decode($uinfo, true); 36 | } else { 37 | $data['userinfo'] = array(); 38 | } 39 | $obj = new Dispatch($data); 40 | $back = "

404 Not Found


Swoole
\n"; 41 | if (!empty($obj->getStrategy())) { 42 | $back = $obj->exec(); 43 | if ($back) { 44 | $server->push($frame->fd, $back, WEBSOCKET_OPCODE_BINARY); 45 | } 46 | } 47 | Log::show('Tcp Strategy <<< data=' . $back); 48 | } else { 49 | Log::show($data['msg']); 50 | } 51 | Log::split('}'); 52 | } 53 | 54 | public function onClose(Server $server, int $fd, int $reactorId): void 55 | { 56 | //清除登陆信息变量 57 | $this->loginFail($fd, '3'); 58 | } 59 | 60 | public function onOpen(WebSocketServer $server, Request $request): void 61 | { 62 | $fd = $request->fd; 63 | $game_conf = config('game'); 64 | $query = $request->get; 65 | $cookie = $request->cookie; 66 | $token = ''; 67 | if (isset($cookie['USER_INFO'])) { 68 | $token = $cookie['USER_INFO']; 69 | } elseif (isset($query['token'])) { 70 | $token = $query['token']; 71 | } 72 | if ($token) { 73 | $uinfo = json_decode($token, true); 74 | //允许连接, 并记录用户信息 75 | $uinfo['fd'] = $fd; 76 | $redis = redis(); 77 | $user_bind_key = sprintf($game_conf['user_bind_key'], $uinfo['account']); 78 | $last_fd = (int)$redis->get($user_bind_key); 79 | //之前信息存在, 清除之前的连接 80 | if ($last_fd) { 81 | //处理双开的情况 82 | $this->loginFail($last_fd, '1'); 83 | $server->disconnect($last_fd); 84 | //清理redis. 85 | $redis->del($user_bind_key); //清除上一个绑定关系 86 | $redis->del(sprintf($game_conf['user_info_key'], $last_fd)); //清除上一个用户信息 87 | } 88 | //保存登陆信息 89 | $redis->set($user_bind_key, $fd, $game_conf['expire']); 90 | //设置绑定关系 91 | $redis->set(sprintf($game_conf['user_info_key'], $fd), json_encode($uinfo), $game_conf['expire']); 92 | $this->loginSuccess($server, $fd, $uinfo['account']); //登陆成功 93 | } else { 94 | $this->loginFail($fd, '2'); 95 | $server->disconnect($fd); 96 | } 97 | } 98 | 99 | /** 100 | * 登陆成功下发协议 101 | * @param $server 102 | * @param $fd 103 | * @param $account 104 | */ 105 | private function loginSuccess($server, $fd, $account) 106 | { 107 | //原封不动发回去 108 | if ($server->getClientInfo($fd) !== false) { 109 | //查询用户是否在房间里面 110 | $info = $this->getRoomData($account); 111 | $data = array('status' => 'success'); 112 | if (!empty($info)) { 113 | $data['is_room'] = 1; 114 | } else { 115 | $data['is_room'] = 0; 116 | } 117 | $data = Packet::packFormat('OK', 0, $data); 118 | $back = Packet::packEncode($data, MainCmd::CMD_SYS, SubCmd::LOGIN_SUCCESS_RESP); 119 | $server->push($fd, $back, WEBSOCKET_OPCODE_BINARY); 120 | } 121 | } 122 | 123 | /** 124 | * 发送登陆失败请求到客户端 125 | * @param $server 126 | * @param $fd 127 | * @param string $msg 128 | */ 129 | private function loginFail($fd, $msg = '') 130 | { 131 | //原封不动发回去 132 | $server = server(); 133 | if ($server->getClientInfo($fd) !== false) { 134 | $data = Packet::packFormat('OK', 0, array('data' => 'login fail' . $msg)); 135 | $back = Packet::packEncode($data, MainCmd::CMD_SYS, SubCmd::LOGIN_FAIL_RESP); 136 | $server->push($fd, $back, WEBSOCKET_OPCODE_BINARY); 137 | } 138 | } 139 | 140 | /** 141 | * 获取房间信息 142 | * @param $account 143 | * @return array 144 | */ 145 | protected function getRoomData($account) 146 | { 147 | $user_room_data = array(); 148 | //获取用户房间号 149 | $room_no = $this->getRoomNo($account); 150 | //房间信息 151 | $game_key = $this->getGameConf('user_room_data'); 152 | if ($game_key) { 153 | $user_room_key = sprintf($game_key, $room_no); 154 | $user_room_data = redis()->hGetAll($user_room_key); 155 | } 156 | return $user_room_data; 157 | } 158 | 159 | /** 160 | * 获取用户房间号 161 | * @param $account 162 | * @return mixed 163 | */ 164 | protected function getRoomNo($account) 165 | { 166 | $game_key = $this->getGameConf('user_room'); 167 | //获取用户房间号 168 | $room_key = sprintf($game_key, $account); 169 | $room_no = redis()->get($room_key); 170 | return $room_no ? $room_no : 0; 171 | } 172 | 173 | /** 174 | * 返回游戏配置 175 | * @param string $key 176 | * @return string 177 | */ 178 | protected function getGameConf($key = '') 179 | { 180 | $conf = config('game'); 181 | if (isset($conf[$key])) { 182 | return $conf[$key]; 183 | } else { 184 | return ''; 185 | } 186 | } 187 | } -------------------------------------------------------------------------------- /app/Task/GameSyncTask.php: -------------------------------------------------------------------------------- 1 | sCard($game_conf['room_list']); 30 | $serv = server(); 31 | if ($len >= 3) { 32 | //匹配成功, 下发手牌数据, 并进入房间数据 33 | $users = $users_key = $fds = array(); 34 | for ($i = 0; $i < 3; $i++) { 35 | $account = $redis->sPop($game_conf['room_list']); 36 | $key = sprintf($game_conf['user_bind_key'], $account); 37 | //根据账号获取fd 38 | $fds[$account] = $redis->get($key); 39 | //获取账号数 40 | $users[] = $account; 41 | } 42 | //获取房间号 43 | $room_no_key = $game_conf['user_room_no']; 44 | if ($redis->exists($room_no_key)) { 45 | $room_no = $redis->get($room_no_key); 46 | $room_no++; 47 | $redis->set($room_no_key, $room_no); 48 | } else { 49 | $room_no = intval(1000001); 50 | $redis->set($room_no_key, $room_no); 51 | } 52 | //存入房间号和用户对应关系 53 | foreach ($users as $v) { 54 | $user_key = sprintf($game_conf['user_room'], $v); 55 | $user_room[$user_key] = $room_no; 56 | } 57 | 58 | if (!empty($user_room)) { 59 | $redis->mset($user_room); 60 | } 61 | 62 | //随机发牌 63 | $obj = new DdzPoker(); 64 | $card = $obj->dealCards($users); 65 | 66 | //存入用户信息 67 | $room_data = array( 68 | 'room_no' => $room_no, 69 | 'hand' => $card['card']['hand'] 70 | ); 71 | foreach ($users as $k => $v) { 72 | $room_data['uinfo'][] = $v; 73 | $room_data[$v] = array( 74 | 'card' => $card['card'][$v], 75 | 'chair_id' => ($k + 1) 76 | ); 77 | } 78 | $user_room_data_key = sprintf($game_conf['user_room_data'], $room_no); 79 | $this->arrToHashInRedis($room_data, $user_room_data_key); 80 | //分别发消息给三个人 81 | foreach ($users as $k => $v) { 82 | if (isset($fds[$v])) { 83 | $data = Packet::packFormat('OK', 0, $room_data[$v]); 84 | $data = Packet::packEncode($data, MainCmd::CMD_SYS, SubCmd::ENTER_ROOM_SUCC_RESP); 85 | $serv->push($fds[$v], $data, WEBSOCKET_OPCODE_BINARY); 86 | } 87 | } 88 | } else { 89 | //匹配失败, 请继续等待 90 | $msg = array( 91 | 'status' => 'fail', 92 | 'msg' => '人数不够3人,请耐心等待!' 93 | ); 94 | $data = Packet::packFormat('OK', 0, $msg); 95 | $data = Packet::packEncode($data, MainCmd::CMD_SYS, SubCmd::ENTER_ROOM_FAIL_RESP); 96 | $serv->push($fd, $data, WEBSOCKET_OPCODE_BINARY); 97 | } 98 | } 99 | 100 | /** 101 | * 广播叫地主 102 | * @param $account 103 | * @param $calltype 104 | * @param $chair_id 105 | */ 106 | public function gameCall($account, $calltype, $chair_id) 107 | { 108 | $fds = $this->_getRoomFds($account); 109 | //匹配失败, 请继续等待 110 | $msg = array( 111 | 'account' => $account, 112 | 'calltype' => $calltype, 113 | 'chair_id' => $chair_id, 114 | 'calltime' => time() 115 | ); 116 | $data = Packet::packFormat('OK', 0, $msg); 117 | $data = Packet::packEncode($data, MainCmd::CMD_GAME, SubCmd::SUB_GAME_CALL_TIPS_RESP); 118 | $serv = server(); 119 | $this->pushToUsers($serv, $fds, $data); 120 | } 121 | 122 | /** 123 | * 当connetions属性无效时可以使用此方法,服务器广播消息, 此方法是给所有的连接客户端, 广播消息,通过方法getClientList广播 124 | * @param $serv 125 | * @param $data 126 | * @return array 127 | */ 128 | protected function pushToAll($serv, $data) 129 | { 130 | $client = array(); 131 | $start_fd = 0; 132 | while (true) { 133 | $conn_list = $serv->getClientList($start_fd, 10); 134 | if ($conn_list === false or count($conn_list) === 0) { 135 | echo "BroadCast finish\n"; 136 | break; 137 | } 138 | $start_fd = end($conn_list); 139 | foreach ($conn_list as $fd) { 140 | //获取客户端信息 141 | $client_info = $serv->getClientInfo($fd); 142 | $client[$fd] = $client_info; 143 | if (isset($client_info['websocket_status']) && $client_info['websocket_status'] == 3) { 144 | $serv->push($fd, $data, WEBSOCKET_OPCODE_BINARY); 145 | } 146 | } 147 | } 148 | return $client; 149 | } 150 | 151 | /** 152 | * 对多用发送信息 153 | * @param $serv 154 | * @param $users 155 | * @param $data 156 | */ 157 | protected function pushToUsers($serv, $users, $data) 158 | { 159 | foreach ($users as $fd) { 160 | //获取客户端信息 161 | $client_info = $serv->getClientInfo($fd); 162 | $client[$fd] = $client_info; 163 | if (isset($client_info['websocket_status']) && $client_info['websocket_status'] == 3) { 164 | $serv->push($fd, $data, WEBSOCKET_OPCODE_BINARY); 165 | } 166 | } 167 | } 168 | 169 | /** 170 | * 通过accounts获取fds 171 | * @param $account 172 | * @return array 173 | */ 174 | private function _getRoomFds($account) 175 | { 176 | $game_conf = config('game'); 177 | $user_room_data = $game_conf['user_room_data']; 178 | $uinfo = redis()->hGet($user_room_data, $account); 179 | $uinfo = json_decode($uinfo, true); 180 | $accs = isset($uinfo['account']) ? $uinfo['account'] : array(); 181 | $binds = $fds = array(); 182 | if (!empty($accs)) { 183 | foreach ($accs as $v) { 184 | $binds[] = sprintf($game_conf['user_bind_key'], $v); 185 | } 186 | $fds = redis()->mget($binds); 187 | } 188 | return $fds; 189 | } 190 | 191 | /** 192 | * 把php数组存入redis的hash表中 193 | * @param $arr 194 | * @param $hash_key 195 | */ 196 | protected function arrToHashInRedis($arr, $hash_key) 197 | { 198 | foreach ($arr as $key => $val) { 199 | redis()->hSet($hash_key, $key, json_encode($val)); 200 | } 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /app/Game/Core/AStrategy.php: -------------------------------------------------------------------------------- 1 | _params = $params; 35 | $this->obj_ddz = new DdzPoker(); 36 | } 37 | 38 | /** 39 | * 执行方法,每条游戏协议,实现这个方法就行 40 | */ 41 | abstract public function exec(); 42 | 43 | /** 44 | * 服务器广播消息, 此方法是给所有的连接客户端, 广播消息 45 | * @param $serv 46 | * @param $data 47 | */ 48 | protected function Broadcast($serv, $data) 49 | { 50 | foreach ($serv->connections as $fd) { 51 | $serv->push($fd, $data, WEBSOCKET_OPCODE_BINARY); 52 | } 53 | } 54 | 55 | /** 56 | * 当connetions属性无效时可以使用此方法,服务器广播消息, 此方法是给所有的连接客户端, 广播消息,通过方法getClientList广播 57 | * @param $serv 58 | * @param $data 59 | */ 60 | protected function BroadCast2($serv, $data) 61 | { 62 | $start_fd = 0; 63 | while (true) { 64 | $conn_list = $serv->getClientList($start_fd, 10); 65 | if ($conn_list === false or count($conn_list) === 0) { 66 | Log::show("BroadCast finish"); 67 | break; 68 | } 69 | $start_fd = end($conn_list); 70 | foreach ($conn_list as $fd) { 71 | //获取客户端信息 72 | $client_info = $serv->getClientInfo($fd); 73 | if (isset($client_info['websocket_status']) && $client_info['websocket_status'] == 3) { 74 | $serv->push($fd, $data, WEBSOCKET_OPCODE_BINARY); 75 | } 76 | } 77 | } 78 | } 79 | 80 | /** 81 | * 对多用发送信息 82 | * @param $serv 83 | * @param $users 84 | * @param $data 85 | */ 86 | protected function pushToUsers($serv, $users, $data) 87 | { 88 | foreach ($users as $fd) { 89 | //获取客户端信息 90 | $client_info = $serv->getClientInfo($fd); 91 | $client[$fd] = $client_info; 92 | if (isset($client_info['websocket_status']) && $client_info['websocket_status'] == 3) { 93 | $serv->push($fd, $data, WEBSOCKET_OPCODE_BINARY); 94 | } 95 | } 96 | } 97 | 98 | /** 99 | * 获取房间信息 100 | * @param $account 101 | * @return array 102 | */ 103 | protected function getRoomData($account) 104 | { 105 | $user_room_data = array(); 106 | //获取用户房间号 107 | $room_no = $this->getRoomNo($account); 108 | //房间信息 109 | $game_key = $this->getGameConf('user_room_data'); 110 | if ($game_key) { 111 | $user_room_key = sprintf($game_key, $room_no); 112 | $user_room_data = redis()->hGetAll($user_room_key); 113 | } 114 | return $user_room_data; 115 | } 116 | 117 | /** 118 | * 获取房间信息通过key 119 | * @param $account 120 | * @param $key 121 | * @return mixed 122 | */ 123 | protected function getRoomDataByKey($account, $key) 124 | { 125 | $data = array(); 126 | $no = $this->getRoomNo($account); 127 | $game_key = $this->getGameConf('user_room_data'); 128 | if ($no && $game_key) { 129 | $user_room_key = sprintf($game_key, $no); 130 | $user_room_data = redis()->hGet($user_room_key, $key); 131 | $data = json_decode($user_room_data, true); 132 | if (is_null($data)) { 133 | $data = $user_room_data; 134 | } 135 | } 136 | return $data; 137 | } 138 | 139 | /** 140 | * 获取用户房间号 141 | * @param $account 142 | * @return mixed 143 | */ 144 | protected function getRoomNo($account) 145 | { 146 | $game_key = $this->getGameConf('user_room'); 147 | //获取用户房间号 148 | $room_key = sprintf($game_key, $account); 149 | $room_no = redis()->get($room_key); 150 | return $room_no ? $room_no : 0; 151 | } 152 | 153 | /** 154 | * 获取房间信息通过key 155 | * @param $account 156 | * @return mixed 157 | */ 158 | protected function getRoomUserInfoDataByKey($account) 159 | { 160 | $user_data = array(); 161 | $no = $this->getRoomNo($account); 162 | $game_key = $this->getGameConf('user_room_data'); 163 | if ($no && $game_key) { 164 | //房间信息 165 | $user_room_key = sprintf($game_key, $no); 166 | $user_data = redis()->hGet($user_room_key, $account); 167 | $user_data = json_decode($user_data, true); 168 | } 169 | return $user_data; 170 | } 171 | 172 | /** 173 | * 设置房间用户玩牌信息 174 | * @param $account 175 | * @param $key 176 | * @param $value 177 | */ 178 | protected function setRoomData($account, $key, $value) 179 | { 180 | $no = $this->getRoomNo($account); 181 | $game_key = $this->getGameConf('user_room_data'); 182 | if ($no && $game_key) { 183 | $user_room_key = sprintf($game_key, $no); 184 | redis()->hSet($user_room_key, $key, $value); 185 | } 186 | } 187 | 188 | /** 189 | * 批量设置房间信息 190 | * @param $account 191 | * @param $params 192 | */ 193 | protected function muitSetRoomData($account, $params) 194 | { 195 | $no = $this->getRoomNo($account); 196 | $game_key = $this->getGameConf('user_room_data'); 197 | if ($no && $game_key) { 198 | $user_room_key = sprintf($game_key, $no); 199 | redis()->hMSet($user_room_key, $params); 200 | } 201 | } 202 | 203 | /** 204 | * 设置房间信息 205 | * @param $room_user_data 206 | * @param $account 207 | * @param $key 208 | * @param $value 209 | */ 210 | protected function setRoomUserInfoDataByKey($room_user_data, $account, $key, $value) 211 | { 212 | $no = $this->getRoomNo($account); 213 | $game_key = $this->getGameConf('user_room_data'); 214 | if ($no && $game_key) { 215 | $room_user_data[$key] = $value; 216 | $user_room_key = sprintf($game_key, $no); 217 | redis()->hSet($user_room_key, $account, json_encode($room_user_data)); 218 | } 219 | } 220 | 221 | /** 222 | * 通过accounts获取fds 223 | * @param $account 224 | * @return array 225 | */ 226 | protected function getRoomFds($account) 227 | { 228 | $accs = $this->getRoomDataByKey($account, 'uinfo'); 229 | $game_key = $this->getGameConf('user_bind_key'); 230 | $binds = $fds = array(); 231 | if (!empty($accs) && $game_key) { 232 | foreach ($accs as $v) { 233 | $binds[] = sprintf($game_key, $v); 234 | } 235 | $fds = redis()->mget($binds); 236 | } 237 | return $fds; 238 | } 239 | 240 | /** 241 | * 批量清除用户房间号 242 | * @param $users 243 | */ 244 | protected function clearRoomNo($users) 245 | { 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 | { 263 | foreach ($arr as $key => $val) { 264 | redis()->hSet($hash_key, $key, json_encode($val)); 265 | } 266 | } 267 | 268 | /** 269 | * 返回游戏配置 270 | * @param string $key 271 | * @return string 272 | */ 273 | protected function getGameConf($key = '') 274 | { 275 | $conf = config('game'); 276 | if (isset($conf[$key])) { 277 | return $conf[$key]; 278 | } else { 279 | return ''; 280 | } 281 | } 282 | 283 | /** 284 | * 设置游戏房间玩牌步骤信息, 方便后面录像回放 285 | * @param $account 286 | * @param $key 287 | * @param $value 288 | */ 289 | protected function setRoomPlayCardStep($account, $key, $value) 290 | { 291 | $no = $this->getRoomNo($account); 292 | $game_key = $this->getGameConf('user_room_play'); 293 | if ($no && $game_key) { 294 | $play_key = sprintf($game_key, $no);; 295 | redis()->hSet($play_key, $key, $value); 296 | } 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | //并下发出牌提示 122 | $step = array( 123 | 'round' => $round, //轮次 124 | 'hand_num' => $hand, //首次 125 | 'chair_id' => $out_cards['chair_id'], //出牌椅子 126 | 'account' => $account, //出牌账号 127 | 'show_type' => $out_cards['status'], //1,跟牌, 2, 过牌 128 | 'next_chair_id' => $next_chair_id, //下一个出牌的椅子id 129 | 'is_first_round' => $is_first_round, //是否为首轮, 下一个出牌人的情况 130 | 'card' => $out_cards['card'], //本次出牌 131 | 'card_type' => $card_type, //显示牌型 132 | 'last_card' => json_decode($last_card, true), //上次最大牌 133 | 'is_game_over' => $is_game_over //游戏是否结束 134 | ); 135 | 136 | //记录一下出牌数据, 记录没步骤录像数据 137 | $this->setRoomPlayCardStep($account, 'step_' . $hand, json_encode($step)); 138 | //广播打牌结果 139 | $ret = $this->gameOutCardResp($this->_params['serv'], $account, $step); 140 | //游戏结束, 重置游戏数据 141 | $this->gameOver($account, json_decode($user_room_data['uinfo'], true), $is_game_over); 142 | //记录步骤信息 143 | $logger = ApplicationContext::getContainer()->get(\Hyperf\Logger\LoggerFactory::class); 144 | $logger->get()->info(json_encode($step)); 145 | return $ret; 146 | } 147 | 148 | /** 149 | * 轮次开始 150 | * @param $user_room_data 151 | * @param $out_cards 152 | * @param $account 153 | * @param $hand 154 | * @param $next_chair_id 155 | * @return array 156 | */ 157 | protected function roundStart($user_room_data, $out_cards, $account, $hand, $next_chair_id) 158 | { 159 | //当前轮次 160 | $round = isset($user_room_data['round']) ? $user_room_data['round'] + 1 : 1; 161 | //本轮次开始时椅子id 162 | $start_chair_id = $out_cards['chair_id']; 163 | //本轮次最大牌椅子id 164 | $last_chair_id = $out_cards['chair_id']; 165 | //本轮次最大牌椅子i牌型 166 | $last_card_type = $out_cards['card_type']; 167 | //本轮次最大牌椅子牌值 168 | $last_card = $out_cards['card']; 169 | 170 | //结果存入redis 171 | $param = array( 172 | 'round' => $round, 173 | 'hand_num' => $hand, 174 | 'start_chair_id' => $start_chair_id, 175 | 'last_chair_id' => $last_chair_id, 176 | 'last_card_type' => $last_card_type, 177 | 'last_card' => json_encode($last_card), 178 | 'next_chair_id' => $next_chair_id 179 | ); 180 | $this->muitSetRoomData($account, $param); 181 | return $param; 182 | } 183 | 184 | /** 185 | * 轮次结束 186 | * @param $account 187 | * @param $last_chair_id 188 | * @param $next_chair_id 189 | * @param $hand 190 | */ 191 | protected function roundEnd($account, $last_chair_id, $hand, $next_chair_id) 192 | { 193 | //结果存入redis 194 | $param = array( 195 | 'start_chair_id' => $last_chair_id, 196 | 'last_card_type' => 0, 197 | 'last_card' => json_encode(array()), 198 | 'hand_num' => $hand, 199 | 'next_chair_id' => $next_chair_id 200 | ); 201 | $this->muitSetRoomData($account, $param); 202 | } 203 | 204 | /** 205 | * 跟牌 206 | * @param $out_cards 207 | * @param $account 208 | * @param $next_chair_id 209 | * @param $hand 210 | */ 211 | protected function roundFollow($out_cards, $account, $hand, $next_chair_id) 212 | { 213 | //跟牌 214 | $param = array(); 215 | if ($out_cards['status'] == 1) { 216 | //本轮次上一次最大牌椅子id 217 | $param = array( 218 | 'last_chair_id' => $out_cards['chair_id'], 219 | 'last_card' => json_encode($out_cards['card']), 220 | ); 221 | } 222 | $param['next_chair_id'] = $next_chair_id; 223 | $param['hand_num'] = $hand; 224 | //结果存入redis 225 | $this->muitSetRoomData($account, $param); 226 | } 227 | 228 | /** 229 | * 游戏结束 230 | * @param $account 231 | * @param $uinfo 232 | * @param $is_game_over 233 | */ 234 | protected function gameOver($account, $uinfo, $is_game_over): void 235 | { 236 | if ($is_game_over) { 237 | //设置游戏结束标识 238 | $this->setRoomData($account, 'is_game_over', $is_game_over); 239 | //清除数据, 进行下一轮玩牌, 随机分配 240 | $this->clearRoomNo($uinfo); 241 | } 242 | } 243 | 244 | /** 245 | * 设置我的手牌 246 | * @param $user_room_data 247 | * @param $cards 248 | * @param $account 249 | * @return mixed 250 | */ 251 | protected function setMyCard($user_room_data, $cards, $account) 252 | { 253 | //根据椅子查询手牌信息 254 | $my_card = json_decode($user_room_data[$account], true); 255 | $hand_card = array_unique(array_values(array_diff($my_card['card'], $cards['card']))); 256 | if (isset($my_card['out_card'])) { 257 | $out_card = array_unique(array_values(array_merge($my_card['out_card'], $cards['card']))); 258 | } else { 259 | $out_card = $cards['card']; 260 | } 261 | $my_card['card'] = $hand_card; 262 | $my_card['out_card'] = $out_card; 263 | //写会redis 264 | $this->setRoomData($account, $account, json_encode($my_card)); 265 | return $my_card; 266 | } 267 | 268 | /** 269 | * 根据椅子id找出这个一直用户的手牌 270 | * @param $user_room_data 271 | * @param $chair_id 272 | * @return array 273 | */ 274 | protected function findCardsByChairId($user_room_data, $chair_id) 275 | { 276 | $uinfo = json_decode($user_room_data['uinfo'], true); 277 | $cards = array(); 278 | foreach ($uinfo as $v) { 279 | $d = json_decode($user_room_data[$v], true); 280 | if (isset($d['chair_id']) && $d['chair_id'] == $chair_id) { 281 | $cards = $d['card']; 282 | break; 283 | } 284 | } 285 | return $cards; 286 | } 287 | 288 | /** 289 | * 向客户端发送出牌提示响应, 单发 290 | * @param $param 291 | * @return array|string 292 | */ 293 | protected function gameOutCard($param) 294 | { 295 | $data = Packet::packFormat('OK', 0, $param); 296 | $data = Packet::packEncode($data, MainCmd::CMD_GAME, SubCmd::SUB_GAME_OUT_CARD); 297 | return $data; 298 | } 299 | 300 | /** 301 | * 向客户端广播出牌响应, 群发 302 | * @param $serv 303 | * @param $account 304 | * @param $param 305 | * @return int 306 | */ 307 | protected function gameOutCardResp($serv, $account, $param) 308 | { 309 | $data = Packet::packFormat('OK', 0, $param); 310 | $data = Packet::packEncode($data, MainCmd::CMD_GAME, SubCmd::SUB_GAME_OUT_CARD_RESP); 311 | $this->pushToUsers($serv, $this->getRoomFds($account), $data); 312 | //并提示成功 313 | return $this->gameOutCard(array('status' => 0, 'msg' => '出牌成功', 'data' => $param)); 314 | } 315 | } -------------------------------------------------------------------------------- /ai/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 | { 133 | if ($account) { 134 | $this->account = $account; 135 | } 136 | } 137 | 138 | /** 139 | * 运行服务器 140 | */ 141 | public function run() 142 | { 143 | if ($this->account) { 144 | $this->createConnection(); 145 | } else { 146 | \App\Game\Core\Log::show("账号错误!"); 147 | } 148 | } 149 | 150 | /** 151 | * 创建链接 152 | */ 153 | protected function createConnection() 154 | { 155 | co(function () { 156 | $cli = new \Swoole\Coroutine\Http\Client(self::IP, self::PORT); 157 | $cli->set($this->_setconfig); 158 | $cli->setHeaders($this->_header); 159 | $cli->setMethod("GET"); 160 | $self = $this; 161 | $data = array('account' => $this->account); 162 | $token = json_encode($data); 163 | $ret = $cli->upgrade('/?token=' . $token); 164 | if ($ret && $cli->connected) { 165 | //清除断线重连定时器, 断线重连次数重置为0 166 | Swoole\Timer::clear($this->reback_timer); 167 | $this->reback_count = 0; 168 | // $self->chatMsgReq($cli); //测试聊天请求 169 | $self->heartAskReq($cli); //发送心跳 170 | while (true) { 171 | $ret = $self::onMessage($cli, $cli->recv()); 172 | if (!$ret) { 173 | break; 174 | } 175 | } 176 | 177 | } 178 | }); 179 | } 180 | 181 | /** 182 | * websocket 消息处理 183 | * @param $cli 184 | * @param $frame 185 | * @return bool 186 | */ 187 | public function onMessage($cli, $frame) 188 | { 189 | \App\Game\Core\Log::show('原数据:' . $frame->data); 190 | $ret = false; 191 | if ($cli->connected && $frame) { 192 | $total_data = $frame->data; 193 | $total_len = strlen($total_data); 194 | if ($total_len < 4) { 195 | //清除定时器 196 | Swoole\Timer::clear($this->timer); 197 | //断开链接 198 | $cli->close(); 199 | \App\Game\Core\Log::show('数据包格式有误!'); 200 | } else { 201 | //需要进行粘包处理 202 | $off = 0; //结束时指针 203 | while ($total_len > $off) { 204 | $header = substr($total_data, $off, 4); 205 | $arr = unpack("Nlen", $header); 206 | $len = isset($arr['len']) ? $arr['len'] : 0; 207 | if ($len) { 208 | $data = substr($total_data, $off, $off + $len + 4); 209 | $body = \App\Game\Core\Packet::packDecode($data); 210 | $this->dispatch($cli, $body); 211 | $off += $len + 4; 212 | } else { 213 | break; 214 | } 215 | } 216 | } 217 | $ret = true; 218 | } else { 219 | //清除定时器 220 | Swoole\Timer::clear($this->heart_timer); 221 | Swoole\Timer::clear($this->reback_timer); 222 | //链接断开, 可以尝试断线重连逻辑 223 | $cli->close(); 224 | \App\Game\Core\Log::show('链接断开: 清除定时器, 断开链接!'); 225 | //断线重连逻辑 226 | $this->rebackConnection(); 227 | } 228 | return $ret; 229 | } 230 | 231 | /** 232 | * 断线重连 233 | */ 234 | protected function rebackConnection() 235 | { 236 | \App\Game\Core\Log::show('断线重连开始'); 237 | //定时器发送数据,发送心跳数据 238 | $this->reback_timer = Swoole\Timer::tick($this->reback_interval, function () { 239 | if ($this->reback_count < $this->reback_times) { 240 | $this->reback_count++; 241 | $this->createConnection(); 242 | \App\Game\Core\Log::show('断线重连' . $this->reback_count . '次'); 243 | } else { 244 | Swoole\Timer::clear($this->reback_timer); 245 | Swoole\Timer::clear($this->heart_timer); 246 | } 247 | }); 248 | } 249 | 250 | /** 251 | * 转发到不同的逻辑处理 252 | * @param $cli 253 | * @param $cmd 254 | * @param $scmd 255 | * @param $data 256 | */ 257 | protected function dispatch($cli, $data) 258 | { 259 | $cmd = isset($data['cmd']) ? intval($data['cmd']) : 0; 260 | $scmd = isset($data['scmd']) ? intval($data['scmd']) : 0; 261 | $len = isset($data['len']) ? intval($data['len']) : 0; 262 | $method = isset($this->route[$cmd][$scmd]) ? $this->route[$cmd][$scmd] : ''; 263 | if ($method) { 264 | if ($method != 'heartAskResp') { 265 | \App\Game\Core\Log::show('----------------------------------------------------------------------------------------------'); 266 | \App\Game\Core\Log::show('cmd = ' . $cmd . ' scmd =' . $scmd . ' len=' . $len . ' method=' . $method); 267 | } 268 | $this->$method($cli, $data['data']['data']); 269 | } else { 270 | \App\Game\Core\Log::show('cmd = ' . $cmd . ' scmd =' . $scmd . ' ,method is not exists'); 271 | } 272 | } 273 | 274 | /** 275 | * 聊天请求 276 | * @param $cli 277 | */ 278 | protected function chatMsgReq($cli) 279 | { 280 | if ($cli->connected) { 281 | $msg = array('data' => 'this is a test msg'); 282 | $data = \App\Game\Core\Packet::packEncode($msg, \App\Game\Conf\MainCmd::CMD_GAME, \App\Game\Conf\SubCmd::CHAT_MSG_REQ); 283 | $cli->push($data, WEBSOCKET_OPCODE_BINARY); 284 | } 285 | } 286 | 287 | /** 288 | * 触发心跳 289 | * @param $cli 290 | */ 291 | protected function heartAskReq($cli) 292 | { 293 | //定时器发送数据,发送心跳数据 294 | $this->heart_timer = Swoole\Timer::tick($this->heart_interval, function () use ($cli) { 295 | list($t1, $t2) = explode(' ', microtime()); 296 | $time = (float)sprintf('%.0f', (floatval($t1) + floatval($t2)) * 1000); 297 | $msg = array('time' => $time); 298 | $data = \App\Game\Core\Packet::packEncode($msg, \App\Game\Conf\MainCmd::CMD_SYS, \App\Game\Conf\SubCmd::HEART_ASK_REQ); 299 | $ret = $cli->push($data, WEBSOCKET_OPCODE_BINARY); 300 | if (!$ret) { 301 | $this->loginFail($cli); 302 | } 303 | }); 304 | } 305 | 306 | /** 307 | * 触发游戏开始 308 | * @param $cli 309 | */ 310 | protected function gameStartReq($cli) 311 | { 312 | if ($cli->connected) { 313 | $msg = array('data' => 'game start'); 314 | $data = \App\Game\Core\Packet::packEncode($msg, \App\Game\Conf\MainCmd::CMD_GAME, \App\Game\Conf\SubCmd::SUB_GAME_START_REQ); 315 | $cli->push($data, WEBSOCKET_OPCODE_BINARY); 316 | } 317 | } 318 | 319 | /** 320 | * 发送叫地主请求 321 | * @param $cli 322 | * @param int $status 0表示不叫地主, 1表示叫地主 323 | */ 324 | protected function gameCallReq($cli, $status = 0) 325 | { 326 | if ($cli->connected) { 327 | $data = array('type' => $status); 328 | $data = \App\Game\Core\Packet::packEncode($data, \App\Game\Conf\MainCmd::CMD_GAME, \App\Game\Conf\SubCmd::SUB_GAME_CALL_REQ); 329 | $cli->push($data, WEBSOCKET_OPCODE_BINARY); 330 | } 331 | } 332 | 333 | /** 334 | * 出牌请求 335 | * @param $cli 336 | * @param bool $is_first_round 是否为首轮, 首轮必须出牌 337 | */ 338 | protected function outCardReq($cli, $is_first_round = false) 339 | { 340 | if ($cli->connected) { 341 | \App\Game\Core\Log::show("开始出牌:"); 342 | if ($is_first_round) { 343 | $status = 1; 344 | $card = array(array_shift($this->hand_card)); //第一张牌, 打出去 345 | } else { 346 | //跟牌默认过牌, TODO:需要实现跟牌逻辑, 需要从自己手牌中找出打过上次牌的牌, 根据情况决定是否跟牌 347 | $status = 0; //出牌状态随机 348 | $card = array(); 349 | } 350 | $msg = array( 351 | 'status' => $status, //打牌还是过牌, 1跟牌, 0是过牌 352 | 'chair_id' => $this->chair_id, 353 | 'card' => $card, 354 | ); 355 | $data = \App\Game\Core\Packet::packEncode($msg, \App\Game\Conf\MainCmd::CMD_GAME, \App\Game\Conf\SubCmd::SUB_GAME_OUT_CARD_REQ); 356 | $cli->push($data, WEBSOCKET_OPCODE_BINARY); 357 | } 358 | } 359 | 360 | 361 | /** 362 | * 响应登录失败 363 | * @param $cli 364 | */ 365 | protected function loginFailResp($cli, $data) 366 | { 367 | $cli->close(); 368 | Swoole\Timer::clear($this->heart_timer); 369 | Swoole\Timer::clear($this->reback_timer); 370 | \App\Game\Core\Log::show("关闭客户端, 清除定时器"); 371 | } 372 | 373 | /** 374 | * 响应登录成功 375 | * @param $cli 376 | */ 377 | protected function loginSucessResp($cli, $data) 378 | { 379 | //登录成功, 开始游戏逻辑 380 | \App\Game\Core\Log::show("登录成功, 开始游戏请求"); 381 | $this->gameStartReq($cli); 382 | } 383 | 384 | /** 385 | * 响应处理心跳 386 | * @param $cli 387 | */ 388 | protected function heartAskResp($cli, $data) 389 | { 390 | //定时器发送数据,发送心跳数据 391 | \App\Game\Core\Log::show('心跳(毫秒):' . $data['time']); 392 | } 393 | 394 | /** 395 | * 响应处理聊天 396 | * @param $cli 397 | */ 398 | protected function chatMsgResp($cli, $data) 399 | { 400 | //定时器发送数据,发送心跳数据 401 | \App\Game\Core\Log::show('聊天内容:' . json_encode($data)); 402 | } 403 | 404 | /** 405 | * 触发游戏开始 406 | * @param $cli 407 | */ 408 | protected function gameStartResp($cli, $data) 409 | { 410 | \App\Game\Core\Log::show('游戏场景数据:' . json_encode($data)); 411 | } 412 | 413 | /** 414 | * 解说用户信息协议 415 | * @param $cli 416 | */ 417 | protected function userInfoResp($cli, $data) 418 | { 419 | \App\Game\Core\Log::show('用户数据数据:' . json_encode($data)); 420 | } 421 | 422 | /** 423 | * 进入房间后, 开始抢地主 424 | * @param $cli 425 | */ 426 | protected function enterRoomSuccResp($cli, $data) 427 | { 428 | if ($data['is_game_over']) { 429 | \App\Game\Core\Log::show('游戏结束'); 430 | //触发开始游戏 431 | $this->gameStartReq($cli); 432 | } else { 433 | \App\Game\Core\Log::show('进入房间成功数据:' . json_encode($data)); 434 | //保存用户信息和手牌信息 435 | $this->chair_id = $data['chair_id']; 436 | $this->hand_card = $data['card']; 437 | $this->my_room_info = $data; 438 | //如果没有叫地主, 触发叫地主动作 439 | if (!isset($data['calltype'])) { 440 | //根据自己的牌是否可以发送是否叫地主, 0,不叫, 1,叫地主, 2, 抢地主 441 | $obj = new \App\Game\Core\DdzPoker(); 442 | $ret = $obj->isGoodCard($this->hand_card); 443 | $status = $ret ? 1 : 0; 444 | //发送是否叫地主操作 445 | $this->gameCallReq($cli, $status); 446 | } 447 | //是否轮到自己出牌, 如果是, 请出牌 448 | if (isset($data['index_chair_id']) && $data['index_chair_id'] == $this->chair_id) { 449 | if (isset($data['is_first_round']) && $data['is_first_round']) { 450 | //首轮出牌 451 | \App\Game\Core\Log::show('请出牌'); 452 | } else { 453 | //跟牌操作 454 | \App\Game\Core\Log::show('请跟牌'); 455 | } 456 | $this->outCardReq($cli, $data['is_first_round']); 457 | } 458 | } 459 | } 460 | 461 | /** 462 | * 自己叫完地主提示响应 463 | * @param $cli 464 | */ 465 | protected function gameCallResp($cli, $data) 466 | { 467 | \App\Game\Core\Log::show('叫地主成功提示:' . json_encode($data)); 468 | } 469 | 470 | /** 471 | * 叫完地主广播提示 472 | * @param $cli 473 | */ 474 | protected function gameCallTipsResp($cli, $data) 475 | { 476 | $tips = $data['calltype'] ? $data['account'] . '叫地主' : $data['account'] . '不叫'; 477 | \App\Game\Core\Log::show('广播叫地主提示:' . $tips); 478 | } 479 | 480 | /** 481 | * 触发游戏开始 482 | * @param $cli 483 | */ 484 | protected function catchGameCardResp($cli, $data) 485 | { 486 | $tips = $data['user'] . '摸底牌' . $data['hand_card']; 487 | \App\Game\Core\Log::show('摸底牌广播:' . $tips); 488 | if (isset($data['chair_id']) && $data['chair_id'] == $this->chair_id) { 489 | //合并手牌 490 | $hand_card = json_decode($data['hand_card'], true); 491 | $this->hand_card = $this->getDdzObj()->_sortCardByGrade(array_merge($this->hand_card, $hand_card)); 492 | \App\Game\Core\Log::show('地主[' . $this->account . ']出牌:' . json_encode($this->hand_card)); 493 | //地主首次出牌 494 | $this->outCardReq($cli, true); 495 | } 496 | } 497 | 498 | /** 499 | * 出牌提示 500 | * @param $cli 501 | */ 502 | protected function gameOutCard($cli, $data) 503 | { 504 | \App\Game\Core\Log::show('出牌提示:' . json_encode($data)); 505 | //移除手牌 506 | if (isset($data['status']) == 0 && isset($data['data']['card'])) { 507 | $this->hand_card = array_unique(array_values(array_diff($this->hand_card, $data['data']['card']))); 508 | } 509 | } 510 | 511 | /** 512 | * 出牌广播 513 | * @param $cli 514 | * @param $data 515 | */ 516 | protected function gameOutCardResp($cli, $data) 517 | { 518 | \App\Game\Core\Log::show('出牌广播提示:' . json_encode($data)); 519 | if (isset($data['is_game_over']) && $data['is_game_over']) { 520 | $tips = '广播:游戏结束,' . $data['account'] . '胜利, 请点击"开始游戏",进行下一轮游戏'; 521 | \App\Game\Core\Log::show($tips); 522 | //触发开始游戏 523 | $this->gameStartReq($cli); 524 | } else { 525 | $play = (isset($data['show_type']) && $data['show_type'] == 1) ? '跟牌' : '过牌'; 526 | $play = (isset($data['last_card']) && empty($data['last_card'])) ? '出牌' : $play; 527 | $last_card = !empty($data['last_card']) ? json_encode($data['last_card']) : '无'; 528 | $out_card = !empty($data['card']) ? json_encode($data['card']) : '无'; 529 | $tips = '广播: 第' . $data['round'] . '回合,第' . $data['hand_num'] . '手出牌, ' . $data['account'] . $play . ', 上次牌值是' . $last_card . ', 本次出牌值是' . $out_card . ', 本次出牌牌型' . $data['card_type']; 530 | \App\Game\Core\Log::show($tips); 531 | //下次出牌是否轮到自己, 轮到自己, 请出牌 532 | if (isset($data['next_chair_id']) && $data['next_chair_id'] == $this->chair_id) { 533 | //出牌请求, 默认过牌操作 534 | if (isset($data['is_first_round']) && $data['is_first_round']) { 535 | //首轮出牌 536 | \App\Game\Core\Log::show('请出牌'); 537 | //地主首次出牌 538 | } else { 539 | //跟牌操作 540 | \App\Game\Core\Log::show('请跟牌'); 541 | //地主首次出牌 542 | } 543 | $this->outCardReq($cli, $data['is_first_round']); 544 | } 545 | } 546 | } 547 | 548 | /** 549 | * 说有没处理的方法, 输出 550 | * @param $name 551 | * @param $arguments 552 | */ 553 | public function __call($name, $arguments) 554 | { 555 | \App\Game\Core\Log::show($name . ':' . json_encode($arguments[1])); 556 | } 557 | 558 | /** 559 | * 获取手牌对象 560 | */ 561 | public function getDdzObj() 562 | { 563 | if ($this->ddz === null) { 564 | $this->ddz = new \App\Game\Core\DdzPoker(); 565 | } 566 | return $this->ddz; 567 | } 568 | } 569 | 570 | $ai = new Ai($argv[1]); 571 | $ai->run(); 572 | -------------------------------------------------------------------------------- /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 | /** 139 | * 随机获取1张牌,不包括王 140 | */ 141 | public static function getOneCard() 142 | { 143 | $card = self::$card_value_list; 144 | unset($card[79]); 145 | $card_key = array_rand($card, 1); 146 | if (!is_array($card_key)) { 147 | $card_key = array($card_key); 148 | } 149 | return $card_key; 150 | } 151 | 152 | /** 153 | * 去除赖子,并且排序 154 | */ 155 | private static function exceptLaizi($arr) 156 | { 157 | $key = array_search(self::$laizi_value, $arr); //键值有可能0 158 | if ($key !== false) { 159 | unset($arr[$key]); 160 | self::$is_laizi = true; 161 | } else { 162 | self::$is_laizi = false; 163 | } 164 | sort($arr); 165 | return $arr; 166 | } 167 | 168 | /** 169 | * 获取牌内容,根据牌的key,获取牌的内容 170 | */ 171 | private static function getCard($arr) 172 | { 173 | $card = array(); 174 | foreach ($arr as $v) { 175 | $card[$v] = self::$card_value_list[$v]; 176 | } 177 | return $card; 178 | } 179 | 180 | /** 181 | * 获取牌内容,并显示花色, 方便直观查看 182 | */ 183 | public static function showCard($arr) 184 | { 185 | $show = array(); 186 | $card = self::getCard($arr); 187 | foreach ($card as $k => $v) { 188 | if ($k != self::$laizi_value) { 189 | $key = floor($k / 16); 190 | $show[] = self::$card_color[$key] . '_' . $v; 191 | } else { 192 | $show[] = $v; 193 | } 194 | 195 | } 196 | return implode(',', $show); 197 | } 198 | 199 | /** 200 | * 不带赖子皇家同花顺 201 | */ 202 | public static function isBigTongHuaShun() 203 | { 204 | return (self::$is_tonghua && self::$is_shunzi && self::$is_big_shunzi && !self::$is_laizi) ? true : false; 205 | } 206 | 207 | /** 208 | * 带来赖子皇家同花顺 209 | */ 210 | public static function isBigTongHuaShunByLaizi() 211 | { 212 | return (self::$is_tonghua && self::$is_shunzi && self::$is_big_shunzi && self::$is_laizi) ? true : false; 213 | } 214 | 215 | /** 216 | * 是否为同花顺 217 | */ 218 | public static function isTongHuaShun() 219 | { 220 | return (self::$is_tonghua && self::$is_shunzi) ? true : false; 221 | } 222 | 223 | /** 224 | * 是否为同花牌,判断同花的算法 225 | */ 226 | public static function isTongHua($arr) 227 | { 228 | $sub = array(); 229 | foreach ($arr as $v) { 230 | $sub[] = floor($v / 16); 231 | } 232 | $u = array_unique($sub); 233 | if (count($u) == 1) { 234 | self::$is_tonghua = true; 235 | } else { 236 | self::$is_tonghua = false; 237 | } 238 | return self::$is_tonghua; 239 | } 240 | 241 | /** 242 | * 是否为顺子牌,判断顺子的算法 243 | */ 244 | public static function isShunZi($arr) 245 | { 246 | $flag = 0; 247 | $card = self::getCard($arr); 248 | asort($card); 249 | $min = key($card) % 16; 250 | if ($min >= 2 && $min <= 10) { 251 | //最小或者最大顺子,需要特殊处理 252 | /* if(($min == 2 || $min == 10) && array_search('A', $card) !== false) { 253 | $flag++; 254 | } */ 255 | if (array_search('A', $card) !== false) { 256 | if ($min == 2) { 257 | $min = 1; 258 | } elseif ($min == 10) { 259 | $flag++; 260 | } 261 | } 262 | $cnt = count($arr); 263 | for ($i = 1; $i < 5; $i++) { 264 | $next = $min + $i; 265 | if (in_array($next, $arr) || in_array(($next + 16), $arr) || in_array(($next + 32), $arr) || in_array(($next + 48), $arr)) { 266 | $flag++; 267 | } 268 | } 269 | } 270 | if ($flag == $cnt - 1) { 271 | self::$is_shunzi = true; 272 | } else { 273 | self::$is_shunzi = false; 274 | } 275 | //是否为最大顺子,是true,否false 276 | if ($min == 10) { 277 | self::$is_big_shunzi = true; 278 | } else { 279 | self::$is_big_shunzi = false; 280 | } 281 | return self::$is_shunzi; 282 | } 283 | 284 | /** 285 | * 取模值,算对子,两对,三张,四条,5条的算法 286 | */ 287 | public static function _getModValue($arr) 288 | { 289 | $flag = $type = 0; 290 | $mod = array(); 291 | foreach ($arr as $k => $v) { 292 | $mod[] = $v % 16; 293 | } 294 | $v = array_count_values($mod); 295 | $cnt = count($v); 296 | if (self::$is_laizi) { 297 | if (in_array(1, $v) && $cnt == 4) { 298 | //对子 299 | $card = self::getCard($arr); 300 | if (array_search('A', $card) !== false || array_search('K', $card) !== false) { 301 | $type = 1; //对K或更大 302 | } 303 | } elseif (in_array(2, $v) && $cnt == 3) { 304 | $type = 3; //三张 305 | } elseif (in_array(2, $v) && $cnt == 2) { 306 | $type = 4; //葫芦 307 | } elseif (in_array(3, $v)) { 308 | $type = 5; //四条 309 | } elseif (in_array(4, $v)) { 310 | $type = 6; //五条 311 | } 312 | } else { 313 | if (in_array(2, $v) && $cnt == 4) { 314 | //对子 315 | $card = self::getCard($arr); 316 | $card_key = array_count_values($card); 317 | arsort($card_key); 318 | $kw = key($card_key); 319 | if ($kw == 'A' || $kw == 'K') { 320 | $type = 1; //对K或更大 321 | } 322 | } elseif (in_array(2, $v) && $cnt == 3) { 323 | $type = 2; //两对 324 | } elseif (in_array(3, $v) && $cnt == 3) { 325 | $type = 3; //三张 326 | } elseif (in_array(3, $v) && $cnt == 2) { 327 | $type = 4; //葫芦 328 | } elseif (in_array(4, $v)) { 329 | $type = 5; //四条 330 | } 331 | } 332 | return $type; 333 | } 334 | 335 | /** 336 | * 五张 337 | */ 338 | public static function isWuZhang($type) 339 | { 340 | return $type == 6 ? true : false; 341 | } 342 | 343 | /** 344 | * 四张 345 | */ 346 | public static function isSiZhang($type) 347 | { 348 | return $type == 5 ? true : false; 349 | } 350 | 351 | /** 352 | * 葫芦 353 | */ 354 | public static function isHulu($type) 355 | { 356 | return $type == 4 ? true : false; 357 | } 358 | 359 | /** 360 | * 三张 361 | */ 362 | public static function isSanZhang($type) 363 | { 364 | return $type == 3 ? true : false; 365 | } 366 | 367 | /** 368 | * 两对 369 | */ 370 | public static function isLiangDui($type) 371 | { 372 | return $type == 2 ? true : false; 373 | } 374 | 375 | /** 376 | * 大于对K或更大 377 | */ 378 | public static function isDaYuQDui($type) 379 | { 380 | return $type == 1 ? true : false; 381 | } 382 | 383 | /** 384 | * 检查牌型,判断用户所翻的牌为那种牌型 385 | */ 386 | public static function checkCardType($arr) 387 | { 388 | //去除赖子牌 389 | $arr_card = self::exceptLaizi($arr); 390 | $type = self::_getModValue($arr_card); 391 | if (self::isWuZhang($type)) { 392 | return 9; //五条 393 | } elseif (self::isSiZhang($type)) { 394 | return 7; //四条 395 | } elseif (self::isHulu($type)) { 396 | return 6; //葫芦,三张两对 397 | } elseif (self::isSanZhang($type)) { 398 | return 3; //三张 399 | } elseif (self::isLiangDui($type)) { 400 | return 2; //两对 401 | } else { 402 | $back = 0; 403 | if (self::isDaYuQDui($type)) { 404 | $back = 1; //对K或者大于 405 | } 406 | if (self::isShunZi($arr_card)) { 407 | $back = 4; //是否为顺子 408 | } 409 | if (self::isTongHua($arr_card)) { 410 | $back = 5; //是否为同花 411 | } 412 | if (self::isTongHuaShun()) { 413 | $back = 8; //是否为同花顺 414 | } 415 | if (self::isBigTongHuaShunByLaizi()) { 416 | $back = 10; //带赖子皇家同花顺 417 | } 418 | if (self::isBigTongHuaShun()) { 419 | $back = 11; //皇家同花顺 420 | } 421 | return $back; 422 | } 423 | } 424 | 425 | /** 426 | * 找出牌型里那些牌需要高亮显示 427 | */ 428 | public static function highLight($arr, $type) 429 | { 430 | $card_key = array(); 431 | $card = self::getCard($arr); 432 | $val = array_count_values($card); 433 | if ($type > 3) { 434 | $card_key = $arr; 435 | } elseif ($type == 3) { 436 | //三条 437 | arsort($val); 438 | $kw = key($val); 439 | $card_key = array(); 440 | foreach ($card as $k => $v) { 441 | if ($v == $kw || $k == self::$laizi_value) { 442 | $card_key[] = $k; 443 | } 444 | } 445 | } elseif ($type == 2) { 446 | //两对 447 | $kw = $card_key = array(); 448 | foreach ($val as $k => $v) { 449 | if ($v == 2) { 450 | $kw[] = $k; 451 | } 452 | } 453 | foreach ($card as $k => $v) { 454 | if (in_array($v, $kw)) { 455 | $card_key[] = $k; 456 | } 457 | } 458 | } elseif ($type == 1) { 459 | //对A后者对K 460 | foreach ($card as $k => $v) { 461 | if (in_array($v, array('A', 'K')) || $k == self::$laizi_value) { 462 | $card_val[$k] = $v; 463 | } 464 | } 465 | $t_val = array_count_values($card_val); 466 | arsort($t_val); 467 | $kw = key($t_val); 468 | if (!self::$is_laizi) { 469 | if (count($t_val) > 1) { 470 | foreach ($card_val as $k => $v) { 471 | if ($kw != $v) { 472 | unset($card_val[$k]); 473 | } 474 | } 475 | } 476 | } else { 477 | //去除k 478 | if (count($t_val) > 2) { 479 | foreach ($card_val as $k => $v) { 480 | if ($v == 'K') { 481 | unset($card_val[$k]); 482 | } 483 | } 484 | } 485 | } 486 | $card_key = array_keys($card_val); 487 | } 488 | return $card_key; 489 | } 490 | 491 | /** 492 | * 是否翻倍, 玩家翻倍处理 493 | */ 494 | public static function getIsDoubleCard($m_card = 2, $pos = 2, $arr = array()) 495 | { 496 | $list = self::$card_value_list; 497 | unset($list[self::$laizi_value]); //去除赖子大王 498 | $card_list = array_rand($list, 4); 499 | //概率运算 500 | if (!empty($arr)) { 501 | $rate = self::_getRate($arr); 502 | } else { 503 | $rate = self::_getRate(self::$is_double_rate); 504 | } 505 | 506 | $min = $m_card % 16; 507 | //拿到最大牌A和最小牌2的概率需要特殊处理一下 508 | if ($min == 1 && $rate == 3) { 509 | //最大牌A出现, 对方肯定是平手或者输 510 | $rate = rand(1, 2); 511 | } elseif ($min == 2 && $rate == 1) { 512 | //最小牌2,出现对方肯定是平手或者赢 // $rate = rand(2,3); 513 | $rate = rand(2, 3); 514 | } 515 | //最小牌 516 | if ($rate == 2) { 517 | //不翻倍,奖励不扣除 518 | $key = $min; 519 | } elseif ($rate == 3) { 520 | //翻倍,奖励累加, 系统数, 发大牌 521 | if ($min == 13) { 522 | $key = 1; 523 | } else { 524 | $key = rand($min + 1, 13); 525 | } 526 | } else { 527 | //不翻倍,丢失全部奖励,系统赢发小牌 528 | if ($min == 1) { 529 | $key = rand(2, 13); 530 | } else { 531 | $key = rand(2, $min - 1); 532 | } 533 | } 534 | //根据key组牌 535 | $card_val = array($key, $key + 16, $key + 32, $key + 48); 536 | //去除相同的值 537 | $card_val = array_diff($card_val, $card_list); 538 | $card_key = array_rand($card_val, 1); 539 | $card_list[$pos] = $card_val[$card_key]; 540 | return 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)); 541 | } 542 | 543 | /** 544 | * 计算概率算法 545 | * @param array $prizes 奖品概率数组 546 | * 格式:array(奖品id => array( 'rate'=>概率),奖品id => array('rate'=>概率)) 547 | * @return int 548 | */ 549 | private static function _getRate($arr = array()) 550 | { 551 | $key = 0; 552 | //首先生成一个1W内的数 553 | $rid = rand(1, 10000); 554 | //概率值(按设置累加) 555 | $rate = 0; 556 | foreach ($arr as $k => $v) { 557 | //根据设置的概率向上累加 558 | $rate += $v; 559 | //如果生成的概率数小于或等于此数据,表示当前道具ID即是,退出查找 560 | if ($rid <= $rate) { 561 | $key = $k; 562 | break; 563 | } 564 | } 565 | return $key; 566 | } 567 | 568 | /** 569 | * 获取牌型结果 570 | */ 571 | public static function getCardType($arr) 572 | { 573 | $type = self::checkCardType($arr); 574 | $highlight = self::highLight($arr, $type); 575 | return array('card' => $arr, 'type' => $type, 'typenote' => self::$card_type[$type], 'rate' => self::$card_rate[$type], 'highlight' => $highlight); 576 | } 577 | 578 | /** 579 | * 设置翻倍的概率 580 | */ 581 | public static function setRate($rate = array()) 582 | { 583 | if (empty($rate)) { 584 | self::$is_double_rate = $rate; 585 | } 586 | } 587 | } 588 | 589 | /* 590 | 591 | header("Content-type: text/html; charset=utf-8"); 592 | 593 | $act = isset($_REQUEST['act']) ? trim($_REQUEST['act']) : ''; 594 | //类调用 595 | $obj = new JokerPoker(); 596 | 597 | if($act == 'getcard') { 598 | //获取5张牌 599 | $key = $obj->getFiveCard(); 600 | //$key = array(17,37,39,40,42); 601 | exit(json_encode($key)); 602 | } elseif($act == 'turncard') { 603 | //翻牌 604 | $tmp = isset($_REQUEST['card']) ? trim($_REQUEST['card']) : ''; 605 | if(!empty($tmp)) { 606 | $key = explode('|',$tmp); 607 | } else { 608 | $key = array(); 609 | } 610 | $key = array_map('intval', $key); 611 | $card = $obj->getFiveCard($key); 612 | $res = $obj->getCardType($card); 613 | exit(json_encode($res)); 614 | } elseif($act == 'isdouble') { 615 | //翻倍处理 616 | $card = isset($_REQUEST['card']) && !empty($_REQUEST['card']) ? intval($_REQUEST['card']) : 2; 617 | $pos = (isset($_REQUEST['pos']) && $_REQUEST['pos'] < 4) ? intval($_REQUEST['pos']) : 2; 618 | $res = $obj->getIsDoubleCard($card, $pos); 619 | exit(json_encode($res)); 620 | } 621 | 622 | //测试牌型结果 623 | $tmp = isset($_REQUEST['test']) ? trim($_REQUEST['test']) : ''; 624 | if(!empty($tmp)) { 625 | $key = explode('|',$tmp); 626 | } else { 627 | $key = array(); 628 | } 629 | 630 | //类调用 631 | $obj = new JokerPoker(); 632 | $key = $obj->getFiveCard(); 633 | $key = array(13,18,24,27,43); 634 | $card = $obj->showCard($key); 635 | 636 | var_dump($key, $card, $obj->getCardType($key),$obj->getIsDoubleCard()); 637 | 638 | */ 639 | -------------------------------------------------------------------------------- /public/client/msgpack.js: -------------------------------------------------------------------------------- 1 | /*!{id:msgpack.js,ver:1.05,license:"MIT",author:"uupaa.js@gmail.com"}*/ 2 | 3 | // === msgpack === 4 | // MessagePack -> http://msgpack.sourceforge.net/ 5 | 6 | this.msgpack || (function(globalScope) { 7 | 8 | globalScope.msgpack = { 9 | pack: msgpackpack, // msgpack.pack(data:Mix, 10 | // toString:Boolean = false):ByteArray/ByteString/false 11 | // [1][mix to String] msgpack.pack({}, true) -> "..." 12 | // [2][mix to ByteArray] msgpack.pack({}) -> [...] 13 | unpack: msgpackunpack, // msgpack.unpack(data:BinaryString/ByteArray):Mix 14 | // [1][String to mix] msgpack.unpack("...") -> {} 15 | // [2][ByteArray to mix] msgpack.unpack([...]) -> {} 16 | worker: "msgpack.js", // msgpack.worker - WebWorkers script filename 17 | upload: msgpackupload, // msgpack.upload(url:String, option:Hash, callback:Function) 18 | download: msgpackdownload // msgpack.download(url:String, option:Hash, callback:Function) 19 | }; 20 | 21 | var _ie = /MSIE/.test(navigator.userAgent), 22 | _bin2num = {}, // BinaryStringToNumber { "\00": 0, ... "\ff": 255 } 23 | _num2bin = {}, // NumberToBinaryString { 0: "\00", ... 255: "\ff" } 24 | _num2b64 = ("ABCDEFGHIJKLMNOPQRSTUVWXYZ" + 25 | "abcdefghijklmnopqrstuvwxyz0123456789+/").split(""), 26 | _buf = [], // decode buffer 27 | _idx = 0, // decode buffer[index] 28 | _error = 0, // msgpack.pack() error code. 1 = CYCLIC_REFERENCE_ERROR 29 | _isArray = Array.isArray || (function(mix) { 30 | return Object.prototype.toString.call(mix) === "[object Array]"; 31 | }), 32 | _isUint8Array = function(mix) { 33 | return Object.prototype.toString.call(mix) === "[object Uint8Array]"; 34 | }, 35 | _toString = String.fromCharCode, // CharCode/ByteArray to String 36 | _MAX_DEPTH = 512; 37 | 38 | // for WebWorkers Code Block 39 | self.importScripts && (onmessage = function(event) { 40 | if (event.data.method === "pack") { 41 | postMessage(base64encode(msgpackpack(event.data.data))); 42 | } else { 43 | postMessage(msgpackunpack(event.data.data)); 44 | } 45 | }); 46 | 47 | // msgpack.pack 48 | function msgpackpack(data, // @param Mix: 49 | toString) { // @param Boolean(= false): 50 | // @return ByteArray/BinaryString/false: 51 | // false is error return 52 | // [1][mix to String] msgpack.pack({}, true) -> "..." 53 | // [2][mix to ByteArray] msgpack.pack({}) -> [...] 54 | 55 | _error = 0; 56 | 57 | var byteArray = encode([], data, 0); 58 | 59 | return _error ? false 60 | : toString ? byteArrayToByteString(byteArray) 61 | : byteArray; 62 | } 63 | 64 | // msgpack.unpack 65 | function msgpackunpack(data) { // @param BinaryString/ByteArray: 66 | // @return Mix/undefined: 67 | // undefined is error return 68 | // [1][String to mix] msgpack.unpack("...") -> {} 69 | // [2][ByteArray to mix] msgpack.unpack([...]) -> {} 70 | 71 | _buf = typeof data === "string" ? toByteArray(data) : data; 72 | _idx = -1; 73 | return decode(); // mix or undefined 74 | } 75 | 76 | // inner - encoder 77 | function encode(rv, // @param ByteArray: result 78 | mix, // @param Mix: source data 79 | depth) { // @param Number: depth 80 | var size, i, iz, c, pos, // for UTF8.encode, Array.encode, Hash.encode 81 | high, low, sign, exp, frac; // for IEEE754 82 | 83 | if (mix == null) { // null or undefined -> 0xc0 ( null ) 84 | rv.push(0xc0); 85 | } else if (mix === false) { // false -> 0xc2 ( false ) 86 | rv.push(0xc2); 87 | } else if (mix === true) { // true -> 0xc3 ( true ) 88 | rv.push(0xc3); 89 | } else { 90 | switch (typeof mix) { 91 | case "number": 92 | if (mix !== mix) { // isNaN 93 | rv.push(0xcb, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff); // quiet NaN 94 | } else if (mix === Infinity) { 95 | rv.push(0xcb, 0x7f, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00); // positive infinity 96 | } else if (Math.floor(mix) === mix) { // int or uint 97 | if (mix < 0) { 98 | // int 99 | if (mix >= -32) { // negative fixnum 100 | rv.push(0xe0 + mix + 32); 101 | } else if (mix > -0x80) { 102 | rv.push(0xd0, mix + 0x100); 103 | } else if (mix > -0x8000) { 104 | mix += 0x10000; 105 | rv.push(0xd1, mix >> 8, mix & 0xff); 106 | } else if (mix > -0x80000000) { 107 | mix += 0x100000000; 108 | rv.push(0xd2, mix >>> 24, (mix >> 16) & 0xff, 109 | (mix >> 8) & 0xff, mix & 0xff); 110 | } else { 111 | high = Math.floor(mix / 0x100000000); 112 | low = mix & 0xffffffff; 113 | rv.push(0xd3, (high >> 24) & 0xff, (high >> 16) & 0xff, 114 | (high >> 8) & 0xff, high & 0xff, 115 | (low >> 24) & 0xff, (low >> 16) & 0xff, 116 | (low >> 8) & 0xff, low & 0xff); 117 | } 118 | } else { 119 | // uint 120 | if (mix < 0x80) { 121 | rv.push(mix); // positive fixnum 122 | } else if (mix < 0x100) { // uint 8 123 | rv.push(0xcc, mix); 124 | } else if (mix < 0x10000) { // uint 16 125 | rv.push(0xcd, mix >> 8, mix & 0xff); 126 | } else if (mix < 0x100000000) { // uint 32 127 | rv.push(0xce, mix >>> 24, (mix >> 16) & 0xff, 128 | (mix >> 8) & 0xff, mix & 0xff); 129 | } else { 130 | high = Math.floor(mix / 0x100000000); 131 | low = mix & 0xffffffff; 132 | rv.push(0xcf, (high >> 24) & 0xff, (high >> 16) & 0xff, 133 | (high >> 8) & 0xff, high & 0xff, 134 | (low >> 24) & 0xff, (low >> 16) & 0xff, 135 | (low >> 8) & 0xff, low & 0xff); 136 | } 137 | } 138 | } else { // double 139 | // THX!! @edvakf 140 | // http://javascript.g.hatena.ne.jp/edvakf/20101128/1291000731 141 | sign = mix < 0; 142 | sign && (mix *= -1); 143 | 144 | // add offset 1023 to ensure positive 145 | // 0.6931471805599453 = Math.LN2; 146 | exp = ((Math.log(mix) / 0.6931471805599453) + 1023) | 0; 147 | 148 | // shift 52 - (exp - 1023) bits to make integer part exactly 53 bits, 149 | // then throw away trash less than decimal point 150 | frac = mix * Math.pow(2, 52 + 1023 - exp); 151 | 152 | // S+-Exp(11)--++-----------------Fraction(52bits)-----------------------+ 153 | // || || | 154 | // v+----------++--------------------------------------------------------+ 155 | // 00000000|00000000|00000000|00000000|00000000|00000000|00000000|00000000 156 | // 6 5 55 4 4 3 2 1 8 0 157 | // 3 6 21 8 0 2 4 6 158 | // 159 | // +----------high(32bits)-----------+ +----------low(32bits)------------+ 160 | // | | | | 161 | // +---------------------------------+ +---------------------------------+ 162 | // 3 2 21 1 8 0 163 | // 1 4 09 6 164 | low = frac & 0xffffffff; 165 | sign && (exp |= 0x800); 166 | high = ((frac / 0x100000000) & 0xfffff) | (exp << 20); 167 | 168 | rv.push(0xcb, (high >> 24) & 0xff, (high >> 16) & 0xff, 169 | (high >> 8) & 0xff, high & 0xff, 170 | (low >> 24) & 0xff, (low >> 16) & 0xff, 171 | (low >> 8) & 0xff, low & 0xff); 172 | } 173 | break; 174 | case "string": 175 | // http://d.hatena.ne.jp/uupaa/20101128 176 | iz = mix.length; 177 | pos = rv.length; // keep rewrite position 178 | 179 | rv.push(0); // placeholder 180 | 181 | // utf8.encode 182 | for (i = 0; i < iz; ++i) { 183 | c = mix.charCodeAt(i); 184 | if (c < 0x80) { // ASCII(0x00 ~ 0x7f) 185 | rv.push(c & 0x7f); 186 | } else if (c < 0x0800) { 187 | rv.push(((c >>> 6) & 0x1f) | 0xc0, (c & 0x3f) | 0x80); 188 | } else if (c < 0x10000) { 189 | rv.push(((c >>> 12) & 0x0f) | 0xe0, 190 | ((c >>> 6) & 0x3f) | 0x80, (c & 0x3f) | 0x80); 191 | } 192 | } 193 | size = rv.length - pos - 1; 194 | 195 | if (size < 32) { 196 | rv[pos] = 0xa0 + size; // rewrite 197 | } else if (size < 0x100) { // 8 198 | rv.splice(pos, 1, 0xd9, size); 199 | } else if (size < 0x10000) { // 16 200 | rv.splice(pos, 1, 0xda, size >> 8, size & 0xff); 201 | } else if (size < 0x100000000) { // 32 202 | rv.splice(pos, 1, 0xdb, 203 | size >>> 24, (size >> 16) & 0xff, 204 | (size >> 8) & 0xff, size & 0xff); 205 | } 206 | break; 207 | default: // array, hash, or Uint8Array 208 | if (_isUint8Array(mix)) { 209 | size = mix.length; 210 | 211 | if (size < 0x100) { // 8 212 | rv.push(0xc4, size); 213 | } else if (size < 0x10000) { // 16 214 | rv.push(0xc5, size >> 8, size & 0xff); 215 | } else if (size < 0x100000000) { // 32 216 | rv.push(0xc6, size >>> 24, (size >> 16) & 0xff, 217 | (size >> 8) & 0xff, size & 0xff); 218 | } 219 | Array.prototype.push.apply(rv, mix); 220 | break; 221 | } 222 | if (++depth >= _MAX_DEPTH) { 223 | _error = 1; // CYCLIC_REFERENCE_ERROR 224 | return rv = []; // clear 225 | } 226 | if (_isArray(mix)) { 227 | size = mix.length; 228 | if (size < 16) { 229 | rv.push(0x90 + size); 230 | } else if (size < 0x10000) { // 16 231 | rv.push(0xdc, size >> 8, size & 0xff); 232 | } else if (size < 0x100000000) { // 32 233 | rv.push(0xdd, size >>> 24, (size >> 16) & 0xff, 234 | (size >> 8) & 0xff, size & 0xff); 235 | } 236 | for (i = 0; i < size; ++i) { 237 | encode(rv, mix[i], depth); 238 | } 239 | } else { // hash 240 | // http://d.hatena.ne.jp/uupaa/20101129 241 | pos = rv.length; // keep rewrite position 242 | rv.push(0); // placeholder 243 | size = 0; 244 | for (i in mix) { 245 | ++size; 246 | encode(rv, i, depth); 247 | encode(rv, mix[i], depth); 248 | } 249 | if (size < 16) { 250 | rv[pos] = 0x80 + size; // rewrite 251 | } else if (size < 0x10000) { // 16 252 | rv.splice(pos, 1, 0xde, size >> 8, size & 0xff); 253 | } else if (size < 0x100000000) { // 32 254 | rv.splice(pos, 1, 0xdf, 255 | size >>> 24, (size >> 16) & 0xff, 256 | (size >> 8) & 0xff, size & 0xff); 257 | } 258 | } 259 | } 260 | } 261 | return rv; 262 | } 263 | 264 | // inner - decoder 265 | function decode() { // @return Mix: 266 | var size, i, iz, c, num = 0, 267 | sign, exp, frac, ary, hash, 268 | buf = _buf, type = buf[++_idx]; 269 | 270 | if (type >= 0xe0) { // Negative FixNum (111x xxxx) (-32 ~ -1) 271 | return type - 0x100; 272 | } 273 | if (type < 0xc0) { 274 | if (type < 0x80) { // Positive FixNum (0xxx xxxx) (0 ~ 127) 275 | return type; 276 | } 277 | if (type < 0x90) { // FixMap (1000 xxxx) 278 | num = type - 0x80; 279 | type = 0x80; 280 | } else if (type < 0xa0) { // FixArray (1001 xxxx) 281 | num = type - 0x90; 282 | type = 0x90; 283 | } else { // if (type < 0xc0) { // FixRaw (101x xxxx) 284 | num = type - 0xa0; 285 | type = 0xa0; 286 | } 287 | } 288 | switch (type) { 289 | case 0xc0: return null; 290 | case 0xc2: return false; 291 | case 0xc3: return true; 292 | case 0xca: // float 293 | num = buf[++_idx] * 0x1000000 + (buf[++_idx] << 16) + 294 | (buf[++_idx] << 8) + buf[++_idx]; 295 | sign = num & 0x80000000; // 1bit 296 | exp = (num >> 23) & 0xff; // 8bits 297 | frac = num & 0x7fffff; // 23bits 298 | if (!num || num === 0x80000000) { // 0.0 or -0.0 299 | return 0; 300 | } 301 | if (exp === 0xff) { // NaN or Infinity 302 | return frac ? NaN : Infinity; 303 | } 304 | return (sign ? -1 : 1) * 305 | (frac | 0x800000) * Math.pow(2, exp - 127 - 23); // 127: bias 306 | case 0xcb: // double 307 | num = buf[++_idx] * 0x1000000 + (buf[++_idx] << 16) + 308 | (buf[++_idx] << 8) + buf[++_idx]; 309 | sign = num & 0x80000000; // 1bit 310 | exp = (num >> 20) & 0x7ff; // 11bits 311 | frac = num & 0xfffff; // 52bits - 32bits (high word) 312 | if (!num || num === 0x80000000) { // 0.0 or -0.0 313 | _idx += 4; 314 | return 0; 315 | } 316 | if (exp === 0x7ff) { // NaN or Infinity 317 | _idx += 4; 318 | return frac ? NaN : Infinity; 319 | } 320 | num = buf[++_idx] * 0x1000000 + (buf[++_idx] << 16) + 321 | (buf[++_idx] << 8) + buf[++_idx]; 322 | return (sign ? -1 : 1) * 323 | ((frac | 0x100000) * Math.pow(2, exp - 1023 - 20) // 1023: bias 324 | + num * Math.pow(2, exp - 1023 - 52)); 325 | // 0xcf: uint64, 0xce: uint32, 0xcd: uint16 326 | case 0xcf: num = buf[++_idx] * 0x1000000 + (buf[++_idx] << 16) + 327 | (buf[++_idx] << 8) + buf[++_idx]; 328 | return num * 0x100000000 + 329 | buf[++_idx] * 0x1000000 + (buf[++_idx] << 16) + 330 | (buf[++_idx] << 8) + buf[++_idx]; 331 | case 0xce: num += buf[++_idx] * 0x1000000 + (buf[++_idx] << 16); 332 | case 0xcd: num += buf[++_idx] << 8; 333 | case 0xcc: return num + buf[++_idx]; 334 | // 0xd3: int64, 0xd2: int32, 0xd1: int16, 0xd0: int8 335 | case 0xd3: num = buf[++_idx]; 336 | if (num & 0x80) { // sign -> avoid overflow 337 | return ((num ^ 0xff) * 0x100000000000000 + 338 | (buf[++_idx] ^ 0xff) * 0x1000000000000 + 339 | (buf[++_idx] ^ 0xff) * 0x10000000000 + 340 | (buf[++_idx] ^ 0xff) * 0x100000000 + 341 | (buf[++_idx] ^ 0xff) * 0x1000000 + 342 | (buf[++_idx] ^ 0xff) * 0x10000 + 343 | (buf[++_idx] ^ 0xff) * 0x100 + 344 | (buf[++_idx] ^ 0xff) + 1) * -1; 345 | } 346 | return num * 0x100000000000000 + 347 | buf[++_idx] * 0x1000000000000 + 348 | buf[++_idx] * 0x10000000000 + 349 | buf[++_idx] * 0x100000000 + 350 | buf[++_idx] * 0x1000000 + 351 | buf[++_idx] * 0x10000 + 352 | buf[++_idx] * 0x100 + 353 | buf[++_idx]; 354 | case 0xd2: num = buf[++_idx] * 0x1000000 + (buf[++_idx] << 16) + 355 | (buf[++_idx] << 8) + buf[++_idx]; 356 | return num < 0x80000000 ? num : num - 0x100000000; // 0x80000000 * 2 357 | case 0xd1: num = (buf[++_idx] << 8) + buf[++_idx]; 358 | return num < 0x8000 ? num : num - 0x10000; // 0x8000 * 2 359 | case 0xd0: num = buf[++_idx]; 360 | return num < 0x80 ? num : num - 0x100; // 0x80 * 2 361 | // 0xdb: str32, 0xda: str16, 0xd9: str8, 0xa0: fixstr 362 | case 0xdb: num += buf[++_idx] * 0x1000000 + (buf[++_idx] << 16); 363 | case 0xda: num += buf[++_idx] << 8; 364 | case 0xd9: num += buf[++_idx]; 365 | case 0xa0: // utf8.decode 366 | for (ary = [], i = _idx, iz = i + num; i < iz; ) { 367 | c = buf[++i]; // lead byte 368 | ary.push(c < 0x80 ? c : // ASCII(0x00 ~ 0x7f) 369 | c < 0xe0 ? ((c & 0x1f) << 6 | (buf[++i] & 0x3f)) : 370 | ((c & 0x0f) << 12 | (buf[++i] & 0x3f) << 6 371 | | (buf[++i] & 0x3f))); 372 | } 373 | _idx = i; 374 | return ary.length < 10240 ? _toString.apply(null, ary) 375 | : byteArrayToByteString(ary); 376 | // 0xc6: bin32, 0xc5: bin16, 0xc4: bin8 377 | case 0xc6: num += buf[++_idx] * 0x1000000 + (buf[++_idx] << 16); 378 | case 0xc5: num += buf[++_idx] << 8; 379 | case 0xc4: num += buf[++_idx]; 380 | var end = ++_idx + num 381 | var ret = buf.slice(_idx, end); 382 | _idx += num; 383 | return ret; 384 | // 0xdf: map32, 0xde: map16, 0x80: map 385 | case 0xdf: num += buf[++_idx] * 0x1000000 + (buf[++_idx] << 16); 386 | case 0xde: num += (buf[++_idx] << 8) + buf[++_idx]; 387 | case 0x80: hash = {}; 388 | while (num--) { 389 | // make key/value pair 390 | size = buf[++_idx] - 0xa0; 391 | 392 | for (ary = [], i = _idx, iz = i + size; i < iz; ) { 393 | c = buf[++i]; // lead byte 394 | ary.push(c < 0x80 ? c : // ASCII(0x00 ~ 0x7f) 395 | c < 0xe0 ? ((c & 0x1f) << 6 | (buf[++i] & 0x3f)) : 396 | ((c & 0x0f) << 12 | (buf[++i] & 0x3f) << 6 397 | | (buf[++i] & 0x3f))); 398 | } 399 | _idx = i; 400 | hash[_toString.apply(null, ary)] = decode(); 401 | } 402 | return hash; 403 | // 0xdd: array32, 0xdc: array16, 0x90: array 404 | case 0xdd: num += buf[++_idx] * 0x1000000 + (buf[++_idx] << 16); 405 | case 0xdc: num += (buf[++_idx] << 8) + buf[++_idx]; 406 | case 0x90: ary = []; 407 | while (num--) { 408 | ary.push(decode()); 409 | } 410 | return ary; 411 | } 412 | return; 413 | } 414 | 415 | // inner - byteArray To ByteString 416 | function byteArrayToByteString(byteArray) { // @param ByteArray 417 | // @return String 418 | // http://d.hatena.ne.jp/uupaa/20101128 419 | try { 420 | return _toString.apply(this, byteArray); // toString 421 | } catch(err) { 422 | ; // avoid "Maximum call stack size exceeded" 423 | } 424 | var rv = [], i = 0, iz = byteArray.length, num2bin = _num2bin; 425 | 426 | for (; i < iz; ++i) { 427 | rv[i] = num2bin[byteArray[i]]; 428 | } 429 | return rv.join(""); 430 | } 431 | 432 | // msgpack.download - load from server 433 | function msgpackdownload(url, // @param String: 434 | option, // @param Hash: { worker, timeout, before, after } 435 | // option.worker - Boolean(= false): true is use WebWorkers 436 | // option.timeout - Number(= 10): timeout sec 437 | // option.before - Function: before(xhr, option) 438 | // option.after - Function: after(xhr, option, { status, ok }) 439 | callback) { // @param Function: callback(data, option, { status, ok }) 440 | // data - Mix/null: 441 | // option - Hash: 442 | // status - Number: HTTP status code 443 | // ok - Boolean: 444 | option.method = "GET"; 445 | option.binary = true; 446 | ajax(url, option, callback); 447 | } 448 | 449 | // msgpack.upload - save to server 450 | function msgpackupload(url, // @param String: 451 | option, // @param Hash: { data, worker, timeout, before, after } 452 | // option.data - Mix: 453 | // option.worker - Boolean(= false): true is use WebWorkers 454 | // option.timeout - Number(= 10): timeout sec 455 | // option.before - Function: before(xhr, option) 456 | // option.after - Function: after(xhr, option, { status, ok }) 457 | callback) { // @param Function: callback(data, option, { status, ok }) 458 | // data - String: responseText 459 | // option - Hash: 460 | // status - Number: HTTP status code 461 | // ok - Boolean: 462 | option.method = "PUT"; 463 | option.binary = true; 464 | 465 | if (option.worker && globalScope.Worker) { 466 | var worker = new Worker(msgpack.worker); 467 | 468 | worker.onmessage = function(event) { 469 | option.data = event.data; 470 | ajax(url, option, callback); 471 | }; 472 | worker.postMessage({ method: "pack", data: option.data }); 473 | } else { 474 | // pack and base64 encode 475 | option.data = base64encode(msgpackpack(option.data)); 476 | ajax(url, option, callback); 477 | } 478 | } 479 | 480 | // inner - 481 | function ajax(url, // @param String: 482 | option, // @param Hash: { data, ifmod, method, timeout, 483 | // header, binary, before, after, worker } 484 | // option.data - Mix: upload data 485 | // option.ifmod - Boolean: true is "If-Modified-Since" header 486 | // option.method - String: "GET", "POST", "PUT" 487 | // option.timeout - Number(= 10): timeout sec 488 | // option.header - Hash(= {}): { key: "value", ... } 489 | // option.binary - Boolean(= false): true is binary data 490 | // option.before - Function: before(xhr, option) 491 | // option.after - Function: after(xhr, option, { status, ok }) 492 | // option.worker - Boolean(= false): true is use WebWorkers 493 | callback) { // @param Function: callback(data, option, { status, ok }) 494 | // data - String/Mix/null: 495 | // option - Hash: 496 | // status - Number: HTTP status code 497 | // ok - Boolean: 498 | function readyStateChange() { 499 | if (xhr.readyState === 4) { 500 | var data, status = xhr.status, worker, byteArray, 501 | rv = { status: status, ok: status >= 200 && status < 300 }; 502 | 503 | if (!run++) { 504 | if (method === "PUT") { 505 | data = rv.ok ? xhr.responseText : ""; 506 | } else { 507 | if (rv.ok) { 508 | if (option.worker && globalScope.Worker) { 509 | worker = new Worker(msgpack.worker); 510 | worker.onmessage = function(event) { 511 | callback(event.data, option, rv); 512 | }; 513 | worker.postMessage({ method: "unpack", 514 | data: xhr.responseText }); 515 | gc(); 516 | return; 517 | } else { 518 | byteArray = _ie ? toByteArrayIE(xhr) 519 | : toByteArray(xhr.responseText); 520 | data = msgpackunpack(byteArray); 521 | } 522 | } 523 | } 524 | after && after(xhr, option, rv); 525 | callback(data, option, rv); 526 | gc(); 527 | } 528 | } 529 | } 530 | 531 | function ng(abort, status) { 532 | if (!run++) { 533 | var rv = { status: status || 400, ok: false }; 534 | 535 | after && after(xhr, option, rv); 536 | callback(null, option, rv); 537 | gc(abort); 538 | } 539 | } 540 | 541 | function gc(abort) { 542 | abort && xhr && xhr.abort && xhr.abort(); 543 | watchdog && (clearTimeout(watchdog), watchdog = 0); 544 | xhr = null; 545 | globalScope.addEventListener && 546 | globalScope.removeEventListener("beforeunload", ng, false); 547 | } 548 | 549 | var watchdog = 0, 550 | method = option.method || "GET", 551 | header = option.header || {}, 552 | before = option.before, 553 | after = option.after, 554 | data = option.data || null, 555 | xhr = globalScope.XMLHttpRequest ? new XMLHttpRequest() : 556 | globalScope.ActiveXObject ? new ActiveXObject("Microsoft.XMLHTTP") : 557 | null, 558 | run = 0, i, 559 | overrideMimeType = "overrideMimeType", 560 | setRequestHeader = "setRequestHeader", 561 | getbinary = method === "GET" && option.binary; 562 | 563 | try { 564 | xhr.onreadystatechange = readyStateChange; 565 | xhr.open(method, url, true); // ASync 566 | 567 | before && before(xhr, option); 568 | 569 | getbinary && xhr[overrideMimeType] && 570 | xhr[overrideMimeType]("text/plain; charset=x-user-defined"); 571 | data && 572 | xhr[setRequestHeader]("Content-Type", 573 | "application/x-www-form-urlencoded"); 574 | 575 | for (i in header) { 576 | xhr[setRequestHeader](i, header[i]); 577 | } 578 | 579 | globalScope.addEventListener && 580 | globalScope.addEventListener("beforeunload", ng, false); // 400: Bad Request 581 | 582 | xhr.send(data); 583 | watchdog = setTimeout(function() { 584 | ng(1, 408); // 408: Request Time-out 585 | }, (option.timeout || 10) * 1000); 586 | } catch (err) { 587 | ng(0, 400); // 400: Bad Request 588 | } 589 | } 590 | 591 | // inner - BinaryString To ByteArray 592 | function toByteArray(data) { // @param BinaryString: "\00\01" 593 | // @return ByteArray: [0x00, 0x01] 594 | var rv = [], bin2num = _bin2num, remain, 595 | ary = data.split(""), 596 | i = -1, iz; 597 | 598 | iz = ary.length; 599 | remain = iz % 8; 600 | 601 | while (remain--) { 602 | ++i; 603 | rv[i] = bin2num[ary[i]]; 604 | } 605 | remain = iz >> 3; 606 | while (remain--) { 607 | rv.push(bin2num[ary[++i]], bin2num[ary[++i]], 608 | bin2num[ary[++i]], bin2num[ary[++i]], 609 | bin2num[ary[++i]], bin2num[ary[++i]], 610 | bin2num[ary[++i]], bin2num[ary[++i]]); 611 | } 612 | return rv; 613 | } 614 | 615 | // inner - BinaryString to ByteArray 616 | function toByteArrayIE(xhr) { 617 | var rv = [], data, remain, 618 | charCodeAt = "charCodeAt", 619 | loop, v0, v1, v2, v3, v4, v5, v6, v7, 620 | i = -1, iz; 621 | 622 | iz = vblen(xhr); 623 | data = vbstr(xhr); 624 | loop = Math.ceil(iz / 2); 625 | remain = loop % 8; 626 | 627 | while (remain--) { 628 | v0 = data[charCodeAt](++i); // 0x00,0x01 -> 0x0100 629 | rv.push(v0 & 0xff, v0 >> 8); 630 | } 631 | remain = loop >> 3; 632 | while (remain--) { 633 | v0 = data[charCodeAt](++i); 634 | v1 = data[charCodeAt](++i); 635 | v2 = data[charCodeAt](++i); 636 | v3 = data[charCodeAt](++i); 637 | v4 = data[charCodeAt](++i); 638 | v5 = data[charCodeAt](++i); 639 | v6 = data[charCodeAt](++i); 640 | v7 = data[charCodeAt](++i); 641 | rv.push(v0 & 0xff, v0 >> 8, v1 & 0xff, v1 >> 8, 642 | v2 & 0xff, v2 >> 8, v3 & 0xff, v3 >> 8, 643 | v4 & 0xff, v4 >> 8, v5 & 0xff, v5 >> 8, 644 | v6 & 0xff, v6 >> 8, v7 & 0xff, v7 >> 8); 645 | } 646 | iz % 2 && rv.pop(); 647 | 648 | return rv; 649 | } 650 | 651 | // inner - base64.encode 652 | function base64encode(data) { // @param ByteArray: 653 | // @return Base64String: 654 | var rv = [], 655 | c = 0, i = -1, iz = data.length, 656 | pad = [0, 2, 1][data.length % 3], 657 | num2bin = _num2bin, 658 | num2b64 = _num2b64; 659 | 660 | if (globalScope.btoa) { 661 | while (i < iz) { 662 | rv.push(num2bin[data[++i]]); 663 | } 664 | return btoa(rv.join("")); 665 | } 666 | --iz; 667 | while (i < iz) { 668 | c = (data[++i] << 16) | (data[++i] << 8) | (data[++i]); // 24bit 669 | rv.push(num2b64[(c >> 18) & 0x3f], 670 | num2b64[(c >> 12) & 0x3f], 671 | num2b64[(c >> 6) & 0x3f], 672 | num2b64[ c & 0x3f]); 673 | } 674 | pad > 1 && (rv[rv.length - 2] = "="); 675 | pad > 0 && (rv[rv.length - 1] = "="); 676 | return rv.join(""); 677 | } 678 | 679 | // --- init --- 680 | (function() { 681 | var i = 0, v; 682 | 683 | for (; i < 0x100; ++i) { 684 | v = _toString(i); 685 | _bin2num[v] = i; // "\00" -> 0x00 686 | _num2bin[i] = v; // 0 -> "\00" 687 | } 688 | // http://twitter.com/edvakf/statuses/15576483807 689 | for (i = 0x80; i < 0x100; ++i) { // [Webkit][Gecko] 690 | _bin2num[_toString(0xf700 + i)] = i; // "\f780" -> 0x80 691 | } 692 | })(); 693 | 694 | _ie && document.write('