本博客为资源:基于STM32F407的五子棋游戏设计内的说明文档。

目录

一、设计目标

三、设计方案

1.游戏模式

2.游戏过程

3.游戏设计

四、硬件配置

1.TFT-LCD液晶屏模块

(1)工作原理

(2)硬件连线

(3)配置步骤

(4)相关库函数

2.触摸屏模块

(1)工作原理

(2)硬件连线

(3)模块初始化

(4)相关库函数

3.LED灯

(1)工作原理

(2)硬件连接

(3)模块初始化

4.蜂鸣器

(1)工作原理

(2)硬件连接

(3)模块初始化

5.RNG模块

(1)工作原理

(2)模块初始化

(3)相关库函数

6.定时器模块

(1)工作原理

(2)配置步骤

(3)模块初始化

7.独立看门狗模块

(1)工作原理

(2)配置步骤

(3)模块初始化

(4)相关库函数

四、软件设计

1.定时器中断服务程序

2.主程序

3.游戏主程序

3.五子棋算法

(1)算法1

(2)算法2

五、实现效果


一、设计目标

基于ALIENTEK 探索者 STM32F407开发板,设计一款五子棋游戏。

通过LCD液晶屏显示游戏画面,通过触摸屏幕进行游戏模式选择、落子、暂停游戏、重新开始游戏、悔棋等操作。

编写五子棋算法,实现人机对战功能。

三、设计方案

1.游戏模式

五子棋游戏分为PVE(Player VS Environment的简称,即人机对战)、PVP(Player VS Player的简称,即人人对战)两个模式。在PVE模式中,玩家持黑子先下,电脑玩家持白子后下。在PVP模式中,两玩家分别持黑白子,先下者为黑子。

2.游戏过程

在进入游戏时,首先显示主菜单。玩家在PVE模式和PVP模式中进行选择。其中,PVE模式又分为简单、一般和困难三个模式。选择模式后即可进入游戏。在游戏中,点击棋盘相应空子位置即可落子,落子时蜂鸣器短暂鸣叫,提示已落子。可以通过LED灯和屏幕下方提示,获悉当前落子玩家。若DS0亮起,则到左手玩家(即黑子)落子。若DS1亮起,则到右手玩家(即白子)落子。可以通过点击左上角UNDO键进行悔棋操作,回到上一次落子前状态。可以通过上方HELP键由电脑帮助落子,该电脑下棋水平等同于PVE模式中的困难模式。也可以通过右上角PAUSE键暂停游戏,暂停时会显示暂停对话框,可以选择重新开始游戏、退出游戏或者关闭对话框。

当有一方五子连珠时,蜂鸣器会有节奏长鸣,然后弹出游戏结束对话框。此时也可以选择重新开始游戏或者退出游戏。

3.游戏设计

    五子棋游戏棋盘为15*15,共有225个落子位置,可使用15*15的数组来存储落子情况。数组初始全为0,按落子顺序,对每枚棋子编号,从1开始,将编号存储在数组对应位置。按黑子先下原则,所有单数编号的棋子均为黑子,所有双数编号的棋子均为白子。上一编号为双数,则下一落子为黑子玩家,反之亦然。而悔棋即将数组中存储上一编号的位置置零,重新落子。

四、硬件配置

围绕上述方案,在五子棋游戏中,使用STM32F407开发板相关硬件,实现以下功能:

(1)完成LCD液晶屏驱动程序的设计,使用LCD显示五子棋游戏内容。

(2)完成触摸屏驱动程序的设计,使用触摸屏进行游戏控制。

(3)使用定时器、LED和蜂鸣器进行游戏相关提示。

(4)使用独立看门狗来防止程序跑飞。

本部分会详细叙述上述硬件资源的相关工作原理、硬件电路的连接、配置步骤和相关库函数,具体如下所示:

1.TFT-LCD液晶屏模块

本设计采用4.3寸LCD屏幕显示游戏内容,屏幕像素数为800*480,可以调用相关硬件函数绘制点、线、圆或矩阵。

(1)工作原理

TFT-LCD 即薄膜晶体管液晶显示器。TFT-LCD 与无源 TN-LCD、STN-LCD 的简单矩阵不同,它在液晶显示屏的每一个像素上都设置有一个薄膜晶体管(TFT),可有效地克服非选通时的串扰,使显示液晶屏的静态特性与扫描线数无关,因此大大提高了图像质量。

其模块原理图如图1所示。

 

图 1 TFTLCD 4.3寸原理图

STM32F4使用FSMC来驱动TFT-LCD。FSMC是灵活的静态存储控制器,能够与同步或异步存储器和16位PC存储器卡连接。FSMC可以驱动LCD的主要原因是因为FSMC的读写时序和LCD的读写时序相似,于是把LCD当成一个外部存储器来用。利用FSMC在相应的地址读或写相关数值时,STM32F4的FSMC会在硬件上自动完成时序上的控制,所以只要设置好读写相关时序寄存器后,FSMC就可以完成时序上的控制。

(2)硬件连线

下面是TFTLCD 模块与 ALIETEK 探索者STM32F4 开发板的连接,探索者 STM32F4 开发板底板的 LCD 接口和 ALIENTEK TFTLCD 模块直接可以对插,连接关系如图2所示。

图 2 TFTLCD接口

在硬件上,TFTLCD 模块与探索者 STM32F4 开发板的 IO 口对应关系如下:

LCD_BL(背光控制)对应 PB0;LCD_CS 对应 PG12 即 FSMC_NE4;LCD _RS 对应 PF12 即 FSMC_A6;LCD _WR 对应 PD5 即 FSMC_NWE;LCD _RD 对应 PD4 即 FSMC_NOE;LCD _D[15:0]则直接连接在 FSMC_D15~FSMC_D0;

