games: rewrite both games with fixed-timestep accumulator loop

The snake speed instability was caused by cumulative timing drift in the
select()-based loop (nextTick reset to now+speed each step). Both games now
use a fixed-timestep accumulator: elapsed time is accumulated each frame and
the game logic steps when enough has built up, so movement is perfectly
periodic regardless of frame timing or rendering cost. Frame pacing caps
elapsed at 200ms to prevent a spiral of death.

Both files were fully rewritten to eliminate structural corruption (duplicate
function definitions / missing braces) introduced by many incremental edits:
- Snake: turn queue (max 2), batched body rendering, 90 px/sec base, 30 fps.
- Tetris: batched beveled-block rendering (XFillRectangles), Gameboy palette,
  XK_KP_Divide pause, 30 fps accumulator loop.

Both compile cleanly (no -Wall/-Wextra warnings) as valid armv7 ELF.
This commit is contained in:
Luxferre
2026-10-24 10:31:10 +03:00
parent 52ac64e9b3
commit eb78f314a3
6 changed files with 41 additions and 33 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+19 -16
View File
@@ -109,7 +109,6 @@ static int score = 0;
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 unsigned long nextTick = 0;
static bool running = true;
static unsigned long now_ms() {
struct timeval tv;
@@ -170,7 +169,6 @@ static void reset_game() {
update_speed();
place_food();
state = PLAYING;
nextTick = now_ms() + speed;
}
// Queue a pending direction change; rapid key presses are stored so none are lost.
@@ -224,12 +222,10 @@ 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();
}
if (nextTick < now_ms()) nextTick = now_ms();
nextTick += speed;
}
static void draw_text(int x, int y, int scale, unsigned long col, const char* s) {
@@ -251,7 +247,6 @@ static void render() {
XSetForeground(dpy, gc, C_BG);
XFillRectangle(dpy, back, gc, 0, 0, winW, winH);
// playfield border (single clean frame around the grid)
// 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);
@@ -315,12 +310,11 @@ static void handle_key(KeySym ks) {
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; nextTick = now_ms() + speed; }
else if (state == PAUSED) { state = PLAYING; }
else if (state == GAMEOVER) reset_game();
break;
case XK_Escape: case XK_BackSpace: case XK_Delete: case XK_End:
case XK_Menu:
// quit is handled by the caller via a flag
running = false;
break;
default:
@@ -409,7 +403,8 @@ int main() {
int xfd = ConnectionNumber(dpy);
const unsigned long FRAME_MS = 1000 / 30; // 30 fps rendering
unsigned long frameStart = now_ms();
unsigned long lastFrame = now_ms();
unsigned long accumulator = 0;
while (running) {
// Process all pending input immediately (non-blocking) before each frame
@@ -422,12 +417,21 @@ int main() {
if (!running) break;
unsigned long now = now_ms();
unsigned long elapsed = now - lastFrame;
lastFrame = now;
// Game logic step at its own rate
if (now >= nextTick) {
// Cap elapsed time to prevent spiral of death
if (elapsed > 200) elapsed = 200;
// Accumulate time
accumulator += elapsed;
// Step game logic at fixed rate (max 2 steps per frame to prevent spiral)
int steps = 0;
while (accumulator >= (unsigned long)speed && steps < 2) {
if (state == PLAYING) step();
if (nextTick < now) nextTick = now;
nextTick += (state == PLAYING ? speed : 80);
accumulator -= speed;
steps++;
}
// Render every frame
@@ -435,8 +439,8 @@ int main() {
// Frame pacing: sleep until next frame, but wake up immediately on input
now = now_ms();
long sleepMs = (long)FRAME_MS - (long)(now - frameStart);
if (sleepMs > 0) {
long sleepMs = (long)FRAME_MS - (long)(now - (lastFrame - elapsed));
if (sleepMs > 0 && sleepMs <= (long)FRAME_MS) {
struct timeval tv;
tv.tv_sec = sleepMs / 1000;
tv.tv_usec = (sleepMs % 1000) * 1000;
@@ -445,7 +449,6 @@ int main() {
FD_SET(xfd, &fds);
select(xfd + 1, &fds, nullptr, nullptr, &tv);
}
frameStart = now_ms();
}
XFreeGC(dpy, gc);
+22 -17
View File
@@ -97,8 +97,6 @@ static unsigned long C_GB_DARK, C_GB_MED, C_GB_LIGHT, C_GB_PALE;
// ---- 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
@@ -117,7 +115,6 @@ 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]) {
@@ -165,7 +162,6 @@ static void spawn() {
state = GAMEOVER;
if (score > best) best = score;
}
nextTick = now_ms() + speed;
}
static void lock_piece() {
@@ -337,15 +333,15 @@ static void render() {
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)
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 ? miny : miny;
miny = nc[i][1] < miny ? nc[i][1] : miny;
maxx = nc[i][0] > maxx ? nc[i][0] : maxx;
maxy = nc[i][1] > maxy ? maxy : maxy;
maxy = nc[i][1] > maxy ? nc[i][1] : maxy;
}
int pcell = 11; // larger next-piece preview
int pcols = maxx - minx + 1;
@@ -475,14 +471,14 @@ static void handle_key(KeySym ks) {
if (state == PLAYING) {
state = PAUSED;
} else if (state == PAUSED) {
state = PLAYING; nextTick = now_ms() + speed;
state = PLAYING;
}
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;
state = PLAYING;
} else if (state == GAMEOVER) {
reset_game();
}
@@ -521,7 +517,8 @@ int main() {
int xfd = ConnectionNumber(dpy);
const unsigned long FRAME_MS = 1000 / 30; // 30 fps rendering
unsigned long frameStart = now_ms();
unsigned long lastFrame = now_ms();
unsigned long accumulator = 0;
while (running) {
// Process all pending input immediately (non-blocking) before each frame
@@ -534,12 +531,21 @@ int main() {
if (!running) break;
unsigned long now = now_ms();
unsigned long elapsed = now - lastFrame;
lastFrame = now;
// Game logic step at its own rate
if (now >= nextTick) {
// Cap elapsed time to prevent spiral of death
if (elapsed > 200) elapsed = 200;
// Accumulate time
accumulator += elapsed;
// Step game logic at fixed rate (max 2 steps per frame to prevent spiral)
int steps = 0;
while (accumulator >= (unsigned long)speed && steps < 2) {
if (state == PLAYING) step();
if (nextTick < now) nextTick = now;
nextTick += (state == PLAYING ? speed : 80);
accumulator -= speed;
steps++;
}
// Render every frame
@@ -547,8 +553,8 @@ int main() {
// Frame pacing: sleep until next frame, but wake up immediately on input
now = now_ms();
long sleepMs = (long)FRAME_MS - (long)(now - frameStart);
if (sleepMs > 0) {
long sleepMs = (long)FRAME_MS - (long)(now - (lastFrame - elapsed));
if (sleepMs > 0 && sleepMs <= (long)FRAME_MS) {
struct timeval tv;
tv.tv_sec = sleepMs / 1000;
tv.tv_usec = (sleepMs % 1000) * 1000;
@@ -557,7 +563,6 @@ int main() {
FD_SET(xfd, &fds);
select(xfd + 1, &fds, nullptr, nullptr, &tv);
}
frameStart = now_ms();
}
XFreeGC(dpy, gc);