Table Of Contents

骑驴找蚂蚁

全干工程师

Swoole和WebSocket之斗地主中篇准备发牌抢地主

这篇文章是Swoole和WebSocket之斗地主上篇进房间的续集,在这里我们主要讲解准备->发牌->抢地主整个流程。

实现效果

开始

环境和上次保持一致即可.

功能实现

我们这期需要实现的有准备协议发牌推送抢地主这几个协议.

准备协议

当一个用户准备,需要通知其它的用户更改显示状态, 当最后一个人准备的时候我们就发牌,我要通知用户抢地主.

  • 功能分析

当用户准备的时候,我要通知其它用户更新准备用户的状态, 若是最后一个准备我们需要洗牌推送发牌协议.

Note:在这里我们抢地主不作复杂的流程,我们要是抢了就是成功为地主,没有抢就将消息发给下一个人抢。

  • 服务端

修改协议文件,实现准备协议的代码

  1. 修改协议路由protocol.php

$app->addProtocol(
    'player.ready', //准备
    LandownerController::class . ':ready'
);

$app->addProtocol(
    'player.grab', //抢地主
    LandownerController::class . ':grabLandowner'
);
  1. 实现代码

...

/**
 * @param $body
 * @return array
 */
public function ready($body)
{
    if (!$this->redis->sIsMember(RedisConstant::FULL_CONNECT_FD, $this->frame->fd)) {
        return [
            "flag" => 500,
            "requestId" => $body->requestId,
        ];
    }

    $ready = 0;
    if (isset($body->ready)) {
        echo "ready";
        $ready = (int) $body->ready;
    }
    $this->redis->hSet(self::READY_KEY, $this->frame->fd, $ready);

    $count = $this->redis->sCard(RedisConstant::FULL_CONNECT_FD);
    if ($count >= 2) {
        $players = $this->redis->sMembers(RedisConstant::FULL_CONNECT_FD);
        $otherPlayer = [];
        foreach ($players as $player) {
            if ($player == $this->frame->fd) {
                continue;
            }
            $otherPlayer[] = $player;
        }
        $this->task([
            'from' => $otherPlayer,
            'content' => json_encode(["listen" => "readyStatus", "content" => [
                'playerId' => $this->frame->fd,
                'ready'  => $ready,
            ]])
        ], PushTaskHandle::class); //通知其它用户更新状态

        if ($count >= 3) {
            $status = 1;
            foreach ($players as $player) {
                $status = $status & (int)$this->redis->hGet(self::READY_KEY, $player);
            }
            if ($status) { //全部准备
                $playerInfo = $this->poker($players);
                $playerPoker = $playerInfo['poker'];
                unset($playerInfo['poker']);
                foreach ($players as $player) {
                    $playerInfo['poker'] = $playerPoker[$player];
                    $this->task([
                        'from' => $player,
                        'content' => json_encode(["listen" => "assignPoker", "content" => $playerInfo])
                    ], PushTaskHandle::class); //通知发牌
                }
            }
        }
    }
    return [
        "flag" => 0,
        "requestId" => $body->requestId,
    ];
}

/**
 * 洗牌
 * @param array $player
 * @return array
 */
private function poker(array $player)
{
    $playId = mt_rand(100, 1000000); //局号

    $poker = range(1, 54);

    shuffle($poker);
    shuffle($poker);
    shuffle($poker);

    $landowner = [];

    for ($i = 1; $i <= 3; $i++) {
        $key = array_rand($poker, 1);
        $landowner[] = $poker[$key];
        unset($poker[$key]);
    }

    $playerPoker = [];
    foreach (array_chunk($poker, 3) as $value) {
        $playerPoker[$player[0]][] = $value[0];
        if (isset($value[1])) {
            $playerPoker[$player[1]][] = $value[1];
        }
        if (isset($value[2])) {
            $playerPoker[$player[2]][] = $value[2];
        }
    }

    $this->redis->hSet(sprintf(self::PLAYER_INFO, $playId), 'poker', json_encode($playerPoker)); //写入用户的牌
    $this->redis->hSet(sprintf(self::PLAYER_INFO, $playId), 'luck_poker', json_encode($landowner)); //写入当前局的3张底牌

    return [
        'playId' => $playId,
        'poker'  => $playerPoker,
        'landowner' => $landowner,
    ];
}

