1

I want to expand my personal projects portfolio, so I decided to make a card game. To be more precise, it's called Macau. I've read this answer on StackOverflow and tried to follow the steps from the first answer. I made a narrative in which I describe the general rules of the game and its particularities.

Macao game. The minimum number of players is 2. The deck of cards initially contains 52 cards. Each card has the following attributes:

  • Suit (diamonds, clubs, hearts and spades)
  • Rank (2, 3, 4, 5, 6, 7, 8, 9, A, J, Q, K, Joker)

Starting the game, the deck is shuffled and every player receives 5 cards from it. The first player that remains out of cards is the winner. Afterwards, a card is taken from the deck and put on the table with the face up.

The current player looks at the card that's been put on the table and decides whether he can put a card from his packet on the table or not. A player can put a card on the table if their card is compatible with the other one.

Two cards are compatible if they accomplish one of the following conditions: they have the same rank or the same suit. Expanding upon the cards domain, there exist several special cards. These have to obey the specified rules too, but they have an extra attribute: the special ability. They may have any suit, but these ranks qualify a card to be special: 2, 3, 4, 7, A, and Joker. The Joker is a wildcard as it doesn’t need to obey to the earlier specified rule. That is, the player can put it on the table at any time.

The next section gives a thorough description of the mentioned cards’ special abilities:

  • Rank: 7. When put on the table, this card requires the current player to specify a suit. The next player is obliged to put any card that has the earlier specified suit, otherwise he will receive a new card from the deck.

  • Rank: 2 and 3. This card obliges the next player to receive 2/3 (2 cards if the rank is 2, same rule for 3) cards from the deck. Now, if the next player also has a 2 or a 3 with the same suit, or a Joker, then he can put it on the table and the next player has to receive the total amount of cards. The same rules apply in a circular manner. If the next player has a 4 with the same suit, then he can stop the obligation injected by the previous player.

  • Rank: 4. Special ability: it can stop an obligation injected by the previous player. If it is put over a regular card (i.e. a card that has no special ability), then it acts like one.

  • Rank: A (Ace). Special ability: it skips the next player's turn.
  • Rank: Joker. It is a wildcard so it can be put on the table at almost all instances. The exception arises when the card that is on the top of the table is an A (Ace), then the current player's turn is skipped. Special ability: the same as the 2/3 card, except that the next player will have to receive 5 cards (for the Black Joker) or 10 (in case of the Red one).

Having the rules clarified, the game continues. If the current player owns such a compatible card (or more), then it's his choice whether to put one on the table or receive a new card from the deck. Also, while in game, there can appear the situation when the deck would be emptied. In that case, all the cards from the table that are located under the top one are taken, shuffled, and used to refill the deck. The card on the top is untouched.

Now it's the next player's turn, and the earlier described steps repeat until one of the players remains out of cards. That player is the winner.

From this narrative I extracted the following classes:

  • Card
  • Deck
  • Player (maybe Hand?)
  • Table (that contains the Deck and the stack of already played cards)
  • Game

Here come my problems.

  1. I'm not sure if I made the right abstractions.
  2. I wouldn't know how to design the special cards. Should there exist a separate (inherited from Card) class for every special card? Or should I use composition? Also, how should I implement the Joker cards? They don't really have a Suit, they just have the Rank.
  3. Should the Game class manage players' turns and the exceptional cases (those cases when the top card would be a special one)?
  4. Should Player be just an interface? This would leave space for additional features like an AI player (that implements Player).

This is my first medium-sized object-oriented project and I want to be sure that I'm on the right path.

6
  • 2
    You might try implementing some of this, and then when you have a problem ask that question.
    – esoterik
    Commented Jun 25, 2018 at 19:04
  • 1
    I don't see any glaring misconceptions, and I would start with that too. But you'll find out when you code it. Sometimes the devil is in the details.
    – Aganju
    Commented Jun 25, 2018 at 20:33
  • I had actually started coding, but I got stuck on implementing the special cards.
    – I. S.
    Commented Jun 26, 2018 at 19:42
  • I've got a question. I want to keep track of the special cards as they don't change, but, since not every Card Game has special cards, I thought to put the data structure (hash table/binary search tree) that holds them in the run method rather than the private field of the Game class. How clean is this? Also, should I edit the main post so that everyone will be notified?
    – I. S.
    Commented Jun 28, 2018 at 9:32
  • Is the deck really 52 cards, or 54 (4*13+2)? Commented Jul 2, 2018 at 19:25

