Information wants to be free...

Old Curses Game

Here is a re-release of an old game I programmed a long time ago, called simply "cgame". It was one of my first "real" projects with C and the curses library. The code has gone through heavy re-factoring for readability, so it looks almost nothing like the original, but the game mechanics remains the same.

Here's what it looks like:

Screenshot of cgame.


The rules are simple, collect all the "money" and avoid being captured by the yellow 'M'. The idea behind the 'M' actually came from a game on the Commodore 64, called "Escape MCP", that I recalled playing as a kid. Here's what the main screen looks like:

Screenshot of Escape MCP.


Finally, the code:

#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <ncurses.h>

#define GAME_ROWS 24
#define GAME_COLS 80

#define AMOUNT_OF_MONEY  20
#define AMOUNT_OF_MINES  10
#define AMOUNT_OF_CRATES 5

#define PAIR_YELLOW 1
#define PAIR_CYAN 2

typedef enum {
  DIRECTION_NONE,
  DIRECTION_LEFT,
  DIRECTION_RIGHT,
  DIRECTION_UP,
  DIRECTION_DOWN,
} direction_t;

typedef enum {
  MOVE_OK,
  MOVE_BLOCKED,
  MOVE_ON_MINE,
  MOVE_ON_MONEY,
  MOVE_ON_ENEMY,
} move_t;

typedef struct object_s {
  int y, x;
  char symbol;
  int attributes;
  int taken;
} object_t;

static object_t player;
static object_t enemy;
static object_t money[AMOUNT_OF_MONEY];
static object_t mines[AMOUNT_OF_MINES];
static object_t crates[AMOUNT_OF_CRATES];

static int money_collected;
static int steps_taken;
static int enemy_idle_count;

static void draw_object(object_t *object)
{
  if (object->attributes != 0 && has_colors())
    wattrset(stdscr, object->attributes);
  mvaddch(object->y, object->x, object->symbol);
  if (object->attributes != 0 && has_colors())
    wattrset(stdscr, A_NORMAL);
}

static void find_empty_spot(int *y, int *x)
{
  do {
    move((random() % (GAME_ROWS - 2)) + 1, (random() % (GAME_COLS - 2)) + 1);
  } while ((inch() & A_CHARTEXT) != ' ');
  getyx(stdscr, *y, *x);
}

static void init_game(void)
{
  int i;

  money_collected  = 0;
  steps_taken      = 0;
  enemy_idle_count = 0;

  srandom(time(NULL));

  player.symbol = '@';
  player.attributes = 0;
  find_empty_spot(&player.y, &player.x);
  draw_object(&player);

  enemy.symbol = 'M';
  enemy.attributes = A_BOLD | COLOR_PAIR(PAIR_YELLOW);
  find_empty_spot(&enemy.y, &enemy.x);
  draw_object(&enemy);

  for (i = 0; i < AMOUNT_OF_MONEY; i++) {
    money[i].symbol = '$';
    money[i].attributes = A_BOLD;
    money[i].taken = 0;
    find_empty_spot(&money[i].y, &money[i].x);
    draw_object(&money[i]);
  }

  for (i = 0; i < AMOUNT_OF_MINES; i++) {
    mines[i].symbol = '*';
    mines[i].attributes = COLOR_PAIR(PAIR_CYAN);
    find_empty_spot(&mines[i].y, &mines[i].x);
    draw_object(&mines[i]);
  }

  for (i = 0; i < AMOUNT_OF_CRATES; i++) {
    crates[i].symbol = '#';
    crates[i].attributes = COLOR_PAIR(PAIR_YELLOW);
    find_empty_spot(&crates[i].y, &crates[i].x);
    draw_object(&crates[i]);
  }
}

static direction_t direction_to_player(void)
{
  int y, x;

  y = enemy.y - player.y;
  x = enemy.x - player.x;

  if ((x < 0) && ((x <= (-y)) || (x >= y)) &&
     ((x <= y)                || (x >= (-y))))
    return DIRECTION_RIGHT;
  if ((x > 0) && ((x <= y) || (x >= (-y))) &&
     ((x <= (-y))          || (x >= y)))
    return DIRECTION_LEFT;
  if (y > 0)
    return DIRECTION_UP;
  if (y < 0)
    return DIRECTION_DOWN;

  return DIRECTION_NONE;
}

static direction_t random_direction(void)
{
  switch (random() % 4) {
  case 0:
    return DIRECTION_LEFT;
  case 1:
    return DIRECTION_RIGHT;
  case 2:
    return DIRECTION_UP;
  case 3:
    return DIRECTION_DOWN;
  }
  return DIRECTION_NONE;
}

static void move_object_actual(object_t *object, direction_t direction)
{
  switch (direction) {
  case DIRECTION_LEFT:
    object->x--;
    break;
  case DIRECTION_RIGHT:
    object->x++;
    break;
  case DIRECTION_UP:
    object->y--;
    break;
  case DIRECTION_DOWN:
    object->y++;
    break;
  default:
    break;
  }
}

