资讯详情

用四个整数编写一个贪吃蛇游戏

作者 | Andrei Cioban

译者 | 弯月

出品 | CSDN(ID:CSDNnews)

b63844a16c01216d4011bc0891ee5423.gif

记得上次写贪吃蛇游戏还是很多年前的事了,现在我打算尽力在一些特殊的方面做到极致:

  • 将游戏地图保存到一个uint32_t其中一个表示蛇的身体。因此,整个地图包括4x8个位置。

  • 用另一个unit64_t作为方向数组,蛇可以移动,保持身体生长的位置。

  • 在另一个uint32_t保存几个5比特数据head(蛇头)、tail(蛇尾)、apple(苹果)和length(当前长度)。保存键盘输入还有两个比特。

  • 使用8比特变量(uint8_t)循环变量。

由于标准C不提供键盘交互功能,因此必须依赖于curses,因此,如果您想编译此程序,请确保该库安装在计算机上。如果您使用正确的操作系统,则很可能curses已经存在了。如果没有,可以用任何包管理器安装。

不幸的是,curses我需要消耗内存,但毕竟处理各种转义字符和底层函数很麻烦,我不想自己去做。这种做法可能有点作弊。

在阅读这篇文章之前,请记住,文章中的代码只是为了娱乐,练习。由于上述限制,本文将编写大量模糊的宏来进行位置操作,并使用全球变量,重复使用相同的计数器等。这些都不是易读代码的最佳实践。

请参见完整代码GitHub:

git clone git@github.com:nomemory/integers-snake.git

编译与操作:

gcc -Wall snake.c -lcurses && ./a.out

保存所有游戏数据,首先定义4个整数:

uint32_t map = ...;

uint32_t vars = ...;

uint64_t shape = ...;

int8_t i = ...;

map变量负责屏幕显示。map变量为32比特,使用curses渲染成4x8的方格:

访问每个比特并设置0或1,使用以下宏:

#define s_is_set(b) ((map&(1<<(b)))!=0) // checks if the b bit from the map is set to 1 #define s_tog(b) (map^=(1<<(b))) // toggles the b bit of the map (currently not used) #define s_set_0(b) (map&=~(1<<b)) // sets to 0 the b bit from the map #define s_set_1(b) (map|=(1<<b)) // sets to 1 the b bit from the map

vars用于保存以下数据的32位整数:

  • hpos (比特0~4)表示蛇头的位置,表示从map最低开始偏移量;

  • tpos(比特5~9)蛇尾的位置表示从map最低开始偏移量;

  • len(比特10~14)表示蛇的长度;

  • apos(比特15~19)表示苹果的位置,表示从map最低开始偏移量;

  • chdir(比特20~21)表示最后一次按键,两个比特就够了,因为只需要四个方向键;

  • 其余的比特没有使用。循环计数器也可以使用uint8_t但是为了简单起见,我还是用了单独的变量。

我们定义了以下宏访问hpos、hpos等等。这些宏就像针对每一段一样getter/setter一样。

#define s_mask(start,len) (s_ls_bits(len)<<(start)) // creates a bitmask of len starting from position start #define s_prep(y,start,len) (((y)&s_ls_bits(len))<<(start)) // prepares the mask   // Gets the the 'len' number of bits, starting from position 'start' of 'y' #define s_get(y,start,len) (((y)>>(start))&s_ls_bits(len))  // Sets the the 'len' number of bits, starting from position 'start' of 'y' to the value 'bf' #define s_set(x,bf,start,len) (x=((x)&~s_mask(start,len))|s_prep(bf,start,len))   #define s_hpos s_get(vars,0,5) // gets the last 5 bits of 'vars', which corresponds to s_hpos #define s_tpos s_get(vars,5,5) // sets the last 5 bits of 'vars', which corresonds to s_hpos #define s_len s_get(vars,10,5) #define s_apos s_get(vars,15,5) #define s_chdir s_get(vars,20,2) #define s_hpos_set(pos) s_set(vars,pos,0,5) #define s_tpos_set(pos) s_set(vars,pos,5,5) #define s_len_set(len) s_set(vars,len,10,5) #define s_apos_set(app) s_set(vars,app,15,5) #define s_chdir_set(cdir) s_set(vars,cdir,20,2) #define s_len_inc s_len_set(s_len 1)

更多关于宏背后的技巧,请参阅本文:https://www.coranac.com/documents/working-with-bits-and-bitfields/

