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

news/2024/11/29 8:59:04/

文章目录

    • 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);elsesetNewRegion(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.ppmy.cn/news/19251.html

相关文章

常见递归模式

常见递归模式递归模式遍历二叉树模式回溯模式子问题分解模式递归模式 常见递归模式&#xff1a; 遍历二叉树模式回溯模式子问题分解模式 遍历二叉树模式 只要涉及递归的问题&#xff0c;都是树的问题&#xff0c;或者说树的遍历。 void traverse(TreeNode root) { // 遍历…

【MySQL基础】MySQL多表操作详解

序号系列文章4【MySQL基础】MySQL表的七大约束5【MySQL基础】字符集与校对集详解6【MySQL基础】MySQL单表操作详解7【MySQL基础】运算符及相关函数详解文章目录前言1&#xff0c;多表关系1.1&#xff0c;一对一1.2&#xff0c;一对多1.3&#xff0c;多对多2&#xff0c;多表查询…

Keil C51工程转VSCode Keil Assistant开发全过程

Keil C51工程转VSCode Keil Assistant开发全过程✨这里以stc15W408AS为例。&#x1f4cc;相关篇《【开源分享】自制STC15W408AS开发板》 &#x1f4fa;编译-烧录演示&#xff1a; &#x1f4cb;转VSCODE开发环境主要原因可能代码提示以及代码跳转功能&#xff0c;或者其他。 &…

Python(for和while)循环嵌套及用法

Python 不仅支持 if 语句相互嵌套&#xff0c;while 和 for 循环结构也支持嵌套。所谓嵌套&#xff08;Nest&#xff09;&#xff0c;就是一条语句里面还有另一条语句&#xff0c;例如 for 里面还有 for&#xff0c;while 里面还有 while&#xff0c;甚至 while 中有 for 或者 …

Nginx与LUA(7)

您好&#xff0c;我是湘王&#xff0c;这是我的CSDN博客。值此新春佳节&#xff0c;我给您拜年啦&#xff5e;祝您在新的一年中所求皆所愿&#xff0c;所行皆坦途&#xff0c;展宏“兔”&#xff0c;有钱“兔”&#xff0c;多喜乐&#xff0c;常安宁&#xff01;软件开发中&…

React错误边界

首先 我们先构建出问题的场景 我们创建一个react项目 然后在src下创建 components 文件夹目录 在下面创建一个 error.jsx 组件 参开代码如下 import React from "react";export default class App extends React.Component{constructor(props){super(props);this.…

【每日一题Day97】LC1828统计一个圆中点的数目 | 模拟

统计一个圆中点的数目【LC1828】 给你一个数组 points &#xff0c;其中 points[i] [xi, yi] &#xff0c;表示第 i 个点在二维平面上的坐标。多个点可能会有 相同 的坐标。 同时给你一个数组 queries &#xff0c;其中 queries[j] [xj, yj, rj] &#xff0c;表示一个圆心在 …

单绞机控制算法模型(Simulink仿真)

线缆行业单绞机PLC控制算法详细解读可以参看下面的文章链接: 线缆行业单绞机控制算法(详细图解+代码)_RXXW_Dor的博客-CSDN博客在了解单绞机之前需要大家对收放卷以及排线控制有一定的了解,不清楚的可以参看下面几篇博客,这里不再赘述,受水平和能力所限,文中难免出现错…