feat: add 2048 game

This commit is contained in:
2026-05-19 21:34:28 +02:00
parent d07d70a026
commit a44a65d432
6 changed files with 678 additions and 4 deletions
+79
View File
@@ -0,0 +1,79 @@
# Makefile for 2048 (standalone MTK game) on MontaukOS
# Copyright (c) 2026 Daniel Hammer
MAKEFLAGS += -rR
.SUFFIXES:
TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-elf-
ifneq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),)
CXX := $(TOOLCHAIN_PREFIX)g++
else
CXX := g++
endif
PROG_INC := ../../include
LINK_LD := ../../link.ld
BINDIR := ../../bin
OBJDIR := obj
LIBDIR := ../../lib
CXXFLAGS := \
-std=gnu++20 \
-g -O2 -pipe \
-Wall \
-Wextra \
-Wno-unused-parameter \
-Wno-unused-function \
-nostdinc \
-ffreestanding \
-fno-stack-protector \
-fno-stack-check \
-fno-PIC \
-fno-rtti \
-fno-exceptions \
-ffunction-sections \
-fdata-sections \
-m64 \
-march=x86-64 \
-msse \
-msse2 \
-mno-red-zone \
-mcmodel=small \
-MMD -MP \
-I $(PROG_INC) \
-isystem $(PROG_INC)/libc \
-isystem ../../../kernel/freestnd-c-hdrs/x86_64/include \
-isystem ../../../kernel/freestnd-cxx-hdrs/x86_64/include
LDFLAGS := \
-nostdlib \
-static \
-Wl,--build-id=none \
-Wl,--gc-sections \
-Wl,-m,elf_x86_64 \
-z max-page-size=0x1000 \
-T $(LINK_LD)
SRCS := main.cpp stb_truetype_impl.cpp
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
TARGET := $(BINDIR)/apps/2048/2048.elf
LOADER_LIB := $(LIBDIR)/libloader/liblibloader.a
LIBC_LIB := $(LIBDIR)/libc/liblibc.a
.PHONY: all clean
all: $(TARGET)
$(TARGET): $(OBJS) $(LINK_LD) Makefile $(LOADER_LIB) $(LIBC_LIB)
mkdir -p $(BINDIR)/apps/2048
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LOADER_LIB) $(LIBC_LIB) -o $@
$(OBJDIR)/%.o: %.cpp Makefile
mkdir -p $(dir $@)
$(CXX) $(CXXFLAGS) -c $< -o $@
-include $(OBJS:.o=.d)
clean:
rm -rf $(OBJDIR) $(TARGET)
+545
View File
@@ -0,0 +1,545 @@
/*
* main.cpp
* MontaukOS 2048 - standalone MTK game
* Copyright (c) 2026 Daniel Hammer
*/
#include <cstdint>
#include <montauk/syscall.h>
#include <gui/gui.hpp>
#include <gui/canvas.hpp>
#include <gui/dialogs.hpp>
#include <gui/mtk.hpp>
#include <gui/mtk/settings.hpp>
#include <gui/standalone.hpp>
#include <gui/truetype.hpp>
extern "C" {
#include <stdio.h>
}
using namespace gui;
static constexpr int INIT_W = 420;
static constexpr int INIT_H = 540;
static constexpr int BOARD_N = 4;
static constexpr int HEADER_PAD = 16;
static constexpr int SCORE_CARD_H = 64;
static constexpr int SCORE_CARD_W = 112;
static constexpr int SCORE_LABEL_SIZE = 16;
static constexpr int SCORE_VALUE_SIZE = 32;
static constexpr int BUTTON_W = 94;
static constexpr int BUTTON_H = 30;
static constexpr int FOOTER_H = 34;
enum MoveDir : uint8_t {
DIR_LEFT = 0,
DIR_RIGHT,
DIR_UP,
DIR_DOWN,
};
struct GameState {
uint32_t cells[BOARD_N][BOARD_N];
unsigned long score;
unsigned long best;
bool game_over;
bool game_over_dialog_shown;
bool won;
};
struct AppLayout {
Rect title;
Rect score_card;
Rect best_card;
Rect new_button;
Rect board;
int tile;
int gap;
};
static WsWindow g_win;
static GameState g_game = {};
static uint32_t g_rng = 0x20482048u;
static int g_mouse_x = -1;
static int g_mouse_y = -1;
static char g_status[96] = {};
static uint64_t g_status_time = 0;
static Color g_accent = colors::ACCENT;
static Color shade(Color c, int delta) {
return Color::from_rgb(
(uint8_t)gui_clamp((int)c.r + delta, 0, 255),
(uint8_t)gui_clamp((int)c.g + delta, 0, 255),
(uint8_t)gui_clamp((int)c.b + delta, 0, 255));
}
static void set_status(const char* text) {
snprintf(g_status, sizeof(g_status), "%s", text ? text : "");
g_status_time = montauk::get_milliseconds();
}
static bool status_visible() {
return g_status[0] && (montauk::get_milliseconds() - g_status_time < 4000);
}
static void seed_rng() {
uint32_t seed = 0;
if (montauk::getrandom(&seed, sizeof(seed)) != (int64_t)sizeof(seed) || seed == 0) {
seed = (uint32_t)montauk::get_milliseconds();
seed ^= (uint32_t)montauk::get_ticks() << 11;
seed ^= (uint32_t)montauk::getpid() * 2654435761u;
}
if (seed == 0) seed = 0x20482048u;
g_rng = seed;
}
static uint32_t next_random() {
uint32_t x = g_rng;
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
if (x == 0) x = 0x6D2B79F5u;
g_rng = x;
return x;
}
static int count_digits(uint32_t value) {
int digits = 1;
while (value >= 10) {
value /= 10;
digits++;
}
return digits;
}
static mtk::Theme app_theme() {
mtk::Theme theme = mtk::make_theme(g_accent);
theme.radius_sm = 4;
theme.radius_md = 6;
theme.surface = Color::from_rgb(0xF0, 0xF2, 0xF4);
theme.surface_alt = Color::from_rgb(0xF7, 0xF8, 0xFA);
theme.surface_hover = mtk::mix(theme.surface, theme.accent, 26);
return theme;
}
static AppLayout compute_layout(int w, int h) {
AppLayout lo = {};
int pad = gui_clamp(w / 26, 12, HEADER_PAD);
int card_w = gui_clamp((w - pad * 2 - 8) / 2, 72, SCORE_CARD_W);
bool compact = w < 390;
lo.title = {pad, pad + 2, 116, 40};
if (compact) {
lo.new_button = {gui_max(pad, w - pad - BUTTON_W), pad + 8, BUTTON_W, BUTTON_H};
lo.score_card = {pad, pad + 56, card_w, SCORE_CARD_H};
lo.best_card = {pad + card_w + 8, pad + 56, card_w, SCORE_CARD_H};
} else {
lo.best_card = {w - pad - SCORE_CARD_W, pad, SCORE_CARD_W, SCORE_CARD_H};
lo.score_card = {lo.best_card.x - SCORE_CARD_W - 8, pad, SCORE_CARD_W, SCORE_CARD_H};
lo.new_button = {w - pad - BUTTON_W, pad + SCORE_CARD_H + 8, BUTTON_W, BUTTON_H};
}
int header_h = compact
? (pad + 56 + SCORE_CARD_H + 14)
: (pad + SCORE_CARD_H + BUTTON_H + 20);
int max_board_w = gui_max(w - pad * 2, 80);
int max_board_h = gui_max(h - header_h - FOOTER_H - pad, 80);
int board_size = gui_min(max_board_w, max_board_h);
int gap = gui_clamp(board_size / 42, 6, 10);
int tile = (board_size - gap * (BOARD_N + 1)) / BOARD_N;
if (tile < 12) tile = 12;
board_size = tile * BOARD_N + gap * (BOARD_N + 1);
lo.gap = gap;
lo.tile = tile;
lo.board = {(w - board_size) / 2, header_h, board_size, board_size};
if (lo.board.x < pad) lo.board.x = pad;
return lo;
}
static int text_w(const char* text, int size, bool bold = false) {
TrueTypeFont* font = (bold && fonts::system_bold) ? fonts::system_bold : fonts::system_font;
return gui::text_width(font, text, size);
}
static void draw_text_center(Canvas& c, const Rect& rect, const char* text,
Color color, int size, bool bold = false) {
TrueTypeFont* font = (bold && fonts::system_bold) ? fonts::system_bold : fonts::system_font;
int tw = gui::text_width(font, text, size);
int th = gui::text_height(font, size);
int x = rect.x + (rect.w - tw) / 2;
int y = rect.y + (rect.h - th) / 2;
gui::draw_text(c, font, x, y, text, color, size);
}
static void draw_text_center_fit(Canvas& c, const Rect& rect, const char* text,
Color color, int max_size, int min_size, bool bold = false) {
int size = max_size;
while (size > min_size && text_w(text, size, bold) > rect.w)
size -= 2;
draw_text_center(c, rect, text, color, size, bold);
}
static void draw_text_left(Canvas& c, int x, int y, const char* text,
Color color, int size, bool bold = false) {
TrueTypeFont* font = (bold && fonts::system_bold) ? fonts::system_bold : fonts::system_font;
gui::draw_text(c, font, x, y, text, color, size);
}
static Color tile_color(uint32_t value, const mtk::Theme& theme) {
switch (value) {
case 0: return mtk::mix(theme.surface, theme.accent, 10);
case 2: return Color::from_rgb(0xE7, 0xF0, 0xF7);
case 4: return Color::from_rgb(0xD8, 0xF0, 0xE6);
case 8: return Color::from_rgb(0xF9, 0xDE, 0x8F);
case 16: return Color::from_rgb(0xF5, 0xB3, 0x75);
case 32: return Color::from_rgb(0xF0, 0x85, 0x72);
case 64: return Color::from_rgb(0xE1, 0x5D, 0x5D);
case 128: return Color::from_rgb(0xB5, 0x7E, 0xD8);
case 256: return Color::from_rgb(0x6D, 0x9D, 0xE8);
case 512: return Color::from_rgb(0x44, 0xB7, 0xAF);
case 1024: return Color::from_rgb(0x69, 0x83, 0xD8);
case 2048: return Color::from_rgb(0x56, 0xB8, 0x76);
default: return shade(theme.accent, 18);
}
}
static Color tile_text_color(uint32_t value) {
if (value == 0) return colors::TEXT_COLOR;
if (value <= 16) return Color::from_rgb(0x32, 0x3A, 0x42);
return colors::WHITE;
}
static Rect tile_rect(const AppLayout& lo, int row, int col) {
int x = lo.board.x + lo.gap + col * (lo.tile + lo.gap);
int y = lo.board.y + lo.gap + row * (lo.tile + lo.gap);
return {x, y, lo.tile, lo.tile};
}
static void draw_score_card(Canvas& c, const Rect& rect, const char* label,
unsigned long value, const mtk::Theme& theme) {
char value_text[24];
snprintf(value_text, sizeof(value_text), "%lu", value);
draw_text_center(c, {rect.x, rect.y + 3, rect.w, 22}, label,
theme.text, SCORE_LABEL_SIZE, false);
draw_text_center_fit(c, {rect.x, rect.y + 25, rect.w, 34}, value_text,
theme.text, SCORE_VALUE_SIZE, 20, true);
}
static void draw_board(Canvas& c, const AppLayout& lo, const mtk::Theme& theme) {
Color board_bg = mtk::mix(theme.surface, theme.accent, 30);
mtk::draw_rounded_frame(c, lo.board, 8, board_bg, board_bg);
for (int row = 0; row < BOARD_N; row++) {
for (int col = 0; col < BOARD_N; col++) {
uint32_t value = g_game.cells[row][col];
Rect tile = tile_rect(lo, row, col);
Color fill = tile_color(value, theme);
c.fill_rounded_rect(tile.x, tile.y, tile.w, tile.h, 6, fill);
if (value != 0) {
char text[12];
snprintf(text, sizeof(text), "%u", (unsigned)value);
int digits = count_digits(value);
int size = lo.tile * 47 / 100;
if (digits == 4) size = lo.tile * 39 / 100;
if (digits >= 5) size = lo.tile * 33 / 100;
size = gui_clamp(size, 16, 42);
while (size > 12 && text_w(text, size, true) > tile.w - 10)
size -= 2;
draw_text_center(c, tile, text, tile_text_color(value), size, true);
}
}
}
}
static void render() {
mtk::StandaloneHost host(&g_win);
Canvas c = host.canvas();
mtk::Theme theme = app_theme();
AppLayout lo = compute_layout(g_win.width, g_win.height);
c.fill(theme.window_bg);
draw_text_left(c, lo.title.x, lo.title.y, "2048", theme.text, 34, true);
draw_score_card(c, lo.score_card, "Score", g_game.score, theme);
draw_score_card(c, lo.best_card, "Best", g_game.best, theme);
mtk::draw_button(c, lo.new_button, "New Game", mtk::BUTTON_PRIMARY,
mtk::widget_state(false, lo.new_button.contains(g_mouse_x, g_mouse_y)),
theme);
draw_board(c, lo, theme);
const char* status = "";
if (g_game.game_over) {
status = "Game over";
} else if (status_visible()) {
status = g_status;
}
if (status[0]) {
int sw = text_w(status, 14);
draw_text_left(c, (g_win.width - sw) / 2,
gui_min(lo.board.y + lo.board.h + 12, g_win.height - FOOTER_H + 8),
status, theme.text_subtle, 14, false);
}
host.present();
}
static bool has_moves() {
for (int row = 0; row < BOARD_N; row++) {
for (int col = 0; col < BOARD_N; col++) {
uint32_t v = g_game.cells[row][col];
if (v == 0) return true;
if (col + 1 < BOARD_N && g_game.cells[row][col + 1] == v) return true;
if (row + 1 < BOARD_N && g_game.cells[row + 1][col] == v) return true;
}
}
return false;
}
static bool spawn_tile() {
int empty[BOARD_N * BOARD_N];
int count = 0;
for (int row = 0; row < BOARD_N; row++) {
for (int col = 0; col < BOARD_N; col++) {
if (g_game.cells[row][col] == 0)
empty[count++] = row * BOARD_N + col;
}
}
if (count <= 0) return false;
int pick = empty[next_random() % (uint32_t)count];
uint32_t value = (next_random() % 10u == 0u) ? 4 : 2;
g_game.cells[pick / BOARD_N][pick % BOARD_N] = value;
return true;
}
static void clear_board() {
for (int row = 0; row < BOARD_N; row++) {
for (int col = 0; col < BOARD_N; col++)
g_game.cells[row][col] = 0;
}
}
static void new_game() {
clear_board();
g_game.score = 0;
g_game.game_over = false;
g_game.game_over_dialog_shown = false;
g_game.won = false;
spawn_tile();
spawn_tile();
set_status("New board");
}
static void show_game_over_dialog() {
if (!g_game.game_over || g_game.game_over_dialog_shown) return;
g_game.game_over_dialog_shown = true;
char message[128];
snprintf(message, sizeof(message),
"No moves left.\nScore: %lu\nStart a new game?",
g_game.score);
gui::dialogs::MessageBoxResult result =
gui::dialogs::message_box("2048", message, gui::dialogs::MESSAGE_BOX_YES_NO);
if (result == gui::dialogs::MESSAGE_BOX_RESULT_YES)
new_game();
}
static unsigned long slide_values(const uint32_t in[BOARD_N], uint32_t out[BOARD_N]) {
uint32_t compact[BOARD_N] = {};
int compact_count = 0;
unsigned long gained = 0;
for (int i = 0; i < BOARD_N; i++) {
if (in[i] != 0)
compact[compact_count++] = in[i];
}
int out_count = 0;
for (int i = 0; i < compact_count; i++) {
if (i + 1 < compact_count && compact[i] == compact[i + 1]) {
out[out_count] = compact[i] * 2;
gained += out[out_count];
out_count++;
i++;
} else {
out[out_count++] = compact[i];
}
}
while (out_count < BOARD_N)
out[out_count++] = 0;
return gained;
}
static bool apply_move(MoveDir dir) {
uint32_t next[BOARD_N][BOARD_N] = {};
unsigned long gained = 0;
for (int line = 0; line < BOARD_N; line++) {
uint32_t in[BOARD_N] = {};
uint32_t out[BOARD_N] = {};
for (int i = 0; i < BOARD_N; i++) {
switch (dir) {
case DIR_LEFT: in[i] = g_game.cells[line][i]; break;
case DIR_RIGHT: in[i] = g_game.cells[line][BOARD_N - 1 - i]; break;
case DIR_UP: in[i] = g_game.cells[i][line]; break;
case DIR_DOWN: in[i] = g_game.cells[BOARD_N - 1 - i][line]; break;
}
}
gained += slide_values(in, out);
for (int i = 0; i < BOARD_N; i++) {
switch (dir) {
case DIR_LEFT: next[line][i] = out[i]; break;
case DIR_RIGHT: next[line][BOARD_N - 1 - i] = out[i]; break;
case DIR_UP: next[i][line] = out[i]; break;
case DIR_DOWN: next[BOARD_N - 1 - i][line] = out[i]; break;
}
}
}
bool changed = false;
for (int row = 0; row < BOARD_N; row++) {
for (int col = 0; col < BOARD_N; col++) {
if (next[row][col] != g_game.cells[row][col])
changed = true;
}
}
if (!changed) return false;
for (int row = 0; row < BOARD_N; row++) {
for (int col = 0; col < BOARD_N; col++) {
g_game.cells[row][col] = next[row][col];
if (g_game.cells[row][col] >= 2048 && !g_game.won) {
g_game.won = true;
set_status("2048 reached");
}
}
}
g_game.score += gained;
if (g_game.score > g_game.best)
g_game.best = g_game.score;
spawn_tile();
if (!has_moves()) {
g_game.game_over = true;
set_status("Game over");
render();
show_game_over_dialog();
}
return true;
}
static bool handle_key(const Montauk::KeyEvent& key) {
if (!key.pressed) return false;
if (key.scancode == 0x01)
return false;
if (key.ascii == 'r' || key.ascii == 'R') {
new_game();
return true;
}
if (g_game.game_over)
return false;
if (key.scancode == 0x4B || key.ascii == 'a' || key.ascii == 'A')
return apply_move(DIR_LEFT);
if (key.scancode == 0x4D || key.ascii == 'd' || key.ascii == 'D')
return apply_move(DIR_RIGHT);
if (key.scancode == 0x48 || key.ascii == 'w' || key.ascii == 'W')
return apply_move(DIR_UP);
if (key.scancode == 0x50 || key.ascii == 's' || key.ascii == 'S')
return apply_move(DIR_DOWN);
return false;
}
static bool handle_mouse(const Montauk::WinEvent& ev) {
int old_x = g_mouse_x;
int old_y = g_mouse_y;
g_mouse_x = ev.mouse.x;
g_mouse_y = ev.mouse.y;
AppLayout lo = compute_layout(g_win.width, g_win.height);
bool redraw = old_x != g_mouse_x || old_y != g_mouse_y;
bool clicked = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1);
if (!clicked) return redraw;
if (lo.new_button.contains(g_mouse_x, g_mouse_y)) {
new_game();
return true;
}
return redraw;
}
extern "C" void _start() {
if (!fonts::init())
montauk::exit(1);
g_accent = mtk::load_system_accent();
seed_rng();
new_game();
if (!g_win.create("2048", INIT_W, INIT_H))
montauk::exit(1);
render();
while (true) {
Montauk::WinEvent ev;
int r = g_win.poll(&ev);
if (r < 0) break;
if (r == 0) {
montauk::sleep_ms(16);
continue;
}
bool redraw = false;
if (ev.type == 3) break;
if (ev.type == 2) {
redraw = true;
} else if (ev.type == 0) {
if (ev.key.scancode == 0x01 && ev.key.pressed)
break;
redraw = handle_key(ev.key);
} else if (ev.type == 1) {
redraw = handle_mouse(ev);
} else if (ev.type == 4) {
redraw = true;
}
if (redraw)
render();
}
g_win.destroy();
montauk::exit(0);
}
+11
View File
@@ -0,0 +1,11 @@
[app]
name = "2048"
binary = "2048.elf"
icon = "2048.svg"
[menu]
category = "Games"
visible = true
[launch]
pass_home_dir = false
+33
View File
@@ -0,0 +1,33 @@
/*
* stb_truetype_impl.cpp
* Single compilation unit for stb_truetype in MontaukOS freestanding environment
* Copyright (c) 2026 Daniel Hammer
*/
#include <cstdint>
#include <cstddef>
#include <montauk/heap.h>
#include <montauk/string.h>
#include <gui/stb_math.h>
#define STBTT_ifloor(x) ((int) stb_floor(x))
#define STBTT_iceil(x) ((int) stb_ceil(x))
#define STBTT_sqrt(x) stb_sqrt(x)
#define STBTT_pow(x,y) stb_pow(x,y)
#define STBTT_fmod(x,y) stb_fmod(x,y)
#define STBTT_cos(x) stb_cos(x)
#define STBTT_acos(x) stb_acos(x)
#define STBTT_fabs(x) stb_fabs(x)
#define STBTT_malloc(x,u) ((void)(u), montauk::malloc(x))
#define STBTT_free(x,u) ((void)(u), montauk::mfree(x))
#define STBTT_memcpy(d,s,n) montauk::memcpy(d,s,n)
#define STBTT_memset(d,v,n) montauk::memset(d,v,n)
#define STBTT_strlen(x) montauk::slen(x)
#define STBTT_assert(x) ((void)(x))
#define STB_TRUETYPE_IMPLEMENTATION
#include <gui/stb_truetype.h>