2024 刘谦春晚魔术 《守岁共此时》
视频
操作步骤
- 步骤 1:选牌 随机选取 4 张扑克牌。
- 步骤 2:洗牌 将牌随机打乱。
- 步骤 3:撕牌 将扑克牌撕成两份。
- 步骤 4:拼牌 将扑克牌并叠在一起。
- 步骤 5:按姓名字数洗牌 将牌堆顶数量为 【名字字数】 的牌移至牌堆底。
- 步骤 6:取三张放中间 将前三张牌放在牌堆中间。
- 步骤 7:藏牌 取出牌堆顶的牌,放置在一旁。
- 步骤 8:地域选择 取牌堆顶(南方人取 1 张,北方人取 2 张,不确定取三张)插入牌堆中间。
- 步骤 9:性别选择 男生扔掉牌堆顶 1 张,女生扔掉牌堆顶 2 张。
- 步骤 10:见证奇迹的时刻 执行“见证奇迹的时刻”循环,每说一个字,就取出牌堆顶一张牌放置在牌堆底。
- 步骤 11:好运留下来,烦恼扔出去 从牌堆顶开始,每次先将牌堆顶的一张牌放在牌堆底,再扔掉牌堆顶的一张牌,重复以上操作直到只剩一张牌。
- 完成:检查此牌和放置在一旁的牌是否吻合。 若吻合,则魔术成功
代码视频介绍
展示
随机选取 4张扑克牌
1 / 11 选牌
代码展示
主vue文件代码
vue
<template>
<div class="atTimeGuardYear">
<div class="buttons">
<el-popover :visible="restartShow" placement="top" :width="280" trigger="click">
<p>要重新开始魔术?</p>
<div style="text-align: right">
<el-button size="small" @click="restartShow = false">取消</el-button>
<el-button size="small" type="primary" @click="restart">确定</el-button>
</div>
<template #reference>
<el-button type="warning" @click="restartShow = true" :disabled="hasDoIt">
重新开始
</el-button>
</template>
</el-popover>
</div>
<div class="buttons">
<el-button type="primary" @click="handlePrev" :disabled="!ctx.hasPrev || hasDoIt">
上一步
</el-button>
<el-button type="primary" @click="handleNext" :disabled="!ctx.hasNext || hasDoIt">
下一步
</el-button>
</div>
<div class="operation">
<div style="text-align: center" v-html="operationDescriptor?.title"></div>
<div v-html="operationDescriptor?.getDesc"></div>
<template v-if="operationDescriptor?.type !== 'none'">
<template v-if="operationDescriptor?.type === 'radio'">
<el-radio-group v-model="operationValue" v-if="!operationDescriptor.isDoIt">
<el-radio-button v-for="item in operationDescriptor.payload.options" :label="item.value"
:key="item.label">{{ item.label }}
</el-radio-button>
</el-radio-group>
</template>
<template v-else-if="operationDescriptor?.type === 'number'">
<template v-if="!operationDescriptor.isDoIt">
<el-input-number :max="operationDescriptor?.payload.max" :min="operationDescriptor?.payload.min"
v-model="operationValue">
</el-input-number>
</template>
</template>
<el-button v-if="!operationDescriptor?.isDoIt" type="success" :disabled="!hasDoIt" @click="doIt">
Just Do It!
</el-button>
</template>
</div>
<h1 class="title">
{{ ctx.currentIndex + 1 }} / {{ ctx.stages.length }}
{{ ctx.currentStage.name }}
</h1>
<div class="desk">
<CardDesk v-bind="ctx.desk"></CardDesk>
</div>
</div>
</template>
<script setup lang="ts">
defineOptions({
name: "atTimeGuardYear",
});
import CardDesk from "./CardDesk.vue";
import { reactive, ref, computed } from "vue";
import { InteractiveDescriptor, StageContext } from "./stages";
// 创建执行环境
let ctx = reactive(new StageContext(500));
const isLoading = ref(false);
// 中间操作值
const operationValue = ref(0);
// 操作默认状态
const operationDescriptor = ref<InteractiveDescriptor>({
type: "none",
title: "",
payload: null,
getOperation: () => void 0,
isDoIt: false,
});
// 用于接收 Generator 的返回值
const opt = ref();
// 重新开始的弹窗
const restartShow = ref(false);
// 执型方法
const play = async () => {
restartShow.value = false;
if (isLoading.value) return;
operationDescriptor.value = ctx.getInteractiveDescriptor();
if (operationDescriptor.value.type === "none") {
// 无交互操作
isLoading.value = true;
opt.value = operationDescriptor.value.getOperation();
await ctx.play(opt.value);
isLoading.value = false;
} else {
// 有交互操作
if (operationDescriptor.value.isDoIt) {
// 已经交互过
await ctx.play();
} else {
// 未交互
isLoading.value = true;
opt.value = operationDescriptor.value.getOperation();
operationValue.value = operationDescriptor.value.payload.defaultValue;
// Generator 状态值
opt.value.next();
}
}
};
play();
// 执行交互操作
const doIt = async () => {
await ctx.play(opt.value.next(operationValue.value).value);
isLoading.value = false;
};
// 下一步
const handleNext = async () => {
ctx.next();
play();
};
// 上一步
const handlePrev = () => {
ctx.undo();
ctx.prev();
operationDescriptor.value = ctx.getInteractiveDescriptor();
opt.value = operationDescriptor.value.getOperation();
};
// 重新开始
const restart = () => {
ctx = reactive(new StageContext(500));
play();
};
// 是否可以交互
const hasDoIt = computed(() => {
return (
isLoading.value ||
(ctx.hasGetInteractiveDescriptor()
? false
: !operationDescriptor.value.isDoIt)
);
});
</script>
<style lang="scss" scoped>
.atTimeGuardYear {
.buttons {
display: flex;
justify-content: space-around;
margin-bottom: 16px;
}
.title {
text-align: center;
margin-top: 30px;
}
.desk {
width: 100%;
height: 200px;
margin: 0 auto;
/* background: lightblue; */
margin-top: 50px;
}
.operation {
height: 96px;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
&>* {
margin-top: 10px;
}
}
}
</style>
Card.vue card组件代码
vue
<template>
<div class="card" :class="card.tear" :key="card.cardId">
<img :src="card.imagePath" :alt="card.value" :class="`card-img ${card.tear}`" />
</div>
</template>
<script setup lang="ts">
import { Card } from './card';
const card = defineProps<Card>();
</script>
<style scoped>
.card {
position: absolute;
/* outline: 1px solid #ccc; */
border-radius: 10px;
overflow: hidden;
transition: transform 0.5s, opacity 0.5s, left 0.5s, top 0.5s;
box-shadow: 0.1rem 0.1rem 0.3rem var(--jk-box-shadow);
}
.card.keep-top {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
.card.keep-bottom {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.card-img {
width: 100%;
position: absolute;
left: 0;
top: 0;
}
.card-img.keep-bottom {
top: auto;
bottom: 0;
}
</style>
CardDesk.vue card 魔术桌面代码
vue
<template>
<div class="container" ref="containerEl">
<div class="inner" v-if="isRendered">
<TransitionGroup appear name="fade">
<Card v-for="renderCard in renderCards" :key="renderCard.card.cardId" v-bind="renderCard.card" :style="{
...renderCard.styles,
}"></Card>
</TransitionGroup>
</div>
</div>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue';
import { Desk } from './desk';
import { useResizeObserver } from '@vueuse/core';
import Card from './Card.vue';
import { useRenderCards } from './useRenderCards';
const desk = defineProps<Desk>();
const containerEl = ref<HTMLElement | null>(null);
const containerSize = reactive({ width: 0, height: 0 });
const isRendered = ref(false);
useResizeObserver(containerEl, (entries) => {
const entry = entries[0];
const { width, height } = entry.contentRect;
containerSize.width = width;
containerSize.height = height;
isRendered.value = true;
});
const renderCards = useRenderCards(desk, isRendered, containerSize);
</script>
<style scoped>
.container {
width: 100%;
height: 100%;
}
.inner {
width: 100%;
height: 100%;
position: relative;
}
.fade-enter-from {
transform: translateY(-50px);
}
.fade-leave-to {
transform: translateY(-50px);
}
</style>
stages.ts 步骤类代码
ts
import { createCard, createRandomCards } from './card';
import { Desk, initDesk } from './desk';
import { cloneDeep, random } from 'lodash-es';
// 模板字符串tag方法
function valueTag(strings: TemplateStringsArray, ...keys: any[]): string {
let result = [strings[0]];
keys.forEach((key, i) => {
result.push(key, strings[i + 1]);
});
return result.join('');
}
// 延时函数
function delay(duration = 1000) {
return new Promise((resolve) => {
setTimeout(resolve, duration);
});
}
// 交互描述符
export interface InteractiveDescriptor {
type: 'radio' | 'number' | 'none';
title: string;
payload: any;
isDoIt: boolean,
getOperation(interactive?: any): any;
getDesc?: string;
value?: number,
desc?: string;
}
// 步骤顺序
export class StageContext {
public stages: Stage[];
public currentIndex = 0;
public desk: Desk = initDesk();
constructor(public animationDuration: number) {
this.stages = [
new InitialStage(this),
new ShuffleStage(this),
new TearStage(this),
new ConcatStage(this),
new NameStage(this),
new TakeThreeCardsToMiddle(this),
new ConcealStage(this),
new LocationChooseStage(this),
new SexChooseStage(this),
new SevenWordsSpellStage(this),
new KeepOneStage(this),
];
}
get hasPrev() {
return this.currentIndex > 0;
}
get hasNext() {
return this.currentIndex < this.stages.length - 1;
}
get currentStage() {
return this.stages[this.currentIndex];
}
// 获取当前交互描述内容
getInteractiveDescriptor() {
return this.currentStage.getInteractiveDescriptor();
}
// 执型动作
async play(options?: any) {
await this.currentStage.play(options);
}
undo() {
this.currentStage.undo();
}
// 获取当前是否已经交互过
hasGetInteractiveDescriptor() {
return this.currentStage.isInteractiveDescriptor;
}
next() {
if (this.hasNext) {
this.currentIndex++;
}
}
prev() {
if (this.hasPrev) {
this.currentIndex--;
}
}
}
export abstract class Stage {
// 是否已经交互过判定
private hasGetInteractiveDescriptor = false;
// 初始状态
private initialDesk: Desk | null = null;
// 结束状态
private playedDesk: Desk | null = null;
// 交互描述默认内容
protected defaultDescriptor: InteractiveDescriptor = {
type: 'none',
title: '',
payload: null,
getOperation: () => void 0,
getDesc: '',
isDoIt: false
};
constructor(public ctx: StageContext, public name: string) { }
// 获取交互描述内容 带缓存
getInteractiveDescriptor(): InteractiveDescriptor {
if (this.hasGetInteractiveDescriptor) {
return this.defaultDescriptor;
}
const desc = this._getInteractiveDescriptor();
this.hasGetInteractiveDescriptor = true;
return desc;
}
// 用于子级重写方法
_getInteractiveDescriptor(): InteractiveDescriptor {
return this.defaultDescriptor;
}
// 获取是否已经交互过
get isInteractiveDescriptor() {
return this.hasGetInteractiveDescriptor;
}
// 定义抽象方法 泳衣继承重写
abstract _play(options: any): Promise<void> | void;
async play(options: any): Promise<void> {
if (!this.initialDesk) {
this.initialDesk = cloneDeep(this.ctx.desk);
}
if (this.playedDesk) {
this.ctx.desk = cloneDeep(this.playedDesk);
return;
}
await this._play(options);
this.playedDesk = cloneDeep(this.ctx.desk);
return;
}
undo(): void {
if (this.initialDesk) {
this.ctx.desk = cloneDeep(this.initialDesk);
}
}
}
// 1/11 选牌
class InitialStage extends Stage {
_play() {
this.ctx.desk.line1Cards = createRandomCards(4);
}
_getInteractiveDescriptor(): InteractiveDescriptor {
this.defaultDescriptor = {
...this.defaultDescriptor,
title: "随机选取 <strong>4张扑克牌</strong> "
}
return this.defaultDescriptor
}
constructor(ctx: StageContext) {
super(ctx, '选牌');
}
}
// 2/11 选牌
class ShuffleStage extends Stage {
_play() {
for (let i = this.ctx.desk.line1Cards.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * i);
[this.ctx.desk.line1Cards[i], this.ctx.desk.line1Cards[j]] = [
this.ctx.desk.line1Cards[j],
this.ctx.desk.line1Cards[i],
];
}
}
_getInteractiveDescriptor(): InteractiveDescriptor {
this.defaultDescriptor = {
...this.defaultDescriptor,
title: "将扑克牌 <strong>随机打乱</strong>"
}
return this.defaultDescriptor
}
constructor(ctx: StageContext) {
super(ctx, '洗牌');
}
}
// 3/11 撕牌
class TearStage extends Stage {
_play() {
for (const card of this.ctx.desk.line1Cards) {
card.tear = 'keep-top';
this.ctx.desk.line2Cards.push(createCard(card.value, 'keep-bottom'));
}
}
_getInteractiveDescriptor(): InteractiveDescriptor {
this.defaultDescriptor = {
...this.defaultDescriptor,
title: "将扑克牌 <strong>撕成两份</strong>"
}
return this.defaultDescriptor
}
constructor(ctx: StageContext) {
super(ctx, '撕牌');
}
public undo() {
this.ctx.desk.line2Cards = [];
setTimeout(() => {
super.undo();
}, this.ctx.animationDuration);
}
}
// 4/11 拼牌
class ConcatStage extends Stage {
_play() {
this.ctx.desk.line1Cards = [
...this.ctx.desk.line1Cards,
...this.ctx.desk.line2Cards,
];
this.ctx.desk.line2Cards = [];
}
_getInteractiveDescriptor(): InteractiveDescriptor {
this.defaultDescriptor = {
...this.defaultDescriptor,
title: "将扑克牌 <strong>并叠在一起</strong>"
}
return this.defaultDescriptor
}
constructor(ctx: StageContext) {
super(ctx, '拼牌');
}
}
// 将牌放到牌堆底部
class TakeCardsToTail extends Stage {
async _play(takeNumber: number) {
for (let i = 0; i < takeNumber; i++) {
this.beforeCardChange();
const [head, ...rest] = this.ctx.desk.line1Cards;
this.ctx.desk.line1Cards = [...rest, head];
await delay(this.ctx.animationDuration);
}
this.afterChange();
}
beforeCardChange() { }
afterChange() { }
}
// 5/11 按姓名字数洗牌
class NameStage extends TakeCardsToTail {
_getInteractiveDescriptor(): InteractiveDescriptor {
this.defaultDescriptor = {
title: '你的姓名有几个字?',
type: 'number',
payload: {
defaultValue: 3,
min: 1,
max: 10,
},
isDoIt: false,
getOperation: function* () {
let value: number
value = yield
this.value = value
this.isDoIt = true
this.getDesc = valueTag`填写的,名字 <strong>长度为${this.value}</strong> ,取 <em>${this.value}张牌</em> 放置到牌堆底部`
return value
},
}
return this.defaultDescriptor;
}
constructor(ctx: StageContext) {
super(ctx, '按姓名字数洗牌');
}
}
// 将牌放到牌堆中间
abstract class InsertStage extends Stage {
_play(takeNumber: number) {
const cards = this.ctx.desk.line1Cards;
if (takeNumber > cards.length - 2 || takeNumber <= 0) {
return;
}
const insertFromTopNumber = random(1, cards.length - takeNumber - 1);
const lastChangeIndex = takeNumber + insertFromTopNumber - 1;
for (let i = takeNumber; i <= lastChangeIndex; i++) {
for (let j = i, k = 0; k < takeNumber; k++, j--) {
// 交换j和j-1
[cards[j], cards[j - 1]] = [cards[j - 1], cards[j]];
}
}
}
}
// 6/11 取三张放中间
class TakeThreeCardsToMiddle extends InsertStage {
constructor(ctx: StageContext) {
super(ctx, '取三张放中间');
}
_getInteractiveDescriptor(): InteractiveDescriptor {
this.defaultDescriptor = {
...this.defaultDescriptor,
title: "将 <strong>前三张牌</strong> 放在 <strong>牌堆中间</strong> ",
getOperation: () => 3,
};
return this.defaultDescriptor;
}
}
// 7/11 藏牌
class ConcealStage extends Stage {
_play() {
this.ctx.desk.concealCard = this.ctx.desk.line1Cards.shift()!;
}
_getInteractiveDescriptor(): InteractiveDescriptor {
this.defaultDescriptor = {
...this.defaultDescriptor,
title: "取出 <strong>牌堆顶的牌</strong> ,<strong>放置一旁</strong>",
};
return this.defaultDescriptor;
}
constructor(ctx: StageContext) {
super(ctx, '藏牌');
}
}
// 8/11 地域选择
class LocationChooseStage extends InsertStage {
_getInteractiveDescriptor(): InteractiveDescriptor {
this.defaultDescriptor = {
title: '请选择你的地域',
type: 'radio',
payload: {
defaultValue: 1,
options: [
{ label: '南方人', value: 1 },
{ label: '北方人', value: 2 },
{ label: '不清楚', value: 3 },
],
},
isDoIt: false,
getOperation: function* () {
let value: number
this.value = value = yield
this.isDoIt = true
this.getDesc = valueTag`<strong>${this.payload.options.find((item: any) => this.value === item.value).label}</strong> ,取 <em>${this.value}</em> 张牌插入 <em>牌堆中间</em> `
return value
}
};
return this.defaultDescriptor;
}
constructor(ctx: StageContext) {
super(ctx, '地域选择');
}
}
// 丢牌
class ThrowHeadCardsStage extends Stage {
async _play(takeNumber: number) {
for (let i = 0; i < takeNumber; i++) {
this.ctx.desk.line1Cards.shift();
await delay(this.ctx.animationDuration);
}
}
}
// 9/11 性别选择
class SexChooseStage extends ThrowHeadCardsStage {
_getInteractiveDescriptor(): InteractiveDescriptor {
this.defaultDescriptor = {
title: '请选择你的性别',
type: 'radio',
payload: {
defaultValue: 1,
options: [
{ label: '男', value: 1 },
{ label: '女', value: 2 },
],
},
isDoIt: false,
getOperation: function* () {
let value: number
this.value = value = yield
this.isDoIt = true
this.getDesc = valueTag` <strong>${this.payload.options.find((item: any) => this.value === item.value).label}生</strong> ,<em>丢弃牌堆顶 ${this.value}</em> 张牌`
return value
}
};
return this.defaultDescriptor;
}
constructor(ctx: StageContext) {
super(ctx, '性别选择');
}
}
// 10/11 见证奇迹的时刻
class SevenWordsSpellStage extends TakeCardsToTail {
private words: string;
private currentIndex = 0;
_getInteractiveDescriptor(): InteractiveDescriptor {
this.defaultDescriptor = {
...this.defaultDescriptor,
title: "念出 <strong>“见证奇迹的时刻”</strong> 7个字,<br/> 每念一个字,就 <em>取牌堆顶一张牌放置在牌堆底</em>",
getOperation: () => 7,
};
return this.defaultDescriptor;
}
beforeCardChange(): void {
this.name = this.words[this.currentIndex++];
}
afterChange(): void {
this.currentIndex = 0;
this.name = this.words;
}
constructor(ctx: StageContext) {
super(ctx, '见证奇迹的时刻');
this.words = this.name;
}
}
// 11/11 好运留下来,烦恼丢出去
class KeepOneStage extends Stage {
private words: string[];
async _play() {
const stages = [this.takeStage, this.throwStage];
let i = 0;
let oldDuration = this.ctx.animationDuration;
this.ctx.animationDuration *= 2;
let wordsIndex = 0;
while (this.ctx.desk.line1Cards.length > 1) {
const stage = stages[i++ % stages.length];
this.name = this.words[wordsIndex++ % this.words.length];
await stage._play(1);
}
this.ctx.desk.meet = [
this.ctx.desk.line1Cards[0],
this.ctx.desk.concealCard!,
];
this.ctx.desk.concealCard = null;
this.ctx.desk.line1Cards = [];
this.ctx.animationDuration = oldDuration;
this.name = this.words.join(',');
}
_getInteractiveDescriptor(): InteractiveDescriptor {
this.defaultDescriptor = {
...this.defaultDescriptor,
title: "重复<strong>“好运留下来,烦恼扔出去”</strong>,<br/>从牌堆顶开始,<br/>“好运留下来”将<em>堆顶的牌放在堆底</em>,<br/>“烦恼扔出去”<em>扔掉堆顶的一张牌</em>,<br/><strong>重复以上操作</strong>直到<em>只剩一张牌</em>",
}
return this.defaultDescriptor;
}
private throwStage = new ThrowHeadCardsStage(this.ctx, '');
private takeStage = new TakeCardsToTail(this.ctx, '');
constructor(ctx: StageContext) {
super(ctx, '好运留下来,烦恼丢出去');
this.words = this.name.split(",")
}
}
useRenderCards Hooks代码
ts
import { Ref, reactive, watchEffect } from 'vue';
import { Card } from './card';
import { Desk } from './desk';
export interface RenderCard {
card: Card;
styles: object;
}
const cardSize = {
width: 80,
height: 122,
};
function findRenderCard(renderCards: RenderCard[], card: Card) {
return renderCards.find((item) => item.card.cardId === card.cardId);
}
function findCard(cards: Card[], renderCard: RenderCard) {
return cards.find((item) => item.cardId === renderCard.card.cardId);
}
export function useRenderCards(
desk: Desk,
isRender: Ref<boolean>,
containerSize: { width: number; height: number }
): RenderCard[] {
const renderCards = reactive<RenderCard[]>([]);
function updateRenderCards(lineCards: Card[], lineIndex: 0 | 1) {
const len = lineCards.length;
for (let i = 0; i < len; i++) {
const card = lineCards[i];
// 确定横坐标
const space = containerSize.width - cardSize.width;
let step = space / (len - 1);
if (!isFinite(step)) {
step = 0;
}
const left = i * step + 'px';
// 确定纵坐标
let top = '0px';
if (lineIndex === 1) {
top = cardSize.height / 2 + 50 + 'px';
}
// 确定宽高
const width = cardSize.width + 'px';
let h = cardSize.height;
if (card.tear !== 'none') {
h /= 2;
}
const height = h + 'px';
// 确定 z-index
const zIndex = i + 1;
const styles = {
left,
top,
width,
height,
zIndex,
};
// 找到对应的渲染卡片
const renderCard = findRenderCard(renderCards, card);
if (renderCard) {
renderCard.styles = styles;
renderCard.card.tear = card.tear;
} else {
renderCards.push({
card,
styles,
});
}
}
}
watchEffect(() => {
if (!isRender.value) {
return;
}
updateRenderCards(desk.line1Cards, 0);
updateRenderCards(desk.line2Cards, 1);
// 处理藏牌
if (desk.concealCard) {
const styles = {
left: '0px',
bottom: '0px',
width: cardSize.width + 'px',
height: cardSize.height + 'px',
zIndex: 999,
};
if (desk.concealCard.tear !== 'none') {
styles.height = cardSize.height / 2 + 'px';
}
const renderCard = findRenderCard(renderCards, desk.concealCard);
if (renderCard) {
renderCard.styles = styles;
} else {
renderCards.push({
card: desk.concealCard,
styles,
});
}
}
// 处理删除的卡片
for (let i = renderCards.length - 1; i >= 0; i--) {
const renderCard = renderCards[i];
const card =
findCard(desk.line1Cards, renderCard) ||
findCard(desk.line2Cards, renderCard) ||
findCard(desk.meet || [], renderCard);
if (!card && desk.concealCard?.cardId !== renderCard.card.cardId) {
renderCards.splice(i, 1);
}
}
// 处理遇牌
if (desk.meet) {
const x = containerSize.width / 2 - cardSize.width / 2;
const y1 = containerSize.height / 2 - cardSize.height;
const y2 = containerSize.height / 2 - cardSize.height / 2;
const styles1 = {
left: x + 'px',
top: y1 + 'px',
width: cardSize.width + 'px',
height: cardSize.height / 2 + 'px',
zIndex: 999,
};
const styles2 = {
...styles1,
top: y2 + 'px',
};
renderCards.find((c) => c.card.tear === 'keep-top')!.styles = styles1;
renderCards.find((c) => c.card.tear === 'keep-bottom')!.styles = styles2;
}
});
return renderCards;
}
card.ts card类 代码
ts
const cardValues = [
// 红心
'♥A',
'♥2',
'♥3',
'♥4',
'♥5',
'♥6',
'♥7',
'♥8',
'♥9',
'♥10',
'♥J',
'♥Q',
'♥K',
// 黑桃
'♠A',
'♠2',
'♠3',
'♠4',
'♠5',
'♠6',
'♠7',
'♠8',
'♠9',
'♠10',
'♠J',
'♠Q',
'♠K',
// 方块
'♦A',
'♦2',
'♦3',
'♦4',
'♦5',
'♦6',
'♦7',
'♦8',
'♦9',
'♦10',
'♦J',
'♦Q',
'♦K',
// 梅花
'♣A',
'♣2',
'♣3',
'♣4',
'♣5',
'♣6',
'♣7',
'♣8',
'♣9',
'♣10',
'♣J',
'♣Q',
'♣K',
// 王牌
'Black Joker',
'Red Joker',
] as const;
type CardValues = (typeof cardValues)[number];
export type TearType = 'none' | 'keep-top' | 'keep-bottom';
export interface Card {
readonly value: CardValues;
readonly imagePath: string;
readonly cardId: number;
tear: TearType;
}
let uniqueCardId = 1;
const colorMap = {
'♦': '4',
'♥': '2',
'♠': '1',
'♣': '3',
};
const valueMap = {
A: '1',
'2': '2',
'3': '3',
'4': '4',
'5': '5',
'6': '6',
'7': '7',
'8': '8',
'9': '9',
'10': '0',
J: 'a',
Q: 'b',
K: 'c',
'Red Joker': 'a',
'Black Joker': 'b',
};
export function createCard(
cardValue: CardValues,
tear: TearType = 'none'
): Card {
const cardId = uniqueCardId++;
const isJoker = cardValue === 'Red Joker' || cardValue === 'Black Joker';
const value = isJoker
? valueMap[cardValue]
: valueMap[cardValue.slice(1) as keyof typeof valueMap];
const color = isJoker ? '' : colorMap[cardValue[0] as keyof typeof colorMap];
const name = `${color}${value}`;
// const url = new URL(`/assets/demo/atTimeGuardYear/${name}.jpg`, import.meta.url);
const imagePath = `/assets/demo/atTimeGuardYear/${name}.jpg`;
return {
value: cardValue,
imagePath,
cardId,
tear,
};
}
// 随机抽牌
export function createRandomCards(count: number): Card[] {
if (count > cardValues.length) {
throw new Error(`count should be less than ${cardValues.length}`);
}
if (count < 0) {
throw new Error(`count should be greater than 0`);
}
const indexes = new Set<number>();
while (indexes.size < count) {
indexes.add(Math.floor(Math.random() * cardValues.length));
}
return Array.from(indexes).map((index) => createCard(cardValues[index]));
}
desk.ts 魔术桌面代码
ts
import { Card } from './card';
export interface Desk {
line1Cards: Card[];
line2Cards: Card[];
concealCard: Card | null;
meet: [Card, Card] | null;
}
export function initDesk(): Desk {
return {
line1Cards: [],
line2Cards: [],
concealCard: null,
meet: null,
};
}