就你小子不好好吃饭,搁着偷偷卷是吧——实现纸牌游戏

168 阅读5分钟

起因

上周五部门聚餐,有同事提出一个酒桌纸牌游戏,给予输的人一定的惩罚,好巧不巧轮到了我,但我以百般借口推脱,逃过一劫。

image.png 事后回想此事,感觉这个游戏逻辑也不复杂,就用vue将其实现。

游戏规则

1.游戏开始时,每人有一张起始牌,选择大、小按钮并点击猜牌按钮,出现结果牌。
2.当起始牌和结果牌数字大小相同时,根据花型(红桃>黑桃>梅花>方块)进行比较。
3.当结果牌为7,8时则需要重新猜牌,若结果牌大于起始牌则获胜,反之同理。
4.当结果牌为大王,小王时,游戏规则更改为猜颜色,若结果牌与起始牌同色,则获胜,反之同理。 5.其中红桃A最大,方块2最小。

游戏体验

考虑到有些小伙伴不愿意仔细看规则,可以先体验几局回过头再来看这个规则。

游戏实现

我分析一下比较关键的几个点,重要的是思路,源码戳这就可以看了。

初始化纸牌数组

一张扑克牌具有大小、花色、颜色(红/黑)、花色所占的权重值。那么类型数组如下

    const typeList = [
     {
       typeName: "红桃",
       weight: 4,
       color: "red",
       typeValue: "hearts",
       number: 2,
     },
     {
       typeName: "黑桃",
       weight: 3,
       color: "black",
       typeValue: "spades",
       number: 4,
     },
     {
       typeName: "梅花",
       weight: 2,
       color: "black",
       typeValue: "clubs",
     },
     {
       typeName: "方块",
       weight: 1,
       color: "red",
       typeValue: "diamonds",
     },
   ];

因为还有大小王,大小王额外进行定义

  const ghostCardList = [
     { typeName: "小王", number: 9999, weight: 99999, color: "black" },
     { typeName: "大王", number: 9999, weight: 99999, color: "red" },
   ];

其中规则中定义A的值最大,因此循环由2-14,其中14作为A的数值 到此为止,卡牌初始化的代码如下

   //初始化卡牌
 const initCard = () => {
   const typeList = [
     {
       typeName: "红桃",
       weight: 4,
       color: "red",
       typeValue: "hearts",
       number: 2,
     },
     {
       typeName: "黑桃",
       weight: 3,
       color: "black",
       typeValue: "spades",
       number: 4,
     },
     {
       typeName: "梅花",
       weight: 2,
       color: "black",
       typeValue: "clubs",
     },
     {
       typeName: "方块",
       weight: 1,
       color: "red",
       typeValue: "diamonds",
     },
   ];
   const ghostCardList = [
     { typeName: "小王", number: 9999, weight: 99999, color: "black" },
     { typeName: "大王", number: 9999, weight: 99999, color: "red" },
   ];
   for (let i = 2; i <= 14; i++) {
     typeList.forEach((item) => {
       state.cardList.push({
         number: i,
         typeName: item.typeName,
         weight: item.weight,
         color: item.color,
         typeValue: item.typeValue,
       });
     });
   }
   state.cardList = [...state.cardList, ...ghostCardList];
   console.log("list", state.cardList);
 };

但因为这样生成的纸牌是按顺序的,于是我们使用洗牌算法,模仿人的洗牌行为. 洗牌算法的核心倒序输出,随机交换

//洗牌算法
 const shuffle = (list) => {
   let len = list.length;
   for (let i = len - 1; i >= 0; i--) {
     let randomIndex = Math.floor(Math.random() * i);
     let temp = list[i];
     list[i] = list[randomIndex];
     list[randomIndex] = temp;
   }
   return list;
 };

到这一步纸牌的初始化算的上彻底完成。

猜牌

猜牌的本质是将下一张牌与当前这张牌进行规则比较,倘若是比大小,就是进行数字比较。要是猜黑红,就是颜色比较。
其中数字规则下,7/8需要重新猜牌,遇到大小王时,游戏规则变为猜黑红,猜黑红结束,游戏规则变回猜数字。

游戏规则是数字的处理

 //猜数字的规则
   if (state.gameRule == "num") {
     //如果猜到7,8,重新发牌,并且将拿到的牌更改为7,8
     if (state.guessCardRes.number == 7 || state.guessCardRes.number == 8) {
       showModal("平");
       console.log("因为您抽到7/8了,重新发牌");
       return;
     }
     const compareResult = compareCardNumRes(
       state.firstCard,
       state.guessCardRes
     );

     if (
       status == "big" &&
       state.guessCardRes.typeName != "大王" &&
       state.guessCardRes.typeName != "小王"
     ) {
       state.gameRes = compareResult < 0 ? "赢" : "输";
       showModal(state.gameRes);
     }
     if (
       status == "small" &&
       state.guessCardRes.typeName != "大王" &&
       state.guessCardRes.typeName != "小王"
     ) {
       state.gameRes = compareResult > 0 ? "赢" : "输";
       showModal(state.gameRes);
     }
   }