shape用来保存蛇的方向。每个方向2比特就足够了,所以总共可以保存32个方向:

以下宏表示方向的意义:

#define SU 0 //UP                        #define SD 1 //DOWN                   #define SL 2 //LEFT                 #define SR 3 //RIGHT

每次蛇在map在移动方格时,我们需要使用以下宏循环方向:

#define s_hdir ((shape>>(s_len*2)&3)) // retrieves the head direction (based on s_slen) #define s_tdir (shape&3) // retrieves the last 2 bits which corresponds to the tail #define s_hdir_set(d) s_set(shape,d,s_len*2,2) // sets the head direction #define s_tdir_set(d) s_set(shape,d,0,2) // sets the tail direction  // Macros for changing the shape each time the snake moves #define s_shape_rot(nd) do { shape>>=2; s_hdir_set(nd); } while(0);  #define s_shape_add(nd) do { s_len_inc; shape<<=2; s_tdir_set(n); } while(0);

当蛇移动且没有吃掉苹果时,我们调用s_shape_rot宏,删除最后一个方向,然后添加一个新的蛇头(根据s_chdir)。

这么看来,蛇的行为有点像队列:

当蛇移动并吃掉一个苹果时,我们调用s_shape_add,仅增加长度,并添加一个新的蛇尾s_tdir。

主循环如下所示。

// Some macros to make the code more readable
// (or unreadable depending on you)
#define s_init do { srand(time(0)); initscr(); keypad(stdscr, TRUE); cbreak(); noecho(); } while(0);
#define s_exit(e) do { endwin(); exit(e); } while(0);
#define s_key_press(k1, k2) if (s_hdir==k2) break; s_chdir_set(k1); break;


int main(void) {
    s_init; // initialize the curses context
    rnd_apple(); // creates a random position for the apple
    while(1) {
        show_map(); // renders the map on screen
        timeout(80); // getch() timeouts after waiting for user input
        switch (getch()) {
            case KEY_UP : { s_key_press(SU, SD) }; 
            case KEY_DOWN : { s_key_press(SD, SU) };
            case KEY_LEFT : { s_key_press(SL, SR) };
            case KEY_RIGHT : { s_key_press(SR, SL) };
            case 'q' : exit(0); // Quits the game
        }
        move_snake(); // The snake moves inside the grid
        s_shape_rot(s_chdir); // The shape is getting updated
        napms(200); // frame rate :))
    }
    s_exit(0); // games exits
}

每当某个键按下时,就展开s_key_press,检查移动是否允许,然后更新s_chdir(使用s_chdir_set)。

s_key_press有两个输入参数的作用是去除相反方向。例如,如果蛇当前向右移动(SR),那么SL就是不可能的输入,从而中断switch语句。

move_snake()中实现了大部分逻辑:

#define s_next_l s_mask5(s_hpos+1) // incrementing the offset to go right
#define s_next_r s_mask5(s_hpos-1) // decrementing the offset to go left
#define s_next_u s_mask5(s_hpos+8) // change row up, by adding 8 positions to the offset
#define s_next_d s_mask5(s_hpos-8) // change row down, by removing 8 positions from the offset


// Check if a left movement is possible. 
static void check_l() { if ((s_mod_p2(s_next_l,8) < s_mod_p2(s_hpos,8)) || s_is_set(s_next_l)) s_exit(-1); }
// Check if a right movement is possible. 
static void check_r() { if ((s_mod_p2(s_next_r,8) > s_mod_p2(s_hpos,8)) || s_is_set(s_next_r)) s_exit(-1); }
// Check if a up movement is possible
static void check_u() { if ((s_next_u < s_hpos) || s_is_set(s_next_u)) s_exit(-1); }
// Check if a down movement is possible
static void check_d() { if ((s_next_d > s_hpos) || s_is_set(s_next_d)) s_exit(-1); }
static void move_snake() {
    if (s_hdir==SL) { check_l(); s_hpos_set(s_hpos+1); } 
    else if (s_hdir==SR) { check_r(); s_hpos_set(s_hpos-1); } 
    else if (s_hdir==SU) { check_u(); s_hpos_set(s_hpos+8); }
    else if (s_hdir==SD) { check_d(); s_hpos_set(s_hpos-8); }
    // Sets the bit based on the current s_hdir and s_hpos
    s_set_1(s_hpos);
    // If an apple is eaten
    if (s_apos==s_hpos) {
        // We generate another apple so we don't starve
        rnd_apple();
        // Append to the tail
        s_shape_add(s_tdir);
        // We stop clearning the tail bit
        return;
    }
    // Clear the tail bit
    s_set_0(s_tpos);
    // Update the t_pos so we can clear the next tail bit when the snake moves
    if (s_tdir==SL) { s_tpos_set(s_tpos+1); } 
    else if (s_tdir==SR) { s_tpos_set(s_tpos-1); } 
    else if (s_tdir==SU) { s_tpos_set(s_tpos+8); } 
    else if (s_tdir==SD) { s_tpos_set(s_tpos-8); }
}

