美文网首页
100行代码写出三国杀结算流程(体力篇)

100行代码写出三国杀结算流程(体力篇)

作者: 夜风SAI | 来源:发表于2020-10-20 16:21 被阅读0次

    没错,就是你认为的那个三国杀(#滑稽)
    当然,此次还是用vue来写。可能有点标题党,但是代码绝对简练,保证你一看就会

    1.透过现象看本质

    写代码是需要事先构思的,最开始的时候,我问了自己一个问题:

    三国杀的本质是什么?

    作为一个杀龄7年的老玩家,这个问题我整整想了一天,最终的答案是:

    本质上这是一个牌和体力的游戏

    无论你干了什么,发动了什么技能。最终的结果无非都是改变场上的牌和体力罢了
    当然,这个游戏本质是牌和体力,但是不止牌和体力,还有一个很重要的元素,它就是游戏中各式各样的阶段 ,它是游戏的助推剂,没有阶段,游戏将根本无法进行
    所以这篇文章将分为3节分别是:
    1. 体力篇
    2. 阶段篇(附带技能实现)
    3. 牌篇

    为什么牌放最后讲呢?因为牌的结算最为复杂,并且大部分牌都是在出牌阶段使用的,所以放在阶段后面讲。

    2.准备工作

    其实准备工作很少,就是类似这样,准备若干个玩家就行了

    <div>  // game.vue
         // seat是玩家座位,类似于id来确保玩家唯一性
         <player v-for="i in players" :name="i.name" :seat="i.seat" :hp="i.hp" :skills="i.skills">
    </div>
    
    <div>  // player.vue
        {{name}}
        {{hp}}
    </div>
    

    3.从一张图开始

    我们先来看一张伤害流程图(由易到难,杀的结算流程第三篇会讲

    注:体力的变化不止伤害,也有回复。但是流程都是差不多的

    可以发现,一个完整的伤害流程由4个事件构成。
    好,发现是发现了,但是怎么用代码实现一个完整的伤害流程?
    在vue里面,你可能会想,用$emit$on来实现不就行了吗?
    伤害来源通过$emit一个伤害事件,这个事件目标可以用$on来接受,这不是很简单吗?类似这样

    // player.vue
    this.$on('damage',e = >{
    //当接受到伤害事件后,我就发动卖血技能,嘿嘿
    })
    

    但是仔细一想,假如是下面的场景:

    神周瑜发动【业炎】,对司马郭嘉曹丕各造成了点伤害

    上面的场景中,需要先询问司马懿是否发动【反馈】,执行【反馈】之后,才能询问郭嘉是否发动【遗计】,并且还要等待郭嘉分牌才能结算曹丕等。
    可以发现,通过$on注册的事件,是不好处理异步函数的。它不能return 一个 Promise,然后通过then来继续结算当前事件。你或许想到使用callback,但是callback每一次都会不一样,并且会层层嵌套,让事件难以被理清。这种方法反而是费力不讨好

    4.事件池

    于是我换了一种思路
    改为创建一个事件池

    //game.vue
    eId:0,  //用于区分事件池内事件流程,eId是事件流程整体的id,并且事件流程内部所有的子事件都是这个id
    EventPool: [],  //闪亮登场,没想到吧,我只是个简简单单的数组
    

    事件池由两部分组成:添加运行
    事件池将会从左到右,依次运行事件。而添加事件之后,不会立即运行,而是等待所有排队的事件运行完才运行。添加运行是独立作业,互不干扰的。

    事件池的添加

    比如以下这个函数

    // player.vue
    this.damage(target, num)  //this对target造成num点伤害
    

    这个函数不会立即执行结算伤害等等,它只是将一个完整的伤害流程添加进事件池,类似这样:

    //  player.vue
    damage(target, num = 1, cards = []) {
        const e = {
            source: this.seat,  //伤害来源
            target,  //伤害目标
            num,  //伤害数量
            cards,  //造成伤害的牌,默认为空
        };
        return this.createDamageEvent(e);
    },
    
    //  player.vue
    createDamageEvent(e) {
        // 创建伤害事件流程
        const progress = [
            // 里面的每一项都是子事件的名称
            'source.damage',  //造成伤害时
            'target.wounded',  //受到伤害时
            'target.woundedContent',  //执行扣血的内容函数,不触发任何技能
            'source.damageEnd',  //造成伤害后
            'target.woundedEnd',  //受到伤害后
        ];
        this.$parent.pushEventPool(e, progress);
    },
    
    //  game.vue
    // 代码已做适量精简
    pushEventPool(e, list) {  //list为事件流程,是一个数组
        const arr = [];
        const id = this.eId++;
        const { EventPool } = this;
        forEach(list, (i, k) => {  //这里只示例第一次循环的结果,注意
            const iarr = i.split('.');  //iarr = ['source', 'damage']
            const name = iarr[1];
            const ev = {//新事件ev融合老事件e,并添加新的必要属性
                name,  //name = 'damage',代表这是【造成伤害时】这个时机
                id,  //0
                ...e,  //将老e解构在新ev的内部
                finish() {
                    // 事件取消即移除其(指事件流程)在事件池中的剩余子事件
                    //调用:ev.finish();
                    //例如,如果在受到伤害时,并且伤害为1时发动【名士】,则之后的子事件将会被移除
                    //而整个伤害事件流程将因为没有剩余子事件而直接结束
                    //例如公孙瓒的【趫猛】('damageEnd')(造成伤害后)就无法发动了
                    //因为它时机在【名士】之后,由于其和其之后的子事件都被移除了,自然无法触发
                    remove(EventPool, item => item.id === this.id);  //lodash函数 
                    console.log('事件取消');
                },
            };
            if (!ev.player) {  //这里的player即子事件的执行者
                //假如source和target都有【裸衣】,则只会由source来执行【裸衣】,player就是指定谁来执行的
                const player = e[iarr[0]];// player = e['source']
                ev.player = player;
            }
            arr.push(ev);
        });
        EventPool.splice(0, 0, ...arr);  //为什么是splice而不是push?接下来会讲
        //并且此语句在事件池为空时,等同于 EventPool.push(...arr)
    },
    

    事件池的插入

    插入其实也是添加的一部分。只不过事件流程是从事件池的头部被添加进去
    同时,事件池会移除已经执行的事件,正在执行的事件也被移除了。所以能保证,头部的事件就是即将执行的事件!
    假设一个新技能:
    【反噬】:当你受到一次伤害时,你对伤害来源造成等量的伤害。
    再来看一个经典案例:

    郭嘉,拥有【遗计】
    曹操,拥有【反噬】,【奸雄】
    郭嘉曹操使用【杀】造成伤害时,【曹操】发动【反噬】,对郭嘉造成了一点伤害。

    之后该怎么结算?老玩家应该都知道,先【遗计】【奸雄】,这是三国杀的插入结算机制
    可是按照事件池从左到右的执行顺序,会先【奸雄】【遗计】,那怎么办?
    这个时候从头部插入的优势就体现了。此时:

    郭嘉曹操造成一点伤害,eId为0
    曹操郭嘉造成一点伤害,eId为1,因为这是一个新的伤害事件流程

    再来张图帮助你们理解。不同的事件流程用了不同颜色帮助区分。但是注意,图中的技能并不在事件池里,

    事件池.png

    事件池的执行

    // game.vue
    async IterEventPool() {
        while (!this.empty) {  //当事件池不为空
            const ev = this.EventPool.shift();  //执行的时候就已经被移除了
            //这里做了一个优化,即this.triggersAll不包含事件名时,则不运行主体函数
            //例如,全场没有卖血流时,this.triggersAll自然不会包括'woundEnd'(受到伤害后)这个事件名
            //这样可以加快程序运行速度
            if (includes(this.triggersAll, ev.name) || ev.name.indexOf('Content') !== -1) {
                //获取player组件。ev.player其实是ev.player的seat来代替
                const player = this.getPlayer(ev.player);  
                //这里是检测是否是事件的content,例如伤害事件流程的content就是woundedContent(执行扣血)
                if (ev.name.indexOf('Content') !== -1) {
                    await player[ev.name](ev);  //player.woundedContent(ev)
                } else {
                    // 查询此时机是否有其他玩家的技能可以响应
                    // 如果有,则按当前回合玩家逆时针排序依次结算
                    // 如果无,则事件执行者直接结算
                    // findTriggerGlobal函数用于查找所有的global技能,例如【悲歌】【献图】【鸩毒】等
                    const skills = this.findTriggerGlobal(ev.name);
                    if (skills) {
                        // getPlayersBySkill即通过技能来查找玩家seat,返回一个数组
                        const seats = this.getPlayersBySkill(skills);
                        // 将事件执行者也push进去,进行排序
                        seats.push(ev.player);
                        // 获取排序后的玩家seat列表
                        const sorted = intersection(this.currenSeats, seats);
                        const players = this.getPlayers(sorted);
                        let i = 0;
                        while (i < players.length) {
                            ev.player = sorted[i];
                            // trigger方法用于玩家发动技能,是一个async方法
                            await players[i].trigger(ev.name, ev);  //await是核心
                            i++;
                        }
                    } else {
                        await player.trigger(ev.name, ev);
                    }
                }
            }
        }
    },
    

    下一篇阶段篇将顺带讲解技能实现哦!想继续看的关注我吧,嘻嘻

    相关文章

      网友评论

          本文标题:100行代码写出三国杀结算流程(体力篇)

          本文链接:https://www.haomeiwen.com/subject/gharmktx.html