feat: add 2048 game
This commit is contained in:
@@ -59,7 +59,7 @@ BINDIR := bin
|
|||||||
PROGRAMS := $(notdir $(wildcard src/*))
|
PROGRAMS := $(notdir $(wildcard src/*))
|
||||||
|
|
||||||
# Programs with custom Makefiles (built separately).
|
# Programs with custom Makefiles (built separately).
|
||||||
CUSTOM_BUILDS := doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer audio music video bluetooth network terminal klog procmgr calculator desktop login shell rpgdemo paint tcc lua screenshot texteditor mandelbrot printers timezone printd printctl dialogs test_dialogs test_dl libloader libhello crashpad
|
CUSTOM_BUILDS := 2048 doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer audio music video bluetooth network terminal klog procmgr calculator desktop login shell rpgdemo paint tcc lua screenshot texteditor mandelbrot printers timezone printd printctl dialogs test_dialogs test_dl libloader libhello crashpad
|
||||||
SYSTEM_PROGRAMS := $(filter-out $(CUSTOM_BUILDS),$(PROGRAMS))
|
SYSTEM_PROGRAMS := $(filter-out $(CUSTOM_BUILDS),$(PROGRAMS))
|
||||||
|
|
||||||
# Build targets: system programs go to bin/os/, apps go to bin/apps/<name>/.
|
# Build targets: system programs go to bin/os/, apps go to bin/apps/<name>/.
|
||||||
@@ -86,9 +86,9 @@ CONFIGDST := $(patsubst $(CONFIGDIR)/%,$(BINDIR)/config/%,$(CONFIGSRC))
|
|||||||
# Common shared assets (wallpapers, etc.)
|
# Common shared assets (wallpapers, etc.)
|
||||||
COMMONKEEP := $(BINDIR)/common/.keep
|
COMMONKEEP := $(BINDIR)/common/.keep
|
||||||
|
|
||||||
.PHONY: all clean doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer audio music video bluetooth network terminal klog procmgr calculator login desktop shell rpgdemo paint tcc lua screenshot texteditor mandelbrot printers timezone printd printctl dialogs icons fonts configs bearssl libc tls libjpeg libjpegwrite install-apps libloader libs libhello test_dialogs test_dl crashpad
|
.PHONY: all clean 2048 doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer audio music video bluetooth network terminal klog procmgr calculator login desktop shell rpgdemo paint tcc lua screenshot texteditor mandelbrot printers timezone printd printctl dialogs icons fonts configs bearssl libc tls libjpeg libjpegwrite install-apps libloader libs libhello test_dialogs test_dl crashpad
|
||||||
|
|
||||||
all: bearssl libc libjpeg libjpegwrite tls libloader libs libhello $(TARGETS) fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer audio music video bluetooth network terminal klog procmgr calculator doom rpgdemo paint tcc lua screenshot texteditor mandelbrot printers timezone printd printctl dialogs login desktop shell icons fonts install-apps test_dialogs test_dl crashpad $(MANDST) $(WWWDST) $(CA_CERTS) $(CONFIGDST) $(COMMONKEEP)
|
all: bearssl libc libjpeg libjpegwrite tls libloader libs libhello $(TARGETS) fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer audio music video bluetooth network terminal klog procmgr calculator 2048 doom rpgdemo paint tcc lua screenshot texteditor mandelbrot printers timezone printd printctl dialogs login desktop shell icons fonts install-apps test_dialogs test_dl crashpad $(MANDST) $(WWWDST) $(CA_CERTS) $(CONFIGDST) $(COMMONKEEP)
|
||||||
|
|
||||||
# Build BearSSL static library (cross-compiled for freestanding x86_64).
|
# Build BearSSL static library (cross-compiled for freestanding x86_64).
|
||||||
BEARSSL_INCLUDES := -isystem $(shell cd .. && pwd)/kernel/freestnd-c-hdrs/x86_64/include -isystem $(abspath include/libc)
|
BEARSSL_INCLUDES := -isystem $(shell cd .. && pwd)/kernel/freestnd-c-hdrs/x86_64/include -isystem $(abspath include/libc)
|
||||||
@@ -259,6 +259,10 @@ texteditor: libc libloader dialogs
|
|||||||
mandelbrot: libc
|
mandelbrot: libc
|
||||||
$(MAKE) -C src/mandelbrot
|
$(MAKE) -C src/mandelbrot
|
||||||
|
|
||||||
|
# Build 2048 standalone MTK game (depends on libc, libloader, and libdialogs).
|
||||||
|
2048: libc libloader dialogs
|
||||||
|
$(MAKE) -C src/2048
|
||||||
|
|
||||||
# Build RPG demo game via its own Makefile (depends on libc).
|
# Build RPG demo game via its own Makefile (depends on libc).
|
||||||
rpgdemo: libc
|
rpgdemo: libc
|
||||||
$(MAKE) -C src/rpgdemo
|
$(MAKE) -C src/rpgdemo
|
||||||
@@ -309,7 +313,7 @@ test_dialogs: libc libloader dialogs
|
|||||||
$(MAKE) -C src/test_dialogs
|
$(MAKE) -C src/test_dialogs
|
||||||
|
|
||||||
# Install app bundles (manifests, icons, data files) into bin/apps/<name>/.
|
# Install app bundles (manifests, icons, data files) into bin/apps/<name>/.
|
||||||
install-apps: doom rpgdemo paint spreadsheet wordprocessor weather wikipedia imageviewer fontpreview pdfviewer disks devexplorer installer audio music video bluetooth network terminal klog procmgr calculator screenshot texteditor mandelbrot printers timezone crashpad
|
install-apps: 2048 doom rpgdemo paint spreadsheet wordprocessor weather wikipedia imageviewer fontpreview pdfviewer disks devexplorer installer audio music video bluetooth network terminal klog procmgr calculator screenshot texteditor mandelbrot printers timezone crashpad
|
||||||
../scripts/install_apps.sh
|
../scripts/install_apps.sh
|
||||||
|
|
||||||
# Copy man pages into bin/man/ so mkramdisk.sh picks them up.
|
# Copy man pages into bin/man/ so mkramdisk.sh picks them up.
|
||||||
@@ -380,6 +384,7 @@ clean:
|
|||||||
$(MAKE) -C src/rpgdemo clean
|
$(MAKE) -C src/rpgdemo clean
|
||||||
$(MAKE) -C src/screenshot clean
|
$(MAKE) -C src/screenshot clean
|
||||||
$(MAKE) -C src/mandelbrot clean
|
$(MAKE) -C src/mandelbrot clean
|
||||||
|
$(MAKE) -C src/2048 clean
|
||||||
$(MAKE) -C src/tcc clean
|
$(MAKE) -C src/tcc clean
|
||||||
$(MAKE) -C src/printd clean
|
$(MAKE) -C src/printd clean
|
||||||
$(MAKE) -C src/printctl clean
|
$(MAKE) -C src/printctl clean
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[app]
|
||||||
|
name = "2048"
|
||||||
|
binary = "2048.elf"
|
||||||
|
icon = "2048.svg"
|
||||||
|
|
||||||
|
[menu]
|
||||||
|
category = "Games"
|
||||||
|
visible = true
|
||||||
|
|
||||||
|
[launch]
|
||||||
|
pass_home_dir = false
|
||||||
@@ -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>
|
||||||
@@ -15,6 +15,7 @@ ICON_SRC="$PROJECT_ROOT/programs/gui/icons/Flat-Remix-Blue-Light-darkPanel"
|
|||||||
# App definitions: name|icon_source_path
|
# App definitions: name|icon_source_path
|
||||||
# The binary is already built by each app's Makefile into bin/apps/<name>/
|
# The binary is already built by each app's Makefile into bin/apps/<name>/
|
||||||
APPS=(
|
APPS=(
|
||||||
|
"2048|categories/scalable/2048.svg"
|
||||||
"doom|apps/scalable/doom.svg"
|
"doom|apps/scalable/doom.svg"
|
||||||
"spreadsheet|mimetypes/scalable/spreadsheet.svg"
|
"spreadsheet|mimetypes/scalable/spreadsheet.svg"
|
||||||
"wordprocessor|apps/scalable/libreoffice6.0-writer.svg"
|
"wordprocessor|apps/scalable/libreoffice6.0-writer.svg"
|
||||||
|
|||||||
Reference in New Issue
Block a user