我正在为带有纸牌的游戏设计课程。请查看我的代码。


每个玩家都可以得到纸牌之手
甲板上的牌可以被洗掉,并且可以从甲板上一次发一张牌,然后添加到玩家手中。

具体地说,我对dealCard类中的方法Deck的良好方法签名感到困惑。我可以将播放器方法传递给-void dealCard(Player player)之类的方法,也可以获取卡并添加到播放器-Card dealCard()的手中。根据OOPS的实践,以下哪种实施方式是好的?我在这种情况下如何看待?任何建议都可能会有所帮助

    package main;
        //Each card has a value and a suite.
    public class Card {
        int value;
        SUITE suite;

        public Card(int value, SUITE suite) {
            this.value = value;
            this.suite = suite;
        }

        public int getValue() {
            return value;
        }

        public SUITE getSuite() {
            return suite;
        }

        public void setValue(int value) {
            this.value = value;
        }

        public void setSuite(SUITE suite) {
            this.suite = suite;
        }

        @Override
        public String toString() {
            return "Card{" +
                    "value=" + value +
                    ", suite=" + suite +
                    '}';
        }
    }



package main;    
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;

public class Deck {
    List<Card> cardDeck;

    public Deck() {
        this.cardDeck = new ArrayList<Card>();
        for(int value = 1 ; value <= 13 ; value++){
            for(SUITE suite : SUITE.values()){
             cardDeck.add(new Card(value,suite));
            }
        }
    }

    @Override
    public String toString() {
        return "Deck{" +
                "cardDeck=" + cardDeck +
                '}';
    }

    public void shuffle(){
        Random rand = new Random();
        //Generate two random numbers between 0 to 51
        for(int i = 0 ; i < 20 ; i ++){
            int firstCard = rand.nextInt(52);
            int secondCard = rand.nextInt(52);
            Collections.swap(cardDeck,firstCard,secondCard);
        }
    }

    public void dealCard(Player player){
       //Get next card and add to hand of the player
        Card removedCard = cardDeck.remove(0);
        player.getHand().add(removedCard);
    }

    public Card dealCard(){
        Card removedCard = cardDeck.remove(0);
        return removedCard;
    }

    //Size of the deck for testing purpose
    public int getSizeOfDeck(){
        return cardDeck.size();
    }
}



package main;    
import java.util.ArrayList;
import java.util.List;

public class Player {
    List<Card> hand;

    public Player() {
        this.hand = new ArrayList<Card>();
    }

    public List<Card> getHand() {
        return hand;
    }

    @Override
    public String toString() {
        return "Player{" +
                "hand=" + hand +
                '}';
    }
}



package main;

public enum SUITE {
    HEART,
    SPADE,
    CLUB,
    DIAMOND
}

package main;    
public class Main {
    public static void main(String args[]) {
        Deck deck = new Deck();
        System.out.println(deck);
        System.out.println("Size of deck is: "+deck.getSizeOfDeck());
        deck.shuffle();
        System.out.println("Deck after shuffling is "+deck);

        Player player1 = new Player();
        Player player2 = new Player();

        deck.dealCard(player1);
        System.out.println("Size of deck after dealing a card to player is "+deck.getSizeOfDeck());
        deck.dealCard(player2);
        System.out.println("Size of deck after dealing a card to player is "+deck.getSizeOfDeck());

        System.out.println("Hand of player 1 is "+player1.getHand());
        System.out.println("Hand of player 2 is "+player2.getHand());

        player1.getHand().add(deck.dealCard());
    }
}


评论

供参考,它是“西装”,而不是“西装”。

非常糟糕的随机播放。

此外,随机播放中还有一个错误。您假设套牌中有52张卡片,如果抽出了任何卡片,可能会抛出界外错误。您也可以将2张卡套组合到卡组中,从而允许52张以上的卡(例如在赌场中)

这是您控制设计的任务还是项目?

#1 楼

在那么小的空间中有太多话要说。可以通过多种方式回答此问题,这主要是由于您需要对卡进行建模的原因。没有适当的上下文,需求将发生变化,因此可能会更改整个模型。因此,假设您打算构建一个(或多个)纸牌游戏。

关注点分离

有很多重要的OOP原则,但我主要是为了解决关注点分离(或与计算机科学相关的关注点,因为通常这是您首先需要考虑的问题:为什么拥有类/对象,以及它们的预期目的是什么?

经过某种简化可以说:一个对象/类不必关心自身之外的东西。这可以允许替换任何类的内部,只要公共API(调用的公共方法)它)保持不变。我将尝试举例说明与您提供的各种类相关的内容。

Card

换句话说,应该将Card不必担心自己可能会使用它以及如何使用它。处理卡的基本方法应该保持不变。

更具体地讲,这似乎有些奇怪Card可以更改其值或适合。什么样的游戏卡可以做到这一点?如果它是钻石之王,那么它通常永远保持这种状态。