(3)配置步骤

初始化函数为 LCD_Init,该函数先初始化 STM32 与TFTLCD 连接的 IO 口,并配置 FSMC 控制器,然后读取 LCD 控制器的型号,根据控制 IC 的型号执行不同的初始化代码,其步骤如下:

① 使能 PD,PE,PF,PG 时钟

RCC_AHB1PeriphClockCmd();

使能 FSMC 时钟,

RCC_AHB3PeriphClockCmd();

② GPIO 初始化:

GPIO_Init()

③ 设置引脚复用映射。

④ FSMC 初始化:

FSMC_NORSRAMInit()

⑤ FSMC 使能:

FSMC_NORSRAMCmd()

⑥ 不同的 LCD 驱动器的初始化代码。

(4)相关库函数

void LCD_Clear(u16 Color);  //清屏
void LCD_DrawPoint(u16 x,u16 y); //画点
void LCD_Fast_DrawPoint(u16 x,u16 y,u16 color); //快速画点
void LCD_Draw_Circle(u16 x0,u16 y0,u8 r);   //画圆
void LCD_DrawLine(u16 x1, u16 y1, u16 x2, u16 y2);  //画线
void LCD_DrawRectangle(u16 x1, u16 y1, u16 x2, u16 y2);    //画矩形
void LCD_Fill(u16 sx,u16 sy,u16 ex,u16 ey,u16 color);   //填充单色
void LCD_Color_Fill(u16 sx,u16 sy,u16 ex,u16 ey,u16 *color);  //填充指定颜色
void LCD_ShowChar(u16 x,u16 y,u8 num,u8 size,u8 mode);  //显示一个字符
void LCD_ShowNum(u16 x,u16 y,u32 num,u8 len,u8 size);  //显示一个数字
void LCD_ShowxNum(u16 x,u16 y,u32 num,u8 len,u8 size,u8 mode);   //显示 数字
void LCD_ShowString(u16 x,u16 y,u16 width,u16 height,u8 size,u8 *p); //显示一个字符串,12/16字体

2.触摸屏模块

本设计使用的是4.3寸的投射式电容触摸屏,用来进行游戏相关触摸操作。

(1)工作原理

投射式电容触摸屏采用纵横两列电极组成感应矩阵,来感应触摸。以两个交叉的电极矩阵,即:X 轴电极和 Y 轴电极,来检测每一格感应单元的电容变化。

图 3 投射式电容屏电极矩阵

电容触摸屏一般都需要一个驱动 IC 来检测电容触摸,且一般是通过 IIC 接口输出触摸数据的。ALIENTEK 4.3寸 TFTLCD 模块使用 GT9147 作为驱动 IC,采用 17*10 的驱动结构(10 个感应通道,17 个驱动通道)。

(2)硬件连线

TFTLCD 模块的触摸屏(电阻触摸屏)总共有 5 跟线与 STM32F4 连接,连接电路图如4图所示。

图 4 触摸屏与STM32F4的连接图

(3)模块初始化

触摸屏初始化函数:TP_Init,该函数根据 LCD 的 ID(即 lcddev.id)判别是电阻屏还是电容屏,执行不同的初始化,该函数代码如下:

/触摸屏初始化
//返回值:0,没有进行校准 1,进行过校准
u8 TP_Init(void)
{if(lcddev.id==0X5510) //电容触摸屏{if(GT9147_Init()==0) //是 GT9147?{tp_dev.scan=GT9147_Scan; //扫描函数指向 GT9147 触摸屏扫描}else{OTT2001A_Init();tp_dev.scan=OTT2001A_Scan;//扫描函数指向 OTT2001A 触摸屏扫描}tp_dev.touchtype|=0X80; //电容屏tp_dev.touchtype|=lcddev.dir&0X01;//横屏还是竖屏return 0;} else if(lcddev.id==0X1963){FT5206_Init();tp_dev.scan=FT5206_Scan; //扫描函数指向 GT9147 触摸屏扫描tp_dev.touchtype|=0X80; //电容屏tp_dev.touchtype|=lcddev.dir&0X01;//横屏还是竖屏return 0;} else{RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB|RCC_AHB1Periph_GPIOC|RCC_AHB1Periph_GPIOF, ENABLE);//使能 GPIOB,C,F 时钟//GPIOB1,2 初始化设置GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_2;//PB1/2 设置为上拉输入GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN;//输入模式GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;//推挽输出GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;//100MHzGPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;//上拉GPIO_Init(GPIOB, &GPIO_InitStructure);//初始化GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;//PB0 设置为推挽输出GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;//输出模式GPIO_Init(GPIOB, &GPIO_InitStructure);//初始化GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13;//PC13 设置为推挽输出GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;//输出模式GPIO_Init(GPIOC, &GPIO_InitStructure);//初始化GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;//PF11 设置推挽输出GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;//输出模式GPIO_Init(GPIOF, &GPIO_InitStructure);//初始化TP_Read_XY(&tp_dev.x[0],&tp_dev.y[0]);//第一次读取初始化AT24CXX_Init(); //初始化 24CXXif(TP_Get_Adjdata()) return 0;//已经校准else //未校准?{LCD_Clear(WHITE);//清屏TP_Adjust(); //屏幕校准TP_Save_Adjdata();}TP_Get_Adjdata();}return 1;
}

(4)相关库函数

tp_dev.scan(0);//触摸屏扫描函数

3.LED灯

本设计使用LED灯来提示玩家落子,DS0和DS1分别对应一位玩家。

(1)工作原理

