用Delphi模拟实现纸牌魔术
技术与开发
在绝大多数人心目中,魔术是神秘的、奇妙的,非常多的人对魔术充满了好奇和向往,但由于学习、工作的关系,没有时间和精力去研究魔术。CCTV10的探索发现栏目专门就纸牌魔术做了一期节目,揭秘了两个纸牌魔术:心电感应和空中取牌,但是CCTV揭秘的魔术需要相当的手法和技巧,不适合初学者。本期以一个不需要手法、即学即会的扑克牌魔术为例,带领大家深入到纸牌魔术世界,大家通过本期的学习一定能对纸牌魔术有一定了解并能为亲朋好友表演一些玄幻的纸牌魔术!
首先我们了解扑克牌的起源和含义。扑克牌的诞生至今已有数百年历史,但哪个国家最早发明了扑克牌迄今没有一个定论。扑克牌的造型、规格、张数由早期各国不一(如意大利为22张,德国为32张,西班牙为40张,法国为52张)发展到的54张扑克牌是由1392年法国创始出现的52张扑克牌的模式,外加大、小王演变而来。大王代表太阳、小王代表月亮,其余52张牌代表一年中的52个星期;红桃、方块、梅花、黑桃四种花色分别象征着春、夏、秋、冬四个季节;每种花色有13张牌,表示每个季节有13个星期。如果把J、Q、K当作11、12、13点,大王、小王为半点,一副扑克牌的总点数恰好是365点。而闰年把大、小王各算为1点,共366点。
本期为大家讲解一个精彩的纸牌魔术,并用Delphi来模拟表演,希望大家看了本期的文章后不但能明白魔术的原理,能自己动手给朋友表演,还能对Delphi如何实现有深刻的认识。
魔术表演:
第一步:魔术师拿出一副扑克牌(不要大小王),自己洗牌(也可以让观众洗牌)后,魔术师握住这叠牌(牌背面向下,正面向上),一张一张依次从上面取25张牌,把取出的25张牌按取出的顺序叠好后(背面向上)备用。

第二步:魔术师此时手上剩下27张牌,让观众从剩下的27张牌中随机抽出3张牌(A牌、B牌、C牌),牌正面向上分别横放在桌上。

第三步:魔术师此时手上还有24张牌。开始合牌,合牌规则是:

从手上剩下的牌叠中扔掉一些牌:每次扔掉的牌的张数为13减去牌面点数,(其中:J表示11点,Q表示12点,K表示13点)比如:
A牌是9,魔术师就从手上剩下的牌中依次扔掉4张;(梅花A、梅花7、梅花Q、红桃J)
B牌为9,魔术师再从手上剩下的牌中依次扔掉4张;
C牌为K,魔术师再从手上剩下的牌中依次扔掉0张;
如果扔掉的牌超过24张(魔术师手上剩余的牌的总和此时为24张),那么就从上面的25张叠牌中补足,比如应该扔掉30张,那么魔术师手上的牌全部扔掉且从25张叠牌中扔掉6张。
魔术师把手上剩下的牌叠加到备用的25张牌上,并把牌叠握在手中(牌背面向上,正面向下)。
第四步:魔术师叫观众将ABC这3张牌的点数相加,(比如3张牌为9、9、K,则9十9十13=31),魔术师从备用的牌叠上依次扔掉31张牌,此时魔术师对观众说我知道我手上牌叠的第一张是什么牌(比如黑桃7),然后魔术师叫观众翻开牌叠的第一张牌,果然就是魔术师说的那一张。那么魔术师是如何知道的呢?

设计步骤
这种魔术是典型的数字游戏,魔术师甚至可以从头到尾不碰一下牌,也能准确的猜牌,不需要任何手法和道具,关键的是记住第一步25张叠牌中第15张牌的牌点数即可。
魔术原理:25+(24-(13-a)-(13-b)-(13-c))-(a+b+c)=10,由于牌面向下所以在牌叠中,从下向上数第10张牌或者从上向下数第15张牌就是魔术师要猜测的牌,也就是不论abc的值如何变化,不论你选择哪三张牌,最后的结果是固定的!
我们要在delphi环境下“画”出扑克牌,就需要安装扑克牌的控件。在网上下载一个扑克牌控件之后需要在delphi环境下安装,安装成功后,就可以在delphi环境下添加扑克牌控件了。
第一步:新建一个项目,在主画面上添加四个按钮、[魔术开始]、[取25张牌]、[合牌]、[谜底]。
第二步:点击[魔术开始]后,将随机打乱52张牌并显示出来,这里的难点是如何打乱52张牌,网上介绍了很多算法,实现的手段也五花八门,有使用指针、结构体、数组等等,在本例中采用产生一个不重复数字的数组,并打乱数组元素的顺序的方法来实现。

