【C#】WPF实现经典纸牌游戏,适合新手入门

news2024/9/20 14:03:52

文章目录

    • 1 纸牌类
    • 2 布局
    • 3 初始化
    • 4 事件
      • 点击牌堆
      • 拖动
      • 牌的去留
    • 源代码

1 纸牌类

之所以产生这个无聊至极的念头,是因为发现Unicode中竟然有这种字符。。。

黑桃🂡 🂢 🂣 🂤 🂥 🂦 🂧 🂨 🂩 🂪 🂫 🂬 🂭 🂮
红心🂱 🂲 🂳 🂴 🂵 🂶 🂷 🂸 🂹 🂺 🂻 🂼 🂽 🂾
钻石🃁 🃂 🃃 🃄 🃅 🃆 🃇 🃈 🃉 🃊 🃋 🃌 🃍 🃎
草花🃑 🃒 🃓 🃔 🃕 🃖 🃗 🃘 🃙 🃚 🃛 🃜 🃝 🃞

这就意味着不用任何资源就可以实现一些纸牌游戏,效果如下图所示

在这里插入图片描述

实现这个游戏的第一步就是新建一个纸牌类,一张扑克牌至少包含三个信息,分别是花色、颜色以及牌序,以及三合一的图案。由于我们要做一个经典纸牌,所以最好在纸牌类中包含一些游戏时需要用到的信息。

#region 常量
private static readonly Dictionary<string, string[]> CardNames = new Dictionary<string, string[]>{
{"Spade", new string[13]{"🂡","🂢","🂣","🂤","🂥","🂦","🂧","🂨","🂩","🂪","🂫","🂭","🂮" } },
{"Heart", new string[13]{"🂱", "🂲", "🂳", "🂴", "🂵", "🂶", "🂷", "🂸", "🂹", "🂺", "🂻", "🂽", "🂾" } },
{"Diamond", new string[13]{"🃁", "🃂", "🃃", "🃄", "🃅", "🃆", "🃇", "🃈", "🃉", "🃊", "🃋", "🃍", "🃎" } },
{"Club" , new string[13]{"🃑", "🃒", "🃓", "🃔", "🃕", "🃖", "🃗", "🃘", "🃙", "🃚", "🃛", "🃝", "🃞"}}
};
#endregion

#region 卡牌类型
private class Card
{
	public Card(string name, int number, string type, bool red, int index)
	{
		Name =name;
		Number = number;
		Type = type;
		Red = red;
		Index = index;
	}

	public int Index;
	public string Name;
	public int Number;
	public bool Red;
	public string Type;
	public int Region;
}
#endregion

在实现了纸牌类之后,将每个纸牌放到一个ButtonTag中,然后再 为Button添加各种事件,就能实现这个游戏了。

2 布局

由于是动态布局,所以建议使用Canvasxaml界面十分简洁,除了一个刷新按钮,剩下的就只有画布了。

    <StackPanel>
        <ToolBar DockPanel.Dock="Top" Margin="0 0 0 20">
            <Button Content="🔄" Click="btnUpdate_Click"/>
        </ToolBar>
        <Canvas x:Name="cvMain" Height="400"/>

    </StackPanel>

经典纸牌游戏大致可以分为12个区域,如图所示

在这里插入图片描述
这些个区域就可决定纸牌的位置,所以需要一个用来存放区间信息的变量

private List<int>[] cardIndex;

cardIndex是由12个List<int>组成的数组,然后每个Button的位置用下面的方式来设定

private void setBtnPosition(Button btn, int region)
{
	Canvas.SetLeft(btn, region % 6 * dw);
	Canvas.SetTop(btn, region / 6 * dh);
	Canvas.SetZIndex(btn, cardIndex[region].Count);
}

其中,SetLeft即控件据Canvas左端的距离,可以理解为x坐标;dwdh为全局变量,用来存放每个区间的尺寸。SetTop对应的为y坐标。SetZIndex表示层级关系,值越大则越在上面。

3 初始化

初始化需要一个随机数组,目的是将牌打散。这里用了一个非常Low的方案,即生成随机数,然后交换自然序列中两个随机数所在位置的值。

