Search code examples
class-designplaying-cards

Design for Card Game


I just had a small question related to design patterns.

Consider Player objects that can hold Card objects.

Player player1;
Player player2;
Player dealer;

Players can give cards to each other. Is there a OO way to design a method that handles this?

player1.giveCard(Card, player2);

It doesn't seem right that player1 could utilize another player's methods. Any thoughts? For instance, should all players have a method called getCard?

Player::giveCard(Card card, Player player) {
player.getCard(card)

}

Solution

  • The beautiful thing about Object Oriented approaches is that there are infinite ways to abstract and design the same concept. The question is which you would like to choose.

    It seems that in this particular instance, your concern with the abstraction that you have described is that player1 needs to use the methods of player2 to accomplish the goal. However, I'd like to change the way you think of methods.

    Methods, in general, should be thought of, first, as our public interfaces with the world. Properties, on the other hand, are those private things that we keep secret and locked up inside our objects/heads/bodies/etc.

    It is true, that many programming languages have private and protected methods that are intended for internal use only, but that is really just to clean up the code so our public methods are not hundreds of lines long.

    For your card game, a Player can be given a card by any other Object, so I would define the public method Player.giveCard( Card ). This is the simple part, which has already been touched on by other answers.

    But the question becomes, what happens inside this method? Also, how does the Card get removed from the original hand?

    We can do several things here, and this list is by no means complete:

    1. The player can interact just with the other player.

    In this situation, player1 chooses to give card_589 to player2. So, player1 calls the method player2.giveCard( card_589 ). In the real world, this would be demonstrated by player1 physically holding the card out for player2 to take. If player2 accepts the card, player1 no longer has it and must remove it from his hand. If player2 does not accept it, then player1 does not loose the card, and puts it back in his hand.

    To model this, we would make one simple rule: the giveCard method returns a boolean result to indicate whether player2 takes the card.... After player1 calls player2.giveCard(), he has no say in whether player2 takes the card, because that is up to player2, in this scenario.

    Our code might look like this somewhere inside player1's functions:

    //begin pseudocode to give card to player identified by player2
    //let self refer to player1's own methods
    Player{
    public giveCardToPlayer( player2, card_id ){
        card = self.removeCardFromHand( card_id ); 
        cardTaken = player2.giveCard( card );
        if( cardTaken === false ){
            self.addCardToHand( card );
        }
    }
    //in the game management system, or elsewhere in player1's code, you then write
    player1.giveCardToPlayer( player2, card_587 );
    //or, if in another method "player1.play()", for example:
    //self.giveCardToPlayer( player2, card_587 )
    //
    //end pseudocode
    

    This is the simplest solution. Here, player1 does not see anything within player2's decision making as to whether card1 gets taken. Player1 chooses to remove the card from his own hand before he hands it over so the card is not in two places at once. If Player2 does not take the card, player1 puts it back in his deck, but otherwise does nothing, because the card is now with player2.

    Personally, this is the simplest way to abstract the model, and my favorite.

    1. The player can interact through some intermediary

    This is my favorite scenario when we are modeling a game that has some sort of delay such as in a computer network simulation, or a chess by mail simulation. Player1 mails the card to player2, but player2 may or may not receive the card. In this game, lets assume you have a table, like a poker table, and any player can put a card down between himself and another player so that only that other player can reach the card.

    For this scenario, we would create a new object called Table, and while there are many ways that we can choose to abstract the placing of the card on the table, I will choose this method as the publicly available interface for the action:

    Table.placeCardForUser( card, userId, myId, securityToken ) : bool
    Table.countCardsOnTableToUserFromUser( userId, myId, securityToken ) : int
    Table.pickUpCardToUser( userId, myId, securityToken ) : Card[0..*]
    Table.pickUpCardsToMe( myId, securityToken ) : Card[0..*]
    

    This introduces security issues, because I am telling the Table that only userId can pick up the card, and only myId can verify and retrieve the card, so the Table object needs some way to verify that I ("the current object") have the right to access the spot on the table identified by "userId" and "myId", but there are lots of solutions to this as well.

    //begin psuedocode for current example
    //we are in player1's function body
    card = self.removeCardFromHand( card_587 );
    player2_id = self.identifyPlayerToReceive( card );
    table.placeCardForUser( card, player2_id, myId, securityToken );
    //end current action
    
    //at next opportunity to act, check to see
    //if card was taken
    cardCount = table.countCardsOnTableToUserFromUser( userId, myId, securityToken );
    if( cardCount > 1 ){
    //player2 has not taken card or other cards that have accumulated
        pickUpCards = self.decideToPickUpCardsToPlayer( player2_id );
        if( pickUpCards === true ){
            cards = table.pickUpCardToUser( player2_id, myId, securityToken );
            foreach( cards as card ){
                self.addToHand( card );
            }
        }
    }
    //now check to see if anyone has given me cards between last round and this round
    cards = table.pickUpCardsToMe( myId, securityToken );
    foreach( cards as card ){
         //let us assume that player1 takes all cards given to him
         self.addToHand( card );
    }
    

    Variations of this can be made. You can imagine a tunnel between player1 and player2. Player1 establishes the tunnel by recognizing that he does not currently have a way to give cards to player2 and so he creates a tunnel. He gives a copy of the tunnel to player2, holding the "other end", and player2 then keeps a copy of the tunnel as well. Like the table situation, this tunnel now is a place that can keep items that are being passed back and forth to player2, but because only player1 and player2 have links, or pointers, to the tunnel, only these two players can put items in the tunnel or take them out, so therefore, we have an intermediary that does not require as much security. We can create tunnels to link all players with all other players, and this is still a variation of the intermediary design.

    1. The self aware card

    Sometimes, we want designs that are easier to code and less like reality. What happens if the code for the Player object forgets to remove the card object from his hand? Now, because objects are generally passed by reference, player2 and player1 each have a reference to the card, and the game simulation thinks there are two copies of the same card!

    In this situation, we can design the card to be self-aware, and give the card access to a player's hand.

    For this abstraction, I will model the card as such:

    //begin pseudocode
    Card{
         private owner; 
         //this is a private link to the object in which the card lies
         //we will allow any object to be the owner of the card, as long
         //as the object implements the "CardOwner" interface.
    
         public putInto( newOwner ){
         //whoever takes the card must specify a newOwner, which will
         //implement the "CardHolder" interface.
               success = newOwner.addCard( self ); 
               if( success ){
                   self.owner.removeCard( self );
                   self.owner = newOwner;
               }
         }
    }
    

    We then can define the interface as follows:

    //begin psuedocode
    iCardHolder{
        public removeCard( card ) : bool
        public addCard( card ) : bool
    }
    

    In this scenario, we have divorced ourselves from "reality" by giving the card itself the ability to perform actions. But where this is useful is in large projects where you cannot trust the other programmers to remember the details about how the Card is properly handled.

    By giving the card control over who has pointers to it, we can ensure that only one copy of the card exists at any time, no matter who is using it.

    Now, player1's code might look like this:

     //give card to player2
     card = self.chooseCard();
     player2.giveCard( card );
    
     //put card on the floor
     card = self.chooseCard();
     floor.giveCard( card );
    
     //put card on the table
     card = self.chooseCard();
     table.giveCard( card );
    

    And in each of these objects, we have the freedom to change the way we receive the card and where we keep it.

    //player2 - is a simple CardHolder
    public function giveCard( card ){
         myHand = self;
         card.putInto( myHand );
    }
    
    //the dealer is a cheat and does not implement CardHolder, 
    //but he has three locations that can act as CardHoldes
    //they are:
    //  dealer.sleave, dealer.hand, and dealer.pocket
    public function giveCard( card ){
         location = self.chooseCardOwner( [ sleeve, hand, pocket ] );
         card.putInto( location );
    }
    
    //the floor has random piles that are accumulating
    public function giveCard( card ){
        pile = self.chooseRandomPile();
        card.putInto( pile );
    }
    

    This option is bizarre, but it gives us a lot of flexibility. In the above example, the Dealer and the Floor are not even implementers of the iCardHolder interface, but they hold references to objects to that do implement that interface, so they can still take the card.

    In each of these implementations using iCardHolder, which is completely different from the others, the code is incredibly simple, because we have offloaded the manipulation of the cards location and put that responsibility on the card itself, and all the card cares about is that the objects that interact with it, agree to a contract of sorts and promise to implement one removeCard method, and one addCard method. As security, the card keeps a copy of the current owner in its own memory, so that if there is a mistake by one of the CardHolders, the Card itself holds the answer to its current owner.

    Long Story Short

    There is no one right way to model your game. It really is all about personal preference and how you want the system to behave. That's the beautiful thing about being a programmer. As the person who is making the code-design, you get to set the rules for how the program will operate, and what is good object-interaction, and what is bad object-interaction.