/**
 * 抢地主
 *
 */
public function grabLandowner($body)
{
    if (!$this->redis->sIsMember(RedisConstant::FULL_CONNECT_FD, $this->frame->fd)) {
        return [
            "flag" => 500,
            "requestId" => $body->requestId,
        ];
    }

    $playId = $body->playId; //当前局

    if ($body->grab) {

        $poker = json_decode($this->redis->hGet(sprintf(self::PLAYER_INFO, $playId), 'poker'), true);

        $landowner = json_decode($this->redis->hGet(sprintf(self::PLAYER_INFO, $playId), 'luck_poker'), true);

        $poker[$this->frame->fd] = array_merge($poker[$this->frame->fd], $landowner);

        return [
            "flag" => 0,
            "requestId" => $body->requestId,
        ];
    }

    $players = $this->redis->sMembers(RedisConstant::FULL_CONNECT_FD);

    $currentPos = 0;
    foreach ($players as $key => $player) {
        if ($player == $this->frame->fd) {
            $currentPos = $key;
        }
    }
    if ($currentPos == count($players) - 1) {
        $currentPos = 0;
    } else {
        $currentPos += 1;
    }
    $this->container->get('server')->getServer()->push($players[$currentPos], json_encode([
        "listen" => "grabLandowner",
        "content" => "grabLandowner",
    ]));
    return [
        "flag" => 500,
        "requestId" => $body->requestId,
    ];
}

  • 客户端

我们只需要实现对应的readyStatusassignPoker就行了.

实现的客户业务代码



