基于STM32设计的UNO卡牌游戏(双人、多人对战)

411 阅读8分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第34天,点击查看活动详情

一、前言

UNO扑克是一种起源于欧洲流行于全世界的牌类游戏。简单易学,不用动什么脑筋,适合各年龄层人士玩。

"UNO"扑克是世界上最大的玩具公司美国美泰玩具公司的代表作,在全球的销售量已超过十亿付!UNO是历史上最成功的玩具,风靡世界!在国外,酒吧和咖啡馆里,UNO是年轻人最喜欢的普及型游戏。

UNO纸牌已经风靡全球数十年,被誉为是世界上最好玩的纸牌游戏,据说由意大利一个理发师发明,其中UNO的版本众多,被加入许多新的功能,玩法更加刺激,而在此游戏中最考的是集中和反应,还有相互间的思维较量。

卡牌介绍 每副UNO牌包括:108张牌(76张数字牌,24张功能牌,8张万能牌)。其中,数字牌与功能牌有红、黄、蓝、绿4种颜色,万能牌没有颜色。

数字牌 数字牌共有10种(0、1、2、3、4、5、6、7、8、9),每种4个颜色(红、蓝、黄、绿),每种颜色的0各1张、1~9各2张,共计76张。计分时按数字大小计分。

功能牌 普通功能牌同样有四种颜色(红、蓝、黄、绿),共24张,分为禁止,反转,+2 三种,各8张。

游戏基础规则 1、决定庄家。庄家可以玩家自己决定。 2、每人发7张手牌。 3、翻开牌库顶第一张作为引导牌,庄家根据引导牌出牌,然后玩家打出的牌依次成为引导牌,即最后一张打出的牌是引导牌。 4、从庄家开始,大家轮流出牌,直至有一个人出完手上的牌。

游戏行为

出牌 出牌时,可以打出与引导牌同样颜色或图案(数字、功能)的牌,也可以使用万能牌。每次只能出1张牌。

抽牌 当无牌可出时,要从牌堆顶抽取1张牌。若这张牌可以出则可以立即打出,不能出牌则跳过。

宣言UNO 打出倒数第2张手牌时,需要宣言"UNO"(优诺),表示只剩1张手牌,这有些类似于斗地主的报单,但是在UNO中是必须做的。

事实上,以上只是UNO的其中一些规则,玩家们可以按照自己喜欢的玩法来游戏。

UNO双人玩法总结:

(1)牌堆里只有数字牌,数字牌共有10种(0、1、2、3、4、5、6、7、8、9),每种4个颜色(红、蓝、黄、绿),每种颜色的0各1张、1~9各2张,共计76张。计分时按数字大小计分。

(2)一局游戏限时3分钟,3分钟时间到达之后,谁剩下的牌少就为赢家

二、设计要求

项目名称:基于stm32的UNO游戏机设计

设计要求总结:

(1)LCD屏上做3个界面:开始界面,游戏界面,结束界面。

(2)游戏设备做两台,采用ESP8266-WIFI模块实现通信,实现联机对弈。牌先打完的就赢了。根据游戏规则去玩。

设备A当做主机,使用ESP8266创建热点,并创建游戏房间。

设备B当做从机,使用ESP8266链接设备A的热点,加入游戏房间。

设备A与设备B都连接上之后,开始发牌,游戏开始。

image-20220417165937318

image-20220417165952476

image-20220417170010804

image-20220417170027692

image-20220417170046517

image-20220428101019570

image-20220428101039376

image-20220428101054617

image-20220428101115402

image-20220428101136345

image-20220428101150996

image-20220428101210502

image-20220428101224601

三、硬件选型

3.1 STM32开发板

主控CPU采用STM32F103RCT6,这颗芯片包括48 KB SRAM、256 KB Flash、2个基本定时器、4个通用 定时器、2个高级定时器、51个通用IO口、5个串口、2个DMA控制器、3个SPI、2个I2C、1个USB、1个 CAN、3个12位ADC、1个12位DAC、1个SDIO接口,芯片属于大容量类型,配置较高,整体符合硬件选 型设计。当前选择的这款开发板自带了一个1.4寸的TFT-LCD彩屏,可以显示当前传感器数据以及一些运 行状态信息。

