首先将这个游戏需求拆分成三个部分:
假如我们的盒子模型不是个二维的平面,而是有个三维的体积,让它可以有正反两面,那我们在做的时候是不是只要将它真实的翻个面就可以了。让我们来想想将它变成三维的方法。 之后发现了这个属性:
1 |
transform: translateZ(1px); |
使用了它,就可以把盒子内部的元素与盒子的底部撑出个高度。
1 2 3 4 |
<!-- html --> <div class="card"> <div class="top">我是正面哦~</div> </div> |
只用给叫做“top”的子盒子一个“距离父亲的距离”,再将叫做“card”的父盒子预先翻转180度rotateY(180deg),等到点击的时候给它翻回来transform: rotateY(0)就可以了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
.card{ ... height: 100%; width: 100%; position: relative; transform-style: preserve-3d; transform: rotateY(180deg); transition: all 600ms; background: pink; &.select { transform: rotateY(0); } .top{ ... height: 100%; width: 100%; position: absolute; top: 0; left: 0; box-sizing: border-box; background: white; border: 2px solid #b6a6dc; transform: translateZ(1px); } } |
我们先来说下在理想环境中,每个元素都能匀均出现(次数相等)的情况。再来说下不能均匀出现的情况下,怎样最大限度的均匀。
此算法脑内模型由西塔(θ)先生友情提供
假设我们一共需要20个元素,有5个不同类型的格子,正好每个格子出现4次。我们就有了一个待分配元素的集合W:
1 2 3 4 5 6 7 8 |
const total = 20 const icons = ['a', 'b', 'c', 'd', 'e'] // => 得到集合W const W = ['a', 'a', 'a', 'a', 'b', 'b', 'b', 'b', 'c', 'c', 'c', 'c', 'd', 'd', 'd', 'd', 'e', 'e', 'e', 'e'] |
混淆集合
有个指针p从下标0开始,在长度为20的数组格子里面负责填图案,填图案的规律是从集合w中随机取一个元素,取完后删除该元素,p移动到下一个格子里,迭代至完成。
1 2 3 4 5 6 7 8 9 10 11 12 |
function createRandomList(W: string[], total: number) { const list: any[] = [] function it(time: number): any { if (time === 0) return list // 随机每次集合元素下标 const randomNum = Math.floor(Math.random() * (W.length)) list.push(W[randomNum]) // 新数组中加入随机到的元素 W.splice(randomNum, 1) // 删除集合中的元素 return it(--time) } return it(total) } |
我们再让这个方法灵活一点,使它的返回结果能够随便指定格式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// fn非必传项,默认返回原数据 function createRandomList(W: string[], total: number, fn: (<T>(icon: string, index?: number) => T) = icon => icon) { const list: any[] = [] // 迭代器 function it(time: number): any { if (time === 0) return list // 随机每次集合元素下标 const randomNum = Math.floor(Math.random() * (W.length)) list.push(fn(W[randomNum], total-time)) // 将元素和下标传入fn中,将fn的计算结果加入到新数组中 W.splice(randomNum, 1) // 删除集合中的元素 return it(--time) } return it(total) } |
1 |
const W = [] |
不均匀元素,其实就是集合W里的元素分布规则改变了,混淆算法仍然不受影响。之后,让我们来思考下怎么定义一个“不均匀中的最大程度均匀”的集合。 将集合W分为两个部分: 最大可均匀分布部分 + 随机部分
最大可均匀分布的部分,它代表着icons中的每个元素都能出现相同的最多偶数次。可以这样得到它:
1 2 3 4 |
// 得到最大重复次数 const times = Math.floor(total / (icons.length * 2)) for (let index = 0; index < times; index++) W.push(...icons, ...icons) |
剩下的是需要随机分布的部分,它代表着,某几个元素可以在这里出现2次,剩下的则不会出现。
第(3)条是不是听起来很耳熟,好像前面做过,没错就是前面写的createRandomList函数,W集合变成了icons,total变成了需要的个数n。
1 2 3 4 5 |
// 剩下未分配的格子个数 const lastCount = total % (icons.length * 2) // 从icons中随机获取n个数 const lastList = createRandomList(icons, lastCount / 2) W.push(...lastList, ...lastList) |
合在一起就是就是创建W的方法:
1 2 3 4 5 6 7 8 9 10 |
function createW(icons: string[], total: number) { const times = Math.floor(total / (icons.length * 2)) const lastCount = total % (icons.length * 2) const W = [] for (let index = 0; index < times; index++) W.push(...icons, ...icons) const lastList = createRandomList(icons, lastCount / 2) W.push(...lastList, ...lastList) return W } |
完整的生成随机数组代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
function createW(icons: string[], total: number) { const times = Math.floor(total / (icons.length * 2)) const lastCount = total % (icons.length * 2) const W = [] for (let index = 0; index < times; index++) W.push(...icons, ...icons) const lastList = createRandomList(icons, lastCount / 2) W.push(...lastList, ...lastList) return W } function createRandomList(W: string[], total: number, fn: (<T>(icon: string, index?: number) => T) = icon => icon) { const list: any[] = [] function it(time: number): any { if (time === 0) return list const randomNum = Math.floor(Math.random() * (W.length)) list.push(fn(W[randomNum], total-time)) W.splice(randomNum, 1) return it(--time) } return it(total) }
// ['a', 'b', 'c', "d"] => ['c', 'd'...x15...'b', 'c', 'a'] createRandomList(createW(icons, total), total) |
乱序的随机数组有了,点一点还不简单吗! 先让生成的数组属性更丰富一些,来帮助我们展示内容。
1 2 3 4 5 6 |
type CardItem = { icon: string; isDel: boolean; isSelect: boolean, index: number }
let list: CardItem[] = []
// isSelect属性判断是否翻转,isDel属性判断是否已经消除,icon属性标注元素属性,index用来快速找到点击元素位于数组中的位置 list = createRandomList(createW(icons, total), total, (icon: string, index) => ({ icon, isDel: false, isSelect: false, index })) |
这下可以用生成的数组去展示了。接下来我们写个点击事件,接收参数是点击的数组元素:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// isLock用来锁定动画完成前不能进行别的操作 function handlerTap(card: CardItem) { if (isLock) return list[card.index].isSelect = true const selectors = list.filter(item => item.isSelect && !item.isDel) // 假如选择元素<2,直接返回,不走之后流程 if (selectors.length <= 1) return isLock = true const [item1, item2] = selectors // 翻转动画完成后进行操作 setTimeout(() => { // 如果选择的元素相同,则消除属性等于true if (item1.icon === item2.icon) { list[item1.index].isDel = true list[item2.index].isDel = true } //将所有卡牌翻转过背面 list = list.map(item => ({...item, isSelect: false})) isLock = false // 判断是否所有卡牌都已经翻转完成 if (list.every(item => item.isDel)) console.log( "your win!") }, 800) } |
100行整)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 |
<script lang="ts"> type CardItem = { icon: string; isDel: boolean; isSelect: boolean, index: number } const icons = ['a', 'b', 'c', "d"] const total = 20 let list: CardItem[] = [] let isLock = false
function handlerTap(card: CardItem) { if (isLock) return list[card.index].isSelect = true const selectors = list.filter(item => item.isSelect && !item.isDel) if (selectors.length <= 1) return isLock = true const [item1, item2] = selectors setTimeout(() => { if (item1.icon === item2.icon) { list[item1.index].isDel = true list[item2.index].isDel = true } list = list.map(item => ({...item, isSelect: false})) isLock = false if (list.every(item => item.isDel)) console.log( "your win!") }, 800) } function createW(icons: string[], total: number) { const times = Math.floor(total / (icons.length * 2)) const lastCount = total % (icons.length * 2) const W = [] for (let index = 0; index < times; index++) W.push(...icons, ...icons) const lastList = createRandomList(icons, lastCount / 2) W.push(...lastList, ...lastList) return W } function createRandomList(W: string[], total: number, fn: (<T>(icon: string, index?: number) => T) = icon => icon) { const list: any[] = [] function it(time: number): any { if (time === 0) return list const randomNum = Math.floor(Math.random() * (W.length)) list.push(fn(W[randomNum], total-time)) W.splice(randomNum, 1) return it(--time) } return it(total) }
list = createRandomList(createW(icons, total), total, (icon: string, index) => ({ icon, isDel: false, isSelect: false, index })) </script> <div class="game-box"> {#each list as item} <div class="grid"> {#if !item.isDel} <div class="card {item.isSelect && 'select'}" on:click="{() => handlerTap(item)}"> <div class="top">{item.icon}</div> </div> {/if} </div> {/each} </div> <style lang="less"> .game-box{ margin: 10px auto 0; width: 90vw; height: 80vh; display: grid; grid-template-columns: repeat(4, calc(100% / 4 - 3px)); grid-template-rows: repeat(5, calc(100% / 5 - 3px)); grid-row-gap:3px; grid-column-gap: 3px; .card{ height: 100%; width: 100%; box-sizing: border-box; position: relative; transform-style: preserve-3d; transform: rotateY(180deg); transition: all 600ms; background: pink; &.select { transform: rotateY(0); } .top{ height: 100%; width: 100%; position: absolute; top: 0; left: 0; box-sizing: border-box; display: flex; justify-content: center; align-items: center; background: white; border: 2px solid #b6a6dc; transform: translateZ(1px); } } } </style> |