Add modem/networks DWM plugins, Dual-SIM & PIN/PUK support, built-in dmenu views, and game UX fixes

This commit is contained in:
Luxferre
2026-10-24 18:59:48 +03:00
parent 668a4da11d
commit e549aaf413
63 changed files with 4781 additions and 426 deletions
+230 -142
View File
@@ -3,28 +3,32 @@
// bitmap font so it has no font-file dependency.
//
// Controls (Nokia keypad + D-pad):
// 2 / Up -> turn up 8 / Down -> turn down
// 2 / Up -> turn up 8 / Down -> turn down
// 4 / Left -> turn left 6 / Right -> turn right
// 5 / OK / Enter -> pause / restart (after game over)
// # / * / P -> pause / resume
// Back / Esc -> quit
//
// Build: g++ -O2 snake.cpp -lX11 -o snake
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/keysym.h>
#include <X11/XF86keysym.h>
#ifndef _EVDEVK
#define _EVDEVK(_v) (0x10081000 + (_v))
#endif
#include <X11/Xatom.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <cstdint>
#include <cerrno>
#include <unistd.h>
#include <sys/select.h>
#include <sys/time.h>
#include <X11/Xutil.h>
#include <X11/keysym.h>
#include <X11/Xatom.h>
#include <time.h>
#include <sys/timerfd.h>
#include <cstdint>
#include <cerrno>
static const int TOPBAR = 24;
static const int COLS = 24;
@@ -48,21 +52,22 @@ static const Glyph GLYPHS[] = {
{'G', {0b01110,0b10001,0b10000,0b10111,0b10001,0b10001,0b01111}},
{'H', {0b10001,0b10001,0b10001,0b11111,0b10001,0b10001,0b10001}},
{'I', {0b01110,0b00100,0b00100,0b00100,0b00100,0b00100,0b01110}},
{'J', {0b00010,0b00010,0b00010,0b00010,0b10010,0b10010,0b01100}},
{'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}},
{'Q', {0b01110,0b10001,0b10001,0b10001,0b10101,0b10010,0b01101}},
{'R', {0b11110,0b10001,0b10001,0b11110,0b10100,0b10010,0b10001}},
{'S', {0b01111,0b10000,0b10000,0b01110,0b00001,0b00001,0b11110}},
{'T', {0b11111,0b00100,0b00100,0b00100,0b00100,0b00100,0b00100}},
{'U', {0b10001,0b10001,0b10001,0b10001,0b10001,0b10001,0b01110}},
{'V', {0b10001,0b10001,0b10001,0b10001,0b10001,0b01010,0b00100}},
{'W', {0b10001,0b10001,0b10001,0b10101,0b10101,0b11011,0b10001}},
{'X', {0b10001,0b10001,0b01010,0b00100,0b01010,0b10001,0b10001}},
{'Y', {0b10001,0b10001,0b01010,0b00100,0b00100,0b00100,0b00100}},
{'J', {0b00010,0b00010,0b00010,0b00010,0b10010,0b10010,0b01100}},
{'Q', {0b01110,0b10001,0b10001,0b10001,0b10101,0b10010,0b01101}},
{'U', {0b10001,0b10001,0b10001,0b10001,0b10001,0b10001,0b01110}},
{'Z', {0b11111,0b00010,0b00100,0b01000,0b10000,0b10000,0b11111}},
{'0', {0b01110,0b10011,0b10101,0b10101,0b11001,0b10001,0b01110}},
{'1', {0b00100,0b01100,0b00100,0b00100,0b00100,0b00100,0b01110}},
@@ -81,11 +86,19 @@ static const Glyph GLYPHS[] = {
{'=', {0b00000,0b00000,0b11111,0b00000,0b11111,0b00000,0b00000}},
};
static const int NGLYPH = int(sizeof(GLYPHS) / sizeof(GLYPHS[0]));
static unsigned char GLYPH_LUT[128][7];
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
static void init_glyph_lut() {
memset(GLYPH_LUT, 0, sizeof(GLYPH_LUT));
for (int i = 0; i < NGLYPH; i++) {
unsigned char idx = (unsigned char)GLYPHS[i].ch;
if (idx < 128) memcpy(GLYPH_LUT[idx], GLYPHS[i].r, 7);
}
}
static inline const unsigned char* glyph(char c) {
unsigned char idx = (unsigned char)c;
return (idx < 128) ? GLYPH_LUT[idx] : GLYPH_LUT[(unsigned char)' '];
}
// ---- X11 state ----
@@ -96,6 +109,7 @@ static Pixmap back;
static int screen;
static int cell, gx0, gy0, gw, gh;
static int winW = 240, winH = 296; // overridden at runtime from the actual screen
static Atom wmDeleteAtom = None;
// ---- colors (Nokia green monochrome) ----
static unsigned long C_BG, C_FG, C_HEAD, C_FOOD, C_DIM;
@@ -114,10 +128,18 @@ static int best = 0;
static int speed = 100; // ms per step (derived from pps below)
static int pps = 90; // base speed px/sec; +1 px/sec per 5 food eaten
static bool running = true;
static unsigned long now_ms() {
static bool dirty = true;
static bool resyncTimer = false;
static int64_t lastStepNs = 0;
static int64_t now_ns() {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (unsigned long)(ts.tv_sec * 1000 + ts.tv_nsec / 1000000);
return (int64_t)ts.tv_sec * 1000000000LL + (int64_t)ts.tv_nsec;
}
static inline int64_t step_ns() {
return (int64_t)speed * 1000000LL;
}
// Derive the per-step delay (ms) from the target pixels/second. The snake
@@ -173,30 +195,33 @@ static void reset_game() {
update_speed();
place_food();
state = PLAYING;
lastStepNs = now_ns();
resyncTimer = true;
dirty = true;
}
// Queue a pending direction change; rapid key presses are stored so none are lost.
// Filters reversals and duplicates against the last queued (or current) direction.
static void tq_push(int dx, int dy) {
static bool tq_push(int dx, int dy) {
int refx = (tqn > 0) ? tqx[tqn - 1] : dirx;
int refy = (tqn > 0) ? tqy[tqn - 1] : diry;
if (dx == -refx && dy == -refy) return; // reversal
if (dx == refx && dy == refy) return; // duplicate
if (dx == -refx && dy == -refy) return false; // reversal
if (dx == refx && dy == refy) return false; // duplicate (including X11 key repeat)
if (tqn < 2) {
tqx[tqn] = dx;
tqy[tqn] = dy;
tqn++;
return true;
}
return false;
}
static void step() {
// apply queued turn (no reversing); rapid presses are not lost
if (tqn > 0) {
int dx = tqx[0], dy = tqy[0];
// shift queue
tqx[0] = tqx[1]; tqy[0] = tqy[1];
tqn--;
// apply if not a reversal
if (!(dx == -dirx && dy == -diry) && !(dx == 0 && dy == 0)) {
dirx = dx; diry = dy;
}
@@ -208,11 +233,14 @@ static void step() {
hy = (hy + ROWS) % ROWS;
// 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; }
if (snake[i][0] == hx && snake[i][1] == hy) {
state = GAMEOVER;
dirty = true;
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];
@@ -226,109 +254,159 @@ static void step() {
score += 10;
if (score > best) best = score;
int food = score / 10; // food items eaten so far
pps = 90 + food / 5; // +1 px/sec per 5 food eaten (progressive difficulty)
pps = 90 + food / 5; // +1 px/sec per 5 food eaten (progressive difficulty)
update_speed();
place_food();
}
lastStepNs = now_ns();
dirty = true;
}
// Batched text rendering: coalesces horizontal pixel runs in each glyph row
// and emits a single XFillRectangles call per string.
static void draw_text(int x, int y, int scale, unsigned long col, const char* s) {
XRectangle rects[512];
int nrects = 0;
int cx = x;
XSetForeground(dpy, gc, col);
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)))
XFillRectangle(dpy, back, gc, cx + b * scale, y + r * scale, scale, scale);
if (!row) continue;
int b = 0;
while (b < 5) {
if (row & (1 << (4 - b))) {
int start = b;
while (b < 5 && (row & (1 << (4 - b)))) b++;
if (nrects < (int)(sizeof(rects) / sizeof(rects[0]))) {
rects[nrects].x = (short)(cx + start * scale);
rects[nrects].y = (short)(y + r * scale);
rects[nrects].width = (unsigned short)((b - start) * scale);
rects[nrects].height = (unsigned short)scale;
nrects++;
}
} else {
b++;
}
}
}
cx += 6 * scale;
}
if (nrects > 0) {
XSetForeground(dpy, gc, col);
XFillRectangles(dpy, back, gc, rects, nrects);
}
}
static void render() {
XSetForeground(dpy, gc, C_BG);
XFillRectangle(dpy, back, gc, 0, 0, winW, winH);
// border framing the whole window (adapts to the actual screen size)
XSetForeground(dpy, gc, C_DIM);
XDrawRectangle(dpy, back, gc, 2, 2, winW - 4, winH - 4);
// food
if (foodx >= 0) {
XSetForeground(dpy, gc, C_FOOD);
XFillRectangle(dpy, back, gc, gx0 + foodx * cell, gy0 + foody * cell, cell, cell);
}
// snake (batched for speed)
XRectangle bodyRects[COLS * ROWS];
int nbody = 0;
for (int i = 0; i < snakelen - 1; i++) {
bodyRects[nbody].x = gx0 + snake[i][0] * cell;
bodyRects[nbody].y = gy0 + snake[i][1] * cell;
bodyRects[nbody].width = cell;
bodyRects[nbody].height = cell;
nbody++;
}
XSetForeground(dpy, gc, C_FG);
XFillRectangles(dpy, back, gc, bodyRects, nbody);
// head
XSetForeground(dpy, gc, C_HEAD);
XFillRectangle(dpy, back, gc, gx0 + snake[snakelen - 1][0] * cell, gy0 + snake[snakelen - 1][1] * cell, cell, cell);
// bright frame around the playfield so its boundaries are unmistakable
XSetForeground(dpy, gc, C_FG);
XDrawRectangle(dpy, back, gc, gx0, gy0, gw, gh);
if (state == PLAYING) {
// border framing the whole window (adapts to the actual screen size)
XSetForeground(dpy, gc, C_DIM);
XDrawRectangle(dpy, back, gc, 2, 2, winW - 4, winH - 4);
// food
if (foodx >= 0) {
XSetForeground(dpy, gc, C_FOOD);
XFillRectangle(dpy, back, gc, gx0 + foodx * cell, gy0 + foody * cell, cell, cell);
}
// snake body (batched for speed)
XRectangle bodyRects[COLS * ROWS];
int nbody = 0;
for (int i = 0; i < snakelen - 1; i++) {
bodyRects[nbody].x = (short)(gx0 + snake[i][0] * cell);
bodyRects[nbody].y = (short)(gy0 + snake[i][1] * cell);
bodyRects[nbody].width = (unsigned short)cell;
bodyRects[nbody].height = (unsigned short)cell;
nbody++;
}
if (nbody > 0) {
XSetForeground(dpy, gc, C_FG);
XFillRectangles(dpy, back, gc, bodyRects, nbody);
}
// head
XSetForeground(dpy, gc, C_HEAD);
XFillRectangle(dpy, back, gc, gx0 + snake[snakelen - 1][0] * cell,
gy0 + snake[snakelen - 1][1] * cell, cell, cell);
// bright frame around the playfield (1px outside the cell grid so row 0 / col 0 are not overlapped)
XSetForeground(dpy, gc, C_FG);
XDrawRectangle(dpy, back, gc, gx0 - 1, gy0 - 1, gw + 1, gh + 1);
// score / best sit in the margin between the window frame (outer) and the
// playfield frame (inner), vertically centred there so the playfield boundary
// stays clear
int scoreY = (2 + gy0) / 2 - 3;
char buf[32];
snprintf(buf, sizeof(buf), "SCORE %d", score);
draw_text(gx0, scoreY, 1, C_DIM, buf);
if (best > 0) {
snprintf(buf, sizeof(buf), "BEST %d", best);
draw_text(gx0 + gw - 11 * 6, scoreY, 1, C_DIM, buf);
}
if (state == PAUSED || state == GAMEOVER) {
XSetForeground(dpy, gc, C_BG);
XFillRectangle(dpy, back, gc, 0, 0, winW, winH);
}
if (state == PAUSED) {
// score / best sit in the margin between the window frame (outer) and the
// playfield frame (inner), vertically centred there
int scoreY = (2 + gy0 - 1) / 2 - 3;
char buf[32];
snprintf(buf, sizeof(buf), "SCORE %d", score);
draw_text(gx0, scoreY, 1, C_DIM, buf);
if (best > 0) {
snprintf(buf, sizeof(buf), "BEST %d", best);
int bw = (int)strlen(buf) * 6;
draw_text(gx0 + gw - bw, scoreY, 1, C_DIM, buf);
}
} else if (state == PAUSED) {
XSetForeground(dpy, gc, C_DIM);
XDrawRectangle(dpy, back, gc, 2, 2, winW - 4, winH - 4);
draw_text(winW / 2 - 5 * 6, winH / 2 - 7 * 2, 2, C_FG, "PAUSE");
draw_text(winW / 2 - 4 * 6, winH / 2 + 7 * 2, 1, C_DIM, "5 RESUME");
} else if (state == GAMEOVER) {
XSetForeground(dpy, gc, C_DIM);
XDrawRectangle(dpy, back, gc, 2, 2, winW - 4, winH - 4);
draw_text(winW / 2 - 9 * 6, winH / 2 - 14, 2, C_FG, "GAME OVER");
char buf[32];
snprintf(buf, sizeof(buf), "SCORE %d", score);
draw_text(winW / 2 - 5 * 6, winH / 2 + 4, 1, C_DIM, buf);
draw_text(winW / 2 - (int)strlen(buf) * 3, winH / 2 + 4, 1, C_DIM, buf);
draw_text(winW / 2 - 5 * 6, winH / 2 + 4 + 9, 1, C_DIM, "5 NEW GAME");
}
XCopyArea(dpy, back, win, gc, 0, 0, winW, winH, 0, 0);
XFlush(dpy);
dirty = false;
}
static void handle_turn(int dx, int dy) {
if (state != PLAYING) return;
if (tq_push(dx, dy)) {
// If this is the first queued turn and at least half a step interval has
// already elapsed since the previous step, execute the turn immediately
// and re-anchor the step timer so turns feel instantaneous (< 1ms latency)
// without allowing rapid key mashing or U-turns to jump 2 cells at once.
if (tqn == 1 && (now_ns() - lastStepNs) >= (step_ns() / 2)) {
step();
resyncTimer = true;
}
}
}
static void handle_key(KeySym ks) {
switch (ks) {
case XK_Up: case XK_w: case XK_KP_Up: case XK_2:
tq_push(0, -1); break;
case XK_Down: case XK_s: case XK_KP_Down: case XK_8: case XK_x:
tq_push(0, 1); break;
case XK_Left: case XK_a: case XK_KP_Left: case XK_4:
tq_push(-1, 0); break;
case XK_Right: case XK_d: case XK_KP_Right: case XK_6:
tq_push(1, 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; }
case XK_Up: case XK_w: case XK_KP_Up: case XK_2: case XK_KP_2:
handle_turn(0, -1);
break;
case XK_Down: case XK_s: case XK_KP_Down: case XK_8: case XK_KP_8: case XK_x:
handle_turn(0, 1);
break;
case XK_Left: case XK_a: case XK_KP_Left: case XK_4: case XK_KP_4:
handle_turn(-1, 0);
break;
case XK_Right: case XK_d: case XK_KP_Right: case XK_6: case XK_KP_6:
handle_turn(1, 0);
break;
case XK_p: case XK_numbersign: case XK_slash: case XK_KP_Divide:
case XK_asterisk: case XK_KP_Multiply:
if (state == PLAYING) { state = PAUSED; dirty = true; }
else if (state == PAUSED) { state = PLAYING; lastStepNs = now_ns(); resyncTimer = true; dirty = true; }
break;
case XK_5: case XK_KP_5: case XK_Return: case XK_KP_Enter: case XK_space:
if (state == PLAYING) { state = PAUSED; dirty = true; }
else if (state == PAUSED) { state = PLAYING; lastStepNs = now_ns(); resyncTimer = true; dirty = true; }
else if (state == GAMEOVER) reset_game();
break;
case XK_Escape: case XK_BackSpace: case XK_Delete: case XK_End:
case XK_Menu:
case XK_Menu: case XF86XK_Back: case XF86XK_KbdLcdMenu2:
running = false;
break;
default:
if (state == GAMEOVER) reset_game();
break;
}
}
@@ -349,10 +427,15 @@ static void setup_window() {
if (winW < 96) winW = 96;
if (winH < 96) winH = 96;
win = XCreateWindow(dpy, RootWindow(dpy, screen), bw, TOPBAR + bw, winW, winH, 0,
CopyFromParent, InputOutput, CopyFromParent, 0, nullptr);
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); }
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);
@@ -366,10 +449,14 @@ static void setup_window() {
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);
wmDeleteAtom = XInternAtom(dpy, "WM_DELETE_WINDOW", False);
XSetWMProtocols(dpy, win, &wmDeleteAtom, 1);
gc = XCreateGC(dpy, win, 0, nullptr);
// CRITICAL: disable GraphicsExposures so XCopyArea does not emit a NoExpose
// event on every frame (which previously woke select() in an infinite 100%
// CPU redraw storm).
XSetGraphicsExposures(dpy, gc, False);
back = XCreatePixmap(dpy, win, winW, winH, DefaultDepth(dpy, screen));
C_BG = BlackPixel(dpy, screen);
@@ -379,73 +466,74 @@ static void setup_window() {
C_FOOD = 0x66ff66;
C_DIM = 0x2f9f2f;
XSelectInput(dpy, win, KeyPressMask | StructureNotifyMask | ExposureMask);
XSelectInput(dpy, win, KeyPressMask | StructureNotifyMask | ExposureMask | FocusChangeMask);
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;
if ((Atom)e.xclient.data.l[0] == wmDeleteAtom) running = false;
} else if (e.type == ConfigureNotify) {
if (e.xconfigure.width != winW || e.xconfigure.height != winH) {
winW = e.xconfigure.width; winH = e.xconfigure.height;
winW = e.xconfigure.width;
winH = e.xconfigure.height;
if (back) XFreePixmap(dpy, back);
back = XCreatePixmap(dpy, win, winW, winH, DefaultDepth(dpy, screen));
compute_grid();
dirty = true;
}
} else if (e.type == Expose) {
render();
if (e.xexpose.count == 0) dirty = true;
} else if (e.type == FocusOut) {
if (e.xfocus.mode == NotifyNormal && state == PLAYING) {
state = PAUSED;
dirty = true;
}
} 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; }
if (ks == XK_q || ks == XK_Q) { running = false; return; }
handle_key(ks);
}
}
int main() {
srand((unsigned)time(nullptr) ^ (unsigned)getpid());
init_glyph_lut();
setup_window();
reset_game();
render();
int xfd = ConnectionNumber(dpy);
// Precise, drift-free stepping via a kernel high-resolution timer (timerfd).
// The timer fires on an absolute CLOCK_MONOTONIC deadline, so wakeups are not
// quantized to the kernel tick the way a select() timeout is. A select()
// timeout shorter than one tick gets rounded UP to a whole tick, which made
// the final few milliseconds of each wait overshoot by up to ~20ms and showed
// up as speed jitter. Input still arrives on the X connection fd, so we
// select() on BOTH fds and react to either immediately -- there is no
// busy-spin, so the X server stays free and the controls stay responsive.
int tfd = -1;
#if defined(__linux__)
tfd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC);
#endif
unsigned long nextStep = now_ms() + speed;
int64_t nextStep = now_ns() + step_ns();
State prevState = PLAYING;
resyncTimer = false;
auto arm_timer = [&](unsigned long deadline_ms) {
auto arm_timer = [&](int64_t deadline_ns) {
if (tfd < 0) return;
struct itimerspec its;
its.it_interval.tv_sec = 0; its.it_interval.tv_nsec = 0; // one-shot
its.it_value.tv_sec = (time_t)(deadline_ms / 1000);
its.it_value.tv_nsec = (long)((deadline_ms % 1000) * 1000000UL);
if (its.it_value.tv_nsec >= 1000000000L) {
its.it_value.tv_sec += 1; its.it_value.tv_nsec -= 1000000000L;
}
its.it_interval.tv_sec = 0;
its.it_interval.tv_nsec = 0;
its.it_value.tv_sec = (time_t)(deadline_ns / 1000000000LL);
its.it_value.tv_nsec = (long)(deadline_ns % 1000000000LL);
timerfd_settime(tfd, TFD_TIMER_ABSTIME, &its, nullptr);
};
auto disarm_timer = [&]() {
if (tfd < 0) return;
struct itimerspec its;
memset(&its, 0, sizeof(its));
timerfd_settime(tfd, 0, &its, nullptr);
};
if (tfd >= 0) arm_timer(nextStep);
arm_timer(nextStep);
while (running) {
fd_set fds;
@@ -455,18 +543,15 @@ int main() {
struct timeval tvbuf, *tvp = nullptr;
if (tfd >= 0) {
// While playing, also wait on the timer fd. While paused / game over the
// timer is disarmed, so we block on input only (resume stays instant).
if (state == PLAYING) {
FD_SET(tfd, &fds);
if (tfd + 1 > nfds) nfds = tfd + 1;
}
} else {
// Fallback (timerfd unavailable): bounded select so input stays live.
long sleepMs = (state == PLAYING) ? (long)nextStep - (long)now_ms() : 30;
if (sleepMs < 0) sleepMs = 0;
tvbuf.tv_sec = sleepMs / 1000;
tvbuf.tv_usec = (sleepMs % 1000) * 1000;
} else if (state == PLAYING) {
int64_t remNs = nextStep - now_ns();
if (remNs < 0) remNs = 0;
tvbuf.tv_sec = (time_t)(remNs / 1000000000LL);
tvbuf.tv_usec = (suseconds_t)((remNs % 1000000000LL) / 1000LL);
tvp = &tvbuf;
}
@@ -484,42 +569,45 @@ int main() {
if (!running) break;
}
// 2) (Re)start play cleanly: resync the clock on resume / restart / launch,
// and stop the timer whenever we leave the PLAYING state.
if (state != prevState) {
if (state == PLAYING) { nextStep = now_ms() + speed; if (tfd >= 0) arm_timer(nextStep); }
else if (tfd >= 0) disarm_timer();
// 2) Resync timer on state transitions or immediate turn steps.
if (state != prevState || resyncTimer) {
if (state == PLAYING) {
nextStep = now_ns() + step_ns();
arm_timer(nextStep);
} else {
disarm_timer();
}
prevState = state;
}
// 3) Advance the simulation exactly once per timer expiration (or, in the
// fallback path, whenever the deadline is reached). If we ever fell more
// than one interval behind (e.g. the process was frozen for a screenshot)
// we drop the backlog instead of racing to catch up.
if (state == PLAYING) {
resyncTimer = false;
} else if (state == PLAYING) {
// 3) Advance simulation on scheduled timer expiration.
bool due = false;
if (tfd >= 0) {
if (FD_ISSET(tfd, &fds)) {
uint64_t expirations = 0;
ssize_t n = read(tfd, &expirations, sizeof(expirations));
(void)n; (void)expirations;
(void)n;
due = true;
}
} else {
if ((long)(now_ms() - nextStep) >= 0) due = true;
if (now_ns() >= nextStep) due = true;
}
if (due) {
step();
nextStep += speed; // advance by exactly one interval (no drift)
unsigned long now = now_ms();
if ((long)(now - nextStep) > (long)speed) nextStep = now + speed;
if (tfd >= 0 && state == PLAYING) arm_timer(nextStep);
if (state == PLAYING) {
nextStep += step_ns();
int64_t now = now_ns();
if (now - nextStep > step_ns()) nextStep = now + step_ns();
arm_timer(nextStep);
} else {
disarm_timer();
prevState = state;
}
}
}
// 4) Repaint on every loop turn (input or step) so the display never
// appears frozen and key feedback is immediate.
render();
// 4) Repaint only when visual state actually changed.
if (dirty) render();
}
if (tfd >= 0) close(tfd);
+327 -199
View File
@@ -4,28 +4,31 @@
//
// 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
// 2 / Up -> rotate CW 8 / Down -> soft drop
// 0 / 5 / 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 <X11/Xutil.h>
#include <X11/keysym.h>
#include <X11/XF86keysym.h>
#ifndef _EVDEVK
#define _EVDEVK(_v) (0x10081000 + (_v))
#endif
#include <X11/Xatom.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <cstdint>
#include <cerrno>
#include <unistd.h>
#include <sys/select.h>
#include <sys/time.h>
#include <X11/Xutil.h>
#include <X11/keysym.h>
#include <X11/Xatom.h>
#include <time.h>
#include <sys/timerfd.h>
#include <cstdint>
#include <cerrno>
static const int TOPBAR = 24;
static const int PCOLS = 10;
@@ -44,27 +47,28 @@ static const Glyph GLYPHS[] = {
{'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,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}},
{'H', {0b10001,0b10001,0b10001,0b11111,0b10001,0b10001,0b10001}},
{'I', {0b01110,0b00100,0b00100,0b00100,0b00100,0b00100,0b01110}},
{'J', {0b00010,0b00010,0b00010,0b00010,0b10010,0b10010,0b01100}},
{'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}},
{'Q', {0b01110,0b10001,0b10001,0b10001,0b10101,0b10010,0b01101}},
{'R', {0b11110,0b10001,0b10001,0b11110,0b10100,0b10010,0b10001}},
{'S', {0b01111,0b10000,0b10000,0b01110,0b00001,0b00001,0b11110}},
{'T', {0b11111,0b00100,0b00100,0b00100,0b00100,0b00100,0b00100}},
{'U', {0b10001,0b10001,0b10001,0b10001,0b10001,0b10001,0b01110}},
{'V', {0b10001,0b10001,0b10001,0b10001,0b10001,0b01010,0b00100}},
{'W', {0b10001,0b10001,0b10001,0b10101,0b10101,0b11011,0b10001}},
{'X', {0b10001,0b10001,0b01010,0b00100,0b01010,0b10001,0b10001}},
{'Y', {0b10001,0b10001,0b01010,0b00100,0b00100,0b00100,0b00100}},
{'D', {0b11110,0b10001,0b10001,0b10001,0b10001,0b10001,0b01110}},
{'J', {0b00010,0b00010,0b00010,0b00010,0b10010,0b10010,0b01100}},
{'K', {0b10001,0b10010,0b10100,0b11000,0b10100,0b10010,0b10001}},
{'Q', {0b01110,0b10001,0b10001,0b10001,0b10101,0b10010,0b01101}},
{'U', {0b10001,0b10001,0b10001,0b10001,0b10001,0b10001,0b01110}},
{'W', {0b10001,0b10001,0b10001,0b10101,0b10101,0b11011,0b10001}},
{'Z', {0b11111,0b00010,0b00100,0b01000,0b10000,0b10000,0b11111}},
{'0', {0b01110,0b10011,0b10101,0b10101,0b11001,0b10001,0b01110}},
{'1', {0b00100,0b01100,0b00100,0b00100,0b00100,0b00100,0b01110}},
@@ -83,10 +87,19 @@ static const Glyph GLYPHS[] = {
{'=', {0b00000,0b00000,0b11111,0b00000,0b11111,0b00000,0b00000}},
};
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;
static unsigned char GLYPH_LUT[128][7];
static void init_glyph_lut() {
memset(GLYPH_LUT, 0, sizeof(GLYPH_LUT));
for (int i = 0; i < NGLYPH; i++) {
unsigned char idx = (unsigned char)GLYPHS[i].ch;
if (idx < 128) memcpy(GLYPH_LUT[idx], GLYPHS[i].r, 7);
}
}
static inline const unsigned char* glyph(char c) {
unsigned char idx = (unsigned char)c;
return (idx < 128) ? GLYPH_LUT[idx] : GLYPH_LUT[(unsigned char)' '];
}
// ---- X11 state ----
@@ -96,6 +109,7 @@ static GC gc;
static Pixmap back;
static int screen;
static int winW = 240, winH = 296; // overridden at runtime from the actual screen
static Atom wmDeleteAtom = None;
static unsigned long C_BG, C_FG, C_HEAD, C_FOOD, C_DIM;
static unsigned long C_GB_DARK, C_GB_MED, C_GB_LIGHT, C_GB_PALE;
@@ -120,6 +134,8 @@ static int score = 0, best = 0, lines = 0, level = 1;
static int speed = 600; // ms per gravity step (derived from pps below)
static int pps = 23; // base speed px/sec; +1 px/sec per 10 lines cleared
static bool running = true;
static bool dirty = true;
static bool resyncTimer = false;
static void piece_cells(int type, int rot, int out[4][2]) {
int r = ((rot % 4) + 4) % 4;
@@ -139,10 +155,14 @@ static bool collide(int px, int py, int cells[4][2]) {
return false;
}
static unsigned long now_ms() {
static int64_t now_ns() {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (unsigned long)(ts.tv_sec * 1000 + ts.tv_nsec / 1000000);
return (int64_t)ts.tv_sec * 1000000000LL + (int64_t)ts.tv_nsec;
}
static inline int64_t step_ns() {
return (int64_t)speed * 1000000LL;
}
// Derive the gravity delay (ms) from the target pixels/second. A piece drops
@@ -166,6 +186,8 @@ static void spawn() {
state = GAMEOVER;
if (score > best) best = score;
}
resyncTimer = true;
dirty = true;
}
static void lock_piece() {
@@ -174,7 +196,12 @@ static void lock_piece() {
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; }
if (cy < 0) {
state = GAMEOVER;
if (score > best) best = score;
dirty = true;
return;
}
board[cx][cy] = curType + 1;
}
// clear full lines
@@ -206,6 +233,7 @@ static void step() {
piece_cells(curType, curRot, cells);
if (!collide(curX, curY + 1, cells)) {
curY++;
dirty = true;
} else {
lock_piece();
}
@@ -223,7 +251,10 @@ static void hard_drop() {
static void try_move(int dx) {
int cells[4][2];
piece_cells(curType, curRot, cells);
if (!collide(curX + dx, curY, cells)) curX += dx;
if (!collide(curX + dx, curY, cells)) {
curX += dx;
dirty = true;
}
}
static void try_rotate() {
@@ -236,6 +267,7 @@ static void try_rotate() {
if (!collide(curX + kicks[k], curY, cells)) {
curX += kicks[k];
curRot = nr;
dirty = true;
return;
}
}
@@ -245,36 +277,81 @@ 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; update_speed();
nextType = rand() % 7;
spawn();
state = PLAYING;
spawn();
}
static void draw_text(int x, int y, int scale, unsigned long col, const char* s) {
// Append a string's coalesced glyph rectangles into a caller-supplied array.
static int append_text_rects(int x, int y, int scale, const char* s,
XRectangle* rects, int maxRects, int nrects) {
int cx = x;
XSetForeground(dpy, gc, col);
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)))
XFillRectangle(dpy, back, gc, cx + b * scale, y + r * scale, scale, scale);
if (!row) continue;
int b = 0;
while (b < 5) {
if (row & (1 << (4 - b))) {
int start = b;
while (b < 5 && (row & (1 << (4 - b)))) b++;
if (nrects < maxRects) {
rects[nrects].x = (short)(cx + start * scale);
rects[nrects].y = (short)(y + r * scale);
rects[nrects].width = (unsigned short)((b - start) * scale);
rects[nrects].height = (unsigned short)scale;
nrects++;
}
} else {
b++;
}
}
}
cx += 6 * scale;
}
return nrects;
}
// Draw a Gameboy-style beveled cell: medium body, pale top/left highlight,
// dark bottom/right shadow. bodycol is the fill shade (settled vs active).
static void draw_gb_cell(int x, int y, int size, unsigned long bodycol) {
XSetForeground(dpy, gc, bodycol);
XFillRectangle(dpy, back, gc, x + 1, y + 1, size - 2, size - 2);
XSetForeground(dpy, gc, C_GB_PALE);
XFillRectangle(dpy, back, gc, x + 1, y, size - 2, 1);
XFillRectangle(dpy, back, gc, x, y + 1, 1, size - 2);
XSetForeground(dpy, gc, C_GB_DARK);
XFillRectangle(dpy, back, gc, x + 1, y + size - 1, size - 2, 1);
XFillRectangle(dpy, back, gc, x + size - 1, y + 1, 1, size - 2);
static void draw_text(int x, int y, int scale, unsigned long col, const char* s) {
XRectangle rects[512];
int nrects = append_text_rects(x, y, scale, s, rects, 512, 0);
if (nrects > 0) {
XSetForeground(dpy, gc, col);
XFillRectangles(dpy, back, gc, rects, nrects);
}
}
static inline void append_bevel(int bx, int by, int size,
XRectangle* bodies, int& nb,
XRectangle* highlights, int& nh,
XRectangle* shadows, int& ns) {
bodies[nb].x = (short)(bx + 1);
bodies[nb].y = (short)(by + 1);
bodies[nb].width = (unsigned short)(size - 2);
bodies[nb].height = (unsigned short)(size - 2);
nb++;
highlights[nh].x = (short)(bx + 1);
highlights[nh].y = (short)by;
highlights[nh].width = (unsigned short)(size - 2);
highlights[nh].height = 1;
nh++;
highlights[nh].x = (short)bx;
highlights[nh].y = (short)(by + 1);
highlights[nh].width = 1;
highlights[nh].height = (unsigned short)(size - 2);
nh++;
shadows[ns].x = (short)(bx + 1);
shadows[ns].y = (short)(by + size - 1);
shadows[ns].width = (unsigned short)(size - 2);
shadows[ns].height = 1;
ns++;
shadows[ns].x = (short)(bx + size - 1);
shadows[ns].y = (short)(by + 1);
shadows[ns].width = 1;
shadows[ns].height = (unsigned short)(size - 2);
ns++;
}
static void render() {
@@ -282,113 +359,136 @@ static void render() {
XSetForeground(dpy, gc, C_BG);
XFillRectangle(dpy, back, gc, 0, 0, winW, winH);
// single border framing the whole game (playfield + side panel) at the
// window edges, so it is visible on all four sides including right/bottom
// Outer border framing the whole game window
XSetForeground(dpy, gc, C_DIM);
XDrawRectangle(dpy, back, gc, 2, 2, winW - 4, winH - 4);
// settled blocks (batched for speed)
XRectangle bodies[PCOLS * PROWS], highlights[PCOLS * PROWS * 2], shadows[PCOLS * PROWS * 2];
int nb = 0, nh = 0, ns = 0;
for (int x = 0; x < PCOLS; x++)
for (int y = 0; y < PROWS; y++)
if (board[x][y]) {
int bx = px0 + x * CELL, by = py0 + y * CELL;
bodies[nb].x = bx + 1; bodies[nb].y = by + 1;
bodies[nb].width = CELL - 2; bodies[nb].height = CELL - 2;
nb++;
highlights[nh].x = bx + 1; highlights[nh].y = by;
highlights[nh].width = CELL - 2; highlights[nh].height = 1;
nh++;
highlights[nh].x = bx; highlights[nh].y = by + 1;
highlights[nh].width = 1; highlights[nh].height = CELL - 2;
nh++;
shadows[ns].x = bx + 1; shadows[ns].y = by + CELL - 1;
shadows[ns].width = CELL - 2; shadows[ns].height = 1;
ns++;
shadows[ns].x = bx + CELL - 1; shadows[ns].y = by + 1;
shadows[ns].width = 1; shadows[ns].height = CELL - 2;
ns++;
}
XSetForeground(dpy, gc, C_GB_MED); XFillRectangles(dpy, back, gc, bodies, nb);
XSetForeground(dpy, gc, C_GB_PALE); XFillRectangles(dpy, back, gc, highlights, nh);
XSetForeground(dpy, gc, C_GB_DARK); XFillRectangles(dpy, back, gc, shadows, ns);
// active piece
if (state == PLAYING) {
// 1px frame around the 10x20 well so its walls/floor are unmistakable
XDrawRectangle(dpy, back, gc, px0 - 1, py0 - 1, PCOLS * CELL + 1, PROWS * CELL + 1);
// Side panel preview frame
int sx = px0 + PCOLS * CELL + 12;
int boxY = py0 + 10;
int boxH = 52;
XDrawRectangle(dpy, back, gc, sx - 2, boxY + 1, winW - sx - 3, boxH);
// Batched beveled blocks: settled board, active piece, and next-piece preview
XRectangle settledBodies[PCOLS * PROWS];
XRectangle activeBodies[4];
XRectangle previewBodies[4];
XRectangle highlights[(PCOLS * PROWS + 8) * 2];
XRectangle shadows[(PCOLS * PROWS + 8) * 2];
int nsb = 0, nab = 0, npb = 0, nh = 0, ns = 0;
for (int x = 0; x < PCOLS; x++) {
for (int y = 0; y < PROWS; y++) {
if (board[x][y]) {
append_bevel(px0 + x * CELL, py0 + y * CELL, CELL,
settledBodies, nsb, highlights, nh, shadows, ns);
}
}
}
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)
draw_gb_cell(px0 + cx * CELL, py0 + cy * CELL, CELL, C_GB_LIGHT);
if (cy >= 0) {
append_bevel(px0 + cx * CELL, py0 + cy * CELL, CELL,
activeBodies, nab, highlights, nh, shadows, ns);
}
}
}
// vertical separator line between the playfield and the side panel
int sepX = px0 + PCOLS * CELL + 6;
XSetForeground(dpy, gc, C_FG);
XDrawLine(dpy, back, gc, sepX, py0, sepX, py0 + PROWS * CELL);
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++) {
if (nc[i][0] < minx) minx = nc[i][0];
if (nc[i][1] < miny) miny = nc[i][1];
if (nc[i][0] > maxx) maxx = nc[i][0];
if (nc[i][1] > maxy) maxy = nc[i][1];
}
int pcell = 11;
int pcols = maxx - minx + 1;
int prows = maxy - miny + 1;
int nx0 = sx + ((winW - sx - 4) - pcols * pcell) / 2;
int ny0 = boxY + (boxH - prows * pcell) / 2 + 12;
for (int i = 0; i < 4; i++) {
int cx = nx0 + (nc[i][0] - minx) * pcell;
int cy = ny0 + (nc[i][1] - miny) * pcell;
append_bevel(cx, cy, pcell, previewBodies, npb, highlights, nh, shadows, ns);
}
// side panel
int sx = px0 + PCOLS * CELL + 12;
draw_text(sx, py0, 1, C_DIM, "NEXT");
// framed preview box for the next piece
int boxY = py0 + 10;
int boxH = 52;
XSetForeground(dpy, gc, C_DIM);
XDrawRectangle(dpy, back, gc, sx - 2, boxY + 1, winW - sx - 3, boxH);
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 = 11; // larger next-piece preview
int pcols = maxx - minx + 1;
int prows = maxy - miny + 1;
int nx0 = sx + ((winW - sx - 4) - pcols * pcell) / 2;
int ny0 = boxY + (boxH - prows * pcell) / 2 + 12; // sit lower in the frame
for (int i = 0; i < 4; i++) {
int cx = nx0 + (nc[i][0] - minx) * pcell;
int cy = ny0 + (nc[i][1] - miny) * pcell;
draw_gb_cell(cx, cy, pcell, C_HEAD);
}
int ty = boxY + boxH + 6;
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 (nsb > 0) {
XSetForeground(dpy, gc, C_GB_MED);
XFillRectangles(dpy, back, gc, settledBodies, nsb);
}
if (nab > 0) {
XSetForeground(dpy, gc, C_GB_LIGHT);
XFillRectangles(dpy, back, gc, activeBodies, nab);
}
if (npb > 0) {
XSetForeground(dpy, gc, C_HEAD);
XFillRectangles(dpy, back, gc, previewBodies, npb);
}
if (nh > 0) {
XSetForeground(dpy, gc, C_GB_PALE);
XFillRectangles(dpy, back, gc, highlights, nh);
}
if (ns > 0) {
XSetForeground(dpy, gc, C_GB_DARK);
XFillRectangles(dpy, back, gc, shadows, ns);
}
// concise controls in the free space below the stats
int ky = ty + 27 + 12;
draw_text(sx, ky, 1, C_DIM, "KEYS");
draw_text(sx, ky + 9, 1, C_DIM, "4/6 MOVE");
draw_text(sx, ky + 18, 1, C_DIM, "2 ROTATE");
draw_text(sx, ky + 27, 1, C_DIM, "8 SOFT");
draw_text(sx, ky + 36, 1, C_DIM, "0 DROP");
draw_text(sx, ky + 45, 1, C_DIM, "# PAUSE");
draw_text(sx, ky + 54, 1, C_DIM, "5 OK");
// Vertical separator line between playfield and side panel
int sepX = px0 + PCOLS * CELL + 6;
XSetForeground(dpy, gc, C_FG);
XDrawLine(dpy, back, gc, sepX, py0, sepX, py0 + PROWS * CELL);
if (state == PAUSED || state == GAMEOVER) {
XSetForeground(dpy, gc, C_BG);
XFillRectangle(dpy, back, gc, 0, 0, winW, winH);
}
if (state == PAUSED) {
// Batch ALL side-panel text (NEXT, stats, and controls) into a single XFillRectangles call!
XRectangle textRects[1024];
int ntr = 0;
ntr = append_text_rects(sx, py0, 1, "NEXT", textRects, 1024, ntr);
int ty = boxY + boxH + 6;
char buf[32];
snprintf(buf, sizeof(buf), "SCORE %d", score);
ntr = append_text_rects(sx, ty, 1, buf, textRects, 1024, ntr);
snprintf(buf, sizeof(buf), "LINES %d", lines);
ntr = append_text_rects(sx, ty + 9, 1, buf, textRects, 1024, ntr);
snprintf(buf, sizeof(buf), "LEVEL %d", level);
ntr = append_text_rects(sx, ty + 18, 1, buf, textRects, 1024, ntr);
if (best > 0) {
snprintf(buf, sizeof(buf), "BEST %d", best);
ntr = append_text_rects(sx, ty + 27, 1, buf, textRects, 1024, ntr);
}
int ky = ty + 27 + 12;
ntr = append_text_rects(sx, ky, 1, "KEYS", textRects, 1024, ntr);
ntr = append_text_rects(sx, ky + 9, 1, "4/6 MOVE", textRects, 1024, ntr);
ntr = append_text_rects(sx, ky + 18, 1, "2 ROTATE", textRects, 1024, ntr);
ntr = append_text_rects(sx, ky + 27, 1, "8 SOFT", textRects, 1024, ntr);
ntr = append_text_rects(sx, ky + 36, 1, "0 DROP", textRects, 1024, ntr);
ntr = append_text_rects(sx, ky + 45, 1, "# PAUSE", textRects, 1024, ntr);
ntr = append_text_rects(sx, ky + 54, 1, "5 OK", textRects, 1024, ntr);
if (ntr > 0) {
XSetForeground(dpy, gc, C_DIM);
XFillRectangles(dpy, back, gc, textRects, ntr);
}
} else if (state == PAUSED) {
draw_text(winW / 2 - 5 * 6, winH / 2 - 7, 2, C_FG, "PAUSE");
draw_text(winW / 2 - 4 * 6, winH / 2 + 10, 1, C_DIM, "5 RESUME");
} else if (state == GAMEOVER) {
draw_text(winW / 2 - 9 * 6, 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);
char buf[32];
snprintf(buf, sizeof(buf), "SCORE %d", score);
draw_text(winW / 2 - (int)strlen(buf) * 3, winH / 2 + 4, 1, C_DIM, buf);
draw_text(winW / 2 - 5 * 6, winH / 2 + 13, 1, C_DIM, "5 NEW GAME");
}
XCopyArea(dpy, back, win, gc, 0, 0, winW, winH, 0, 0);
XFlush(dpy);
dirty = false;
}
static void setup_window() {
@@ -407,10 +507,15 @@ static void setup_window() {
if (winW < 96) winW = 96;
if (winH < 96) winH = 96;
win = XCreateWindow(dpy, RootWindow(dpy, screen), bw, TOPBAR + bw, winW, winH, 0,
CopyFromParent, InputOutput, CopyFromParent, 0, nullptr);
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); }
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);
@@ -422,10 +527,14 @@ static void setup_window() {
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);
wmDeleteAtom = XInternAtom(dpy, "WM_DELETE_WINDOW", False);
XSetWMProtocols(dpy, win, &wmDeleteAtom, 1);
gc = XCreateGC(dpy, win, 0, nullptr);
// CRITICAL: disable GraphicsExposures so XCopyArea does not emit a NoExpose
// event on every frame (which previously woke select() in an infinite 100%
// CPU redraw storm).
XSetGraphicsExposures(dpy, gc, False);
back = XCreatePixmap(dpy, win, winW, winH, DefaultDepth(dpy, screen));
C_BG = BlackPixel(dpy, screen);
@@ -439,121 +548,134 @@ static void setup_window() {
C_GB_LIGHT = 0x7fdf7f; // active piece body
C_GB_PALE = 0xbfffbf; // top/left highlight
XSelectInput(dpy, win, KeyPressMask | StructureNotifyMask | ExposureMask);
XSelectInput(dpy, win, KeyPressMask | StructureNotifyMask | ExposureMask | FocusChangeMask);
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); }
case XK_Left: case XK_a: case XK_KP_Left: case XK_4: case XK_KP_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); }
case XK_Right: case XK_d: case XK_KP_Right: case XK_6: case XK_KP_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(); }
case XK_Up: case XK_w: case XK_KP_Up: case XK_2: case XK_KP_2:
if (state == PLAYING) try_rotate();
break;
case XK_Down: case XK_s: case XK_KP_Down: case XK_8: case XK_x:
case XK_Down: case XK_s: case XK_KP_Down: case XK_8: case XK_KP_8: case XK_x:
if (state == PLAYING) {
int cells[4][2];
piece_cells(curType, curRot, cells);
if (!collide(curX, curY + 1, cells)) {
curY++;
resyncTimer = true;
dirty = true;
} else {
lock_piece();
}
}
break;
case XK_space: case XK_0:
if (state == PLAYING) { hard_drop(); }
case XK_space: case XK_0: case XK_KP_0:
if (state == PLAYING) hard_drop();
else if (state == PAUSED) { state = PLAYING; resyncTimer = true; dirty = true; }
else if (state == GAMEOVER) reset_game();
break;
// Pause: P, 3, *, and the # key. NOTE: on this device the # key emits
// XK_KP_Divide (not XK_numbersign), so register KP_Divide too while the
// on-screen label still shows '#'.
case XK_p: case XK_3: case XK_numbersign: case XK_asterisk: case XK_KP_Divide:
// Pause: P, 3, *, and the # key (which emits XK_KP_Divide on nokia-argon).
case XK_p: case XK_3: case XK_KP_3:
case XK_numbersign: case XK_slash: case XK_KP_Divide:
case XK_asterisk: case XK_KP_Multiply:
if (state == PLAYING) {
state = PAUSED;
dirty = true;
} else if (state == PAUSED) {
state = PLAYING;
resyncTimer = true;
dirty = true;
}
break;
case XK_5: case XK_Return: case XK_KP_Enter:
case XK_5: case XK_KP_5: case XK_Return: case XK_KP_Enter:
if (state == PLAYING) {
hard_drop();
} else if (state == PAUSED) {
state = PLAYING;
resyncTimer = true;
dirty = true;
} 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;
case XK_Escape: case XK_BackSpace: case XK_Delete: case XK_End:
case XK_Menu: case XF86XK_Back: case XF86XK_KbdLcdMenu2:
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;
if ((Atom)e.xclient.data.l[0] == wmDeleteAtom) running = false;
} else if (e.type == ConfigureNotify) {
winW = e.xconfigure.width; winH = e.xconfigure.height;
if (e.xconfigure.width != winW || e.xconfigure.height != winH) {
winW = e.xconfigure.width;
winH = e.xconfigure.height;
if (back) XFreePixmap(dpy, back);
back = XCreatePixmap(dpy, win, winW, winH, DefaultDepth(dpy, screen));
dirty = true;
}
} else if (e.type == Expose) {
render();
if (e.xexpose.count == 0) dirty = true;
} else if (e.type == FocusOut) {
if (e.xfocus.mode == NotifyNormal && state == PLAYING) {
state = PAUSED;
dirty = true;
}
} 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; }
if (ks == XK_q || ks == XK_Q) { running = false; return; }
handle_key(ks);
}
}
int main() {
srand((unsigned)time(nullptr) ^ (unsigned)getpid());
init_glyph_lut();
setup_window();
reset_game();
render();
int xfd = ConnectionNumber(dpy);
// Precise, drift-free stepping via a kernel high-resolution timer (timerfd).
// The timer fires on an absolute CLOCK_MONOTONIC deadline, so wakeups are not
// quantized to the kernel tick the way a select() timeout is. A select()
// timeout shorter than one tick gets rounded UP to a whole tick, which made
// the final few milliseconds of each wait overshoot by up to ~20ms and showed
// up as speed jitter. Input still arrives on the X connection fd, so we
// select() on BOTH fds and react to either immediately -- there is no
// busy-spin, so the X server stays free and the controls stay responsive.
int tfd = -1;
#if defined(__linux__)
tfd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC);
#endif
unsigned long nextStep = now_ms() + speed;
int64_t nextStep = now_ns() + step_ns();
State prevState = PLAYING;
resyncTimer = false;
auto arm_timer = [&](unsigned long deadline_ms) {
auto arm_timer = [&](int64_t deadline_ns) {
if (tfd < 0) return;
struct itimerspec its;
its.it_interval.tv_sec = 0; its.it_interval.tv_nsec = 0; // one-shot
its.it_value.tv_sec = (time_t)(deadline_ms / 1000);
its.it_value.tv_nsec = (long)((deadline_ms % 1000) * 1000000UL);
if (its.it_value.tv_nsec >= 1000000000L) {
its.it_value.tv_sec += 1; its.it_value.tv_nsec -= 1000000000L;
}
its.it_interval.tv_sec = 0;
its.it_interval.tv_nsec = 0;
its.it_value.tv_sec = (time_t)(deadline_ns / 1000000000LL);
its.it_value.tv_nsec = (long)(deadline_ns % 1000000000LL);
timerfd_settime(tfd, TFD_TIMER_ABSTIME, &its, nullptr);
};
auto disarm_timer = [&]() {
if (tfd < 0) return;
struct itimerspec its;
memset(&its, 0, sizeof(its));
timerfd_settime(tfd, 0, &its, nullptr);
};
if (tfd >= 0) arm_timer(nextStep);
arm_timer(nextStep);
while (running) {
fd_set fds;
@@ -563,18 +685,15 @@ int main() {
struct timeval tvbuf, *tvp = nullptr;
if (tfd >= 0) {
// While playing, also wait on the timer fd. While paused / game over the
// timer is disarmed, so we block on input only (resume stays instant).
if (state == PLAYING) {
FD_SET(tfd, &fds);
if (tfd + 1 > nfds) nfds = tfd + 1;
}
} else {
// Fallback (timerfd unavailable): bounded select so input stays live.
long sleepMs = (state == PLAYING) ? (long)nextStep - (long)now_ms() : 30;
if (sleepMs < 0) sleepMs = 0;
tvbuf.tv_sec = sleepMs / 1000;
tvbuf.tv_usec = (sleepMs % 1000) * 1000;
} else if (state == PLAYING) {
int64_t remNs = nextStep - now_ns();
if (remNs < 0) remNs = 0;
tvbuf.tv_sec = (time_t)(remNs / 1000000000LL);
tvbuf.tv_usec = (suseconds_t)((remNs % 1000000000LL) / 1000LL);
tvp = &tvbuf;
}
@@ -592,42 +711,51 @@ int main() {
if (!running) break;
}
// 2) (Re)start play cleanly: resync the clock on resume / restart / launch,
// and stop the timer whenever we leave the PLAYING state.
if (state != prevState) {
if (state == PLAYING) { nextStep = now_ms() + speed; if (tfd >= 0) arm_timer(nextStep); }
else if (tfd >= 0) disarm_timer();
// 2) Resync timer on state transitions, soft drop, or piece spawn.
if (state != prevState || resyncTimer) {
if (state == PLAYING) {
nextStep = now_ns() + step_ns();
arm_timer(nextStep);
} else {
disarm_timer();
}
prevState = state;
}
// 3) Advance the simulation exactly once per timer expiration (or, in the
// fallback path, whenever the deadline is reached). If we ever fell more
// than one interval behind (e.g. the process was frozen for a screenshot)
// we drop the backlog instead of racing to catch up.
if (state == PLAYING) {
resyncTimer = false;
} else if (state == PLAYING) {
// 3) Advance simulation on scheduled gravity timer expiration.
bool due = false;
if (tfd >= 0) {
if (FD_ISSET(tfd, &fds)) {
uint64_t expirations = 0;
ssize_t n = read(tfd, &expirations, sizeof(expirations));
(void)n; (void)expirations;
(void)n;
due = true;
}
} else {
if ((long)(now_ms() - nextStep) >= 0) due = true;
if (now_ns() >= nextStep) due = true;
}
if (due) {
step();
nextStep += speed; // advance by exactly one interval (no drift)
unsigned long now = now_ms();
if ((long)(now - nextStep) > (long)speed) nextStep = now + speed;
if (tfd >= 0 && state == PLAYING) arm_timer(nextStep);
if (state == PLAYING) {
if (resyncTimer) {
nextStep = now_ns() + step_ns();
resyncTimer = false;
} else {
nextStep += step_ns();
int64_t now = now_ns();
if (now - nextStep > step_ns()) nextStep = now + step_ns();
}
arm_timer(nextStep);
} else {
disarm_timer();
prevState = state;
resyncTimer = false;
}
}
}
// 4) Repaint on every loop turn (input or step) so the display never
// appears frozen and key feedback is immediate.
render();
// 4) Repaint only when visual state actually changed.
if (dirty) render();
}
if (tfd >= 0) close(tfd);