俄罗斯方块
一、设置控制台
    1、更改屏幕的背景色,字体颜色
        printf("\33[%dm", i);
        30<= i <=37  设置字体颜色
        30黑,31红,32绿,33黄,34蓝,35紫,36深绿,37白
        40<= i <=47  设置背景颜色
        40黑,41红,42绿,43黄,44蓝,45紫,46深绿,47白


    2、任意指定屏幕的输出坐标
        printf("\33[x;yH");  将输出坐标定位在第x行,第y列
        printf("\33[%d;%dH", x,y);  将输出坐标定位在第x行,第y列
二、菜单
    1、边框
        游戏区宽48,高31;辅助区宽27,高31
        游戏区起始坐标(2,3);辅助区起始坐标(2,53)(18,53)
    2、开始
        用空格和[]描出一个fight单词
    3、结束
        用空格和[]描出一个game over单词

三、绘制方块
    1、一个小方格
        由于一个字符在屏幕上占的空间是一个长方形,所以用[]来表示一个方格
    2、俄罗斯方块
        总共有7个类型的方块,每个方块有4种摆放的方式(可以旋转)
    一个方块最多占用4个小方格
        用4x4的矩阵来表示一个俄罗斯方块,矩阵中为1的地方,绘制[]
        {0, 0, 0, 0}                     
        {0, 0, 1, 0}    ----->            []
        {0, 0, 1, 1}                     [][]  
        {0, 0, 1, 0}                     []
    3、用一个三位数组表示所有的俄罗斯方块
        int shape[i][j][k]
        i表示方块的类型,j表示方块摆放的方式,k将4x4矩阵变为一维数组
        将上面的二维数组变成一维的{0,0,0,0, 0,0,1,0, 0,0,1,0, 0,0,1,0}
    4、起始坐标
        绘制方块从4x4矩阵的左下角开始,制定其坐标为row、col
        俄罗斯方块中小方格的坐标rrow = row + j/4-3   ccol = ccol + (i%4)*2

四、 自动下落
    1、内核定时器
        setitimer(int which, const struct itimerval *val, struct itimerval *oldval)
        参数which:
            ITIMER_REAL 用系统实时的时间计算,时间减1,减到0发送SIGALRM信号
            ITIMER_VIRTUAL 当前进程用户态计数, 计数完成发送SIGVTALRM信号
            ITIMER_PROF 用户态和内核态同时计数,计数完成发送SIGPROF信号
        参数val:
        struct itimerval{
            struct timeval it_interval;  //定时时间
            struct timeval it_value;     //定时器开始执行的时间
        }
        struct timeval{
            long second;        //秒
            long usec;            //微妙
        }
        参数oldval:
            保存旧的状态

        struct itimerval val = {
                            {0,500000},   //0.5秒的定时时间
                            {1,0}          //1s后开启定时器
                            };
        setitimer{ITIMER_REAL, &val, NULL};
    2、信号捕捉
        sigaction(int signo, struct sigaction *act, struct sigaction *oldact);
        参数signo:
            要捕捉的信号
        参数act:
            为信号设置动作
        参数oldact:
            保存旧的状态,NULL就是不保存

        struct sigaction{
            void (*sa_handler)(int);    //信号执行的函数
            sigset_t mask;                //信号执行过程中要屏蔽的其他信号
        }

        struct sigaction act;
        act.sa_handler = self_down;
        sigaction(SIGALRM, &act, NULL);

