Beat Your Grandma at Cards - Chapter 3 - Modelling Euchre

I know we’re trying to build an AI that can beat grandmas and not a fully-fledged card game app, but I think it helps a great deal to have some basic visualizations when working on a difficult technical problem.

There are a few more game concepts we haven’t discussed yet. Most of them are pretty simple, so let’s do some rapidfire modelling:

We need a way to refer to the four players. One common way of doing that in a four person game is to use the four cardinal directions. I’ll call it TablePosition:

enum TablePosition {
  north, east, south, west
}
 
extension TablePositionExtension on TablePosition {
  // … useful extensions here
}

Once again, I added some useful extension methods for naming and opposite and next and such. They’re pretty simple, so I’ll leave them out of this tutorial, but you can see them here.

Team is also just a simple enum:

enum Team {
  north_south,
  east_west
}

Yes, I really like enums.

Next, let’s model two concepts I’ve alluded to a few times called Trick and Kitty:

class Kitty {
  final List<PlayingCard> buried;
  final PlayingCard upCard;
  final PlayingCard discardCard;
 
  const Kitty(this.buried, this.upCard, this.discardCard);
}

You notice Kitty has a place where the dealer discard card is stored. We could’ve put this elsewhere, but this is mostly a throwback to when I played as a kid and we’d put the discard back into the kitty to keep it hidden.

Okay, and finally: Trick. Trick needs a method to determine the winner of the trick. In order to do that, I’m going to introduce card scoring. Card scoring is simply assigning higher int values to higher value cards. So the right bauer gets the highest value, the left is next, then the ace of trump and so on. There are also several contexts where cards can be scored. For example, if you’re in a Trick, there is a lead suit, and every card of that suit is more valuable than cards of other suits (except trump, of course, which is always the most valuable suit). So you’ll see here in Trick that I use a method called ‘trickCardScore’. You can see all the scoring here. It’s pretty straightforward, and very verbose, so I won’t include the code in this tutorial, but click the link so you get the idea.

Once scoring is figured out, we can write our Trick class:

class Trick {
  final Suit trump;
  final TablePosition lead;
  final Map<TablePosition, PlayingCard> plays;
 
  Trick(this.trump, this.lead, this.plays);
 
  TablePosition determineWinner() {
    TablePosition pos = lead;
    Suit leadSuit = plays[lead].getSuit(trump);
    int highScore = trickCardScore(plays[pos], leadSuit, trump);
    TablePosition winner = pos;
    for (int i = 0; i < 3; i++) {
      pos = pos.next;
      int s = trickCardScore(plays[pos], leadSuit, trump);
      if (s > highScore) {
        highScore = s;
        winner = pos;
      }
    }
    return winner;
  }
}

Alright, that’s all the necessary modelling for Euchre. Let’s render a nice looking euchre table with our four players. We need some widget that can render the players on the four sides of the table. Luckily, with flutter, layout like this is a cinch. Here’s a widget that will do the job:

class EuchreTableWidget extends StatelessWidget {
  final Map<TablePosition, Widget> players;
  final Widget tableCenter;
 
  const EuchreTableWidget({this.players, this.tableCenter});
 
  @override
  Widget build(BuildContext context) => Column(
    mainAxisAlignment: MainAxisAlignment.spaceBetween,
    children: <Widget>[
      RotatedBox(quarterTurns: 2, child: players[TablePosition.north]),
      Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: <Widget>[
          RotatedBox(quarterTurns: 1, child: players[TablePosition.west]),
          tableCenter,
          RotatedBox(quarterTurns: 3, child: players[TablePosition.east]),
        ]
      ),
      players[TablePosition.south],
    ]
  );
}

Most of this is simple, hopefully. The cards will be rendered at the sides of the table. I’ve used RotatedBox to make it look like the cards are facing the player. I also provided a “tableCenter” widget. This can be some UI element that gets passed in. The center is a reasonable place to put a UI for a simple AI visualizer.

Let’s throw this in our main. We can just center it inside the same container we were using before, like so:

class EuchreGame extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    Size screenSize = MediaQuery.of(context).size;
    Size tableSize = screenSize.width > screenSize.height
      ? Size(screenSize.height * .9, screenSize.height * .9)
      : Size(screenSize.width * .9, screenSize.width * .9);
    double cardHeight = tableSize.height / 6.0;
    double cardWidth = cardHeight * .714; // yes, I measured an actual playing card
    Size cardSize = Size(cardWidth, cardHeight);
 
    // deal an actual deck!
    List<PlayingCard> cards = newShuffledDeck();
 
    Widget north = CardStackWidget(.7, cards.sublist(0, 5).map((c) => CardBackWidget(cardSize)).toList(), cardSize);
    Widget east = CardStackWidget(.7, cards.sublist(5, 10).map((c) => CardBackWidget(cardSize)).toList(), cardSize);
    Widget west = CardStackWidget(.7, cards.sublist(10, 15).map((c) => CardBackWidget(cardSize)).toList(), cardSize);
    Widget south = CardStackWidget(-.2, cards.sublist(15, 20).map((c) => SimpleCardWidget(c, cardSize)).toList(), cardSize);
 
 
    return Scaffold(
      body: Container(
        color: Colors.green,
        child: Center(
          child: Container(
            height: tableSize.height,
            width: tableSize.width,
            child: EuchreTableWidget(
              players: {
                TablePosition.north: north,
                TablePosition.west: west,
                TablePosition.east: east,
                TablePosition.south: south,
              },
              tableCenter: SizedBox(width: 20, height: 20,), // Nothing...
            ),
          ),
        ),
      )
    );
  }
}

If we run that code, it looks like this:

card_table_example.png

I think this is a suitable look and feel for our Euchre AI visualizer. It’s simple, readable, and not completely atrocious. In the next chapter, we’ll add interactivity and a basic random AI to make the game playable!