├── 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 |
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 | 
18 |
19 | 2, 打牌逻辑, 根据按钮来操作打牌逻辑, 消息框里会提示打牌逻辑过程,打牌逻辑如下图:
20 | 
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 | 
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 |
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 |
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('