为了验证蛇是否可以在方格中移动,我们实现了check_*()函数:

  • check_l()检查蛇的X坐标(s_hpos % 8)是否大于上一个位置的X坐标;

  • check_r()检查蛇的X坐标(s_hpos % 8)是否小于上一个位置的X坐标;

  • check_u()和check_d()的原理相同,检查增加s_hpos是否会导致溢出。如果溢出,表明移动超出了方格边界。这里溢出当做一个特性使用。

这是需要实现的最后一个函数:

static void show_map() {
    clear();
    i=32;
    while(i-->0) { // !! Trigger warning for sensitive people, incoming '-->0'
        // If the bit is an apple, we render the apple '@'
        if (i==s_apos) { addch('@'); addch(' '); }
        // We draw either the snake bit ('#') or the empty bit ('.')
        else { addch(s_is_set(i) ? '#':'.'); addch(' '); }
        // We construct the grid by inserting a new line
        if (!s_mod_p2(i,8)) { addch('\n'); }
    };
}

所有宏展开之后,代码如下所示:

uint32_t map = 0x700;
uint32_t vars = 0x20090a;
uint64_t shape = 0x2a;
int8_t i = 0;
static void rnd_apple() {
    i = (rand()&(32 -1));
    while(((map&(1<<(i)))!=0)) i = (rand()&(32 -1));
    (vars=((vars)&~(((1<<(5))-1)<<(15)))|(((i)&((1<<(5))-1))<<(15)));
}
static void show_map() {
    wclear(stdscr);
    i=32;
    while(i-->0) {
        if (i==(((vars)>>(15))&((1<<(5))-1))) { waddch(stdscr,'@'); waddch(stdscr,' '); }
        else { waddch(stdscr,((map&(1<<(i)))!=0) ? '#':'.'); waddch(stdscr,' '); }
        if (!(i&(8 -1))) { waddch(stdscr,'\n'); }
    };
}
static void check_l() { if ((((((((vars)>>(0))&((1<<(5))-1))+1)&0x1f)&(8 -1)) < ((((vars)>>(0))&((1<<(5))-1))&(8 -1))) || ((map&(1<<((((((vars)>>(0))&((1<<(5))-1))+1)&0x1f))))!=0)) do { endwin(); exit(-1); } while(0);; }
static void check_r() { if ((((((((vars)>>(0))&((1<<(5))-1))-1)&0x1f)&(8 -1)) > ((((vars)>>(0))&((1<<(5))-1))&(8 -1))) || ((map&(1<<((((((vars)>>(0))&((1<<(5))-1))-1)&0x1f))))!=0)) do { endwin(); exit(-1); } while(0);; }
static void check_u() { if (((((((vars)>>(0))&((1<<(5))-1))+8)&0x1f) < (((vars)>>(0))&((1<<(5))-1))) || ((map&(1<<((((((vars)>>(0))&((1<<(5))-1))+8)&0x1f))))!=0)) do { endwin(); exit(-1); } while(0);; }
static void check_d() { if (((((((vars)>>(0))&((1<<(5))-1))-8)&0x1f) > (((vars)>>(0))&((1<<(5))-1))) || ((map&(1<<((((((vars)>>(0))&((1<<(5))-1))-8)&0x1f))))!=0)) do { endwin(); exit(-1); } while(0);; }
static void move_snake() {
    if (((shape>>((((vars)>>(10))&((1<<(5))-1))*2)&3))==2) { check_l(); (vars=((vars)&~(((1<<(5))-1)<<(0)))|((((((vars)>>(0))&((1<<(5))-1))+1)&((1<<(5))-1))<<(0))); }
    else if (((shape>>((((vars)>>(10))&((1<<(5))-1))*2)&3))==3) { check_r(); (vars=((vars)&~(((1<<(5))-1)<<(0)))|((((((vars)>>(0))&((1<<(5))-1))-1)&((1<<(5))-1))<<(0))); }
    else if (((shape>>((((vars)>>(10))&((1<<(5))-1))*2)&3))==0) { check_u(); (vars=((vars)&~(((1<<(5))-1)<<(0)))|((((((vars)>>(0))&((1<<(5))-1))+8)&((1<<(5))-1))<<(0))); }
    else if (((shape>>((((vars)>>(10))&((1<<(5))-1))*2)&3))==1) { check_d(); (vars=((vars)&~(((1<<(5))-1)<<(0)))|((((((vars)>>(0))&((1<<(5))-1))-8)&((1<<(5))-1))<<(0))); }
    (map|=(1<<(((vars)>>(0))&((1<<(5))-1))));
    if ((((vars)>>(15))&((1<<(5))-1))==(((vars)>>(0))&((1<<(5))-1))) {
        rnd_apple();
        do { (vars=((vars)&~(((1<<(5))-1)<<(10)))|((((((vars)>>(10))&((1<<(5))-1))+1)&((1<<(5))-1))<<(10))); shape<<=2; (shape=((shape)&~(((1<<(2))-1)<<(0)))|((((shape&3))&((1<<(2))-1))<<(0))); } while(0);;
        return;
    }
    (map&=~(1<<(((vars)>>(5))&((1<<(5))-1))));
    if ((shape&3)==2) { (vars=((vars)&~(((1<<(5))-1)<<(5)))|((((((vars)>>(5))&((1<<(5))-1))+1)&((1<<(5))-1))<<(5))); }
    else if ((shape&3)==3) { (vars=((vars)&~(((1<<(5))-1)<<(5)))|((((((vars)>>(5))&((1<<(5))-1))-1)&((1<<(5))-1))<<(5))); }
    else if ((shape&3)==0) { (vars=((vars)&~(((1<<(5))-1)<<(5)))|((((((vars)>>(5))&((1<<(5))-1))+8)&((1<<(5))-1))<<(5))); }
    else if ((shape&3)==1) { (vars=((vars)&~(((1<<(5))-1)<<(5)))|((((((vars)>>(5))&((1<<(5))-1))-8)&((1<<(5))-1))<<(5))); }
}