static move_t move_object_check(object_t *object, direction_t direction)
{
  int i, hit_y, hit_x;
  char hit_object;

  switch (direction) {
  case DIRECTION_LEFT:
    hit_y = object->y;
    hit_x = object->x - 1;
    break;
  case DIRECTION_RIGHT:
    hit_y = object->y;
    hit_x = object->x + 1;
    break;
  case DIRECTION_UP:
    hit_y = object->y - 1;
    hit_x = object->x;
    break;
  case DIRECTION_DOWN:
    hit_y = object->y + 1;
    hit_x = object->x;
    break;
  case DIRECTION_NONE:
  default:
    return MOVE_BLOCKED;
  }
  hit_object = mvinch(hit_y, hit_x) & A_CHARTEXT;

  if (object->symbol == '@') { /* Player. */
    switch (hit_object) {
    case '*':
      move_object_actual(object, direction);
      return MOVE_ON_MINE;
    case '$':
      move_object_actual(object, direction);
      return MOVE_ON_MONEY;
    case 'M':
      move_object_actual(object, direction);
      return MOVE_ON_ENEMY;
    case '#':
      for (i = 0; i < AMOUNT_OF_CRATES; i++) {
        if (hit_y == crates[i].y && hit_x == crates[i].x) {
          if (move_object_check(&crates[i], direction) == MOVE_OK) {
            move_object_actual(object, direction);
            return MOVE_OK;
          } else {
            return MOVE_BLOCKED;
          }
        }
      }
      return MOVE_BLOCKED;
    case ' ':
      move_object_actual(object, direction);
      return MOVE_OK;
    default:
      return MOVE_BLOCKED;
    }

  } else if (object->symbol == 'M') { /* Enemy. */
    if (hit_object == ' ' || hit_object == '@') {
      move_object_actual(object, direction);
      return MOVE_OK;
    } else {
      return MOVE_BLOCKED;
    }

  } else { /* Crates, etc. */
    if (hit_object == ' ') {
      move_object_actual(object, direction);
      return MOVE_OK;
    } else {
      return MOVE_BLOCKED;
    }
  }
}

static void draw_screen(void)
{
  int i;

  erase();

  box(stdscr, '|', '-');

  for (i = 0; i < AMOUNT_OF_MONEY; i++)
    if (! money[i].taken)
      draw_object(&money[i]);
  for (i = 0; i < AMOUNT_OF_MINES; i++)
    draw_object(&mines[i]);
  for (i = 0; i < AMOUNT_OF_CRATES; i++)
    draw_object(&crates[i]);
  draw_object(&player);
  draw_object(&enemy);

  move(player.y, player.x);

  refresh();
}

int main(void)
{
  int i, maxx, maxy, direction;
  char *done = NULL;

  initscr();
  if (has_colors()) {
    start_color();
    init_pair(PAIR_YELLOW, COLOR_YELLOW, COLOR_BLACK);
    init_pair(PAIR_CYAN,   COLOR_CYAN,   COLOR_BLACK);
  }
  noecho();
  keypad(stdscr, TRUE);
  getmaxyx(stdscr, maxy, maxx);

  if (maxy != GAME_ROWS || maxx != GAME_COLS) {
    endwin();
    fprintf(stderr, "Terminal must be %dx%d!\n", GAME_ROWS, GAME_COLS);
    return 1;
  }

  init_game();

  while (done == NULL) {
    draw_screen();

    /* Get player input. */
    direction = DIRECTION_NONE;
    switch (getch()) {
    case KEY_LEFT:
      direction = DIRECTION_LEFT;
      break;

    case KEY_RIGHT:
      direction = DIRECTION_RIGHT;
      break;

    case KEY_UP:
      direction = DIRECTION_UP;
      break;

    case KEY_DOWN:
      direction = DIRECTION_DOWN;
      break;

    case KEY_RESIZE:
      done = "Window resize detected, aborted...";
      break;

    case 'q':
    case 'Q':
    case '\e':
      done = "Quitting...";
      break;

    default:
      break;
    }

    /* Move player, and check for collisions. */
    switch (move_object_check(&player, direction)) {
    case MOVE_OK:
      steps_taken++;
      break;

    case MOVE_BLOCKED:
      break;

    case MOVE_ON_MINE:
      done = "You were killed by a mine!";
      break;

    case MOVE_ON_MONEY:
      for (i = 0; i < AMOUNT_OF_MONEY; i++) {
        if (player.y == money[i].y && player.x == money[i].x) {
          money_collected++;
          money[i].taken = 1;
        }
      }
      break;

    case MOVE_ON_ENEMY:
      done = "You moved onto the enemy!";
      break;
    }

    draw_screen(); /* To update the positions of the objects, not visible. */

    /* Move enemy, and check for collision with player. */
    if (move_object_check(&enemy, direction_to_player()) == MOVE_BLOCKED) {
      enemy_idle_count++;
      if (enemy_idle_count > 3) {
        if (move_object_check(&enemy, random_direction()) == MOVE_OK)
          enemy_idle_count = 0;
      }
    }

    if (player.y == enemy.y && player.x == enemy.x) {
      done = "You were killed by the enemy!";
      break;
    }

    if (money_collected == AMOUNT_OF_MONEY)
      done = "Well done! all money collected!";
  }

  draw_screen();
  endwin();

  fprintf(stderr, "%s\n", done);
  fprintf(stderr, "You collected %d$ of %d$.\n",
    money_collected, AMOUNT_OF_MONEY);
  fprintf(stderr, "You walked %d step%s.\n", 
    steps_taken, (steps_taken == 1) ? "" : "s");

  return 0;
}
          


Topic: Scripts and Code, by Kjetil @ 06/11-2011, Article Link