private int[] RandomArray(int length)
{
    int[] arr = new int[length];
    for (int i = 0; i < length; i++)
        arr[i] = i;
    int times = rand.Next(10, 100);
    for (int i = 0; i < times; i++)
    {
        int a = rand.Next(0, length - 1);
        int b = rand.Next(0, length - 1);
        var temp = arr[a];
        arr[a] = arr[b];
        arr[b] = temp;
    }
    return arr;
}

接下来就是初始化代码,这里按照平时发牌的顺序,先生成这个区域的纸牌

在这里插入图片描述
然后再生成牌堆。

public void InitCards()
{
    cvMain.Children.Clear();
    cards = new List<Card>();
    cardIndex = new List<int>[12];          //所有的扑克被划分为12个区域
    for (int i = 0; i < 12; i++)
        cardIndex[i] = new List<int>();

    int index = 0;
    foreach (var key in CardNames.Keys)
        for (int i = 0; i < 13; i++)
            cards.Add(new Card(CardNames[key][i], i, key,
                key == "Heart" || key == "Diamond", index++));
    
    var orders = RandomArray(52);

    index = 0;
    for (int i = 0; i < 6; i++)
        for (int j = i; j < 6; j++)
        {
            var card = cards[orders[index++]];
            cardIndex[6 + j].Add(cvMain.Children.Count);
            var btn = setOneButton(card);
            if (i == j)
            {
                coverCard(btn, false);      //当i==j时翻面
                SetOneColumn(i);
            }
            card.Region = 6 + j;
            setBtnPosition(btn, card.Region);
        }

    while (index<52)
    {
        var card = cards[orders[index++]];
        cardIndex[0].Add(cvMain.Children.Count);
        var btn = setOneButton(card);
        card.Region = 0;
        setBtnPosition(btn, 0);
        btn.Click += Card_Click;
    }
}

其中,SetOneColumn用于下面牌的上下排序,定义为


private void SetOneColumn(int region)
{
    var count = cardIndex[region].Count;
    var left = (region - 6) * dw;
    var top0 = dh;
    int i = 0;
    var ddh = (0.8 + 2.5 * count / 15) * dh / count;
    foreach (var index in cardIndex[region])
    {
        var btn = cvMain.Children[index];
        Canvas.SetLeft(btn, left);
        Canvas.SetTop(btn, top0 + ddh * (i++));
        Canvas.SetZIndex(btn, i);
    }
}

4 事件

针对纸牌游戏来说,鼠标事件可分为两类,一是点击牌堆需要发牌;二是拖动其他位置的牌。

点击牌堆

点击牌堆需要注意,当牌堆中的牌没有了之后,需要将1区的牌还给牌堆。

private void Card_Click(object sender, RoutedEventArgs e)
{
    var btn = sender as Button;
    var card = btn.Tag as Card;
    if (card.Region > 0)
        return;

    var count = cardIndex[0].Count;
    var num = Math.Min(count, numCard);
    for (int _ = 0; _ < num; _++)
    {
        var index = cardIndex[0][count - num];      //canvas中的顺序
        cardIndex[0].Remove(index);
        cardIndex[1].Add(index);

        btn = cvMain.Children[index] as Button;
        btn.Click -= Card_Click;
        btn.PreviewMouseLeftButtonDown += Card_PreviewLeftDown;
        coverCard(btn, false);
        setBtnPosition(btn, 1);
        card = btn.Tag as Card;
        card.Region = 1;
    }

    if (cardIndex[0].Count > 0 || cardIndex[1].Count <= numCard)
        return;

    foreach (var index in cardIndex[1])
    {
        cardIndex[0].Add(index);
        btn = cvMain.Children[index] as Button;
        btn.Click += Card_Click;
        btn.PreviewMouseLeftButtonDown -= Card_PreviewLeftDown;
        coverCard(btn, true);
        setBtnPosition(btn, 0);
        card = btn.Tag as Card;
        card.Region = 0;
    }
    cardIndex[1] = new List<int>();
}

拖动

拖动主要包含三个动作,即鼠标按下、鼠标挪动、鼠标弹开,所以对应三个函数,且当鼠标按下之后,才挂载鼠标挪动的事件。而鼠标弹起之后,则判断我们拖动的牌的最终位置。

