Beat Your Grandma at Cards - Chapter 5 - Euchre Phases

In the last chapter, we implemented the first bidding phase of the game. We got to see how the phases result feeds into changes to the GameState, which in turn leads to subsequent phases. In this chapter, we’ll implement the remaining phases and write the widgetry for displaying them.

Let’s add our remaining phases to the controller’s build method:

  @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;
    Size cardSize = Size(cardWidth, cardHeight);
    
    switch (_euchreGameState.phase) {
      case GamePhase.first_bidding_round:
        return _simpleTable(tableSize, BidFirstRoundWidget(
          cardSize: cardSize,
          state: _euchreGameState,
          onResult: _onFirstRoundBidResult
        ));
      case GamePhase.second_bidding_round:
        return _simpleTable(tableSize, BidSecondRoundWidget(
          cardSize: cardSize,
          state: _euchreGameState,
          onResult: _onSecondRoundBidResult
        ));
      case GamePhase.discard:
        return _simpleTable(tableSize, DiscardPhaseWidget(
          cardSize: cardSize,
          state: _euchreGameState,
          onResult: _onDiscard
        ));
      case GamePhase.play:
        return _tableWithTrumpAndScore(tableSize, PlayPhaseWidget(
          cardSize: cardSize,
          state: _euchreGameState,
          onResult: _onPlay
        ));
      case GamePhase.resolve_trick:
        return _tableWithTrumpAndScore(tableSize, PlayPhaseWidget(
          cardSize: cardSize,
          state: _euchreGameState,
          onResult: null
        ));
      case GamePhase.finished:
        return _simpleTable(tableSize, GameOverWidget(cardSize: cardSize, state: _euchreGameState, onNewGame: () {
          setState(() {
            _euchreGameState = startNewGame(_euchreGameState.dealerPosition.next);
            _maybeProcessAITurn();
          });
        },));
    }
  }
 
  Widget _tableWithTrumpAndScore(Size size, Widget child) {
    OrientationBuilder theGame = OrientationBuilder(
      builder: (context, orientation) {
        List<Widget> items = [
          Column(children: [
            Text("team $ won the bid."),
            Text("trump is $"),
          ]),
          Container(
            width: size.width,
            height: size.height,
            child: child
          ),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              Column(children: [
                Text("tricks"),
                Text("N/S $"),
                Text("E/W $")
              ]),
              Column(children: [
                Text("points"),
                Text("N/S $"),
                Text("E/W $")
              ])
            ]
          ),
        ];
        return orientation == Orientation.portrait
          ? Column(
            key: UniqueKey(),
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: items
          )
          : Column(
            key: UniqueKey(),
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Row(
                mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                children: items
              )
            ]
          );
      },
    );
    return Container(
      color: Colors.green,
      child: theGame
    );
  }

We also have a new set of widgets for rendering the current state of the game (i.e. how many tricks each team has and the current trump).

Next, we need to implement our callbacks for when the controller receives actions from players. We already implemented _onFirstRoundBidResult in the last chapter, let’s implement the rest now:

  void _onSecondRoundBidResult(CallResult result) {
    setState(() {
      if (result.call) {
        _euchreGameState = _euchreGameState.setTrumpAndPlay(result.suit);
      } else {
        if (_euchreGameState.currentPlayer == _euchreGameState.dealerPosition) {
            _euchreGameState = startNewGame(_euchreGameState.dealerPosition.next);
        } else {
            _euchreGameState = _euchreGameState.nextPlayer();
        }
      }
      _maybeProcessAITurn();
    });
  }
 
  void _onDiscard(PlayingCard card) {
    setState(() {
      _euchreGameState = _euchreGameState.handleDiscard(card);
      _maybeProcessAITurn();
    });
  }
 
  void _onPlay(PlayingCard card) {
    setState(() {
      _euchreGameState = _euchreGameState.handlePlay(card);
      if (_euchreGameState.phase == GamePhase.resolve_trick) {
        Timer(Duration(milliseconds: 800), () {
          setState(() {
            _euchreGameState = _euchreGameState.newTrick();
            if (_euchreGameState.totalTricks == 5) {
              _euchreGameState = _euchreGameState.gameOver();
              _handleScoreUpdates();
            } else {
              _maybeProcessAITurn();
            }
          });
        });
      } else {
        _maybeProcessAITurn();
      }
    });
  }

One unorthodox thing to call out here is the Timer wait. This is just so the UI doesn’t update too quickly, and a human player can get a better idea of what’s happening.

This is basically the entire EuchreGameController class. If you want to see it all in one place, check it out here.

