基於STM32的五子棋小遊戲
前言
之前寫過一篇文章是關於C語言實現五子棋,知道了實現五子棋的基本原理。本來打算很快的把五子棋移植到STM32F103精英版上,但是前幾周實驗室納新、期中考試、開放實驗各種事情導致一直沒有重大進展。在忙完以後終於在這個周完成了五子棋的全部功能。在調試的過程中總是會出現各種問題,然後去尋找問題、解決問題。這也是第一次一個人獨自做一個比較完備的具體項目,通過項目學到了很多知識以及知道如何具有一個系統的思維來完成這樣一個項目。
項目要求
基礎要求:實現下棋並判斷輸贏和基本的遊戲介紹
高級要求:界面美觀、可以選擇人機模式、雙人對戰模式、智能提示落子點、悔棋以及一些簡單設置。
項目總體設計
通過定義一個16*16的數組g_Chess[16][16]和一個記錄棋子數量的變量g_ChessNum。初始化棋子總數爲0且每點都沒有棋子,則初始化數組全部爲0。每當檢測到有棋子落下時棋子總數加一,而該點的座標所對應的數組的值爲下該棋子時棋子的總數。
模式選擇:定義一個變量g_Mode,初始化g_Mode=0。如果g_Mode=1則是人機模式,如果g_Mode=2則是雙人對戰模式。
下棋:判斷該點座標對應的數組的值是否爲0,如果是0則可以下棋,如果不是0則返回重新判斷。如果是人機模式,則在觸摸屏沒有按下時進行計算相關棋子座標。每下一顆棋子後判斷是否連成5顆棋子。如果連接成5顆棋子
判斷白棋或者黑棋:通過棋子總數對2求餘來判斷該點是黑棋還是白棋。
悔棋:讓該點座標對應的數組值等於0棋子總數減1,清除該棋子。如果是人機對戰,則清理最後下的兩顆棋子,棋子總數減二。
重新開始:讓數組的值都爲0以及棋子總數等於0,重新畫界面
返回:返回主界面選擇模式。
軟件實現
重要繪製圖形函數介紹
繪製按鈕函數
void DrawButton(int x, int y, int width, int height, u8* Text, char sta)
{
int iLen = strlen((char*)Text);
if (x > 239 || x < 0 || y < 0 || y > 319) return;
POINT_COLOR = WHITE;
LCD_DrawRectangle(x, y, x+width, y+height);
if (sta==1)
{
LCD_Fill(x+1, y+1, x+width-1, y+height-1, BLUE);
BACK_COLOR = BLUE;
Show_Str(x+((width-iLen*8)/2), y+((height-16)/2), 200, 16, Text, 16, 0);
}
else if(sta==0)
{
LCD_Fill(x+1, y+1, x+width-1, y+height-1, GRAY);
BACK_COLOR = GRAY;
Show_Str(x+((width-iLen*8)/2)+2, y+((height-16)/2)+2, 200, 16, Text, 16, 0);
}
else if(sta==2)
{
LCD_Fill(x+1, y+1, x+width-1, y+height-1, GREEN );
BACK_COLOR = GREEN ;
Show_Str(x+((width-iLen*8)/2)+2, y+((height-16)/2)+2, 200, 16, Text, 16, 0);
}
}
可以通過調用該函數來實現畫按鈕,通過改變參數sta的值來改變按鈕的背景顏色
sta=1 | sta=2 | sta=0 |
---|---|---|
藍色 | 綠色 | 灰色 |
繪製棋子函數
void DrawChess(int x, int y, int num, char sta)
{
uint16_t ChessColor;
uint16_t xChess;
uint16_t yChess;
if (x>15 || x<0 || y>15 || y<0)return; //超範圍了
//判斷是下棋還是覆盤
if (sta)
{
if (g_Chess[x][y]!=0)return; //已有棋子
}
//判斷棋子顏色
(num%2==0) ? (ChessColor=BLACK) : (ChessColor=WHITE);
//計算棋子座標
xChess = CHESSBOARD_START_X + (x*CHESSGRID_SIZE);
yChess = CHESSBOARD_START_Y + (y*CHESSGRID_SIZE);
LcdDrawCircleA(xChess,
yChess,
CHESSGRID_SIZE/2-1,
ChessColor,
1);
if(voice==1) //判斷燈光是否打開 若打開 每次下子蜂鳴器響100ms
{
LED0=0;
delay_ms(100);
LED0=1;
}
delay_ms(10);
if (sta == 0) return;
g_ChessNum++;
g_Chess[x][y] = g_ChessNum;
}
每畫一個棋子棋子數量加一,在判斷畫棋子前判斷是否可以下棋,同時判斷燈光是否打開。如果可以下棋且燈光打開每下一顆棋LE0閃爍100ms。
繪製棋盤界面函數
void DrawChessBoard(void)
{
int i;
//填充棋盤背景色
LCD_Fill(CHESSBOARD_START_X-5, CHESSBOARD_START_Y-5,
CHESSBOARD_END_X+5, CHESSBOARD_END_Y+5, CHESSBOARD_COLOR);
POINT_COLOR = BLACK;
//繪製邊框線
LCD_DrawRectangle(CHESSBOARD_START_X-3, CHESSBOARD_START_Y-3,
CHESSBOARD_END_X+3, CHESSBOARD_END_Y+3);
LCD_DrawRectangle(CHESSBOARD_START_X-4, CHESSBOARD_START_Y-4,
CHESSBOARD_END_X+4, CHESSBOARD_END_Y+4);
LCD_DrawRectangle(CHESSBOARD_START_X, CHESSBOARD_START_Y,
CHESSBOARD_END_X, CHESSBOARD_END_Y);
POINT_COLOR = BLACK;
//繪製垂直線
for (i = 1; i < 15; i++)
{
LCD_DrawLine(CHESSBOARD_START_X,
CHESSBOARD_START_Y + (CHESSGRID_SIZE*i),
CHESSBOARD_END_X,
CHESSBOARD_START_Y + (CHESSGRID_SIZE*i));
}
//繪製水平線
for (i = 1; i < 15; i++)
{
LCD_DrawLine(CHESSBOARD_START_X + (CHESSGRID_SIZE*i),
CHESSBOARD_START_Y,
CHESSBOARD_START_X + (CHESSGRID_SIZE*i),
CHESSBOARD_END_Y);
}
DrawButton(5, 285, 80, 25, (u8*)"重新開始", 1);
DrawButton(87, 285, 48, 25, (u8*)"悔棋", 1);
DrawButton(137, 285, 48, 25, (u8*)"幫助", 1);
DrawButton(187, 285, 49, 25, (u8*)"返回", 1);
}
重要界面函數介紹
模式選擇界面
void Main(void)//主界面函數
{
LCD_Clear(GBLUE);
BACK_COLOR = GBLUE;
POINT_COLOR = RED;
Show_Str((240 - 24*3)/2, 30, 200, 24, (u8*)"五子棋", 24, 0);
//創建按鈕
DrawButton(40, 100, 160, 40, (u8*)"人機對戰", 1);
DrawButton(40, 150, 160, 40, (u8*)"雙人對戰", 1);
DrawButton(40, 200, 160, 40, (u8*)"遊戲介紹", 1);
DrawButton(40, 250, 160, 40, (u8*)"設置", 1);
}
對戰界面
void One(void)
{
LCD_Clear(BRRED);
POINT_COLOR = WHITE;
DrawChessBoard();
BACK_COLOR = BRRED;
POINT_COLOR=BLACK;
Show_Str(34, 10, 240, 34, (u8*)"五子棋人機對戰", 24, 0);
One_test();
}
設置界面
void Setting(void)
{
POINT_COLOR = WHITE;
//繪製關於窗口
LCD_DrawRectangle(20, 110, 220, 220);
LCD_DrawRectangle(21, 111, 219, 219);
LCD_Fill(22, 112, 218, 218, BLACK);
BACK_COLOR = BLACK;
Show_Str(104, 115, 32, 16, (u8*)"設置", 16, 0);
Show_Str(40, 140, 136, 16, (u8*)"燈光", 16, 0);
Show_Str(40, 165, 136, 16, (u8*)"關於", 16, 0);
//繪製按鈕
DrawButton(150, 140, 25, 20, (u8*)"開", 2);
DrawButton(175, 140, 25, 20, (u8*)"關", 0);
DrawButton(150, 165, 50, 20, (u8*)"打開", 2);
DrawButton(90, 195, 60, 20, (u8*)"返回", 1);
Set_test();
}
關於界面
void About(void)
{
POINT_COLOR = WHITE;
//繪製關於窗口
LCD_DrawRectangle(20, 110, 220, 220);
LCD_DrawRectangle(21, 111, 219, 219);
LCD_Fill(22, 112, 218, 218, BLACK);
BACK_COLOR = BLACK;
//文本
Show_Str(104, 115, 32, 16, (u8*)"關於", 16, 0);
Show_Str(40, 135, 136, 16, (u8*)"平臺:STM32F103精英版", 16, 0);
Show_Str(40, 155, 136, 16, (u8*)"作者:YWL", 16, 0);
Show_Str(40, 175, 136, 16, (u8*)"時間:2019-11-30", 16, 0);
//繪製按鈕
DrawButton(90, 195, 60, 20, (u8*)"返回", 1);
About_test();
}
重要屏幕測試相關函數
屏幕測試模塊原理基本差別不大,主要是位置的差別。博主在這裏主要介紹單人對戰時的屏幕測試函數
單人對戰下棋函數
void One_test(void)//雙人對戰屏幕掃描
{
u16 x, y;
static int xChess = 0;
static int yChess = 0;
// static int xPre = 0;
// static int yPre = 0;
static uint8_t State = 0;
//判斷觸摸屏是否被按下
if (tp_dev.sta&TP_PRES_DOWN)
{
while(1)
{
tp_dev.scan(0);
x = tp_dev.x[0];
y = tp_dev.y[0];
if(tp_dev.sta&TP_PRES_DOWN) //觸摸屏被按下
{
if(tp_dev.x[0]<CHESSBOARD_END_X&&tp_dev.y[0]<CHESSBOARD_END_Y)
{
if(tp_dev.x[0]>CHESSBOARD_START_X &&tp_dev.y[0]>CHESSBOARD_START_Y)
{
if(((x - CHESSBOARD_START_X )%CHESSGRID_SIZE)<=7)
xChess = (x - CHESSBOARD_START_X ) / CHESSGRID_SIZE;
else
xChess = ((x - CHESSBOARD_START_X ) / CHESSGRID_SIZE)+1;
if((yChess = (y - CHESSBOARD_START_Y ) % CHESSGRID_SIZE)<7)
yChess = (y - CHESSBOARD_START_Y ) / CHESSGRID_SIZE;
else
yChess = (y - CHESSBOARD_START_Y ) / CHESSGRID_SIZE+1;
// if (g_ChessNum%2)delay_ms(300);
if((g_ChessNum%2)==0)
DrawChess(xChess, yChess, g_ChessNum, 1);
State = GameOver(xChess, yChess);
if(State==1||State==2)
{
POINT_COLOR = WHITE;
LCD_DrawRectangle(18, 138, 222, 178);
LCD_DrawRectangle(19, 139, 221, 177);
LCD_Fill(20, 140, 220, 176, BLACK);
BACK_COLOR = BLACK;
switch(State)
{
case 1:
Show_Str(20 + (200 - 16*2)/2, 150, 200, 16, (u8*)"和棋", 16, 0);
break;
case 2:
if (g_Mode == 1)
{
if (g_ChessNum % 2)
{
POINT_COLOR=RED;
Show_Str((240 - 16*4)/2, 150, 200, 16, (u8*)"你贏啦!!", 16, 0);
}
else
{
Show_Str((240 - 16*3)/2, 150, 200, 16, (u8*)"很遺憾 你輸了", 16, 0);
}
}
delay_ms(1000);//延時提示1秒
Recover();
Over_test();
break;
}
}
}
}
else
{
if (x > 5 && x < 85 && y > 285 && y < 310)//重新開始
ResStart();
if (x > 87 && x < 135 && y > 285 && y < 310)//悔棋
{
if(g_Mode==1)
RetractChess();
}
if(x > 137 && x < 185 && y > 285 && y < 310)
{
//計算AI下子座標
GetAIPoint(&xChess, &yChess);
delay_ms(10);
CHESS_Blink(xChess,yChess);
}
if (x > 187 && x < 236 && y > 285 && y < 310)//返回
{
ResStart();
GameInit();
}
}
}
else
{
if(!State)
{
if ((g_ChessNum>0)&&(g_ChessNum%2==1))
{
//計算AI下子座標
GetAIPoint(&xChess, &yChess);
if((g_ChessNum%2==1))
DrawChess(xChess, yChess, g_ChessNum, 1);
State = GameOver(xChess, yChess);
if(State==1||State==2)
{
POINT_COLOR = WHITE;
LCD_DrawRectangle(18, 138, 222, 178);
LCD_DrawRectangle(19, 139, 221, 177);
LCD_Fill(20, 140, 220, 176, BLACK);
BACK_COLOR = BLACK;
switch(State)
{
case 1:
Show_Str(20 + (200 - 16*2)/2, 150, 200, 16, (u8*)"和棋", 16, 0);
break;
case 2:
if (g_Mode == 1)
{
if (g_ChessNum % 2)
{
POINT_COLOR=RED;
Show_Str((240 - 16*4)/2, 150, 200, 16, (u8*)"你贏啦!!", 16, 0);
}
else
{
Show_Str((240 - 16*3)/2, 150, 200, 16, (u8*)"很遺憾 你輸了", 16, 0);
}
}
delay_ms(1000);//延時提示1秒
Recover();
Over_test();
break;
}
}
}
}
else
{
t++;
if(t==10)
{
t=0;
}
}
}
}
}else delay_ms(10); //沒有按鍵按下的時候
}
這個函數大致分爲兩部分,第一部分在棋盤範圍內,主要負責下棋還有判斷是否連接成5顆棋子。第二部分主要是在下方按鈕範圍內檢測並調用相關函數。同時在屏幕沒有按下時來改變t的值來產生僞隨機數。從而爲AI下棋提供僞隨機數。同時在判斷如果有一方贏了的情況下提示勝利一秒,然後回到勝利後的最終界面,可以選擇悔棋重新思考下棋位置,也可以選擇重新開始返回主界面等。在有5顆棋子連接成的情況下點擊棋盤區域不能有反應。
有一方勝利後調用函數
恢復至最終勝利界面的函數
void Recover(void)
{
u8 m,n;
DrawChessBoard();//恢復棋盤結束前的模樣
for (m= 0; m < 15; m++)
{
for (n = 0; n < 15; n++)
{
if (g_Chess[m][n] > 0)
{
int num = g_Chess[m][n];
DrawChess(m, n, num-1, 0);
}
}
}
}
在判斷有一方勝利以後調用該函數,可以恢復至最終勝利界面後的函數。
void Over_test(void)
{
int a,b;
while(1)
{
tp_dev.scan(0);
a = tp_dev.x[0];
b = tp_dev.y[0];
if(tp_dev.sta&TP_PRES_DOWN) //觸摸屏被按下
{
if (a > 5 && a < 85 && b > 285 && b < 310)//重新開始
{
ResStart();
}
if (a > 87 && a < 135 && b > 285 && b < 310)//悔棋
{
RetractChess();
Recover();
Two_test();
}
if (a > 187 && a < 236 && b > 285 && b < 310)//返回
{
ResStart();
GameInit();
}
}
}
}
在恢復至最終勝利界面後調用該函數,主要實現的功能是按棋盤範圍內沒有反應,按下方按鈕纔有反應。主要避免在測試界面判斷勝利以後恢復至最終勝利界面依舊可以下棋。
總結
通過用STM32F103實現五子棋讓我學到了很多知識,對如何完成一個工程有了更好的認識和理解,最終實現了預期的所有功能讓自己很有成就感。但是這個項目也有不足之處,不能選擇誰先走誰後走,後期可以再完善一下。
實際效果視頻鏈接
同時紀念一下第一次手寫代碼,加油!!!衝鴨!