image-20220414092118753

3.2 LCD屏-1.44寸

image-20220414092149899

3.3 ESP8266 WIFI

(1)模块采用串口(LVTTL)与MCU(或其他串口设备)通信,内置TCP/IP协议栈,能够实现串口与WIFI之 间的转换 (2)模块支持LVTTL串口,兼容3.3V和5V单片机系统 (3)模块支持串口转WIFI STA、串口转AP和WIFI STA+WIFIAP的模式,从而快速构建串口-WIFI数据传 输方案

image-20220414092301109

3.4 电容矩阵键盘

image-20220414092410468

image-20220414092445597

四、实现思路

4.1 流程思路分析

代码分为了两个工程,分别是从机和主机。

image-20220428101442275

主机的初始化里,先完成ESP8266的初始化,配置ESP8266为AP+TCP服务器模式,创建一个热点,然后开启TCP服务器,等待从机加入。这里的从机就是玩家,玩家加入房间之后才可以进行游戏过程。

从机的初始化里,先完成ESP8266的初始化,配ESP8266为STA+TCP客户端模式,去连接指定的热点,也就是主机创建的热点,连接上之后就接着去连接服务器,连接成功,主机就进入下一步,界面上提示请按下#号按键开始游戏。这时候从机也是处于等待状态,当主机按下#号按键之后,主机就会开始洗牌Uno_shuffle(),洗牌两次之后再开始发牌Uno_DealCard(User,num),发牌完成后会在串口上打印详细的信息,发来信息,然后从牌堆里取出引导牌,接在在主机的LCD界面上完成引导牌显示、对家牌数量,定时器时间、用户ID等信息,给对家发送的牌信息都会存放在static struct clinet_InitCard clinet_init_info结构体里。下面就调用ESP8266_ServerSendData函数将这个clinet_init_info结构体发送出去;当从机收到clinet_init_info结构体之后,解析结构体数据,完成LCD屏界面绘制显示,从机数据解析处理完毕会向主机发送USER_OK指令,表示从机已经准备好可以开始游戏,主机收到USER_OK指令之后,向串口打印游戏正式开始,接着主机界面上就会提示请出牌,然后按下矩阵键盘上的按键选择牌开始出牌。牌的选择是依靠矩阵键盘上的一排排数字,当做牌的下标键,选中拍界面上也会绘制出这张牌的信息,牌选中之后,按下#号按键即可出牌。出牌后,会发送牌的信息给从机完成交互,发送函数:ESP8266_ServerSendData(0,(u8 *)&clinet_info,sizeof(clinet_info))。开始游戏过程中,从机主机之间交互的数据传输就一直使用clinet_info这个结构体变量来存放,实现双方的数据交互通信。从机收到主机发来的clinet_info信息之后,就可以选牌,界面上会显示对家出的牌,也会显示对家牌剩余数量,在串口上也会提示当前可以出的牌是哪些牌,可以在串口调试助手上查看打印的信息。从机选择牌之后,也是和主机一样按下#号按键出牌,然后再发送clinet_info结构体给主机,这样,游戏就开始正确运行了。

在主机上还有时间显示,3分钟时间到达,就算牌没有出完,游戏也会自动结束,在界面上会提示游戏结束,显示玩家的ID,谁赢了输了。

功能牌:

 /*
 P -- 禁止牌
 R --反转
 L --加2
 N --普通牌
 U --万能牌
 */
 const char card_func[]={'P','R','L','N','U'};//牌功能信息

