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:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+19
-16
@@ -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
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user