puke: array[1..52] of integer;//全局变量
wzCard:array[1..52] of TCard; //全局变量
procedure TForm1.Button1Click(Sender: TObject);
var
i,tmp,pos1,pos2:integer;
begin
Randomize;
for i:=1 to 52 do//给puke数组赋初值,1-13表示这张牌为黑桃A-黑桃K, 14-26表示这张牌为红桃A-红桃K, 27-39表示这张牌为方块A-方块K, 40-52表示这张牌为梅花A-梅花K
begin
puke[i]:=i;
end;
//打乱数组
for i:=1 to 50 do//打乱的次数设定为50,I的值越大,打乱的效果越明显
begin
pos1:= random(52)+1;
pos2:= random(52)+1;
tmp:=puke[pos1];
puke[pos1]:=puke[pos2];
puke[pos2]:= tmp;
end;
for i:=1 to 52 do//发牌
begin
wzCard[i]:=TCard.Create(self);
wzCard[i].Parent:=Form1;
wzCard[i].Top:=200;//牌的坐标定位取决于top和left的值
wzCard[i].Left:=2+14*i;
if (puke[i]>0) and (puke[i]<=13) then
//读取数组的值,根据这个值来判定是哪张牌,也就是根据数组元素的值确定牌的花色和点数
begin
wzCard[i].Value :=puke[i];//牌的点数
wzCard[i].Suit :=Spades;//牌的花色为黑桃
end;
// if (puke[i]>13) and (puke[i]<=26) then
begin
wzCard[i].Value :=puke[i]-13; //牌的点数
wzCard[i].Suit :=hearts; //牌的花色为红桃
end;
………………
end;
end;
第三步:点击[取25张牌]按钮后,如下图,把最后的25张牌依次放到画面的上方并使牌的背面向上,在程序中,我们用数组puke2来存储这25张牌。
puke2: array[1..52] of TCard;//全局变量,存储这25张牌
procedure TForm1.Button2Click(Sender: TObject);
var
i:integer;
begin
for i:=1 to 52 do
begin
if i < 26 then
begin
puke2[i]:=TCard.Create(self);
puke2[i].Parent:=Form1;
puke2[i].Value :=wzCard[53-i].Value ;//依次定义这25张牌的点数
puke2[i].Suit :=wzCard[53-i].Suit ; //依次定义这25张牌的花色
puke2[i].Top:=100; //依次定义这25张牌的坐标
puke2[i].Left:=2+14*i;
puke2[i].ShowDeck :=true; //依次定义这25张牌的背面向上
end
else
begin
if i > 27 then//下面52张牌拿走25张后,为制造拿走的效果,必须把拿走的25张牌隐藏起来
wzCard[i].Visible :=false;
end;
end;
for i:=1 to 27 do
wzCard[i].OnClick:=Card1Click;//剩下的27张牌允许用户点击,执行点击事件
end;
第四步:用户在27张牌中选择三张牌:(扑克牌的点击事件)
procedure TForm1.Card1Click(Sender: TObject);
var
i,j:integer;
begin
if count2 <= 3 then
begin
card := TCard.Create(self);
card.Parent := Form1;
card.Value := (Sender as TCard).Value; //点击的三张牌显示在画面的下方
card.Suit := (Sender as TCard).Suit;
card.Top :=300 ;
card.Left := 100*count2-85;
count := count + 13 - card.Value;//count为扔掉的牌的张数之和
count3 := count3 + card.Value;//3张牌的点数之和
i := ((Sender as TCard).Left - 2) div 14;//I 的值表示点击的那张牌在数组中的位置,每点击取走1张牌,剩下的牌需要重新排列,后面的依次向前移动,这个动画不是必须的,但是处理起来有相当的难度,特别处理数组的“边界”。
labelCaption := inttostr(count3);//显示3张牌的点数之和
for j := i to (27 - count2) do//点击的那张牌拿走后,后面的牌的位置依次前移
begin
wzCard[j].Value := wzCard[j+1].Value;
wzCard[j].Suit := wzCard[j+1].Suit;
end;
wzCard[28-count2].Visible := false;
for j:= 1 to (27 - count2) do
wzCard[j].Left := 2+14*j;
count2 := count2 + 1;
end;
end;
第五步:点击合牌按钮后,下面的牌(正面向上)全部隐藏,同时,魔术师把手上剩余的牌按合牌规则扔掉部分牌后把余牌堆放到上面的25张牌中。
for i:= 1 to 24 - count do
begin
puke2[25+i] := TCard.Create(self);
puke2[25+i].Parent := Form1;
puke2[25+i].Value := wzCard[i].Value;
puke2[25+i].Suit := wzCard[i].Suit;
puke2[25+i].Top := 100;
puke2[25+i].Left := puke2[25].Left + 14*i;
puke2[25+i].ShowDeck:=true;
end;
第六步:谜底按钮是计算三张牌的点数之和“count3”,再扔掉“count3”张牌,这时定位下一张牌,并显示出来:
procedure TForm1.Button4Click(Sender: TObject);
var
i,j:integer;
begin
if count >= 24 then// count为扔掉的牌的张数之和,如果选中三张牌为AAA,那么魔术师手里的牌就不够数了,这里需要做一个判断
j := 25 - (count - 24)
else
j := 25 + 24 - count;
for i:=1 to count3 do
puke2[j-i+1].Visible :=false;
puke2[j-count3].ShowDeck :=false ;//这张牌就是魔术师猜测的牌
end;
第七步:为了验证魔术师的正确性,我们再在画面上增加一张扑克牌,这张扑克牌直接读取25张牌的第10张的属性,最后和puke2[j-count3].进行对比,如果显示一致,表示魔术成功,程序逻辑也成功,在[取25张牌]的按钮中增加代码:
Card1.Value := puke2[10].Value ;
Card1.Suit:= puke2[10].Suit ;
总结
程序的难点是数组的“边界”处理,抽取3张牌后,牌要依次前移,数组重新赋值的过程中边界处理不好就容易出现数组超界的情况。
灵活使用数组,不但适用于魔术牌的模拟表演,同样适用于其他地方,比如对学生成绩的统计、职工工资的数据分析等等,使用数组将大大方便程序的开发。