对我来说,让卡具有定义输出外观的方法会更加自然。例如,选择其他文本变体:“值:13,花色:黑桃”,“黑桃王”,“ K♠”甚至“🂮”。最后两个示例来自:Wikipedia的Unicode打牌。

父级Card可能应该为空,并允许子类指定该卡的标准。像普通的纸牌,Uno纸牌或Pokemon纸牌等。

Deck

DeckCard组成,但是除了基本的卡处理外,对卡的了解应该不多。而且,使用Deck对PlayerGame也不应该有任何了解。

这将回答您在dealCard()上的问题。它对播放器一无所知,基本上只应该从卡组中取出Card,然后将其退还给可能随便使用的人。

Deck的其他有趣功能可能是将卡重新插入到卡座中,并可能验证卡在这种卡座中是否合法。想像蜘蛛纸牌游戏(仅使用Spade套件)或使用双层游戏。


PlayerHand吗?

我不是确定我会在Player类中添加什么,但是在纸牌游戏的背景下,我想我更喜欢使用Hand类来描述当前的纸牌集。

在这种情况下,很自然地向手添加/移除卡。并以各种替代方式进行显示。相对于另一手牌,它可能有一些通用的方法来描述一手牌的顺序。

对于子类化,也可以想象该手牌可以计算出该手牌的整体价值,例如扑克价值,或者是否包含组或系列,我可以想象有一些方法可以计算我的手的价值,例如扑克手的价值,或者我是否有任何系列或组。子类可以指定哪只手彼此之间最有价值。

代码回顾


通常,您的代码看起来不错,带有适当的缩进和括号用法。我希望在与定义相关的给定位置注释方法/类时,保持一点一致性。您可以/应该考虑使用Javadoc或给定的文档标准来描述您的类。
一个shuffle()方法每次只能交换2张卡,只能随机播放20张卡,这似乎是一种很糟糕的随机播放卡组的方法。我相信您可以做得更好。我想也许可以在卡座中循环一次,然后将卡片放置位置切换到卡座中的其他地方?
更好的演示方法会更好。请参阅上面有关Card类的评论。
有关SUIT枚举的次要细节。我将其订购为SPADEHEARTDIAMONDCLUB,因为这是很多纸牌游戏中纸牌价值的常见排序方式。并不是完全为Uppercasing感到兴奋,但是不确定Java中用来表示此类全局变量的通用方法是什么。

PS!我已经使用Wikipedia作参考,确实存在更好的描述,但是为了保持一致,我选择在这里使用它们。

PPS!另一个有趣的原理是SOLID原理,但是如果您是编程的新手,可能很难将其扎根。

评论


\ $ \ begingroup \ $
和随机数一样,很容易出错。因此,我建议使用已知的算法进行混洗,例如Fisher-Yates。
\ $ \ endgroup \ $
–艾米莉·L。
17年4月25日在22:43

\ $ \ begingroup \ $
@EmilyL。只需调用Collections.shuffle
\ $ \ endgroup \ $
– Pete Kirkham
17年4月26日在12:23

\ $ \ begingroup \ $
@PeteKirkham我的评论主要是回复一次Maybe在卡座中循环一次,然后将卡片放置位置切换到卡座中的其他位置吗?指出要想出一个好的算法比想象的要难。当然,Collections.shuffle。应该使用(可能是Fisher-Yates)。
\ $ \ endgroup \ $
–艾米莉·L。
17年4月26日在13:17

\ $ \ begingroup \ $
要使用一些适当的行话,发牌应包括两个操作,第一个操作是从卡组中抽出一张牌。应通过从交易重命名功能来确保清晰度!
\ $ \ endgroup \ $
–user14393
17-4-27 4:00



#2 楼



我认为,如果Deck构造函数不实例化Cards而是接受Cards,那就更好了:

public Deck(final List<Card> cards) {
    this.cardDeck = cards;
}


对象rand的范围可以扩大,我认为您不必每次都创建一个新的Random对象,这应该可以为您提供更好的分配。 (就我所知道的情况,我不确定我提出的关于更好分配的最后一点。)

private final Random rand = new Random();
public void shuffle(){
    //Generate two random numbers between 0 to 51
    for(int i = 0 ; i < 20 ; i ++){
        int firstCard = rand.nextInt(52);
        int secondCard = rand.nextInt(52);
        Collections.swap(cardDeck,firstCard,secondCard);
    }
}


不要盲目地暴露手播放器的外观,如下所示:

public List<Card> getHand() {
    return hand;
}


我认为最好公开所需的功能,例如:

public class Player {
    private final List<Card> hand;

    public Player() {
        this.hand = new ArrayList<Card>();
    }

    public void dealCards(Card... cards) {
        // implementation here..
    }

    public boolean holdsCard(Card card) {
        // implementation here..
    }
}


基本上,每当您看到如下所示的方法链时。.

player.getHand().add(removedCard);


您可以考虑自己的设计。

