作者 | Andrei Cioban
譯者 | 彎月
出品 | CSDN(ID:CSDNnews)
記得上次編寫貪吃蛇遊戲還是很多年以前的事,如今我打算盡己所能,在一些很特別的方面做到極致:
將遊戲的地圖保存到一個uint32_t中,其中的1表示蛇的身體。因此整個地圖包括4x8個位置。
用另一個unit64_t作為方向數組,這樣可以實現蛇的移動,還可以保持不斷增長的身體的位置。
在另一個uint32_t中使用幾個5比特數據來保存head(蛇頭)、tail(蛇尾)、apple(蘋果)和length(當前長度)。還有兩個比特用來保存鍵盤輸入。
用一個8比特變量(uint8_t)作為循環變量。
因為標準C沒有提供鍵盤交互功能,因此必須依賴於curses,所以如果你想編譯該程序,請確保計算機上安裝了該庫。如果你使用的是正確的操作系統,很可能curses已經存在了。如若不然,你可以使用任何包管理器進行安裝。
不幸的是,curses本身需要消耗內存,但畢竟處理各種轉義字符和底層函數很麻煩,我不想自己實現。這種做法也許有點算作弊。
在閲讀本文之前,請記住文中的代碼僅供娛樂,只是一個練習。出於前面提到的限制,本文會編寫大量晦澀的宏來進行位操作,還會使用全局變量、重複使用同一個計數器,等等。這些都不是易讀代碼的最佳實踐。
代碼完整的代碼,請參見GitHub:
編譯和運行:
內存佈局
首先定義4個整數,用於保存所有遊戲數據:
uint32_t map = ...;
uint32_t vars = ...;
uint64_t shape = ...;
int8_t i = ...;
mapmap變量負責屏幕顯示。map變量有32比特,利用curses渲染成4x8的方格:
訪問每個比特並設置0或1,需要使用下面的宏:
#define s_is_set(b) ((map&(1/ checks if the b bit from the map is set to 1#define s_tog(b) (map^=(1/ toggles the b bit of the map (currently not used)#define s_set_0(b) (map&=~(1/ sets to 0 the b bit from the map#define s_set_1(b) (map|=(1/ sets to 1 the b bit from the mapvarsvars是一個32位整數,用於保存下面的數據:
hpos (比特0~4)表示蛇頭的位置,表示為從map的最低位開始的偏移量;
tpos(比特5~9)表示蛇尾的位置,表示為從map的最低位開始的偏移量;
len(比特10~14)表示蛇的長度;
apos(比特15~19)表示蘋果的位置,表示為從map的最低位開始的偏移量;
chdir(比特20~21)表示表示最後一次按下的鍵,2個比特足夠了,因為只需要四個方向鍵;
其餘的比特沒有使用。我們也可以把循環計數器的uint8_t放在這兒,但為了簡單起見,我還是使用了單獨的變量。
我們定義了以下的宏來訪問hpos、hpos等。這些宏就像是針對每個段的getter/setter一樣。
#define s_mask(start,len) (s_ls_bits(len)/ creates a bitmask of len starting from position start#define s_prep(y,start,len) (((y)&s_ls_bits(len))/ 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/
shapeshape用來保存蛇的每一節的方向。每個方向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(nd); } 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 possiblestatic void check_u() { if ((s_next_u < s_hpos) || s_is_set(s_next_u)) s_exit(-1); }// Check if a down movement is possiblestatic 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<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<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<>(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<>(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<>(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<>(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<>(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<>(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)<>(10))&((1<5))-1))*2)))|((((((vars)>>(20))&((1<2))-1)))&((1<2))-1))<>(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
—