Beat Your Grandma at Cards - Chapter 2 - Modelling Cards

If we’re serious about beating a Michigan grandma, we need our software to have a firm grasp of Euchre concepts. Fortunately, modelling card games can be a fun exercise. The scope is usually small enough that a constrained and elegant set of classes and functions feels like it’s within reach.

In this article, we’ll establish these fundamental building blocks of Euchre, and we’ll lay the foundations of a visualizer for seeing the fruits of our AI labors.

Suits and Values

The core components of many playing card games are suits and values:

enum Suit {
  spades,
  hearts,
  diamonds,
  clubs
}
 
enum CardValue {
  nine,
  ten,
  jack,
  queen,
  king,
  ace
}

I mentioned in the previous article that Euchre only uses nine through ace, so there’s no reason to include the other values. There’s a few helpful features that I like to associate with suits and values, and dart has a nice feature called “extensions” that can be used for that purpose.

The first things I like to have are for printing nice strings for suits and values:

extension SuitExtension on Suit {
  String get shortName {
    switch (this) {
      case Suit.spades:
        return "♠";
      case Suit.hearts:
        return "♥";
      case Suit.diamonds:
        return "♦";
      case Suit.clubs:
        return "♣";
    }
  }
}
 
extension CardValueExtension on CardValue {
  String get shortName {
    switch (this) {
      case CardValue.nine:
        return "9";
      case CardValue.ten:
        return "10";
      case CardValue.jack:
        return "J";
      case CardValue.queen:
        return "Q";
      case CardValue.king:
        return "K";
      case CardValue.ace:
        return "A";
    }
  }
}

This way, I can write Suit.spades.shortName, and it will return ♠.

In the suit extension, I also like to include the notion of sister suits (suits of the same color), and opposite suits (suits of a different color). Sister suits are important to Euchre because the left bauer (the second best card in the game), is the jack of the sister suit of trump. Opposite suits are useful for other purposes like ordering cards (for, say, displaying somebody’s hand).

  // suit of same color
  Suit get sister {
    switch (this) {
      case Suit.spades:
        return Suit.clubs;
      case Suit.hearts:
        return Suit.diamonds;
      case Suit.diamonds:
        return Suit.hearts;
      case Suit.clubs:
        return Suit.spades;
    }
  }
 
  // different color suit. spades<->diamonds, hearts<->clubs.
  Suit get opposite {
    switch (this) {
      case Suit.spades:
        return Suit.diamonds;
      case Suit.hearts:
        return Suit.clubs;
      case Suit.diamonds:
        return Suit.spades;
      case Suit.clubs:
        return Suit.hearts;
    }
  }

Cards and Decks

Now we can model an actual card. I’ll call it PlayingCard to avoid conflicts with material’s Card widget:

class PlayingCard {
  final Suit _suit;
  final CardValue value;
 
  PlayingCard(this._suit, this.value);
 
  String toString() {
    return this.value.shortName + this._suit.shortName;
  }
 
  Suit get baseSuit => _suit;
 
  Suit getSuit(Suit trump) {
    return value == CardValue.jack && _suit == trump.sister ? trump : _suit;
  }
}

This is fairly straightforward, with the exception of the weird getSuit method. This is, unfortunately, a necessary piece of the Euchre puzzle, because the left bauer’s suit is always considered to be trump, even though it’s actual suit is the sister suit of trump. For example, if trump is ♥, that means the left bauer is J♦, but in all game logic, the J♦ needs to be treated like it’s a ♥, so we’ll use this method whenever we want to look at a card’s suit.

Next, let’s do a Deck:

List<PlayingCard> newShuffledDeck() {
  List<PlayingCard> cards = List<PlayingCard>();
  for (Suit s in Suit.values) {
    for (CardValue v in CardValue.values) {
      cards.add(PlayingCard(s, v));
    }
  }
  cards.shuffle();
  return cards;
}

Nothing fancy here, but it’ll do. We don’t actually need a “Deck” class or anything, as Dart’s List provides most functions you’d want.

Okay, now let’s finally put some stuff on a screen!

It seems reasonable to start with rendering cards. Let’s make a simple white card that displays the card value in the center:

