/* piano.c --- version 2026-09-29 --- public domain * * https://sedcore.eu.org/piano.c * * a small puzzle * inspired by https://sauropods.win/@futurebird/117328806582315024 * * the goal is to put the big square initially at top-left to bottom-left * (can also be whatever else you want, be free!) * * was written on a debian system, the program should work on any * posix/X11/cairo system * * how to build/run: * as root: * apt install build-essential libx11-dev libcairo2-dev * then as non-root: * gcc -Wall -o piano piano.c -lX11 -lcairo * then: * ./piano * * how to play: * move the mouse pointer around * when on a piece, use left-right-up-down keys of your keyboard to move * the piece (the piece will, obviously, move only if it can legally move) * * notes: * - ugly colors? look for cairo_set_source_rgb and change values * - cairo is overkill, sorry * - it could be possible to use click&move to get rid of keyboard use * i'm too lazy to implement this as of now * - a javascript version? no, please... behave * - why call it 'piano'? because the inspiring source has a picture * where the big square is labelled 'piano' * * (this comment is way too long) */ #include #include #include #include #include #include #include #include #define WIDTH 400 #define HEIGHT 500 char init_pos[5][4] = { { 'A', 'A', ' ', ' ' }, { 'A', 'A', 'B', 'B' }, { 'C', 'D', 'E', 'E' }, { 'F', 'G', 'H', 'H' }, { 'F', 'G', 'I', 'I' }, }; cairo_surface_t *cs; cairo_surface_t *csout; void expose(Display *d, Window w, Pixmap p) { XCopyArea(d, p, w, DefaultGC(d, DefaultScreen(d)), 0,0, WIDTH, HEIGHT, 0,0); } void paint_cube(cairo_t *c, char target, char pos[5][4], char selected) { int x_min = 10, x_max = -10; int y_min = 10, y_max = -10; /* find min/max for desired target piece */ for (int i = 0; i < 5; i++) for (int j = 0; j < 4; j++) { if (pos[i][j] != target) continue; if (i < y_min) y_min = i; if (i > y_max) y_max = i; if (j < x_min) x_min = j; if (j > x_max) x_max = j; } x_max++; y_max++; int x_top_left_screen = (x_min * WIDTH) / 4 + 5; int x_bottom_right_screen = (x_max * WIDTH) / 4 - 5; int y_top_left_screen = (y_min * HEIGHT) / 5 + 5; int y_bottom_right_screen = (y_max * HEIGHT) / 5 - 5; if (target == selected) cairo_set_source_rgb(c, 0.9, 0.9, 0.7); else cairo_set_source_rgb(c, 0.7, 0.7, 0.3); cairo_rectangle(c, x_top_left_screen, y_top_left_screen, x_bottom_right_screen - x_top_left_screen + 1, y_bottom_right_screen - y_top_left_screen + 1); cairo_fill(c); } void paint(char pos[5][4], char selected) { cairo_t *c; c=cairo_create(cs); /* clear screen */ cairo_rectangle(c, 0, 0, WIDTH, HEIGHT); cairo_set_source_rgb(c, 0.1, 0.1, 0.3); cairo_fill(c); /* draw stuff */ for (char i = 'A'; i <= 'I'; i++) paint_cube(c, i, pos, selected); cairo_destroy(c); /* and put it all to the xpixmap */ c=cairo_create(csout); cairo_set_source_surface(c, cs, 0, 0); cairo_rectangle(c, 0, 0, WIDTH, HEIGHT); cairo_paint(c); cairo_destroy(c); } char get_selected_from_mouse(char pos[5][4], int mouse_x, int mouse_y) { if (mouse_x < 0 || mouse_y < 0) return -1; if (mouse_x >= WIDTH || mouse_y >= HEIGHT) return -1; int x = mouse_x * 4 / WIDTH; int y = mouse_y * 5 / HEIGHT; if (x >= 0 && x < 4 && y >= 0 && y < 5) return pos[y][x]; return -1; } int move(char pos[5][4], int x_delta, int y_delta, char selected) { /* can we move? */ for (int y = 0; y < 5; y++) for (int x = 0; x < 4; x++) { int target = pos[y][x]; /* other objects won't move */ if (target != selected) continue; int new_x = x + x_delta; int new_y = y + y_delta; /* target can move to empty (' ') or target only */ if (new_x < 0 || new_x >= 4 || new_y < 0 || new_y >= 5) return 0; int destination = pos[new_y][new_x]; if (!(destination == selected || destination == ' ')) return 0; } /* move */ char new_pos[5][4]; memset(new_pos, ' ', 5*4); for (int y = 0; y < 5; y++) for (int x = 0; x < 4; x++) { if (pos[y][x] == ' ') continue; if (pos[y][x] != selected) { new_pos[y][x] = pos[y][x]; continue; } int new_x = x + x_delta; int new_y = y + y_delta; new_pos[new_y][new_x] = selected; } memcpy(pos, new_pos, 5*4); return 1; } int main(void) { Display *d; Window w; Pixmap p; fd_set r; int redraw; int repaint; int mouse_x = -1; int mouse_y = -1; char selected = -1; char pos[5][4]; memcpy(pos, init_pos, 5*4); cs = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, WIDTH, HEIGHT); d = XOpenDisplay(0); if (!d) abort(); w = XCreateSimpleWindow(d, DefaultRootWindow(d), 0, 0, WIDTH, HEIGHT, 0, 0, 0); XSelectInput(d, w, KeyPressMask | ExposureMask | PointerMotionMask | ButtonPressMask | EnterWindowMask | LeaveWindowMask); XMapWindow(d, w); p = XCreatePixmap(d, w, WIDTH,HEIGHT, DefaultDepth(d, DefaultScreen(d))); csout = cairo_xlib_surface_create(d, p, DefaultVisual(d, DefaultScreen(d)), WIDTH, HEIGHT); /* get a smaller cursor */ Cursor c = XCreateFontCursor(d, XC_plus); XDefineCursor(d, w, c); XFreeCursor(d, c); paint(pos, selected); redraw = 0; repaint = 0; while (1) { XEvent ev; while (XPending(d)) { XNextEvent(d, &ev); switch (ev.type) { case Expose: redraw = 1; break; case KeyPress: { char buf[65]; KeySym ks; XLookupString(&ev.xkey, buf, 64, &ks, NULL); switch (ks) { case XK_Right: if (move(pos, 1, 0, selected)) repaint = 1; break; case XK_Left: if (move(pos, -1, 0, selected)) repaint = 1; break; case XK_Up: if (move(pos, 0, -1, selected)) repaint = 1; break; case XK_Down: if (move(pos, 0, 1, selected)) repaint = 1; break; default: break; } break; } case MotionNotify: { mouse_x = ev.xmotion.x; mouse_y = ev.xmotion.y; break; } case EnterNotify: { mouse_x = ev.xcrossing.x; mouse_y = ev.xcrossing.y; break; } case LeaveNotify: { mouse_x = mouse_y = -1; break; } default: break; } } char new_selected = get_selected_from_mouse(pos, mouse_x, mouse_y); if (new_selected != selected) { selected = new_selected; repaint = 1; } if (repaint) { paint(pos, selected); repaint = 0; redraw = 1; } if (redraw) { expose(d, w, p); redraw = 0; } FD_ZERO(&r); FD_SET(ConnectionNumber(d), &r); XFlush(d); if (select(ConnectionNumber(d)+1, &r, 0, 0, 0) == -1) abort(); } XFreePixmap(d, p); XCloseDisplay(d); cairo_surface_destroy(cs); cairo_surface_destroy(csout); cairo_debug_reset_static_data(); return 0; }