How to write “Snake” in four variables?

We write the classic “Snake”, as on KDPV, in four variables. According to the author, “You can write with two, but why complicate your life?” To the start C++ development course we invite under cat.

Long time since I not wrote “Snake” and today I decided to try to make it with unusual restrictions:

  • Save the game map to uint32_twhere the body of the reptile will be formed by 1s. On the map 4x8, that is, 32 positions. Enough for fun!

  • Let’s save uint64_t is an array of directions for the snake to move while its growing form remains intact.

  • Sew into uint32_t a few more values ​​for 5 bits – to save the position head (“heads”) tail (“tail”) apple (“apples”) and length (current “length”). Any data entered from the keyboard is stored there. Two bits is enough.

  • Save the 8-bit (uint8_t) variable to loop.

There is no standard way to interact with the keyboard in C, so you have to use curses. Install it to compile the program. If you have the correct type of operating system, curses it probably already has it. If not, you can install using the package manager.

Unfortunately, in the curses additional memory is being used. But let’s be honest: hacking with mysterious escape characters and low-level system functions is not that much fun, and certainly not something I wanted to try myself. Yes, a scam, just like this article!

Before you continue reading (if you’re still reading), please note: the code is not to be taken seriously; it’s an exercise in minimalism, if you will. Macros for bitwise operations, global variables, the same counter, etc. are unsightly due to restrictions, so the code is not the most elegant and readable.

The code

Everything is on GitHub:

git clone

Compile and run the program:

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

Memory allocation scheme

Let’s start by defining four integers that will contain all the game data:

uint32_t map = ...;
uint32_t vars = ...;
uint64_t shape = ...;
int8_t i = ...;


map is what will be displayed on the screen. From 32 bit in map a grid will form 4x8rendered with curses:

To access memory and set bits to 0 or 1, macros are needed:

#define s_is_set(b) ((map&(1<<(b)))!=0) // проверяет, установлен ли бит b в map в 1
#define s_tog(b) (map^=(1<<(b))) // переключает в map бит b (сейчас не используется)
#define s_set_0(b) (map&=~(1<<b)) // устанавливает бит b в map в 0
#define s_set_1(b) (map|=(1<<b)) // устанавливает бит b в map в 1


vars – this is 32-bit integer that will store the following data:

  • hpos (bits from 0 on 4) is the position of the snake’s head as an offset from the least significant bit map;

  • tpos (bits from 5 on 9) – the position of the snake’s tail is similar;

  • apos (bits from 15 on 19) – the position of the apple is similar;

  • len (bits from 10 on 14) is the length of the snake;

  • chdir (bits from 20 on 21) is the last key pressed (2 a bit is enough, because only arrows are registered, and there are four of them);

  • the remaining bits are not used. There could be a counter here uint8_tbut for the sake of simplicity, I chose a separate variable.

To access hpos, tpos etc. we have defined the following macros (each one will be a getter/setter for the respective segments):

#define s_mask(start,len) (s_ls_bits(len)<<(start)) // создаёт битовую маску len, начиная с позиции start
#define s_prep(y,start,len) (((y)&s_ls_bits(len))<<(start)) // подготавливает маску

// Получает количество битов 'len', начиная с позиции 'start' в 'y'.
#define s_get(y,start,len) (((y)>>(start))&s_ls_bits(len)) 
// Устанавливает количество битов 'len', начиная с позиции 'start' в 'y', в значение '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) // получает последние 5 бит 'vars', что соответствует s_hpos
#define s_tpos s_get(vars,5,5) // устанавливает последние 5 бит 'vars', что соответствует 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)

For more information about how macros work, see the article Working with bits and bitfields (“Working with Bits and Bitfields”).


AT shape the directions (total 32) of each snake cell are stored. Two bits are enough for the direction:

The possible directions are displayed using macros:

#define SU 0 //ВВЕРХ                       
#define SD 1 //ВНИЗ                 
#define SL 2 //ВЛЕВО                
#define SR 3 //ВПРАВО

When the snake moves inside the grid mapwe cycle through the directions using macros:

