Spades Tutorial Part 2

Playing a card

When a player tries to play a card from his hand (by clicking), the frontend will send a message like play_card player_1 td to the backend indicating that player_1 is attempting to play the card named td. (player_1 is there for historical reasons and not actually needed – use PLAYER_NAME to find out who is sending the message instead.)

../../_images/spades_a6c.png

When this happens, the backend will look for and attempt to execute a RECEIVER named play_card with two arguments. Let’s go ahead and define that receiver now.

We need to accomplish a few things:

  • for the player sending the message, check that it is actually their turn. If not, we should REJECT the message as an illegal action.
  • check that the card specified in the message actually exists in the given player’s hand. (We need to validate the input here because unscrupulous users could hijack the frontend and send any kind of message they want.)
  • check that the given card follows suit.
  • if it does follow suit, we need to go ahead and actually play it.

Whose turn is it

1
2
3
4
5
RECEIVE play_card $dummy $card_name:
  IF NOT EQUAL $PLAYER_NAME GET_POSITION_ELT GET_PROP 'play_order' 0:
    REJECT CONCAT "not player turn: " $PLAYER_NAME;
  END_IF
END_RECEIVE
  • As we said, the first argument is not needed, so we have called it $dummy.
  • Instead we are checking whether PLAYER_NAME is equal to the first element of play_order, which we’re using to keep track of play order.
  • If the check fails, we immediately REJECT with a helpful message to let the frontend know what went wrong.

Check the card exists in player hand

1
2
3
4
5
6
7
8
9
RECEIVE play_card $dummy $card_name:
  IF NOT EQUAL $PLAYER_NAME GET_POSITION_ELT GET_PROP 'play_order' 0:
    REJECT CONCAT "not player turn: " $PLAYER_NAME;
  END_IF
  ASSIGN 'player_hand' GET_CONTAINER CONCAT $PLAYER_NAME "_hand";
  IF NOT CONTAINS_BY_NAME $player_hand $card_name:
    REJECT CONCAT "card not in player hand: " $card_name;
  END_IF
END_RECEIVE

Check the card follows suit

This one is a bit trickier. A card is said to follow suit if it is the same suit as what was led. In Spades, you must follow suit unless you are leading or unable to follow suit. So there are a few possible ways for a play to be valid:

  1. It’s the leading card (no cards have been played yet this trick).
  2. It is the same suit as what was led.
  3. The player has no cards of the same suit as the leading suit.

We are using the PROP suit_led to keep track of what suit was led. If nothing has been played yet, this will be NULL.

Let’s create a few helper functions to make this logic easier:

DEF get_suit $card:
  RETURN TOKEN_GET_PROP $card 'suit';
END_DEF

DEF follows_suit $card:
  RETURN EQUAL get_suit $card GET_PROP 'suit_led';
END_DEF

We are now ready to check the follows suit logic:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
RECEIVE play_card $dummy $card_name:
  IF NOT EQUAL $PLAYER_NAME GET_POSITION_ELT GET_PROP 'play_order' 0:
    REJECT CONCAT "not player turn: " $PLAYER_NAME;
  END_IF
  ASSIGN 'player_hand' GET_CONTAINER CONCAT $PLAYER_NAME "_hand";
  IF NOT CONTAINS_BY_NAME $player_hand $card_name:
    REJECT CONCAT "card not in player hand: " $card_name;
  END_IF
  # get the number of cards in player hand of the led suit
  ASSIGN 'ct' 0;
  ASSIGN 'i' PLUS SIZE $player_hand -1;
  WHILE GREATER_OR_EQUAL $i 0:
    IF follows_suit GET_BY_POSITION $player_hand $i:
      ASSIGN 'ct' PLUS $ct 1;
    END_IF
    ASSIGN 'i' PLUS $i -1;
  END_WHILE
  IF NOT
    OR
      OR
        IS_NULL GET_PROP 'suit_led'
        follows_suit GET_TOKEN $card_name
      EQUAL $ct 0:
    REJECT CONCAT "must follow suit: " $card_name;
  END_IF
END_RECEIVE

Here we used a loop to iterate over the player’s hand and count how many of their cards are of the led suit. That total is saved to the variable $ct. Finally we combine all the logic together to decide whether to reject the received message.

Updating game state to reflect card was played

If we’ve made it this far, that means that the card was a legal play, so we should go ahead and make the play.

To play a card, we must remove it from the player’s hand and place it into a special container for the current play. We don’t discard the card right away as it is helpful for the other players to see what has currently been played.

If this is all ok, we finish by ACCEPT ing the message.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
RECEIVE play_card $dummy $card_name:
  IF NOT EQUAL $PLAYER_NAME GET_POSITION_ELT GET_PROP 'play_order' 0:
    REJECT CONCAT "not player turn: " $PLAYER_NAME;
  END_IF
  ASSIGN 'player_hand' GET_CONTAINER CONCAT $PLAYER_NAME "_hand";
  IF NOT CONTAINS_BY_NAME $player_hand $card_name:
    REJECT CONCAT "card not in player hand: " $card_name;
  END_IF
  # get the number of cards in player hand of the led suit
  ASSIGN 'ct' 0;
  ASSIGN 'i' PLUS SIZE $player_hand -1;
  WHILE GREATER_OR_EQUAL $i 0:
    IF follows_suit GET_BY_POSITION $player_hand $i:
      ASSIGN 'ct' PLUS $ct 1;
    END_IF
    ASSIGN 'i' PLUS $i -1;
  END_WHILE
  IF NOT
    OR
      OR
        IS_NULL GET_PROP 'suit_led'
        follows_suit GET_TOKEN $card_name
      EQUAL $ct 0:
    REJECT CONCAT "must follow suit: " $card_name;
  END_IF
  # ok to play card
  ASSIGN 'player_play' GET_CONTAINER CONCAT $PLAYER_NAME '_play';
  ASSIGN 'card_token' POP_BY_NAME $player_hand $card_name;
  IF IS_NULL $card_token:
    REJECT "Something went wrong";
  END_IF
  APPEND $player_play $card_token;
  ACCEPT;
END_RECEIVE

Now, we are able to play cards from our hand and follow the rules of following suit.