发光二极管简称为LED。发光二极管是由一个PN结组成,也具有单向导电性。当给发光二极管加上正向电压后,从P区注入到N区的空穴和由N区注入到P区的电子,在PN结附近数微米内分别与N区的电子和P区的空穴复合,产生自发辐射的荧光。

在STM32F4中,使用GPIO的IO口驱动LED。

(2)硬件连接

图 5 LED硬件连接

(3)模块初始化

//初始化PF9和PF10为输出口.并使能这两个口的时钟
//LED IO初始化
void LED_Init(void)
{        GPIO_InitTypeDef  GPIO_InitStructure;RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF, ENABLE);//使能GPIOF时钟//GPIOF9,F10初始化设置GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_10;//LED0和LED1对应IOGPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;//普通输出模式GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;//推挽输出GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;//100MHzGPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;//上拉GPIO_Init(GPIOF, &GPIO_InitStructure);//初始化GPIOGPIO_SetBits(GPIOF,GPIO_Pin_9 | GPIO_Pin_10);//GPIOF9,F10设置高,灯灭
}

4.蜂鸣器

本设计采用蜂鸣器的短鸣提示玩家落子,有节奏长鸣提示玩家游戏结束。

(1)工作原理

蜂鸣器是一种一体化结构的电子讯响器,采用直流电压供电,广泛应用于计算机、打印机、复印机、报警器、电子玩具、汽车电子设备、电话机、定时器等电子产品中作发声器件。蜂鸣器主要分为压电式蜂鸣器和电磁式蜂鸣器两种类型。探索者 STM32F4 开发板板载的蜂鸣器是电磁式的有源蜂鸣器,如图6所示。

 

图 6 有源蜂鸣器

在STM32F4中,使用GPIO的IO口驱动蜂鸣器。

(2)硬件连接

 

图 7 硬件连接

(3)模块初始化

/初始化 PF8 为输出口
//BEEP IO 初始化
void BEEP_Init(void)
{GPIO_InitTypeDef GPIO_InitStructure;RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF, ENABLE);//使能 GPIOF 时钟//初始化蜂鸣器对应引脚 GPIOF8GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;//普通输出模式GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;//推挽输出GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;//100MHzGPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_DOWN;//下拉GPIO_Init(GPIOF, &GPIO_InitStructure);//初始化 GPIOGPIO_ResetBits(GPIOF,GPIO_Pin_8); //蜂鸣器对应引脚 GPIOF8 拉低,
}

5.RNG模块

本设计在五子棋算法中,会使用随机数来选择随机选择分值相同的落子点,需要使用到RNG模块来生成随机数。

(1)工作原理

STM32F4 自带了硬件随机数发生器(RNG),RNG 处理器是一个以连续模拟噪声为基础的

随机数发生器,在主机读数时提供一个 32 位的随机数。STM32F4 的随机数发生器框图如图8所示。

 

图 8 随机数发生器(RNG)框图

STM32F4 的随机数发生器(RNG)采用模拟电路实现。此电路产生馈入线性反馈移位寄存

器 (RNG_LFSR) 的种子,用于生成 32 位随机数。

该模拟电路由几个环形振荡器组成,振荡器的输出进行异或运算以产生种子。RNG_LFSR由专用时钟 (PLL48CLK) 按恒定频率提供时钟信息,因此随机数质量与 HCLK 频率无关。当将大量种子引入 RNG_LFSR 后,RNG_LFSR 的内容会传入数据寄存器 (RNG_DR)。

同时,系统会监视模拟种子和专用时钟 PLL48CLK,当种子上出现异常序列,或 PLL48CLK时钟频率过低时,可以由 RNG_SR 寄存器的对应位读取到,如果设置了中断,则在检测到错误时,还可以产生中断。

(2)模块初始化

//初始化 RNG
//返回值:0,成功;1,失败
u8 RNG_Init(void)
{u16 retry=0;RCC_AHB2PeriphClockCmd(RCC_AHB2Periph_RNG, ENABLE); //开启 RNG 时钟RNG_Cmd(ENABLE); //使能 RNGwhile(RNG_GetFlagStatus(RNG_FLAG_DRDY)==RESET&&retry<10000)//等待就绪{ retry++; delay_us(100);}if(retry>=10000) return 1;//随机数产生器工作不正常return 0;
}

(3)相关库函数

//得到随机数
//返回值:获取到的随机数
u32 RNG_Get_RandomNum(void);
//生成[min,max]范围的随机数
int RNG_Get_RandomRange(int min,int max);

6.定时器模块

本设计通过定时器产生100ms的脉冲,用来控制LED的闪烁频率。

(1)工作原理

STM32F4 的通用定时器包含一个 16 位或 32 位自动重载计数器(CNT),该计数器由可编程预分频器(PSC)驱动。STM32F4 的通用定时器可以被用于:测量输入信号的脉冲长度(输入捕获)或者产生输出波形(输出比较和 PWM)等。 使用定时器预分频器和 RCC 时钟控制器预分频器,脉冲长度和波形周期可以在几个微秒到几个毫秒间调整。

(2)配置步骤

①使能定时器时钟。

RCC_APB1PeriphClockCmd();

②初始化定时器,配置ARR、PSC、CR1。

TIM_TimeBaselnit();

③开启定时器中断,配置NVIC。

TIM_ITConfig();NVIC_Init();

④使能定时器。

TIM_Cmd();

⑤编写中断服务函数。

TlMx_IRQHandler();

(3)模块初始化