通常最好使用私有实例变量,因为您似乎对它们具有默认访问权限。 (因此,访问修饰符=默认访问权限。)

总体来说不错。

评论


\ $ \ begingroup \ $
使用Java,即使在同一线程上创建的每个Random对象都在同一毫秒内创建,也具有唯一的种子。
\ $ \ endgroup \ $
–莫妮卡基金的诉讼
17年4月26日在16:53

#3 楼

对于您的随机播放算法,除非您有扎实的统计背景,否则我建议您使用既定算法。由于算法仅交换20对卡,因此您可以确保至少有12张卡保留在其初始位置。在Fischer-Yates混洗中,每张纸牌都有相等的机会被置换,从而导致公平的混洗。

另外,您的SUIT枚举应在主要使用的卡类中定义。在任何情况下都不需要使用没有Card的SUIT,这将消除Card和Main类之间的纠缠。

评论


\ $ \ begingroup \ $
我甚至建议使用Collections.suffle()帮助器方法...
\ $ \ endgroup \ $
–提莫西·卡特勒(Timothy Truckle)
17年4月26日在7:51

\ $ \ begingroup \ $
或只是不洗牌。绘制时只需从集合中选择一个随机条目。
\ $ \ endgroup \ $
–塔米尔
17年4月26日在7:54

\ $ \ begingroup \ $
我正在Deck类中使用SUIT创建初始52张卡。如果我将SUIT移至CARD,我猜不可能
\ $ \ endgroup \ $
–mc20
17年4月26日在8:22

\ $ \ begingroup \ $
@TimothyTruckle是的,重塑方向盘毫无意义
\ $ \ endgroup \ $
– JollyJoker
17年4月26日在12:06

\ $ \ begingroup \ $
@ mc20您应该将Suit枚举移动到Card类,同时使其可以公开访问。由于您已经在Deck中使用Card,因此也可以在Deck中使用Suit。
\ $ \ endgroup \ $
–kcazllerraf
17年4月27日在1:40

#4 楼

我还将添加...

关于包装的思考

没有卡的西服是没用的,所以西服应该与卡在同一个包装或子包装中。玩家可以使用卡,但不需要卡。因此,播放器应位于其他包装中。这对于权限级别很重要,因为您可能希望将Card Set方法设置为Protected。将玩家移至新软件包将使Deck有权重写卡,但不会授予玩家。

请考虑“您需要什么” /“这看起来像什么”

我还要提出一个论点,即您不需要纸牌或手牌,并且两者都是纸牌池。语义在变量名中。

在Shuffle上

我要说改变

for(int i = 0 ; i < 20 ; i ++){
    int firstCard = rand.nextInt(52);




for(int firstCard  = 0 ; firstCard  < cardDeck.length() ; firstCard++){
    int secondCard = rand.nextInt(cardDeck.length());


这样可以确保在随机播放中,至少触摸每张卡一次。由于Deck并非线程安全的,因此我还将使Random对象成为全局对象,以便您可以在实例化时使其一次并重用它。另外,您应该迭代卡组的长度,而不是静态的52,因为我们对卡组的真正了解是它包含0+张卡片。

您还需要考虑“关心线程?”。如果2个线程共享一个卡座,则它们可能会重复或丢弃卡。但是,使Deck Thread-safe也增加了开销,因此您应该只在您认为重要的范围内容纳它。

评论


\ $ \ begingroup \ $
修改后的代码仍然不能确保每张卡至少被触摸一次,因为在所有迭代中,卡被多重选择的可能性非常高。取而代之的是,最好从一个牌组到另一个牌组进行洗牌-然后肯定会碰到每张牌。实现是微不足道的。
\ $ \ endgroup \ $
–肯乔格
17年4月26日在6:46

\ $ \ begingroup \ $
@Konchong每张卡至少被触摸一次。当索引到达该位置时,卡片处于其原始位置(在这种情况下,它将作为firstCard被触摸),或者在较早的索引处被移动(在这种情况下,随后被触摸)
\ $ \ endgroup \ $
–塔米尔
17-4-26在7:53



\ $ \ begingroup \ $
仍然不是一个公平的洗牌;请参阅其他答案的评论以获得更好的建议。
\ $ \ endgroup \ $
– Toby Speight
17年4月26日在14:09

#5 楼

等级,西服和卡片都应该是不变的。我将枚举用于等级,西装和卡片。示例代码在这里。我使用了一个抽象的Cards类,该类包含一系列纸牌,并将其扩展为套牌类和手牌类。

评论


\ $ \ begingroup \ $
使它们不变的原因是什么?
\ $ \ endgroup \ $
–mc20
17-4-26在8:23



\ $ \ begingroup \ $
实际上只有一个俱乐部,一个ace和一个aceOfClubs。他们永远都不会改变。一种简单的方法是使它们成为枚举。如果它们是枚举,则很容易添加其他状态和/或行为。
\ $ \ endgroup \ $
–雷·塔耶克(Ray Tayek)
17年4月26日在9:00