int main(void) {
    do { srand(time(0)); initscr(); keypad(stdscr, 1); cbreak(); noecho(); } while(0);;
    rnd_apple();
    while(1) {
        show_map();
        wtimeout(stdscr,80);
        switch (wgetch(stdscr)) {
            case 0403 : { if (((shape>>((((vars)>>(10))&((1<<(5))-1))*2)&3))==1) break; (vars=((vars)&~(((1<<(2))-1)<<(20)))|(((0)&((1<<(2))-1))<<(20))); break; };
            case 0402 : { if (((shape>>((((vars)>>(10))&((1<<(5))-1))*2)&3))==0) break; (vars=((vars)&~(((1<<(2))-1)<<(20)))|(((1)&((1<<(2))-1))<<(20))); break; };
            case 0404 : { if (((shape>>((((vars)>>(10))&((1<<(5))-1))*2)&3))==3) break; (vars=((vars)&~(((1<<(2))-1)<<(20)))|(((2)&((1<<(2))-1))<<(20))); break; };
            case 0405 : { if (((shape>>((((vars)>>(10))&((1<<(5))-1))*2)&3))==2) break; (vars=((vars)&~(((1<<(2))-1)<<(20)))|(((3)&((1<<(2))-1))<<(20))); break; };
            case 'q' : exit(0);
        }
        move_snake();
        do { shape>>=2; (shape=((shape)&~(((1<<(2))-1)<<((((vars)>>(10))&((1<<(5))-1))*2)))|((((((vars)>>(20))&((1<<(2))-1)))&((1<<(2))-1))<<((((vars)>>(10))&((1<<(5))-1))*2))); } while(0);;
        napms(200);
    }
    do { endwin(); exit(0); } while(0);;
}

上述代码非常难懂,上下滚动屏幕甚至会感到头晕。

这个练习很有趣。完整的代码在此(https://github.com/nomemory/integers-snake/blob/main/snake.c),大约100行,只用了四个整数。

如果在你的终端上蛇跑得太快,可以尝试增加s_napms。

*本文由CSDN翻译,未经授权,禁止转载。

原文链接:https://www.andreinc.net/2022/05/01/4-integers-are-enough-to-write-a-snake-game

标签: 400v25kvar电容电抗器

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

锐单商城 - 一站式电子元器件采购平台