Пару недель назад, на меня вышла HR "светлячков" и пригласила поговорить о позиции AI программера в их новую старую игру. Я был несколько удивлен, потому что знаком с парой ребят из студии и знаю, что найм у них сейчас остановлен. Но всегда интересно пообщаться с умными людьми, поэтому отказываться не стал. Первый созвон с HR вышел стандартный, где над чем работал, какие игры шипнул. Не очень понимаю зачем все это было спрашивать, если все это есть на линкедине подробно и с датами. Ну да ладно - видно такая их эйчарская доля по тридцать три раза переспрашивать. Или HR дальше второй страницы просто поленилась почитать.

Забукали время технического интервью. В полдень четверга на встречу приходит сотрудник студии и начинает просматривать резюмешку дальше второй страницы, где натыкается на скрин опенсорсного проекта StoneKingdoms, в который я некоторое время активно комитил. Проект, если что, получил благословение самого Simon Bradbury (владельца студии), так что проблем с правами на использование ресурсов из Stronghold нет. Посыпались вопросы, а что за проект? а как делаете? и что все на lua? а как же плюсы? Где-то на середине разговора к нам подключился другой разработчик "светлячков", с которым мое знакомство началось еще в 2010, когда он помогал восстанавливать исходники Caesar III и просто давал консультации как реализована игровая симуляция. Мы и сейчас иногда общаемся на форуме по ремейкам старых игр.


Есть у этого человека одна особенность - если затронуть проекты где он участвовал, то следующие полчаса все будут слушать байки и истории как этот проект делали. А рассказывает он интересно и с огоньком, бывало и парой часов все не заканчивалось. Мы как раз обсуждали реализацию теней на изометрической карте. Надо был видеть выражение лица интервьюера, когда присоединившийся поздоровался со мной по имени и спросил как продвигается работа над Фараоном. А потом мы слушали рассказ как тени были реализованы в первом Stronghold'e, технические вопросы как-то отошли в сторону. Чуть позже разговор и вовсе перешел на обсуждение особенностей устройства игр времен Древнего Рима и пятой династии Египта.

Про опенсорс Stronghold: я туда уже не комичу, но если у кого будет время обязательно посмотрите, там много чего восстановили из оригинальной игры (https://gitlab.com/stone-kingdoms/stone-kingdoms)

The stock pile is full. My lord!
The stock pile is full. My lord!
А это оригинал, как говорится - найдите пять отличий
А это оригинал, как говорится - найдите пять отличий

Час, который был отведен на интервью пролетел, как будто и не было его. Под завершение разговора интервьюер спросил в шутку соглашусь ли на тестовое, я также в шутку согласился. Понятно, что смысла в нем никакого не было, но меня всегда интересовали разные мозголомные задания, которые студии придумывают для потенциальных сотрудников. Итак, за второй час собеса на лайв кодинге надо было написать игру, рассказывая о принятых решениях по ходу создания игры. Но не просто написать - за каждую лишнюю строку после 150-ой отнимался балл, за каждую пустую строчку кода перед 150-ой - соответственно добавлялся балл. Как потом сказали мои знакомые, рекорд был 85 строк рабочего арканоида и читабельного кода. За каждую дополнительную игровую фичу вроде жизней или очков, еще плюс 10 баллов. За нечитаемый или непонятный код также снимают баллы, максимум могут снять 30.

За час надо было сделать играбельный арканоид.

Вот такое говорят люди пишут за 5 наносекунд
Вот такое говорят люди пишут за 5 наносекунд

Но не подумайте, что там совсем уж звери сидят - с нуля писать игру. Даже такую простую. Каркас приложения уже написан, надо накидать своей логики, и как я сказал выше, постараться уместить в 150 строк кода. Я не очень быстро пишу код, и зачастую мне нужно время чтобы подумать над решением, даже если оно очевидное или где-то уже было сделано в прошлых проектах. Поэтому для меня это было действительно непростой задачей, в плане успеть по времени.

#include <SDL.h>

const int SCREEN_WIDTH = 800;
const int SCREEN_HEIGHT = 600;

const int BRICK_WIDTH = 48;
const int BRICK_HEIGHT = 20;

struct Brick{
    SDL_Rect rect;
} ;

struct Ball {
    SDL_Rect rect;
} ;

struct Paddle {
    SDL_Rect rrect;
} ;

struct Game {
    Brick bricks[16][10];
    Ball ball;
    Paddle paddle;
};

int main(int argc, char* args[]) {
    Game game;

    SDL_Window* window = NULL;
    SDL_Surface* screenSurface = SDL_GetWindowSurface(window);

    SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO);
    window = SDL_CreateWindow( "Strongout", 50, 50, 
                              SCREEN_WIDTH, SCREEN_HEIGHT, SDL_WINDOW_SHOWN );
    bool quit = false;
    while (!quit) {
        SDL_Event event;
        while (SDL_PollEvent(&event)){
            if( event.type == SDL_QUIT){
                quit = true;
                break;
            }

            if( event.type == SDL_KEYDOWN ){
            }

            if( event.type == SDL_KEYUP ){
            } 
        }
        SDL_FillRect(screenSurface, NULL, 
                     SDL_MapRGB( screenSurface->format, 0, 0, 0));
        // Your code here
        SDL_UpdateWindowSurface(window);
    }

    SDL_DestroyWindow(window);
    SDL_Quit();
    return 0;
}

Первым делом была добавлена отрисовка кирпичей. Прошу не ругать сильно за код, в условиях ограниченного времени и необходимости отладить функционал думать об архитектуре очень сложно, поэтому писал просто и топорно, чтобы можно было быстро итерироваться и отлаживать.

void drawLiveBricks(SDL_Surface* screen, SDL_Window* window, Game &game) {
    for (int i = 0; i < 16; i++) {
        for (int j = 0; j < 10; j++){
            if (game.bricks[i][j].is_alive) {
                SDL_FillRect(screen, &game.bricks[i][j].rect, 
                             SDL_MapRGB( screen->format, 255, 0, 0));
            }
        }
    }
}

Потом настал черед планки и шарика

SDL_FillRect(screenSurface, &game.paddle.rect, SDL_MapRGB( screenSurface->format, 0, 255, 0));
SDL_FillRect(screenSurface, &game.ball.rect, SDL_MapRGB( screenSurface->format, 255, 255, 255));

Теперь заставим планку перемещатьcя при нажатии кнопок A/D. Velocity нужна, чтобы передать направление в планку.

struct Paddle {
    SDL_Point velocity;
    SDL_Rect area;

    Paddle() {
        area = {350, 550, 100, 20};
        velocity = {0, 0, 0, 0};
    }

    void move(SDL_Keycode key) {
        if (key == SDLK_a){
            velocity.x = -1;
            if (area.x <= 0) {
                velocity.x = 0;
                area.x = 0;
            }
        }
        if (key == SDLK_d){
            velocity.x = 1;
            if (area.x + area.w >= SCREEN_WIDTH) {
                velocity.x = 0;
                area.x = SCREEN_WIDTH - area.w;
            }
        }
    }

    void update() {
        area.x += velocity.x;
        if (area.x <= 0) {
            area.x = 0;
        }
        if (area.x + area.w >= SCREEN_WIDTH) {
            area.x = SCREEN_WIDTH - area.w;
        }
    }

};

И конечно же апдейт перемещения шарика, пусть в данном приближении наш шарик выглядит как квадрат. Да простят меня боги хорошего кода, я прокинул обсчет столкновений со стенками и планкой прямо в апдейт шарика.

    bool update(const Paddle &paddle) {
        if (SDL_GetTicks() % 2 == 1) { return true; }
        velocity.x *= (position.x <= 0 || position.x >= SW.x - position.w) ? -1 : 1;
        velocity.y *= (position.y <= 0) ? -1 : 1;
        if (position.y >= SW.y - position.h) {
            velocity = {0, 0};
            position = {395, 295, 10, 10};
            return false;
        }
        velocity.y *= (position.y == (paddle.y) && (position.x >= paddle.x) && (position.x <= paddle.x + paddle.w)) ? -1 : 1;
        position.x += velocity.x;
        position.y += velocity.y;
        (SDL_Point&)r = (SDL_Point&)position;
        return true;
    }

Осталось сделать разбивание блоков. Не придумалось ничего лучше, как просто смотреть пересечение всех блоков с шариком после апдейта мува. Даже если получится ситуация, что шарик будет внутри блока, то все равно получаем его разбитие.

        for (int i = 0; i < 160; i++) {
            if (SDL_HasIntersection(&game.ball.r, &game.bricks[i].r) && game.bricks[i].is_alive) {
                game.ball.position.y += 2;
                game.ball.velocity.y *= -1;
                game.bricks[i].is_alive = false;
            }
        }

На все это плюс отладочные запуски ушло примерно 30 минут, плюс приходилось комментировать свои действия, что тоже оказалось довольно сложно параллельно с написанием кода. В итоге получился вот такой код:

Strongout/146 lines
сonst int SCREEN_WIDTH = 800;
const int SCREEN_HEIGHT = 600;

const int BRICK_WIDTH = 48;
const int BRICK_HEIGHT = 20;

using p = SDL_Rect; using v = p;

struct Brick {
    p r;
    bool is_alive;
} ;

struct Paddle {
    v velocity = {0, 0, 0, 0};
    p area = {350, 550, 100, 20};

    void move(SDL_Keycode key) {
        if (key == SDLK_a){
            velocity.x = -1;
            if (area.x <= 0) {
                velocity.x = 0;
                area.x = 0;
            }
        }
        if (key == SDLK_d){
            velocity.x = 1;
            if (area.x + area.w >= SCREEN_WIDTH) {
                velocity.x = 0;
                area.x = SCREEN_WIDTH - area.w;
            }
        }
    }

    void update() {
        area.x += velocity.x;
        if (area.x <= 0) {
            area.x = 0;
        }
        if (area.x + area.w >= SCREEN_WIDTH) {
            area.x = SCREEN_WIDTH - area.w;
        }
    }
};

struct Ball {
    v velocity = {0, 0, 0, 0};
    p position = {395, 295, 10, 10};
    p r = position;

    void start(SDL_Keycode key) {
        if (key == SDLK_SPACE && velocity.x == 0 && velocity.y == 0)
            velocity = {1, 1};
    }

    bool update(const Paddle &paddle) {
        if (SDL_GetTicks() % 2 == 0) {
            if (position.x <= 0 || position.x >= SCREEN_WIDTH - position.w) {
                velocity.x *= -1;
            }
            if (position.y <= 0) {
                velocity.y *= -1;
            }

            if (position.y >= SCREEN_HEIGHT - position.h) {
                velocity = {0, 0};
                position = {395, 295, 10, 10};
                return false;
            }
            if (position.y == (paddle.area.y)) {
                if ((position.x >= paddle.area.x) && (position.x <= paddle.area.x + paddle.area.w)) {
                    velocity.y *= -1;
                }
            }

            position.x += velocity.x;
            position.y += velocity.y;
            (SDL_Point&)r = (SDL_Point&)position;
            return true;
        }
    }
};

struct Game {
    std::array<Brick, 16 * 10> bricks;
    Ball ball;
    Paddle paddle;

    Game() {
        for (auto &brick: bricks) {
            int i = std::distance(bricks.data(), &brick);
            brick.r = {1 + ((i % 16) * (BRICK_WIDTH + 2)), 1 + ((i / 16) * (BRICK_HEIGHT + 2)), BRICK_WIDTH, BRICK_HEIGHT};
            brick.is_alive = true;   
        }
    }
};

int main(int argc, char* args[]) {
    Game game;
    bool quit = false;
    SDL_Window* window = NULL;
    SDL_Surface* screenSurface = NULL;
    SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO);
    window = SDL_CreateWindow("Strongout", 50, 50, SCREEN_WIDTH, SCREEN_HEIGHT, SDL_WINDOW_SHOWN );
    while (!quit) {
        SDL_Event event;
        while (SDL_PollEvent(&event)){
            if( event.type == SDL_QUIT){
                quit = true;
                break;
            }
            if( event.type == SDL_KEYDOWN ){
                game.paddle.move(event.key.keysym.sym);
                game.ball.start(event.key.keysym.sym);
            }
            if( event.type == SDL_KEYUP ){
                game.paddle.velocity.x = 0;
            } 
        }
        if (game.lives > 0) {
            game.paddle.update();
            game.ball.update(game.paddle);
        }
        int numBricks = 160;
        for (int i = 0; i < numBricks; i++) {
            if (SDL_HasIntersection(&game.ball.r, &game.bricks[i].r) && game.bricks[i].is_alive) {
                game.ball.position.y += 2;
                game.ball.velocity.y *= -1;
                game.bricks[i].is_alive = false;
            }
        }
        screenSurface = SDL_GetWindowSurface(window);
        SDL_FillRect(screenSurface, NULL, SDL_MapRGB( screenSurface->format, 0, 0, 0));
        for (auto &brick: game.bricks) {
            if (brick.is_alive) {
                SDL_FillRect(screenSurface, &brick.r, SDL_MapRGB( screenSurface->format, 255, 0, 0));
            }
        }
        SDL_FillRect(screenSurface, &game.paddle.area, SDL_MapRGB( screenSurface->format, 0, 255, 0));
        SDL_FillRect(screenSurface, &game.ball.r, SDL_MapRGB( screenSurface->format, 255, 255, 255));
        SDL_UpdateWindowSurface(window);
    }
    SDL_DestroyWindow(window);
    SDL_Quit();
    return 0;
}

Вот что я смог накропать за полчаса
Вот что я смог накропать за полчаса

Но как видите, будет мало баллов по шкале арканоидов за написанный код. Кстати я заметил, что компании стали менее охотно давать задания на оффлайн выполнение, видимо это связано с невозможностью проверить будет ли оно выполнено сеткой или человек сделает его сам. Возвращаясь к коду арканоида, первое что пришло на ум - это поубирать все лишние строки и свернуть часть скобок и функций, чтобы получить побольше баллов.

Strongout/132 lines
сonst int SCREEN_WIDTH = 800;
const int SCREEN_HEIGHT = 600;
const int BRICK_WIDTH = 48;
const int BRICK_HEIGHT = 20;
using p = SDL_Rect; using v = p;
struct Brick {
    p r;
    bool is_alive;
} ;
struct Paddle {
    v velocity = {0, 0, 0, 0};
    p area = {350, 550, 100, 20};
    void move(SDL_Keycode key) {
        if (key == SDLK_a){
            velocity.x = -1;
            if (area.x <= 0) {
                velocity.x = 0;
                area.x = 0;
            }
        }
        if (key == SDLK_d){
            velocity.x = 1;
            if (area.x + area.w >= SCREEN_WIDTH) {
                velocity.x = 0;
                area.x = SCREEN_WIDTH - area.w;
            }
        }
    }
    void update() {
        area.x += velocity.x;
        if (area.x <= 0) {
            area.x = 0;
        }
        if (area.x + area.w >= SCREEN_WIDTH) {
            area.x = SCREEN_WIDTH - area.w;
        }
    }
};
struct Ball {
    v velocity = {0, 0, 0, 0};
    p position = {395, 295, 10, 10};
    p r = position;
    void start(SDL_Keycode key) {
        if (key == SDLK_SPACE && velocity.x == 0 && velocity.y == 0)
            velocity = {1, 1};
    }
    bool update(const Paddle &paddle) {
        if (SDL_GetTicks() % 2 == 0) {
            if (position.x <= 0 || position.x >= SCREEN_WIDTH - position.w) {
                velocity.x *= -1;
            }
            if (position.y <= 0) {
                velocity.y *= -1;
            }
            if (position.y >= SCREEN_HEIGHT - position.h) {
                velocity = {0, 0};
                position = {395, 295, 10, 10};
                return false;
            }
            if (position.y == (paddle.area.y)) {
                if ((position.x >= paddle.area.x) && (position.x <= paddle.area.x + paddle.area.w)) {
                    velocity.y *= -1;
                }
            }
            position.x += velocity.x;
            position.y += velocity.y;
            (SDL_Point&)r = (SDL_Point&)position;
            return true;
        }
    }
};
struct Game {
    std::array<Brick, 16 * 10> bricks;
    Ball ball;
    Paddle paddle;
    Game() {
        for (auto &brick: bricks) {
            int i = std::distance(bricks.data(), &brick);
            brick.r = {1 + ((i % 16) * (BRICK_WIDTH + 2)), 1 + ((i / 16) * (BRICK_HEIGHT + 2)), BRICK_WIDTH, BRICK_HEIGHT};
            brick.is_alive = true;   
        }
    }
};
int main(int argc, char* args[]) {
    Game game;
    bool quit = false;
    SDL_Window* window = NULL;
    SDL_Surface* screenSurface = NULL;
    SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO);
    window = SDL_CreateWindow("Strongout", 50, 50, SCREEN_WIDTH, SCREEN_HEIGHT, SDL_WINDOW_SHOWN );
    while (!quit) {
        SDL_Event event;
        while (SDL_PollEvent(&event)){
            if( event.type == SDL_QUIT){
                quit = true;
                break;
            }
            if( event.type == SDL_KEYDOWN ){
                game.paddle.move(event.key.keysym.sym);
                game.ball.start(event.key.keysym.sym);
            }
            if( event.type == SDL_KEYUP ){
                game.paddle.velocity.x = 0;
            } 
        }
        if (game.lives > 0) {
            game.paddle.update();
            game.ball.update(game.paddle);
        }
        int numBricks = 160;
        for (int i = 0; i < numBricks; i++) {
            if (SDL_HasIntersection(&game.ball.r, &game.bricks[i].r) && game.bricks[i].is_alive) {
                game.ball.position.y += 2;
                game.ball.velocity.y *= -1;
                game.bricks[i].is_alive = false;
            }
        }
        screenSurface = SDL_GetWindowSurface(window);
        SDL_FillRect(screenSurface, NULL, SDL_MapRGB( screenSurface->format, 0, 0, 0));
        for (auto &brick: game.bricks) {
            if (brick.is_alive) {
                SDL_FillRect(screenSurface, &brick.r, SDL_MapRGB( screenSurface->format, 255, 0, 0));
            }
        }
        SDL_FillRect(screenSurface, &game.paddle.area, SDL_MapRGB( screenSurface->format, 0, 255, 0));
        SDL_FillRect(screenSurface, &game.ball.r, SDL_MapRGB( screenSurface->format, 255, 255, 255));
        SDL_UpdateWindowSurface(window);
    }
    SDL_DestroyWindow(window);
    SDL_Quit();
    return 0;
}

Получилось в итоге 132 строки. Убирать больше особо нечего, до границы в 100 строк дальше, чем до Эдинбурга. Тут я понимаю, что надо либо кардинально сворачивать код, что конечно же снижает читаемость и ведет к штрафным баллам, либо добивать игру фичами. За пять минут получилось набросать отображение жизней для шарика и логику их убывания если шарик оказался под планкой. Хотел еще добавить отображения очков, но пришлось бы тащить отображение шрифтов, для 10 оставшихся минут задача непосильная. В итоге сосредоточился на сворачивании кода уже не особо заботясь о читабельности.

Получился все еще читаемый код, но уже ближе к сотне строк. Спрашиваю у собеседника, во сколько уложился он - оказалось в 90 строк.

Strongout/100 lines
using p = SDL_Rect; using v = p;
const p SW{800, 600, 48, 20};
struct Brick : public p { bool is_alive = true; };
struct Paddle : public p {
    v velocity = {0, 0, 0, 0};
    void move(SDL_Keycode key) {
        if (key == SDLK_a){
            velocity.x = -1;
            if (x <= 0) { velocity.x = x = 0; }
        }
        if (key == SDLK_d){
            velocity.x = 1;
            if (x + w >= SW.x) {
                velocity.x = 0;
                x = SW.x - w;
            }
        }
    }
    void update() {
        x = std::max(x += velocity.x, 0);
        if (x + w >= SW.x) { x = SW.x- w; }
    }
};
struct Ball {
    v velocity = {0, 0, 0, 0};
    p position = {395, 295, 10, 10};
    p r = position;
    void start(SDL_Keycode key) {
        if (key == SDLK_SPACE && velocity.x == 0 && velocity.y == 0)
            velocity = {1, 1};
    }
    bool update(const Paddle &paddle) {
        if (SDL_GetTicks() % 2 == 1) { return true; }
        velocity.x *= (position.x <= 0 || position.x >= SW.x - position.w) ? -1 : 1;
        velocity.y *= (position.y <= 0) ? -1 : 1;
        if (position.y >= SW.y - position.h) {
            velocity = {0, 0};
            position = {395, 295, 10, 10};
            return false;
        }
        velocity.y *= (position.y == (paddle.y) && (position.x >= paddle.x) && (position.x <= paddle.x + paddle.w)) ? -1 : 1;
        position.x += velocity.x;
        position.y += velocity.y;
        (SDL_Point&)r = (SDL_Point&)position;
        return true;
    }
};
int main(int argc, char* args[]) {
    std::array<Brick, 16 * 10> bricks;
    Ball ball;
    Paddle paddle = {350, 550, 100, 20};
    int lives = 3;
    bool quit = false;
    SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO);
    auto window = SDL_CreateWindow("Strongout", 50, 50, SW.x, SW.y, SDL_WINDOW_SHOWN );
    for (auto &brick: bricks) {
        int i = std::distance(bricks.data(), &brick);
        brick = {1 + ((i % 16) * (SW.w + 2)), 1 + ((i / 16) * (SW.h + 2)), SW.w, SW.h};
    }
    while (!quit) {
        SDL_Event event;
        while (SDL_PollEvent(&event)){
            if(event.type == SDL_QUIT){
                quit = true;
                break;
            }
            if(event.type == SDL_KEYDOWN){
                paddle.move(event.key.keysym.sym);
                ball.start(event.key.keysym.sym);
            }
            if (event.type == SDL_KEYUP) { paddle.velocity.x = 0; } 
        }
        if (lives > 0) {
            paddle.update();
            bool alive = ball.update(paddle);
            lives -= alive ? 0 : 1;
        }
        for (auto &brick: bricks) {
            if (SDL_HasIntersection(&ball.r, &brick) && brick.is_alive) {
                ball.position.y += 2;
                ball.velocity.y *= -1;
                brick.is_alive = false;
            }
        }
        auto screenSurface = SDL_GetWindowSurface(window);
        SDL_FillRect(screenSurface, NULL, SDL_MapRGB( screenSurface->format, 0, 0, 0));
        for (auto &brick: bricks) {
            brick.is_alive && SDL_FillRect(screenSurface, &brick, SDL_MapRGB( screenSurface->format, 255, 0, 0));
        }
        SDL_FillRect(screenSurface, &paddle, SDL_MapRGB( screenSurface->format, 0, 255, 0));
        SDL_FillRect(screenSurface, &ball.r, SDL_MapRGB( screenSurface->format, 255, 255, 255));
        for (int i = 0; i < lives; i++) {
            SDL_Rect rrect{5 + (i * 15), (SW.y - 15), 10, 10};
            SDL_FillRect(screenSurface, &rrect, SDL_MapRGB( screenSurface->format, 255, 255, 255));
        }
        SDL_UpdateWindowSurface(window);
    }
    SDL_DestroyWindow(window);
    SDL_Quit();
    return 0;
}

Чем я хуже потомственного британца? Свернуть можно все, разворачивать потом будет больно. Оставшиеся 10 минут я потратил на "художества", кодом получившийся результат назвать язык не поворачивается, но оно работает. Заменил все возможные if на тернарники и операции логического AND. Свернул получившиеся однострочные функции в одну строку. Подменил, где можно, функции на операторы выполнения, и константы на прямые значения. И "это" все еще оставалось относительно читаемым. Рекорд британца по шкале арканодов я побил.

Встречайте, Breakfunoid в 32 строчках.

using P = SDL_Rect; struct Brick : public P { bool a = true; }; auto FillRect = &SDL_FillRect; auto MapRGB = &SDL_MapRGB;
struct Paddle : public P { P v{0, 0, 0, 0};
    void operator()() { x = std::max(x += v.x, 0); (x + w >= 800) && (x = 800 - w); }
    void operator()(SDL_Keycode k) { (v.x = (k == 97) ? -1 : v.x); (k == 97) && (x <= 0) && (v.x = x = 0); (v.x = k == 100 ? 1 : v.x); (k == 100) && (x + w >= 800) && (x = 800 - w) && (v.x = 0); }
};
struct Ball : public P { P p{395, 295, 10, 10}, v{0, 0, 0, 0};
    void operator()(SDL_Keycode k) { if (k == SDLK_SPACE && v.x == 0 && v.y == 0) { v = {1, 1}; } }
    bool operator()(const Paddle &paddle) { if (!(SDL_GetTicks() % 2)) { return true; }
        v.x *= (p.x <= 0 || p.x >= 800 - p.w) ? -1 : 1; v.y *= (p.y <= 0) ? -1 : 1;
        if (p.y >= 600 - p.h) { v = {0, 0}; p = {395, 295, 10, 10}; return false; }
        v.y *= (p.y == (paddle.y) && (p.x >= paddle.x) && (p.x <= paddle.x + paddle.w)) ? -1 : 1;
        p.x += v.x; p.y += v.y;
        (SDL_Point &)*this = (SDL_Point &)p;
        return true;
    }
};
int main(int, char**) { SDL_Init(0x30);
    Brick bricks[160]; Ball ball{395, 295, 10, 10}; Paddle pad{350, 550, 100, 20}; int lives = 3; SDL_Event e;
    auto window = SDL_CreateWindow("brkt.cpp", 50, 50, 800, 600, 4); auto s = SDL_GetWindowSurface(window);  auto f = s->format; 
    for (int i = 0; i < 160; ++i) { *(bricks+i) = {1 + ((i % 16) * (48 + 2)), 1 + ((i / 16) * (20 + 2)), 48, 20}; }
    while (true) { 
        while (SDL_PollEvent(&e)) { auto t = e.type; auto sym = e.key.keysym.sym;
          switch (t) { case 256: return 0;
                       case 768: pad(sym); ball(sym); break;
                       case 769: pad.v.x = 0; break; }
        } if (lives > 0) { pad(); lives -= ball(pad) ? 0 : 1; }
        FillRect(s, 0, MapRGB( f, 0, 0, 0)); FillRect(s, &pad, MapRGB( f, 0, -1, 0)); FillRect(s, &ball, MapRGB( f, -1, -1, -1));
        for (auto &br: bricks) { SDL_HasIntersection(&ball, &br) && br.a && (ball.p.y += 2) && (ball.v.y *= -1) && (br.a = 0); br.a && FillRect(s, &br, MapRGB(f, -1, 0, 0)); }
        for (int i = 0; i < lives; i++) { P r{5 + (i * 15), 585, 10, 10}; FillRect(s, &r, MapRGB(f, -1, -1, -1)); }
        SDL_UpdateWindowSurface(window);
    } return 0;
}

В итоге я получил 120 баллов за количество строк плюс 10 за фичу с жизнями и минус 30 (максимум) за читаемость кода. Думаю, они подымут максимальный штраф за читаемость после этого случая.

В общем посмеялись - разошлись.

Но это еще не конец истории, дома меня не отпускала мысль что можно свернуть и больше. Я точно видел код рендера на визитке.

Пускай будет код арканоида на визитке. Поколдовав еще немного за ноутом получилось что-то в этом стиле. Нечитабельно, непрактично, отладить не выйдет, но прикольно и работает.

#include <SDL.h>
#define RE return
#define OO operator
using R=SDL_Rect;using P=SDL_Point;struct B:public R{bool a=1;};auto $= &SDL_FillRect;
using I=int;I i=0;struct Pad:public R{R v{0}; void OO()(){x=std::max(x+=v.x,0);(x+w >=
800)&&(x=800-w);} void OO()(I k){(v.x=(k==97)?-1:v.x);(k==97)&&(x<=0)&&(v.x=x=0);(v.x=
k==100?1:v.x);(k==100)&&(x+w>=800)&&(x=800-w)&&(v.x=0);}};struct BL:public R{R p {395,
295,10,10},v{0};void OO()(I k){if(k==32&&!v.x&&!v.y){v={1,1};}}bool OO()(Pad &pd){if(!
(SDL_GetTicks()%2)){RE 1;}v.x*=(p.x<=0||p.x>=800-p.w)?-1:1;v.y*=(p.y<=0)?-1:1;if(p.y>=
600-p.h){v={0};p={395,295,10,10};RE 0;}v.y *=(p.y==(pd.y)&& (p.x>=pd.x)&&(p.x<=pd.x+pd
.w))?-1:1;p.x+=v.x;p.y+=v.y;(P&)*this=(P&)p;RE 1;}};I main(I,char**){SDL_Init(0x30); B
bricks[160];BL ball{395,295,0xA,10};Pad pad{350,550,100,20};I l=3;SDL_Event e; auto w=
SDL_CreateWindow("brk",50,50,800,600,4);auto s=SDL_GetWindowSurface(w); for(i=0;i<160;
++i){*(bricks+i)={1+((i%16)*50),1+((i/16)*22),48,20};}while(1){while(SDL_PollEvent(&e)
){I s=e.key.keysym.sym;switch(e.type){case 256:return 0;case 768:pad(s);ball(s);break;
case 769:pad.v.x=0;break;}}if(l>0){pad();l-=ball(pad)?0:1;}$(s,0,0xff000000);$(s,&pad,
0xff00ff00); $(s,&ball,0xffffffff); for(auto&br:bricks){SDL_HasIntersection(&ball,&br)
&&br.a&&(ball.p.y+=2)&&(ball.v.y*=-1)&&(br.a=0);br.a &&$(s,&br,0xffff0000);}for(i=0;i<
l;i++){R r{5+(i*15),585,10,10};$(s,&r,-1);}SDL_UpdateWindowSurface(w);} RE 0;}

https://godbolt.org/z/896s6j883

Подумываю сделать себе такую
Подумываю сделать себе такую

Всех с наступающим!

З.Ы. NDA на тестовое я никакой не подписывал, да и про это задание к "светлячкам" было известно, потому что они его раздавали на игровых конференциях, сами понимаете для чего :) Традиция

Комментарии (21)


  1. starik-2005
    17.12.2023 19:41

    Класс! Вспомнил детство и бейсик, на котором арканойда пилил этак в шестом классе. Строк в 30 вроде бы уложился, но точно не скажу )))))


  1. azTotMD
    17.12.2023 19:41

    Код, конечно, хорош, но

    позиции AI

    всё ожидал про AI почитать, как статья и закончилась...


    1. dalerank Автор
      17.12.2023 19:41

      извините что не оправдал ожиданий, про AI можно почитать тут (https://habr.com/ru/articles/774506/) и тут (https://habr.com/ru/articles/769696/)


      1. azTotMD
        17.12.2023 19:41

        это я читал, спасибо


      1. azTotMD
        17.12.2023 19:41

        Сейчас делаю вот такие штуки для НПС: объединение в поселения, добыча руды, разведение растений, обжиг руды, постройка. Но код получается довольно узко-специальный, хотелось бы большей общности и изящности. Смотрю вот разные подходы, может что и мне подойдёт.

        Осторожно, тяжелые гифки


        1. dalerank Автор
          17.12.2023 19:41

          Behavior Tree используете?


          1. azTotMD
            17.12.2023 19:41

            какая-то дикая смесь решающих деревьев, конченных автоматов и эвристик


            1. dalerank Автор
              17.12.2023 19:41

              Лучше сразу перейти на BT если есть возможность, это возможно займет некоторое время на имплементацию, но потом вернется сторицей в виде уменьшения времени разработки поведения и простоты разработки конечных блоков. Понятно, что серебряной пули нет, но BT самое близкое к ней решение пока что, опыт UE/Unity/Godot не даст соврать


              1. azTotMD
                17.12.2023 19:41

                Не уверен, что тут это будет оптимально как по эффективности, так и по читабельности.

                Даже для простых ситуаций, когда в соседней клеточке враг - дерево поведения может быть очень длинным. Ну да, скорее всего его надо бить. А может убегать, если мало жизней? А стоит ли - пока будешь поворачиваться - добьют? А может на полу лежит дубина получше - сначала её взять и бить? Или вдруг пол под ногами загорелся или треск в потолке и возможно пещера обвалится - нужно бежать.

                А уж если НПС болтается без дела и думает чем бы заняться, там вообще миллион вариантов и всё со всем связано. Крафтить то или другое, добыть ещё руды и обжигать? А если нету поблизости нужных минералов? отправится на поиски или заняться чем-то другим?

                А если представить, что НПС - это существа коллективные, они могут обмениваться вещами, помогать друг другу или мешать, разделять роли и т.д....


                1. dalerank Автор
                  17.12.2023 19:41

                  Все это решается приоритетами, как минимум до 10-20 активных нпс на одном треде вам не о чем беспокоиться. Плюс можно посмотреть механизм мониторов https://vimeo.com/272377974


                  1. azTotMD
                    17.12.2023 19:41

                    мониторы - это хорошо, спасибо, посмотрю.

                    а 10-20 - это как-то скромненько, их должны быть сотни.


                    1. dalerank Автор
                      17.12.2023 19:41

                      зависит от размера конечно, bt хорошо параллелится, создаете новую таску/тред, отдаете туда свою пачку нпс и вперед считать. 10 тасок и вот вам две сотни мобов, каждый со своим уникальным поведением


              1. vkni
                17.12.2023 19:41

                Правильно ли я понимаю, что BT — это такой многоуровневый pattern matching? Т.е. что-то вроде

                case a of
                   A1 -> case b of
                           B1 ..
                   A2 -> case c of
                           C1 ...

                Только уровень вложенности сумасшедший.


                1. dalerank Автор
                  17.12.2023 19:41

                  Любой ЯП это совокупность таких патернов, BT наоборот призван скрыть сложность инструментария, предоставив дизайнерам оперировать блоками уровня "иди туда", "открой дверь", "возьми объект" и тд


  1. kushchin
    17.12.2023 19:41

    А над чем работать предлагают?



  1. cher-nov
    17.12.2023 19:41

    Мы и сейчас иногда общаемся на форуме по ремейкам старых игр.

    А что это за форум такой, не подскажете?



  1. QuietWave
    17.12.2023 19:41

    Какое извращение


  1. JordanCpp
    17.12.2023 19:41

    Чем я хуже потомственного британца? Свернуть можно все, разворачивать потом будет больно. Оставшиеся 10 минут я потратил на "художества", кодом получившийся результат назвать язык не поворачивается, но оно работает.

    Но для чего. Так можно почти любое количество строк впихнуть. В чём смысл обфусцировать пробелы и табы?

    Никто же в здравом уме не будет ревьюить и принимать в прод такой код.

    Ваш код можно уместить в одну строку, но сами понимаете:)


    1. dalerank Автор
      17.12.2023 19:41

      Условия тестового такие, какие есть и формально это можно обернуть себе на пользу. Всё можно уместить в одну строку, любой тест можно сломать. Если у вас это вызвало улыбку, значит время на статью потрачено не зря.