5 Answers 5

4

I'm not sure if I made the right abstractions.

Well, it is a start, but you will find only out if you stop overthinking it and start to write some code using these abstractions. This code could be tests, real game logic, or both.

I wouldn't know how to design the special cards. Should there exist a separate (inherited from Card) class for every special card? Or should I use composition?

Neither. If I got this right, being "special" is just a property which can be derived from the rank. So when a Card class has an attribute Rank (as well as Suit), it carries all the information the game logic requires to make a decision about the "special abilities".

Also, how should I implement the Joker cards? They don't really have a Suit, they just have the Rank.

Simple: introduce a fifth Suit "None" for Jokers. Your card's constructor can throw an exception if someone tries to initialize a card with a suit "None" for non-Jokers or a card with a rank "Joker" and a different suit.

Should the Game class manage players' turns and the exceptional cases (those cases when the top card would be a special one)?

Somewhere you need to implement the game's logic. Putting it initially into the Game class is a sensible start. If that class evolves, and it becomes too large / gets too many responsibilities, try to identify such responsibilities, give them a name and refactor them to separate classes. There is no need to overanalyse this beforehand.

Should Player be just an interface? This would leave space for additional features like an AI player (that implements Player).

Again, there is no need to make this decision now. Start with one Player class, and when you come to the point where you need a HumanPlayer and an AIPlayer, it is early enough to refactor.

2
  • Thanks, your observations made the situation more clear.
    – I. S.
    Commented Jun 26, 2018 at 19:43
  • The Game class already has 8 methods aside from the run() one, and I've implemented just the basic functionality (i.e. without special cards). I feel the class will keep getting bigger and bigger, is this the right timing for a refactoring?
    – I. S.
    Commented Jun 30, 2018 at 9:07
2

Card game engine

You've got a decent headstart, but don't feel like you absolutely must have every possible class before you begin coding. The code naturally evolves to add additional features or extend existing classes.

If I may make a recommendation, try to separate the part of your program which deals with generic "card game" logic, such as dealing cards from a deck, player turns, discards, draws, basically anything you'd tend to want to do in a card game.

Then I would turn your Game class into an abstract class for handling a card game, with basic methods to allow you perform these basic card functions such as draw, discard, change turns, etc.

MacaoGame

You then extend Game to be MacaoGame, and MacaoGame is the "brain" of handling the game logic. Anything which MacaoGame specifically requires to handling this particular type of game can be added directly to the MacaoGame class, but you should treat it like a separate part of your program (the part that helps you implement MacaoGame, and not the part which deals with your basic card game engine).

For example, perhaps you create an enum to identify the cards which have special meaning (in Java, you can directly pass the card to its constructor, so that you can easily check if a card is SuitChangeCard for instance). In this way it would be easy to change later, and it also doesn't change the underlying card game engine, making it a very flexible design decision.

Player extending?

For now, Player can be an implementation class holding everything you need to know about a player. However, if player behavior differs if the player is human or an AI, then yes, this would be the first class to turn abstract and extend. I think the crucial part of the player is the actual play, so you're asking for input. The input can come from a human player or an AI potentially, and while this doesn't make too much difference, you should note that one is synchronous while the other is not.

