Adding 2048.c

This commit is contained in:
Lluciocc 2026-04-16 17:58:08 +02:00 committed by GitHub
parent 5141eaea60
commit 914c60e1f1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

519
src/userland/games/2048.c Normal file
View file

@ -0,0 +1,519 @@
#include "libc/syscall.h"
#include "libc/libui.h"
#include "libc/stdlib.h"
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
/*
@Lluciocc
2048 for BoredOS
Controls:
- WASD keys or arrow keys or numpad keys to move
- R key to restart
*/
#define WINDOW_W 300
#define WINDOW_H 430
#define BOARD_SIZE 4
#define TILE_SIZE 56
#define TILE_GAP 8
#define BOARD_X 18
#define BOARD_Y 96
#define BTN_W 54
#define BTN_H 30
#define COLOR_BG 0xFF121212
#define COLOR_PANEL 0xFF202020
#define COLOR_PANEL_2 0xFF2A2A2A
#define COLOR_BORDER 0xFF3D3D3D
#define COLOR_TEXT 0xFFF2F2F2
#define COLOR_TEXT_DARK 0xFF202020
#define COLOR_MUTED 0xFFBBBBBB
#define COLOR_ACCENT 0xFF6EA8FE
#define COLOR_GREEN 0xFF69DB7C
#define COLOR_RED 0xFFFF6B6B
#define COLOR_EMPTY_TILE 0xFF2D2D2D
static int board[BOARD_SIZE][BOARD_SIZE];
static int score = 0;
static int best_tile = 0;
static bool game_over = false;
static bool game_won = false;
static bool has_moved_last_turn = false;
static uint32_t random_seed = 0xC0FFEE12u;
static uint32_t random_next(void) {
random_seed = random_seed * 1664525u + 1013904223u;
return random_seed;
}
static int max_int(int a, int b) {
return (a > b) ? a : b;
}
static void clear_board(void) {
for (int y = 0; y < BOARD_SIZE; y++) {
for (int x = 0; x < BOARD_SIZE; x++) {
board[y][x] = 0;
}
}
}
static int count_empty_cells(void) {
int count = 0;
for (int y = 0; y < BOARD_SIZE; y++) {
for (int x = 0; x < BOARD_SIZE; x++) {
if (board[y][x] == 0) {
count++;
}
}
}
return count;
}
static void add_random_tile(void) {
int empty_count = count_empty_cells();
if (empty_count <= 0) {
return;
}
int pick = (int)(random_next() % (uint32_t)empty_count);
for (int y = 0; y < BOARD_SIZE; y++) {
for (int x = 0; x < BOARD_SIZE; x++) {
if (board[y][x] == 0) {
if (pick == 0) {
/* 90% chance of a 2, 10% chance of a 4 */
board[y][x] = ((random_next() % 10u) == 0u) ? 4 : 2;
best_tile = max_int(best_tile, board[y][x]);
return;
}
pick--;
}
}
}
}
static bool can_make_any_move(void) {
for (int y = 0; y < BOARD_SIZE; y++) {
for (int x = 0; x < BOARD_SIZE; x++) {
if (board[y][x] == 0) {
return true;
}
if (x + 1 < BOARD_SIZE && board[y][x] == board[y][x + 1]) {
return true;
}
if (y + 1 < BOARD_SIZE && board[y][x] == board[y + 1][x]) {
return true;
}
}
}
return false;
}
static void refresh_end_state(void) {
game_won = false;
for (int y = 0; y < BOARD_SIZE; y++) {
for (int x = 0; x < BOARD_SIZE; x++) {
if (board[y][x] >= 2048) {
game_won = true;
}
if (board[y][x] > best_tile) {
best_tile = board[y][x];
}
}
}
game_over = !can_make_any_move();
}
static void init_game(void) {
clear_board();
score = 0;
best_tile = 0;
game_over = false;
game_won = false;
has_moved_last_turn = false;
add_random_tile();
add_random_tile();
refresh_end_state();
}
static void copy_line_from_row(int row, int out[BOARD_SIZE]) {
for (int i = 0; i < BOARD_SIZE; i++) {
out[i] = board[row][i];
}
}
static void copy_line_to_row(int row, const int in[BOARD_SIZE]) {
for (int i = 0; i < BOARD_SIZE; i++) {
board[row][i] = in[i];
}
}
static void copy_line_from_col(int col, int out[BOARD_SIZE]) {
for (int i = 0; i < BOARD_SIZE; i++) {
out[i] = board[i][col];
}
}
static void copy_line_to_col(int col, const int in[BOARD_SIZE]) {
for (int i = 0; i < BOARD_SIZE; i++) {
board[i][col] = in[i];
}
}
static void reverse_line(int line[BOARD_SIZE]) {
for (int i = 0; i < BOARD_SIZE / 2; i++) {
int tmp = line[i];
line[i] = line[BOARD_SIZE - 1 - i];
line[BOARD_SIZE - 1 - i] = tmp;
}
}
static bool slide_and_merge_line_left(int line[BOARD_SIZE]) {
int compact[BOARD_SIZE];
int merged[BOARD_SIZE];
int compact_len = 0;
int write_index = 0;
bool changed = false;
for (int i = 0; i < BOARD_SIZE; i++) {
merged[i] = 0;
if (line[i] != 0) {
compact[compact_len++] = line[i];
}
}
for (int i = 0; i < compact_len; i++) {
if (i + 1 < compact_len && compact[i] == compact[i + 1]) {
int value = compact[i] * 2;
merged[write_index++] = value;
score += value;
best_tile = max_int(best_tile, value);
i++; /* Skip the second tile, it has been merged. */
} else {
merged[write_index++] = compact[i];
}
}
while (write_index < BOARD_SIZE) {
merged[write_index++] = 0;
}
for (int i = 0; i < BOARD_SIZE; i++) {
if (line[i] != merged[i]) {
changed = true;
}
line[i] = merged[i];
}
return changed;
}
typedef enum {
MOVE_LEFT,
MOVE_RIGHT,
MOVE_UP,
MOVE_DOWN
} move_dir_t;
static bool apply_move(move_dir_t dir) {
bool changed = false;
if (game_over) {
return false;
}
if (dir == MOVE_LEFT || dir == MOVE_RIGHT) {
for (int row = 0; row < BOARD_SIZE; row++) {
int line[BOARD_SIZE];
copy_line_from_row(row, line);
if (dir == MOVE_RIGHT) {
reverse_line(line);
}
if (slide_and_merge_line_left(line)) {
changed = true;
}
if (dir == MOVE_RIGHT) {
reverse_line(line);
}
copy_line_to_row(row, line);
}
} else {
for (int col = 0; col < BOARD_SIZE; col++) {
int line[BOARD_SIZE];
copy_line_from_col(col, line);
if (dir == MOVE_DOWN) {
reverse_line(line);
}
if (slide_and_merge_line_left(line)) {
changed = true;
}
if (dir == MOVE_DOWN) {
reverse_line(line);
}
copy_line_to_col(col, line);
}
}
if (changed) {
add_random_tile();
}
has_moved_last_turn = changed;
refresh_end_state();
return changed;
}
static uint32_t get_tile_color(int value) {
switch (value) {
case 0: return COLOR_EMPTY_TILE;
case 2: return 0xFFEEE4DA;
case 4: return 0xFFEDE0C8;
case 8: return 0xFFF2B179;
case 16: return 0xFFF59563;
case 32: return 0xFFF67C5F;
case 64: return 0xFFF65E3B;
case 128: return 0xFFEDCF72;
case 256: return 0xFFEDCC61;
case 512: return 0xFFEDC850;
case 1024: return 0xFFEDC53F;
case 2048: return 0xFFEDC22E;
default: return 0xFF3C91E6;
}
}
static uint32_t get_tile_text_color(int value) {
(void)value;
return 0xFF000000; // for visibility
}
static void int_to_text(int value, char *out) {
itoa(value, out);
}
static void draw_centered_text(ui_window_t win, int x, int y, int w, int h,
const char *text, uint32_t color, float scale) {
(void)scale;
uint32_t text_w = ui_get_string_width(text);
uint32_t text_h = ui_get_font_height();
int draw_x = x + (w - (int)text_w) / 2;
int draw_y = y + (h - (int)text_h) / 2;
ui_draw_string(win, draw_x, draw_y, text, color);
}
static void draw_button(ui_window_t win, int x, int y, int w, int h,
const char *label, uint32_t color) {
ui_draw_rounded_rect_filled(win, x, y, w, h, 6, color);
draw_centered_text(win, x, y, w, h, label, COLOR_TEXT, 1.0f);
}
static void draw_score_box(ui_window_t win, int x, int y, int w, int h,
const char *title, int value) {
char buf[16];
int_to_text(value, buf);
ui_draw_rounded_rect_filled(win, x, y, w, h, 8, COLOR_PANEL_2);
draw_centered_text(win, x, y + 3, w, 14, title, COLOR_MUTED, 0.8f);
draw_centered_text(win, x, y + 16, w, 18, buf, COLOR_TEXT, 1.0f);
}
static void draw_tile(ui_window_t win, int x, int y, int value) {
char buf[16];
float scale = 1.6f;
ui_draw_rounded_rect_filled(win, x, y, TILE_SIZE, TILE_SIZE, 8, get_tile_color(value));
if (value == 0) {
return;
}
int_to_text(value, buf);
if (value >= 1000) {
scale = 1.2f;
} else if (value >= 100) {
scale = 1.4f;
}
draw_centered_text(
win,
x, y,
TILE_SIZE, TILE_SIZE,
buf,
0xFF000000,
scale
);
}
static void draw_board(ui_window_t win) {
int board_w = BOARD_SIZE * TILE_SIZE + (BOARD_SIZE + 1) * TILE_GAP;
int board_h = board_w;
ui_draw_rounded_rect_filled(win, BOARD_X, BOARD_Y, board_w, board_h, 10, COLOR_PANEL_2);
for (int y = 0; y < BOARD_SIZE; y++) {
for (int x = 0; x < BOARD_SIZE; x++) {
int px = BOARD_X + TILE_GAP + x * (TILE_SIZE + TILE_GAP);
int py = BOARD_Y + TILE_GAP + y * (TILE_SIZE + TILE_GAP);
draw_tile(win, px, py, board[y][x]);
}
}
}
static void draw_status(ui_window_t win) {
if (game_over) {
ui_draw_string(win, 18, 70, "Game over", COLOR_RED);
} else if (game_won) {
ui_draw_string(win, 18, 70, "2048 reached - keep going", COLOR_GREEN);
} else {
ui_draw_string(win, 18, 70, "Combine tiles to reach 2048", COLOR_MUTED);
}
}
static void game_paint(ui_window_t win) {
ui_draw_rect(win, 0, 0, WINDOW_W, WINDOW_H, COLOR_BG);
ui_draw_string_scaled(win, 18, 12, "2048", COLOR_TEXT, 1.6f);
ui_draw_string(win, 18, 42, "Use WASD keys", COLOR_MUTED);
draw_score_box(win, 155, 14, 56, 40, "SCORE", score);
draw_score_box(win, 220, 14, 62, 40, "BEST", best_tile);
draw_status(win);
draw_board(win);
/* Not recommended to use
draw_button(win, 18, 352, 86, 30, "Restart", COLOR_ACCENT);
draw_button(win, 123, 352, BTN_W, BTN_H, "Left", COLOR_BORDER);
draw_button(win, 186, 352, BTN_W, BTN_H, "Right", COLOR_BORDER);
draw_button(win, 92, 390, BTN_W, BTN_H, "Up", COLOR_BORDER);
draw_button(win, 155, 390, BTN_W, BTN_H, "Down", COLOR_BORDER);
*/
}
static bool point_in_rect(int px, int py, int x, int y, int w, int h) {
return px >= x && px < x + w && py >= y && py < y + h;
}
/*
37 = left arrow
38 = up arrow
39 = right arrow
40 = down arrow
72 = numpad 8
75 = numpad 4
77 = numpad 6
80 = numpad 2
key = letter
*/
static bool is_left_key(int key) {
return key == 'a' || key == 'A' || key == 37 || key == 75;
}
static bool is_right_key(int key) {
return key == 'd' || key == 'D' || key == 39 || key == 77;
}
static bool is_up_key(int key) {
return key == 'w' || key == 'W' || key == 38 || key == 72;
}
static bool is_down_key(int key) {
return key == 's' || key == 'S' || key == 40 || key == 80;
}
static void handle_click(int x, int y) {
if (point_in_rect(x, y, 18, 352, 86, 30)) {
init_game();
return;
}
if (point_in_rect(x, y, 123, 352, BTN_W, BTN_H)) {
apply_move(MOVE_LEFT);
return;
}
if (point_in_rect(x, y, 186, 352, BTN_W, BTN_H)) {
apply_move(MOVE_RIGHT);
return;
}
if (point_in_rect(x, y, 92, 390, BTN_W, BTN_H)) {
apply_move(MOVE_UP);
return;
}
if (point_in_rect(x, y, 155, 390, BTN_W, BTN_H)) {
apply_move(MOVE_DOWN);
return;
}
}
static void handle_key(int key) {
if (key == 'r' || key == 'R') {
init_game();
return;
}
if (is_left_key(key)) {
apply_move(MOVE_LEFT);
} else if (is_right_key(key)) {
apply_move(MOVE_RIGHT);
} else if (is_up_key(key)) {
apply_move(MOVE_UP);
} else if (is_down_key(key)) {
apply_move(MOVE_DOWN);
}
}
int main(int argc, char **argv) {
(void)argc;
(void)argv;
ui_window_t win = ui_window_create("2048", 240, 120, WINDOW_W, WINDOW_H);
if (!win) {
return 1;
}
init_game();
gui_event_t ev;
while (1) {
if (ui_get_event(win, &ev)) {
if (ev.type == GUI_EVENT_PAINT) {
game_paint(win);
ui_mark_dirty(win, 0, 0, WINDOW_W, WINDOW_H);
} else if (ev.type == GUI_EVENT_CLICK) {
handle_click(ev.arg1, ev.arg2);
game_paint(win);
ui_mark_dirty(win, 0, 0, WINDOW_W, WINDOW_H);
} else if (ev.type == GUI_EVENT_KEY) {
handle_key(ev.arg1);
game_paint(win);
ui_mark_dirty(win, 0, 0, WINDOW_W, WINDOW_H);
} else if (ev.type == GUI_EVENT_CLOSE) {
sys_exit(0);
} else if (ev.type == GUI_EVENT_RESIZE) {
game_paint(win);
ui_mark_dirty(win, 0, 0, WINDOW_W, WINDOW_H);
}
} else {
sys_yield();
}
}
return 0;
}