Faster graphics; touchscreen support
[tennix.git] / game.c
blob4ae6a1b0b82fec7a29b2035ea9ca0b1e3f90e8fc
2 /**
4 * Tennix! SDL Port
5 * Copyright (C) 2003, 2007 Thomas Perl <thp@perli.net>
6 *
7 * This program is free software; you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License
9 * as published by the Free Software Foundation; either version 2
10 * of the License, or (at your option) any later version.
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
17 * You should have received a copy of the GNU General Public License
18 * along with this program; if not, write to the Free Software
19 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
20 * MA 02110-1301, USA.
22 **/
24 #include <stdio.h>
25 #include <math.h>
27 #include "tennix.h"
28 #include "game.h"
29 #include "graphics.h"
30 #include "input.h"
31 #include "sound.h"
33 void game( bool singleplayer) {
34 GameState s = {
35 { 0, 0, 0.0, 0.0, 0.0 },
36 { 0, 0, 0, 0, 0 },
37 { GAME_X_MIN-RACKET_X_MID*2, GAME_Y_MID, 0, 0, 1, DESIRE_NORMAL, PLAYER_TYPE_HUMAN, false, 0, 0 },
38 { GAME_X_MAX+RACKET_X_MID*2, GAME_Y_MID, 0, 0, 0, DESIRE_NORMAL, PLAYER_TYPE_HUMAN, false, 0, 0 },
42 true,
43 "welcome to tennix " VERSION,
44 { 0 },
45 { 0 },
46 REFEREE_NORMAL,
48 WINNER_NONE,
49 false,
50 GR_CTT_GRASS,
51 -1,
52 { 0 },
54 false,
55 { { { 0 } } },
56 0.0,
57 SOUND_MAX,
58 0.0,
59 0.0,
63 strcpy( s.game_score_str, format_game( &s));
64 strcpy( s.sets_score_str, format_sets( &s));
66 Uint32 ot = SDL_GetTicks();
67 Uint32 nt;
68 Uint32 dt = GAME_TICKS;
69 Uint32 diff;
70 Uint32 accumulator = 0;
71 bool quit = false;
72 int x, y, z;
74 if( singleplayer) {
75 #ifdef DEBUG
76 s.player1.type = PLAYER_TYPE_AI;
77 #endif
78 s.player2.type = PLAYER_TYPE_AI;
81 game_setup_serve( &s);
83 /* smoothen n-gram */
84 for( x = 0; x<NGRAM_STEPS; x++) {
85 for( y = 0; y<NGRAM_STEPS; y++) {
86 for( z = 0; z<NGRAM_STEPS; z++) {
87 s.ngram[x][y][z] = 1;
92 while( !quit) {
93 nt = SDL_GetTicks();
94 diff = nt-ot;
95 if( diff > 2000) {
96 diff = 0;
99 accumulator += diff;
100 ot = nt;
102 while( accumulator >= dt) {
103 quit = step( &s);
104 s.time += dt;
105 accumulator -= dt;
107 if( s.was_stopped) {
108 ot = SDL_GetTicks();
109 s.was_stopped = false;
113 render( &s);
118 bool step( GameState* s) {
119 Uint8 *keys;
120 SDL_Event e;
122 if( get_phase( s) < 1.0) {
123 if( !s->ground.jump) {
124 s->play_sound = SOUND_GROUND;
126 if( IS_OUT_Y( s->ball.y)) {
127 /* out - responsibilities stay the same */
128 s->status = "out!";
129 s->play_sound = SOUND_OUT;
130 s->referee = REFEREE_OUT;
131 } else {
132 /* not out - responsibilities change */
133 s->player1.responsible = !(s->player2.responsible = !s->player2.responsible);
134 s->status = format_status( s);
135 s->referee = REFEREE_NORMAL;
138 s->ground.jump = 3;
139 s->ground.x = s->ball.x;
140 s->ground.y = s->ball.y;
141 } else {
142 if( s->ground.jump && !(s->time%5)) s->ground.jump--;
145 if( IS_OUT_X(s->ball.x) || IS_OFFSCREEN_Y(s->ball.y)) {
146 if( IS_OFFSCREEN( s->ball.x, s->ball.y)) {
147 s->player1_serves = s->player1.responsible;
149 score_game( s, s->player2.responsible);
150 strcpy( s->game_score_str, format_game( s));
151 strcpy( s->sets_score_str, format_sets( s));
153 if( s->player1.responsible) {
154 if( s->player1.type == PLAYER_TYPE_HUMAN && s->player2.type == PLAYER_TYPE_AI) {
155 s->status = "computer scores";
156 } else {
157 s->status = "player 2 scores";
159 s->referee = REFEREE_PLAYER2;
160 } else {
161 if( s->player1.type == PLAYER_TYPE_HUMAN && s->player2.type == PLAYER_TYPE_AI) {
162 s->status = "player scores";
163 } else {
164 s->status = "player 1 scores";
166 s->referee = REFEREE_PLAYER1;
169 game_setup_serve( s);
170 s->play_sound = SOUND_APPLAUSE;
171 SDL_Delay( 500);
172 start_fade();
173 s->was_stopped = true;
174 s->history_size = 0;
175 s->history_is_locked = 0;
176 s->ngram_prediction = 0.0;
177 #ifdef DEBUG
178 printf( "-- game reset --\n");
179 #endif
182 if( IS_OUT_X(s->ball.x)) {
183 if( !s->history_is_locked && s->referee != REFEREE_OUT) {
184 s->history[s->history_size] = (int)(NGRAM_STEPS*s->ball.y/HEIGHT);
185 s->history_size++;
186 if( s->history_size == 3) {
187 s->ngram[s->history[0]][s->history[1]][s->history[2]] += 10;
188 #ifdef DEBUG
189 printf( "history: %d, %d, %d\n", s->history[0], s->history[1], s->history[2]);
190 #endif
191 s->ngram_prediction = ngram_predictor( s);
192 s->history[0] = s->history[1];
193 s->history[1] = s->history[2];
194 s->history_size--;
196 s->history_is_locked = true;
198 if( s->ball.move_x <= 0 && IS_NEAR_X( s->player1.x, s->ball.x) && IS_NEAR_Y( s->player1.y, s->ball.y) && s->player1.state && s->referee != REFEREE_OUT) {
199 s->ball.x = GAME_X_MIN;
200 if( s->player1.state == PLAYER_STATE_MAX) {
201 s->ball.move_x = PLAYER_POWERSHOT;
202 } else {
203 s->ball.move_x = 2.5 + 2.0*s->player1.state/PLAYER_STATE_MAX;
205 s->ball.move_y = get_move_y( s, 1);
206 s->player2.responsible = !(s->player1.responsible = 1);
207 s->ball.jump += 1.0-2.0*(s->player1.state<5);
208 s->play_sound = SOUND_RACKET;
209 pan_sample(SOUND_RACKET, 0.4);
210 } else if( s->ball.move_x >= 0 && IS_NEAR_X( s->player2.x, s->ball.x) && IS_NEAR_Y( s->player2.y, s->ball.y) && s->player2.state && s->referee != REFEREE_OUT) {
211 s->ball.x = GAME_X_MAX;
212 if( s->player2.state == PLAYER_STATE_MAX) {
213 s->ball.move_x = -PLAYER_POWERSHOT;
214 } else {
215 s->ball.move_x = -(2.5 + 2.0*s->player2.state/PLAYER_STATE_MAX);
217 s->ball.move_y = get_move_y( s, 2);
218 s->player1.responsible = !(s->player2.responsible = 1);
219 s->ball.jump += 1.0-2.0*(s->player2.state<5);
220 s->play_sound = SOUND_RACKET;
221 pan_sample(SOUND_RACKET, 0.6);
224 } else {
225 s->history_is_locked = false;
228 /* Update ball_dest for debugging purposes */
229 get_move_y( s, 1);
230 get_move_y( s, 2);
232 s->ball.x += s->ball.move_x;
233 s->ball.y += s->ball.move_y;
235 if( s->player1.state) s->player1.state--;
236 if( s->player2.state) s->player2.state--;
238 SDL_PollEvent( &e);
239 keys = SDL_GetKeyState( NULL);
240 switch(e.type) {
241 case SDL_JOYAXISMOTION:
242 if (e.jaxis.axis == JOYSTICK_Y_AXIS) {
243 s->joystick_y = JOYSTICK_PERCENTIZE(e.jaxis.value);
244 } else if (e.jaxis.axis == JOYSTICK_X_AXIS) {
245 s->joystick_x = JOYSTICK_PERCENTIZE(e.jaxis.value);
247 break;
248 case SDL_JOYBUTTONUP: case SDL_JOYBUTTONDOWN:
249 if (e.jbutton.button == JOYSTICK_BUTTON_A) {
250 s->joystick_a = (e.jbutton.state == SDL_PRESSED);
252 break;
255 if( keys['c'] && s->time%50==0) {
256 s->court_type++;
257 if( s->court_type > GR_CTT_LAST) {
258 s->court_type = GR_CTT_FIRST;
262 if( !is_fading() && !s->is_over) {
263 if( s->player1.type == PLAYER_TYPE_HUMAN) {
264 input_human( &s->player1,
265 keys['w'] || keys[SDLK_UP] || s->joystick_y < -JOYSTICK_TRESHOLD,
266 keys['s'] || keys[SDLK_DOWN] || s->joystick_y > JOYSTICK_TRESHOLD,
267 keys['d'] || keys[SDLK_SPACE] || keys[SDLK_LCTRL] || s->joystick_a,
268 ENABLE_MOUSE,
270 } else {
271 input_ai( &s->player1, &s->ball, &s->player2, s);
274 if( s->player2.type == PLAYER_TYPE_HUMAN) {
275 input_human( &s->player2, keys['o'], keys['l'], keys['k'], false, s);
276 } else {
277 input_ai( &s->player2, &s->ball, &s->player1, s);
281 if( keys['f']) SDL_WM_ToggleFullScreen( screen);
282 if( keys['y']) SDL_SaveBMP( screen, "screenshot.bmp");
284 if( keys['p']) {
285 while( keys['p']) {
286 SDL_PollEvent( &e);
287 keys = SDL_GetKeyState( NULL);
288 SDL_Delay( 10);
290 while( keys['p'] == 0) {
291 SDL_PollEvent( &e);
292 keys = SDL_GetKeyState( NULL);
293 SDL_Delay( 10);
295 while( keys['p']) {
296 SDL_PollEvent( &e);
297 keys = SDL_GetKeyState( NULL);
298 SDL_Delay( 10);
300 s->was_stopped = true;
303 if( keys[SDLK_ESCAPE] || keys['q']) return true;
305 limit_value( &s->player1.y, PLAYER_Y_MIN, PLAYER_Y_MAX);
306 limit_value( &s->player2.y, PLAYER_Y_MIN, PLAYER_Y_MAX);
307 limit_value( &s->ball.jump, BALL_JUMP_MIN, BALL_JUMP_MAX);
309 return false;
312 void render( GameState* s) {
313 if (s->play_sound != SOUND_MAX) {
314 play_sample(s->play_sound);
315 s->play_sound = SOUND_MAX;
317 if( s->winner != WINNER_NONE) {
318 if( !s->is_over) {
319 start_fade();
320 s->is_over = true;
322 clear_screen();
323 store_screen();
324 show_sprite( GR_RACKET, 2*(s->winner-1), 4, WIDTH/2 - get_image_width( GR_RACKET)/8, HEIGHT/2 - get_image_height( GR_RACKET), 255);
325 sprintf( s->game_score_str, "player %d wins the match with %s", s->winner, format_sets( s));
326 font_draw_string( GR_DKC2_FONT, s->game_score_str, (WIDTH-font_get_string_width( GR_DKC2_FONT, s->game_score_str))/2, HEIGHT/2 + 30, s->time/20, ANIMATION_WAVE | ANIMATION_BUNGEE);
327 updatescr();
328 return;
330 if (s->old_court_type != s->court_type) {
331 clear_screen();
332 fill_image(s->court_type, 120, 120, 400, 250);
333 show_image(GR_COURT, 0, 0, 255);
334 s->old_court_type = s->court_type;
335 store_screen();
337 show_sprite( GR_REFEREE, s->referee, 4, 250, 10, 255);
338 show_image( GR_SHADOW, s->ball.x-BALL_X_MID, s->ball.y + get_phase( s) - BALL_Y_MID, 255);
340 show_sprite( GR_RACKET, (!s->player1.state), 4, s->player1.x-RACKET_X_MID, s->player1.y-RACKET_Y_MID, 255);
341 show_sprite( GR_RACKET, (!s->player2.state)+2, 4, s->player2.x-RACKET_X_MID, s->player2.y-RACKET_Y_MID, 255);
343 if( s->ground.jump) {
344 show_sprite( GR_GROUND, s->ground.jump-1, 3, s->ground.x - BALL_X_MID, s->ground.y - BALL_Y_MID, 128);
347 if( s->ball.move_x > 0) {
348 show_sprite( GR_BALL, (s->time/500)%4, 4, s->ball.x-BALL_X_MID, s->ball.y-BALL_Y_MID, 255);
349 } else if( s->ball.move_x < 0) {
350 show_sprite( GR_BALL, 3-(s->time/500)%4, 4, s->ball.x-BALL_X_MID, s->ball.y-BALL_Y_MID, 255);
351 } else {
352 show_sprite( GR_BALL, 0, 4, s->ball.x-BALL_X_MID, s->ball.y-BALL_Y_MID, 255);
356 font_draw_string( GR_DKC2_FONT, s->game_score_str, 14, 14, 0, ANIMATION_NONE);
357 font_draw_string( GR_DKC2_FONT, s->sets_score_str, (WIDTH-font_get_string_width( GR_DKC2_FONT, s->sets_score_str))-14, 14, 0, ANIMATION_NONE);
359 font_draw_string( GR_DKC2_FONT, s->status, (WIDTH-font_get_string_width( GR_DKC2_FONT, s->status))/2, HEIGHT-50, s->time/30, ANIMATION_WAVE);
361 #ifdef DEBUG
362 line_horiz( s->player1.y, 255, 0, 0);
363 line_horiz( s->player2.y, 0, 0, 255);
364 line_horiz( s->ball.y, 0, 255, 0);
366 line_vert( s->player1.x, 255, 0, 0);
367 line_vert( s->player2.x, 0, 0, 255);
368 line_vert( s->ball.x, 0, 255, 0);
370 line_horiz( s->player1.ball_dest, 255, 0, 255);
371 line_horiz( s->player2.ball_dest, 0, 255, 255);
373 line_horiz( GAME_Y_MIN, 100, 100, 100);
374 line_horiz( GAME_Y_MAX, 100, 100, 100);
375 #endif
377 updatescr();
380 void limit_value( float* value, float min, float max) {
381 if( *value < min) {
382 *value = min;
383 } else if( *value > max) {
384 *value = max;
388 float get_phase( GameState* s) {
389 float pos, fract;
390 float x, min, max, direction;
392 x = s->ball.x;
393 min = GAME_X_MIN;
394 max = GAME_X_MAX;
395 direction = s->ball.move_x;
397 pos = (direction>0)?(1-GROUND_PHASE):(GROUND_PHASE);
399 fract = (x-min)/(max-min);
401 if( fract < pos) {
402 fract = fract/pos;
403 return fabsf( cosf(PI*fract/2))*PHASE_AMP*s->ball.jump;
404 } else {
405 fract = (pos-fract)/(1-pos);
406 return fabsf( sinf(PI*fract/2))*PHASE_AMP*s->ball.jump;
410 float get_move_y( GameState* s, unsigned char player) {
411 float pct, dest, x_len, y_len;
412 float py, by, pa, move_x;
414 py = (player==1)?(s->player1.y):(s->player2.y);
415 by = s->ball.y;
416 pa = RACKET_Y_MID*2;
417 move_x = s->ball.move_x;
419 /* -1.0 .. 1.0 for racket hit position */
420 pct = (by-py)/(pa/2);
421 limit_value( &pct, -1.0, 1.0);
423 /* Y destination for ball */
424 dest = GAME_Y_MID + pct*(GAME_Y_MAX-GAME_Y_MIN);
425 if( player == 1) {
426 s->player1.ball_dest = dest;
427 } else {
428 s->player2.ball_dest = dest;
431 /* lengths for the ball's journey */
432 if( player == 1) {
433 x_len = fabsf(GAME_X_MAX - s->ball.x);
434 y_len = dest - by + MOVE_Y_SEED-rand()%MOVE_Y_SEED*2;
435 } else {
436 x_len = s->ball.x - GAME_X_MIN;
437 y_len = by - dest + MOVE_Y_SEED-rand()%MOVE_Y_SEED*2;
440 /* return the should-be value for move_y */
441 return (y_len*move_x)/(x_len);
444 void input_human( Player* player, bool up, bool down, bool hit, bool use_mouse, GameState* s) {
445 int diff = PLAYER_MOVE_Y;
446 int mb;
448 if (use_mouse) {
449 mb = SDL_GetMouseState(&(player->mouse_x), &(player->mouse_y));
450 if (mb&SDL_BUTTON(SDL_BUTTON_LEFT)) {
451 hit = true;
453 if (player->mouse_y < player->y-RACKET_Y_MID-diff) {
454 down = false;
455 up = true;
456 } else if (player->mouse_y > player->y-RACKET_Y_MID+diff) {
457 up = false;
458 down = true;
462 if (fabsf(s->joystick_y) > JOYSTICK_TRESHOLD) {
463 diff = PLAYER_MOVE_Y*fabsf(s->joystick_y)/40.0;
464 if (diff > PLAYER_MOVE_Y) {
465 diff = PLAYER_MOVE_Y;
469 if( up) {
470 player->y -= diff;
473 if( down) {
474 player->y += diff;
477 if( hit) {
478 if( !player->state && !player->state_locked) {
479 player->state = PLAYER_STATE_MAX;
480 player->state_locked = true;
482 } else {
483 player->state_locked = false;
487 void input_ai( Player* player, Ball* ball, Player* opponent, GameState* s) {
488 float fact = 1.7;
489 float target;
491 if( fabsf( player->y - ball->y) > RACKET_Y_MID*5) {
492 fact = 3.5;
495 target = GAME_Y_MID + (opponent->ball_dest - GAME_Y_MID)/5;
497 if( player->responsible) {
498 if( player->desire == DESIRE_NORMAL && !IS_NEAR_Y_AI( player->y, ball->y)) {
499 if( player->y < ball->y) {
500 player->y += fmin( 2*fact, ball->y - player->y);
501 } else if( player->y > ball->y) {
502 player->y -= fmin( 2*fact, player->y - ball->y);
506 if( (ball->move_x != 0 || IS_NEAR_Y_AI( player->y, ball->y)) && IS_NEAR_X_AI( player->x, ball->x) && !player->state && rand()%4) {
507 player->state = PLAYER_STATE_MAX;
509 } else if( ball->move_x == 0) {
510 if( player->desire == DESIRE_NORMAL && !IS_NEAR_Y_AI( player->y, target)) {
511 if( player->y < target) {
512 player->y += fmin( fact, (target-player->y)/40.0);
513 } else if( player->y > target) {
514 player->y -= fmin( fact, (player->y-target)/40.0);
517 } else if( s->ngram_prediction > 0.0) {
518 target = s->ngram_prediction*((float)HEIGHT)/((float)(NGRAM_STEPS));
519 target = GAME_Y_MID + (target-GAME_Y_MID)*1.5;
521 if( player->desire == DESIRE_NORMAL && !IS_NEAR_Y_AI( player->y, target)) {
522 if( player->y < target) {
523 player->y += fmin( fact, (target-player->y)/40.0);
524 } else if( player->y > target) {
525 player->y -= fmin( fact, (player->y-target)/40.0);
528 } else {/*
529 if( player->desire == DESIRE_NORMAL) {
530 if( !IS_NEAR_Y_AI( player->y, target)) {
531 player->y += (target - player->y)/40.0;
537 void game_setup_serve( GameState* s) {
538 s->ball.jump = 7.5;
539 s->ball.y = GAME_Y_MID;
540 s->ball.move_x = 0.0;
541 s->ball.move_y = 0.0;
543 if( s->player1_serves) {
544 s->player1.responsible = true;
545 s->player1.ball_dest = 0.0;
546 s->ball.x = GAME_X_MIN-RACKET_X_MID*1.5;
547 } else {
548 s->player1.responsible = false;
549 s->player2.ball_dest = 0.0;
550 s->ball.x = GAME_X_MAX+RACKET_X_MID*1.5;
553 s->player2.responsible = !(s->player1.responsible);
556 float ngram_predictor( GameState* s) {
557 unsigned int count = 0;
558 unsigned long sum = 0;
559 int x, y, z;
560 float result;
562 if( s->history_size < 3) {
563 return 0.0;
566 x = s->history[1];
567 y = s->history[2];
569 for( z = 0; z<NGRAM_STEPS; z++) {
570 count += s->ngram[x][y][z];
571 sum += z * s->ngram[x][y][z];
574 result = ((float)(sum))/((float)(count));
575 #ifdef DEBUG
576 printf( "predicting next = %.2f\n", result);
577 #endif
579 return result;
582 void score_game( GameState* s, bool player1_scored) {
583 Player* winner = (player1_scored)?(&(s->player1)):(&(s->player2));
584 Player* loser = (player1_scored)?(&(s->player2)):(&(s->player1));
586 if( s->current_set >= SETS_TO_WIN*2-1) {
587 return;
590 winner->game++;
591 if( loser->game < winner->game-1) {
592 if( winner->game >= 4) {
593 winner->game = loser->game = 0;
594 winner->sets[s->current_set]++;
595 /* scoring the set.. */
596 if( (winner->sets[s->current_set] == 6 && loser->sets[s->current_set] < 5) ||
597 winner->sets[s->current_set] == 7) {
598 s->current_set++;
599 s->winner = game_get_winner( s);
605 char* format_sets( GameState* s) {
606 static char sets[100];
607 static char tmp[100];
608 int i, max = s->current_set;
610 sets[0] = '\0';
612 if( s->winner != WINNER_NONE) {
613 max--;
615 for( i=0; i<=max; i++) {
616 sprintf( tmp, "%d:%d, ", s->player1.sets[i], s->player2.sets[i]);
617 strcat( sets, tmp);
620 sets[strlen(sets)-2] = '\0';
622 return sets;
625 char* format_game( GameState* s) {
626 static char game[100];
627 static const int game_scoring[] = { 0, 15, 30, 40 };
629 if( s->player1.game < 4 && s->player2.game < 4) {
630 sprintf( game, "%d - %d", game_scoring[s->player1.game], game_scoring[s->player2.game]);
631 } else if( s->player1.game > s->player2.game) {
632 strcpy( game, "advantage player 1");
633 } else if( s->player1.game < s->player2.game) {
634 strcpy( game, "advantage player 2");
635 } else {
636 strcpy( game, "deuce");
639 return game;
642 char* format_status( GameState* s) {
643 static char status[100];
644 static const char* set_names[] = { "first", "second", "third", "fourth", "fifth" };
646 sprintf( status, "%d:%d in %s set", s->player1.sets[s->current_set], s->player2.sets[s->current_set], set_names[s->current_set]);
648 return status;
651 int game_get_winner( GameState* s) {
652 int i;
653 int sets[2] = {0};
655 for( i=0; i<s->current_set; i++) {
656 if( s->player1.sets[i] > s->player2.sets[i]) {
657 sets[0]++;
658 } else {
659 sets[1]++;
663 if( sets[0] == SETS_TO_WIN) return WINNER_PLAYER1;
664 if( sets[1] == SETS_TO_WIN) return WINNER_PLAYER2;
666 return WINNER_NONE;