From 5f467b02fa05a8ad4feae04d355f1422de72d636 Mon Sep 17 00:00:00 2001 From: Luxferre Date: Fri, 23 Oct 2026 14:29:48 +0300 Subject: [PATCH] feat(games): add Snake and Tetris X11 games with menu integration Add two standalone X11 games (not DWM plugins) for the Nokia 8110 keypad: - src/games/snake.cpp: Nokia Snake II-style game, 240x296, built-in 5x7 font - src/games/tetris.cpp: Tetris with next-piece preview, levels, line clears Both float as dialog windows in dwm, use a self-contained bitmap font (no font-file dependency), and are controlled via the numeric keypad (2/4/6/8 directions, 5 action) with arrow-key fallbacks. - scripts/build-games.sh: Docker armv7 cross-compile (mirrors build-sxmo-x11.sh) - overlay/rootfs/usr/share/sxmo/default_hooks/sxmo_hook_games.sh: Games menu hook - overlay/rootfs/usr/bin/sxmo_appmenu.sh: add 'Games' entry after 'Apps' and a games submenu case Binaries committed to prebuilt/games and overlay/rootfs/usr/bin per repo convention. Deployed and verified on the live device (window maps, dwm focuses it, keypad input reaches the game). --- overlay/rootfs/usr/bin/snake | Bin 0 -> 13672 bytes overlay/rootfs/usr/bin/sxmo_appmenu.sh | 7 +- overlay/rootfs/usr/bin/tetris | Bin 0 -> 13668 bytes .../sxmo/default_hooks/sxmo_hook_games.sh | 26 + prebuilt/games/usr/bin/snake | Bin 0 -> 13672 bytes prebuilt/games/usr/bin/tetris | Bin 0 -> 13668 bytes scripts/build-games.sh | 74 +++ src/games/snake.cpp | 376 +++++++++++++++ src/games/tetris.cpp | 454 ++++++++++++++++++ 9 files changed, 936 insertions(+), 1 deletion(-) create mode 100755 overlay/rootfs/usr/bin/snake create mode 100755 overlay/rootfs/usr/bin/tetris create mode 100644 overlay/rootfs/usr/share/sxmo/default_hooks/sxmo_hook_games.sh create mode 100755 prebuilt/games/usr/bin/snake create mode 100755 prebuilt/games/usr/bin/tetris create mode 100755 scripts/build-games.sh create mode 100644 src/games/snake.cpp create mode 100644 src/games/tetris.cpp diff --git a/overlay/rootfs/usr/bin/snake b/overlay/rootfs/usr/bin/snake new file mode 100755 index 0000000000000000000000000000000000000000..a199a732a8ac4ca139d7bf28fca32b754575fcf5 GIT binary patch literal 13672 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 literal 0 HcmV?d00001 diff --git a/overlay/rootfs/usr/bin/sxmo_appmenu.sh b/overlay/rootfs/usr/bin/sxmo_appmenu.sh index 7e07f05..c337f28 100755 --- a/overlay/rootfs/usr/bin/sxmo_appmenu.sh +++ b/overlay/rootfs/usr/bin/sxmo_appmenu.sh @@ -6,7 +6,7 @@ if [ "${1:-}" = "--fallback" ]; then shift elif [ -p /tmp/dwm_plugin_dmenu.fifo ]; then case "${1:-}" in - ""|sys|applications|scripts|modem|config|power|sthotkeys) + ""|sys|applications|scripts|modem|config|power|games|sthotkeys) exec dmenu "${1:-ctx}" ;; esac @@ -162,6 +162,7 @@ fast_sys_choices() { fi CHOICES="$icon_trm Terminal^0^st -e ${SHELL:-/bin/sh} $icon_grd Apps^0^sxmo_appmenu.sh applications +$icon_gam Games^0^sxmo_appmenu.sh games $icon_grd Scripts^0^sxmo_appmenu.sh scripts $icon_dir Files^0^xfiles $icon_phn Dialer^0^xdialer @@ -191,6 +192,10 @@ getprogchoices() { CHOICES="$(sxmo_hook_apps.sh)" fi ;; + games) + WINNAME="Games" + CHOICES="$(sxmo_hook_games.sh)" + ;; scripts) WINNAME="Scripts" if [ -s "${XDG_RUNTIME_DIR:-/tmp}/sxmo_scripts.cache" ]; then diff --git a/overlay/rootfs/usr/bin/tetris b/overlay/rootfs/usr/bin/tetris new file mode 100755 index 0000000000000000000000000000000000000000..1858184e66c4e516365576c151835d0194a17b44 GIT binary patch literal 13668 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 literal 0 HcmV?d00001 diff --git a/overlay/rootfs/usr/share/sxmo/default_hooks/sxmo_hook_games.sh b/overlay/rootfs/usr/share/sxmo/default_hooks/sxmo_hook_games.sh new file mode 100644 index 0000000..49335cc --- /dev/null +++ b/overlay/rootfs/usr/share/sxmo/default_hooks/sxmo_hook_games.sh @@ -0,0 +1,26 @@ +#!/bin/sh +# configversion: 00000000000000000000000000000000 +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright 2025 Bananarchy Contributors +# shellcheck disable=SC2154 + +# include common definitions +# shellcheck source=configs/default_hooks/sxmo_hook_icons.sh +export PATH="${HOME:-/home/user}/.local/bin:${HOME:-/home/user}/.config/sxmo/hooks:/usr/share/sxmo/default_hooks:$PATH" +. sxmo_hook_icons.sh + +write_line() { + printf "%s ^ 0 ^ %s\n" "$1" "$2" +} + +write_line_app() { + executable="$1" + label="$2" + command="$3" + if command -v "$executable" >/dev/null; then + write_line "$label" "$command" + fi +} + +write_line_app snake "$icon_gam Snake" "snake" +write_line_app tetris "$icon_gam Tetris" "tetris" diff --git a/prebuilt/games/usr/bin/snake b/prebuilt/games/usr/bin/snake new file mode 100755 index 0000000000000000000000000000000000000000..a199a732a8ac4ca139d7bf28fca32b754575fcf5 GIT binary patch literal 13672 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 literal 0 HcmV?d00001 diff --git a/prebuilt/games/usr/bin/tetris b/prebuilt/games/usr/bin/tetris new file mode 100755 index 0000000000000000000000000000000000000000..1858184e66c4e516365576c151835d0194a17b44 GIT binary patch literal 13668 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 literal 0 HcmV?d00001 diff --git a/scripts/build-games.sh b/scripts/build-games.sh new file mode 100755 index 0000000..d6d9d0a --- /dev/null +++ b/scripts/build-games.sh @@ -0,0 +1,74 @@ +#!/bin/sh +# Cross-compile Bananarchy standalone X11 games (Snake, Tetris) for armv7. +set -eu + +BASE_DIR="$(cd "$(dirname "$0")/.." && pwd)" +ROOTFS_DIR="${ROOTFS_DIR:-$BASE_DIR/rootfs}" +if [ -e "$ROOTFS_DIR" ]; then + ROOTFS_DIR="$(realpath "$ROOTFS_DIR")" +fi + +exec docker run --rm --network=host \ + -e HOST_UID="$(id -u)" \ + -e HOST_GID="$(id -g)" \ + -v "$BASE_DIR:/bananarchy" \ + -v "$ROOTFS_DIR:/target-rootfs" \ + alpine:3.24 sh -eu -c ' + cat > /etc/apk/repositories </dev/null + + echo "[2/4] Preparing armv7 sysroot (X11)..." + SYSROOT="/tmp/armv7-sysroot" + mkdir -p "$SYSROOT/etc/apk/keys" + cp /etc/apk/repositories "$SYSROOT/etc/apk/repositories" + cp /etc/apk/keys/* "$SYSROOT/etc/apk/keys/" + apk --root "$SYSROOT" --initdb --arch armv7 add --no-scripts \ + alpine-baselayout \ + musl-dev \ + linux-headers \ + libx11-dev >/dev/null + + export PKG_CONFIG_SYSROOT_DIR="$SYSROOT" + export PKG_CONFIG_LIBDIR="$SYSROOT/usr/lib/pkgconfig:$SYSROOT/usr/share/pkgconfig" + CC="armv7-alpine-linux-musleabihf-gcc --sysroot=$SYSROOT" + CXX="armv7-alpine-linux-musleabihf-g++ --sysroot=$SYSROOT" + STRIP="armv7-alpine-linux-musleabihf-strip" + OPT_CFLAGS="-O2 -march=armv7-a -mtune=cortex-a7 -mfpu=neon-vfpv4 -mfloat-abi=hard -fno-plt -fomit-frame-pointer" + + mkdir -p /bananarchy/prebuilt/games/usr/bin + + echo "[3/4] Compiling Snake and Tetris..." + $CXX $OPT_CFLAGS -std=c++17 -Wall -Wextra \ + /bananarchy/src/games/snake.cpp $(pkgconf --libs x11) \ + -o /bananarchy/prebuilt/games/usr/bin/snake + $CXX $OPT_CFLAGS -std=c++17 -Wall -Wextra \ + /bananarchy/src/games/tetris.cpp $(pkgconf --libs x11) \ + -o /bananarchy/prebuilt/games/usr/bin/tetris + $STRIP --strip-unneeded /bananarchy/prebuilt/games/usr/bin/snake /bananarchy/prebuilt/games/usr/bin/tetris + + echo "[4/4] Installing into overlay and (optional) target rootfs..." + mkdir -p /bananarchy/overlay/rootfs/usr/bin + cp -a /bananarchy/prebuilt/games/usr/bin/. /bananarchy/overlay/rootfs/usr/bin/ + if [ -d /target-rootfs/usr/bin ]; then + cp -a /bananarchy/prebuilt/games/usr/bin/. /target-rootfs/usr/bin/ + fi + + chown -R "$HOST_UID:$HOST_GID" \ + /bananarchy/prebuilt/games \ + /bananarchy/overlay/rootfs/usr/bin \ + /bananarchy/src/games + ls -lh /bananarchy/prebuilt/games/usr/bin/snake /bananarchy/prebuilt/games/usr/bin/tetris +' diff --git a/src/games/snake.cpp b/src/games/snake.cpp new file mode 100644 index 0000000..40d5d96 --- /dev/null +++ b/src/games/snake.cpp @@ -0,0 +1,376 @@ +// snake.cpp - classic Nokia-style Snake for Bananarchy (Sxmo/dwm, 240x296) +// Standalone X11 application in C++. Self-contained: uses a built-in 5x7 +// bitmap font so it has no font-file dependency. +// +// Controls (Nokia keypad + D-pad): +// 2 / Up -> turn up 8 / Down -> turn down +// 4 / Left -> turn left 6 / Right -> turn right +// 5 / OK / Enter -> pause / restart (after game over) +// Back / Esc -> quit +// +// Build: g++ -O2 snake.cpp -lX11 -o snake + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static const int WIN_W = 240; +static const int WIN_H = 296; +static const int COLS = 24; +static const int ROWS = 28; + +// ---- 5x7 bitmap font (rows are 5-bit patterns, MSB = leftmost pixel) ---- +struct Glyph { char ch; unsigned char r[7]; }; +static const Glyph GLYPHS[] = { + {' ', {0,0,0,0,0,0,0}}, + {'!', {0b00100,0b00100,0b00100,0b00100,0b00100,0,0b00100}}, + {'.', {0,0,0,0,0,0b00100,0b00100}}, + {'-', {0,0,0,0b11111,0,0,0}}, + {':', {0,0b00100,0b00100,0,0b00100,0b00100,0}}, + {'?', {0b01110,0b10001,0b00001,0b00110,0b00100,0,0b00100}}, + {'A', {0b01110,0b10001,0b10001,0b11111,0b10001,0b10001,0b10001}}, + {'B', {0b11110,0b10001,0b10001,0b11110,0b10001,0b10001,0b11110}}, + {'C', {0b01110,0b10001,0b10000,0b10000,0b10000,0b10001,0b01110}}, + {'D', {0b11110,0b10001,0b10001,0b10001,0b10001,0b10001,0b11110}}, + {'E', {0b11111,0b10000,0b10000,0b11110,0b10000,0b10000,0b11111}}, + {'F', {0b11111,0b10000,0b10000,0b11110,0b10000,0b10000,0b10000}}, + {'G', {0b01110,0b10001,0b10000,0b10111,0b10001,0b10001,0b01111}}, + {'H', {0b10001,0b10001,0b10001,0b11111,0b10001,0b10001,0b10001}}, + {'I', {0b01110,0b00100,0b00100,0b00100,0b00100,0b00100,0b01110}}, + {'K', {0b10001,0b10010,0b10100,0b11000,0b10100,0b10010,0b10001}}, + {'L', {0b10000,0b10000,0b10000,0b10000,0b10000,0b10000,0b11111}}, + {'M', {0b10001,0b11011,0b10101,0b10101,0b10001,0b10001,0b10001}}, + {'N', {0b10001,0b10001,0b11001,0b10101,0b10011,0b10001,0b10001}}, + {'O', {0b01110,0b10001,0b10001,0b10001,0b10001,0b10001,0b01110}}, + {'P', {0b11110,0b10001,0b10001,0b11110,0b10000,0b10000,0b10000}}, + {'R', {0b11110,0b10001,0b10001,0b11110,0b10100,0b10010,0b10001}}, + {'S', {0b01111,0b10000,0b10000,0b01110,0b00001,0b00001,0b11110}}, + {'T', {0b11111,0b00100,0b00100,0b00100,0b00100,0b00100,0b00100}}, + {'V', {0b10001,0b10001,0b10001,0b10001,0b10001,0b01010,0b00100}}, + {'W', {0b10001,0b10001,0b10001,0b10101,0b10101,0b11011,0b10001}}, + {'Y', {0b10001,0b10001,0b01010,0b00100,0b00100,0b00100,0b00100}}, + {'0', {0b01110,0b10011,0b10101,0b10101,0b11001,0b10001,0b01110}}, + {'1', {0b00100,0b01100,0b00100,0b00100,0b00100,0b00100,0b01110}}, + {'2', {0b01110,0b10001,0b00001,0b00110,0b01000,0b10000,0b11111}}, + {'3', {0b11110,0b00001,0b00001,0b01110,0b00001,0b00001,0b11110}}, + {'4', {0b00010,0b00110,0b01010,0b10010,0b11111,0b00010,0b00010}}, + {'5', {0b11111,0b10000,0b11110,0b00001,0b00001,0b10001,0b01110}}, + {'6', {0b00110,0b01000,0b10000,0b11110,0b10001,0b10001,0b01110}}, + {'7', {0b11111,0b00001,0b00010,0b00100,0b01000,0b01000,0b01000}}, + {'8', {0b01110,0b10001,0b10001,0b01110,0b10001,0b10001,0b01110}}, + {'9', {0b01110,0b10001,0b10001,0b01111,0b00001,0b00010,0b01100}}, +}; +static const int NGLYPH = int(sizeof(GLYPHS) / sizeof(GLYPHS[0])); + +static const unsigned char* glyph(char c) { + for (int i = 0; i < NGLYPH; i++) + if (GLYPHS[i].ch == c) return GLYPHS[i].r; + return GLYPHS[0].r; // space +} + +// ---- X11 state ---- +static Display* dpy = nullptr; +static Window win; +static GC gc; +static Pixmap back; +static int screen; +static int winW = WIN_W, winH = WIN_H; +static int cell, gx0, gy0, gw, gh; + +// ---- colors (Nokia green monochrome) ---- +static unsigned long C_BG, C_FG, C_HEAD, C_FOOD, C_DIM; + +// ---- game state ---- +enum State { PLAYING, PAUSED, GAMEOVER }; +static State state = PLAYING; +static int snake[COLS * ROWS][2]; // x,y per segment (0 = tail) +static int snakelen = 4; +static int dirx = 1, diry = 0; // current direction +static int ndirx = 1, ndiry = 0; // buffered next direction +static int foodx, foody; +static int score = 0; +static int best = 0; +static int speed = 130; // ms per step (speeds up with score) +static unsigned long nextTick = 0; +static bool running = true; +static unsigned long now_ms() { + struct timeval tv; + gettimeofday(&tv, nullptr); + return (unsigned long)(tv.tv_sec * 1000 + tv.tv_usec / 1000); +} + +static void compute_grid() { + int m = 4; + int aw = winW - 2 * m; + int ah = winH - 2 * m; + cell = aw / COLS; + if ((ah / ROWS) < cell) cell = ah / ROWS; + if (cell < 4) cell = 4; + gw = cell * COLS; + gh = cell * ROWS; + gx0 = (winW - gw) / 2; + gy0 = (winH - gh) / 2; +} + +static void place_food() { + bool occ[COLS * ROWS]; + for (int i = 0; i < COLS * ROWS; i++) occ[i] = false; + for (int i = 0; i < snakelen; i++) occ[snake[i][0] + snake[i][1] * COLS] = true; + int freecnt = 0; + for (int i = 0; i < COLS * ROWS; i++) if (!occ[i]) freecnt++; + if (freecnt == 0) { foodx = -1; foody = -1; return; } + int pick = (int)(rand() % freecnt); + int idx = 0; + for (int i = 0; i < COLS * ROWS; i++) { + if (!occ[i]) { + if (idx == pick) { foodx = i % COLS; foody = i / COLS; return; } + idx++; + } + } +} + +static void reset_game() { + snakelen = 4; + int cx = COLS / 2, cy = ROWS / 2; + for (int i = 0; i < snakelen; i++) { + snake[i][0] = cx - i; // tail to the left, head at cx + snake[i][1] = cy; + } + dirx = 1; diry = 0; + ndirx = 1; ndiry = 0; + score = 0; + speed = 130; + place_food(); + state = PLAYING; + nextTick = now_ms() + speed; +} + +static void step() { + // apply buffered direction (no reversing) + if (!(ndirx == -dirx && ndiry == -diry) && !(ndirx == 0 && ndiry == 0)) { + dirx = ndirx; diry = ndiry; + } + int hx = snake[snakelen - 1][0] + dirx; + int hy = snake[snakelen - 1][1] + diry; + if (hx < 0 || hx >= COLS || hy < 0 || hy >= ROWS) { state = GAMEOVER; return; } + // self collision (ignore current tail cell since it will move, unless growing) + for (int i = 0; i < snakelen - 1; i++) { + if (snake[i][0] == hx && snake[i][1] == hy) { state = GAMEOVER; return; } + } + bool ate = (hx == foodx && hy == foody); + if (!ate) { + // shift body toward tail + for (int i = 0; i < snakelen - 1; i++) { + snake[i][0] = snake[i + 1][0]; + snake[i][1] = snake[i + 1][1]; + } + } else { + if (snakelen < COLS * ROWS) snakelen++; + } + snake[snakelen - 1][0] = hx; + snake[snakelen - 1][1] = hy; + if (ate) { + score += 10; + if (score > best) best = score; + int ns = 130 - (score / 50) * 8; + if (ns < 60) ns = 60; + speed = ns; + place_food(); + } + nextTick = now_ms() + speed; +} + +static void draw_text(int x, int y, int scale, unsigned long col, const char* s) { + int cx = x; + for (const char* p = s; *p; p++) { + const unsigned char* g = glyph(*p); + for (int r = 0; r < 7; r++) { + unsigned char row = g[r]; + for (int b = 0; b < 5; b++) { + if (row & (1 << (4 - b))) { + XSetForeground(dpy, gc, col); + XFillRectangle(dpy, back, gc, cx + b * scale, y + r * scale, scale, scale); + } + } + } + cx += 6 * scale; + } +} + +static void render() { + XSetForeground(dpy, gc, C_BG); + XFillRectangle(dpy, back, gc, 0, 0, winW, winH); + + // playfield border + XSetForeground(dpy, gc, C_DIM); + XDrawRectangle(dpy, back, gc, gx0 - 1, gy0 - 1, gw + 2, gh + 2); + + // food + if (foodx >= 0) { + XSetForeground(dpy, gc, C_FOOD); + XFillRectangle(dpy, back, gc, gx0 + foodx * cell, gy0 + foody * cell, cell, cell); + } + // snake + for (int i = 0; i < snakelen; i++) { + XSetForeground(dpy, gc, (i == snakelen - 1) ? C_HEAD : C_FG); + XFillRectangle(dpy, back, gc, gx0 + snake[i][0] * cell, gy0 + snake[i][1] * cell, cell, cell); + } + + // score (top-left, small) + char buf[32]; + snprintf(buf, sizeof(buf), "SCORE %d", score); + draw_text(gx0, 2, 1, C_DIM, buf); + if (best > 0) { + snprintf(buf, sizeof(buf), "BEST %d", best); + draw_text(gx0 + gw - 11 * 6, 2, 1, C_DIM, buf); + } + + if (state == PAUSED) { + draw_text(winW / 2 - 3 * 6 * 2, winH / 2 - 7 * 2, 2, C_FG, "PAUSE"); + draw_text(winW / 2 - 5 * 6, winH / 2 + 7 * 2, 1, C_DIM, "5 RESUME"); + } else if (state == GAMEOVER) { + draw_text(winW / 2 - 4 * 6 * 2, winH / 2 - 14, 2, C_FG, "GAME OVER"); + snprintf(buf, sizeof(buf), "SCORE %d", score); + draw_text(winW / 2 - 5 * 6, winH / 2 + 4, 1, C_DIM, buf); + draw_text(winW / 2 - 9 * 6, winH / 2 + 4 + 9, 1, C_DIM, "5 NEW GAME"); + } + + XCopyArea(dpy, back, win, gc, 0, 0, winW, winH, 0, 0); + XFlush(dpy); +} + +static void handle_key(KeySym ks) { + switch (ks) { + case XK_Up: case XK_w: case XK_KP_Up: case XK_2: + ndirx = 0; ndiry = -1; break; + case XK_Down: case XK_s: case XK_KP_Down: case XK_8: case XK_x: + ndirx = 0; ndiry = 1; break; + case XK_Left: case XK_a: case XK_KP_Left: case XK_4: + ndirx = -1; ndiry = 0; break; + case XK_Right: case XK_d: case XK_KP_Right: case XK_6: + ndirx = 1; ndiry = 0; break; + case XK_5: case XK_Return: case XK_KP_Enter: case XK_space: case XK_p: + if (state == PLAYING) state = PAUSED; + else if (state == PAUSED) { state = PLAYING; nextTick = now_ms() + speed; } + else if (state == GAMEOVER) reset_game(); + break; + case XK_Escape: case XK_BackSpace: case XK_Delete: case XK_End: + case XK_Menu: + // quit is handled by the caller via a flag + running = false; + break; + default: + if (state == GAMEOVER) reset_game(); + break; + } +} + +static void setup_window() { + dpy = XOpenDisplay(nullptr); + if (!dpy) { fprintf(stderr, "snake: cannot open display\n"); exit(1); } + screen = DefaultScreen(dpy); + win = XCreateWindow(dpy, RootWindow(dpy, screen), 0, 0, winW, winH, 0, + CopyFromParent, InputOutput, CopyFromParent, 0, nullptr); + XStoreName(dpy, win, "Snake"); + XClassHint* ch = XAllocClassHint(); + if (ch) { ch->res_name = (char*)"snake"; ch->res_class = (char*)"snake"; XSetClassHint(dpy, win, ch); XFree(ch); } + + // float as a dialog so dwm doesn't tile it + Atom wtype = XInternAtom(dpy, "_NET_WM_WINDOW_TYPE", False); + Atom dialog = XInternAtom(dpy, "_NET_WM_WINDOW_TYPE_DIALOG", False); + XChangeProperty(dpy, win, wtype, XA_ATOM, 32, PropModeReplace, (unsigned char*)&dialog, 1); + + // fixed size hints + XSizeHints sh; + sh.flags = PMinSize | PMaxSize | PSize; + sh.min_width = sh.max_width = winW; + sh.min_height = sh.max_height = winH; + XSetWMNormalHints(dpy, win, &sh); + + Atom wm_del = XInternAtom(dpy, "WM_DELETE_WINDOW", False); + XSetWMProtocols(dpy, win, &wm_del, 1); + + gc = XCreateGC(dpy, win, 0, nullptr); + back = XCreatePixmap(dpy, win, winW, winH, DefaultDepth(dpy, screen)); + + C_BG = BlackPixel(dpy, screen); + // Nokia green monochrome + C_FG = 0x33ff33; + C_HEAD = 0x99ff99; + C_FOOD = 0x66ff66; + C_DIM = 0x0a3a0a; + + XSelectInput(dpy, win, KeyPressMask | StructureNotifyMask | ExposureMask); + XMapWindow(dpy, win); + compute_grid(); +} + +static void handle_event(XEvent& e) { + if (e.type == ClientMessage) { + Atom wm_del = XInternAtom(dpy, "WM_DELETE_WINDOW", False); + if ((Atom)e.xclient.data.l[0] == wm_del) running = false; + } else if (e.type == ConfigureNotify) { + if (e.xconfigure.width != winW || e.xconfigure.height != winH) { + winW = e.xconfigure.width; winH = e.xconfigure.height; + compute_grid(); + } + } else if (e.type == Expose) { + render(); + } else if (e.type == KeyPress) { + char buf[8]; + KeySym ks = 0; + XLookupString(&e.xkey, buf, sizeof(buf), &ks, nullptr); + if (ks == NoSymbol) ks = XLookupKeysym(&e.xkey, 0); + if (ks == XK_q) { running = false; return; } + handle_key(ks); + } +} + +int main() { + srand((unsigned)time(nullptr) ^ (unsigned)getpid()); + setup_window(); + reset_game(); + render(); + + int xfd = ConnectionNumber(dpy); + while (running) { + long until = (long)nextTick - (long)now_ms(); + if (until < 0) until = 0; + struct timeval tv; + tv.tv_sec = until / 1000; + tv.tv_usec = (until % 1000) * 1000; + fd_set fds; + FD_ZERO(&fds); + FD_SET(xfd, &fds); + int r = select(xfd + 1, &fds, nullptr, nullptr, &tv); + if (r > 0) { + XEvent e; + while (XPending(dpy)) { + XNextEvent(dpy, &e); + handle_event(e); + if (!running) break; + } + if (!running) break; + } + if (running && now_ms() >= nextTick) { + if (state == PLAYING) step(); + if (state == PLAYING) nextTick = now_ms() + speed; + else nextTick = now_ms() + 80; // keep loop responsive while paused/over + render(); + } + } + + XFreeGC(dpy, gc); + XFreePixmap(dpy, back); + XDestroyWindow(dpy, win); + XCloseDisplay(dpy); + return 0; +} diff --git a/src/games/tetris.cpp b/src/games/tetris.cpp new file mode 100644 index 0000000..51c62ab --- /dev/null +++ b/src/games/tetris.cpp @@ -0,0 +1,454 @@ +// tetris.cpp - Tetris for Bananarchy (Sxmo/dwm, 240x296) +// Standalone X11 application in C++. Self-contained: uses a built-in 5x7 +// bitmap font so it has no font-file dependency. +// +// Controls (Nokia keypad + D-pad): +// 4 / Left -> move left 6 / Right -> move right +// 2 / Up -> rotate CW 8 / Down -> soft drop +// 0 / Space -> hard drop # / * / P -> pause +// 5 / OK -> resume / new game +// Back / Esc -> quit +// +// Build: g++ -O2 tetris.cpp -lX11 -o tetris + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static const int WIN_W = 240; +static const int WIN_H = 296; +static const int PCOLS = 10; +static const int PROWS = 20; +static const int CELL = 10; + +// ---- 5x7 bitmap font ---- +struct Glyph { char ch; unsigned char r[7]; }; +static const Glyph GLYPHS[] = { + {' ', {0,0,0,0,0,0,0}}, + {'!', {0b00100,0b00100,0b00100,0b00100,0b00100,0,0b00100}}, + {'.', {0,0,0,0,0,0b00100,0b00100}}, + {'-', {0,0,0,0b11111,0,0,0}}, + {':', {0,0b00100,0b00100,0,0b00100,0b00100,0}}, + {'?', {0b01110,0b10001,0b00001,0b00110,0b00100,0,0b00100}}, + {'A', {0b01110,0b10001,0b10001,0b11111,0b10001,0b10001,0b10001}}, + {'B', {0b11110,0b10001,0b10001,0b11110,0b10001,0b10001,0b11110}}, + {'C', {0b01110,0b10001,0b10000,0b10000,0b10000,0b10001,0b01110}}, + {'E', {0b11111,0b10000,0b10000,0b11110,0b10000,0b10000,0b11111}}, + {'F', {0b11111,0b10000,0b10000,0b11110,0b10000,0b10000,0b10000}}, + {'G', {0b01110,0b10001,0b10000,0b10111,0b10001,0b10001,0b01111}}, + {'I', {0b01110,0b00100,0b00100,0b00100,0b00100,0b00100,0b01110}}, + {'L', {0b10000,0b10000,0b10000,0b10000,0b10000,0b10000,0b11111}}, + {'M', {0b10001,0b11011,0b10101,0b10101,0b10001,0b10001,0b10001}}, + {'N', {0b10001,0b10001,0b11001,0b10101,0b10011,0b10001,0b10001}}, + {'O', {0b01110,0b10001,0b10001,0b10001,0b10001,0b10001,0b01110}}, + {'P', {0b11110,0b10001,0b10001,0b11110,0b10000,0b10000,0b10000}}, + {'R', {0b11110,0b10001,0b10001,0b11110,0b10100,0b10010,0b10001}}, + {'S', {0b01111,0b10000,0b10000,0b01110,0b00001,0b00001,0b11110}}, + {'T', {0b11111,0b00100,0b00100,0b00100,0b00100,0b00100,0b00100}}, + {'V', {0b10001,0b10001,0b10001,0b10001,0b10001,0b01010,0b00100}}, + {'X', {0b10001,0b10001,0b01010,0b00100,0b01010,0b10001,0b10001}}, + {'Y', {0b10001,0b10001,0b01010,0b00100,0b00100,0b00100,0b00100}}, + {'0', {0b01110,0b10011,0b10101,0b10101,0b11001,0b10001,0b01110}}, + {'1', {0b00100,0b01100,0b00100,0b00100,0b00100,0b00100,0b01110}}, + {'2', {0b01110,0b10001,0b00001,0b00110,0b01000,0b10000,0b11111}}, + {'3', {0b11110,0b00001,0b00001,0b01110,0b00001,0b00001,0b11110}}, + {'4', {0b00010,0b00110,0b01010,0b10010,0b11111,0b00010,0b00010}}, + {'5', {0b11111,0b10000,0b11110,0b00001,0b00001,0b10001,0b01110}}, + {'6', {0b00110,0b01000,0b10000,0b11110,0b10001,0b10001,0b01110}}, + {'7', {0b11111,0b00001,0b00010,0b00100,0b01000,0b01000,0b01000}}, + {'8', {0b01110,0b10001,0b10001,0b01110,0b10001,0b10001,0b01110}}, + {'9', {0b01110,0b10001,0b10001,0b01111,0b00001,0b00010,0b01100}}, +}; +static const int NGLYPH = int(sizeof(GLYPHS) / sizeof(GLYPHS[0])); +static const unsigned char* glyph(char c) { + for (int i = 0; i < NGLYPH; i++) + if (GLYPHS[i].ch == c) return GLYPHS[i].r; + return GLYPHS[0].r; +} + +// ---- X11 state ---- +static Display* dpy = nullptr; +static Window win; +static GC gc; +static Pixmap back; +static int screen; +static int winW = WIN_W, winH = WIN_H; +static unsigned long C_BG, C_FG, C_HEAD, C_FOOD, C_DIM; + +// ---- tetromino definitions (state 0 within a 4x4 box) ---- +// type 0..6 -> I,O,T,S,Z,J,L +static const int BASE[7][4][2] = { + {{0,1},{1,1},{2,1},{3,1}}, // I + {{1,0},{2,0},{1,1},{2,1}}, // O + {{1,0},{0,1},{1,1},{2,1}}, // T + {{1,0},{2,0},{0,1},{1,1}}, // S + {{0,0},{1,0},{1,1},{2,1}}, // Z + {{0,0},{0,1},{1,1},{2,1}}, // J + {{2,0},{0,1},{1,1},{2,1}}, // L +}; + +enum State { PLAYING, PAUSED, GAMEOVER }; +static State state = PLAYING; +static int board[PCOLS][PROWS]; // 0 empty, else type+1 +static int curType = 0, curRot = 0, curX = 3, curY = 0; +static int nextType = 0; +static int score = 0, best = 0, lines = 0, level = 1; +static int speed = 600; // ms per gravity step +static unsigned long nextTick = 0; +static bool running = true; + +static void rotate_cells(const int src[4][2], int dst[4][2], int times) { + for (int i = 0; i < 4; i++) { + int x = src[i][0], y = src[i][1]; + for (int t = 0; t < times; t++) { int nx = 3 - y; int ny = x; x = nx; y = ny; } + dst[i][0] = x; dst[i][1] = y; + } +} + +static void piece_cells(int type, int rot, int out[4][2]) { + rotate_cells(BASE[type], out, ((rot % 4) + 4) % 4); +} + +static bool collide(int px, int py, int cells[4][2]) { + for (int i = 0; i < 4; i++) { + int cx = px + cells[i][0]; + int cy = py + cells[i][1]; + if (cx < 0 || cx >= PCOLS || cy >= PROWS) return true; + if (cy >= 0 && board[cx][cy]) return true; + } + return false; +} + +static unsigned long now_ms() { + struct timeval tv; + gettimeofday(&tv, nullptr); + return (unsigned long)(tv.tv_sec * 1000 + tv.tv_usec / 1000); +} + +static void spawn() { + curType = nextType; + nextType = rand() % 7; + curRot = 0; + curX = 3; + curY = 0; + int cells[4][2]; + piece_cells(curType, curRot, cells); + if (collide(curX, curY, cells)) { + state = GAMEOVER; + if (score > best) best = score; + } + nextTick = now_ms() + speed; +} + +static void lock_piece() { + int cells[4][2]; + piece_cells(curType, curRot, cells); + for (int i = 0; i < 4; i++) { + int cx = curX + cells[i][0]; + int cy = curY + cells[i][1]; + if (cy < 0) { state = GAMEOVER; if (score > best) best = score; return; } + board[cx][cy] = curType + 1; + } + // clear full lines + int cleared = 0; + for (int y = PROWS - 1; y >= 0; y--) { + bool full = true; + for (int x = 0; x < PCOLS; x++) if (!board[x][y]) { full = false; break; } + if (full) { + cleared++; + for (int yy = y; yy > 0; yy--) + for (int x = 0; x < PCOLS; x++) board[x][yy] = board[x][yy - 1]; + for (int x = 0; x < PCOLS; x++) board[x][0] = 0; + y++; // recheck same row + } + } + if (cleared > 0) { + lines += cleared; + int pts[5] = {0, 100, 300, 500, 800}; + score += pts[cleared] * level; + if (score > best) best = score; + int nl = 1 + lines / 10; + if (nl != level) { level = nl; speed = 600 - (level - 1) * 50; if (speed < 120) speed = 120; } + } + spawn(); +} + +static void step() { + int cells[4][2]; + piece_cells(curType, curRot, cells); + if (!collide(curX, curY + 1, cells)) { + curY++; + } else { + lock_piece(); + } +} + +static void hard_drop() { + int cells[4][2]; + piece_cells(curType, curRot, cells); + while (!collide(curX, curY + 1, cells)) curY++; + score += 2; + if (score > best) best = score; + lock_piece(); +} + +static void try_move(int dx) { + int cells[4][2]; + piece_cells(curType, curRot, cells); + if (!collide(curX + dx, curY, cells)) curX += dx; +} + +static void try_rotate() { + int cells[4][2]; + piece_cells(curType, (curRot + 1) % 4, cells); + // simple wall kick: try shift left/right if blocked + if (!collide(curX, curY, cells)) { curRot = (curRot + 1) % 4; return; } + if (!collide(curX - 1, curY, cells)) { curX--; curRot = (curRot + 1) % 4; return; } + if (!collide(curX + 1, curY, cells)) { curX++; curRot = (curRot + 1) % 4; return; } + if (!collide(curX - 2, curY, cells)) { curX -= 2; curRot = (curRot + 1) % 4; return; } + if (!collide(curX + 2, curY, cells)) { curX += 2; curRot = (curRot + 1) % 4; return; } +} + +static void reset_game() { + for (int x = 0; x < PCOLS; x++) for (int y = 0; y < PROWS; y++) board[x][y] = 0; + score = 0; lines = 0; level = 1; speed = 600; + nextType = rand() % 7; + spawn(); + state = PLAYING; +} + +static void draw_text(int x, int y, int scale, unsigned long col, const char* s) { + int cx = x; + for (const char* p = s; *p; p++) { + const unsigned char* g = glyph(*p); + for (int r = 0; r < 7; r++) { + unsigned char row = g[r]; + for (int b = 0; b < 5; b++) + if (row & (1 << (4 - b))) { + XSetForeground(dpy, gc, col); + XFillRectangle(dpy, back, gc, cx + b * scale, y + r * scale, scale, scale); + } + } + cx += 6 * scale; + } +} + +static void render() { + int px0 = 8, py0 = 8; + XSetForeground(dpy, gc, C_BG); + XFillRectangle(dpy, back, gc, 0, 0, winW, winH); + + // playfield border + XSetForeground(dpy, gc, C_DIM); + XDrawRectangle(dpy, back, gc, px0 - 1, py0 - 1, PCOLS * CELL + 2, PROWS * CELL + 2); + + // settled blocks + for (int x = 0; x < PCOLS; x++) + for (int y = 0; y < PROWS; y++) + if (board[x][y]) { + XSetForeground(dpy, gc, C_FG); + XFillRectangle(dpy, back, gc, px0 + x * CELL, py0 + y * CELL, CELL, CELL); + } + + // active piece + if (state == PLAYING) { + int cells[4][2]; + piece_cells(curType, curRot, cells); + XSetForeground(dpy, gc, C_HEAD); + for (int i = 0; i < 4; i++) { + int cx = curX + cells[i][0]; + int cy = curY + cells[i][1]; + if (cy >= 0) + XFillRectangle(dpy, back, gc, px0 + cx * CELL, py0 + cy * CELL, CELL, CELL); + } + } + + // side panel + int sx = px0 + PCOLS * CELL + 12; + draw_text(sx, py0, 1, C_DIM, "NEXT"); + int nc[4][2]; + piece_cells(nextType, 0, nc); + int minx = 4, miny = 4, maxx = -1, maxy = -1; + for (int i = 0; i < 4; i++) { + minx = nc[i][0] < minx ? nc[i][0] : minx; + miny = nc[i][1] < miny ? nc[i][1] : miny; + maxx = nc[i][0] > maxx ? nc[i][0] : maxx; + maxy = nc[i][1] > maxy ? nc[i][1] : maxy; + } + int pcell = 6; + int nx0 = sx + 2, ny0 = py0 + 10; + XSetForeground(dpy, gc, C_HEAD); + for (int i = 0; i < 4; i++) { + int cx = nx0 + (nc[i][0] - minx) * pcell; + int cy = ny0 + (nc[i][1] - miny) * pcell; + XFillRectangle(dpy, back, gc, cx, cy, pcell, pcell); + } + int ty = py0 + 60; + char buf[32]; + snprintf(buf, sizeof(buf), "SCORE %d", score); draw_text(sx, ty, 1, C_DIM, buf); + snprintf(buf, sizeof(buf), "LINES %d", lines); draw_text(sx, ty + 9, 1, C_DIM, buf); + snprintf(buf, sizeof(buf), "LEVEL %d", level); draw_text(sx, ty + 18, 1, C_DIM, buf); + if (best > 0) { snprintf(buf, sizeof(buf), "BEST %d", best); draw_text(sx, ty + 27, 1, C_DIM, buf); } + + if (state == PAUSED) { + draw_text(winW / 2 - 3 * 6 * 2, winH / 2 - 7, 2, C_FG, "PAUSE"); + draw_text(winW / 2 - 5 * 6, winH / 2 + 10, 1, C_DIM, "5 RESUME"); + } else if (state == GAMEOVER) { + draw_text(winW / 2 - 5 * 6 * 2, winH / 2 - 14, 2, C_FG, "GAME OVER"); + snprintf(buf, sizeof(buf), "SCORE %d", score); draw_text(winW / 2 - 5 * 6, winH / 2 + 4, 1, C_DIM, buf); + draw_text(winW / 2 - 9 * 6, winH / 2 + 13, 1, C_DIM, "5 NEW GAME"); + } + + XCopyArea(dpy, back, win, gc, 0, 0, winW, winH, 0, 0); + XFlush(dpy); +} + +static void setup_window() { + dpy = XOpenDisplay(nullptr); + if (!dpy) { fprintf(stderr, "tetris: cannot open display\n"); exit(1); } + screen = DefaultScreen(dpy); + win = XCreateWindow(dpy, RootWindow(dpy, screen), 0, 0, winW, winH, 0, + CopyFromParent, InputOutput, CopyFromParent, 0, nullptr); + XStoreName(dpy, win, "Tetris"); + XClassHint* ch = XAllocClassHint(); + if (ch) { ch->res_name = (char*)"tetris"; ch->res_class = (char*)"tetris"; XSetClassHint(dpy, win, ch); XFree(ch); } + + Atom wtype = XInternAtom(dpy, "_NET_WM_WINDOW_TYPE", False); + Atom dialog = XInternAtom(dpy, "_NET_WM_WINDOW_TYPE_DIALOG", False); + XChangeProperty(dpy, win, wtype, XA_ATOM, 32, PropModeReplace, (unsigned char*)&dialog, 1); + + XSizeHints sh; + sh.flags = PMinSize | PMaxSize | PSize; + sh.min_width = sh.max_width = winW; + sh.min_height = sh.max_height = winH; + XSetWMNormalHints(dpy, win, &sh); + + Atom wm_del = XInternAtom(dpy, "WM_DELETE_WINDOW", False); + XSetWMProtocols(dpy, win, &wm_del, 1); + + gc = XCreateGC(dpy, win, 0, nullptr); + back = XCreatePixmap(dpy, win, winW, winH, DefaultDepth(dpy, screen)); + + C_BG = BlackPixel(dpy, screen); + C_FG = 0x33ff33; + C_HEAD = 0x99ff99; + C_FOOD = 0x66ff66; + C_DIM = 0x0a3a0a; + + XSelectInput(dpy, win, KeyPressMask | StructureNotifyMask | ExposureMask); + XMapWindow(dpy, win); +} + +static void handle_key(KeySym ks) { + switch (ks) { + case XK_Left: case XK_a: case XK_KP_Left: case XK_4: + if (state == PLAYING) { try_move(-1); } + break; + case XK_Right: case XK_d: case XK_KP_Right: case XK_6: + if (state == PLAYING) { try_move(1); } + break; + case XK_Up: case XK_w: case XK_KP_Up: case XK_2: + if (state == PLAYING) { try_rotate(); } + break; + case XK_Down: case XK_s: case XK_KP_Down: case XK_8: case XK_x: + if (state == PLAYING) { + int cells[4][2]; + piece_cells(curType, curRot, cells); + if (!collide(curX, curY + 1, cells)) { + curY++; score += 1; if (score > best) best = score; + } else { + lock_piece(); + } + nextTick = now_ms() + speed; + } + break; + case XK_space: case XK_0: + if (state == PLAYING) { hard_drop(); } + break; + case XK_p: case XK_3: case XK_numbersign: case XK_asterisk: + if (state == PLAYING) { + state = PAUSED; + } else if (state == PAUSED) { + state = PLAYING; nextTick = now_ms() + speed; + } + break; + case XK_5: case XK_Return: case XK_KP_Enter: + if (state == PAUSED) { + state = PLAYING; nextTick = now_ms() + speed; + } else if (state == GAMEOVER) { + reset_game(); + } + break; + case XK_Escape: case XK_BackSpace: case XK_Delete: case XK_End: case XK_Menu: + running = false; break; + default: + if (state == GAMEOVER) { reset_game(); } + break; + } +} + +static void handle_event(XEvent& e) { + if (e.type == ClientMessage) { + Atom wm_del = XInternAtom(dpy, "WM_DELETE_WINDOW", False); + if ((Atom)e.xclient.data.l[0] == wm_del) running = false; + } else if (e.type == ConfigureNotify) { + winW = e.xconfigure.width; winH = e.xconfigure.height; + } else if (e.type == Expose) { + render(); + } else if (e.type == KeyPress) { + char buf[8]; + KeySym ks = 0; + XLookupString(&e.xkey, buf, sizeof(buf), &ks, nullptr); + if (ks == NoSymbol) ks = XLookupKeysym(&e.xkey, 0); + if (ks == XK_q) { running = false; return; } + handle_key(ks); + } +} + +int main() { + srand((unsigned)time(nullptr) ^ (unsigned)getpid()); + setup_window(); + reset_game(); + render(); + + int xfd = ConnectionNumber(dpy); + while (running) { + long until = (long)nextTick - (long)now_ms(); + if (until < 0) until = 0; + struct timeval tv; + tv.tv_sec = until / 1000; + tv.tv_usec = (until % 1000) * 1000; + fd_set fds; + FD_ZERO(&fds); + FD_SET(xfd, &fds); + int r = select(xfd + 1, &fds, nullptr, nullptr, &tv); + if (r > 0) { + XEvent e; + while (XPending(dpy)) { + XNextEvent(dpy, &e); + handle_event(e); + if (!running) break; + } + if (!running) break; + } + if (running && now_ms() >= nextTick) { + if (state == PLAYING) step(); + else nextTick = now_ms() + 80; + render(); + } + } + + XFreeGC(dpy, gc); + XFreePixmap(dpy, back); + XDestroyWindow(dpy, win); + XCloseDisplay(dpy); + return 0; +}