#define s_hdir ((shape>>(s_len*2)&3)) // извлекает направление головы (на основе s_slen).
#define s_tdir (shape&3) // извлекает последние 2 бита, которые соответствуют хвосту
#define s_hdir_set(d) s_set(shape,d,s_len*2,2) // задаёт направление движения головы
#define s_tdir_set(d) s_set(shape,d,0,2) // задаёт направление хвоста
// Макросы изменения формы при каждом движении змеи
#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);

If the snake does not eat the apple, we call the macro s_shape_rotremoving the last direction and adding a new head with s_chdir.

Here shape looks like a queue:

If the snake eats the apple, call s_shape_addincreasing the length and adding a new tail s_tdir.

Game loop

The game loop looks like this:

// макросы, чтобы код стал читабельным
// (или нечитабельным, зависит от вас)
#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; // инициализирует контекст curses
    rnd_apple(); // создаёт случайную позицию яблока
    while(1) {
        show_map(); // отображает карту на экране
        timeout(80); // getch() таймаут после ожидания ввода пользователя
        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); // Выход из игры
        move_snake(); // Змея движется внутри решётки
        s_shape_rot(s_chdir); // Очертания змеи обновляются
        napms(200); // частота кадров :))
    s_exit(0); // выход из игры

Each time the key is pressed, it expands s_key_press and check if the move is possible. Then using s_chdir_set updated s_chdir.

Why in s_key_press two inputs? To rule out the opposite direction. For example, if the snake is now crawling to the right (SR), then SL impossible, and therefore switch works break.

Snake move function

Most of the logic is implemented in move_snake():

#define s_next_l s_mask5(s_hpos+1) // увеличение смещения для перехода вправо
#define s_next_r s_mask5(s_hpos-1) // уменьшение смещения для перехода влево
#define s_next_u s_mask5(s_hpos+8) // изменяет ряд вверх, добавляя к смещению 8 позиций
#define s_next_d s_mask5(s_hpos-8) // изменяет ряд вниз, удаляя из смещения 8 позиций

// Проверяет возможность движения влево. 
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); }
// Проверяет возможность движения вправо.
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); }
// Проверяет возможность движения вверх.
static void check_u() { if ((s_next_u < s_hpos) || s_is_set(s_next_u)) s_exit(-1); }
// Проверяет возможность движения вниз.
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); }
    // Устанавливает бит на основе текущих s_hdir и s_hpos
    // Если яблоко съедено
    if (s_apos==s_hpos) {
        // Генерирует ещё яблоко, чтобы змея не умерла с голоду
        // Прикрепляет яблоко к хвосту
        // Перестаёт очищать хвостовой бит
    // Очищает хвостовой бит
    // Обновляет t_pos, чтобы при движении змеи можно было очистить следующий бит хвоста
    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); }

To check if the snake can move on the grid, we have implemented the functions check_*():

  • check_l() – check if the X coordinate of the snake (modulo %8 from s_hpos) than in the previous position;

  • check_r() – check if the X coordinate of the snake (modulo %8 from s_hpos) than in the previous position;

  • Principle of operation check_u() and check_d similar: they keep track of whether overflow occurs when increasing s_hpos. If so, then we are out of the grid. Overflows are used as a functional.

Function for displaying a snake

Let’s implement the last function:

static void show_map() {
    while(i-->0) { // !! Триггерное предупреждение для чувствительных людей, идём к '-->0'
        // Если бит — это яблоко, оно отображается как "@".
        if (i==s_apos) { addch('@'); addch(' '); }
        // Рисуем змеиный бит ('#'), или пустой ('.').
        else { addch(s_is_set(i) ? '#':'.'); addch(' '); }
        // Строим сетку, вставляя новую строку
        if (!s_mod_p2(i,8)) { addch('\n'); }

After expanding the macro

After deploying all the macros, we get the final code:

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));
static void show_map() {
    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))); }
    if ((((vars)>>(15))&((1<<(5))-1))==(((vars)>>(0))&((1<<(5))-1))) {
        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);;
    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);;
    while(1) {
        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);
        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);;
    do { endwin(); exit(0); } while(0);;

Not that beautiful, but mesmerizing. When I scroll through the code, these bitwise movements make me sick.


It was an interesting exercise. The entire code (~100 lines and four integers) is located here.

If the snake in the terminal is crawling too fast, increase s_napms.

And we will help you upgrade your skills or master a profession that is relevant at any time from the very beginning:

Similar Posts

Leave a Reply