private void Card_PreviewLeftDown(object sender, MouseButtonEventArgs e)
{
    btnNow = sender as Button;
    if (btnNow.Content.ToString() == bgCard)
        return;
    var card = btnNow.Tag as Card;
    regionNow = cardIndex[card.Region];
    indexNow = regionNow.IndexOf(
        cvMain.Children.IndexOf(btnNow));

    var count = regionNow.Count;
    offsets = new List<Point>();
    for (int i = indexNow; i < count; i++)
    {
        var btn = cvMain.Children[regionNow[i]] as Button;
        offsets.Add(Mouse.GetPosition(btn));
        Canvas.SetZIndex(btnNow, 100 + i);
    }

    btnNow.PreviewMouseLeftButtonUp += Card_PreviewLeftUp;
    btnNow.PreviewMouseMove += Card_PreviewMouseMove;
}

private void Card_PreviewMouseMove(object sender, MouseEventArgs e)
{
    var p = Mouse.GetPosition(cvMain);
    for (int i = indexNow; i < regionNow.Count; i++)
    {
        var btn = cvMain.Children[regionNow[i]] as Button;
        Canvas.SetLeft(btn, p.X - offsets[i - indexNow].X);
        Canvas.SetTop(btn, p.Y - offsets[i - indexNow].Y);
    }
}

private void Card_PreviewLeftUp(object sender, MouseButtonEventArgs e)
{

    btnNow.PreviewMouseLeftButtonUp -= Card_PreviewLeftUp;
    btnNow.PreviewMouseMove -= Card_PreviewMouseMove;

    var p = Mouse.GetPosition(cvMain);
    int region = (int)(p.X / dw) + (p.Y > dh ? 6 : 0);
    var index = cardIndex[region].Count - 1;        //目标区域最上面的牌的序号
    var card = btnNow.Tag as Card;
    int srcRegion = card.Region;

    //牌在挪动之后有两种可能,一种是成功了,另一种是失败了
    bool suc = region != srcRegion;     //如果挪动的区域相同,则必失败
    bool subSuc;
    suc &= region > 1;              //如果向牌堆挪动,则必失败。
    if (index < 0)
    {//A和K的情况满足任何一种即可成功
        subSuc = region > 1 && region < 6 && card.Number == 0;
        subSuc |= region > 5 && card.Number == 12;
    }
    else
    {
        var tarBtn = cvMain.Children[cardIndex[region][index]] as Button;
        var tarCard = tarBtn.Tag as Card;
        var flag = tarCard.Type == card.Type;
        var minus = card.Number - tarCard.Number;
        subSuc = region > 1 && region < 6 && flag && (minus == 1);
        subSuc |= region > 5 && (!flag) && (minus == -1);
    }
    reGroup(suc & subSuc, card, srcRegion, region);
}

牌的去留

通过reGroup函数决定牌最终的状态。

private void reGroup(bool suc, Card card, int srcRegion, int tarRegion)
{
    if (suc)
    {
        if (tarRegion > 5)
            setNewRegion(srcRegion, tarRegion);
        else
            setNewRegion(btnNow, card, srcRegion, tarRegion);
    }
    if (srcRegion < 6)
        setBtnPosition(btnNow, srcRegion);
    else
    {
        SetOneColumn(srcRegion);
        var i = regionNow.Count;
        if (i > 0)
        {
            var btn = cvMain.Children[regionNow[i - 1]] as Button;
            coverCard(btn, false);
        }
    }

}
private void setNewRegion(Button btn, Card card, int src, int tar)
{
    int i = cardIndex[src].Count - 1;
    cardIndex[tar].Add(cardIndex[src][i]);
    cardIndex[src].RemoveAt(i);
    card.Region = tar;
    setBtnPosition(btn, tar);
}

//src和tar均为大区
private void setNewRegion(int src, int tar)
{
    var count = regionNow.Count;
    for (int i = indexNow; i < count; i++)
    {
        var btn = cvMain.Children[regionNow[indexNow]] as Button;
        cardIndex[tar].Add(regionNow[indexNow]);
        regionNow.RemoveAt(indexNow);
        var card = btn.Tag as Card;
        card.Region = tar;
        Canvas.SetZIndex(btn, cardIndex[tar].Count);
    }
    SetOneColumn(tar);
    if (regionNow.Count > 0)
    {
        var btn = cvMain.Children[regionNow[indexNow - 1]] as Button;
        coverCard(btn, false);
    }

}