It might therefore be worth your while to use a callback to receive input (unless you don't mind the running thread hanging in wait for user input).

Joker

The Joker card is its own card. In the card game engine part of your program, you can deal with rank, but don't put too much emphasis on it, as rank can change based on the game. For instance, in some games, the ace is considered lower than the 2. Treat it as any other card everywhere except in MacaoGame and you should be fine.

Conclusion

You're definitely on the right track. I don't think you'd run into serious problems if you combined MacaoGame logic with the rest, however it is somewhat a less flexible design should you ever want to implement other card games. It would also help you organize the development in your mind as the part of the program which deals with card game engine functionality and the specific game of Macao. Good luck!

1
  • Thank you for your ideas, I'm looking forward to implement them.
    – I. S.
    Commented Jun 26, 2018 at 19:49
1

For modelling games like this forget OO. Its fine for the UI, but until you can create a simple model of the game telling you who's go it is and what the valid moves are it will only get you in trouble.

All turn based games follow the same pattern. a player has control and makes plays untill control switches to another player or the game is over.

In your case the moves are enumerable as all the cards in the deck, with some extra for the specials with options, such as specifying the following suit. (You'll have to decide if that's better represented as two moves or one)

1h, 2h, 3h.... 1c, 2c ... Jr, Jb etc

the players have an order in which play proceeds and their moves are limited to the cards in their hands.

You can now model the game very simply with a few string arrays. You can even make some simple bots that just choose a random card.

Once you have that working. Then start worrying about what abstractions make sense. But they may end up bearing no relation to the physical apparatus required for humans to play

20
  • 1
    Not saying the general strategy presented in your answer is not fine, but when I already know my cards need a suit and a rank, I would not encrypt that information into two-letter strings "3h", "2c". I am then already at the point where I know I need a card abstraction, even if it is not more than a struct with two attributes rank and suit.
    – Doc Brown
    Commented Jun 26, 2018 at 5:55
  • Its not a card its a play. not all plays are cards when you have the specials, or 'pass'. Sure you probably do want some structs in there eventually, but the OO way, creating Card and then inheriting SpecialCard from it and adding a Play() Method etc falls over for this kind of thing.
    – Ewan
    Commented Jun 26, 2018 at 6:05
  • (also in your own answer you have already broken your struct for jokers, now you have possibilities like 2-nosuit)
    – Ewan
    Commented Jun 26, 2018 at 6:10
  • 1
    Honestly, if you need days instead of hours by using some explicit, readable classes instead of some cryptic strings for encoding the same information (just for saving some keystrokes), you are doing something wrong.
    – Doc Brown
    Commented Jun 26, 2018 at 19:45
  • 1
    @I.S.: no, here I am with Ewan. This is not about flexibility, it is about clean coding in a readable, self-documenting fashion vs. creating unreadable, cryptic technical debt. Cryptic strings may be indeen more flexible, but they tend to become unmaintainable quickly.
    – Doc Brown
    Commented Jun 26, 2018 at 19:49
0

Rule of thumb for checking your design

There are two simple rules to check whether your design conforms to Object-Oriented principles:

  1. All (object and method) names mean something in the domain. This means no Service, Manager object names, no execute() methods, and so on.

  2. There are no data leaked from objects. This means no methods that just return data that was already available. I.e. no getter methods.

Disclaimer

Now, I'm not saying this should always be done with every project without compromise. But for your case, and if you really want to get it right, you should not stray from this path.

This won't be easy at first, but it will get you in the right mindset sooner or later. It also almost automatically guarantees that you will conform to principles like: The Law of Demeter, Single Responsibility Principle, and so on.

5
  • Thank you. Also, thanks for mentioning those principles.
    – I. S.
    Commented Jun 26, 2018 at 19:56
  • I've come at the point when a method of the Game class would need information on the cards' ranks (to determine whether they are special or not). The only options I can think about are: getter or friendship. Do I have a design flaw?
    – I. S.
    Commented Jun 29, 2018 at 19:48
  • You definitely need to come up with a different design. If you want to exercise good design principles that is. Try to think about why you want this information, and then how you can put that logic into the card, or elsewhere where the needed information is already present. Commented Jun 29, 2018 at 20:31
  • Should a card know that it is a special one?
    – I. S.
    Commented Jun 29, 2018 at 20:47
  • That is certainly a way. Try to come up with a design that does not require getting data out of objects. Commented Jun 29, 2018 at 22:57
0

Let's look at the cards first:

4 Suits, 13 ranks. To facilitate colour-wish-cards, one can add a second suit or split that rank into one per suit. The former can more easily be used for many other card-games.

So, 2 bit for suit, 2 bit for next suit, 4 bit for rank, makes one octett. Rank zero should be reserved so all-zero means "not a card", and you still have plenty values left for other considerations.


Make a vector for each player's hand, discard pile and redraw stack, you don't need one for the deck, just add a function to get a whole new one. Now you just need a few more functions to actually do things and you are set.

3
  • I made a separate class called Hand that holds that vector. I also made the discard pile class, but what do you mean by redraw stack? Should that be a method?
    – I. S.
    Commented Jun 28, 2018 at 9:34
  • Well, the redraw stack is just where all the cards you can still draw are. And I would only make a class for players, any more Looks like overengineering. Commented Jun 28, 2018 at 11:16
  • Alright, I just called it deck. For now, the extra class Hand doesn't seem to provide any advantage, and you're kinda right. Maybe in the future I will benefit from it.
    – I. S.
    Commented Jun 28, 2018 at 11:28

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.