//通用定时器 3 中断初始化
//arr:自动重装值。 psc:时钟预分频数
//定时器溢出时间计算方法:Tout=((arr+1)*(psc+1))/Ft us.
//Ft=定时器工作频率,单位:Mhz
//这里使用的是定时器 3!
void TIM3_Int_Init(u16 arr,u16 psc)
{TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;NVIC_InitTypeDef NVIC_InitStructure;RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE); //①使能 TIM3 时钟TIM_TimeBaseInitStructure.TIM_Period = arr; //自动重装载值TIM_TimeBaseInitStructure.TIM_Prescaler=psc; //定时器分频TIM_TimeBaseInitStructure.TIM_CounterMode=TIM_CounterMode_Up; //向上计数模式TIM_TimeBaseInitStructure.TIM_ClockDivision=TIM_CKD_DIV1;TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitStructure);// ②初始化定时器 TIM3TIM_ITConfig(TIM3,TIM_IT_Update,ENABLE); //③允许定时器 3 更新中断NVIC_InitStructure.NVIC_IRQChannel=TIM3_IRQn; //定时器 3 中断NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=0x01; //抢占优先级 1NVIC_InitStructure.NVIC_IRQChannelSubPriority=0x03; //响应优先级 3NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;NVIC_Init(&NVIC_InitStructure);// ④初始化 NVICTIM_Cmd(TIM3,ENABLE); //⑤使能定时器 3
}

7.独立看门狗模块

本设计用独立看门狗来防止程序跑飞,在主程序和游戏主程序的主循环中进行喂狗。

(1)工作原理

在由单片机构成的徽型计算机系统中,由于单片机的工作常常会受到来自外界电磁场的干扰,造成程序的跑飞,而陷入死循环,程序的正常运行被打断,由单片机控制的系统无法继续工作,会造成整个系统的陷入停滞状态,发生不可预料的后果,所以出于对单片机运行状态进行实时监测的考虑,便产生了一种专门用于监测单片机程序运行状态的模块或者芯片,俗称着门狗”(watch dog)。

在键值寄存器( IwDG_KR)中写入0xcccc(启动),开始启用独立看门狗。此时计数器开始从其复位值OxFFF递减,当计数器值计数到尾值Ox000时会产生一个复位信号(IwDG_RESET)。

无论何时,只要往键值寄存器IwDG_KR中写入OxAAAA(俗称喂狗),自动重装载寄存器wDG_RLR的值就会重新加载到计数器,从而避免看门狗复位。

如果程序异常,就无法正常喂狗,则导致系统复位。

(2)配置步骤

①取消寄存器写保护:

IWDG _WriteAccessCmd();

②设置独立看门狗的预分频系数,确定时钟:

IWDG _SetPrescaler();

③设置看门狗重装载值,确定溢出时间:

IWDG _SetReload();

Tout=(4×2^prerxrlr)/32

④使能(启动)看门狗

IWDG _Enable();

⑤应用程序喂狗:

IWDG_ReloadCounter();

(3)模块初始化

void lwDG_lnit(u8 prer,u16 rlr)
{IWDG_WriteAccessCmd(lwDG_writeAccess_Enable);//关写保护IWDG_SetPrescaler(prer);//分频系数IWDG_SetReload(rlr)//重载值IWDG_Enable();//启用(使能)看门狗,写OxccCC到KRIWDG_ReloadCounter();//重载(喂狗),写OxAAAA到KR
}

(4)相关库函数

void IWDG_Feed(void);//喂狗

四、软件设计

本部分围绕程序主要代码,介绍具体的实现过程。

1.定时器中断服务程序

定时器中断服务程序负责控制LED闪烁提示当前落子玩家,控制灯闪烁频率为1hz。