源代码

源代码在这里WPF实现纸牌游戏

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/177725.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【设计模式】结构型模式·外观模式

学习汇总入口【23种设计模式】学习汇总(数万字讲解体系思维导图) 写作不易&#xff0c;如果您觉得写的不错&#xff0c;欢迎给博主来一波点赞、收藏~让博主更有动力吧&#xff01;> 学习汇总入口 一.概述 外观&#xff08;Facade&#xff09;模式是七大设计原则“迪米特法则…

谷粒商城-高级篇-Day12-性能压测和缓存

文章目录性能优化nginx动静分离优化三级分类的获取&#xff08;优化业务&#xff09;分布式缓存整合redis高并发下的缓存失效问题缓存穿透缓存雪崩缓存击穿解决这些问题分布式锁Redisson可重入锁&#xff08;Reentrant Lock&#xff09;指定过期时间读写锁闭锁信号量使用Redssi…

Python实现一个简易的CLI翻译程序

Python实现一个简易的CLI翻译程序Python百度翻译API实现一个简易的CLI翻译程序获取百度翻译API编写一个简单的Python程序Python百度翻译API实现一个简易的CLI翻译程序 之前翻译用的linux上的golddict,每次翻译都很慢。。。 所以想写一个简单快速的翻译命令行翻译软件 获取百度…

Allegro如何自动高亮不等长的网络操作指导

Allegro如何自动高亮不等长的网络操作指导 在做PCB设计的时候,时常需要要做等长,Allegro可以自动高亮一组内不等长的网络,可以直观的看到哪些网络长度是不满足的,类似下图 绿色的是通过的,红色是长度不足的,粉色是超长的 具体操作如下 选择Route-Timing Vision出现optio…

Springboot359的医院病历管理系统

目 录 摘 要 I ABSTRACT II 目 录 II 第1章 绪论 1 1.1背景及意义 1 1.2 国内外研究概况 1 1.3 研究的内容 1 第2章 相关技术 2 第3章 系统分析 3 3.1 需求分析 3 3.2 系统可行性分析 4 3.2.1技术可行性&#xff1a;技术背景 4 3.2.2经济…

Ubiquiti MAC Address Changer 3.0 Crack

Ubiquiti MAC Address Changer&#xff0c;目前mac address changer的版本有很多&#xff0c;本次发布的是V3版本&#xff0c;这是一款功能非常强大的修改网卡mac地址软件&#xff0c;基本上所有的网卡MAC地址都支持修改&#xff0c;包括虚拟机和TeamViewer软件都是支持的。 Ea…

5、基本数据类型

目录 一、整数类型 二、浮点类型 三、字符类型 四、布尔类型 一、整数类型 整数类型用来存储整数数值&#xff0c;即没有小数部分的数值。可以是正数&#xff0c;也可以是负数。整 型数据在Java程序中有3种表示形式&#xff0c;分别为十进制、八进制和十六进制。 1.十进…

2.4.4 数值类型的转换

文章目录1.运算时的自转2.运算时的强转3.强转时的精度丢失问题1.运算时的自转 不同数字类型之间的大小关系如下&#xff1a;double > float > long > int > char, short,byte 自转&#xff1a;小类型的数据可以直接赋值给大类型的变量&#xff1b; byte short c…

Linux(五)创建一个miniShell

前情提要&#xff1a;掌握进程控制中的进程创建、进程终止、进程等待、进程替换。可以参考下方博文 LInux&#xff08;四&#xff09;进程控制&#xff08;创建、终止、等待、替换&#xff09; 了解strtok函数的使用 正文&#xff1a; 目录 Shell是什么&#xff1f; 如何…

蓝桥杯之二分与前缀和

蓝桥杯之二分二分板子&#xff1f;第一次和最后一次出现的位置机器人跳跃问题四平方和分巧克力&#xff1f;典型二分找大的&#xff08;从右往左找&#xff09;二分upper_bound(a1,an1,x)-a&#xff1f;递增三元组前缀和取余&#xff1f;K倍区间二维前缀和&#xff1f;激光炸弹…

