Ceramic Hunter – A Simple Game using the Allegro Library

Author’s Note: This is a little game that I designed as a project during the summer break from college. It’s not particularly sophisticated, nor is it especially well-programmed, but I still consider it a step, if a small one, towards what I eventually would like to be able to develop.

The software is provided under the BSD License, allowing you to modify and play with the software at your will. Several graphical resources will be required to play this game, and will be available under this note. They will have to be converted to Windows bitmap (.bmp) format in order to work with the game.

Note: The layout of this blog format doesn’t extend to the 80 columns required to show all of the source code appropriately. You can circumvent this restriction by dragging the mouse over all of the source code and copying it to a text editor; the hidden code will show up.

ammoround crosshair spentround target

/* Copyright (c) 2013, Richard A. Kiernan
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
    * Redistributions of source code must retain the above copyright
      notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright
      notice, this list of conditions and the following disclaimer in the
      documentation and/or other materials provided with the distribution.
    * Neither the name of the  nor the
      names of its contributors may be used to endorse or promote products
      derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL  BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

#include <allegro.h>
#include <stdlib.h>
#include <time.h>

/* screen definitions */
#define MODE GFX_AUTODETECT_WINDOWED
#define WIDTH 640
#define HEIGHT 480

/* colour definitions */
#define WHITE makecol(255, 255, 255)
#define TAN makecol(255, 242, 169)
#define BLACK makecol(0, 0, 0)

/* target definitions */
#define TARGET_XSIZE target_bmp->w
#define TARGET_YSIZE target_bmp->h
#define TARGET_TOPXSPEED 2

/* target bounds definitions */
#define TARGET_TOPY 40
#define TARGET_BOTTOMY SCREEN_H - 60

/* bitmap images */
BITMAP *buffer;
BITMAP *crosshair;
BITMAP *ammoround;
BITMAP *spentround;
BITMAP *target_bmp;

/* target structure */
struct type_target {
    int x, y;
    int x_speed, y_speed;
    int alive;
};

struct type_target target;

int rounds_magazine = 6, rounds_bandolier = 24;
int score = -1;
int updates = 0;

/* for limiting the rate of target creation and round firing */
clock_t last_target_time;
clock_t last_fired_time;

/* function prototypes */
void setupscreen(void);
void updatescore(void);
void drawbullets(void);
void drawbandoliercount(void);
void createtarget(void);
void updatetarget(void);
void roundfired(void);
void reloadmagazine(void);

int main(void)
{
    int mb;
    int ret;
    int n;

    /* initialise program */
    allegro_init();
    setupscreen();
    install_keyboard();
    install_mouse();
    install_timer();
    srand(time(NULL));

    /* create a secondary screen buffer */
    buffer = create_bitmap(WIDTH, HEIGHT);

    /* display title */
    textout_ex(buffer, font, "Ceramic Hunter (ESC to quit)", 0, 1, WHITE, 0);

    /* display score */
    updatescore();

    /* draw line under title & score */
    line(buffer, 0, 12, SCREEN_W, 12, TAN);

    /* display remaining round count in magazine */
    drawbullets();

    /* display remaining round count in bandolier */
    drawbandoliercount();

    /* load the mouse cursor */
    crosshair = load_bitmap("crosshair.bmp", NULL);
    set_mouse_sprite(crosshair);
    set_mouse_sprite_focus(15, 15);
    show_mouse(buffer);

    /* set up initial target */
    createtarget();

    /* main loop - continue while ESC key not pressed and rounds remaining in
     * magazine and/or bandolier */
    while (!key[KEY_ESC] && (rounds_magazine + rounds_bandolier > 0)) {
	mb = (mouse_b & 1);

	if (target.alive)  {
	    updatetarget();
	} else if ((clock() - last_target_time) 
		   / (double) CLOCKS_PER_SEC >= 2.5) {
	    createtarget();
	}

	if (mb && (clock() - last_fired_time) 
	    / (double) CLOCKS_PER_SEC > 0.5) {
	    if (rounds_magazine > 0) {
		roundfired();
	    }
	}

	if (key[KEY_R] && rounds_magazine < 6 && rounds_bandolier > 0) {
	    reloadmagazine();
	}

	blit(buffer, screen, 0, 0, 0, 0, 640, 480);
    }

    /* print final score */
    rest(500);
    for (n = 0; n < 3; n++) {
	/* flashing effects to indicate end of game */
	rectfill(screen, 0, 0, SCREEN_W, SCREEN_H, WHITE);
	rest(5);
	rectfill(screen, 0, 0, SCREEN_W, SCREEN_H, BLACK);
	rest(5);
    }
    textprintf_centre_ex(screen, font, SCREEN_W / 2, SCREEN_H / 2, WHITE, -1,
			 "Final Score: %d", score);
    textout_centre_ex(screen, font, "Press ESC to quit...",
		      SCREEN_W / 2, SCREEN_H / 2 + 10, WHITE, -1);
    while (!key[KEY_ESC])
	;

    /* exit program */
    allegro_exit();
    return 0;
}
END_OF_MAIN();