Finally, we need our individual widgets for displaying each phase of the game. The second bidding round is almost as simple as the first, but we need a few extra buttons for each suit:

class BidSecondRoundWidget extends StatelessWidget {
  final Size cardSize;
  final GameState state;
  final CallSuitCallback onResult;
 
  const BidSecondRoundWidget({this.cardSize, this.state, this.onResult});
 
  Widget _suitButton(Suit suit) => RaisedButton(
    child: Text(suit.shortName),
    onPressed: () {
      this.onResult(CallResult(true, suit));
    }
  );
 
  Widget _callSuit() => Column(
    children: <Widget>[
      Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: <Widget>[
          _suitButton(Suit.spades), SizedBox(width: 16), _suitButton(Suit.hearts)
        ],
      ),
      Row(
        children: <Widget>[
          _suitButton(Suit.diamonds), SizedBox(width: 16), _suitButton(Suit.clubs)
        ],
      ),
      Row(
        children: <Widget>[
          RaisedButton(child: Text("pass"), onPressed: () { this.onResult(CallResult(false, Suit.spades)); },)
        ],
      )
    ],
  );
 
  @override
  Widget build(BuildContext context) => EuchreTableWidget(
    players: state.players.map((k, v) => MapEntry<TablePosition, Widget>(k, k == TablePosition.south ?
      PlayerWidget(state: state, cardSize: cardSize, onSelected: null,)
      : OpponentWidget(position: k, state: state, cardSize: cardSize))),
    tableCenter: state.currentPlayer == TablePosition.south ? _callSuit() : WaitingForOtherPlayerWidget(state.currentPlayer),
  );
}

It looks like this:

Screenshot_1586164496.png

Discard is dead simple:

class DiscardPhaseWidget extends StatelessWidget {
  final Size cardSize;
  final GameState state;
  final CardSelectedCallback onResult;
 
  const DiscardPhaseWidget({this.cardSize, this.state, this.onResult});
  
  @override
  Widget build(BuildContext context) => EuchreTableWidget(
    players: state.players.map((k, v) => MapEntry<TablePosition, Widget>(k, k == TablePosition.south ?
      PlayerWidget(state: state, cardSize: cardSize, onSelected: onResult,)
      : OpponentWidget(position: k, state: state, cardSize: cardSize))),
    tableCenter: state.currentPlayer == TablePosition.south ? Text("discard a card") : WaitingForOtherPlayerWidget(state.currentPlayer)
  );
}

Playing is the most complicated phase, as you might imagine, but even it’s not too bad. First, we need a widget to show the in-progress trick. I like to just show each card in front of the respective player, and if they haven’t played yet, we can show a blank card like this:

Screenshot_1586164563.png

This trick widget will look something like this:

class TrickWidget extends StatelessWidget {
  final Map<TablePosition, PlayingCard> cards;
  final Size cardSize;
 
  TrickWidget(this.cards, this.cardSize);
 
  Widget _getCard(TablePosition pos) => cards.containsKey(pos) ? SimpleCardWidget(cards[pos], cardSize) : BlankCardWidget(cardSize);
 
  @override
  Widget build(BuildContext context) {
    return Column(children: <Widget>[
      _getCard(TablePosition.north),
      Row(children: <Widget>[
        RotatedBox(quarterTurns: 1, child: _getCard(TablePosition.west)),
        SizedBox(height: cardSize.width, width: cardSize.width,),
        RotatedBox(quarterTurns: 3, child: _getCard(TablePosition.east)),
      ],),
      _getCard(TablePosition.south),
    ],);
  }
}

And once we have that, the play widget is relatively simple:

class PlayPhaseWidget extends StatelessWidget {
  final Size cardSize;
  final GameState state;
  final CardSelectedCallback onResult;
 
  const PlayPhaseWidget({this.cardSize, this.state, this.onResult});
 
  @override
  Widget build(BuildContext context) => EuchreTableWidget(
    players: state.players.map((k, v) => MapEntry<TablePosition, Widget>(k, k == TablePosition.south ?
      PlayerWidget(state: state, cardSize: cardSize, onSelected: onResult,)
      : OpponentWidget(position: k, state: state, cardSize: cardSize))),
    tableCenter: TrickWidget(state.currentTrick.plays, cardSize)
  );
}

That’s it! That’s all the necessary widgets for the game flow. Now we can play a full game against the DerpAI:

Aaand, I just barely won. Against a completely randomized AI... No wonder my Grandma schooled me so easily. Anyway, tis no matter, because in our next chapter, we’ll discuss Perfect Information AIs, which will be able to utterly destroy the randomized AI.

Jon Bedardgame aiComment