zoukankan      html  css  js  c++  java
  • Linux 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.常用函数

     1 初始化相关:
     2 initscr();            //curses程序初始化必须
     3 cbreak();            //关闭字符流的缓冲
     4 nonl();              //输入资料时, 按下 RETURN 键是否被对应为 NEWLINE 字元 ( 如 
     ).
     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

    #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 初始化变量和屏幕的函数

    #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

    #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

    运行:

    由于curses是面向终端的接口(虚拟终端也包含在内),与图形库无关,所以程序在远程ssh工具上也可以完美运行,就像vim一样。

    可以考虑继续添加的特性:多人游戏    路障  吃字母 网络化 存档文件   用户记录    暂停(ctrl+z)

  • 相关阅读:
    html书签展示(带搜索)
    PHP 无限级分类(递归)
    文件服务器的搭建
    php swoole 和 websocket的初次碰撞
    Linux 服务管理的两种方式service和systemctl
    Jquery 代码参考
    分享几个网址二维码生成api
    WordPress 缩率图学习笔记
    Linux 究级基础入门命令整理
    ltrim的思考
  • 原文地址:https://www.cnblogs.com/eledim/p/4857557.html
Copyright © 2011-2022 走看看