void setupscreen(void)
{
    int ret;
    set_color_depth(24);
    if ((ret = set_gfx_mode(MODE, WIDTH, HEIGHT, 0, 0)) != 0) {
	allegro_message("%s", allegro_error);
	return;
    }
}

void updatescore(void)
{
    /* update and display the score */
    score++;
    textprintf_right_ex(buffer, font, SCREEN_W - 5, 1, WHITE, 0,
			"SCORE: %d", score);
}

void drawbullets(void)
{
    int n;

    if (ammoround == NULL) {
	ammoround = load_bitmap("ammoround.bmp", NULL);
    }
    if (spentround == NULL) {
	spentround = load_bitmap("spentround.bmp", NULL);
    }

    for (n = 0; n < rounds_magazine; n++) { 	draw_sprite(buffer, ammoround, n * ammoround->w,
		    SCREEN_H - ammoround->h);
    }

    for (n; n < 6; n++) { 	draw_sprite(buffer, spentround, n * spentround->w,
		    SCREEN_H - spentround->h);
    }
}

void drawbandoliercount(void)
{
    textprintf_right_ex(buffer, font, SCREEN_W - 5, SCREEN_H - 8, WHITE, 0,
			"BANDOLIER: %2d", rounds_bandolier);
}

void createtarget(void)
{
    if (target_bmp == NULL) {
	target_bmp = load_bitmap("target.bmp", NULL);
    }
    target.alive = 1;
    target.x = rand() % 2 ? 0 : SCREEN_W - TARGET_XSIZE;
    target.y = (rand() % 360) + TARGET_TOPY;
    target.x_speed = target.x == 0 ?
	TARGET_TOPXSPEED : -(TARGET_TOPXSPEED);

    /* if target in top or bottom third of screen, it might have a y speed
     * component; otherwise, y speed is 0. */
    if (target.y < (SCREEN_H / 3)) {
 	target.y_speed = rand() & 1;
    } else if (target.y > (SCREEN_H - (SCREEN_H / 3))) {
	target.y_speed = -(rand() & 1);
    } else {
	target.y_speed = 0;
    }

    /* if target has a y speed component, x speed component might be
     * decreased/increased by 1 to change angle of entry */
    if (target.y_speed != 0) {
	target.x_speed += target.x_speed > 1 ? -(rand() & 1) : rand() & 1;
    }

    last_target_time = clock();
}

void updatetarget(void)
{
    int tx, ty;

    tx = target.x;
    ty = target.y;

    /* hide mouse pointer */
    scare_mouse();

    /* clear target movement area in buffer if updates > 20
     * to clear away graphical glitches caused by mouse movement */
    if (++updates > 20) {
	updates = 0;
	rectfill(buffer, 0, TARGET_TOPY, SCREEN_W, TARGET_BOTTOMY, BLACK);
    }

    /* erase target */
    rectfill(buffer, tx, ty, tx + TARGET_XSIZE, ty + TARGET_YSIZE, BLACK);

    target.x += target.x_speed;
    target.y += target.y_speed;

    if ((target.x > 0 && target.x + TARGET_XSIZE < SCREEN_W) &&
 	(target.y > TARGET_TOPY && target.y + TARGET_YSIZE < TARGET_BOTTOMY)) {
 	/* redraw target on buffer */
 	draw_sprite(buffer, target_bmp, target.x, target.y);
    } else {
 	target.alive = 0;
    }
    /* show mouse pointer */
    unscare_mouse();
}

void roundfired(void) {
     int n;
     --rounds_magazine;
     last_fired_time = clock();
     if ((mouse_x >= target.x && mouse_x < target.x + TARGET_XSIZE) &&
 	(mouse_y >= target.y && mouse_y < target.y + TARGET_YSIZE)) {
	/* destroy target */
	target.alive = 0;
        updatescore();
	rectfill(buffer, target.x, target.y, target.x + TARGET_XSIZE,
		 target.y + TARGET_YSIZE, BLACK);
	/* draw puff of smoke */
	for (n = 0; n < 100; n++) {
 	    putpixel(screen, target.x + (rand() % TARGET_XSIZE),
 		     target.y + (rand() % TARGET_XSIZE), WHITE);
 	}
 	rest(30);
     }
     drawbullets();
}

void reloadmagazine(void) {
     int temp;
     temp = rounds_magazine;
     if (rounds_bandolier > 6 || rounds_bandolier + rounds_magazine > 6) {
	rounds_magazine = 6;
	rounds_bandolier = (rounds_bandolier + temp) - rounds_magazine;
    } else {
	rounds_magazine = rounds_bandolier + temp;
	rounds_bandolier = 0; 
    }
    drawbullets();
    drawbandoliercount();
}