8

[教你做小游戏] 展示斗地主扑克牌,支持按出牌规则排序!支持按大小排序!

 1 year ago
source link: https://blog.51cto.com/hullqin/5640181
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

大家好,我是公众号「线下聚会游戏」作者HullQin,开发了 《联机桌游合集》,是个网页,可以很方便的跟朋友联机玩斗地主、五子棋等游戏。

我们想做一个斗地主游戏,其中最重要的一点是,把扑克牌展示出来。

一副牌有54张,我们给每张牌1个编号(id),取值1-54。如果涉及到2副牌,就取id为1-108。

展示牌,其实就是给你一个id列表,按需展示列表中的牌即可。

而展示牌有3种排序方式:

  1. 不排序,列表是什么,就展示什么。(发牌、底牌常用)
  2. 按照大小排序。(手牌常用)
  3. 按照出牌规则排序。(出牌常用,规则比如顺子、连对、飞机、四带二、炸弹等)

今天,我们就来实现它们!

第1步,展示1张牌

牌有54种,加上牌背面,有55种图案。我们先准备好素材:

[教你做小游戏] 展示斗地主扑克牌,支持按出牌规则排序!支持按大小排序!_js

如果要展示1张牌,以它为背景,使用background-positionwidthheight对整个大图片裁剪即可。

不要拆开这个大图,让用户一次性下载55张图片,那样速度会肉眼可见的慢。因为大多数浏览器不能并发55个请求下载图片,它可能一次最多建立8个TCP连接来下载,你可能需要8次RTT才能下载完。(55除以8向上取整=8)

所以,做Web开发,一定要尽量拼接多个小素材成为一个大图片,再去裁剪它展示素材。

写好css做裁剪

我们利用class,定义一个.poker写所有扑克牌共用的样式,再给每个扑克牌定义一个background-position(裁剪位置)即可。

.poker {
  position: absolute;
  background-image: url('./card.png');
  background-clip: content-box;
  background-repeat: no-repeat;
  width: 116px;
  height: 159px;
  transform-origin: 0 0 0;
  transition: left .2s ease-out, top .2s ease-out;
}

例如,这是id为1的扑克牌的样式。每个扑克牌单独的样式很简单,只有1行,定义background-position即可。因为其它样式都是一模一样的,用.poker复用即可。

.poker-1 {
  background-position: -238px -646px;
}

不再罗列了,可以参考style.css源码:  github.com/HullQin/poker_fe

定义 扑克牌ID->图片ID 的映射

开头我们提到,可能有2幅牌,而他们的图片样式应该是一样的。所以需要通过取余数,把108个ID映射到54个值。

const mapPokerIdToCardId = (id) => {
  // 映射扑克id(可能有多幅牌)至卡片id(只有0-54)
  return (id - 1) % 54 + 1;
};

代码中,我用id = 0表示扑克牌的背面。

封装一个组件

你可以封装为React组件或Vue组件,或其它你采用框架支持的组件。

我代码使用了React,所以封装为React组件。

import cn from 'classnames';

const Poker = (props) => {
  const { id, className, ...otherProps } = props;
  if (typeof id !== 'number') return;
  const cardId = mapPokerIdToCardId(id);
  return (
    <div
      className={cn('poker', `poker-${cardId}`, className)}
      {...otherProps}
    />
  );
};

这是一个非常简洁的组件,只需要传入扑克牌的ID,就会展示这张扑克牌了。此外,可以传入className或者style,自定义样式。

至此,展示1张扑克牌,我们就完成啦!

第2步,不排序展示多张牌

目前还比较简单,只需要提供一个扑克牌ID列表,我们依次展示即可。我们用ids参数作为扑克牌ID列表,需要组件引用者传入。

我们需要关注一下扑克牌图片的高度,我们定义一个默认高度(159),此外也允许引用组件者通过height新设置高度。

我们还需要关注扑克牌之间的间隔:如果是底牌,那么间隔大一些;如果是手牌、或者出牌,牌会比较多,间隔应该是负数,有重叠的效果。我们用overlap参数,表示是否需要重叠。

const StaticPokerList = (props) => {
  const { ids, overlap, height = 159, className, style, ...otherProps } = props;
  const gap = (overlap ? 48 : 116) * height / 159;
  return (
    <div className={cn('static-poker-list', className)} style={{ height, ...style }} {...otherProps}>
      {ids.map((id, index) => (
        <Poker
          key={index}
          id={id}
          style={{ left: index * gap, transform: `scale(${height / 159})` }}
        />
      ))}
    </div>
  );
};

可以看到,我们引用了Poker组件,并控制了每一个扑克牌的left属性,让它们等间距排列。

你可能会问:啊!你为什么用列表的index做Key呢?为什么不用扑克牌ID做Key呢?

因为我们这个列表非常小,不超过108,不会有性能问题,所以采用了最稳妥的方式,以index作Key,是独一无二的,绝不会出错。如果你能保证你传入的扑克牌ID唯一,也可以使用扑克牌ID作Key。

第3步,按照大小排序

扑克牌是有大小的,顺序是:大王、小王、2、A、K、Q、J、10、9、8、7、6、5、4、3。

