Lesson 4. Serial. Card game Durak (beta 2)



  • The purpose of this lesson

    Hi! Today we will create a network card game for two players. What kind of game to create? Let's make a popular card game "Fool", the purpose of which is to get rid of all the cards. You can learn more about the rules here: https://en.wikipedia.org/wiki/Durak

    Figure 1.

    Briefly about theory

    Serial port is the slang name of the RS-232 interface, which was massively equipped with personal computers, special-purpose equipment, including printers. The port is called "serial" because the information is transmitted one bit at a time, bit by bit. Ordinary personal computers are equipped with RS-232, and microcontrollers and M5STACK including uses TTL.
    RS-232 uses a voltage between -3 and -25 V to transmit "0" and +3 To +25 V to transmit "1". unlike RS-232, TTL uses a voltage close to 0 V to transmit "0" and 3.3 V or 5V to transmit "1" to transmit the operating voltage of the integrated circuit (Fig. 2).

    Figure 2. The voltage difference between RS-232 and TTL

    To communicate through the m5stack serial port are used the digital I/o ports R0, R2 (RX) and T0, T2 (TX) (Fig. 3) and also a USB port. It is important to keep in mind that if you are using functions to work with a serial port, you cannot use ports 0 and 1 for other purposes at the same time.


    Figure 3. Rule for connecting two devices through a serial port

    For the port use a standard set of Serial functions of the Arduino IDE https://www.arduino.cc/reference/en/language/functions/communication/serial/

    Review the concept of ASCII table, there are encoded symbols in computing there: https://en.wikipedia.org/wiki/ASCII

    List of components for the lesson

    • M5STACK (2 PCs.);
    • USB-C cable from standard set (2 PCs.).);
    • colored wires from the standard set.

    Begin!

    Step 1. Let's start with the main logo

    Let's draw the game logo that will appear on the device display when the game starts. To do this, use any editor, such as MS Office Word and Paint (Fig. 4).

    Figure 4. Draw the logo of the game in MS Office Word and Paint :)

    Next, use the app from lesson 1.2.1 http://forum.m5stack.com/topic/49/lesson-1-2-1-lcd-how-to-create-image-array convert the image into an array of pixels and get the logo file.c, which will connect to the sketch.

    extern unsigned char logo[];
    

    Step 2. Create the structure of a deck of cards

    According to the rules of the game, a deck containing only 36 cards is used. 6 cards are issued to each player. The playing field is designed for 12 cards. The player can take 10 cards, and then lose. In the game the card has its value and suit, for example 7. In the program, the card has its place: (in the hands of the player - the player's field; in the playing field; in the used deck; in the unused deck) and purpose: free card, inaccessible card, trump card, the player's card, the opponent's card. Let's make a structure that contains parameter sequence numbers. A deck of cards is an array of structures (Fig. 5).

    Figure 5. Map structure and an array of a deck of cards

    #define playerAmount 6
    #define fullAmount 36
    #define playerFieldAmount 10
    #define gameFieldAmount 6
    
    struct playCard
    {
      int card;
      int suit;
      int color;
      int property = -1; // -2 not avaiable, -1 free, 0 trump, 1 player #1, 2 player #2
      int field; // + player field, - game field
    };
    
    String cards[9] = {"6", "7", "8", "9", "10", "J", "Q", "K", "A"};
    char suits[4] = {3, 4, 5, 6}; // ♥, ♦, ♣, ♠
    int colors[2] = {0xe8e4, 0x00}; // red, black
    playCard deckCards[fullAmount];
    

    Step 3. Write rules of the game

    Attack. Case 1. If there are no cards in the playing field, then the player who got the first place in the queue can throw absolutely any card of any suit (Fig. 5.1):

    Figure 5.1. You can make a move with any card

    if ((opponentSteps == playerSteps) && (playerSteps == 0))
    {
      deckCards[selectedCard].field = -1;
      sync();
    }
    

    Attack. Case 2. If the opponent has made a move on the player, the player can fight off the card of the same suit, but a greater value (Fig. 5.2) or a trump card of any value provided that the last card of the opponent in the playing field is not a trump card:

    Figure 5.1. The opponent has to beat the player's card

    else if (opponentSteps > playerSteps)
    {
      if ((((deckCards[selectedCard].card > deckCards[pfCardId].card) && (deckCards[selectedCard].suit == deckCards[pfCardId].suit)) || ((deckCards[selectedCard].suit == trumpSuit) && (deckCards[pfCardId].suit != trumpSuit))))
      {
        deckCards[selectedCard].field = -opponentSteps;
        sync();
      }
    }
    

    Attack. Case 3. When the opponent will beat the card, the number of steps will be equal and the player will be able to throw more cards of the same value as there is in the playing field:

    else if (opponentSteps == playerSteps)
    {
      for (int i = 0; i < fullAmount; i++)
      {
        if ((deckCards[selectedCard].card == deckCards[i].card) && (deckCards[i].field < 0) && (deckCards[i].property != -2))
        {
          deckCards[selectedCard].field = -(playerSteps + 1);
          sync();
          break;
        }
      }
    }
    

    Bito / take. Case 1. If the players have the same number of steps, the player can send the cards to the recaptured deck:

    if (opponentSteps == playerSteps) // бито
    {
    	for (int i = 0; i < fullAmount; i++)
    	{
    		if (deckCards[i].field < 0)
    		{
    			deckCards[i].property = -2;
    		}
    	}
    }
    

    Bito / take. Case 2. If the opponent has made more moves than the player, the player can take all the cards from the playing field:

    else if (opponentSteps > playerSteps) // забрать все карты себе
    {
    	for (int i = 0; i < fullAmount; i++)
    	{
    		if ((deckCards[i].field < 0) && (deckCards[i].property != -2))
    		{
    			addPlayerCard(playerId, i);
    		}
    	}
    }
    

    Step 4. Draw a card

    Because of excellent standard functions for working with the built-in display from the library M5STACK, drawing cards will take a fraction of seconds and almost does not take up the memory of the device.

    void drawCard(int x, int y, playCard card) {
    	M5.Lcd.fillRoundRect(x, y, 30, 50, 5, 0xffff);
    	M5.Lcd.drawRoundRect(x, y, 30, 50, 5, 0x00);
    	M5.Lcd.setTextSize(2);
    	M5.Lcd.setTextColor(colors[card.color]);
    	M5.Lcd.setCursor((x + 3), (y + 6));
    	M5.Lcd.print(cards[card.card]);
    	M5.Lcd.setTextSize(3);
    	M5.Lcd.setCursor((x + 8), (y + 24));
    	M5.Lcd.print(suits[card.suit]);
    }
    
    void drawEmptyCard(int x, int y) {
      M5.Lcd.fillRect(x, y, 30, 50, 0x2589);
      M5.Lcd.drawRoundRect(x, y, 30, 50, 5, 0x00);
    }
    

    Life hack for Windows users: try to go to any text editor and hold down the Alt key on the keyboard and type one of the digits from 3 to 6, then release the pressed keys.

    Step 5. Write motion animation cards

    void drawSelect(playCard card) {
    	int n = card.field - 1;
    	int x = 10 + (n * 30);
    	int y = (yPlayerField - 10);
    	drawEmptyCard(x, yPlayerField);
    	drawCard(x, y, card);
    }
    
    void clearSelect(playCard card) {
    	int n = card.field - 1;
    	int x = 10 + (n * 30);
    	int y = (yPlayerField - 10);
    	M5.Lcd.fillRect(x, y, 30, 50, 0x2589);
    	drawCard(x, yPlayerField, card);
    }
    

    Step 6. Draw the game table and menu

    void drawGameTable() {
    	M5.Lcd.fillScreen(0x2589); // green table
    	drawAllFields();
    }
    
    void drawMenu() {
    	M5.Lcd.fillRect(0, 0, 320, 20, 0x00); // score bar
    	M5.Lcd.fillRect(0, 220, 320, 20, 0x00); // menu bar
    	/* menu buttons */
    	M5.Lcd.setTextSize(2);
    	M5.Lcd.setTextColor(0x7bef);
    	M5.Lcd.setCursor(62, 223);
    	M5.Lcd.print("G");
    	M5.Lcd.setCursor(155, 223);
    	M5.Lcd.print("A");
    	M5.Lcd.setCursor(247, 223);
    	M5.Lcd.print("S");
    }
    

    Step 7. Update graphics

    void updateGraphics() {
    	drawGameTable();
    	drawMenu();
    	drawPlayerId();
    	drawRest();
    	for (int i = 0; i < fullAmount; i++)
    	{
    		if (deckCards[i].property == playerId)
    		{
    	  		if (deckCards[i].field > 0) // if in the hands of
    				drawPlayerCard(deckCards[i]); 
    	  		else // if in the playing field
    				drawPlPfCard(deckCards[i]); 
    		}
    		else if (deckCards[i].property == getOpponentId())
    		{
    	  		if (deckCards[i].field < 0)
    				drawOpPfCard(deckCards[i]); // draw opponent's cards in the playing field
    		}
    		else if (deckCards[i].property == 0)
    		{
    	  		drawTrumpCard(deckCards[i]);
    		}
    	}
    }
    

    Additional functions (for example, drawPlayerId()) You can see the full sketch or write your own it is much better ;)

    Step 8. We have to make functions for receiving/sending data through the serial port

    Figure 5.3

    The send string function appends a newline character to the end of the string received as an argument and sends it to the serial port. Then trying to take a string containing a symbol of the end of data transfer. If the character has come, the function will return true otherwise false.

    bool serialWriteStr(String str) {
    	Serial.print(str);
    	Serial.print('\n');
    	String input = serialReadStr(); 
    	if (input != "")
    	{
    		if (input[0] == (char)0x04) return true;
    	}
    	return false;
    }
    

    The receive string function attempts to accept the string during timeout (3000 milliseconds), clearing the garbage from the beginning to the character of the beginning of the package. In case of failure, returns an empty string.

    String serialReadStr() {
    	String buf = "";
    	long timeout = 3000;
      	long previousMillis = millis();
      	while (true)
      	{
    		unsigned long currentMillis = millis();
    		if (currentMillis - previousMillis > timeout) break;
    		if (Serial.available() > 0)
    		{
    	  		char chr = (char)Serial.read();
    	  		if ((chr != (char)0x16) && (chr != (char)0x04) && (buf == "")) // clear trash
    	  		{
    				Serial.read();
    	  		}
    	  		else
    	  		{
    				if (chr == '\n')
    	  				break;
    				else
    	  				buf += chr;
    	  		}  
    		}
    	}	
    	if (buf != "")
      	{
    		Serial.print((char)0x04);
    		Serial.print('\n');
      	}
      	return buf;
    }
    

    Step 9. Do processing of incoming data packets

    Make it so that any information transmitted between the devices were Packed in special packages (Fig. 6). Write a function that accepts, decompresses, and executes packages. And also write an auxiliary function (parseString), which will allow to extract a certain portion of the string enclosed between special characters separators (similar to the Split method of JS).

    Figure 6. The structure of the data packet

    bool serialRecivePacket() {
    	String input = serialReadStr(); 
    	if (input.length() > 0)
      	{
    		if ((char)input[0] == (char)0x16) 
    		{
    			input[0] = ' ';
    			char groupType = ((parseString(0, (char)0x1d, input)).toInt()) + '0';
    			String groupData = parseString(1, (char)0x1d, input);
    			int groupDataLenmo = (groupData.length() - 1);
    			if (groupData[groupDataLenmo] == (char)0x03)  
    			{
    				groupData[groupDataLenmo] = ' '; 
    				if (groupType == (char)0x31) updateReciveDeckCards(groupData); 
    				else if (groupType == (char)0x32) playerId = 2; 
    				return true;
    		  	}
    		}
    	return false;
    	}
    }
    
    String parseString(int idSeparator, char separator, String str) { // like split JS
    	String output = "";
    	int separatorCout = 0;
    	for (int i = 0; i < str.length(); i++)
    	{
    		if ((char)str[i] == separator)
    		{
      			separatorCout++;
    		}
    		else
    		{
      			if (separatorCout == idSeparator)
      		{
    			output += (char)str[i];
      		}
      		else if (separatorCout > idSeparator)
      		{
    			break;
      		}
    	}
    	return output;
    }
    

    Example of using the parseString function:

    parseString(1,':', 'cat:Bob, Jack:dog: Martin, Kelvin'); - - - - - > Bob, Jack

    parseString(0,',', 'Bob, Jack'); - - - - > Bob

    parseString(1,',', 'Bob, Jack'); - - - - > Jack

    Step 10. We implement a system specifying the number of the player

    When you turn on the device and connect the cable, each of the devices can withstand a random time interval, while listening to incoming packets, and sends the package " I'm here!" (rice. 7). By default, both devices are the first players. The device that first receives the message " I'm here!" immediately appoints himself the number 2 player.

    Figure 7. The principle of the handshake to obtain the player's number

    void handshakePlayerId() {
    	long tpid = random(10, 1001) * 10;
    	long previousMillis = millis();
    	while (!serialRecivePacket())
    	{
    		unsigned long currentMillis = millis();
    		if (currentMillis - previousMillis > tpid) break;
    	}
    	while (!serialSendPlayerTId());
    }
    
    bool serialSendPlayerTId() {
    	String str = "";
    	str += (char)0x16; 
    	str += (char)0x32; // type "player id selector"
    	str += (char)0x1d;
    	str += (char)0x03; 
    	return serialWriteStr(str);
    }
    

    Step 11. Pack and unpack the package with the card

    bool serialSendDeckCards() {
    	String str = "";
    	str += (char)0x16; 
    	str += (char)0x31; // type "card transfer flag"
    	str += (char)0x1d; 
    	for (int i = 0; i < fullAmount; i++)
    	{
    		str += (char)0x1e; 
    	    str += deckCards[i].card;
    	    str += (char)0x1f; 
    	    str += deckCards[i].suit;
    	    str += (char)0x1f;
    	    str += deckCards[i].color;
    	    str += (char)0x1f;
    	    str += deckCards[i].property;
    	    str += (char)0x1f;
    	    str += deckCards[i].field;
      	}
      	str += (char)0x03; 
      	return serialWriteStr(str);
    }
    
    void updateReciveDeckCards(String groupData) {
    	for (int i = 0; i < fullAmount; i++)
    	{
    	    /* update new card */
    	    String record = parseString(i, (char)0x1e, groupData);
    	    deckCards[i].card = (int8_t)(parseString(0, (char)0x1f, record).toInt());
    	    deckCards[i].suit = (int8_t)(parseString(1, (char)0x1f, record).toInt());
    	    deckCards[i].color = (int8_t)(parseString(2, (char)0x1f, record).toInt());
    	    deckCards[i].property = (int8_t)(parseString(3, (char)0x1f, record).toInt());
    	    deckCards[i].field = (int8_t)(parseString(4, (char)0x1f, record).toInt());
      	}
    	getTrumpCard();
    	updateGraphics();
    	checkWinner();
    }
    

    Additional functions (for example, getTrumpCard()) You can see the full sketch or write your own - it is much better ;)

    Step 12. Let's write synchronization function

    In fact, the synchronization function is key in this project. The purpose of the synchronization function is to respond to the actions of players and the exchange of game information. The function is called in manual and automatic mode of the loop(). When the player's turn feature works in manual mode and is executed only after the player's actions. At the same time, the same function works automatically on the opponent's device.

    void sync(bool auto_ = false) {
    	if (queue != playerId) 
    	{
    		while (!serialRecivePacket());
    		queue = playerId;
    	}
    	else
    	{
        	if (!auto_)
        	{
    	      while (!serialSendDeckCards());
    	      updateGraphics();
    	      checkWinner();
    	      queue = getOpponentId();
        	}
      	}
    }
    

    Step 13. Who won?

    The whoWin () function returns the number of the player who won or -1 if no one won.
    According to the rules of the game it is believed that if the player does not have a single card, he won. If the player has 10 (playerFieldAmount) and more cards, he lost.

    int whoWin() {
    	int opponentId = getOpponentId();
    	int playerAmount_ = getAmount(playerId);
    	int opponentAmount = getAmount(opponentId);
    	if ((playerAmount_ == 0) || (opponentAmount >= playerFieldAmount)) return playerId;
    	if ((opponentAmount == 0) || (playerAmount_ >= playerFieldAmount)) return opponentId;
    	return -1; 
    }
    
    void checkWinner() {
    	int winner = whoWin();
    	if (winner != -1) drawWinnerShow(winner);
    }
    

    Step 14. Let's put all the code together and load it into the device

    Note: this is my first game project, so the code is currently in beta that may contain bugs and errors. Thank you for understanding!

    Download a sketch to Arduino IDE in the bottom of the lesson in the Downloads section.

    Step 15. Launch

    Connect the devices using the serial port (Fig. 5.1). And press the red buttons on both devices (Fig. 8). Let's try to play! (rice. 8.1).

    Figure 8. The game's first run

    Figure 8.1. Play!

    Downloads

    The sketch can be downloaded here https://yadi.sk/d/i3fwMYoK3SUEoB