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).
This commit is contained in:
Executable
BIN
Binary file not shown.
@@ -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
|
||||
|
||||
Executable
BIN
Binary file not shown.
@@ -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"
|
||||
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
+74
@@ -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 <<EOF
|
||||
https://mirror.nura.eco/postmarketos/v26.06
|
||||
https://dl-cdn.alpinelinux.org/alpine/edge/main
|
||||
https://dl-cdn.alpinelinux.org/alpine/edge/community
|
||||
https://dl-cdn.alpinelinux.org/alpine/edge/testing
|
||||
EOF
|
||||
cp /bananarchy/keys/* /etc/apk/keys/
|
||||
|
||||
echo "[1/4] Installing host cross-compilation toolchain..."
|
||||
apk add --no-cache \
|
||||
build-base \
|
||||
gcc-armv7 \
|
||||
g++-armv7 \
|
||||
binutils-armv7 \
|
||||
pkgconf >/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
|
||||
'
|
||||
@@ -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 <X11/Xlib.h>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <ctime>
|
||||
#include <unistd.h>
|
||||
#include <sys/select.h>
|
||||
#include <sys/time.h>
|
||||
#include <X11/Xutil.h>
|
||||
#include <X11/keysym.h>
|
||||
#include <X11/Xatom.h>
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -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 <X11/Xlib.h>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <ctime>
|
||||
#include <unistd.h>
|
||||
#include <sys/select.h>
|
||||
#include <sys/time.h>
|
||||
#include <X11/Xutil.h>
|
||||
#include <X11/keysym.h>
|
||||
#include <X11/Xatom.h>
|
||||
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user