// Space Invaders for X11 // build: gcc -O2 -Wall -o invaders invaders.c -lX11 // controls: Left/Right or A/D move, Up/W/Space fire, P pause, Q/Esc quit, R/Enter restart #include #include #include #include #include #include #include #define W 640 #define H 480 #define STEP_DT (1.0 / 60.0) #define ROWS 5 #define COLS 10 #define GAPX 40.0f #define GAPY 34.0f #define SCALE 2 #define A_STEP 5.0f #define A_DROP 8.0f #define B_SPD_P 460.0f #define B_SPD_E 95.0f #define P_SPEED 35.0f #define FIELD_X 150.0f #define FIELD_Y 70.0f #define SHIP_TOP (H - 48) #define SHIP_W 9 #define SHIP_H 6 enum { ST_TITLE, ST_READY, ST_PLAY, ST_DEAD, ST_OVER }; enum { ACT_NONE, ACT_LEFT, ACT_RIGHT, ACT_FIRE, ACT_PAUSE, ACT_QUIT, ACT_RESTART }; // alien sprite frames: [frame][type][row] // type 0=squid(7w/7h) 1=crab(11w/8h) 2=octopus(12w/8h) static const char *SPR[2][3][8] = { { { "...X...", "..XXX..", ".XXXXX.", "XX.X.XX", "XXXXXXX", "..X.X..", ".X...X.", "" }, { "..X.....X..", "...X...X...", "..XXXXXXX..", ".XX.XXX.XX.", "XXXXXXXXXXX", "X.XXXXXXX.X", "X.X.....X.X", "..XX...XX.." }, { "..XXXXXXXX..", ".XXXXXXXXXX.", "XXXXXXXXXXXX", "XX.XXXXXX.XX", ".XXXXXXXXXX.", "..XX.XX.XX..", "..X..XX..X..", "" }, }, { { "...X...", "..XXX..", ".XXXXX.", "XX.X.XX", "XXXXXXX", "...X...", "XX...XX", "" }, { "..X.....X..", "X..X...X..X", "X.XXXXXXX.X", "XXX.XXX.XXX", "XXXXXXXXXXX", ".X.XXXXX.X.", "...X...X...", "..XX...XX.." }, { "..XXXXXXXX..", ".XXXXXXXXXX.", "XXXXXXXXXXXX", "XX.XXXXXX.XX", ".XXXXXXXXXX.", ".XX.X..X.XX.", "..XX....XX..", "" }, }, }; static const int SPR_W[3] = { 7, 11, 12 }; static const int SPR_H[3] = { 7, 8, 8 }; static const int ROWT[ROWS] = { 0, 1, 1, 2, 2 }; static const int PTS[3] = { 30, 20, 10 }; static const char *SHIP[SHIP_H] = { "....X....", "...XXX...", "...XXX...", "..XXXXX..", ".XXXXXXX.", "XXXXXXXXX", }; typedef struct { int on; float x, y; } Shot; static Display *dpy; static Window win; static Pixmap pm; static GC gc; static Atom wm_del; static XFontStruct *fs; static Colormap cmap; static int down[7]; static struct { int st, paused; int alive[ROWS][COLS]; int live; int score, hi, wave, lives; float px; float cool; int frame; float ox, oy; int dir; float t_step, t_fire, t_ready; Shot pb; Shot eb[2]; } g; static double now_s(void) { struct timespec t; clock_gettime(CLOCK_MONOTONIC, &t); return (double)t.tv_sec + 1e-9 * (double)t.tv_nsec; } static float frand(void) { return (float)rand() / (float)RAND_MAX; } static float cx_of(int c) { return FIELD_X + (float)c * GAPX; } static void col(const char *spec) { XColor c; if (XParseColor(dpy, cmap, spec, &c)) { XAllocColor(dpy, cmap, &c); XSetForeground(dpy, gc, c.pixel); } } static void draw_spr(const char **rows, int w, int h, float cx, float y) { int half = (w * SCALE) / 2; for (int r = 0; r < h; r++) { const char *row = rows[r]; for (int c = 0; c < w; c++) if (row[c] == 'X') XFillRectangle(dpy, pm, gc, (int)cx - half + c * SCALE, (int)y + r * SCALE, SCALE, SCALE); } } static void draw_text(const char *s, float x, float ytop) { if (!fs) return; XDrawString(dpy, pm, gc, (int)x, (int)ytop + fs->ascent, s, (int)strlen(s)); } static void draw_center(const char *s, float ytop) { if (!fs) return; draw_text(s, (W - XTextWidth(fs, s, (int)strlen(s))) / 2.0f, ytop); } static void alien_rect(int r, int c, float *x0, float *x1, float *y0, float *y1) { int t = ROWT[r]; float hw = (SPR_W[t] * SCALE) / 2.0f; float ccx = g.ox + cx_of(c); *x0 = ccx - hw; *x1 = ccx + hw; *y0 = g.oy + FIELD_Y + (float)r * GAPY; *y1 = *y0 + SPR_H[t] * SCALE; } static void spawn_wave(void) { for (int r = 0; r < ROWS; r++) for (int c = 0; c < COLS; c++) g.alive[r][c] = 1; g.live = ROWS * COLS; g.dir = 1; g.ox = 0; g.oy = 0; g.frame = 0; g.t_step = 0; g.t_fire = 0.9f; g.pb.on = 0; g.eb[0].on = g.eb[1].on = 0; } static void start_new_game(void) { g.score = 0; g.lives = 3; g.wave = 1; g.px = W / 2.0f; g.cool = 0; spawn_wave(); g.st = ST_READY; g.t_ready = 1.2f; } static void try_fire(void) { if (g.st == ST_TITLE || g.st == ST_OVER) { start_new_game(); return; } if (g.st == ST_PLAY && !g.paused && g.cool <= 0 && !g.pb.on) { g.pb.on = 1; g.pb.x = g.px; g.pb.y = SHIP_TOP; g.cool = 0.25f; } } static void kill_player(void) { g.lives--; g.pb.on = 0; g.eb[0].on = g.eb[1].on = 0; if (g.lives <= 0) { if (g.score > g.hi) g.hi = g.score; g.st = ST_OVER; } else { g.st = ST_DEAD; g.t_ready = 1.2f; } } static void update(float dt) { if (g.paused || g.st == ST_TITLE || g.st == ST_OVER) return; if (g.cool > 0) g.cool -= dt; if (g.st == ST_READY || g.st == ST_DEAD) { if (g.st == ST_DEAD) { g.t_ready -= dt; if (g.t_ready <= 0) { g.st = ST_READY; g.t_ready = 1.0f; } } else { g.t_ready -= dt; if (g.t_ready <= 0) g.st = ST_PLAY; } return; } if (down[ACT_LEFT]) g.px -= P_SPEED * dt; if (down[ACT_RIGHT]) g.px += P_SPEED * dt; if (g.px < 24) g.px = 24; if (g.px > W - 24) g.px = W - 24; if (g.pb.on) { g.pb.y -= B_SPD_P * dt; if (g.pb.y < -10) g.pb.on = 0; } for (int i = 0; i < 2; i++) { if (!g.eb[i].on) continue; g.eb[i].y += B_SPD_E * dt; if (g.eb[i].y > H) g.eb[i].on = 0; } g.t_step += dt; float interval = 0.55f + ((float)g.live / (float)(ROWS * COLS)) * 0.75f; if (g.t_step >= interval) { g.t_step = 0; float lo = 1e9f, hi = -1e9f, bot = 0; for (int r = 0; r < ROWS; r++) for (int c = 0; c < COLS; c++) { if (!g.alive[r][c]) continue; float x0, x1, y0, y1; alien_rect(r, c, &x0, &x1, &y0, &y1); if (x0 < lo) lo = x0; if (x1 > hi) hi = x1; if (y1 > bot) bot = y1; } if ((g.dir > 0 && hi + A_STEP >= W - 12) || (g.dir < 0 && lo - A_STEP <= 12)) { g.dir = -g.dir; g.oy += A_DROP; bot += A_DROP; if (bot >= SHIP_TOP) { if (g.score > g.hi) g.hi = g.score; g.st = ST_OVER; return; } } else { g.ox += (float)g.dir * A_STEP; } g.frame ^= 1; } g.t_fire -= dt; if (g.t_fire <= 0) { int idx = (int)(frand() * (float)(ROWS * COLS)); int r = idx / COLS, c = idx % COLS; g.t_fire = 0.4f + frand() * 1.1f; if (g.alive[r][c]) { for (int i = 0; i < 2; i++) { if (!g.eb[i].on) { float ax0, ax1, ay0, ay1; alien_rect(r, c, &ax0, &ax1, &ay0, &ay1); g.eb[i].on = 1; g.eb[i].x = (ax0 + ax1) / 2; g.eb[i].y = ay1; break; } } } } if (g.pb.on) for (int r = 0; r < ROWS; r++) for (int c = 0; c < COLS; c++) { if (!g.alive[r][c]) continue; float x0, x1, y0, y1; alien_rect(r, c, &x0, &x1, &y0, &y1); if (g.pb.x >= x0 && g.pb.x <= x1 && g.pb.y <= y1 && g.pb.y + 8 >= y0) { g.alive[r][c] = 0; g.live--; g.score += PTS[ROWT[r]]; g.pb.on = 0; break; } } for (int i = 0; i < 2; i++) if (g.eb[i].on) { float x0, x1; x0 = g.px - (SHIP_W * SCALE) / 2.0f; x1 = g.px + (SHIP_W * SCALE) / 2.0f; if (g.eb[i].x >= x0 && g.eb[i].x <= x1 && g.eb[i].y + 8 >= SHIP_TOP && g.eb[i].y <= SHIP_TOP + SHIP_H * SCALE) { kill_player(); return; } } if (g.pb.on) for (int i = 0; i < 2; i++) if (g.eb[i].on && g.eb[i].x >= g.pb.x - 4 && g.eb[i].x <= g.pb.x + 4 && g.eb[i].y <= g.pb.y + 8 && g.eb[i].y + 8 >= g.pb.y) { g.eb[i].on = 0; g.pb.on = 0; } if (g.live == 0) { g.wave++; spawn_wave(); g.st = ST_READY; g.t_ready = 1.2f; } } static void draw(void) { col("#000000"); XFillRectangle(dpy, pm, gc, 0, 0, W, H); if (g.st == ST_TITLE) { col("#00ff7f"); draw_center("SPACE INVADERS", 130); col("#bbbbbb"); draw_center("arrows / A D move", 210); draw_center("space / W / up fire", 235); draw_center("P pause Q / Esc quit", 260); col("#ffffff"); draw_center("press SPACE to start", 320); goto blit; } col("#00ff7f"); draw_text("SCORE", 16, 10); col("#ffffff"); { char b[32]; snprintf(b, sizeof(b), "%d", g.score); draw_text(b, 16, 32); } col("#00ff7f"); draw_text("HI", W / 2.0f - 60, 10); col("#ffffff"); { char b[32]; snprintf(b, sizeof(b), "%d", g.hi); draw_text(b, W / 2.0f - 60, 32); } col("#00ff7f"); draw_text("WAVE", W / 2.0f + 30, 10); col("#ffffff"); { char b[32]; snprintf(b, sizeof(b), "%d", g.wave); draw_text(b, W / 2.0f + 30, 32); } col("#00bfff"); draw_text("LIVES", W - 120, 10); col("#ffffff"); { char b[8]; snprintf(b, sizeof(b), "%d", g.lives); draw_text(b, W - 120, 32); } for (int r = 0; r < ROWS; r++) for (int c = 0; c < COLS; c++) { if (!g.alive[r][c]) continue; int t = ROWT[r]; col(t == 0 ? "#ff5f56" : (t == 1 ? "#ffd400" : "#00ff7f")); draw_spr(SPR[g.frame][t], SPR_W[t], SPR_H[t], g.ox + cx_of(c), g.oy + FIELD_Y + (float)r * GAPY); } if (g.st != ST_OVER) { col("#00bfff"); draw_spr(SHIP, SHIP_W, SHIP_H, g.px, SHIP_TOP); } col("#ffffff"); if (g.pb.on) XFillRectangle(dpy, pm, gc, (int)g.pb.x - 1, (int)g.pb.y, 3, 9); col("#ff2244"); for (int i = 0; i < 2; i++) if (g.eb[i].on) XFillRectangle(dpy, pm, gc, (int)g.eb[i].x - 1, (int)g.eb[i].y, 3, 9); if (g.st == ST_READY) { col("#ffffff"); draw_center("READY", H / 2.0f); } if (g.paused) { col("#ffffff"); draw_center("PAUSED", H / 2.0f + 40); } if (g.st == ST_OVER) { col("#ff5f56"); draw_center("GAME OVER", 180); col("#ffffff"); draw_center("press R / Enter / Space to play again", 220); } blit: XCopyArea(dpy, pm, win, gc, 0, 0, W, H, 0, 0); XFlush(dpy); } static int keymap(KeySym k) { switch (k) { case XK_Left: case XK_a: case XK_A: return ACT_LEFT; case XK_Right: case XK_d: case XK_D: return ACT_RIGHT; case XK_space: case XK_w: case XK_W: case XK_Up: return ACT_FIRE; case XK_p: case XK_P: return ACT_PAUSE; case XK_Escape: case XK_q: case XK_Q: return ACT_QUIT; case XK_Return: case XK_r: case XK_R: return ACT_RESTART; default: return ACT_NONE; } } static void quit(void) { XAutoRepeatOn(dpy); XCloseDisplay(dpy); exit(0); } static void handle(XEvent *e) { if (e->type == ClientMessage) { if ((Atom)e->xclient.data.l[0] == wm_del) quit(); return; } if (!fs) return; if (e->type == KeyPress) { int a = keymap(XLookupKeysym(&e->xkey, 0)); if (a == ACT_LEFT || a == ACT_RIGHT) down[a] = 1; else if (a == ACT_FIRE) try_fire(); else if (a == ACT_RESTART && g.st == ST_OVER) start_new_game(); else if (a == ACT_PAUSE) g.paused = !g.paused; else if (a == ACT_QUIT) quit(); } else if (e->type == KeyRelease) { int a = keymap(XLookupKeysym(&e->xkey, 0)); if (a == ACT_LEFT || a == ACT_RIGHT) down[a] = 0; } } int main(void) { srand((unsigned)time(NULL)); dpy = XOpenDisplay(NULL); if (!dpy) { fprintf(stderr, "cannot open X display\n"); return 1; } int scr = DefaultScreen(dpy); cmap = DefaultColormap(dpy, scr); Window root = RootWindow(dpy, scr); win = XCreateSimpleWindow(dpy, root, 0, 0, W, H, 1, BlackPixel(dpy, scr), BlackPixel(dpy, scr)); XSelectInput(dpy, win, KeyPressMask | KeyReleaseMask | ExposureMask); XStoreName(dpy, win, "Space Invaders"); XSizeHints *sh = XAllocSizeHints(); if (sh) { sh->flags = PMinSize | PMaxSize; sh->min_width = sh->max_width = W; sh->min_height = sh->max_height = H; XSetWMNormalHints(dpy, win, sh); XFree(sh); } wm_del = XInternAtom(dpy, "WM_DELETE_WINDOW", False); XSetWMProtocols(dpy, win, &wm_del, 1); fs = XLoadQueryFont(dpy, "9x15"); if (!fs) fs = XLoadQueryFont(dpy, "-*-courier-medium-r-normal-*-12-*"); pm = XCreatePixmap(dpy, win, W, H, DefaultDepth(dpy, scr)); gc = XCreateGC(dpy, pm, 0, NULL); if (fs) XSetFont(dpy, gc, fs->fid); XMapWindow(dpy, win); XAutoRepeatOff(dpy); memset(&g, 0, sizeof(g)); g.st = ST_TITLE; g.px = W / 2.0f; draw(); for (;;) { double t0 = now_s(); while (XPending(dpy)) { XEvent e; XNextEvent(dpy, &e); handle(&e); } update(STEP_DT); draw(); double sl = STEP_DT - (now_s() - t0); if (sl > 0) { struct timespec rq; rq.tv_sec = (time_t)sl; rq.tv_nsec = (long)(sl - (double)(time_t)sl) * 1000000000L; nanosleep(&rq, NULL); } } return 0; }