手把手带你实现一个双人版的“斗牛牛”游戏

1,330 阅读6分钟

前言

  • 封面图源于百度图片,侵删(留言)谢谢🙏。
  • 扑克牌-斗牛牛游戏相信很多人都玩儿过吧,不知道的没关系,给大家简单介绍下规则:
    1. 拿一副扑克牌去掉大小王和花牌,只保留剩余的 2~A4 种不同花色的 52 张;
    2. 每个人从同一副牌中取出 5 张牌,不同的牌计分如下表; image.png
  • 得分计算:
    1. 找出总和能被 10 整除的 5 张牌中的 3 张;
    2. 如果可以识别出这样的 3 张牌组,剩余 2 张牌总和如果小于等于 10,得分则为 2 张剩余牌的总和,否则为总和减 10(多种 3 张牌组的总和能组成 10 的倍数时,需要让剩余 2 张牌的得分最大)。
    3. 如果不能识别出这样的 3 张牌组,则得分为 0
    4. 例如 score(J, Q, K, 5, 8) = 3, score(2, Q, K, 5, 3) = 10, score(A, 2, 3, 4, A) = 0, score(5, 6, 10, 9, 3) = 3
  • 如果分数不同,则分数高的获胜;
  • 如果分数相同,则按照计分表中玩家手中的最高点数比较(K > Q > J > 10 ...);
  • 如果分数和最高点数都相同,那么就按照黑桃(S) > 红心(H) > 梅花(C) > 菱形(D) 比较最高点数的花色。即“H9S7CAC2D7”(红心 9,黑桃 7,梅花 A,梅花 2 和菱形 7)击败 “D9D5C6S5DA”
  • 文末有整个游戏源码

开始梳理

  • 通过上一部分,游戏规则都清楚了,接下来就是梳理整个流程了,我们需要做哪些事情呢?
    1. 获取给定字符串中所有的数值;
    2. 计算是否存在 3 张牌组可以被 10 整除,并记录剩余 2 张的得分;
    3. 如果分数相同,怎么找出最高点数及对应的花色;
    4. 依次判断分数、最高点数、花色,得出胜者。

获取给定字符串中所有的数值

  • 首先由于分数点对应的存在 J、Q、K、A 四个字母,所以我想到的是定义一个键值转换的对象,可以将分数点对应的字符串转为对应的数字分数点。
// 字符串转化为对应的数值
const score = {
    'A': 1,
    '2': 2,
    '3': 3,
    '4': 4,
    '5': 5,
    '6': 6,
    '7': 7,
    '8': 8,
    '9': 9,
    '10': 10,
    'J': 10,
    'Q': 10,
    'K': 10,
};
  • 遍历给定玩家手牌的字符串,获取对应的 5 张牌分数;(这里在最开始考虑的时候漏掉了 10 这个数字,所有的字符串长度都按照 10 来计算,出了问题)。假定的所有给的测试用例都是正确的。
    1. 遍历判断当前索引之后的第二位是否存在并且对应的不是花色;
    2. 如果不是花色就取向后两位表示分数点,并转换为对应的数值,索引移动 3 位;
    3. 如果是花色就取向后一位表示分数点,并转换为对应的数值,索引移动 2 位;