Landowner.prototype = {
    start: function () {
        document.body.appendChild(this.app.view);
        this.listen(); //初始化监听
    },
    initRender: function (landowner) {
        /**
         * @var landowner Landowner
         */
        if (this.socket.isConnected()) {
            this.socket.send('room.player', {}, function(body) {
                if (body.count > 3) {
                    return ;
                }
                landowner.playerCount = body.count;
                for (var i = 0; i < body.player.length; i++) {
                    landowner.readyWorker(i + 2, !!body.player[i].ready, body.player[i].playerId);
                }
                this.send("enter.room", {}, function (body) {
                    if (body.flag === 0) {
                        landowner.player = body.player;
                        landowner.readyWorker(1, false, body.player);
                    }
                });
            });
        }
    },
    listen: function() {
        var _this = this;
        this.socket.listen("readyStatus", function(content) { // 直接在回调函数里面修改文字
            /**
             * @var button Graphics
             */
            var button = _this.playerReadyButton[content.playerId];
            var text = button.getChildByName('text');
            text.text = content.ready ? "准备中" : "准备";
            text.text = content.ready ? "准备中" : "准备";
            if (content.ready) {
                text.x = 60 - 48;
            }
        });
        this.socket.listen("assignPoker", function (content) {
            _this.playId = content.playId;
            _this.renderBottomCard(content.landowner);
            _this.assignPoker(content.poker);
            _this.userPoker = content.poker;
            _this.landownerPoker = content.landowner;
            for (var key in _this.playerReadyButton) {
                _this.playerReadyButton[key].destroy();
            }
            _this.assignOtherPoker(content.poker.length);
        });
        this.socket.listen("grabLandowner", function () {
            _this.grabWorker(); //抢地主
        });
    },
    renderBottomCard: function (poker) {
        var container = new PIXI.Container();
        container.y = 100;
        container.x = window.innerWidth / 2 - 254;
        this.app.stage.addChild(container);
        for (var key in poker) {
            var bottomPoker = PIXI.Sprite.fromImage('poker/' + poker[key] + '.jpg');
            bottomPoker.x = key * 125;
            container.addChild(bottomPoker);
        }
    },
    assignPoker: function (poker) {
        console.log(poker);
        var createPoker = function () {
            var i = 1;
            return function (index) {
                var poker = PIXI.Sprite.fromImage('poker/' + index + '.jpg');
                poker.interactive = true;

                // Shows hand cursor
                poker.buttonMode = true;
                poker.x = i * 20;
                var clickCounter = 1;
                poker.on('pointerdown', function () { //监听牌的点击
                    if (clickCounter % 2) {
                        poker.y = -20
                    } else {
                        poker.y = 0;
                    }
                    clickCounter++;
                });
                i++;
                return poker;
            }
        };
        poker.sort(function (a, b) { //将牌排好序
            var o = a % 13, t = b % 13;
            if (o >= 0 && o <= 2) {
                o = 13 + o;
            }
            if (t >= 0 && t <= 2) {
                t = 13 + t;
            }
            if (a === 53) o = 16;
            if (a === 54) o = 17;
            if (b === 53) t = 16;
            if (b === 54) t = 17;

            if (o > t) {
                return -1;
            }
            if (o < t) {
                return 1;
            }
            return 0;
        });
        var container = new PIXI.Container();
        container.y = window.innerHeight / 2 + 100;
        container.x = window.innerWidth / 2 - 254;
        this.app.stage.addChild(container);
        var createFactory = createPoker();
        for (var key in poker) {
            container.addChild(createFactory(poker[key]));
        }
    },
    assignOtherPoker: function(count) { // 渲染其它人的牌样式
        var containerRight = new PIXI.Container(),
            containerLeft = new PIXI.Container();
        containerRight.x = window.innerWidth - 200 - 105;
        containerRight.y = 150;
        containerLeft.x = 200;
        containerLeft.y = 150;
        this.app.stage.addChild(containerLeft);
        this.app.stage.addChild(containerRight);
        for (var i = 0; i < count; i++) {
            var pokerSource1 = PIXI.Sprite.fromImage('poker/background.jpg');
            var pokerSource2 = PIXI.Sprite.fromImage('poker/background.jpg');
            pokerSource1.y = i * 20;
            pokerSource2.y = i * 20;
            containerLeft.addChild(pokerSource1);
            containerRight.addChild(pokerSource2);
        }
    },
    grabWorker: function () { //渲染抢地主
        var button = new PIXI.Graphics()
            .beginFill(0x2fb44a)
            .drawRoundedRect(0, 0, 120, 60, 10)
            .endFill();
        var noGrabButton = new PIXI.Graphics()
            .beginFill(0x2fb44a)
            .drawRoundedRect(0, 0, 120, 60, 10)
            .endFill();

        var grab = new PIXI.Text("抢地主", new PIXI.TextStyle({
            fontFamily: "Arial",
            fontSize: 32,
            fill: "white",
        }));
        var noGrab = new PIXI.Text("不抢", new PIXI.TextStyle({
            fontFamily: "Arial",
            fontSize: 32,
            fill: "white",
        }));
        grab.x = 60 - 48;
        grab.y = 30 - 16;
        grab.name = "grab";
        noGrab.x = 60 - 32;
        noGrab.y = 30 - 16;
        noGrab.name = "noGrab";
        button.interactive = true;
        button.buttonMode = true;
        noGrabButton.interactive = true;
        noGrabButton.buttonMode = true;
        button.addChild(grab);
        noGrabButton.addChild(noGrab);
        var playerGrab = function (grab) {
            return function() {
                _this.socket.send('player.grab', {
                    'grab': grab, 'playId': _this.playId
                }, function (body) {
                    if (body.flag === 0) { //抢成功
                        _this.userPoker.concat(_this.landownerPoker); //合并牌
                        _this.app.stage.getChildByName('poker').destroy(); //删除牌
                        _this.assignPoker(_this.userPoker); //重新整理牌
                    }
                    button.destroy();
                    noGrabButton.destroy();
                });
            }
        };
        button.on('pointertap', playerGrab(1));
        noGrabButton.on('pointertap', playerGrab(0));
        var x = y = 0;
        y = window.innerHeight / 2;
        x = window.innerWidth / 2;
        x = x - 200;
        y = y + 10;
        button.x = x;
        button.y = y;
        noGrabButton.x = x + 200;
        noGrabButton.y = y;
        this.app.stage.addChild(button);
        this.app.stage.addChild(noGrabButton);
    },
}

以上服务端和客户端代码就完成了用户准备、发牌、抢地主功能。下一期我们讲下准备出牌的实现.

Note: 在代码中我们以实现功能为主,实际有好多情况没有去考虑以及一些BUG。比如说关闭页面需要断开用户通知给其它用户等等。

源码地址

https://github.com/TianLiangZhou/loocode-example/tree/master/landowner

效果地址

https://example.loocode.com/landowner/index.html

推荐阅读

  1. https://wiki.swoole.com/
  2. https://github.com/TianLiangZhou/surf
  3. https://pixijs.io/examples/
  4. https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API

留言