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

没错,就是你认为的那个三国杀(#滑稽)
当然,此次还是用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);
                }
            }
        }
    }
},

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349