前言
- 封面图源于百度图片,侵删(留言)谢谢🙏。
- 扑克牌-斗牛牛游戏相信很多人都玩儿过吧,不知道的没关系,给大家简单介绍下规则:
- 拿一副扑克牌去掉大小王和花牌,只保留剩余的
2~A的4种不同花色的52张; - 每个人从同一副牌中取出
5张牌,不同的牌计分如下表;
- 拿一副扑克牌去掉大小王和花牌,只保留剩余的
- 得分计算:
- 找出总和能被
10整除的5张牌中的3张; - 如果可以识别出这样的
3张牌组,剩余2张牌总和如果小于等于10,得分则为2张剩余牌的总和,否则为总和减10(多种3张牌组的总和能组成10的倍数时,需要让剩余2张牌的得分最大)。 - 如果不能识别出这样的
3张牌组,则得分为0; - 例如
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”。 - 文末有整个游戏源码。
开始梳理
- 通过上一部分,游戏规则都清楚了,接下来就是梳理整个流程了,我们需要做哪些事情呢?
- 获取给定字符串中所有的数值;
- 计算是否存在
3张牌组可以被10整除,并记录剩余2张的得分; - 如果分数相同,怎么找出最高点数及对应的花色;
- 依次判断分数、最高点数、花色,得出胜者。
获取给定字符串中所有的数值
- 首先由于分数点对应的存在
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来计算,出了问题)。假定的所有给的测试用例都是正确的。- 遍历判断当前索引之后的第二位是否存在并且对应的不是花色;
- 如果不是花色就取向后两位表示分数点,并转换为对应的数值,索引移动
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),再将五个对象放入一个数组中,根据对象的key及value进行大到小排序,排序后取数组第一个,即为对应玩家手牌的最大值及对应花色。
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中正则左右的斜线需要去掉。 - 写完一看,嗯???还有没匹配上的,我赶紧检查了一下写的正则,发现没问题啊。我又看了一下没匹配上的数据,嗯...,居然是有问题的数据,那我手动删掉吗?当然不,几百条呢?我直接修改正则,先转为能用的数据再说。转换后数据
/[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();
拓展
- 如果过滤掉不满足长度为
10的数据之后,还存在异常数据,该怎么校验数据的正确性呢?- 相关的校验正则仓库
test.js文件中有,可以先自己想想、写一写,在对比一下。改了正则后发现有15条数据是满足/([A-Z0-9]{10,};[A-Z0-9]{10,})/g,但却不是有效数据的。
- 相关的校验正则仓库
- 现在这个是两个人的玩儿的,且数据是死数据,我们是否能改成自定义人数(
2~10人)呢? - 其它问题或建议欢迎留言谈论。
GitHub仓库源码,欢迎Fock、Star。
往期精彩
- 前端开发基本规范
- 从0搭建Vite + Vue3 + Element-Plus + Vue-Router + ESLint + husky + lint-staged
- 「前端进阶」JavaScript手写方法/使用技巧自查
- JavaScript设计模式之简介及创建型模式
- 公众号打开小程序最佳解决方案(Vue)
- Axios你可能不知道使用方式
「点赞、收藏和评论」
❤️关注+点赞+收藏+评论+转发❤️,创作不易,鼓励笔者创作更好的文章,谢谢🙏大家。