class SimpleCardWidget extends StatelessWidget {
  final PlayingCard card;
  final Size size;
 
  SimpleCardWidget(this.card, this.size);
 
  @override
  Widget build(BuildContext context) => Container(
      width: size.width,
      height: size.height,
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(_cardBorderRadius),
        color: Colors.white
      ),
      child: Center(
        child: Text(card.toString(), style: TextStyle(fontSize: size.width > 60 ? 20 : 12),)
      )
    );
}

Toss one of those into the main file that flutter generates when you create a new app like so:

void main() => runApp(EuchreApp());
 
class EuchreApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Euchre AI',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: EuchreGame(),
    );
  }
}
 
class EuchreGame extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    Size cardSize = Size(60.0 * .714, 60.0);
    return Scaffold(
      body: Container(
        color: Colors.green,
        child: Center(
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: <Widget>[
              // Right here
              SimpleCardWidget(PlayingCard(Suit.spades, CardValue.ace), cardSize)
            ]
          )
        )
      )
    );
  }
}

And you get something that looks like this:

Screenshot_1584107833.png

Let’s create a widget to render the back of a card as well. After all, you can’t see your opponents cards, right? This one’s pretty simple, it’s basically the same as the other card, but it has an inset rounded blue rectangle:

class CardBackWidget extends StatelessWidget {
  final Size size;
 
  CardBackWidget(this.size);
 
  @override
  Widget build(BuildContext context) => Container(
    width: size.width,
    height: size.height,
    decoration: BoxDecoration(
      borderRadius: BorderRadius.circular(_cardBorderRadius),
      color: Colors.white
    ),
    child: Padding(
      padding: EdgeInsets.all(2),
      child: Container(
        decoration: BoxDecoration(
          borderRadius: BorderRadius.circular(_cardBorderRadius),
          color: Colors.blue
        ),
      )
    )
  );
}

And finally, I’d like a widget to represent an empty space where a card might go. This will come in handy eventually:

class BlankCardWidget extends StatelessWidget {
  final Size size;
 
  BlankCardWidget(this.size);
 
  @override
  Widget build(BuildContext context) => Container(
    decoration: BoxDecoration(
      borderRadius: BorderRadius.circular(_cardBorderRadius),
      border: Border.all()
    ),
    width: size.width,
    height: size.height
  );
}

So here’s a summary of all of our cards:

Screenshot_1584108560.png

And lastly, for this chapter, I’d like the ability to render stacks of cards. This can be accomplished with flutter’s Stack widget. Stack allows you to overlay widgets on top of one another and precisely position them within the Stack’s bounds. We’ll provide our stack with the overlap amount (where 1.0 means the cards are exactly on top of each other), the list of card widgets, and the cardSize so that the stack can accurately size itself. Here’s what it looks like:

class CardStackWidget extends StatelessWidget {
  final double overlap;
  final List<Widget> cards;
  final Size cardSize;
 
  const CardStackWidget(this.overlap, this.cards, this.cardSize);
 
  Widget _cardStack() {
    List<Widget> children = List<Widget>();
    double pos = 0;
    double overlappedCardSize = cardSize.width * (1.0 - overlap);
    for(Widget w in cards) {
      children.add(Positioned(
        top: 0,
        left: pos,
        child: w
      ));
      pos += overlappedCardSize;
    }
    return Container(
      height: cardSize.height,
      width: cardSize.width + (cards.length - 1) * overlappedCardSize,
      child: Stack(children: children)
    );
  }
 
  Widget _cards() => cards.length == 1 ? cards[0] : _cardStack();
 
  @override
  Widget build(BuildContext context) => cards.length == 0 ? SizedBox(height: cardSize.height, width: cardSize.width,) : _cards();
}

And if we put a few of these into our main, it looks like this:

Screenshot_1584108990.png

Alright, we’ve got some simple cards rendering. In the next chapter, we’ll use these cards to build out a basic UI for visualizing Euchre games. Soon enough, we’ll be armed with the necessary infrastructure to tackle the AI. Watch out grandma!

Jon Bedardgame ai