牌堆初始化:

 void Uno_Init(struct player *pls,u8 cnt)
 {
   u8 i;
   //初始化数字牌:yellow
   for(i=0;i<19;i++)
   {
     cards[i].clr=yellow;//颜色
     cards[i].function=card_none;//功能
     cards[i].num=(i==0)?0:(i%10+i/10);//数字
   }
   //初始化数字牌:red
   for(;i<19*2;i++)
   {
     cards[i].clr=red;//颜色
     cards[i].function=card_none;//功能
     cards[i].num=(i==19)?0:((i-19)%10+(i-19)/10);//数字
   }
   //初始化数字牌:red
   for(;i<19*3;i++)
   {
     cards[i].clr=green;//颜色
     cards[i].function=card_none;//功能
     cards[i].num=(i==19*2)?0:((i-19*2)%10+(i-19*2)/10);//数字
   }  
   //初始化数字牌:blue
   for(;i<19*4;i++)
   {
     cards[i].clr=blue;//颜色
     cards[i].function=card_none;//功能
     cards[i].num=(i==19*3)?0:((i-19*3)%10+(i-19*3)/10);//数字
   }  
   //+2
   for(i=76;i<84;i++)
   {
     cards[i].clr = setClr(i%4); 
     cards[i].function=card_plus2;//加2
     cards[i].num=-1;//-1表示功能牌
   }
   //反转
   for(i=84;i<92;i++)
   {
     cards[i].clr = setClr(i%4); 
     cards[i].function=card_reverse;//反转
     cards[i].num=-1;//-1表示功能牌
   }  
   //禁止
   for(i=92;i<100;i++)
   {
     cards[i].clr = setClr(i%4); 
     cards[i].function=card_prohibit;//禁止
     cards[i].num=-1;//-1表示功能牌
   }
   //万能牌
   for(i=100;i<108;i++)
   {
     cards[i].clr =noColor; 
     cards[i].function=card_universal;//万能
     cards[i].num=-2;//-2表示万能牌
   }  
   for(i=0;i<cnt;i++)
   {
     pls[i].num=0;//玩家卡牌张数清0
   }
   pd.num=0;//牌堆的牌清0
   used.num=0;//已出牌数清0
 }

牌堆洗牌:

 void Uno_shuffle(void)
 {
   u16 ran1,ran2,i;
   struct card temp;
   for(i=0;i<UNO_NUMBER*2;i++)//洗两次
   {
     ran1=rand()%UNO_NUMBER;
     ran2=rand()%UNO_NUMBER;
     temp=cards[ran1];
     cards[ran1]=cards[ran2];
     cards[ran2]=temp;
   }
 }

发牌:

 发牌:每人7张牌
 pls --保存玩家卡牌信息
 cnt --玩家个数
 */
 void Uno_DealCard(struct player *pls,u8 cnt)
 {
   u8 i,j;
   for(i=0;i<cnt;i++)//
   {
     for(j=0;j<7;j++)//每个玩家7张牌
     {
         pls[i].cds[j]=cards[7*i+j];
     }
     pls[i].num=7;
   }
   for(i=0;i<UNO_NUMBER-7*cnt;i++)//剩下的牌信息
   {
     pd.cds[i]=cards[7*cnt+i];
   }
   pd.num=i;//剩下的牌张数
 }

