<think>我们正在设计一个基于.NET的蜘蛛纸牌游戏,使用WPF和MVVM架构。我们已经有了核心架构和部分代码,现在需要补充完整代码示例,并解决相关问题。根据用户要求,我们将重点放在代码实现上,同时参考了用户提供的引用内容。引用[1]提到了CPU分片和并发,虽然与游戏逻辑不直接相关,但提醒我们在游戏循环或动画处理中要注意性能。引用[3]提到了蜘蛛纸牌是很多人的编程启蒙,这鼓励我们实现一个完整的游戏。我们将按照以下步骤进行:1.完善游戏模型(Card,CardColumn等)2.完成游戏引擎(SpiderGameEngine)的核心方法,包括发牌、移动、检查完成等3.实现规则验证(GameRules)4.使用MVVM模式连接视图模型和视图5.实现拖拽和动画效果由于用户要求“偏代码多一点”,我们将提供关键部分的完整代码示例。注意:由于篇幅限制,我们不可能写出整个游戏的每一行代码,但会覆盖核心逻辑。现在,我们开始补充代码:一、完善游戏模型我们已经有了Card和CardColumn,但需要补充一些细节:1.Card类需要实现IEquatable接口以便比较,并且需要添加一个属性表示其在游戏中的位置(列和索引)?2.实际上,在MVVM中,我们不建议在模型中包含视图相关的属性(如位置),位置应由视图模型管理。因此,我们创建CardViewModel来包装Card,并添加位置信息。二、游戏引擎核心初始化游戏时,我们需要:-生成两副牌(104张,根据难度不同花色数量不同)-洗牌-发牌:初始每列6张(共54张),剩下的50张作为发牌堆(StockPile)-注意:初始发牌时,每列最上面一张是翻开的三、规则验证-移动规则:只能移动连续的、同花色且降序的序列-目标列规则:空列可以放任何牌;非空列必须满足目标列最后一张牌比要移动的第一张牌大1且花色相同(注意:在蜘蛛纸牌中,目标列不要求花色相同,但要求颜色交替?不对,蜘蛛纸牌的规则是:只有同花色的连续序列才能移动,目标列必须满足降序且同花色?实际上,蜘蛛纸牌的目标列接收规则是:可以接收任何单张或连续的同花色降序序列,但要求目标列的最后一张牌比移动序列的第一张牌大1(不要求花色相同?)——这里需要澄清规则。根据经典蜘蛛纸牌规则:-移动:可以移动连续的、同花色的、降序的序列(例如:红桃10,9,8可以移动)-目标列:空列可以放任何单张或序列;非空列,则移动的序列的第一张牌必须比目标列的最后一张牌小1(花色不限?)——注意:经典蜘蛛纸牌中,目标列接收时并不要求花色相同,只要求降序(即移动的序列的第一张牌比目标列最后一张牌小1即可,且移动的序列本身必须是同花色连续降序)。但是,在目标列上,我们并不要求整个列都是同花色,只要求每一段连续的序列是同花色降序。所以,在移动时,我们只要求移动的序列是同一花色的连续降序,并且目标列的最后一张牌的点数比移动序列的第一张牌大1(花色任意)。因此,我们需要修改规则验证:四、视图模型-主视图模型(SpiderViewModel)包含游戏引擎实例和命令-列视图模型(CardColumnViewModel)管理一列中的牌(CardViewModel的集合)五、UI交互-使用WPF的ItemsControl来显示10列-每列是一个CardColumnView,内部使用Canvas或StackPanel来放置牌(重叠效果)-实现拖拽:开始拖拽时,记录被拖拽的牌以及其后的所有牌(因为可以拖拽连续序列);放置时,判断目标列是否接受六、动画效果-使用WPF的Storyboard实现移动动画下面,我们逐步给出关键代码:1.完善模型我们调整Card类,添加一个ID属性以便区分(因为有两副牌)?实际上,我们通过花色和点数可以区分,但同一花色点数相同的牌有两张,所以需要额外的标识。我们可以不添加,因为游戏过程中我们并不需要区分两张相同的牌。但是,为了在UI上正确显示,我们可能需要唯一标识。我们可以重写Equals和GetHashCode,或者使用引用比较。这里,我们选择不添加ID,因为游戏逻辑中,我们只关心牌的花色和点数,以及是否翻开。修改Card类:```csharppublicenumSuit{Spades,Hearts,Clubs,Diamonds}publicclassCard:INotifyPropertyChanged{publicSuitSuit{get;}publicintRank{get;}//1=A,2-10,11=J,12=Q,13=Kprivatebool_isFaceUp;publicboolIsFaceUp{get=>_isFaceUp;set{_isFaceUp=value;OnPropertyChanged();}}publicCard(Suitsuit,intrank){Suit=suit;Rank=rank;}publiceventPropertyChangedEventHandlerPropertyChanged;protectedvirtualvoidOnPropertyChanged([CallerMemberName]stringname=null){PropertyChanged?.Invoke(this,newPropertyChangedEventArgs(name));}}```2.游戏引擎核心方法初始化游戏:```csharppublicclassSpiderGameEngine{privatereadonlyList<Card>_deck=newList<Card>(104);publicObservableCollection<CardColumn>Columns{get;}=newObservableCollection<CardColumn>();publicStack<Card>StockPile{get;}=newStack<Card>();publicDifficultyDifficulty{get;privateset;}publicvoidInitialize(Difficultydifficulty){Difficulty=difficulty;GenerateDeck();Shuffle();SetupColumns();}privatevoidGenerateDeck(){_deck.Clear();intsuitCount=(int)Difficulty;//两副牌for(inti=0;i<2;i++){for(ints=0;s<suitCount;s++){for(intr=1;r<=13;r++){_deck.Add(newCard((Suit)s,r));}}}}privatevoidShuffle(){Randomrng=newRandom();intn=_deck.Count;while(n>1){n--;intk=rng.Next(n+1);Cardvalue=_deck[k];_deck[k]=_deck[n];_deck[n]=value;}}privatevoidSetupColumns(){Columns.Clear();for(inti=0;i<10;i++){Columns.Add(newCardColumn());}//初始发牌:共54张,前4列每列6张,后6列每列5张(或者每列先发5张,然后再前4列各加1张)intcardIndex=0;//第一轮:每列5张(共50张)for(introw=0;row<5;row++){for(intcol=0;col<10;col++){Cardcard=_deck[cardIndex++];//最上面一张是翻开的card.IsFaceUp=(row==4);//第五行(最后一张)翻开Columns[col].Add(card);}}//第二轮:前4列各加1张(翻开)for(intcol=0;col<4;col++){Cardcard=_deck[cardIndex++];card.IsFaceUp=true;Columns[col].Add(card);}//剩下的50-54张牌?不对,总共104张,发走了54张,剩下50张while(cardIndex<_deck.Count){Cardcard=_deck[cardIndex++];card.IsFaceUp=false;//发牌堆的牌是盖着的StockPile.Push(card);}}//从发牌堆发一张牌到每一列publicvoidDealFromStock(){if(StockPile.Count==0)return;for(inti=0;i<10;i++){if(StockPile.Count==0)break;Cardcard=StockPile.Pop();card.IsFaceUp=true;Columns[i].Add(card);}}}publicclassCardColumn:ObservableCollection<Card>{//检查是否可以接收移动过来的牌(序列)publicboolCanAcceptMove(IList<Card>cards){if(cards==null||cards.Count==0)returnfalse;CardfirstCard=cards[0];if(Count==0){//空列可以接收任意序列returntrue;}CardlastCard=this[Count-1];//目标列的最后一张牌必须比移动序列的第一张牌大1(点数)returnlastCard.Rank==firstCard.Rank+1;}//获取本列最后一张牌publicCardLast()=>this[Count-1];}```3.规则验证在GameRules中,我们需要验证移动的序列是否连续同花色降序,以及目标列是否可以接收。```csharppublicstaticclassGameRules{publicstaticboolValidateMove(CardColumnsource,intstartIndex,CardColumntarget){//获取移动的牌(从startIndex开始到末尾)varmovingCards=newList<Card>();for(inti=startIndex;i<source.Count;i++){movingCards.Add(source[i]);}//检查移动的序列是否是连续同花色降序if(!IsConsecutiveDescendingSameSuit(movingCards))returnfalse;//检查目标列是否可以接收returntarget.CanAcceptMove(movingCards);}//检查序列是否连续(同花色,降序,相邻差1)publicstaticboolIsConsecutiveDescendingSameSuit(IList<Card>cards){if(cards==null||cards.Count==0)returnfalse;if(cards.Count==1)returntrue;//单张牌总是连续的for(inti=0;i<cards.Count-1;i++){if(cards[i].Rank!=cards[i+1].Rank+1)returnfalse;if(cards[i].Suit!=cards[i+1].Suit)returnfalse;}returntrue;}}```4.视图模型我们需要创建CardViewModel来包装Card,并记录其在列中的位置(索引)和所属的列。```csharppublicclassCardViewModel:INotifyPropertyChanged{privatereadonlyCard_card;publicCardViewModel(Cardcard){_card=card;}publicSuitSuit=>_card.Suit;publicintRank=>_card.Rank;publicboolIsFaceUp=>_card.IsFaceUp;//位置信息:在列中的索引(用于计算显示位置)publicintPositionIndex{get;set;}//所属列(视图模型)publicCardColumnViewModelParentColumn{get;set;}//实现属性变化通知(将Card的属性变化传递上来)publiceventPropertyChangedEventHandlerPropertyChanged;privatevoidOnPropertyChanged([CallerMemberName]stringname=null){PropertyChanged?.Invoke(this,newPropertyChangedEventArgs(name));}publicvoidSubscribeToCard(){_card.PropertyChanged+=(s,e)=>{if(e.PropertyName==nameof(Card.IsFaceUp)){OnPropertyChanged(nameof(IsFaceUp));}};}}publicclassCardColumnViewModel:INotifyPropertyChanged{publicCardColumnModel{get;}publicObservableCollection<CardViewModel>Cards{get;}=newObservableCollection<CardViewModel>();publicCardColumnViewModel(CardColumnmodel){Model=model;//将model中的牌转换为CardViewModelforeach(varcardinmodel){AddCard(card);}model.CollectionChanged+=Model_CollectionChanged;}privatevoidModel_CollectionChanged(objectsender,NotifyCollectionChangedEventArgse){//同步到Cardsif(e.Action==NotifyCollectionChangedAction.Add){foreach(Cardcardine.NewItems){AddCard(card);}}//其他操作(如Remove)也需要处理,此处省略}privatevoidAddCard(Cardcard){varcardVM=newCardViewModel(card){ParentColumn=this,PositionIndex=Cards.Count};cardVM.SubscribeToCard();Cards.Add(cardVM);}publiceventPropertyChangedEventHandlerPropertyChanged;}```主视图模型:```csharppublicclassSpiderViewModel:INotifyPropertyChanged{privatereadonlySpiderGameEngine_engine;publicObservableCollection<CardColumnViewModel>Columns{get;}=newObservableCollection<CardColumnViewModel>();publicICommandDealStockCommand{get;}publicICommandMoveCommand{get;}publicSpiderViewModel(){_engine=newSpiderGameEngine();DealStockCommand=newRelayCommand(DealStock);MoveCommand=newRelayCommand<MoveParams>(ExecuteMove);}publicvoidInitializeGame(Difficultydifficulty){_engine.Initialize(difficulty);UpdateView();}privatevoidUpdateView(){Columns.Clear();foreach(varcolumnin_engine.Columns){Columns.Add(newCardColumnViewModel(column));}}privatevoidDealStock(){_engine.DealFromStock();//由于发牌后,列中的牌数量变化,但我们的CardColumnViewModel已经通过CollectionChanged事件同步,所以不需要额外操作//但是,如果发牌后需要刷新整个视图,可以调用UpdateView,但这样效率低。我们选择通过事件机制自动更新。}privatevoidExecuteMove(MoveParamsparam){if(GameRules.ValidateMove(param.Source,param.StartIndex,param.Target)){//执行移动:从源列移除,添加到目标列List<Card>cardsToMove=newList<Card>();for(inti=param.StartIndex;i<param.Source.Count;i++){cardsToMove.Add(param.Source[i]);}//注意:这里不能边遍历边移除,因为移除后索引会变//所以先记录,然后从后往前移除(或者一次性移除)for(inti=param.Source.Count-1;i>=param.StartIndex;i--){param.Source.RemoveAt(i);}foreach(varcardincardsToMove){param.Target.Add(card);}//移动后,检查源列最上面的牌是否要翻开if(param.Source.Count>0&&!param.Source[param.Source.Count-1].IsFaceUp){param.Source[param.Source.Count-1].IsFaceUp=true;}//检查目标列是否完成一组(从K到A的13张同花色连续序列)CheckCompletedSet(param.Target);}}privatevoidCheckCompletedSet(CardColumncolumn){//从后往前检查是否有13张连续同花色降序的牌if(column.Count<13)return;//从最后一张开始,往前推12张intstartIndex=column.Count-13;List<Card>set=newList<Card>();for(inti=startIndex;i<column.Count;i++){set.Add(column[i]);}//检查是否是连续同花色降序(从K到A)if(GameRules.IsConsecutiveDescendingSameSuit(set)&&set[0].Rank==13&&set[12].Rank==1){//移除这13张牌for(inti=0;i<13;i++){column.RemoveAt(startIndex);//每次移除startIndex位置,因为移除后后面的会前移}//记分等操作...}}}publicclassMoveParams{publicCardColumnSource{get;set;}publicintStartIndex{get;set;}publicCardColumnTarget{get;set;}}```5.UI交互(拖拽)在WPF中,我们使用DragDrop实现拖拽。由于拖拽可能涉及多张牌,我们需要在拖拽数据中传递起始索引和源列。在CardControl(表示单张牌的UserControl)中处理鼠标按下事件:```csharpprivatevoidCard_MouseDown(objectsender,MouseButtonEventArgse){if(e.LeftButton==MouseButtonState.Pressed&&e.ClickCount==1){CardViewModelcardVM=(CardViewModel)((FrameworkElement)sender).DataContext;//找到该牌在列中的起始索引(因为可能拖拽多张)intstartIndex=cardVM.PositionIndex;//注意:PositionIndex是视图模型中的位置,但实际移动时,我们需要从该位置开始直到末尾//准备拖拽数据DataObjectdata=newDataObject();data.SetData("SourceColumn",cardVM.ParentColumn.Model);//源列(模型)data.SetData("StartIndex",startIndex);//开始拖拽DragDrop.DoDragDrop(this,data,DragDropEffects.Move);}}```在目标列(CardColumnView)中处理放置事件:```csharpprivatevoidColumn_Drop(objectsender,DragEventArgse){if(e.Data.GetDataPresent("SourceColumn")&&e.Data.GetDataPresent("StartIndex")){CardColumnsource=(CardColumn)e.Data.GetData("SourceColumn");intstartIndex=(int)e.Data.GetData("StartIndex");CardColumnViewModeltargetVM=(CardColumnViewModel)DataContext;CardColumntarget=targetVM.Model;//执行移动命令varmoveParams=newMoveParams{Source=source,StartIndex=startIndex,Target=target};//获取主视图模型(可以通过绑定等方式获得,这里假设CardColumnView的DataContext是CardColumnViewModel,而主视图模型是其父级)//实际项目中,可能需要使用Messenger或者传递命令引用//简单起见,我们假设主视图模型是Application.Current.MainWindow.DataContextSpiderViewModelmainVM=(SpiderViewModel)Application.Current.MainWindow.DataContext;mainVM.MoveCommand.Execute(moveParams);}}```6.动画效果我们可以为每张牌的移动创建动画。但是,由于我们的视图模型和模型是分离的,当牌的位置变化(例如从一列移动到另一列)时,我们可以通过绑定来更新位置,但动画需要依赖UI元素的位置属性。因此,我们可以在CardControl中使用RenderTransform,当牌的位置发生变化(比如在列中的索引变化)时,触发一个动画,将牌从旧位置移动到新位置。由于时间关系,这里不展开动画实现的细节。以上代码示例已经覆盖了蜘蛛纸牌的核心逻辑。由于篇幅限制,我们省略了部分细节(如移除牌时的集合同步、游戏结束判断等),但给出了主要框架。最后,我们根据用户要求生成相关问题:§§相关问题§§1.在蜘蛛纸牌游戏中,如何实现拖拽多张牌的功能?2.如何优化蜘蛛纸牌游戏中的洗牌算法以保证随机性?3.在WPF中,如何实现纸牌的重叠显示效果?4.蜘蛛纸牌游戏中,如何判断游戏胜利条件?5.如何为蜘蛛纸牌游戏添加撤销功能?</think>### .NET 蜘蛛纸牌游戏完整实现 (C# + WPF)
#### 一、核心架构设计
```mermaid
graph TD
A[UI层 View] -->|数据绑定| B[视图模型 ViewModel]
B -->|操作调用| C[游戏引擎 Engine]
C -->|规则验证| D[规则引擎 Rules]
C -->|状态管理| E[持久化服务]
F[牌面模型] --> C
G[动画服务] --> A
```
#### 二、完整代码实现
##### 1. 核心模型定义
```csharp
// 扑克牌模型
public class Card : INotifyPropertyChanged
{
public enum Suit { Spades, Hearts, Clubs, Diamonds }
public Suit CardSuit { get; }
public int Rank { get; } // 1=A, 13=K
private bool _isFaceUp;
public bool IsFaceUp
{
get => _isFaceUp;
set { _isFaceUp = value; OnPropertyChanged(); }
}
public Card(Suit suit, int rank)
{
CardSuit = suit;
Rank = rank;
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string name = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
// 牌列模型
public class CardColumn : ObservableCollection<Card>
{
public bool CanAcceptCard(Card card)
=> Count == 0 || card.Rank == this[^1].Rank - 1;
}
```
##### 2. 游戏引擎核心
```csharp
public class SpiderGameEngine
{
public ObservableCollection<CardColumn> Tableau { get; } = new(10);
public Stack<Card> Stock { get; } = new();
public int Moves { get; private set; }
public void Initialize(Difficulty difficulty)
{
// 生成两副牌(104张)
var deck = GenerateDeck(difficulty);
Shuffle(deck);
// 初始化牌桌
Tableau.Clear();
for (int i = 0; i < 10; i++) Tableau.Add(new CardColumn());
// 发牌逻辑(经典蜘蛛布局)
DealCards(deck);
}
private List<Card> GenerateDeck(Difficulty difficulty)
{
int suits = (int)difficulty;
var deck = new List<Card>(104);
for (int deckNum = 0; deckNum < 2; deckNum++)
{
for (int suit = 0; suit < suits; suit++)
{
for (int rank = 1; rank <= 13; rank++)
{
deck.Add(new Card((Card.Suit)suit, rank));
}
}
}
return deck;
}
private void Shuffle(List<Card> deck)
{
var rng = new Random();
int n = deck.Count;
while (n > 1)
{
n--;
int k = rng.Next(n + 1);
(deck[k], deck[n]) = (deck[n], deck[k]);
}
}
private void DealCards(List<Card> deck)
{
// 初始布局:前4列6张,后6列5张
int index = 0;
for (int row = 0; row < 5; row++)
{
for (int col = 0; col < 10; col++)
{
if (row == 4 && col >= 4) break;
var card = deck[index++];
card.IsFaceUp = (row >= 4); // 最上层翻开
Tableau[col].Add(card);
}
}
// 剩余牌放入发牌堆
foreach (var card in deck.Skip(index))
{
Stock.Push(card);
}
}
public void DealFromStock()
{
if (Stock.Count == 0) return;
for (int i = 0; i < 10 && Stock.Count > 0; i++)
{
var card = Stock.Pop();
card.IsFaceUp = true;
Tableau[i].Add(card);
}
Moves++;
}
public bool MoveCards(CardColumn source, int startIndex, CardColumn target)
{
var movingCards = source.Skip(startIndex).ToList();
if (!GameRules.ValidateMove(movingCards, target))
return false;
// 执行移动
for (int i = source.Count - 1; i >= startIndex; i--)
{
source.RemoveAt(i);
}
movingCards.ForEach(target.Add);
// 翻开源列最上层牌
if (source.Count > 0 && !source[^1].IsFaceUp)
source[^1].IsFaceUp = true;
Moves++;
return true;
}
}
public enum Difficulty { SingleSuit = 1, TwoSuits = 2, FourSuits = 4 }
```
##### 3. 规则引擎实现
```csharp
public static class GameRules
{
public static bool ValidateMove(List<Card> movingCards, CardColumn target)
{
if (movingCards.Count == 0) return false;
// 规则1: 移动序列必须连续同花色
if (!IsConsecutiveSequence(movingCards)) return false;
// 规则2: 目标列必须接受第一张牌
return target.CanAcceptCard(movingCards[0]);
}
private static bool IsConsecutiveSequence(List<Card> cards)
{
for (int i = 0; i < cards.Count - 1; i++)
{
if (cards[i].Rank != cards[i + 1].Rank + 1)
return false;
if (cards[i].CardSuit != cards[i + 1].CardSuit)
return false;
}
return true;
}
// 检查完整序列(K->A)
public static bool IsCompletedSet(List<Card> cards)
{
if (cards.Count < 13) return false;
var last13 = cards.TakeLast(13).ToList();
return IsConsecutiveSequence(last13) && last13[0].Rank == 13;
}
}
```
##### 4. MVVM视图模型
```csharp
public class SpiderViewModel : INotifyPropertyChanged
{
private readonly SpiderGameEngine _engine = new();
public ObservableCollection<CardColumnViewModel> Tableau { get; } = new();
public ICommand DealCommand { get; }
public ICommand MoveCommand { get; }
public int Moves => _engine.Moves;
public int StockCount => _engine.Stock.Count;
public SpiderViewModel()
{
DealCommand = new RelayCommand(DealFromStock);
MoveCommand = new RelayCommand<MoveParams>(ExecuteMove);
InitializeGame(Difficulty.FourSuits);
}
private void InitializeGame(Difficulty difficulty)
{
_engine.Initialize(difficulty);
UpdateView();
}
private void UpdateView()
{
Tableau.Clear();
foreach (var column in _engine.Tableau)
{
Tableau.Add(new CardColumnViewModel(column));
}
OnPropertyChanged(nameof(Moves));
OnPropertyChanged(nameof(StockCount));
}
private void DealFromStock()
{
_engine.DealFromStock();
UpdateView();
}
private void ExecuteMove(MoveParams param)
{
if (_engine.MoveCards(param.Source.Model, param.StartIndex, param.Target.Model))
{
UpdateView();
CheckForCompletedSets();
}
}
private void CheckForCompletedSets()
{
foreach (var column in Tableau)
{
if (GameRules.IsCompletedSet(column.Model.ToList()))
{
// 移除完成的13张牌
for (int i = 0; i < 13; i++) column.Model.RemoveAt(column.Model.Count - 1);
}
}
}
// INotifyPropertyChanged 实现
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string name = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
public class CardColumnViewModel
{
public CardColumn Model { get; }
public CardColumnViewModel(CardColumn model) => Model = model;
}
public class MoveParams
{
public CardColumnViewModel Source { get; set; }
public int StartIndex { get; set; }
public CardColumnViewModel Target { get; set; }
}
```
##### 5. WPF视图实现 (MainWindow.xaml)
```xml
<Window x:Class="SpiderSolitaire.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:SpiderSolitaire"
Title="蜘蛛纸牌" Height="800" Width="1200">
<Grid Background="DarkGreen">
<!-- 牌桌区域 -->
<ItemsControl ItemsSource="{Binding Tableau}" Margin="20">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="10" HorizontalAlignment="Center"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<local:CardColumnView/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- 状态栏 -->
<StackPanel Orientation="Horizontal" VerticalAlignment="Bottom" HorizontalAlignment="Right">
<TextBlock Text="剩余牌数:" Foreground="White" Margin="5"/>
<TextBlock Text="{Binding StockCount}" Foreground="White" Margin="0,5,20,5"/>
<Button Content="发牌" Command="{Binding DealCommand}" Padding="20,5" Margin="10"/>
</StackPanel>
</Grid>
</Window>
```
##### 6. 拖拽交互实现
```csharp
// CardColumnView.xaml.cs
public partial class CardColumnView : UserControl
{
public CardColumnView()
{
InitializeComponent();
AllowDrop = true;
}
private void OnCardMouseDown(object sender, MouseButtonEventArgs e)
{
if (e.LeftButton != MouseButtonState.Pressed) return;
var cardControl = (FrameworkElement)sender;
var cardVM = (CardViewModel)cardControl.DataContext;
var columnVM = (CardColumnViewModel)DataContext;
// 计算拖拽起始索引
int startIndex = columnVM.Cards.IndexOf(cardVM);
// 设置拖拽数据
var data = new DataObject();
data.SetData("SourceColumn", DataContext);
data.SetData("StartIndex", startIndex);
DragDrop.DoDragDrop(cardControl, data, DragDropEffects.Move);
}
private void OnColumnDrop(object sender, DragEventArgs e)
{
if (!e.Data.GetDataPresent("SourceColumn")) return;
var sourceColumn = (CardColumnViewModel)e.Data.GetData("SourceColumn");
int startIndex = (int)e.Data.GetData("StartIndex");
var targetColumn = (CardColumnViewModel)DataContext;
// 执行移动命令
var moveParams = new MoveParams {
Source = sourceColumn,
StartIndex = startIndex,
Target = targetColumn
};
var viewModel = (SpiderViewModel)DataContext;
if (viewModel.MoveCommand.CanExecute(moveParams))
{
viewModel.MoveCommand.Execute(moveParams);
}
}
}
```
##### 7. 动画服务实现
```csharp
public static class CardAnimator
{
public static void AnimateMove(FrameworkElement element, Point toPosition)
{
// 设置初始位置
Canvas.SetLeft(element, Canvas.GetLeft(element));
Canvas.SetTop(element, Canvas.GetTop(element));
// 创建动画
var xAnim = new DoubleAnimation(toPosition.X,
new Duration(TimeSpan.FromMilliseconds(300)));
var yAnim = new DoubleAnimation(toPosition.Y,
new Duration(TimeSpan.FromMilliseconds(300)));
// 启动动画
element.BeginAnimation(Canvas.LeftProperty, xAnim);
element.BeginAnimation(Canvas.TopProperty, yAnim);
}
}
```
#### 三、性能优化建议
1. **虚拟化技术**:对大量卡牌使用UI虚拟化
```xml
<ItemsControl VirtualizingStackPanel.IsVirtualizing="True"
VirtualizingStackPanel.VirtualizationMode="Recycling"/>
```
2. **位图缓存**:减少渲染开销
```xml
<CardControl CacheMode="BitmapCache"/>
```
3. **延迟加载**:只在需要时加载卡牌图片
```csharp
public class CardImageLoader
{
private static readonly Dictionary<string, BitmapImage> _cache = new();
public static ImageSource LoadCardImage(Card card)
{
string key = $"{card.CardSuit}_{card.Rank}";
if (!_cache.TryGetValue(key, out var image))
{
image = new BitmapImage(new Uri($"Images/{key}.png"));
_cache.Add(key, image);
}
return image;
}
}
```
#### 四、游戏规则要点
1. **移动规则**:
- 只能移动同花色连续降序序列
- 空列可放置任何卡牌
- 发牌堆每次发10张牌
2. **胜利条件**:
- 所有花色完成8组K-A序列
- 公式:$W = \sum_{s=1}^{4} C_s$,其中 $C_s$ 是完成的花色组数
3. **计分规则**:
- 移动牌:$-1$ 分
- 完成一组:$+100$ 分
- 最终得分:$Score = 500 + (100 \times CompletedSets) - Moves$
> 关键点:MVVM架构实现关注点分离,数据绑定自动同步状态,规则引擎封装核心游戏逻辑[^1]