乐趣区

关于前端:教你做小游戏-展示斗地主扑克牌支持按出牌规则排序支持按大小排序

我是 HullQin,公众号 线下团聚游戏 的作者(欢送关注公众号,发送加微信,交个敌人),转发本文前需取得作者 HullQin 受权。我独立开发了《联机桌游合集》,是个网页,能够很不便的跟敌人联机玩斗地主、五子棋等游戏,不免费没广告。还开发了《Dice Crush》加入 Game Jam 2022。喜爱能够关注我 HullQin 噢~我有空了会分享做游戏的相干技术。

问题形容

咱们想做一个斗地主游戏,其中最重要的一点是,把扑克牌展现进去。

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

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

而展现牌有 3 种排序形式:

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

明天,咱们就来实现它们!

第 1 步,展现 1 张牌

筹备素材

牌有 54 种,加上牌反面,有 55 种图案。咱们先筹备好素材:

如果要展现 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 噢~我有空了会分享做游戏的相干技术。

退出移动版