Linux Curses編程實現貪吃蛇,curses貪吃蛇
curses庫 簡單而言,提供UNIX中多種終端 操作光標和顯示字符 的接口。我們常見的vi就是使用curses實現的。現在一般都用ncurses庫。
Linux下curses函數庫 Linux curses庫使用 這兩篇文章很詳細地介紹了curses,在此就不詳細介紹了。
1.ubuntu安裝curses函數庫
$sudo apt-get install ncurses-dev
用curses庫,編譯程序:
$gcc program.c -o program -lcurses
2.工作原理
curses工作在屏幕,窗口和子窗口之上。屏幕是設備全部可用顯示面積(對終端是該窗口內所有可用字符位置),窗口與具體例程有關。如基本的stdscr窗口等。
curses使用兩個數據結構映射終端屏幕,stdscr和curscr。stdscr是“標准屏幕”(邏輯屏幕),在curses函數庫產生輸出時就刷新,是默認輸出窗口(用戶不會看到該內容)。curscr是“當前屏幕”(物理屏幕),在調用refresh函數是,函數庫會將curscr刷新為stdscr的樣子。
3.常用函數
![](https://www.aspphp.online/bianchen/UploadFiles_4619/201701/2017012110241242.gif)
![]()
1 初始化相關:
2 initscr(); //curses程序初始化必須
3 cbreak(); //關閉字符流的緩沖
4 nonl(); //輸入資料時, 按下 RETURN 鍵是否被對應為 NEWLINE 字元 ( 如 \n ).
5 noecho(); //關閉字符回顯
6 keypad(stdscr,TRUE); //可以使用鍵盤上的一些特殊字元, 如上下左右
7 refresh(); //刷新物理屏幕
8 int endwin(void); //關閉所有窗口
9
10 移動光標、輸出相關:
11 int move(int new_y, int new_x); //移動stdcsr的光標位置
12 addch(ch); //顯示某個字元.
13 mvaddch(y,x,ch); //在(x,y) 上顯示某個字元.
14 addstr(str); //顯示一串字串.
15 mvaddstr(y,x,str); //在(x,y) 上顯示一串字串.
16 printw(format,str); //類似 printf() , 以一定的格式輸出至螢幕.
17 mvprintw(y,x,format,str); //在(x,y) 位置上做 printw 的工作
18
19 前綴w用於窗口(添加一個WINDOWS指針參數),mv用於光標移動(在該位置執行操作addch或printw)(添加兩個坐標值參數),mvw用於在窗口(如stdscr)中移動光標。組成如下函數:
20 addch, waddch, mvaddch, mvwaddch
21 printw, wprintw, mvprintw, mvwprintw
22
23 屏幕讀取:沒用到就不寫了
24 鍵盤輸入:
25 //與標准io庫的getchar, gets, scanf類似
26 int getch();
27 int getstr(char *string);
28 int getnstr(char *string, int number); //建議使用
29 scanw(format,&arg1,&arg2...): //如同 scanf, 從鍵盤讀取一串字元.
30 清除屏幕;
31 int erase(void); //在屏幕的每個位置寫上空白字符
32 int clear(void); //使用一個終端命令來清除整個屏幕
用到的curses函數
貪吃蛇,要點在於1.蛇的實現;2.移動和按鍵偵聽的同步進行;3.碰撞的檢測。
1.蛇的實現,聯系到curses的特點,想到兩種辦法,一種是隊列存儲每個節點,還有一種是用頭-尾-拐彎節點坐標實現。
想到蛇的長度不可能太長,所以用隊列(循環數組)實現,添加頭結點,刪除尾節點。為了方便就沒有實現循環數組,采用了一個大數組。
2.兩種動作同時進行,兩種方案:1可以用異步編程,涉及到信號,阻塞,可重入問題。2用多線程,或多進程,涉及到變量的共享,互斥鎖等。
3.碰撞檢測。為了效率可以用一個線程把它抽取出來,如果代碼時間復雜度不高,可以直接寫。
用信號實現按鍵和同時移動。又有兩種方案,一種把移動放入定時信號處理函數,主程序用輸入阻塞(本文采用)。一種是把移動放入主程序循環,把輸入放入信號處理函數。
curses的物理屏幕刷新是根據邏輯屏幕的字符,邏輯屏幕那一塊沒有變動,實際屏幕也不變,所以不會像gdi編程畫面 閃動,耗費資源也少。
因此可以想到每次刷新只讓蛇動一個格子,蛇頭,和蛇尾 局部刷新就行了。
FC游戲的卷軸移動,也是類似的道理,內存小,長時間不動的背景就讓他不經過程序,也同時省了cpu。
卡馬克就是根據這種方法設計出當時低速PC上第一個能卷軸移動的游戲引擎。
tanchishe.c
![](https://www.aspphp.online/bianchen/UploadFiles_4619/201701/2017012110241242.gif)
![]()
#include "tanchishe.h"
//2015 10 06
//#define DEBUG__
void gogo(int );
void move_here_show(int );
int collision_detec(void);
void gameover(int );
sigjmp_buf jmpbuffer;
int main()
{
//函數間跳躍函數,保存進程信息,設置跳回點。(信號版,跳回後自動恢復信號屏蔽mask)
//程序首次運行略過if內代碼,游戲結束調用siglongjmp(jmpbuffer, int)跳回
if(sigsetjmp(jmpbuffer,1) != 0){
}
init_tcs();//變量的初始化,和屏幕的初始化顯示,和蛇與食物的顯示
#ifndef DEBUG__
signal(SIGALRM,gogo); //設置定時信號的處理函數,用於蛇的移動,調試時,手動調用
set_ticker(ticker); //每ticher毫秒 產生信號
#endif
while(1){
int i =getch();
switch (i) {
case _KEY_UP :
if(real_g_forward!=_KEY_DOWN &&real_g_forward!=_KEY_UP) //防止回跑
{g_key_forward=_KEY_UP;}break;
case _KEY_DOWN :
if(real_g_forward!=_KEY_UP&&real_g_forward!=_KEY_DOWN )
{g_key_forward=_KEY_DOWN ;}break;
case _KEY_LEFT :
if(real_g_forward!=_KEY_RIGHT&&real_g_forward!=_KEY_LEFT)
{g_key_forward=_KEY_LEFT;}break;//delay_real=&delay;
case _KEY_RIGHT:
if(real_g_forward!=_KEY_RIGHT&&real_g_forward!=_KEY_LEFT)
{g_key_forward=_KEY_RIGHT;}break;
#ifdef DEBUG__
case 'g':gogo(0);//raise(SIGALRM);
#endif
}
}
endwin();
return 0;
}
void gogo(int signum)//zouba
{
//保存可能隨時會變的全局變量g_key_forward(每次按下的希望前進的方向)
int temp_g_forward=g_key_forward;
move_here_show(temp_g_forward);//按照指定方向移動
collision_detec();//碰撞檢測
}
void move_here_show(int forward)
{
switch (forward) {
case _KEY_UP: --row; break; //蛇頭結點按照指定方向的移動
case _KEY_DOWN : ++row; break;
case _KEY_LEFT: --col; break;
case _KEY_RIGHT: ++col; break;
default:return;
}
//此時更新蛇真正的前進方向
//因為此時運動才完畢,且real_g_forward隨時都在鍵盤監聽中被使用
real_g_forward=forward;
//mvaddchstr(snake_s1.snake_body[last_s_node].row,
// snake_s1.snake_body[last_s_node].col,LSBLANK);
//屏幕上刪除(局部擦除)蛇最後一個節點
mvaddstr(snake_s1.snake_body[last_s_node].row,
snake_s1.snake_body[last_s_node].col,BLANK);
//在隊列中新增蛇移動後的頭節點 坐標(蛇頭始終增加到 數組的最小未用單元 (隊列尾))
snake_s1.snake_body[++first_s_node].row= row;
snake_s1.snake_body[first_s_node].col = col;
//mvaddchstr(row,col,NOB);
mvaddstr(row,col,NODE); //在屏幕上顯示出蛇頭新的坐標
#ifdef DEBUG__
//調試用的步數,可以走MAX_SIZE步,大約1800秒,之前肯定讓他死掉,這就是命
printw("%d",first_s_node+1-INIT_NODE_SIZE);
#endif
move(LINES-1,0);refresh();
last_s_node++; //在隊列中減去蛇移動後的尾節點 坐標
}
int collision_detec()
{
int i;
//檢測是否食物
if(col== food_col&& row ==food_row){
srand((unsigned)time(NULL));
food_col = 2+rand()%(COLS-5);
food_row = 1+rand()%(LINES-3);
test_dupe://防止新生成的食物在蛇身上……
for(i=last_s_node;i<first_s_node;++i){
if(food_col == snake_s1.snake_body[i].col)
if(food_row == snake_s1.snake_body[i].row ){
food_col = 2+rand()%(COLS-5);
food_row = 1+rand()%(LINES-3);
goto test_dupe;
}
}
//mvaddchstr(food_row,food_col,FOOD);
//這裡不改,centos也正常顯示,為了保險,也改成單字節版
mvaddstr(food_row,food_col,FOOD);//
last_s_node--;//muhaha
//檢測是否升級 ,右邊數組對應level(下標)升級所需要的食物
if(++eat_num >=times_to_upgrade_each_level[level]){
//升級後定時器減少 每級應減少的時間,更新等級
ticker -=delay_sub_each_level[level++];//注意此處是累減,
//更新定時器,信號函數觸發加快,移動速度加快
set_ticker(ticker);
}
score++;
max_score =max_score>score?max_score:score;
attrset(A_REVERSE); //反色下面的文字
mvprintw(0,COLS/2 -16,"Score:%d Max Score:%d Level:%d",score,max_score,level);
attrset(A_NORMAL);
//move(LINES-1,0);refresh();//沒有也可以,但是level那會閃
}
else{
//檢測是否撞牆
if(col == LEFT_WALL||col == RIGHT_WALL||row == TOP_WALL||row == BUTT_WALL)
gameover(1);
//檢測是否自攻
for(i=last_s_node;i<first_s_node-2;++i){
if(col == snake_s1.snake_body[i].col)
if(row == snake_s1.snake_body[i].row )
gameover(2);
}
}
return 0;
}
void gameover(int type)
{
set_ticker(0);
mvprintw(LINES/2-2,COLS/2 -15, " Game Over! ");
char * S= " Your Score is %d!";
if(score>max_score)
mvprintw(LINES/2+1,COLS/2 -15, " You Cut the Record!!!");
else S= " Your Score is %d! Too Young!!!";
if(score>75)S = " Your Score is %d! Nice!!!";
mvprintw(LINES/2,COLS/2 -15, S,score);
mvprintw(LINES/2+2,COLS/2 -15, "Press <%c> Play Again! <%c> to Quit!",AGAIN_KEY,QUIT_KEY);
refresh();
free(snake_s1.snake_body);
int geta;
while(1){
if((geta =getch()) == AGAIN_KEY){
erase();
refresh();
siglongjmp(jmpbuffer, type);//game over 帶著信息跳到main開始准備好的套子裡……
}
else if(geta == QUIT_KEY)
exit(0);
}
}
int set_ticker(int n_msecs) //把 以毫秒為單位的時間 轉換成對應的 定時器
{
struct itimerval new_timeset;
long n_sec,n_usecs;
n_sec = n_msecs/1000;
n_usecs=(n_msecs%1000)*1000L;
new_timeset.it_interval.tv_sec=n_sec;
new_timeset.it_interval.tv_usec=n_usecs;
new_timeset.it_value.tv_sec = n_sec;
new_timeset.it_value.tv_usec= n_usecs;
return setitimer(ITIMER_REAL,&new_timeset,NULL);
}
View Code
init_tcs.c 初始化變量和屏幕的函數
![](https://www.aspphp.online/bianchen/UploadFiles_4619/201701/2017012110241242.gif)
![]()
#include "tanchishe.h"
int row=0; //the head
int col=0;
int g_key_forward=_KEY_RIGHT;
int real_g_forward=_KEY_RIGHT;
int food_row =0;
int food_col =0;
int eat_num=0;
int ticker = 200;//ms
struct snake_s snake_s1;
int last_s_node=0;
int first_s_node;
int score =0;
int max_score=0;//do not init in retry!
int level=0;
int delay_sub_each_level[MAX_LEVEL]={0};
int times_to_upgrade_each_level[MAX_LEVEL]={0};
void init_tcs(void)
{
initscr();
cbreak(); //關閉緩沖
nonl();
noecho(); //關閉回顯
intrflush(stdscr,FALSE);
keypad(stdscr,TRUE);
curs_set(0); //光標不可見
row =LINES/2-1;
col =COLS/2-1;
eat_num=0;
ticker = 310;//ms
score =0;
g_key_forward=_KEY_RIGHT;
real_g_forward=_KEY_RIGHT;
last_s_node=0;
//first_s_node;
level=0;
//配置每升一級需要吃的食物,和每升一級蛇的快慢,也就是ticker的大小
int sum=delay_sub_each_level[0]=8;//EVERY_LEVEL_SUB_TIME;
int i;
times_to_upgrade_each_level[0]=TIMES_TO_UPGRADE_EACH_LEVEL;
for(i=1;i<MAX_LEVEL;++i){
times_to_upgrade_each_level[i]=times_to_upgrade_each_level[i-1]+(TIMES_TO_UPGRADE_EACH_LEVEL);
if(sum<ticker-50){
if(i<6)
delay_sub_each_level[i] =8;
else if(i<12)
delay_sub_each_level[i] =7;
else if(i <18)
delay_sub_each_level[i] =6;
else
delay_sub_each_level[i] =5;
}
else delay_sub_each_level[i]=delay_sub_each_level[i-1];
sum =sum+delay_sub_each_level[i];
}
//繪制邊框
attrset(A_REVERSE);
for(i=0;i<LINES;++i){
mvaddch(i,0,' ');
mvaddch(i,LEFT_WALL,' ');
mvaddch(i,RIGHT_WALL,' ');
mvaddch(i,COLS-1,' ');
}
for(i=0;i<COLS;++i){
mvaddch(0,i,' ');
mvaddch(LINES-1,i,' ');
}
mvprintw(0,COLS/2 -16,"Score:%d Max Score:%d Level:%d",score,max_score,level);
mvprintw(LINES-1,COLS/2 -18,"Eledim Walks the Earth,%c%c%c%c to Move",_KEY_UP, _KEY_DOWN, _KEY_LEFT, _KEY_RIGHT);
refresh();
attrset(A_NORMAL);
//創建蛇的節點容器
snake_s1.snake_body = malloc( SNAKE_MAX_SIZE *sizeof(struct post)) ;
if(snake_s1.snake_body == NULL)
mvprintw(0,0,"malloc error");
memset(snake_s1.snake_body,0,SNAKE_MAX_SIZE*sizeof(struct post));
srand((unsigned)time(NULL)); //no this ,rand every time return same num
#ifdef DEBUG__
food_row = LINES/2 -1;
food_col =COLS/2 +1;
#else
food_row = 1+rand()%(LINES-3);
food_col =2+rand()%(COLS-5);
#endif
//初始化蛇在容器中的坐標,和顯示
//snake_s1.head.row=row;
//snake_s1.head.col=col;
snake_s1.node_num =INIT_NODE_SIZE;
first_s_node=snake_s1.node_num-1;
for(i=0;i<snake_s1.node_num;++i){
snake_s1.snake_body[i].row=row;
snake_s1.snake_body[i].col=col-snake_s1.node_num+1+i;
//mvaddchstr(row,col-i,NOB);
mvaddstr(row,col-i,NODE);
}
//show food
//mvaddchstr(food_row,food_col,FOOD);
mvaddstr(food_row,food_col,FOOD);
move(LINES-1,0);refresh();
}
View Code
tanchishe.h
![](https://www.aspphp.online/bianchen/UploadFiles_4619/201701/2017012110241242.gif)
![]()
#ifndef TANCHISHE_H
#define TANCHISHE_H
#include <stdio.h>
#include <curses.h>
#include <time.h>
#include <sys/time.h> //timeval
#include <unistd.h> //usleep
#include <sys/select.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h> //memset
#include <setjmp.h>
#define LSBLANK L" "
#define BLANK " "
#define INIT_NODE_SIZE 5
#define SNAKE_MAX_SIZE 8192
#define LEFT_WALL 1
#define RIGHT_WALL (COLS-2)
#define TOP_WALL 0
#define BUTT_WALL (LINES-1)
#define NOB L"#"//
#define LSFOOD L"O"
#define NODE "#"//instead of L"#"
#define FOOD "O"
#define MAX_LEVEL 100
#define SUB_TIME_EACH_LEVEL 6
#define TIMES_TO_UPGRADE_EACH_LEVEL 6
#define _KEY_UP 'w'
#define _KEY_DOWN 's'
#define _KEY_LEFT 'a'
#define _KEY_RIGHT 'd'
#define AGAIN_KEY 'A'
#define QUIT_KEY 'Q'
struct post{
int row;
int col;
};
struct snake_s{
int node_num;
struct post head;
struct post* snake_body;
};
//for all
extern struct snake_s snake_s1;
extern int row;
extern int col;
//extern struct timeval delay;
extern int real_g_forward;
extern int g_key_forward;
extern int food_row;
extern int food_col ;
extern int eat_num;
extern int score ;
extern int ticker ;//ms
extern int last_s_node;
extern int first_s_node;
extern int max_score;//do not init in retry!
extern int level;
extern int delay_sub_each_level[MAX_LEVEL];
extern int times_to_upgrade_each_level[MAX_LEVEL];
//extern int ticker_for_col;
//extern int ticker_for_row;
int set_ticker(int n_msecs);
void init_tcs(void);
#endif
View Code
編譯:
cc tanchishe.c init_tcs.c -lcurses
運行:
![](https://www.aspphp.online/bianchen/UploadFiles_4619/201701/2017012110241228.gif)
由於curses是面向終端的接口(虛擬終端也包含在內),與圖形庫無關,所以程序在遠程ssh工具上也可以完美運行,就像vim一樣。
可以考慮繼續添加的特性:多人游戲 路障 吃字母 網絡化 存檔文件 用戶記錄 暫停(ctrl+z)