17种编程语言实现排序算法-合并排序

开源地址 https://gitee.com/lblbc/simple-works/tree/master/sort/ 覆盖语言&#xff1a;C、C、C#、Java、Kotlin、Dart、Go、JavaScript(JS)、TypeScript(TS)、ArkTS、swift、PHP。 覆盖平台&#xff1a;安卓(Java、Kotlin)、iOS(SwiftUI)、Flutter(Dart)、Window桌面(C#)、…

分享139个ASP源码,总有一款适合您

ASP源码 分享139个ASP源码&#xff0c;总有一款适合您 下面是文件的名字&#xff0c;我放了一些图片&#xff0c;文章里不是所有的图主要是放不下...&#xff0c; 139个ASP源码下载链接&#xff1a;https://pan.baidu.com/s/1Vk4U4EXVCWZWPMWf9ax2dw?pwdif23 提取码&#x…

【C++】类和对象(上)---什么是类?

目录1.面向过程和面向对象初步认识2.类的引入2.1使用struct定义类3.类的定义3.1类的两种定义方式&#xff1a;3.2成员变量命名规则的建议3.3成员函数与成员变量定义的位置建议4.类的访问限定符及封装4.1访问限定符4.2封装5.类的作用域6.类的实例化7.类对象模型7.1如何计算类对象…

springboot静态资源目录访问,及自定义静态资源路径,index页面的访问

springboot静态资源目录访问&#xff0c;及自定义静态资源路径&#xff0c;index页面的访问静态资源目录的访问位置静态资源访问测试自定义静态资源路径和静态资源请求映射web首页的访问自定义静态资源请求映射影响index.html首页的访问的**解决方案**&#xff1a;1.取消自定义…

【JUC系列】CountDownLatch实现原理

简单示例 public class Main {private static final int NUM 3;public static void main(String[] args) throws InterruptedException {CountDownLatch latch new CountDownLatch(NUM);for (int i 0; i < NUM; i) {new Thread(() -> {try {Thread.sleep(2000);Syste…

梯度之上:Hessian 矩阵

原文链接&#xff1a;原文 文章目录梯度之上&#xff1a;Hessian 矩阵梯度、雅克比矩阵海森矩阵海森矩阵应用梯度之上&#xff1a;Hessian 矩阵 本文讨论研究梯度下降法的一个有力的数学工具&#xff1a;海森矩阵。在讨论海森矩阵之前&#xff0c;需要首先了解梯度和雅克比矩阵…

基础知识一览3

这里写目录标题1.Servlet1.1 入门1.2 什么是Servlet1.3 Servlet的作用1.4 Servlet生命周期1.5 Servler的体系结构1.6 Servler的两种配置方式2.Filter2.1 Filter拦截路径配置2.2 过滤器链2.2 入门2.3 过滤器链2.4 过滤器生命周期3.Listener3.1 监听器分类3.1.1 一类监听器4.Serv…

ESP32设备驱动-GA1A12S202光线传感器驱动

GA1A12S202光线传感器驱动 1、GA1A2S202介绍 GA1A1S202 对数刻度模拟光传感器使用起来非常简单,只需添加电源,然后监控模拟输出。大多数光传感器对光强度具有线性响应,这意味着它们对低光水平非常不敏感,然后在高光水平下达到最大值。另一方面,该传感器具有对数响应,这…

第九届蓝桥杯省赛 C++ B组 - 乘积最大

✍个人博客&#xff1a;https://blog.csdn.net/Newin2020?spm1011.2415.3001.5343 &#x1f4da;专栏地址&#xff1a;蓝桥杯题解集合 &#x1f4dd;原题地址&#xff1a;乘积最大 &#x1f4e3;专栏定位&#xff1a;为想参加蓝桥杯的小伙伴整理常考算法题解&#xff0c;祝大家…

Thread的join()方法的作用

文章目录官方文档对join()的解释&#xff1a;结合实例解释官方文档对join()的解释&#xff1a; Thread.join() method javadocs&#xff08;点击跳转&#xff09; join() Waits for this thread to die. 线程类的 join()方法将等待子线程完成&#xff0c;然后继续当前线程。j…