五、 响应键盘
    1、设置终端,键盘可以输入,但是不能在屏幕上显示输入的字符,否则影响游戏画面。
        struct termios old, new;
    
        tcgetattr(0, &old);        //保存旧的状态
        tcgetattr(0, &new);     //获取旧的状态

        new.c_lflag = new.c_lflag & ~(ICANON|ECHO);  //设置屏幕不显示输入的字符
        new.c_cc[VTIME] = 0;
        new.c_cc[VMIN] = 1;

        tcsetattr(0, TCSANOW, &new);  //修改终端的属性,立即生效

    2、响应键盘输入
        判断输入字符
        while(1)
        {    
            c = getchar();
            switch(c)
            {
                case 'a':
                    left();    //左移,列-2,因为一个小方格占2列
                    break;
                case 'd':
                    right();    //右移,列+2
                    break;x
                case 'w':
                    change();  //逆时针旋转
                    break;
                case 's':
                    self_down(1); //自由下落 row+1
                    break;
                default:
                    break;
            }
        }
    
    3、设置标志位
        方块降落到最后,需要用以个二维数组来标志,数组中为1的地方证明有方格
        int flag[30][24];
        void set_flag()
        {
            int i;
            int row, col;

            for(i=0; i<16; i++)
            {
                if(shape[box.sh][box.num][i] == 1)
                {
                    row = box.row + i/4 -3 -2;
                    col = (box.col + (i%4)*2 + 1)/2-2;
                    flag[row][col] = 1;
                }
            }
        }

    4、边界判断
        1)、下落判断
            //判断能有下落,当对应方格的下一行位置有东西,那么就不能下落来了
            //比如现在方格在8行9列,那么如果9行9列有东西(判断标志位),那么
            //方格就不能在下落了
            int is_down()
            {
                int err = 1;
                int i;

                if(box.row >= 31)    //第一次超过31行就不能下落,那是最低的边界
                {
                    err = 0;
                    return err;
                }

                for(i = 0; i < 16; i++)    
                {
                    if(shape[box.sh][box.num][i] == 1)
                    {
                        if(flag[box.row + i/4 -3 -2 + 1][(box.col+i%4*2+1)/2-2] == 1)
                        {
                            err = 0;
                            return err;
                        }
                    }
                }

                return err;
            }

        2)、左移判断
            //首先要获取大方块中的最左边的列
            int get_left()
            {
                int col[4];
                int i, j, temp;

                for(i=0, j=0; i<16; i++)
                {
                    if(shape[box.sh][box.num][i] == 1)    //获取方块中小方格占的列
                    {    
                        col[j] = box.col + (i%4)*2;
                        j++;
                    }
                }

                for(i=1; i<4; i++)
                {
                    if(col[0] > col[i])        //求出列中最小的,也就是最左边的列
                    {    
                        temp = col[0];
                        col[0] = col[i];
                        col[i] = temp;
                    }
                }

                return col[0];
            }

            int is_left()                //判断能否左移
            {
                int err = 1;
                int i;

                int col = get_left();
                if(col == 3)            //最左边的列是第3列,移动到第3列就不能左移了
                    return 0;


                //如果左边有方格,也不能左移,通过标志位来判断
                for(i = 0; i < 16; i++)    
                {
                    if(shape[box.sh][box.num][i] == 1)
                    {
                        if(flag[box.row + i/4 -3 -2][(box.col+i%4*2+1)/2-2-1] == 1)
                        {
                            err = 0;
                            return err;
                        }
                    }
                }

                return err;
            }

        3)、右移判断
            //首先要获取大方块中的最右边的列
            int get_right()
            {
                int col[4];
                int i, j, temp;

                for(i=0, j=0; i<16; i++)
                {
                    if(shape[box.sh][box.num][i] == 1)    //获取方块中小方格占的列
                    {    
                        col[j] = box.col + (i%4)*2;
                        j++;
                    }
                }

                for(i=1; i<4; i++)
                {
                    if(col[0] < col[i])        //求出列中最大的,也就是最右边的列
                    {    
                        temp = col[0];
                        col[0] = col[i];
                        col[i] = temp;
                    }
                }

                return col[0];
            }

            int is_right()                //判断能否右移
            {
                int err = 1;
                int i;

                int col = get_right();
                if(col == 3)            //最右边的列是第49列,移动到第49列就不能右移了
                    return 0;


                //如果右边有方格,也不能右移,通过标志位来判断
                for(i = 0; i < 16; i++)    
                {
                    if(shape[box.sh][box.num][i] == 1)
                    {
                        if(flag[box.row + i/4 -3 -2][(box.col+i%4*2+1)/2-2+1] == 1)
                        {
                            err = 0;
                            return err;
                        }
                    }
                }

                return err;
            }
        4)、旋转判断
            旋转依赖4x4大方格,在4x4大方格内有东西到地方都不能旋转
            int is_change()
            {
                int err=1;
                int i;

                for(i=0; i<16; i++)
                {
                    if(flag[box.row+i/4-3-2+1][(box.col+1+i%4*2)/2-2] == 1)
                    {
                        err = 0;
                        return err;
                    }
                    if(box.col<3)
                    {
                        err = 0;
                        return err;
                    }
                }

                return err;
            }
            
