// tetris.cpp - Tetris for Bananarchy (Sxmo/dwm, 240x296) // Standalone X11 application in C++. Self-contained: uses a built-in 5x7 // bitmap font so it has no font-file dependency. // // Controls (Nokia keypad + D-pad): // 4 / Left -> move left 6 / Right -> move right // 2 / Up -> rotate CW 8 / Down -> soft drop // 0 / Space -> hard drop # / * / P -> pause // 5 / OK -> resume / new game // Back / Esc -> quit // // Build: g++ -O2 tetris.cpp -lX11 -o tetris #include #include #include #include #include #include #include #include #include #include #include static const int 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; }