摸牌和出牌:

 /***********************从牌堆中摸牌*********************
 **函数功能:从牌堆中取牌
 **pls --保存玩家牌信息
 **cnt --要取的牌张数
 **id  --玩家id
 **返回值:0xff  --摸牌失败
 **         0    --正常摸牌,当前牌不可出
 **        其它值  --当前摸的牌可出
 **********************************************************/
 u8 Uno_takeCard(struct player *pls,u8 cnt,u8 id)
 {
   u8 i=0; 
   if(pd.num<cnt)return 0xff;//剩余牌不足
   struct card temp; 
   for(i=0;i<cnt;i++)
   {
     pls[id].cds[pls[id].num]=pd.cds[pd.num-1];//从剩余牌堆中取牌
     temp=pd.cds[pd.num-1];//保存当前牌信息
     pls[id].num++;//牌张数+1
     pd.num--;//剩余牌-1
   }
   if(cnt==1)//当摸一张牌时,判断当前牌是否可出
   {
     u8 cnt=pls[id].num;
     if(Uno_JudgeCard(&temp)==0)
     {
       return cnt-1;//若当前牌可出,则返回当前牌下标
     }
   }
   return 0;
 }
 /*********************出牌**************************
 **函数功能:玩家出牌
 **pls --保存玩家牌信息
 **index --要出的牌的下标
 **id  --玩家id
 **返回值:0 -- 出牌成功
 **        1 --出牌失败
 **      
 ******************************************************/
 u8 Uno_PlayCard(struct player *pls,u8 index,u8 id)
 {
   if(index>=pls[id].num)return 1;//超出手牌范围
   /*判断是否能出*/
   if(Uno_JudgeCard(&(pls[id].cds[index])))return 1;//所选牌不能出
   /*判断是否为功能牌*/
   struct card temp;
   temp=pls[id].cds[index];
   if(temp.function != card_universal ) //当出的牌不为万能牌时
   {
      guidecare_info=temp;  /*保存当前出的牌,作为下次出牌的引导牌*/
   }
   /*将要出的牌放到已出牌堆中*/
   used.cds[used.num]=temp;
   used.num++;//已出牌数量+1
   /*将后面牌网前移动*/
   u8 i=0;
   for(i=index;i<pls[id].num-1;i++)
   {
     pls[id].cds[i]=pls[id].cds[i+1];
   }
   pls[id].num--;//玩家牌数量-1
   return 0;
 }

4.2 使用的相关数据结构

 #ifndef _UNO_H
 #define _UNO_H
 #include "stm32f10x.h"
 #include <stdlib.h>
 #include <stdio.h>
 #include "oled.h"
 #include "key.h"
 #include <string.h>
 #include "delay.h"
 #include "esp8266.h"
 extern unsigned char HZ_FONT_16[][32];
 extern int uno_time;
 #define LCD_W 127
 #define LCD_H 127
 #define UNO_NUMBER 108 //牌张数
 #define PP 4 //最高玩家数
 /*牌颜色*/
 enum color
 {
   yellow=0,
   red,
   green,
   blue,
   noColor,//没有颜色
   err//出错
 }; 
 /*卡片功能*/
 enum Type
 {
   card_prohibit=0,//禁止牌
   card_reverse,//反转
   card_plus2,//加2
   card_none,//普通牌
   card_universal//万能牌
 };
 /*每一张卡片结构体信息*/
 struct card
 {
   enum color clr;//颜色
   enum Type function;//卡片功能
   int num;//卡片数字
 };
 /*玩家手头卡片信息*/
 struct player
 {
   int num;//牌数
   struct card cds[50];//手头卡片张数
 };
 struct paidui
 {
   int num;//牌数
     struct card cds[UNO_NUMBER];
 };
 /*主从机之间交互信息*/
 struct card_clinet
 {
   struct card card_guide;//引导牌信息
   u8 id_num;//玩家剩余手牌张数
   u8 user_id;//玩家ID
   u8 pd_remain;//剩余牌堆数量
 };
 /*初始化客户端结构体信息*/
 struct clinet_InitCard
 {
   struct player user_card;//玩家卡牌信息
   struct paidui pd;//牌堆信息
   u8 user_id;//客户端ID
 };
 ​
 extern enum color colorNow;//牌颜色
 extern enum Type CardType;//卡片功能
 extern struct card cards[UNO_NUMBER];//每一张卡片结构体信息
 extern struct paidui pd,used; //pd-牌堆,used-已出
 extern struct card guidecare_info;//引导牌信息
 void Uno_Init(struct player *pls,u8 cnt);//初始化牌堆
 void Uno_shuffle(void);//洗牌
 void Uno_DealCard(struct player *pls,u8 cnt);//发牌
 u8 Uno_gameStart(u8 num);//开始游戏
 #endif