Files
bananarchy/src/games/tetris.cpp
T
Luxferre 58071b374c games: fix font glyphs, Snake edge-wrap, snappier Snake
Font: add missing bitmap glyphs (/ # * + =) to both Snake and Tetris so
control labels like '4/6 MOVE' and '# PAUSE' render correctly. Also hoist
XSetForeground out of the per-pixel loop in draw_text (both games).

Snake: remove the wall-collision death; the head now wraps through the
opposite edge ((hx+COLS)%COLS, (hy+ROWS)%ROWS). Base speed raised from
70->100 px/sec (~128ms -> ~90ms per step) so it feels less laggy; the
+1 px/sec per 5 food progressive difficulty is unchanged.
2026-10-23 20:32:50 +03:00

520 lines
18 KiB
C++

// 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 TOPBAR = 24;
static const int PCOLS = 10;
static const int PROWS = 20;
static const int CELL = 14;
// ---- 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}},
{'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}},
{'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}},
{'/', {0b00001,0b00010,0b00100,0b00100,0b01000,0b01000,0b10000}},
{'#', {0b01010,0b01010,0b11111,0b01010,0b11111,0b01010,0b01010}},
{'*', {0b00100,0b01010,0b11111,0b01010,0b00100,0b00000,0b00000}},
{'+', {0b00000,0b00100,0b00100,0b11111,0b00100,0b00100,0b00000}},
{'=', {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;
}
// ---- X11 state ----
static Display* dpy = nullptr;
static Window win;
static GC gc;
static Pixmap back;
static int screen;
static int winW = 240, winH = 296; // overridden at runtime from the actual screen
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
// 7 tetrominoes, each with 4 explicit rotation states (classic SRS-style).
// type 0..6 -> I,O,T,S,Z,J,L. Coordinates are within a 4x4 box [x,y].
static const int SHAPES[7][4][4][2] = {
{ {{0,1},{1,1},{2,1},{3,1}}, {{2,0},{2,1},{2,2},{2,3}}, {{0,2},{1,2},{2,2},{3,2}}, {{1,0},{1,1},{1,2},{1,3}} }, // I
{ {{1,0},{2,0},{1,1},{2,1}}, {{1,0},{2,0},{1,1},{2,1}}, {{1,0},{2,0},{1,1},{2,1}}, {{1,0},{2,0},{1,1},{2,1}} }, // O
{ {{1,0},{0,1},{1,1},{2,1}}, {{1,0},{1,1},{2,1},{1,2}}, {{0,1},{1,1},{2,1},{1,2}}, {{1,0},{0,1},{1,1},{1,2}} }, // T
{ {{1,0},{2,0},{0,1},{1,1}}, {{1,0},{1,1},{2,1},{2,2}}, {{1,1},{2,1},{0,2},{1,2}}, {{0,0},{0,1},{1,1},{1,2}} }, // S
{ {{0,0},{1,0},{1,1},{2,1}}, {{2,0},{1,1},{2,1},{1,2}}, {{0,1},{1,1},{1,2},{2,2}}, {{1,0},{0,1},{1,1},{0,2}} }, // Z
{ {{0,0},{0,1},{1,1},{2,1}}, {{1,0},{2,0},{1,1},{1,2}}, {{0,1},{1,1},{2,1},{2,2}}, {{1,0},{1,1},{0,2},{1,2}} }, // J
{ {{2,0},{0,1},{1,1},{2,1}}, {{1,0},{1,1},{1,2},{2,2}}, {{0,1},{1,1},{2,1},{0,2}}, {{0,0},{1,0},{1,1},{1,2}} }, // 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 (derived from pps below)
static int pps = 23; // base speed px/sec; +1 px/sec per 10 lines cleared
static unsigned long nextTick = 0;
static bool running = true;
static void piece_cells(int type, int rot, int out[4][2]) {
int r = ((rot % 4) + 4) % 4;
for (int i = 0; i < 4; i++) {
out[i][0] = SHAPES[type][r][i][0];
out[i][1] = SHAPES[type][r][i][1];
}
}
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);
}
// Derive the gravity delay (ms) from the target pixels/second. A piece drops
// one cell (CELL pixels) per gravity step, so step time = CELL / pps.
static void update_speed() {
pps = 23 + lines / 10; // +1 px/sec per 10 cleared lines (progressive difficulty)
int ms = (CELL * 1000) / pps;
if (ms < 40) ms = 40; // floor on gravity so it stays playable
speed = ms;
}
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;
level = 1 + lines / 10;
int pts[5] = {0, 100, 300, 500, 800};
score += pts[cleared] * level;
if (score > best) best = score;
update_speed(); // +1 px/sec per 10 cleared lines
}
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 nr = (curRot + 1) % 4;
int cells[4][2];
piece_cells(curType, nr, cells);
// wall kicks: try in place, then small horizontal shifts
int kicks[5] = {0, -1, 1, -2, 2};
for (int k = 0; k < 5; k++) {
if (!collide(curX + kicks[k], curY, cells)) {
curX += kicks[k];
curRot = nr;
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; update_speed();
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;
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);
}
cx += 6 * scale;
}
}
static void render() {
int px0 = 8, py0 = 8;
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
XSetForeground(dpy, gc, C_DIM);
XDrawRectangle(dpy, back, gc, 2, 2, winW - 4, winH - 4);
// 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);
}
}
// 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);
// 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); // frame 2px left, 1px down, 1px wider (piece unchanged)
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 ? miny : miny;
maxx = nc[i][0] > maxx ? nc[i][0] : maxx;
maxy = nc[i][1] > maxy ? maxy : 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
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 = 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); }
// 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");
if (state == PAUSED || state == GAMEOVER) {
XSetForeground(dpy, gc, C_BG);
XFillRectangle(dpy, back, gc, 0, 0, winW, winH);
}
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);
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);
}
static void setup_window() {
dpy = XOpenDisplay(nullptr);
if (!dpy) { fprintf(stderr, "tetris: cannot open display\n"); exit(1); }
screen = DefaultScreen(dpy);
int sw = DisplayWidth(dpy, screen);
int scrH = DisplayHeight(dpy, screen);
// dwm draws a `borderpx` border around every client (src/sxmo-dwm/config.h).
// Size/position the window so that, once dwm adds its border back on each
// side, the framed window exactly meets the screen edges and the native
// border stays fully visible instead of being clipped off the right/bottom.
int bw = 1; // must match dwm borderpx
winW = sw - 3 * bw;
winH = (scrH - TOPBAR) - 3 * bw; // room for the status bar + border
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);
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 = 0x2f9f2f;
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++;
} else {
lock_piece();
}
}
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 == PLAYING) {
hard_drop();
} 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:
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);
bool dirty = false;
if (r > 0) {
XEvent e;
while (XPending(dpy)) {
XNextEvent(dpy, &e);
handle_event(e);
if (!running) break;
}
if (!running) break;
dirty = true;
}
if (running && now_ms() >= nextTick) {
if (state == PLAYING) step();
if (state == PLAYING) nextTick = now_ms() + speed;
else nextTick = now_ms() + 80;
dirty = true;
}
if (dirty) render();
}
XFreeGC(dpy, gc);
XFreePixmap(dpy, back);
XDestroyWindow(dpy, win);
XCloseDisplay(dpy);
return 0;
}