六、消行
    1、判断是否需要消行,获取需要消的行号
        在flag数组中,如果有一行全部为1,那么就需要消行了
        int is_clean()
        {
            int err=0, fg=1;
            int num=0;

            int i,j;
            for(i=0; i<30; i++)
            {
                fg = 1;                    //假设这一行能消去
                for(j=0; j<24; j++)
                {
                    if(flag[i][j]==0)    //如果这一行中有一个0,那么就不能消去
                    {
                        fg = 0;    
                        break;
                    }
                }
                if(fg == 1)
                {
                    clean_row[num] = i;    //如果能消行,那么需要记录行号
                    num++;
                    err = 1;
                }
            }        

            return err;
        }
    2、记录以前的状态,设值新的状态
    
        void swap()
        {
            int i,j,k;                    //old_flag记录以前的状态
            for(i=0; i<30; i++)
                for(j=0; j<24; j++)
                    old_flag[i][j] = flag[i][j];


            for(k=0; k<4; k++)            //更新flag,消行后,上面的行要掉下来
                for(i=0; i<clean_row[k]; i++)
                    for(j=0; j<24; j++)
                    {
                        flag[i+1][j] = old_flag[i][j];
                    }
        }
    3、重新画图
        void redraw()
        {
            int i,j;

            //将以前的图形全部擦掉
            for(i=0; i<30; i++)        
            {
                for(j=0; j<24; j++)
                {
                    if(old_flag[i][j]==1)
                        single_box(i+2, (j+1)*2+1, 0);
                }
            }

            //绘制新的图形
            for(i=0; i<30; i++)
            {
                for(j=0; j<24; j++)
                {
                    if(flag[i][j]==1)
                        single_box(i+2, (j+1)*2+1, 1);
                }
            }
        }
七、右侧指示下一个
    1、一开始就产生两个图形,box、next_box
        使用box下落,next_box显示在右侧
        box.sh = 1;
        box.num = 1;    
        box.row = 1;
        box.col = 13;

        big_box(box, 1);

        next();
        display_next( 1);

        
        void next()
        {
            next_box.sh = rand_num(7);
            next_box.num = rand_num(4);
            next_box.row = 1;
            next_box.col = 13;
        }

        void display_next(int mode)
        {
            struct box_info nbox;
            memcpy(&nbox, &next_box, sizeof(struct box_info));
            nbox.row = 10;
            nbox.col = 60;

            big_box(nbox, mode);
        }
    2、当下落结束的时候,需要使用next_box下落,然后接着产生另一个next_box
    
        if(new_flag == 1)
        {
            new_flag = 0;

            display_next(0);    //将右侧的提示去掉
            memcpy(&box, &next_box, sizeof(struct box_info));//将next_box拷贝到box
            next();                    //产生新的next_box
            display_next( 1);        //在右侧提示next_box
            
        }
八、判断游戏是否结束
    
    //判断是否结束游戏
    int is_gameover()
    {
        int err = 0;
        int i,j;

            for(j=0; j<24; j++)
            {
                //flag第0行有1就是结束游戏
                if(flag[0][j] == 1)
                {
                    err = 1;
                    break;
                }
            }

        return err;
    }

    //产生下一个box的时候判断是否游戏结束
    void next()
    {
        if(is_gameover()==1)
        {
            flag_over = 1;
            return;
        }
        next_box.sh = rand_num(7);
        next_box.num = rand_num(4);
        next_box.row = 1;
        next_box.col = 13;
    }

九、修改菜单,完善游戏
    增加游戏的灵活性,进入游戏后,按1是开始,按2是选关,按3是退出。
    选关的时候+是增加难度,-是减少难度,1是开始,3是退出


  最后未完成的双人版,日后补上,有源代码

    压缩文件中有:els.c els.h main.c Makefile

    运行方式:1  make

                    2  ./main