我是 HullQin,公众号 线下团聚游戏 的作者(欢送关注公众号,发送加微信,交个敌人),转发本文前需取得作者 HullQin 受权。我独立开发了《联机桌游合集》,是个网页,能够很不便的跟敌人联机玩斗地主、五子棋等游戏,不免费没广告。还开发了《Dice Crush》加入 Game Jam 2022。喜爱能够关注我 HullQin 噢~我有空了会分享做游戏的相干技术。
问题形容
咱们想做一个斗地主游戏,其中最重要的一点是,把扑克牌展现进去。
一副牌有 54 张,咱们给每张牌 1 个编号(id),取值 1 -54。如果波及到 2 副牌,就取 id 为 1 -108。
展现牌,其实就是给你一个 id 列表,按需展现列表中的牌即可。
而展现牌有 3 种排序形式:
- 不排序,列表是什么,就展现什么。(发牌、底牌罕用)
- 依照大小排序。(手牌罕用)
- 依照出牌规定排序。(出牌罕用,规定比方顺子、连对、飞机、四带二、炸弹等)
明天,咱们就来实现它们!
第 1 步,展现 1 张牌
筹备素材
牌有 54 种,加上牌反面,有 55 种图案。咱们先筹备好素材:
如果要展现 1 张牌,以它为背景,应用 background-position
和width
、height
对整个大图片裁剪即可。
不要拆开这个大图,让用户一次性下载 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,按出牌规定排好序的列表。
- 统计每个数字的呈现次数。
- 依照呈现次数排序,呈现频次高的,放在后面。
- 如果频次雷同,依照数字大小排序。数字小的,放在在前。
- 同样的数字,要依照固定花色程序排序,保障好看。
验证算法正确性:
- 顺子: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 噢~我有空了会分享做游戏的相干技术。