比较数字

 //先比较数字大小
 //再比较花色大小
 const compareCardNumRes = (firstCard, secondCard) => {
   console.log("第一张牌", firstCard);
   console.log("第二张牌", secondCard);
   console.log("比较", firstCard.number - secondCard.number);
   if (firstCard.number == secondCard.number) {
     return firstCard.weight - secondCard.weight;
   }
   return firstCard.number - secondCard.number;
 };

因为比较颜色大致和比较数字是同理,有兴趣的小伙伴直接看源码就好了。

计算赢的概率

举例:比如我猜比2大,只需要计算出剩下的全部纸牌中,比2大的元素有多少项/剩下纸牌的数组长度即可。
我的思路是将起始牌与剩下纸牌的数组进行运算,得到一个胜率数组,再胜率数组中
1. 元素>0表示比起始牌大。
2. 元素<0表示比起始牌小。
3. 元素=0表示平局。
最终根据是猜大还是猜小,对应算出获胜的几率是多少即可。

    //计算数字游戏的赢,输,平的概率
 const calcNumRate = (firstCard, secondCard) => {
   const paceList = [7, 8, 9999]; //7,8,大小王都是再来一次的机会
   if (paceList.includes(secondCard.number)) {
     return 0;
   }
   if (firstCard.number == secondCard.number) {
     return firstCard.weight - secondCard.weight;
   }
   return firstCard.number - secondCard.number;
 };
   //计算数字规则的概率
 const calcWinRate = () => {
   console.log("state.firstCard.number)", state.firstCard.number);
   const rateList = state.cardList.map((item) => {
     return calcNumRate(state.firstCard, item);
   });
   const bigWinRate = Math.round(
     (rateList.filter((item) => item < 0).length / state.cardList.length) *
       100
   );
   const smallWinRate = Math.round(
     (rateList.filter((item) => item > 0).length / state.cardList.length) *
       100
   );
   state.numBtnList[0].rate = bigWinRate;
   state.numBtnList[1].rate = smallWinRate;
   console.log("卡组", state.cardList.length);
 };

其他

不同结果对应不同的弹窗.

这段代码逻辑上没啥问题,但并不简洁,不推荐小伙们这样写

//弹窗提示
 const showModal = (status) => {
   setTimeout(() => {
     if (status == "赢") {
       ElMessageBox.confirm("恭喜您获得胜利,有两把刷子继续不?", "结果", {
         confirmButtonText: "奥利给,冲",
         cancelButtonText: "不了不了",
       })
         .then(() => {
           console.log("是否继续");
           resetCardStatus();
         })
         .catch(() => {
           //进入下一轮
           resetCardStatus();
           state.gameNumber += 1;
         });
     }

     if (status == "输") {
       ElMessageBox.alert("小伙子你寄了", "结果", {
         confirmButtonText: "拜拜了您",
         callback: () => {
           resetCardStatus();
           state.gameNumber += 1;
         },
       });
     }
     if (status == "平") {
       ElMessageBox.alert("7的意志来咯", "结果", {
         confirmButtonText: "再来一次",
         callback: () => {
           resetCardStatus();
         },
       });
     }
     if (status == "王") {
       ElMessageBox.alert("幸运女神朝你微笑哦", "结果", {
         confirmButtonText: "再来一次",
         callback: () => {
           calcColorRate();
           resetCardStatus("color");
         },
       });
     }
   }, 1000);

纸牌花色样式

为了尽可能的让卡牌像扑克牌,通过使用伪类元素:after,:before实现不同的花色

.diamonds:before,
.diamonds:after {
content: "♦";
color: #ff0000;
}

.hearts:before,
.hearts:after {
content: "♥";
color: #ff0000;
}

.clubs:before,
.clubs:after {
content: "♣";
color: #000;
}

.spades:before,
.spades:after {
content: "♠";
color: #000;
}
div[class*="suit"]:before {
position: absolute;
font-size: 35px;
left: 5px;
top: 5px;
}

div[class*="suit"]:after {
position: absolute;
font-size: 35px;
right: 5px;
bottom: 5px;
}

成品展示

image.png

image.png 游戏链接

末尾

最后,并不是每个人的脸皮实力都和作者一样,可以推脱。俗话说得好,要想在职场上如鱼得水,少不了几条为人处事的原则,作为在职场上摸爬滚打了2年多的社畜,给大家分享几条我的原则。

  1. 领导敬酒我不喝,领导夹菜我转桌
  2. 领导开门我上车,领导隐私我乱说
  3. 领导听牌我自摸,领导唱K我切歌 最后,祝大家升官发财。