// 这里假定的所有给的测试用例都是正确的
const filterString = str => {
    const len = str.length;
    const list = [];
    for(let i = 0; i < len;) {
        let NumberValue, flag;
        if(str[i + 2] && !typeList.includes(str[i + 2])) { // 索引之后的第二位是否存在并且对应的不是花色
            NumberValue = score[str.slice(i+1, i + 3)];
            flag = false;
        } else {
            NumberValue = score[str[i+1]];
            flag = true;
        }
        list.push(NumberValue);
        flag ? i += 2 : i += 3;
    }
    return list

计算是否存在 3 张牌组可以被 10 整除,并记录剩余 2 张的得分

  • 上一步我们已经拿到点数数值的数组,接下来就是怎么计算得分了。
  • 最开始我想的是枚举所有组成 3 个和 2 个数组,再对每一个求和判断;想了一下这样做十分复杂,空间浪费多;然后同事说了一句先整个求和再减去遍历的 2 个数值的和不就行了吗?有时候过于固执于正向求解,也可以考虑反向求解。
  • 由于我们只需要考虑最大的分数,不需要记录相同的分数,所以我用的 Set 数据结构,存在相同值就过滤掉,不需要自己再写一个过滤判断。
// 获取最大分数
const getMaxScore = list => {
    const result = new Set([0]);
    const len = list.length;
    const sum = list.reduce((pre, next) => pre + next, 0);
    for(let i = 0; i < len - 1; i ++) {
        for(let j = i + 1; j < len; j ++) { // 遍历到倒数第二个结束,因为需要 2 个数相加, 而且 最后一个 i + 1 无意义
            const twoSum = list[i] + list[j];
            const threeSum = sum - twoSum;
            if(threeSum % 10 === 0) {
                result.add(twoSum  > 10 ? twoSum - 10 : twoSum); // 总和不大于 10,得分则为 2 张剩余牌的总和,否则为总和减 10
            }
        }
    }
    return Math.max(...result);
}

如果分数相同,怎么找出最高点数及对应的花色

  • 通过上一步,我们已经可以求出玩家的分数了,那如果分数相同,又该如何去拿到对应最大分数点及对应的花色呢?
方法一
  • 首先,我们定义一个分数点的数组和不同花色类型的数组
// 分数点
const typeList = ['A', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K'];
// 不同花色
const typeList = ['S', 'H', 'C', 'D'];
  • 然后,我们从后往前遍历分数点数组,当玩家手牌字符串存在对应的分数点时,我们获取对应分数点字符串的索引下标,然后再获取下标的前一个值即可。
方法二
  • 获取给定字符串中所有的数值的方法中,我们获取到对应的分数点时,将分数点转化为一个对象的 key(定义 scoreKey),对应的 value 为花色权值(定义花色权值对象 typeScore),再将五个对象放入一个数组中,根据对象的 keyvalue 进行大到小排序,排序后取数组第一个,即为对应玩家手牌的最大值及对应花色。
const scoreKey = Object.assign(score, {
    'J': 11,
    'Q': 12,
    'K': 13,
})
// 不同花色权值
const typeScore = {
    'S': 4, // 黑桃
    'H': 3, // 红桃
    'C': 2, // 梅花
    'D': 1 // 方片
};
/**
 * @Description: 过滤字符串
 * @param {String}
 * @return {[Object, Array]}
 */
const filterString = str => {
    const len = str.length;
    const strObjList = [];
    const list = [];
    for(let i = 0; i < len;) {
        // 排序数组对象键,分数数值,后移位数
        let key, NumberValue, flag;
        if(str[i + 2] && !typeList.includes(str[i + 2])) { // 索引之后的第二位是否存在并且对应的不是花色
            key = scoreKey[str.slice(i+1, i + 3)];
            NumberValue = score[str.slice(i+1, i + 3)];
            flag = false;
        } else {
            key = scoreKey[str[i+1]];
            NumberValue = score[str[i+1]];
            flag = true;
        }
        strObjList.push({ // 存入五张牌的对象
            key, // 分数点
            value: typeScore[str[i]] // 花色权值
        });
        list.push(NumberValue);
        flag ? i += 2 : i += 3;
    }
    strObjList.sort((a, b) => {
        if(a.key > b.key) { // 分数点排序
            return -1;
        } else if(a.key === b.key) { // 分数相同,花色权值排序
            return a.value - b.value > 0 ? -1 : 1;
        }
        return 1;
    })
    const strMaxObj = strObjList[0];
    return [strMaxObj, list];
}

依次判断分数、最高点数、花色,得出胜者

  • 需要的数据通过前几步已经拿到了,这部分就很简单了,顺序调用写好的方法即可。
// 获取赢者
const getWinPlayer = (player1, player2) => {
    const [player1_strMaxObj, player1_list] = filterString(player1);
    const [player2_strMaxObj, player2_list] = filterString(player2);
    const player1_score = getMaxScore(player1_list);
    const player2_score = getMaxScore(player2_list);

    // 对比分数
    if(player1_score > player2_score) {
        return 'Leon';
    } else if (player1_score < player2_score) {
        return 'Judy'
    } else { // 分数相同
        // 对比最大值
        if(player1_strMaxObj.key > player2_strMaxObj.key) {
            return 'Leon';
        } else if (player1_strMaxObj.key < player2_strMaxObj.key) {
            return 'Judy'
        } else { // 最大值相同
            // 对比花色
            if(player1_strMaxObj.value > player2_strMaxObj.value) {
                return 'Leon';
            } else if (player1_strMaxObj.value < player2_strMaxObj.value) {
                return 'Judy'
            }
        }
    }
}

其它

  • 本以为到这就结束了,直到我拿到测试数据,我才发现还没完。
  • 数据有 3400+ 测试数据 ,而且不是想要的数据结构,那该怎么办呢?观察发现两组数据为一行且用英文分号分割,那就该展示我们的正则表达式功底了。使用 VS Code 编辑器打开文件,Mac 使用 command + f 打开搜索(Windows 使用 ctrl + f),选中正则。
/[A-Z0-9]{10,};[A-Z0-9]{10,}/
  • 注意:VS Code 中正则左右的斜线需要去掉。 image.png
  • 写完一看,嗯???还有没匹配上的,我赶紧检查了一下写的正则,发现没问题啊。我又看了一下没匹配上的数据,嗯...,居然是有问题的数据,那我手动删掉吗?当然不,几百条呢?我直接修改正则,先转为能用的数据再说。转换后数据
/[A-Z0-9]{1,};[A-Z0-9]{1,}/
  • OK,数据是能用了,那就改测试了,既然有异常数据那就过滤掉。
// 过滤无效数据
const filterData = data => {
    const filter = data.filter(item => {
        const regexp = /([A-Z0-9]{10,};[A-Z0-9]{10,})/g;
        return regexp.test(item);
    });
    return filter;
}
// 最终胜者
const getFinallyWinner = () => {
    const newData = filterData(testData);
    let leonCount = 0, judyCount = 0;
    newData.map(item => {
        const players = item.split(';')
        const win = getWinPlayer(players[0], players[1]);
        win === 'Leon' ? leonCount ++ : judyCount ++;
    })
    return `Leon赢:${leonCount} 次,Judy赢:${judyCount} 次`;
}

document.getElementById('winner').innerHTML = getFinallyWinner();

拓展

  1. 如果过滤掉不满足长度为 10 的数据之后,还存在异常数据,该怎么校验数据的正确性呢?
    • 相关的校验正则仓库 test.js 文件中有,可以先自己想想、写一写,在对比一下。改了正则后发现有 15 条数据是满足 /([A-Z0-9]{10,};[A-Z0-9]{10,})/g,但却不是有效数据的。
  2. 现在这个是两个人的玩儿的,且数据是死数据,我们是否能改成自定义人数(2~10人)呢?
  3. 其它问题或建议欢迎留言谈论。
  4. GitHub 仓库源码,欢迎 FockStar

往期精彩

「点赞、收藏和评论」

❤️关注+点赞+收藏+评论+转发❤️,创作不易,鼓励笔者创作更好的文章,谢谢🙏大家。