|
| 1 | +// -*- mode:c; indent-tabs-mode:nil; c-basic-offset:4 -*- |
| 2 | +// vi: set et ft=c ts=4 sts=4 sw=4 fenc=utf-8 |
| 3 | + |
| 4 | +// asteroids by tsotchke |
| 5 | +// https://github.com/tsotchke/asteroids |
| 6 | + |
| 7 | +// clang-format off |
| 8 | + |
| 9 | +#include <stdio.h> |
| 10 | +#include <stdlib.h> |
| 11 | +#include <string.h> |
| 12 | +#include <math.h> |
| 13 | +#include <time.h> |
| 14 | +#include <unistd.h> |
| 15 | +#include <termios.h> |
| 16 | +#include <sys/select.h> |
| 17 | + |
| 18 | +#define SCREEN_WIDTH 80 |
| 19 | +#define SCREEN_HEIGHT 24 |
| 20 | +#define MAX_ASTEROIDS 5 |
| 21 | +#define MAX_BULLETS 5 |
| 22 | + |
| 23 | +typedef struct { |
| 24 | + float x, y; |
| 25 | +} Vector2; |
| 26 | + |
| 27 | +typedef struct { |
| 28 | + Vector2 position; |
| 29 | + Vector2 velocity; |
| 30 | + float angle; |
| 31 | + float radius; |
| 32 | +} GameObject; |
| 33 | + |
| 34 | +GameObject spaceship; |
| 35 | +GameObject asteroids[MAX_ASTEROIDS]; |
| 36 | +GameObject bullets[MAX_BULLETS]; |
| 37 | + |
| 38 | +int score = 0; |
| 39 | +time_t startTime; |
| 40 | +int isGameOver = 0; |
| 41 | +int finalTime = 0; // To store final time at game over |
| 42 | +char display[SCREEN_HEIGHT][SCREEN_WIDTH]; |
| 43 | + |
| 44 | +// Function to clear the screen buffer |
| 45 | +void clearDisplay() { |
| 46 | + memset(display, ' ', sizeof(display)); |
| 47 | +} |
| 48 | + |
| 49 | +// Function to draw a pixel on the screen |
| 50 | +void drawPixel(int x, int y) { |
| 51 | + if (x >= 0 && x < SCREEN_WIDTH && y >= 0 && y < SCREEN_HEIGHT) { |
| 52 | + display[y][x] = '*'; |
| 53 | + } |
| 54 | +} |
| 55 | + |
| 56 | +// Function to draw a line using Bresenham's algorithm |
| 57 | +void drawLine(int x1, int y1, int x2, int y2) { |
| 58 | + int dx = abs(x2 - x1), sx = (x1 < x2) ? 1 : -1; |
| 59 | + int dy = -abs(y2 - y1), sy = (y1 < y2) ? 1 : -1; |
| 60 | + int error = dx + dy, e2; |
| 61 | + |
| 62 | + while (1) { |
| 63 | + drawPixel(x1, y1); |
| 64 | + if (x1 == x2 && y1 == y2) break; |
| 65 | + e2 = 2 * error; |
| 66 | + if (e2 >= dy) { error += dy; x1 += sx; } |
| 67 | + if (e2 <= dx) { error += dx; y1 += sy; } |
| 68 | + } |
| 69 | +} |
| 70 | + |
| 71 | +// Function to draw a circle |
| 72 | +void drawCircle(int centerX, int centerY, int radius) { |
| 73 | + int x = radius - 1, y = 0, dx = 1, dy = 1, err = dx - (radius << 1); |
| 74 | + while (x >= y) { |
| 75 | + drawPixel(centerX + x, centerY + y); |
| 76 | + drawPixel(centerX + y, centerY + x); |
| 77 | + drawPixel(centerX - y, centerY + x); |
| 78 | + drawPixel(centerX - x, centerY + y); |
| 79 | + drawPixel(centerX - x, centerY - y); |
| 80 | + drawPixel(centerX - y, centerY - x); |
| 81 | + drawPixel(centerX + y, centerY - x); |
| 82 | + drawPixel(centerX + x, centerY - y); |
| 83 | + |
| 84 | + if (err <= 0) { |
| 85 | + y++; |
| 86 | + err += dy; |
| 87 | + dy += 2; |
| 88 | + } |
| 89 | + if (err > 0) { |
| 90 | + x--; |
| 91 | + dx += 2; |
| 92 | + err += dx - (radius << 1); |
| 93 | + } |
| 94 | + } |
| 95 | +} |
| 96 | + |
| 97 | +// Initialize a game object |
| 98 | +void initializeGameObject(GameObject *obj, float x, float y, float angle, float radius) { |
| 99 | + obj->position = (Vector2){x, y}; |
| 100 | + obj->velocity = (Vector2){0, 0}; |
| 101 | + obj->angle = angle; |
| 102 | + obj->radius = radius; |
| 103 | +} |
| 104 | + |
| 105 | +// Wrap position of the spaceship and asteroids within screen bounds |
| 106 | +void wrapPosition(Vector2 *pos) { |
| 107 | + if (pos->x < 0) pos->x = SCREEN_WIDTH - 1; |
| 108 | + if (pos->x >= SCREEN_WIDTH) pos->x = 0; |
| 109 | + if (pos->y < 0) pos->y = SCREEN_HEIGHT - 1; |
| 110 | + if (pos->y >= SCREEN_HEIGHT) pos->y = 0; |
| 111 | +} |
| 112 | + |
| 113 | +// Check if two game objects are colliding |
| 114 | +int checkCollision(GameObject *a, GameObject *b) { |
| 115 | + float deltaX = a->position.x - b->position.x; |
| 116 | + float deltaY = a->position.y - b->position.y; |
| 117 | + return sqrt(deltaX * deltaX + deltaY * deltaY) < (a->radius + b->radius); |
| 118 | +} |
| 119 | + |
| 120 | +// Initialize game state |
| 121 | +void initGame() { |
| 122 | + score = 0; // Reset the score |
| 123 | + initializeGameObject(&spaceship, SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2, 0, 2); |
| 124 | + |
| 125 | + for (int i = 0; i < MAX_ASTEROIDS; i++) { |
| 126 | + initializeGameObject(&asteroids[i], |
| 127 | + rand() % SCREEN_WIDTH, |
| 128 | + rand() % SCREEN_HEIGHT, |
| 129 | + 0, |
| 130 | + 2 + rand() % 3); |
| 131 | + asteroids[i].velocity.x = ((float)rand() / RAND_MAX) * 2 - 1; |
| 132 | + asteroids[i].velocity.y = ((float)rand() / RAND_MAX) * 2 - 1; |
| 133 | + } |
| 134 | + |
| 135 | + for (int i = 0; i < MAX_BULLETS; i++) { |
| 136 | + bullets[i].position.x = -1; // Mark bullet as inactive |
| 137 | + bullets[i].position.y = -1; |
| 138 | + } |
| 139 | + |
| 140 | + startTime = time(NULL); |
| 141 | + isGameOver = 0; |
| 142 | + finalTime = 0; // Reset final time |
| 143 | +} |
| 144 | + |
| 145 | +// Draw the spaceship on the screen |
| 146 | +void drawSpaceship() { |
| 147 | + int x = (int)spaceship.position.x; |
| 148 | + int y = (int)spaceship.position.y; |
| 149 | + int size = 3; |
| 150 | + |
| 151 | + float cosAngle = cos(spaceship.angle); |
| 152 | + float sinAngle = sin(spaceship.angle); |
| 153 | + |
| 154 | + int x1 = x + size * cosAngle; |
| 155 | + int y1 = y + size * sinAngle; |
| 156 | + int x2 = x + size * cos(spaceship.angle + 2.5); |
| 157 | + int y2 = y + size * sin(spaceship.angle + 2.5); |
| 158 | + int x3 = x + size * cos(spaceship.angle - 2.5); |
| 159 | + int y3 = y + size * sin(spaceship.angle - 2.5); |
| 160 | + |
| 161 | + drawLine(x1, y1, x2, y2); |
| 162 | + drawLine(x2, y2, x3, y3); |
| 163 | + drawLine(x3, y3, x1, y1); |
| 164 | +} |
| 165 | + |
| 166 | +// Draw all entities on the screen |
| 167 | +void drawEntities(GameObject *entities, int count, void (*drawFunc)(GameObject *)) { |
| 168 | + for (int i = 0; i < count; i++) { |
| 169 | + drawFunc(&entities[i]); |
| 170 | + } |
| 171 | +} |
| 172 | + |
| 173 | +// Draw a bullet on the screen |
| 174 | +void drawBullet(GameObject *bullet) { // Changed to non-const |
| 175 | + if (bullet->position.x >= 0) { |
| 176 | + drawPixel((int)bullet->position.x, (int)bullet->position.y); |
| 177 | + } |
| 178 | +} |
| 179 | + |
| 180 | +// Draw an asteroid on the screen |
| 181 | +void drawAsteroid(GameObject *asteroid) { // Changed to non-const |
| 182 | + drawCircle((int)asteroid->position.x, (int)asteroid->position.y, (int)asteroid->radius); |
| 183 | +} |
| 184 | + |
| 185 | +// Refresh the display |
| 186 | +void updateDisplay() { |
| 187 | + clearDisplay(); |
| 188 | + if (!isGameOver) { |
| 189 | + drawSpaceship(); |
| 190 | + drawEntities(asteroids, MAX_ASTEROIDS, drawAsteroid); |
| 191 | + drawEntities(bullets, MAX_BULLETS, drawBullet); |
| 192 | + } |
| 193 | + |
| 194 | + // Print the screen buffer |
| 195 | + printf("\033[H"); |
| 196 | + for (int y = 0; y < SCREEN_HEIGHT; y++) { |
| 197 | + for (int x = 0; x < SCREEN_WIDTH; x++) { |
| 198 | + putchar(display[y][x]); |
| 199 | + } |
| 200 | + putchar('\n'); |
| 201 | + } |
| 202 | + |
| 203 | + // Display score and elapsed time |
| 204 | + time_t currentTime = time(NULL); |
| 205 | + int elapsedTime = isGameOver ? finalTime : (currentTime - startTime); |
| 206 | + printf("Score: %d | Time: %02d:%02d | %s\n", score, elapsedTime / 60, elapsedTime % 60, isGameOver ? "Game Over!" : " "); |
| 207 | +} |
| 208 | + |
| 209 | +// Update the position of game objects |
| 210 | +void updateGameObject(GameObject *obj, int isBullet) { |
| 211 | + obj->position.x += obj->velocity.x; |
| 212 | + obj->position.y += obj->velocity.y; |
| 213 | + |
| 214 | + // If it's a bullet, check if it's out of bounds |
| 215 | + if (isBullet) { |
| 216 | + if (obj->position.x < 0 || obj->position.x >= SCREEN_WIDTH || obj->position.y < 0 || obj->position.y >= SCREEN_HEIGHT) { |
| 217 | + obj->position.x = -1; // Deactivate bullet |
| 218 | + obj->position.y = -1; |
| 219 | + } |
| 220 | + } else { |
| 221 | + wrapPosition(&obj->position); |
| 222 | + } |
| 223 | +} |
| 224 | + |
| 225 | +// Update the game state |
| 226 | +void updateGame() { |
| 227 | + if (isGameOver) return; |
| 228 | + |
| 229 | + // Update spaceship and apply friction |
| 230 | + updateGameObject(&spaceship, 0); // 0 indicates it's not a bullet |
| 231 | + spaceship.velocity.x *= 0.98; |
| 232 | + spaceship.velocity.y *= 0.98; |
| 233 | + |
| 234 | + // Move asteroids and check for collisions |
| 235 | + for (int i = 0; i < MAX_ASTEROIDS; i++) { |
| 236 | + updateGameObject(&asteroids[i], 0); |
| 237 | + if (checkCollision(&spaceship, &asteroids[i])) { |
| 238 | + isGameOver = 1; |
| 239 | + finalTime = time(NULL) - startTime; |
| 240 | + return; |
| 241 | + } |
| 242 | + } |
| 243 | + |
| 244 | + // Update bullet positions |
| 245 | + for (int i = 0; i < MAX_BULLETS; i++) { |
| 246 | + if (bullets[i].position.x >= 0) { |
| 247 | + updateGameObject(&bullets[i], 1); // 1 indicates it's a bullet |
| 248 | + } |
| 249 | + } |
| 250 | + |
| 251 | + // Check for bullet collisions with asteroids |
| 252 | + for (int i = 0; i < MAX_BULLETS; i++) { |
| 253 | + if (bullets[i].position.x >= 0) { |
| 254 | + for (int j = 0; j < MAX_ASTEROIDS; j++) { |
| 255 | + if (checkCollision(&bullets[i], &asteroids[j])) { |
| 256 | + bullets[i].position.x = -1; // Deactivate bullet |
| 257 | + bullets[i].position.y = -1; |
| 258 | + asteroids[j].position.x = rand() % SCREEN_WIDTH; |
| 259 | + asteroids[j].position.y = rand() % SCREEN_HEIGHT; |
| 260 | + score += 100; |
| 261 | + } |
| 262 | + } |
| 263 | + } |
| 264 | + } |
| 265 | +} |
| 266 | + |
| 267 | +// Fire a bullet |
| 268 | +void shootBullet() { |
| 269 | + for (int i = 0; i < MAX_BULLETS; i++) { |
| 270 | + if (bullets[i].position.x < 0) { |
| 271 | + bullets[i].position = spaceship.position; |
| 272 | + bullets[i].velocity.x = cos(spaceship.angle) * 2; |
| 273 | + bullets[i].velocity.y = sin(spaceship.angle) * 2; |
| 274 | + break; |
| 275 | + } |
| 276 | + } |
| 277 | +} |
| 278 | + |
| 279 | +// Check if a key was hit |
| 280 | +int isKeyHit() { |
| 281 | + struct timeval tv = { 0L, 0L }; |
| 282 | + fd_set fds; |
| 283 | + FD_ZERO(&fds); |
| 284 | + FD_SET(0, &fds); |
| 285 | + return select(1, &fds, NULL, NULL, &tv); |
| 286 | +} |
| 287 | + |
| 288 | +// Configure terminal settings |
| 289 | +void configureTerminal(struct termios *old_tio, struct termios *new_tio) { |
| 290 | + tcgetattr(STDIN_FILENO, old_tio); |
| 291 | + *new_tio = *old_tio; |
| 292 | + new_tio->c_lflag &= (~ICANON & ~ECHO); |
| 293 | + tcsetattr(STDIN_FILENO, TCSANOW, new_tio); |
| 294 | +} |
| 295 | + |
| 296 | +// Restore terminal settings |
| 297 | +void restoreTerminal(struct termios *old_tio) { |
| 298 | + tcsetattr(STDIN_FILENO, TCSANOW, old_tio); |
| 299 | +} |
| 300 | + |
| 301 | +// Main game loop |
| 302 | +int main() { |
| 303 | + srand(time(NULL)); // Seed the random number generator |
| 304 | + initGame(); // Initialize the game state |
| 305 | + |
| 306 | + struct termios old_tio, new_tio; |
| 307 | + configureTerminal(&old_tio, &new_tio); |
| 308 | + |
| 309 | + printf("\033[?25l"); // Hide the cursor |
| 310 | + |
| 311 | + while (1) { |
| 312 | + if (isKeyHit()) { |
| 313 | + char input = getchar(); |
| 314 | + if (input == 27) { // ESC key |
| 315 | + if (getchar() == '[') { // Handle arrow keys |
| 316 | + switch (getchar()) { |
| 317 | + case 'A': // Up arrow |
| 318 | + spaceship.velocity.x += cos(spaceship.angle) * 0.2; |
| 319 | + spaceship.velocity.y += sin(spaceship.angle) * 0.2; |
| 320 | + break; |
| 321 | + case 'B': // Down arrow |
| 322 | + spaceship.velocity.x -= cos(spaceship.angle) * 0.2; |
| 323 | + spaceship.velocity.y -= sin(spaceship.angle) * 0.2; |
| 324 | + break; |
| 325 | + case 'D': spaceship.angle -= 0.2; break; // Left arrow |
| 326 | + case 'C': spaceship.angle += 0.2; break; // Right arrow |
| 327 | + } |
| 328 | + } |
| 329 | + } else if (input == ' ') { |
| 330 | + shootBullet(); // Fire a bullet |
| 331 | + } else if (input == 'q') { |
| 332 | + break; // Quit the game |
| 333 | + } else if (input == 'r' && isGameOver) { |
| 334 | + initGame(); // Restart the game |
| 335 | + } |
| 336 | + } |
| 337 | + |
| 338 | + updateGame(); // Update game state |
| 339 | + updateDisplay(); // Refresh the display |
| 340 | + usleep(50000); // Wait for 50ms (20 FPS) |
| 341 | + } |
| 342 | + |
| 343 | + printf("\033[?25h"); // Show the cursor |
| 344 | + restoreTerminal(&old_tio); // Restore terminal settings |
| 345 | + return 0; |
| 346 | +} |
0 commit comments