]> git.gir.st - VIper.git/blob - viiper.c
fix segfault when eating food
[VIper.git] / viiper.c
1 /*******************************************************************************
2 viiper 0.1
3 By Tobias Girstmair, 2018
4
5 ./viiper 40x25
6 (see ./viiper -h for full list of options)
7
8 KEYBINDINGS: - hjkl to move
9 - p to pause and resume
10 - r to restart
11 - q to quit
12 - (see `./minesviiper -h' for all keybindings)
13
14 GNU GPL v3, see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt
15 *******************************************************************************/
16
17
18 #define _POSIX_C_SOURCE 2 /*for getopt and sigaction in c99, sigsetjmp*/
19 #include <ctype.h>
20 #include <poll.h>
21 #include <setjmp.h>
22 #include <signal.h>
23 #include <stdio.h>
24 #include <stdlib.h>
25 #include <sys/ioctl.h>
26 #include <sys/time.h>
27 #include <termios.h>
28 #include <time.h>
29 #include <unistd.h>
30
31 #include "viiper.h"
32 #include "schemes.h"
33
34 #define MIN(a,b) (a>b?b:a)
35 #define MAX(a,b) (a>b?a:b)
36 #define CLAMP(a,m,M) (a<m?m:(a>M?M:a))
37 #define printm(n, s) for (int _loop = 0; _loop < n; _loop++) fputs (s, stdout)
38 #define print(str) fputs (str?str:"", stdout)
39 #define CTRL_ 0x1F &
40
41 #define COL_OFFSET 1
42 #define LINE_OFFSET 1
43 #define LINES_AFTER 1
44 #define CW 2 /* cell width */
45
46 struct game {
47 int w; /* field width */
48 int h; /* field height */
49 int d; /* direction the snake is looking */
50 int t; /* time of game start */
51 int p; /* score */
52 float v; /* velocity in moves per second */
53 struct snake* s; /* snek */
54 struct item* i; /* items (food, boni) */
55 struct directions* n;/* next direction events to process */
56 } g;
57
58 struct opt {
59 int l; /* initial snake length */
60 int s; /* initial snake speed */
61 struct scheme* scheme;
62 } op;
63
64 jmp_buf game_over;
65
66 int main (int argc, char** argv) {
67 /* defaults: */
68 g.w = 30; //two-char-width
69 g.h = 20;
70 op.l = 10;
71 op.s = 8;
72 op.scheme = &unic0de; //TODO: expose to getopt() once more sets are available
73
74 int optget;
75 opterr = 0; /* don't print message on unrecognized option */
76 while ((optget = getopt (argc, argv, "+h")) != -1) {
77 switch (optget) {
78 case 'h':
79 default:
80 fprintf (stderr, SHORTHELP LONGHELP, argv[0]);
81 return !(optget=='h');
82 }
83 } if (optind < argc) { /* parse Fieldspec */
84 }
85
86 srand(time(0));
87 signal_setup();
88 screen_setup(1);
89 atexit (*quit);
90
91 if (sigsetjmp(game_over, 1)) {
92 timer_setup(0);
93 move_ph (g.h/2+LINE_OFFSET, g.w);
94 printf ("you died :(");
95 fflush(stdout);
96 sleep(5);
97 }
98
99 //TODO: call viiper() in a game loop
100 viiper();
101 quit:
102 return 0;
103 }
104
105 int viiper(void) {
106 init_snake();
107 show_playfield ();
108 g.d = EAST;
109 g.v = op.s;
110
111 timer_setup(1);
112
113 spawn_item(FOOD, rand() % NUM_FOODS); //TODO: shape distribution, so bigger values get selected less
114
115 for(;;) {
116 switch (getctrlseq()) {
117 case '+': g.v++;timer_setup(1);break; //TODO: temporary, to set speed
118 case '#': if (g.v > 1) g.v--;timer_setup(1); break; //TODO: temporary, to set speed
119 case '\n': spawn_item(FOOD, rand() % NUM_FOODS); break; //TODO: for debugging segfault
120
121 case CTRSEQ_CURSOR_LEFT:
122 case 'h': append_movement(WEST); break;
123 case CTRSEQ_CURSOR_DOWN:
124 case 'j': append_movement(SOUTH); break;
125 case CTRSEQ_CURSOR_UP:
126 case 'k': append_movement(NORTH); break;
127 case CTRSEQ_CURSOR_RIGHT:
128 case 'l': append_movement(EAST); break;
129 case 'p': /*TODO: pause*/ break;
130 case 'r': /*TODO:restart*/ return 0;
131 case 'q': return 0;
132 case CTRL_'L':
133 screen_setup(1);
134 show_playfield();
135 break;
136 }
137
138 print ("\033[H\033[J");
139 show_playfield ();//TODO: only redraw diff
140 }
141
142 }
143
144 void snake_advance (void) {
145 if (g.n) {/* new direction in the buffer */
146 int possible_new_dir = get_movement();
147 if (g.d == EAST && possible_new_dir == WEST) goto ignore_new;
148 if (g.d == WEST && possible_new_dir == EAST) goto ignore_new;
149 if (g.d == NORTH && possible_new_dir == SOUTH) goto ignore_new;
150 if (g.d == SOUTH && possible_new_dir == NORTH) goto ignore_new;
151 g.d = possible_new_dir; /*pop off a new direction if available*/
152 ignore_new:
153 1;
154 }
155
156 int new_row = g.s->r +(g.d==SOUTH) -(g.d==NORTH);
157 int new_col = g.s->c +(g.d==EAST) -(g.d==WEST);
158
159 /* detect food hit and spawn a new food */
160 for (struct item* i = g.i; i; i = i->next) {
161 if (i->r == new_row && i->c == new_col) {
162 consume_item (i);
163 spawn_item(FOOD, rand() % NUM_FOODS);
164 }
165 }
166
167 /*NOTE:no idea why I have to use g.w+1, but otherwise we die too early*/
168 if (new_row >= g.h || new_col >= g.w+1 || new_row < 0 || new_col < 0)
169 siglongjmp(game_over, 1/*<-will be the retval of setjmp*/);
170
171 struct snake* new_head;
172 struct snake* new_tail; /* former second-to-last element */
173 for (new_tail = g.s; new_tail->next->next; new_tail = new_tail->next)
174 /* use the opportunity of looping to check if we eat ourselves*/
175 if(new_tail->next->r == new_row && new_tail->next->c == new_col)
176 siglongjmp(game_over, 1/*<-will be the retval of setjmp*/);
177 new_head = new_tail->next; /* reuse element instead of malloc() */
178 new_tail->next = NULL;
179
180 new_head->r = new_row;
181 new_head->c = new_col;
182 new_head->next = g.s;
183
184 g.s = new_head;
185 }
186
187 void spawn_item (int type, int value) {
188 int row, col;
189 try_again:
190 row = rand() % g.h;
191 col = rand() % g.w;
192 /* loop through snake to check if we aren't on it */
193 //TODO: inefficient as snake gets longer; near impossible in the end
194 for (struct snake* s = g.s; s; s = s->next)
195 if (s->r == row && s->c == col) goto try_again;
196
197 //3. get item from category TODO
198 struct item* new_item = malloc (sizeof(struct item));
199 new_item->r = row;
200 new_item->c = col;
201 new_item->t = type;
202 new_item->v = value;
203 new_item->s = time(0);
204 if (g.i) g.i->prev = new_item;
205 new_item->next = g.i;
206 new_item->prev = NULL;
207
208 g.i = new_item;
209 }
210
211 void consume_item (struct item* i) {
212 switch (i->t) {
213 case FOOD:
214 switch (i->v) {
215 case FOOD_5: g.p += 5; break;
216 case FOOD_10: g.p += 10; break;
217 case FOOD_20: g.p += 20; break;
218 }
219 snake_append(&g.s, 0,0); /* position doesn't matter, as item */
220 break; /* will be reused as the head before it is drawn */
221 case BONUS:
222 //TODO: handle bonus
223 break;
224 }
225
226 if (i->next) i->next->prev = i->prev;
227 if (i->prev) i->prev->next = i->next;
228 else g.i = i->next;
229
230 free (i);
231 }
232
233 void show_playfield (void) {
234 //int score_width = g.p > 9999?6:4;
235 int score_width = 4; // v- = the -| spacer |-
236 float half_width = (g.w - score_width/CW - 2)/2.0; //TODO: this whole endavour is ugly
237 /* top border */
238 print(BORDER(T,L));
239 printm (half_width, BORDER(T,C)); //TODO: i bet this breaks in dec mode
240 printf ("%s %0*d %s", BORDER(S,L), score_width, g.p, BORDER(S,R));
241 printm ((int)(half_width+.5), BORDER(T,C));
242 printf ("%s\n", BORDER(T,R));
243
244 /* main area */
245 for (int row = 0; row < g.h; row++)
246 printf ("%s%*s%s\n", BORDER(C,L), CW*g.w, "", BORDER(C,R));
247
248 /* bottom border */
249 print(BORDER(B,L));
250 printm (g.w, BORDER(B,C));
251 print (BORDER(B,R));
252
253 /* print snake */
254 struct snake* last = NULL;
255 int color = -1; //TODO: that's a hack
256 for (struct snake* s = g.s; s; s = s->next) {
257 move_ph (s->r+COL_OFFSET, s->c*CW+LINE_OFFSET);
258
259 int predecessor = (last==NULL)?NONE:
260 (last->r < s->r) ? NORTH:
261 (last->r > s->r) ? SOUTH:
262 (last->c > s->c) ? EAST:
263 (last->c < s->c) ? WEST:NONE;
264 int successor = (s->next == NULL)?NONE:
265 (s->next->r < s->r) ? NORTH:
266 (s->next->r > s->r) ? SOUTH:
267 (s->next->c > s->c) ? EAST:
268 (s->next->c < s->c) ? WEST:NONE;
269
270 printf ("\033[%sm", color==-1?"1m\033[92":color?"92":"32"); //TODO: clean this up
271 print (op.scheme->snake[predecessor][successor]);
272 printf ("\033[0m");
273 last = s;
274 color = (color+1) % 2;
275 }
276
277 /* print item queue */
278 for (struct item* i = g.i; i; i = i->next) {
279 move_ph (i->r+LINE_OFFSET, i->c*CW+COL_OFFSET);
280 if (i->t == FOOD) print (op.scheme->item[i->v]);
281 else if (i->t==BONUS) /* TODO: print bonus */;
282 }
283
284 //TODO: temporarily print speed
285 move_ph (g.h+LINE_OFFSET, g.w);
286 printf ("%f", g.v);
287 }
288
289 void snake_append (struct snake** s, int row, int col) {
290 struct snake* new = malloc (sizeof(struct snake));
291 new->r = row;
292 new->c = col;
293 new->next = NULL;
294
295 if (*s) {
296 struct snake* p = *s;
297 while (p->next) p = p->next;
298 p->next = new;
299 } else {
300 *s = new;
301 }
302 }
303
304 void init_snake() {
305 for (int i = 0; i < op.l; i++)
306 snake_append(&g.s, g.h/2, g.w/2-i);
307 }
308
309 #define free_ll(head) do{ \
310 while (head) { \
311 void* tmp = head; \
312 head = head->next; \
313 free(tmp); \
314 } \
315 }while(0)
316
317 void quit (void) {
318 screen_setup(0);
319 free_ll(g.s);
320 free_ll(g.i);
321 free_ll(g.n); //TODO: doesn't get free'd correctly
322 }
323
324 enum esc_states {
325 START,
326 ESC_SENT,
327 CSI_SENT,
328 MOUSE_EVENT,
329 };
330 int getctrlseq (void) {
331 int c;
332 int state = START;
333 int offset = 0x20; /* never sends control chars as data */
334 while ((c = getchar()) != EOF) {
335 switch (state) {
336 case START:
337 switch (c) {
338 case '\033': state=ESC_SENT; break;
339 default: return c;
340 }
341 break;
342 case ESC_SENT:
343 switch (c) {
344 case '[': state=CSI_SENT; break;
345 default: return CTRSEQ_INVALID;
346 }
347 break;
348 case CSI_SENT:
349 switch (c) {
350 case 'A': return CTRSEQ_CURSOR_UP;
351 case 'B': return CTRSEQ_CURSOR_DOWN;
352 case 'C': return CTRSEQ_CURSOR_RIGHT;
353 case 'D': return CTRSEQ_CURSOR_LEFT;
354 default: return CTRSEQ_INVALID;
355 }
356 break;
357 default:
358 return CTRSEQ_INVALID;
359 }
360 }
361 return 2;
362 }
363
364 void append_movement (int dir) {
365 struct directions* n;
366 for (n = g.n; n && n->next; n = n->next); /* advance to the end */
367 if (n && n->d == dir) return; /* don't add the same direction twice */
368
369 struct directions* new_event = malloc (sizeof(struct directions));
370 new_event->d = dir;
371 new_event->next = NULL;
372
373 if (g.n == NULL)
374 g.n = new_event;
375 else
376 n->next = new_event;
377 }
378
379 int get_movement (void) {
380 if (g.n == NULL) return -1;
381
382 int retval = g.n->d;
383 struct directions* delet_this = g.n;
384 g.n = g.n->next;
385 free(delet_this);
386 return retval;
387 }
388
389 void move_ph (int line, int col) {
390 /* move printhead to zero-indexed position */
391 printf ("\033[%d;%dH", line+1, col+1);
392 }
393
394 void clamp_fieldsize (void) { //TODO: use
395 /* clamp field size to terminal size and mouse maximum: */
396 struct winsize w;
397 ioctl(STDOUT_FILENO, TIOCGWINSZ, &w);
398
399 if (g.w < 1) g.w = 1;
400 if (g.h < 1) g.h = 1;
401
402 if (COL_OFFSET + g.w*CW + COL_OFFSET > w.ws_col)
403 g.w = (w.ws_col - COL_OFFSET - COL_OFFSET)/CW; //TODO: does not work in `-d' (in xterm)
404 if (LINE_OFFSET + g.h + LINES_AFTER > w.ws_row)
405 g.h = w.ws_row - (LINE_OFFSET+LINES_AFTER);
406 }
407
408 void timer_setup (int enable) {
409 static struct itimerval tbuf;
410 tbuf.it_interval.tv_sec = 0;//TODO: make it speed up automatically
411 tbuf.it_interval.tv_usec = (1000000/g.v)-1; /*WARN: 1 <= g.v <= 999999*/
412
413 if (enable) {
414 g.t = time(NULL);//TODO: interferes with 'pause'
415 tbuf.it_value.tv_sec = tbuf.it_interval.tv_sec;
416 tbuf.it_value.tv_usec = tbuf.it_interval.tv_usec;
417 } else {
418 tbuf.it_value.tv_sec = 0;
419 tbuf.it_value.tv_usec = 0;
420 }
421
422 if ( setitimer(ITIMER_REAL, &tbuf, NULL) == -1 ) {
423 perror("setitimer");
424 exit(1);
425 }
426
427 }
428
429 void signal_setup (void) {
430 struct sigaction saction;
431
432 saction.sa_handler = signal_handler;
433 sigemptyset(&saction.sa_mask);
434 saction.sa_flags = 0;
435 if (sigaction(SIGALRM, &saction, NULL) < 0 ) {
436 perror("SIGALRM");
437 exit(1);
438 }
439
440 if (sigaction(SIGINT, &saction, NULL) < 0 ) {
441 perror ("SIGINT");
442 exit (1);
443 }
444 }
445
446 void signal_handler (int signum) {
447 //int dtime;
448 switch (signum) {
449 case SIGALRM:
450 //dtime = difftime (time(NULL), g.t);
451 //move_ph (1, g.w*CW-(CW%2)-3-(dtime>999));
452 //printf ("[%03d]", g.t?dtime:0);
453 snake_advance();
454 break;
455 case SIGINT:
456 exit(128+SIGINT);
457 }
458 }
459
460 void screen_setup (int enable) {
461 if (enable) {
462 raw_mode(1);
463 printf ("\033[s\033[?47h"); /* save cursor, alternate screen */
464 printf ("\033[H\033[J"); /* reset cursor, clear screen */
465 printf ("\033[?25l"); /* hide cursor */
466 print (op.scheme->init_seq); /* swich charset, if necessary */
467 } else {
468 print (op.scheme->reset_seq); /* reset charset, if necessary */
469 printf ("\033[?25h"); /* show cursor */
470 printf ("\033[?47l\033[u"); /* primary screen, restore cursor */
471 raw_mode(0);
472 }
473 }
474
475 /* http://users.csc.calpoly.edu/~phatalsk/357/lectures/code/sigalrm.c */
476 void raw_mode(int enable) {
477 static struct termios saved_term_mode;
478 struct termios raw_term_mode;
479
480 if (enable) {
481 tcgetattr(STDIN_FILENO, &saved_term_mode);
482 raw_term_mode = saved_term_mode;
483 raw_term_mode.c_lflag &= ~(ICANON | ECHO);
484 raw_term_mode.c_cc[VMIN] = 1 ;
485 raw_term_mode.c_cc[VTIME] = 0;
486 tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw_term_mode);
487 } else {
488 tcsetattr(STDIN_FILENO, TCSAFLUSH, &saved_term_mode);
489 }
490 }
Imprint / Impressum