extern int Game_status,ChessIdx;
int cnt=0;
//定时器3中断服务程序
void TIM3_IRQHandler(void)
{                                   if(TIM3->SR&0X0001){if(cnt==9){cnt=0;if(Game_status==0){//游戏中if(ChessIdx%2==0){LED0=!LED0;LED1=1;}else{LED1=!LED1;LED0=1;}        }else{if(LED0||LED1){LED0=1;LED1=1;}} }else cnt++;}TIM3->SR&=~(1<<0);//清除中断标志位
}

2.主程序

主程序代码主要负责初始化各个模块,并进行主循环。在主循环中,控制LCD液晶屏显示主菜单或对话框,并扫描触摸屏,获取用户的操作,完成主菜单选择。

下面是主程序相关的变量和函数。

int Menu_UI=0,Menu_status=0; //菜单UI标志,菜单状态标志
extern int Game_PVEDiff; //电脑难度
void DrawMenuUI(void) //绘制菜单UI
void DrawSelectDiffDialog(void) //绘制难度选择对话框

下面是主程序的代码。

int main(void)
{    Stm32_Clock_Init(336,8,2,7);//设置时钟,168Mhz uart_init(84,115200);        //初始化串口波特率为115200   delay_init(168);            //延时初始化      LED_Init();                    //初始化LEDLCD_Init();                 //LCD初始化 KEY_Init();                //按键初始BEEP_Init();        //初始化蜂鸣器IWDG_Init(4,2000); //溢出时间4秒TIM3_Int_Init(1000-1,8400-1);//初始化定时器,中断间隔100mswhile(RNG_Init());          //随机数初始化tp_dev.init();              //触摸屏初始化u16 x,y;           //触摸像素的横纵坐标while(1){delay_ms(50);tp_dev.scan(0);//扫描触摸屏if(tp_dev.sta&TP_PRES_DOWN){ //获取坐标值x = tp_dev.x[0],y = tp_dev.y[0];        //主菜单模式if(Menu_status==0){if (x > 62 && x < 418&&y > 518 && y < 593){//PVPGameStart(0); //以PVP方式开始游戏Menu_UI=0; //菜单UI未绘制}if (x > 62 && x < 418&&y > 618 && y < 693){//PVEMenu_status=1;//进入对话框状态DrawSelectDiffDialog();//绘制对话框Menu_UI=0;   //菜单UI未绘制   }}//对话框模式if(Menu_status==1){int PVE_ready=0;//是否选择难度if (x > 62 && x < 418&&y > 318 && y < 393){//难度简单Game_PVEDiff=1;PVE_ready=1;}if (x > 62 && x < 418&&y > 418 && y < 493){//难度一般Game_PVEDiff=2;PVE_ready=1;}if (x > 62 && x < 418&&y > 518 && y < 593){//难度困难Game_PVEDiff=3;PVE_ready=1;}if(PVE_ready){//已经选择难度GameStart(1);//以PVE方式开始游戏Menu_status=0;}}}if(Menu_status==0&&!Menu_UI){//在主菜单状态且主菜单未绘制DrawMenuUI();Menu_UI=1;}            //喂狗IWDG_Feed();}
}

主程序的主循环用Menu_status标志区分了两个状态。当Menu_status为0时,是主菜单状态,用以选择模式;当Menu_status为1时,是对话框状态,用于选择PVE模式的难度。当选择了PVP模式后,直接进入游戏,而当选择PVE模式后,会进入对话框状态,进一步选择PVE模式难度。

定义了Menu_UI标志用以标志主菜单是否绘制,当绘制对话框或进入游戏后,Menu_UI为0,当绘制主菜单后,Menu_UI为1。每次主循环判断当在主菜单状态且主菜单未绘制,会绘制主菜单。

主循环中通过触摸屏扫描函数,获取触摸屏状态。若触摸屏已经被触摸过,则读取触摸位置的横纵坐标进行判断。当触摸位置在相关操作区域内,则进行相应操作。例如,按下PVE键,则会进入对话框状态,进一步选择游戏难度。

3.游戏主程序

下面是游戏主程序相关宏定义、变量和函数。

首先,定义了棋盘位置、大小、颜色相关的宏定义,用以绘制棋盘和落子时判断棋子位置。

//定义棋盘起始坐标
#define Board_LeftTop_X 16
#define Board_LeftTop_Y 136
#define Board_RightBottom_X     464
#define Board_RightBottom_Y     584
//棋盘大小
#define CHESSBOARD_SIZE (Board_RightBottom_X-Board_LeftTop_X)
//网格大小
#define CHESS_SIZE  (CHESSBOARD_SIZE / 14)
//棋盘颜色
#define CHESSBOARD_BGCOLOR  BROWN

接着,定义了游戏相关的宏定义和变量。其中,ChessBoard[][]数组用于存储棋子状态,而ChessIdx存储下一落子的棋子编号。Game_status存储游戏当前状态,共有5种状态,初始时为未开始。Game_mode存储当前游戏模式。Game_PVEDiff存储游戏难度。ChessX与ChessY存储落子位置。

//游戏状态
#define INACTIVE -1 //未开始
#define NORMAL 0    //正常
#define PING 1      //平局
#define FIVE 2      //五子连珠
#define PAUSE 3     //暂停//游戏模式
#define PVP 0
#define PVE 1u8 ChessBoard[15][15]; //棋盘状态
u8 ChessIdx=0; //棋子编号
int Game_status=INACTIVE; //游戏状态
int Game_mode=PVP; //游戏模式
int Game_PVEDiff; //游戏难度
int ChessX,ChessY; //落子位置(ChessX,ChessY)
最后,是其他相关函数声明。
int GetNextChess(void);//获取下一落子函数
u16 GetNextChessColor();//获取下一落子颜色函数
void DrawGameUI(void); //绘制游戏UI函数
void DrawDialog(int s);//绘制对话框函数
void DrawWaitingDialog(int t);//绘制等待对话框函数
void DrawChessBoard(void); //绘制棋盘函数
void DrawPoint(int x, int y);//绘制棋盘点函数
void DrawChess(int x, int y,int num);//绘制棋子函数
void DrawChessCircle(int x, int y);//绘制棋子提示圆函数
void DrawNextChess(void);//绘制下一棋子提示函数
void DrawFiveChess(int x, int y, int dx, int dy);//绘制五子连珠提示函数
void Sound(u16 frq);//蜂鸣器发声函数
void PlayMusic(void);//蜂鸣器奏乐函数
void ChessDownBeep(void);//落子提示函数
void UndoTheLastChess(void);//悔棋函数
//获取某一方向上同色棋子数,不可跳空
int GetLineChessNum1(int ChessColor, int x, int y, int dx, int dy);
//获取某一方向上同色棋子数,可跳空
int GetLineChessNum2(int ChessColor, int x, int y, int dx, int dy);
int IsFiveChessContant(int x, int y);//判断是否五子连珠函数
int IsGameOver(int x,int y);//判断是否游戏结束函数
int SearchLine(int ChessColor, int x, int y, int dx, int dy);//搜索棋线
int GetScore(int i, int j, int color);//获取某点分数
int BoardSearch(int *xChess, int *yChess, int ChessColor);//棋盘搜索函数
void FindBestChessLocation1(int *xChess, int *yChess);//获取电脑落子位置1
void FindBestChessLocation2(int *xChess, int *yChess);//获取电脑落子位置2
void ChessDown();//落子函数
void GameInit(void);//游戏初始化函数
void GameStart(int mod);//游戏开始函数,也就是游戏主程序

下面是游戏主程序。

/游戏开始
void GameStart(int mod)
{Game_mode=mod;LCD_Clear(WHITE);DrawWaitingDialog(500);GameInit(); tp_dev.scan(0);u16 x, y;while(1){ if(Game_status==NORMAL&&Game_mode==PVE&&GetNextChess()==WHITECHESS){//电脑走白棋if(ChessBoard[ChessX][ChessY]>0) DrawChess(ChessX,ChessY,ChessIdx);if(Game_PVEDiff==1||Game_PVEDiff==2)FindBestChessLocation1(&ChessX, &ChessY);if(Game_PVEDiff==3)FindBestChessLocation2(&ChessX, &ChessY);ChessDown();continue;}delay_ms(10);tp_dev.scan(0);if(tp_dev.sta&TP_PRES_DOWN){ //获取坐标值x = tp_dev.x[0];y = tp_dev.y[0];switch(Game_status){case NORMAL://棋盘区域if (x > Board_LeftTop_X-10 && x < Board_RightBottom_X+10 &&y > Board_LeftTop_Y-10 && y < Board_RightBottom_Y+20){int xx=(x-Board_LeftTop_X)/CHESS_SIZE,yy=(y-Board_LeftTop_Y)/CHESS_SIZE;if(ChessBoard[xx][yy]>0)  break;          DrawChess(ChessX,ChessY,ChessIdx);ChessX=xx;ChessY=yy;printf("ATTACK:%d DEFFENSE:%d",GetScore(ChessX,ChessY,GetNextChess()),GetScore(ChessX,ChessY,!GetNextChess()));ChessDown();}//悔棋按钮if (x > 0 && x < 50 &&y > 0 && y < 40){DrawWaitingDialog(500);UndoTheLastChess();if(Game_mode==PVE)UndoTheLastChess();DrawChessBoard();DrawNextChess();}//帮助按钮if(x > 200 && x < 280 &&y > 0 && y < 40){if(ChessBoard[ChessX][ChessY]>0) DrawChess(ChessX,ChessY,ChessIdx);FindBestChessLocation2(&ChessX, &ChessY);ChessDown();}//暂停按钮if (x > 430 && x < 480 &&y > 0 && y < 40){DrawDialog(Game_status);Game_status=PAUSE;}break;case PAUSE:case PING:case FIVE:if(Game_status==PAUSE&& (x > 400 && x < 438) &&(y > 330 && y < 380)){//取消暂停DrawWaitingDialog(500);DrawGameUI();Game_status=NORMAL;}if (x > 46 && x < 240 &&y > 450 && y < 514){//重新开始DrawWaitingDialog(500);GameInit();}if (x > 240 && x < 434 &&y > 450 && y < 514){//退出Game_status=INACTIVE;DrawWaitingDialog(500);return;}break;}tp_dev.scan(0);}//喂狗IWDG_Feed();}
}

游戏的主程序为GameStart()函数,由主程序调用后开始游戏。GameStart()有一个参数mode,当mode为0时,进行PVP游戏,当mode为1时,进行PVE游戏。

当游戏开始时,主程序会执行游戏初始化函数GameInit(),用以初始化15*15数组,重置游戏状态并绘制游戏UI。

//游戏初始化
void GameInit(void)
{Game_status=0;//游戏状态归0ChessIdx=0;//编号归0ChessX=-1;ChessY=-1;memset(ChessBoard, 0, sizeof(ChessBoard));DrawGameUI();//绘制游戏UI
};

接着,游戏会等待玩家落子或者电脑玩家自动落子。轮到某位玩家落子时,相应的LED灯和屏幕都会有相应提示。

当玩家触摸到棋盘中未落子位置时,就会运行落子函数。而在PVE模式时,轮到白子落子时,电脑玩家会自动落子。每次落子后,都会运行IsGameOver()函数判断游戏是否结束。而当玩家触摸到功能键时,则会进入相应的游戏状态,并调用相应功能的函数。

3.五子棋算法

本设计的五子棋算法的大致思路是对当前棋盘上所有的空子位置依次进行评分,得到一个分值矩阵,根据分值矩阵选取最优落子位置。但是因为实际周围棋子颜色的不同,判断的角度又分为两种:一种是以黑子的角度,另一种是以白子的角度。以黑子的角度判断周围黑子分布,可以得到一个分值矩阵,而以白子的角度又可以得到一个新的分值矩阵。以持黑子玩家的视角,黑子的分值矩阵可以用于进攻,而白子的分值矩阵可以用于防守。所以,需要综合以上两个分值矩阵来判断最优落子位置。

而对空子位置位置评分的依据,是空子位置周围棋子的分布情况。这里首先对所有算法所涉及的一些五子棋棋型命名,未涉及的棋型不予展示。

如下,是活棋的4种情况。

图 9 活棋

如下,是跳棋的3种情况。

图 10 跳棋

如下,是死棋的3种情况。

图 11 死棋

结合上述棋型,便能对空子位置附近的棋型进行研判,并赋予相应分值。

以下述情况为例,图中虚线位置为空子位置。若虚线位置下黑子,则无法成任一棋型,故黑子为0分。若虚线位置下白子,则能成两个死四棋型。在这种情况下,白子成五子的概率极大,所以对应白子的分值为30000分。

图 12 死四*2

其他情形不一一展示,具体分值如下。

棋型

分值

五连*1

50000

活四*1

30000

死四*2

30000

死四(或死跳四)*1+活三(或跳三)*1

20000

活三*2

20000

跳三*2

20000

活三*1+跳三*1

20000

跳四*1

10000

死四*1+活二*2

5000

活三*1+死三*1

1000

死四*1

500

活三*1

200

活二*2

100

死三*2

50

活二*1

10

死三*1

5

表 1 棋型分值

以上分值,算法只取最高值。因此,便能写出估值函数。

//获取(i,j)点下子的分数
int GetScore(int i, int j, int ChessColor)
{int DeadFour=0, DeadThree=0, DeadTwo=0,WinFive=0, AliveFour=0,
AliveThree=0, AliveTwo=0,JumpThree=0,JumpFour=0,DeadJumpFour=0;int LineStatus[4];//4条棋线状态,即棋型LineStatus[0] = SearchLine(ChessColor, i, j, 0, 1);  //上下LineStatus[1] = SearchLine(ChessColor, i, j, 1, 0);   //左右LineStatus[2] = SearchLine(ChessColor, i, j, 1, 1);   //右上对角线LineStatus[3] = SearchLine(ChessColor, i, j, 1, -1);   //右下对角线//统计各种情况的数目for (int n = 0; n < 4; n++){switch (LineStatus[n]){case 11://死跳四DeadJumpFour++;break;case 10://跳四JumpFour++;break;              case 9://跳三JumpThree++;break;                         case 8://死四DeadFour++;break;case 7://死三DeadThree++;break;case 6://死二DeadTwo++;break;case 5://五连WinFive = 1;break;case 4://活四AliveFour = 1;break;case 3://活三AliveThree++;break;case 2://活二AliveTwo++;break;default:break;}}if(WinFive) return 50000;if(AliveFour) return 30000;if(DeadFour>=2) return 30000;if((DeadFour||DeadJumpFour)&&(AliveThree||JumpThree)) return 20000;if(AliveThree>=2) return 20000;if(JumpThree>=2) return 20000;if(JumpThree&&AliveThree) return 20000;if(JumpFour) return 10000;if(DeadFour&&(AliveTwo>=2)) return 5000;if(AliveThree&&DeadThree) return 1000;if(DeadFour==1) return 500;if(AliveThree==1) return 200;if(AliveTwo>=2) return 100;if(DeadThree>=2) return 50;if(AliveTwo==1) return 10;if(DeadThree==1) return 5;return 1;
}

下面便是对两个分值矩阵的处理的不同方法所产生的的不同算法。

(1)算法1

分别以黑子和白子的视角,得到两个分值矩阵,取两个分值矩阵中的最大点为最佳落子位置。具体算法如下。

//棋盘搜索
int BoardSearch(int *xChess, int *yChess, int ChessColor)
{if(ChessColor!=BLACKCHESS&&ChessColor!=WHITECHESS) return 0;int score,MaxScore = 0;//计算整个棋盘  for(int x=0;x<15;x++)for(int y=0;y<15;y++)if(ChessBoard[x][y]==0) {score=GetScore(x,y,ChessColor);//判断if(score>=50000){*xChess = x;*yChess = y;return score;}if(score>MaxScore){MaxScore = score;*xChess = x;*yChess = y;}else if(score==MaxScore){int randrom=RNG_Get_RandomRange(0, 9);if(randrom%2){MaxScore = score;*xChess = x;*yChess = y;}}}return MaxScore;
}
//xChess,yChess:棋子坐标
void FindBestChessLocation1(int *xChess, int *yChess)
{int WhiteChessScore, BlackChessScore;int wi, wj, bi, bj;WhiteChessScore = BoardSearch(&wi, &wj, WHITECHESS);BlackChessScore = BoardSearch(&bi, &bj, BLACKCHESS);//白棋有利,电脑进攻if((Game_PVEDiff==1?1.5:1)*WhiteChessScore > BlackChessScore){*xChess = wi;*yChess = wj;}else if((Game_PVEDiff==1?1.5:1)*WhiteChessScore <= BlackChessScore) //黑棋有利,电脑防守{*xChess = bi;*yChess = bj;}printf("(%d,%d) DEFFENSE:%d ATTACK:%d",*xChess,*yChess,GetScore(*xChess,*yChess,BLACKCHESS),GetScore(*xChess,*yChess,WHITECHESS));
}

这种算法虽然实现了一定程度的下棋水平,但在部分情况的落子选择不佳,水平有限。分析该算法的问题在于只片面地局限于进攻或者防守的最有利处,而忽略了两者的结合。在部分空子处落子,即可以进攻又可以防守,攻守兼备,收益大于在片面地进攻或防守的最有利处落子,所以这种算法最终实现的下棋水平并不高,故作为PVE模式的一般水平。再对其中白棋的进攻分值做加权处理,降低算法的下棋水平,作为PVE模式的简单水平。

(2)算法2

基于上一算法的问题,考虑将两个分值矩阵做加权和,合并为一个分值矩阵,再取新的分值矩阵的分值最大点为最佳落子点。按黑子先下原则,黑子有优势,故黑子分数的权重为1.2,而白子为1.0。具体函数如下。

void FindBestChessLocation2(int *xChess, int *yChess)
{int score,MaxScore = 0,tb,tw;//计算整个棋盘  for(int x=0;x<15;x++)for(int y=0;y<15;y++)if(ChessBoard[x][y]==0) {int blackScore=GetScore(x,y,BLACKCHESS),whiteScore=GetScore(x,y,WHITECHESS);score=1.2*blackScore+whiteScore;//判断if(whiteScore>=50000){*xChess = x;*yChess = y;printf("x:%d y:%d Score:%d",x,y,score);return;}if(score>MaxScore){MaxScore = score;*xChess = x;*yChess = y;}else if(score==MaxScore){int randrom=RNG_Get_RandomRange(0, 9);if(randrom%2){MaxScore = score;*xChess = x;*yChess = y;}}}printf("x:%d y:%d Score:%d DEFFENSE:%d ATTACK:%d",*xChess,*yChess,MaxScore,GetScore(*xChess,*yChess,BLACKCHESS),GetScore(*xChess,*yChess,WHITECHESS));
}

此算法最终实现的下棋水平极高,故作为PVE模式的困难水平。

五、实现效果

下面截取了几张实际运行时的情况。

图 13 主菜单

图 14 棋盘

图 15 落子

图 16 游戏结束

图 17 选择难度

​基于STM32F407的五子棋游戏设计​相关推荐

  1. android五子棋设计模板,基于android的五子棋游戏设计

    内容介绍 原文档由会员 hfnmb 发布 基于Android的五子棋游戏设计 软件工程 [摘 要]本论文主要阐述以面向对象的程序开发语言eclipse为开发工具, 基于智能手机Android之上设计一 ...

  2. 基于C#的五子棋游戏设计

    目 录 一. 毕业设计内容 3 二. 毕业设计目的 3 三. 工具/准备工作 3 四. 设计步骤和方法 3 (一) 总体设计 3 1. 总体设计思路及设计图 3 2. 界面设计 4 3. 全局变量设计 ...

  3. 基于java的五子棋游戏设计

    技术:Java.JSP等 摘要: 随着互联网迅速的发展,网络游戏已经成为人们普遍生活中不可或缺的一部分,它不仅能使人娱乐,也能够开发人的智力,就像本文所主要讲的五子棋游戏一样能挖掘人们聪明的才干与脑袋 ...

  4. 基于android的五子棋游戏的设计——毕业论文.doc,基于Android的五子棋游戏的设计——毕业论文.doc.doc...

    基于Android的五子棋游戏的设计--毕业论文.doc 躁虐方慎养娇陇榷圣枚茵另裙弧懈舅愤拱玫叙未殆鸿嗽透凝彰枝句坯败醋求惦刑退馆罗拖膨清褐兔捻吮嘘唆鞋匆九若秃纽谓跃捡夺浇居汛纠耻生瘟欣糯弹贯住编却 ...

  5. c语言五子棋开题报告,基于VC的五子棋游戏的设计与实现(附答辩记录)

    基于VC的五子棋游戏的设计与实现(附答辩记录)(包含选题审批表,任务书,开题报告,中期检查报告,毕业论文12300字,程序) 摘 要:以计算机技术和网络技术为核心的现代网络技术已在现实生活和生产中得以 ...

  6. c#五子棋实验报告_基于c#的五子棋游戏的设计与实现毕业论文.doc

    基于c#的五子棋游戏的设计与实现毕业论文 郑 州 科 技 学 院 课 程 设 计 论 文 基于C#的五子棋游戏的设计与实现 学生姓名:王新年 学 号:201015066 年级专业:10级计科二班 指导 ...

  7. 基于Android的五子棋设计与实现,毕业答辩-基于Android的五子棋游戏的设计与实现...

    ,基于安卓的五子棋游戏的设计与实现,本文中设计与开发实现的是一款基于安卓操作系统的五子棋游戏.Android作为当前智能手机市场的主要占有者,发展态势十分火热,截止2017年3月,在我国安卓市场份额达 ...

  8. 基于Java的五子棋游戏的设计与实现

    基于Java的五子棋游戏的设计 摘  要 五子棋作为一个棋类竞技运动,在民间十分流行,为了熟悉五子棋规则及技巧,以及研究简单的人工智能,决定用Java开发五子棋游戏.主要完成了人机对战和玩家之间联网对 ...

  9. C++毕业设计——基于C+++EasyX+剪枝算法的能人机对弈的五子棋游戏设计与实现(毕业论文+程序源码)——五子棋游戏

    基于C+++EasyX+剪枝算法的能人机对弈的五子棋游戏设计与实现(毕业论文+程序源码) 大家好,今天给大家介绍基于C+++EasyX+剪枝算法的能人机对弈的五子棋游戏设计与实现,文章末尾附有本毕业设 ...

最新文章

  1. hive基本操作与应用
  2. SpringBoot与SpringMVC的区别是什么?
  3. 剑指offer---反转链表
  4. 五十八、深入了解 Java 中的注解和自定义注解
  5. 在U盘上安装linux
  6. pfsense 2.2RC版本应用
  7. Codeigniter3学习笔记三(创建类库及使用原生类库)
  8. python 中实现enum
  9. android手机电量测试,Android手机app耗电量测试工具 - Gsam Battery Monitor
  10. 微信公众号——分享给朋友/分享至朋友圈(Vue)
  11. PLC温室大棚自动控制系统
  12. android+语音amr转mp3格式转换,安卓批量amr转mp3 微信amr批量转换
  13. 初学Java---运算符和语句的使用
  14. 超级牛的网站同步工具软件—端端Clouduolc
  15. Vampire:吸血鬼
  16. python运动学仿真的意义_运动学仿真和动力学仿真有什么区别和联系?
  17. 《复杂网络》复杂网络的结构及特点
  18. 足球与oracle系列(4):从巴西惨败于德国,想到,差异的RAC拓扑对比!
  19. 计算机相关的外文翻译,计算机外文翻译
  20. [sicily]部分题目分类

热门文章

  1. 广东工学院计算机教授,胡晓敏(广东工业大学计算机学院副教授)_百度百科...
  2. 华为鸿蒙研发团队负责人,走进华为北研所:EMUI 11 背后的“人因研究”到底是什么?...
  3. iOS9.3.5越狱图文教程
  4. redis的多路复用原理
  5. Weakly Supervised Video Salient Object Detection
  6. 存储连接应用服务器简单入门
  7. 小米air2se耳机只有一边有声音怎么办_盘点2020半入耳蓝牙耳机排名
  8. 软件开发团队必备管理工具
  9. 实现点击不同的按钮显示不同的内容【同一页面】web
  10. 计算机开机最快,教你如何让你的电脑快速开机