#app
.info
button.btn(@click="gameInit") RESTART
.text
span your score: {{ score }}
span(v-if="gameOver") Game Over, thanks for coming
span(v-else) (use less match times to get more bouns score)
//- span {{ select }} {{ selectIndex }}
.container
.card(
v-for="(card, index) in cards.value",
:class="{ visible: card.isVisible, matched: card.isMatch }"
)
.card-cover(
:class="{ hidden: card.isVisible, matched: card.isMatch }",
@click="cardClick(card, index)"
)
ion-icon.icon(:name="card.icon")
View Compiled
@import url('https://fonts.googleapis.com/css2?family=Lobster&family=Waterfall&display=swap')
// const
card_w = 120px
card_h = 160px
bg = linear-gradient(45deg, #F1DAC4, #A69CAC)
card_bc = #FFF
card_cover_bc = #163045
card_border = 3px solid #000
// mixin
setSize(w, h)
width: w
height: h
// global
body
background: bg
margin: 0
display: flex
justify-content: center
align-items: center
height: 100vh
font-family: 'Lobster', cursive
.container
width: 'calc((%s * 6) + 1rem * 6)' % card_w
display: flex
flex-wrap: wrap
margin: 0.25rem
> *
margin: 0.25rem
.info
padding: 0.5rem
font-size: 1.5rem
display: flex
justify-content: space-between
.text
display: flex
align-self: center
padding: 0.5rem 1rem
> *
margin-right: 0.5rem
.btn
position: relative
cursor: pointer
border-radius: 0
padding: 0.5rem 1rem
font-size: 1.5rem
background-color: card_cover_bc
font-family: Lobster
overflow: hidden
color: #ddd
&:before
content: ''
position: absolute
setSize(100%, 100%)
background-color: rgba(#fff, 0.2)
left: 0
top: 0
transform: translateX(50%) skew(30deg)
transition: 0.3s
&:hover
&:before
transform: translateX(-50%) skew(30deg)
// card
.card
setSize(card_w, card_h)
position: relative
background-color: card_bc
border: card_border
border-radius: 4px
display: flex
justify-content: center
align-items: center
transition: 0.5s
transform: rotateY(180deg)
box-shadow: inset 0 0 0 4px #fff, inset 0 0 0 7px #333
&.visible
transform: rotateY(0deg)
&.matched
opacity: 0
pointer-events: none
.icon
font-size: 5.5rem
display: block
.card-cover
setSize(card_w, card_h)
position: absolute
cursor: pointer
background-color: card_cover_bc
transition: 0.5s
border-radius: 4px
border: card_border
z-index: 1
transform: rotateY(0deg)
backface-visibility: hidden
overflow: hidden
&:before
content: ''
position: absolute
setSize(card_w, card_h)
background-color: silver
transform: skew(35deg) scaleX(50%)
box-shadow: 0 0 0 50px card_cover_bc, 0 0 0 100px #fff, 0 0 0 150px card_cover_bc, 0 0 0 200px silver
&:after
content: ''
position: absolute
opacity: 0.3
setSize(card_w, card_h)
background-color: silver
transform: skew(-35deg) scaleX(50%)
box-shadow: 0 0 0 50px card_cover_bc, 0 0 0 100px silver, 0 0 0 150px card_cover_bc, 0 0 0 200px silver
&.hidden
transform: rotateY(180deg)
View Compiled
const { ref, reactive, computed } = Vue;
const cardOptions = [
"accessibility-outline",
"fish-outline",
"hourglass-outline",
"diamond-outline",
"skull-outline",
"shield-outline",
"dice-outline",
"bug-outline",
"boat-outline",
"baseball-outline",
"infinite-outline",
"star-outline"
];
const App = {
setup() {
// https://stackoverflow.com/questions/64416605/vuejs3-reactive-array-in-a-component
let cards = reactive({ value: [] });
let select = ref([]);
let score = ref(0);
let bonus = new Bouns(10);
let matchPair = ref(0);
const selectIndex = computed(() => select.value.map((s) => s.index));
const gameOver = computed(() => cardOptions.length === matchPair.value);
// 初始化
// 洗牌,所有暫存值歸 0
const gameInit = () => {
cards.value = shuffle();
score.value = 0;
matchPair.value = 0;
bonus.init();
select.value = [];
};
// 卡片點擊:
// 翻開卡片, 紀錄點擊的卡片
const cardClick = (card, index) => {
cards.value[index].isVisible = true;
select.value.push({ value: cards.value[index].icon, index });
checkSelect();
};
// 檢查分數:
// 如果待檢查的卡片超過兩張,就清空欄位
// 如果待檢查的卡片等於兩張,檢查是否相符,計算分數
const checkSelect = () => {
if (select.value.length > 2) {
select.value = [];
cards.value.forEach((c) => (c.isVisible = false));
} else if (select.value.length == 2) {
let isMatch = select.value.reduce((a, b) => a.value === b.value);
if (isMatch) {
// 卡片相符:累加分數與重置累計分
select.value.forEach((s) => (cards.value[s.index].isMatch = true));
select.value = [];
score.value += bonus.value;
matchPair.value++;
bonus.init();
} else {
// 沒有相符:失去累計分
bonus.losePoint();
}
}
};
gameInit();
return { cards, score, gameInit, cardClick, select, selectIndex, gameOver };
}
};
// 洗牌
// 建立隨機數字塞入 option, 用 counter 紀錄已經塞過的值
function shuffle() {
let counter = {};
let array = [];
while (array.length < cardOptions.length * 2) {
let randomNum = Math.floor(Math.random() * cardOptions.length);
let c = cardOptions[randomNum];
if (!counter[c] || counter[c] < 2) {
array.push({ icon: c, isVisible: false, isMatch: false });
counter[c] = counter?.[c] ? counter[c] + 1 : 1;
}
}
return array;
}
class Bouns {
constructor(initValue) {
this.initValue = initValue;
this.value = initValue;
}
init() {
this.value = this.initValue;
}
losePoint() {
if (this.value > 1) {
this.value--;
}
}
}
Vue.createApp(App).mount("#app");
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.