|
|
|
@@ -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);
|
|
|
|
|
}
|