单人纸牌游戏,牌桌上有7个堆共28张牌,第一堆1张牌,第二堆2张,。。。第7堆7张,每一堆的第一张牌朝上,其他朝下。牌桌上还有4个suitpiles,一个deck card堆和一个discard card堆,布局如下(参考windows的纸牌游戏)
项目地址
https://github.com/ccclll777/Windows_Solitaire_game
如果有帮助可以点个star
实验内容
使用java/C++语言,利用面向对象技术,模拟windows纸牌小游戏。
单人纸牌游戏,牌桌上有7个堆共28张牌,第一堆1张牌,第二堆2张,。。。第7堆7张,每一堆的第一张牌朝上,其他朝下。牌桌上还有4个suitpiles,一个deck card堆和一个discard card堆,布局如下(参考windows的纸牌游戏)。
总体功能设计
一.需求分析
单人纸牌游戏,牌桌上有7个堆共28张牌,第一堆1张牌,第二堆2张……第7堆7张,每一堆的第一张牌朝上,其他朝下。牌桌上还有4个suitstack,一个deck card堆和一个discard card堆。初始时四个suitstack和一个deck card堆为空,discard card堆中有24张牌且全部背面朝下,在明确规则之后,需要设计图形界面,增加与用户的交互性。为了用户提供更好的游戏体验,可以为用户提供不同难度的纸牌游戏,重新定义发牌规则。
二.目标分析
首先明确游戏的规则,游戏共有十三个牌堆,十三个牌堆被分为四个区域:桌面堆(TableStack),花色堆(SuitStack),发牌堆(DeckStack),和丢弃堆(DisStack)
桌面堆一共有7个牌堆,纸牌的数量是从1到7共28张,每一个桌面堆初始状态是,只有最底端的牌为正面,其他牌为反面。
花色堆初始状态为空。
丢弃堆初始状态也为空。
其他剩下的24张牌放入发牌堆且牌面朝下。
游戏规则:
(1)每次点击发牌堆,会将一张牌加入丢弃堆,表示发了一张牌。
(2)在桌面堆的玩牌规则:如果要将牌拖向桌面堆,需要满足的是,拖动牌堆的最顶端,与放入的牌堆的最底端的颜色不同且大小小1,满足这个条件的牌堆,就可以合在一起。
(3)从丢弃堆向桌面堆拖动牌也需要满足同样的要求,拖动牌堆的最顶端,与放入的牌堆的最底端的颜色不同且大小小1。
(4)花色堆有四个,每一个放一种花色的牌,然后从一开始存放,每次只能拖动一个到花色堆。需要满足的条件是,花色堆顶端的卡牌的花色和需要拖进去卡牌的花色相同,且大小小1.如果满足这个条件,就可以将牌放入花色堆。
游戏功能:
(1)实现几个牌堆的交互,如果符合规则的话可以互相拖动。
(2)如果发牌堆变为空但是丢弃堆没空的话,说明牌没有发完,这时候把丢弃堆的牌在放入发牌堆,然后重新发一遍。直到全部发完为止。
(3)如果用户想重新开始一局新的游戏,可以点击重新启动的按钮,然后重新发牌,开始一局新的游戏。
(4)为用户提供两个等级的难度。low和high。
low难度表示,如果一个桌面堆是空的,你可以把其他任意牌拖到桌面堆。
high难度表示,如果一个桌面堆是空的,他这里只能放从K 开始的牌堆。
low难度可以放松对用户的约束,让用户可以更容易的通关。
(5)如果你通关,可以为用户显示通关成功之类的东西。
设计思路
(1)枚举类Suit和Rank
我们需要纸牌的四个花色和十三个大小,所以初始化了两个枚举类Suit和Rank,分别存放花色和大小,以便卡牌的初始化。
(2)Card类
我们需要卡牌类Card,然后初始化每一张卡牌,Card类实现了Card接口,Card类中有花色,大小,是否正面朝上的属性,以及各个属性对应的get和set方法。还提供了一个静态的方法,在项目编译时会进行初始化,创建了一个数组,里边存了每个花色的每个纸牌的编号,在项目运行过程中,可以通过卡牌编号寻找是那张纸牌,也可以通过卡牌的花色和大小找出卡牌编号,方便我们使用。(静态类和final类型的变量,在编译时就已经创建,并且初始化)
(3)CardStack
由于牌堆时从一头进行出入的,并且是先进后出的,所以我们使用了 ArrayList来模拟了堆栈进行操作。首先创建CardStack类实现CardStack_Interface接口,它是十三个牌堆的父类,它创建了十三个牌堆的属性,实现了牌堆的基本方法。它的基本属性有存放卡牌的ArrayList,实现的方法有,获取ArrayList的大小,获取最顶部卡牌,往牌堆顶添加一张卡牌,获取指定位置的牌,push一组卡牌,删除一组卡牌,删除顶端卡牌,清空牌堆等等方法。
(4)DescStack发牌堆
继承自CardStack,由于添加时需要设置纸牌的背面朝上,所以需要重写父类的push方法。
(5)DiscardStack丢弃堆
继承自CardStack,由于向 DisCardStack添加卡牌时都是朝上的,所以重写了父类的push操作。
(6)SuitStack花色堆
花色堆继承自CardStack,是收集已经有序的牌的地方,由于进入花色堆的牌是正面朝上的,所以重写了父类的push操作,让进入牌堆的牌正面朝上。
(7)TableStack桌面堆
桌面堆是玩牌的主要区域,需要根据游戏的规则堆牌堆进行拖动,达到排序的效果,最终让牌全部放到花色堆。桌面堆继承自CardStack,并且添加了两个其他的方法:
第一可以根据花色和大小,确定这张牌在这个牌堆的位置,在牌堆拖动时,可以用来确定这是那一张牌。
第二可以获得这个牌堆的子牌堆,根据第一个方法获取到这个牌子啊牌堆的位置,将这个牌堆内大小比这个牌都小的牌返回,实现牌堆间的拖动。
**
**
类GameModel实现了有关游戏规则,它是一个final类,在程序编译时将被创建并且初始化,并且该类不能被继承。
在 GameModel类中,对于游戏的十三个牌堆进行了初始化,将十三个牌堆加入泛型为CardStack的ArrayList的中进行管理。
牌堆初始化时,将28张牌分别放入桌面堆中,然后让他们最顶端的卡牌朝上,然后将剩余的24张牌放入发牌堆。
由于GameModel是一个final类,初始化后不能改变,所以在GameModel类中提供一个方法,他是一个静态方法,可以通过类名加函数名访问,返回值是GameModel类的一个实例,这样在类外,就可以通过获取的这个实例来使用这个类中的其他方法了。
GameModel中提供了有关游戏牌堆拖动是否合法的判断,如果拖动合法,则进行牌堆拖动的逻辑。
GameModel中实现了,发牌的逻辑,每次将发牌堆DescStack中最顶端的牌加入到DisCardStack中。
GameModel实现了游戏的重新发牌的调用,实现了如果发牌堆变为空但是丢弃堆没空的话,说明牌没有发完,这时候把丢弃堆的牌在放入发牌堆,然后重新发一遍。直到全部发完为止。
GameModel实现了将牌堆中的牌,通过格式化变成字符串,在拖动时可以放到剪切板内,然后传到目的牌堆上,可以对传递的牌堆进行一系列的处理,看拖动是否合法,是否可以完成相应的拖动。
GameModel实现了对于每一个牌堆的监听功能,如果牌堆发生改变,监听器会调用所有牌堆界面类的绘制操作,对于桌面进行重新绘制。
GameModel提供了设置游戏难度的方法,可以将游戏设置成不同的难度。
**
**
(1)定义一个CardImage类
里面定义两个方法,一个根据对应的card获取牌的图片,另一个获取牌反面的图片。
(2)游戏的界面使用javafx进行绘制
将每一个牌堆的界面都设计为一个界面类,共有SuitPileView(花色堆界面类),DescPileView(发牌堆界面类),DiscardPileView(丢弃堆界面类),TablePileView(桌面堆界面类)。
DescPileView(发牌堆界面类)
发牌堆界面类继承字Hbox,然后每次初始化时,初始化一个按钮,按钮的外观是纸牌的背面,并且给按钮设置点击事件,每点击一次发牌堆的纸牌,就会将发牌堆顶端的牌放入丢弃堆,实现发牌功能。
DiscardPileView(丢弃堆界面类)
里面卡牌都正面朝上放在一起,每次拖走一张会显示下面的一张,如果没有牌时牌堆为空,每次点击发牌,都会增加一张。
SuitPileView(花色堆界面类)
每次有一个牌放入花色堆,都会在界面上绘制出来。
TablePileView(桌面堆界面类)
在初始化时,根据每张牌的正反进行初始化,如果是正,则屏幕上显示正面,如果是反面,则屏幕上显示是卡牌的背面呢,可以支持牌堆互相之间的拖动,如果符合规则会保留在上面。
(3)下面说明牌堆拖动的逻辑
以上所有牌堆的拖动逻辑大致相同,只是放置的规则不同,调用的是不同的函数判断是否可以放置,可以为每一张image进行,设置拖动的事件。拖动事件有以下几种触发方式:
setOnDragOver:当你拖动到目标上方时会执行。
setOnDragEntered:当你进入目标控件时会执行
setOnDragExited:当你移出一个目标控件时会执行
setOnDragDropped:当你移动到目标控件,并且松开鼠标时会执行
setOnDragDetected:当你在某个控件上移动时,会监测到拖动操作,执行这个函数。
setOnDragDetected中的判断逻辑,当鼠标拖动这个项目时,初始化一个拖放操作,然后创建一个剪切板,将你拖动的所有牌,获取它对应的名称(初始化card类时,创建了一个fnial的静态数组,为每个牌初始化了一个名称),然后将他们拼接在一起,放到剪切板中,以便其他牌堆获取拖动内容。
setOnDragOver中的判断逻辑:如果你拖动一个牌堆到另一个牌堆上方时,会不断执行这个函数,他会根据规则判断此次拖动是否合法(根据桌面堆和花色堆不同的规则),如果合法则返回true,可以进行拖动操作。
setOnDragExited中逻辑,当一堆牌移动出目标控件时,会执行这个调用,设置卡牌的效果。
setOnDragEntered,当牌堆进入另一个牌堆时,会执行这个调用,为卡牌设置一些效果。
setOnDragDropped:当你移动到目标控件,并且松开鼠标时会执行,根据之前移动是否合法的判断,来看是否可以放置在这个牌堆上,它在两个牌堆之间传递了剪切板上的内容,首先将剪切板上的内容,转化为一个牌堆,如果可以移动,就在目的牌堆中添加这个牌堆,在原来牌堆中删除这个牌堆,否则不进行任何操作。
(4)定义了一个接口GameModelListener
里面定义了一个方法gameStateChanged() 表示界面做了更新。
我们让SuitPileView(花色堆界面类),DescPileView(发牌堆界面类),DiscardPileView(丢弃堆界面类),TablePileView(桌面堆界面类)都实现这个接口。然后里面调用了更新各自界面的方法。
在GameModel中初始化了一个arraylist 存放的GameModelListener为泛型的变量,然后将每一个初始化的牌堆都加入这个arraylist中,当有界面更新时,通过遍历这个arraylist就可以调用所有界面内的gameStateChanged()的函数,实现界面的同步更新。
(5)最终在主界面Solitaire_GUI中对于以上类进行初始化以及统一的绘制,主界面使用GridPand进行设计,GridPand是一种网格的布局方式,卡牌正好和网格相对应,每一个牌堆可以放在不同的网格之中,所以之后的牌堆都被初始化在了一个个的网格中,会比较整齐。
在Solitaire_GUI中对于每个牌堆进行了初始化,共初始化了13个牌堆的View界面,还有调整游戏难度,以及重新发牌的按钮。
代码实现
具体的代码看github
https://github.com/ccclll777/Windows_Solitaire_game
一.有关Rank和Suit枚举类的实现
Rank为卡牌大小的枚举类,从ACE-KING
Suit为卡牌花色的枚举类。
二.有关card_Interface接口和card类的实现
Card_Interface定义了五个属性,分别为牌的花色Suit,大小Rank,牌面是否朝上faceUp,以及牌面是否为红色isRed。
定义了卡牌的五个方法,‘
获取花色getSuit()
获取卡牌大小getRank()
牌面是否朝上isFaceUp()
牌面是否为红色isRed()
还有设置牌是否正面朝上setFaceUp()。
Card类实现了接口中的方法,并且增加了一个final static的数组CARDS,在初始化时会初始化所有的52张牌到一个二维数组中。
并且可以通过花色和大小获取相应的牌 Card get(Rank pRank, Suit pSuit)
以及根据编号获取相应的牌Card get(String pId)
private final Rank aRank;private final Suit aSuit;private boolean faceUp = false ;private boolean isRed ; private static final Card[][] CARDS = new Card[Suit.values().length][]; static { for ( Suit suit : Suit.values() ) { CARDS[suit.ordinal()] = new Card[Rank.values().length]; for ( Rank rank : Rank.values() ) { CARDS[suit.ordinal()][rank.ordinal()] = new Card(rank, suit); } } } public Card (Rank pRank, Suit pSuit) { aRank = pRank; aSuit = pSuit; if (pSuit == Suit.HEARTS || pSuit == Suit.DIAMONDS) { isRed = true ; } else { isRed = false ; } } public String getIDString () { return Integer.toString(getSuit().ordinal() * Rank.values().length + getRank().ordinal()); } public boolean isFaceUp () { return faceUp; } public void setFaceUp (boolean faceUp) { this .faceUp = faceUp; } public boolean isRed () { return isRed; } public Suit getSuit () { return this .aSuit; } public static Card get (String pId) { if (pId != null ) { int id = Integer.parseInt(pId); return get(Rank.values()[id % Rank.values().length], Suit.values()[id / Rank.values().length]); } return null ; } public static Card get (Rank pRank, Suit pSuit) { if ( pRank != null && pSuit != null ) { return CARDS[pSuit.ordinal()][pRank.ordinal()]; } return null ; } public Rank getRank () { return this .aRank; }
三.牌堆接口以及牌堆类(Card_Interface 和CardStack以及它的子类。)
(1)CardStack_Interface中定一个一个属性pokers_card,里面需要存这个牌堆的所有卡牌。
然后定义了以下方法:
int size();获取牌堆的发小
ArrayList getPokers_card();获取牌堆的实例
void init(Card pCard);向牌堆顶端加一张牌(表示桌面堆最下面的一张)
Card peek();获取最顶端的牌
Card peek(int index);//获取指定位置的牌
boolean isEmpty();牌堆是否为空
void push(CardStack pStack, int index);在指定位置添加一组牌
void pop(Card pCard);删除一张卡牌
void pop();删除顶端卡牌
void clear();清空牌堆。
这里的peek()方法和pop()方法都实现了重载解析,他有不同的参数列表
(2)CardStack类实现了CardStack_Interface接口。
实现了它的所有方法
public CardStack () { pokers_card = new ArrayList<Card>(); } public int size () { return this .pokers_card.size(); } public ArrayList<Card> getPokers_card () { return this .pokers_card; } public Card peek () { if (this .pokers_card.size() >0 ) { return this .pokers_card.get(this .pokers_card.size() - 1 ); } else { return new Card(Rank.ACE,Suit.HEARTS) } } public void init (Card pCard) { this .pokers_card.add(pCard); } public Card peek (int index) { if (index >= 0 && index < size()) { return this .pokers_card.get(index); } return null ;}public boolean isEmpty () { return pokers_card.size() == 0 ; } public void push (CardStack pStack, int index) { for (int i = index; i < pStack.size(); i++) { this .pokers_card.add(pStack.peek(i)); } } public void push (CardStack pStack) { for (int i = 0 ; i < pStack.size(); i++) { this .pokers_card.add(pStack.peek(i)); } } public void pop (Card pCard) { if (!isEmpty()) { for (int i = 0 ; i <this .pokers_card.size() ; i++) { if (pCard.getSuit() == this .pokers_card.get(i).getSuit() &&pCard.getRank() == this .pokers_card.get(i).getRank() ) this .pokers_card.remove(i); } } } public void pop () { if (!isEmpty()) this .pokers_card.remove(pokers_card.size()-1 ); } public void clear () { this .pokers_card.clear(); }
(3)DescStack继承自CardStack类,它继承了来自CardStack的所有方法,由于发牌堆与普通牌堆没有区别,都是背面朝上的牌堆,所以不需要对牌堆进行重写。
(4)DiscardStack继承自CardStack类,它继承了来自CardStack的所有方法,由于丢弃堆最顶端的牌需要正面朝上,所以它重写了父类的push方法,需要在添加卡牌后将牌堆的第一张牌的isfaceup设置成true。
public void push (CardStack pStack, int index) { super .push(pStack, index); if (!this .isEmpty()) this .peek().setFaceUp(true ); }
(5)SuitStack继承自CardStack类,它继承了来自CardStack的所有方法
,由于花色堆的所有牌的正面都是朝上的,所以重写了父类的push方法,在添加卡牌后,将添加的所有卡牌的正面全部朝上。
public void push (CardStack pStack, int index) { super .push(pStack, index); if (!this .isEmpty()) { this .peek().setFaceUp(true ); } }
(6)TableStack继承自CardStack类,它继承了来自CardStack的所有方法
由于桌面堆需要获取子牌堆(一组牌可以同时拖动,可以将他们一同获取),所以需要添加一个获取子牌堆的方法。
我们需要获取一张牌是这个牌堆中的第几个,所以添加一个获取这个牌是牌堆中第几个的方法。(其实第二个方法完全为第一个方法服务)
public int getCardIndex (Rank pRank, Suit pSuit) { if (!this .isEmpty()) { for (int i = this .size() -1 ; i >= 0 ; i--) { if (this .peek(i).getRank() == pRank && this .peek(i).getSuit() == pSuit) return i; } } return -1 ; } public TableStack getSubStack (Card pCard, TableStack stack) { if (pCard != null ) { int index = stack.getCardIndex(pCard.getRank(),pCard.getSuit()); TableStack temp_stack = new TableStack(); for (int i = index ; i< stack.size() ; i++) { temp_stack.init(stack.peek(i)); } return temp_stack; } return null ; }
四.牌堆的主要拖动的逻辑实现类GameModel
GameModel中对十三个牌堆进行了实例化, 然后将它们存入一个泛型为CardStack的ArrayList中。这样已经体现出了面向对象的优越性,可以将子类的对象放入父类变量的泛型之中,便于管理。
并且GameModel是一个final类,在程序编译时就已经初始化,随机出了桌面堆和发牌堆的所有纸牌,在GameModel实现了对于纸牌拖动的一些逻辑的处理过程。
GamoModel中的属性:
private static final GameModel INSTANCE = new GameModel();在final类中实例化了一个GameModel的变量,然后使用静态方法,将它返回到其他类中,实现通过类名使用GameModel中的函数。
private ArrayList card_Stacks 存放十三个牌堆ArrayList.
private int fromIndex;牌堆从哪里移动的
private DeckStack deck_Stack;//0 //ArrayList中的第1个牌堆,发牌堆
private DisCardStack disCard_Stack;//1ArrayList中的第2个牌堆,丢弃堆
private TableStack[] table_Stacks;//2-8 ArrayList中的第2个到第8个牌堆,桌面堆
private SuitStack[] suit_Stacks;//9-12 ArrayList中的第9个到第12个牌堆,花色堆
private static final String SEPARATOR = “;”;//进行字符串分割的符号
private String level = “low”;//设置游戏难度值的符号。
private final List aListeners = new ArrayList(); //存放GameModelListener类的ArrayList,作为所有牌堆的监听器,实现触发后界面的更新。
GameModel中实现的方法:
Void init():初始化十三个牌堆,给桌面堆和玩牌堆随机发牌。将他们每一个牌堆按顺序存入ArrayList中,之后根据序号访问不同的牌堆。
CardStack getStack()可以根据序号从ArrayList中取出自己需要的牌堆,然后返回。实现了内部数据的保护。
GameModel instance()//在初始化时,初始化了GameModel的实例,通过这个静态方法,将GameModel的实例返回,在类外,可以通过类名+函数名调用这个方法,获取GameModel的实例,使用GameModel的函数,方便用户的使用。
void addListener() 为每一个牌堆的界面类设置监听器。
void notifyListeners() 调用时,调用每一个牌堆更新界面的方法,实现界面的同步更新。
CardStack getSubStack() 获得一个桌面堆的子牌堆,在拖动时可以拖动一堆卡牌。
boolean isLegalMove() 判断牌在牌堆之间的移动是否符合规则。
boolean canMoveToTableStack () 判断移动到桌面堆的牌堆是否符合规则。
boolean canMoveToSuitStack() 判断移动到花色堆的牌是否符合规则
void Desc_to_DisCard() 模拟发牌,每次点击发牌按钮,将一张牌从发牌堆加入丢弃堆
boolean moveCard() 牌堆移动的主要逻辑,如果一组卡牌能在两个牌堆之间移动,那么通过调用这个类,将这一组卡牌从起始牌堆删除,然后添加到目的牌堆
void pop_from() 将一组卡牌从起始牌堆删除
void reset_all() 当发牌堆和丢弃堆都没有卡牌时,重新发牌,开始一局新的游戏。
void reset() 当丢弃堆还有卡牌,发牌堆没有卡牌时,将丢弃堆的牌重新放入发牌堆,重新发牌。
CardStack StringToStack 由于需要移动的卡牌在牌堆间的传递,是通过在剪切板中存放字符串实现牌堆间信息的传递的,这里将字符串中表示的卡牌转化成真实的牌堆。
Card getTop()获取剪切板中字符串表示的卡牌中,最顶端的一张卡牌。
String serialize()//将牌堆转换为字符串的方法,传入一组牌堆,通过过去每一张牌的唯一名字,获取卡牌的名称,拼接在字符串中。
void setFromIndex()设置牌堆拖动时的起始牌堆
setLevel(String level) 设置游戏的难度
public void init () { this .table_Stacks = new TableStack[7 ]; for (int i = 0 ; i < table_Stacks.length ; i++) { this .table_Stacks[i] = new TableStack(); } this .deck_Stack = new DeckStack(); this .disCard_Stack = new DisCardStack(); this .suit_Stacks = new SuitStack[4 ]; for (int i = 0 ; i< suit_Stacks.length ; i++) { this .suit_Stacks[i] = new SuitStack(); } this .card_Stacks = new ArrayList<CardStack>(); this .card_Stacks.add(this .deck_Stack); this .card_Stacks.add(this .disCard_Stack); for (int i = 0 ; i < table_Stacks.length ; i ++) { this .card_Stacks.add(this .table_Stacks[i]); } for (int i = 0 ; i < suit_Stacks.length ; i ++) { this .card_Stacks.add(this .suit_Stacks[i]); } Random random = new Random(); ArrayList<Card> normal_Rank = new ArrayList(); Card temp_card = null ; for ( Suit suit : Suit.values() ) { for ( Rank rank : Rank.values() ) { temp_card = new Card(rank, suit); normal_Rank.add(temp_card); } } int i; int index = 0 ; for ( i = 0 ; i < 7 ; ++i) { for (int j = 0 ; j <= i; ++j) { while (true ) { index = random.nextInt(normal_Rank.size()); temp_card = (Card)normal_Rank.get(index); if ( !this .table_Stacks[i].isEmpty() && temp_card.isRed() != this .table_Stacks[i].peek().isRed()) { this .table_Stacks[i].init(temp_card); normal_Rank.remove(index); break ; } if (this .table_Stacks[i].isEmpty() ) { this .table_Stacks[i].init(temp_card); normal_Rank.remove(index); break ; } } } } for (i = 0 ; i < 7 ; ++i) { this .table_Stacks[i].peek().setFaceUp(true ); } for (i = 0 ; i < 24 ; ++i) { index = random.nextInt(normal_Rank.size()); temp_card = (Card)normal_Rank.get(index); this .deck_Stack.init(temp_card); normal_Rank.remove(index); } } public CardStack getStack (int index) { return this .card_Stacks.get(index); } public static GameModel instance () { return INSTANCE; } public void addListener (GameModelListener pListener) { if (pListener != null ); aListeners.add(pListener); } private void notifyListeners () { for ( GameModelListener listener : aListeners ) { listener.gameStateChanged(); } } public CardStack getSubStack (Card pCard, int aIndex) { TableStack stack = (TableStack) GameModel.instance().getStack(aIndex); TableStack temp_stack = stack.getSubStack(pCard,stack); return temp_stack; } public boolean isLegalMove (Card pCard,int aIndex ) { if (aIndex >=1 && aIndex<= 8 ) { return canMoveToTableStack(pCard,aIndex); } else if (aIndex>= 9 && aIndex <=12 ) { return canMoveToSuitStack(pCard,aIndex); } return false ; } public boolean canMoveToTableStack (Card pCard,int aIndex) { if (level == "high" ) { if (pCard!=null ) { CardStack temp_stack = getStack(aIndex); if ( temp_stack.isEmpty() ) { return pCard.getRank() == Rank.KING; } else { return pCard.getRank().ordinal() == temp_stack.peek().getRank().ordinal()-1 && pCard.isRed() != temp_stack.peek().isRed(); } } } else if (level == "low" ) { if (pCard!=null ) { CardStack temp_stack = getStack(aIndex); if ( temp_stack.isEmpty() ) { return true ; } else { return pCard.getRank().ordinal() == temp_stack.peek().getRank().ordinal()-1 && pCard.isRed() != temp_stack.peek().isRed(); } } } return false ; } public boolean canMoveToSuitStack (Card pCard,int aIndex) { assert pCard != null ; CardStack temp_stack = getStack(aIndex); if (temp_stack.isEmpty()) { return pCard.getRank() == Rank.ACE ; } else { return pCard.getRank().ordinal() == temp_stack.peek().getRank().ordinal()+1 && pCard.getSuit()==temp_stack.peek().getSuit(); } } public void Desc_to_DisCard () { Card temp_card = this .getStack(0 ).peek(); this .getStack(1 ).init(temp_card); this .getStack(0 ).pop(); notifyListeners(); } public boolean moveCard (CardStack from,int aIndex) { if (aIndex>=2 && aIndex<=8 ) { TableStack to = (TableStack)this .getStack(aIndex); if (!to.isEmpty()) { this .getStack(aIndex).push(from); pop_from(from); notifyListeners(); return true ; } else if (from.isEmpty()) { return false ; } else { this .getStack(aIndex).push(from); pop_from(from); notifyListeners(); } } else if (aIndex>=9 && aIndex<= 12 ) { SuitStack to = (SuitStack) this .getStack(aIndex); if (to.isEmpty()) { this .getStack(aIndex).push(from); pop_from(from); notifyListeners(); return true ; } else { this .getStack(aIndex).push(from); pop_from(from); notifyListeners(); } } return true ; } public void pop_from (CardStack to) { for (int j = 0 ; j < to.size() ; j ++) { this .getStack(fromIndex).pop(to.peek(j)); } if (!this .getStack(fromIndex).peek().isFaceUp()) { this .getStack(fromIndex).peek().setFaceUp(true ); } } public void reset_all () { init(); notifyListeners(); } public void reset () { for (int i = 0 ; i < this .getStack(1 ).size() ; i++) { this .getStack(0 ).init(this .getStack(1 ).peek(i)); } this .getStack(1 ).clear(); notifyListeners(); } public CardStack StringToStack (String pString) { if (pString != null && pString.length() > 0 ) { String[] tokens = pString.split(SEPARATOR); CardStack aCards = new CardStack(); for ( int i = 0 ; i < tokens.length; i++ ) { aCards.init(Card.get(tokens[i])); } for (int i = 0 ; i<aCards.size() ; i ++) { aCards.peek(i).setFaceUp(true ); } return aCards; } return null ; } public Card getTop (String result) { if ( result != null && result.length() > 0 ) { String[] tokens = result.split(SEPARATOR); Card aCards []; aCards = new Card[tokens.length]; for ( int i = 0 ; i < tokens.length; i++ ) { aCards[i] = Card.get(tokens[i]); } return aCards[0 ]; } return null ; } public String serialize (Card pCard, int aIndex) { CardStack temp_stack = GameModel.instance().getSubStack(pCard, aIndex); String result = "" ; Card temp_card ; for (int i = 0 ; i< temp_stack.size() ; i++) { temp_card = temp_stack.peek(i); result += temp_card.getIDString()+SEPARATOR; } if ( result.length() > 0 ) { result = result.substring(0 , result.length()-1 ); } return result; } public void setFromIndex (int fromIndex) { this .fromIndex = fromIndex; } public void setLevel (String level) { this .level = level; }
五.主要牌堆界面的实现类
首先定义了一个接口GameModelListenter 作为对于所有牌堆更新的监听器,里面定义了一个方法gameStateChanged,为更新牌堆的方法。
然后桌面堆,花色堆,发牌堆,丢弃堆都实现了这个接口,并且定义了方法 gameStateChanged。
这样做的用意是,在GameModel中定义一个泛型GameModelListenter为ArrayList,在每一个牌堆内,由于每个牌堆类都实现了GameModelListenter 的接口,所以可以将他们都加入到ArrayList中,通过 这种方式,如果要触发牌堆的更新,则遍历这个ArrayList中的每一个对象,然后调用他们的gameStateChanged方法更新界面,实现了界面的同步更新。
由于初始化ArrayList的每一个对象属于不同的类,定义了不同的gameStateChanged,调用时,通过多态的思想,会寻找到属于这个对象对应的gameStateChanged方法进行调用。
之后对于每一个界面类,都有自己界面的绘制方法,绘制方法都大致相同。
对于DescStack,将纸牌的背面初始化为按钮,为按钮设置点击事件,每次点击按钮,都会发一张牌到丢弃堆。
对于每一个牌堆的拖动的检测,共有五个回调函数。
createDragDetectedHandler()当你在某个控件上移动时,会监测到拖动操作,执行这个函数。
createDragOverHandler()当你拖动到目标上方时会执行。
createDragEnteredHandler()当你进入目标控件时会执行
createDragExitedHandler()当你移出一个目标控件时会执行
createDragDroppedHandler()当你移动到目标控件,并且松开鼠标时会执行
这五个函数之间配合,实现可卡牌的拖动逻辑。
TablePileView的实现如下,其他界面类的实现差不多:
public class TablePileView extends StackPane implements GameModelListener { private static final int PADDING = 5 ; private static final int Y_OFFSET = 17 ; private static final String SEPARATOR = ";" ; private static CardImages cardImages = new CardImages(); private int aIndex; private static final String BORDER_STYLE = "-fx-border-color: lightgray;" + "-fx-border-width: 2.8;" + " -fx-border-radius: 10.0" ; TablePileView(TablePile pIndex) { aIndex = pIndex.ordinal()+2 ; setPadding(new Insets(PADDING)); setStyle(BORDER_STYLE); setAlignment(Pos.TOP_CENTER); final ImageView image = new ImageView(cardImages.getBack()); image.setVisible(false ); getChildren().add(image); buildLayout(); GameModel.instance().addListener(this ); } private static Image getImage (Card pCard) { return cardImages.getCard(pCard); } private void buildLayout () { getChildren().clear(); TableStack stack = (TableStack) GameModel.instance().getStack(aIndex); if ( stack.isEmpty() ) { ImageView image = new ImageView(cardImages.getBack()); image.setVisible(false ); getChildren().add(image); return ; } for (int i = 0 ; i< stack.size() ; i ++) { Card cardView = stack.peek(i); if (cardView.isFaceUp() == true ) { final ImageView image = new ImageView(getImage(cardView)); image.setTranslateY(Y_OFFSET * i); getChildren().add(image); setOnDragOver(createDragOverHandler(image, cardView)); setOnDragEntered(createDragEnteredHandler(image, cardView)); setOnDragExited(createDragExitedHandler(image, cardView)); setOnDragDropped(createDragDroppedHandler(image, cardView)); image.setOnDragDetected(createDragDetectedHandler(image, cardView)); } else { final ImageView image = new ImageView(cardImages.getBack()); image.setTranslateY(Y_OFFSET * i); getChildren().add(image); } } } private EventHandler<MouseEvent> createDragDetectedHandler (final ImageView pImageView, final Card pCard) { return new EventHandler<MouseEvent>() { @Override public void handle (MouseEvent pMouseEvent) { Dragboard db = pImageView.startDragAndDrop(TransferMode.ANY); ClipboardContent content = new ClipboardContent(); content.putString(GameModel.instance().serialize(pCard,aIndex)); db.setContent(content); pMouseEvent.consume(); GameModel.instance().setFromIndex(aIndex); } }; } private EventHandler<DragEvent> createDragOverHandler (final ImageView pImageView, final Card pCard) { return new EventHandler<DragEvent>() { @Override public void handle (DragEvent pEvent) { if (pEvent.getGestureSource() != pImageView && pEvent.getDragboard().hasString()) { if ( GameModel.instance().isLegalMove(GameModel.instance().getTop(pEvent.getDragboard().getString()), aIndex) ) { pEvent.acceptTransferModes(TransferMode.MOVE); } } pEvent.consume(); } }; } private EventHandler<DragEvent> createDragEnteredHandler (final ImageView pImageView, final Card pCard) { return new EventHandler<DragEvent>() { @Override public void handle (DragEvent pEvent) { if ( GameModel.instance().isLegalMove(GameModel.instance().getTop(pEvent.getDragboard().getString()), aIndex) ) { pImageView.setEffect(new DropShadow()); } pEvent.consume(); } }; } private EventHandler<DragEvent> createDragExitedHandler (final ImageView pImageView, final Card pCard) { return new EventHandler<DragEvent>() { @Override public void handle (DragEvent pEvent) { pImageView.setEffect(null ); pEvent.consume(); } }; } private EventHandler<DragEvent> createDragDroppedHandler (final ImageView pImageView, final Card pCard) { return new EventHandler<DragEvent>() { @Override public void handle (DragEvent pEvent) { Dragboard db = pEvent.getDragboard(); boolean success = false ; if (db.hasString()) { boolean a = GameModel.instance().moveCard(GameModel.instance().StringToStack(db.getString()), aIndex); System.out.println(a); success = true ; ClipboardContent content = new ClipboardContent(); content.putString(null ); db.setContent(content); } pEvent.setDropCompleted(success); pEvent.consume(); } }; } public void gameStateChanged () { buildLayout(); } }
悔牌的逻辑;将每一步的操作保存到堆栈中,然后每次点击一次悔牌按钮,都让一个arraylist出栈,然后实现了悔牌功能。
if (!listStack.isEmpty()) { ArrayList<CardStack> temp_list = listStack.peek(); listStack.pop(); ArrayList<Integer> flag = new ArrayList<Integer>(); for (int i = 2 ; i<9 ; i++) { if (this .card_Stacks.get(i).size() != temp_list.get(i).size()) { flag.add(i); } } if (flag.size() == 1 ) { if (this .card_Stacks.get(flag.get(0 )).size() <temp_list.get(flag.get(0 )).size() &&this .card_Stacks.get(flag.get(0 )).size() >0 && temp_list.get(flag.get(0 )).size() > 1 ) { if (temp_list.get(flag.get(0 )).peek(temp_list.get(flag.get(0 )).size() - 2 ).isFaceUp()) { temp_list.get(flag.get(0 )).peek(temp_list.get(flag.get(0 )).size()-2 ).setFaceUp(false ); } } } if (flag.size() ==2 ) { if (this .card_Stacks.get(flag.get(0 )).size() -temp_list.get(flag.get(0 )).size() == 1 ) { temp_list.get(flag.get(1 )).peek(temp_list.get(flag.get(1 )).size()-2 ).setFaceUp(false ); } if (this .card_Stacks.get(flag.get(1 )).size() -temp_list.get(flag.get(1 )).size() ==1 ) { temp_list.get(flag.get(0 )).peek(temp_list.get(flag.get(0 )).size()-2 ).setFaceUp(false ); } if (this .card_Stacks.get(flag.get(0 )).size() - temp_list.get(flag.get(0 )).size() >=2 ) { int i = this .card_Stacks.get(flag.get(0 )).size() - temp_list.get(flag.get(0 )).size(); temp_list.get(flag.get(1 )).peek(temp_list.get(flag.get(1 )).size()-i-1 ).setFaceUp(false ); } if (this .card_Stacks.get(flag.get(1 )).size() -temp_list.get(flag.get(1 )).size() >=2 ) { int i = this .card_Stacks.get(flag.get(1 )).size() - temp_list.get(flag.get(1 )).size(); temp_list.get(flag.get(0 )).peek(temp_list.get(flag.get(0 )).size()-i-1 ).setFaceUp(false ); } } for (int i = 0 ; i< 13 ; i++) { this .card_Stacks.get(i).clear(); } for (int i = 0 ; i< 13 ; i++) { for (int j = 0 ; j <temp_list.get(i).size() ; j++) { this .card_Stacks.get(i).init(temp_list.get(i).peek(j)); } } notifyListeners(); } else { return ; }
结果展示