此外,为了美观,我们也期望同样大小的数字的花色,也是有顺序的。例如按照♥️、♦️、♠️、♣️的顺序排列,当你有很多炸弹时,会非常漂亮,令玩家舒适。

所以,我们要按数字大小排列,数字相同时,按固定花色顺序排列。

只要修改一下StaticPokerList,对它的ids参数做一个排序即可。

排序依据是什么呢?需要手写函数嘛?

答案是:当然不需要!只要我们把54个ID映射到54个数字,再按数字排序,就大功告成了!这是效率非常高的方式!

这定义了映射,传入ID为1-54,即可映射到牌的具体大小。规定四个花色的小数部分不一样,分别为.2 .4 .6 .8,这样数字相同时,就按花色排序啦。

定义好每张牌的数字,再根据大小数值排序即可。

const pokerMap = [0, 14.2, 15.2, 3.2, 4.2, 5.2, 6.2, 7.2, 8.2, 9.2, 10.2, 11.2, 12.2, 13.2, 14.4, 15.4, 3.4, 4.4, 5.4, 6.4, 7.4, 8.4, 9.4, 10.4, 11.4, 12.4, 13.4, 14.6, 15.6, 3.6, 4.6, 5.6, 6.6, 7.6, 8.6, 9.6, 10.6, 11.6, 12.6, 13.6, 14.8, 15.8, 3.8, 4.8, 5.8, 6.8, 7.8, 8.8, 9.8, 10.8, 11.8, 12.8, 13.8, 54, 53];

这就是排序函数,传入ids,输出排好序的ids

const sortPokersById = (ids) => {
  return ids.sort((a, b) => pokerMap[mapPokerIdToCardId(b)] - pokerMap[mapPokerIdToCardId(a)]);
};

当然,这调用了mapPokerIdToCardId,是考虑到2幅牌的情况,先把1-108映射到1-54,再映射到具体数值。

第4步,按照规则排序

上面按大小排序还是太简单,只有结合了游戏规则的排序,才是最难的!

我根据斗地主规则,总结了这样的排序算法:

输入:ids,即你出的牌的列表(前提:是符合斗地主规则的一串牌)。

输出:sortedIds,按出牌规则排好序的列表。

  1. 统计每个数字的出现次数。
  2. 按照出现次数排序,出现频次高的,放在前面。
  3. 如果频次相同,按照数字大小排序。数字小的,放在在前。
  4. 同样的数字,要按照固定花色顺序排序,保证美观。

验证算法正确性:

  • 顺子:3、4、5、6、7。频次都是1,排序结果是3、4、5、6、7。
  • 连对:QQ、KK、AA。频次都是2,排序结果是QQ、KK、AA。
  • 三带一:KKK2。K频次是3,2频次是1。排序结果是KKK、2。
  • 四带两对:44443322。4频次是4,3和2频次是2。排序结果是44443322。
  • 王炸:大王、小王。频次都是1,规定大王数字更小,那么排序结果是大王、小王。

这里定义pokerNumberMap为数字大小,为了让王炸时大王在前小王在后,我们规定大王=53、小王=54即可。

pokerRuleMap同样有小数部分,是为了同数字时按花色排序。

const pokerRuleMap = [0, 14.2, 15.2, 3.2, 4.2, 5.2, 6.2, 7.2, 8.2, 9.2, 10.2, 11.2, 12.2, 13.2, 14.4, 15.4, 3.4, 4.4, 5.4, 6.4, 7.4, 8.4, 9.4, 10.4, 11.4, 12.4, 13.4, 14.6, 15.6, 3.6, 4.6, 5.6, 6.6, 7.6, 8.6, 9.6, 10.6, 11.6, 12.6, 13.6, 14.8, 15.8, 3.8, 4.8, 5.8, 6.8, 7.8, 8.8, 9.8, 10.8, 11.8, 12.8, 13.8, 53, 54];
const pokerNumberMap = [0, 14, 15, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 54, 53];

下面是排序的函数,可以看到,我们先统计了frequency,对ids排序时,判断了2个数字谁出现频次更高,高的靠前。

const sortPokersByRule = (ids) => {
  const frequency = {};
  for (const id of ids) {
    const cardNumber = pokerNumberMap[mapPokerIdToCardId(id)];
    if (cardNumber in frequency) {
      frequency[cardNumber] += 1;
    } else {
      frequency[cardNumber] = 1;
    }
  }
  return ids.sort((a, b) => {
    a = mapPokerIdToCardId(a);
    b = mapPokerIdToCardId(b);
    const frequencyA = frequency[pokerNumberMap[a]];
    const frequencyB = frequency[pokerNumberMap[b]];
    if (frequencyA === frequencyB) {
      return pokerRuleMap[a] - pokerRuleMap[b];
    }
    return frequencyB - frequencyA;
  });
};

我是HullQin,公众号线下聚会游戏的作者(欢迎关注公众号,发送加微信,交个朋友),转发本文前需获得作者HullQin授权。我独立开发了 《联机桌游合集》,是个网页,可以很方便的跟朋友联机玩斗地主、五子棋等游戏,不收费没广告。还独立开发了 《合成大西瓜重制版》。还开发了 《Dice Crush》参加Game Jam 2022。喜欢可以关注我  HullQin 噢~我有空了会分享做游戏的相关技术。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK