Newer
Older
cneboy_games / snake / snake.c
@Mark Bavis Mark Bavis on 9 Aug 2022 14 KB Rebuild snake with new API
#include <client_api.h>
#include <stdint.h>

#if 0
#define BLOCK_SIZE 6
#define BOARD_W 25
#define BOARD_H 22
#else
#define BLOCK_SIZE 8
#define BOARD_W 19
#define BOARD_H 16
#endif

uint8_t* board;

#define EMPTY 0
#define BLOCK 1
#define SN_RIGHT 2
#define SN_UP 3
#define SN_LEFT 4
#define SN_DOWN 5
#define FRUIT 6

#define NUM_LEVEL_FRUIT 5

#define PAD_X (BLOCK_SIZE + (SCREEN_W - BLOCK_SIZE * (BOARD_W + 1)) / 2)
#define PAD_Y 2

#define INDEX(x, y) (y * BOARD_W + x)

#define INITIAL_FILL_MS 30

#define FRUIT_FLASH 600

#define COUNTDOWN 3
#define COUNTDOWN_MS 800

int countdown = COUNTDOWN;
int countdown_timer = COUNTDOWN_MS;

int fillMS = INITIAL_FILL_MS;
int fillTimer = 0;

int tailWaitTimer = 0;

int level;
int lastGoodLevel = 0;

typedef struct
{
    int x, y;
    int px, py;
    int fill;
} snake_t;

bool boardValid = false;

int emptyTiles = 0;

float scoreIncrease = 10;
int score = 0;
int lastPScore = 0;
int prestige = 1;
void writeUI();

snake_t tail, head;

void coords(int index, int* x, int* y)
{
    *y = index / BOARD_W;
    *x = index - *y * BOARD_W;
}

uint8_t snakeState(snake_t* snake)
{
    if (snake->px < snake->x) return SN_RIGHT;
    if (snake->px > snake->x) return SN_LEFT;
    if (snake->py < snake->y) return SN_DOWN;
    if (snake->py > snake->y) return SN_UP;
    return EMPTY;
}

void nextBlock(int x, int y, uint8_t state, int* nx, int* ny)
{
    *nx = x;
    *ny = y;
    switch (state)
    {
        case SN_RIGHT: (*nx)++; break;
        case SN_LEFT: (*nx)--; break;
        case SN_DOWN: (*ny)++; break;
        case SN_UP: (*ny)--; break;
    }
}

void prevBlock(int x, int y, uint8_t state, int* nx, int* ny)
{
    *nx = x;
    *ny = y;
    switch (state)
    {
        case SN_RIGHT: (*nx)--; break;
        case SN_LEFT: (*nx)++; break;
        case SN_DOWN: (*ny)--; break;
        case SN_UP: (*ny)++; break;
    }
}

void drawSnake(int x, int y, int fill, uint8_t in_state, uint8_t fill_state, uint8_t out_state, uint8_t color);
bool spawnFruit();

bool setStart(int x, int y, uint8_t dir)
{
    if (tail.x != -1)
    {
        printf("Board has multiple starts\n");
        return false;
    }
    board[INDEX(x, y)] = dir;
    tail.x = head.px = x;
    tail.y = head.py = y;
    nextBlock(x, y, dir, &head.x, &head.y);
    if (board[INDEX(head.x, head.y)] != EMPTY)
    {
        printf("Snake start point would put the head of the snake in a wall\n");
        return false;
    }
    board[INDEX(head.x, head.y)] = dir;
    emptyTiles -= 2;
    return true;
}

void clearSnake(snake_t* s)
{
    s->x = -1;
    s->y = -1;
    s->px = -1;
    s->py = -1;
    s->fill = 0;
}

pbm_t* block;

int num_fruit = 0;
pbm_t** fruit;

int level_fruit[NUM_LEVEL_FRUIT];
int current_fruit = 0;

void startLevel(int l);

void reset()
{
    fillMS = INITIAL_FILL_MS;

    score = 0;
    lastPScore = 0;
    scoreIncrease = 10;
    prestige = 1;

    startLevel(1);
}

void loadBoard(char* path);

void startLevel(int l)
{
    level = l;
    char b[32];
    sprintf(b, "/board%d_%d.txt", BLOCK_SIZE, level);
    loadBoard(b);
    writeUI();

    countdown = COUNTDOWN;
    countdown_timer = COUNTDOWN_MS;
}

void loadBoard(char* path)
{
    d_clear();

    clearSnake(&tail);
    clearSnake(&head);

    head.fill = 3;
    tail.fill = 0;

    fillTimer = 0;

    tailWaitTimer = BLOCK_SIZE * 3;

    boardValid = false;

    current_fruit = 0;
    
    emptyTiles = BOARD_W * BOARD_H;

    int x = 0, y = 0;

    size_t board_desc_len;
    char* board_desc = f_contents(path, &board_desc_len);
    if (!board_desc)
    {
        printf("Invalid %s\n", path);
        goto cleanup;
    }

    if (!board) board = malloc(BOARD_W * BOARD_H);
    if (!board)
    {
        printf("Couldn't alloc board\n");
        goto cleanup;
    }

    memset(board, EMPTY, BOARD_W * BOARD_H);

    for (int i = 0; i < board_desc_len; i++)
    {
        if (y >= BOARD_H) break;
        char b = board_desc[i];
        bool whitespace = isspace(b);
        if (b == '\n')
        {
            x = 0;
            y++;
            continue;
        }
        if (x >= BOARD_W)
        {
            if (whitespace) continue;
            while (i + 1 < board_desc_len && board_desc[i + 1] != '\n') i++;
            printf("y=%d has too many characters\n", y);
            continue;
        }

        if (b == ' ' || b == '0')
        {
            x++;
            continue;
        }

        if (whitespace) continue;

        switch (b)
        {
            case '^': if (!setStart(x, y, SN_UP)) goto cleanup; break;
            case '>': if (!setStart(x, y, SN_RIGHT)) goto cleanup; break;
            case '<': if (!setStart(x, y, SN_LEFT)) goto cleanup; break;
            case 'v': if (!setStart(x, y, SN_DOWN)) goto cleanup; break;
            default:
              {
                  board[INDEX(x, y)] = BLOCK;
                  emptyTiles--;
              }
              break;
        }
        x++;
    }
    if (tail.x == -1)
    {
        printf("Board didn't include start position\n");
        goto cleanup;
    }

    boardValid = true;

    for (int y = 0; y < BOARD_H; y++)
    {
        for (int x = 0; x < BOARD_W; x++)
        {
            if (board[INDEX(x, y)] == BLOCK)
            {
                d_pbm(PAD_X + x * BLOCK_SIZE, PAD_Y + y * BLOCK_SIZE, block, 0, 0, 0, 0, BLACK, WHITE, R_NONE, false, false);
            }
        }
    }

    uint8_t startState = board[INDEX(head.x, head.y)];

    drawSnake(tail.x, tail.y, BLOCK_SIZE - 1, 0, startState, startState, BLACK);
    drawSnake(tail.x, tail.y, tail.fill, 0, startState, startState, WHITE);
    drawSnake(head.x, head.y, head.fill, startState, startState, startState, BLACK);

    for (int i = 0; i < NUM_LEVEL_FRUIT; i++)
    {
        level_fruit[i] = random(0, num_fruit);
    }

    spawnFruit();

cleanup:
    if (board_desc) free(board_desc);
}

int fruitFlashTimer = FRUIT_FLASH;

void drawFruitUIIndex(int i, int yOrder, bool clear);
void drawFruitUI()
{
    d_fillRect(0, 0, PAD_X, (BLOCK_SIZE + 2) * NUM_LEVEL_FRUIT + 10, WHITE);
    for (int i = current_fruit; i < NUM_LEVEL_FRUIT; i++)
    {
        drawFruitUIIndex(i, i - current_fruit, false);
    }
}

void drawFruitUIIndex(int i, int yOrder, bool clear)
{
    int x = (PAD_X - BLOCK_SIZE) / 2;
    int y = 3 + (BLOCK_SIZE + 2) * yOrder;
    if (clear) d_fillRect(x, y, BLOCK_SIZE, BLOCK_SIZE, WHITE);

    if (yOrder != 0 || fruitFlashTimer > 0)
    {
        d_pbm(x, y, fruit[level_fruit[i]], 0, 0, 0, 0, BLACK, WHITE, R_NONE, false, false);
    }
}

bool spawnFruit()
{
    if (emptyTiles <= 0) return false;

    int fruit_type = level_fruit[current_fruit];

    long r = random(0, emptyTiles);

    int c = -1;
    int i = -1;
    while (c < r)
    {
        i++;
        while (board[i] != EMPTY) i++;
        c++;
    }

    board[i] = FRUIT;

    int x, y;
    coords(i, &x, &y);
    printf("Fruit_%d at %d, index %d: %dx%d\n", fruit_type, r, i, x, y);

    d_pbm(PAD_X + x * BLOCK_SIZE, PAD_Y + y * BLOCK_SIZE, fruit[fruit_type], 0, 0, 0, 0, BLACK, WHITE, R_NONE, false, false);

    emptyTiles--;

    drawFruitUI();

    return true;
}

void clearBlock(int x, int y)
{
    d_fillRect(PAD_X + x * BLOCK_SIZE, PAD_Y + y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE, WHITE);
}

void drawSnake(int x, int y, int fill, uint8_t in_state, uint8_t fill_state, uint8_t out_state, uint8_t color)
{
    int tlX = PAD_X + x * BLOCK_SIZE;
    int tlY = PAD_Y + y * BLOCK_SIZE;

    if (in_state != EMPTY)
    {
        if (in_state == SN_RIGHT) d_fillRect(tlX, tlY + 1, 1, BLOCK_SIZE - 2, color);
        if (in_state == SN_DOWN) d_fillRect(tlX + 1, tlY, BLOCK_SIZE - 2, 1, color);
        if (in_state == SN_LEFT) d_fillRect(tlX + BLOCK_SIZE - 1, tlY + 1, 1, BLOCK_SIZE - 2, color);
        if (in_state == SN_UP) d_fillRect(tlX + 1, tlY + BLOCK_SIZE - 1, BLOCK_SIZE - 2, 1, color);
    }

    for (int i = 0; i < min(fill, BLOCK_SIZE - 2); i++)
    {
        if (fill_state == SN_RIGHT) d_fillRect(tlX + i + 1, tlY + 1, 1, BLOCK_SIZE - 2, color);
        if (fill_state == SN_DOWN) d_fillRect(tlX + 1, tlY + i + 1, BLOCK_SIZE - 2, 1, color);
        if (fill_state == SN_LEFT) d_fillRect(tlX + BLOCK_SIZE - 1 - i - 1, tlY + 1, 1, BLOCK_SIZE - 2, color);
        if (fill_state == SN_UP) d_fillRect(tlX + 1, tlY + BLOCK_SIZE - 1 - i - 1, BLOCK_SIZE - 2, 1, color);
    }

    if (fill == BLOCK_SIZE - 1 && out_state != EMPTY)
    {
        if (out_state == SN_LEFT) d_fillRect(tlX, tlY + 1, 1, BLOCK_SIZE - 2, color);
        if (out_state == SN_UP) d_fillRect(tlX + 1, tlY, BLOCK_SIZE - 2, 1, color);
        if (out_state == SN_RIGHT) d_fillRect(tlX + BLOCK_SIZE - 1, tlY + 1, 1, BLOCK_SIZE - 2, color);
        if (out_state == SN_DOWN) d_fillRect(tlX + 1, tlY + BLOCK_SIZE - 1, BLOCK_SIZE - 2, 1, color);
    }
}

int setup(int arg)
{
    char p[32];
    sprintf(p, "/block%d.pbm", BLOCK_SIZE);
    printf("PATH: %s\n", p);
    block = f_bitmap(p);
    if (!block) return 1;

    sprintf(p, "/fruit%d_%d.pbm", BLOCK_SIZE, num_fruit + 1);
    while (f_exists(p))
    {
        num_fruit++;
        sprintf(p, "/fruit%d_%d.pbm", BLOCK_SIZE, num_fruit + 1);
    }
    if (!num_fruit)
    {
        printf("Found no fruit graphics\n");
        return 1;
    }

    fruit = malloc(sizeof(pbm_t*) * num_fruit);
    for (int i = 0; i < num_fruit; i++)
    {
        sprintf(p, "/fruit%d_%d.pbm", BLOCK_SIZE, i + 1);
        fruit[i] = f_bitmap(p);
        if (!fruit[i])
        {
            printf("Failed to load %s\n", p);
            return 1;
        }
    }

    printf("Found %d fruit graphics\n", num_fruit);

    reset();
    if (!boardValid) return 1;

    return 0;
}


void writeUI()
{
    int y = SCREEN_H - BLOCK_SIZE * 2 + 3;

    d_fillRect(0, y, SCREEN_W, BLOCK_SIZE * 2, WHITE);
    d_text(8, y, BLACK, "SCORE: %d", score);
    d_textR(SCREEN_W - 8, y, BLACK, "P%d", prestige);
}

int loop(int ms)
{
    if (!boardValid) return 1;

    if (countdown > 0)
    {
    }

    uint8_t headState = board[INDEX(head.x, head.y)];
    uint8_t tailState = board[INDEX(tail.x, tail.y)];

    fillTimer += ms;
    while (fillTimer > fillMS)
    {
        fillTimer -= fillMS;

        // move tail
        if (tailWaitTimer > 0)
        {
            tailWaitTimer--;
        }
        else
        {
            tail.fill++;
        }

        if (tail.fill == BLOCK_SIZE)
        {
            clearBlock(tail.x, tail.y);

            board[INDEX(tail.x, tail.y)] = EMPTY;
            tail.px = tail.x;
            tail.py = tail.y;
            nextBlock(tail.px, tail.py, tailState, &tail.x, &tail.y);
            tail.fill = 0;

            emptyTiles++;
        }


        drawSnake(tail.x, tail.y, tail.fill, snakeState(&tail), tailState, tailState, WHITE);

        // move head
        head.fill++;
        uint8_t prevHeadMovement = snakeState(&head);
        if (head.fill == BLOCK_SIZE)
        {
            // redraw the last block in case we changed direction
            clearBlock(head.x, head.y);
            drawSnake(head.x, head.y, BLOCK_SIZE - 1, prevHeadMovement, prevHeadMovement, headState, BLACK);

            head.fill = 0;
            head.px = head.x;
            head.py = head.y;
            nextBlock(head.px, head.py, headState, &head.x, &head.y);
            bool newFruit = false;
            if (board[INDEX(head.x, head.y)] == FRUIT)
            {
                score += (int)roundf(scoreIncrease);
                scoreIncrease *= (1.0f + 0.1f * prestige);
                writeUI();

                // don't do emptyTiles-- here, this tile wasn't empty
                newFruit = true;
                tailWaitTimer = BLOCK_SIZE * 2;
            }
            else if (board[INDEX(head.x, head.y)] != EMPTY)
            {
                reset();
                return 0;
            }
            else
            {
                emptyTiles--;
            }

            board[INDEX(head.x, head.y)] = headState;

            if (newFruit)
            {
                if (current_fruit == NUM_LEVEL_FRUIT - 1)
                {
                    startLevel(level + 1);
                    if (boardValid)
                    {
                        lastGoodLevel = level;
                    }
                    else
                    {
                        startLevel(lastGoodLevel);
                    }
                    return 0;
                }
                current_fruit++;
                spawnFruit();
            }
            //printf("%d %d %d\n", headX, headY, headState);
            //
            prevHeadMovement = snakeState(&head);
        }

        clearBlock(head.x, head.y);
        drawSnake(head.x, head.y, head.fill, prevHeadMovement, prevHeadMovement, headState, BLACK);

    }

    if (button_down(DPAD_RIGHT) && head.px <= head.x) board[INDEX(head.x, head.y)] = SN_RIGHT;
    if (button_down(DPAD_LEFT) && head.px >= head.x) board[INDEX(head.x, head.y)] = SN_LEFT;
    if (button_down(DPAD_UP) && head.py >= head.y) board[INDEX(head.x, head.y)] = SN_UP;
    if (button_down(DPAD_DOWN) && head.py <= head.y) board[INDEX(head.x, head.y)] = SN_DOWN;
    //printf("%d %d %d %d\n", button_down(DPAD_RIGHT), button_down(DPAD_LEFT), button_down(DPAD_UP), button_down(DPAD_DOWN));
    
    if (button_pressed(BUTTON_A) && fillMS > 5)
    {
        prestige++;
        fillMS = max(5, fillMS - 5);
        writeUI();

        scoreIncrease = fmax(10, (score - lastPScore) * 0.1f);
        lastPScore = score;

        int nx, ny;

        nextBlock(tail.x, tail.y, board[INDEX(tail.x, tail.y)], &nx, &ny);
        while (nx != head.x || ny != head.y)
        {
            board[INDEX(tail.x, tail.y)] = EMPTY;
            printf("%d %d %d %d %d %d %d\n", tail.x, tail.y, nx, ny, board[INDEX(nx, ny)], head.x, head.y);
            clearBlock(tail.x, tail.y);
            tail.px = tail.x;
            tail.py = tail.y;
            tail.x = nx;
            tail.y = ny;
            nextBlock(tail.x, tail.y, board[INDEX(tail.x, tail.y)], &nx, &ny);
            emptyTiles++;
        }
        tail.fill = 0;
        uint8_t tailState = board[INDEX(tail.x, tail.y)];
        drawSnake(tail.x, tail.y, BLOCK_SIZE - 1, snakeState(&tail), tailState, tailState, BLACK);
        drawSnake(tail.x, tail.y, tail.fill, snakeState(&tail), tailState, tailState, WHITE);

        tailWaitTimer = BLOCK_SIZE * 3;
    }

    int oldFruitFlash = fruitFlashTimer;
    fruitFlashTimer -= ms;
    if (fruitFlashTimer <= -FRUIT_FLASH)
    {
        fruitFlashTimer = FRUIT_FLASH;
    }
    if ((oldFruitFlash < 0) != (fruitFlashTimer < 0))
    {
        drawFruitUIIndex(current_fruit, 0, true);
    }

    return 0;
}