Kjetil's Information Center: A Blog About My Projects

Airline Management Simulator

I made a simple airline management simulator game as part of studying how the GNU Readline library works with regards to command completion. This means the game is based around a CLI which offers tab completion.

The goal of the game is to reach a certain amount of cash after buying different airplanes and setting up routes between cities at different ticket prices. Most of the data, like the cities, are randomly generated on startup. The challenge lies in the hidden information that can only be found through trial and error, like finding which cities have citizens that are willing to pay for profitable ticket prices. I realized that it is very hard to balance simulation games, so this game as well might be too hard or too easy, I am not sure.

What it looks like:

Screenshot airline management simulator.


Anyway, here is the source code, try to compile it with: gcc -lreadline -lncurses -lm -Wall

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
#include <stdarg.h>
#include <string.h>
#include <time.h>
#include <math.h>
#include <inttypes.h>

#include <readline/readline.h>
#include <readline/history.h>



#define CITY_MAX 24
#define AIRPLANE_MAX 96

#define CITY_X_MAX 32768 /* Kilometers */
#define CITY_Y_MAX 32768 /* Kilometers */
#define CITY_DISTANCE_MINIMUM 500

#define COMMAND_NAME_MAX 10
#define COMMAND_ARG_MAX 4
#define COMMAND_ARG_NAME_MAX 16

#define GAME_CASH_START 200000
#define GAME_CASH_GOAL 1000000000
#define GAME_RED_DAYS_MAX 10



typedef enum {
  COMMAND_ARG_TYPE_NONE = -1,
  COMMAND_ARG_TYPE_PRICE,
  COMMAND_ARG_TYPE_CITY_NAME,
  COMMAND_ARG_TYPE_CITY_SRC,
  COMMAND_ARG_TYPE_CITY_DST,
  COMMAND_ARG_TYPE_AIRPLANE_ID,
  COMMAND_ARG_TYPE_AIRPLANE_TYPE,
  COMMAND_ARG_TYPE_MAX,
} command_arg_type_t;

typedef char *(*command_arg_completion_function_t)(const char *, int);

typedef struct command_arg_data_s {
  char name[6];
  bool optional;
  command_arg_completion_function_t function;
} command_arg_data_t;

typedef void (*command_function_t)(int, const char *[]);

typedef struct command_s {
  char name[COMMAND_NAME_MAX];
  command_function_t function;
  command_arg_type_t arg[COMMAND_ARG_MAX];
} command_t;

typedef struct city_s {
  char name[4]; /* Airport Code! */
  uint16_t x;
  uint16_t y;
  uint32_t demand[CITY_MAX];
  uint8_t size;
  uint8_t stinginess;
} city_t;

typedef enum {
  AIRPLANE_TYPE_INVALID = -1,
  AIRPLANE_TYPE_SMALL,
  AIRPLANE_TYPE_MEDIUM,
  AIRPLANE_TYPE_LARGE,
  AIRPLANE_TYPE_MAX,
} airplane_type_t;

typedef struct airplane_s {
  bool exists;
  bool parked;
  int src;
  int dst;
  uint32_t ticket_price;
  airplane_type_t type;
} airplane_t;

typedef struct airplane_data_s {
  char name[8];
  uint32_t capacity;
  uint32_t price;
  double fuel_consumption_factor;
  uint32_t upkeep_cost;
} airplane_data_t;

typedef struct game_state_s {
  bool loop_running;
  uint32_t day;
  int64_t cash;
  uint32_t current_fuel_price;
  uint32_t nominal_fuel_price;
  uint8_t days_in_the_red;
} game_state_t;



static void game_cycle(void);

static void function_airplane(int argc, const char *argv[]);
static void function_buy(int argc, const char *argv[]);
static void function_city(int argc, const char *argv[]);
static void function_help(int argc, const char *argv[]);
static void function_park(int argc, const char *argv[]);
static void function_quit(int argc, const char *argv[]);
static void function_route(int argc, const char *argv[]);
static void function_sell(int argc, const char *argv[]);
static void function_status(int argc, const char *argv[]);
static void function_wait(int argc, const char *argv[]);
#ifdef DEBUG_COMMAND
static void function_debug(int argc, const char *argv[]);
#endif /* DEBUG_COMMAND */

static char *command_arg_completion_city(const char *text, int state);
static char *command_arg_completion_airplane_id(const char *text, int state);
static char *command_arg_completion_airplane_type(const char *text, int state);



static command_t commands[] = {
  {"airplane", function_airplane, {-1,-1,-1,-1}},
  {"buy",      function_buy,      {COMMAND_ARG_TYPE_AIRPLANE_TYPE,-1,-1,-1}},
  {"city",     function_city,     {COMMAND_ARG_TYPE_CITY_NAME,-1,-1,-1}},
  {"help",     function_help,     {-1,-1,-1,-1}},
  {"park",     function_park,     {COMMAND_ARG_TYPE_AIRPLANE_ID,-1,-1,-1}},
  {"quit",     function_quit,     {-1,-1,-1,-1}},
  {"route",    function_route,    {COMMAND_ARG_TYPE_AIRPLANE_ID,
                                   COMMAND_ARG_TYPE_CITY_SRC,
                                   COMMAND_ARG_TYPE_CITY_DST,
                                   COMMAND_ARG_TYPE_PRICE}},
  {"sell",     function_sell,     {COMMAND_ARG_TYPE_AIRPLANE_ID,-1,-1,-1}},
  {"status",   function_status,   {-1,-1,-1,-1}},
  {"wait",     function_wait,     {-1,-1,-1,-1}},
#ifdef DEBUG_COMMAND
  {"debug",    function_debug,    {-1,-1,-1,-1}},
#endif /* DEBUG_COMMAND */
};

#define COMMANDS_MAX (sizeof(commands) / sizeof(commands[0]))

static command_arg_data_t command_arg_data[COMMAND_ARG_TYPE_MAX] = {
  {"price", false, NULL},
  {"city",  true,  command_arg_completion_city},
  {"src",   false, command_arg_completion_city},
  {"dst",   false, command_arg_completion_city},
  {"id",    false, command_arg_completion_airplane_id},
  {"type",  false, command_arg_completion_airplane_type},
};

static airplane_data_t airplane_data[AIRPLANE_TYPE_MAX] = {
  {"small",   50,  50000, 0.5,  500},
  {"medium", 150, 150000, 1.0, 1000},
  {"large",  400, 400000, 2.0, 5000},
};



static game_state_t game;
static city_t city[CITY_MAX];
static airplane_t airplane[AIRPLANE_MAX];



static int city_distance(int city1_index, int city2_index)
{
  /* Pythagorean theorem */
  double a = abs(city[city1_index].x - city[city2_index].x);
  double b = abs(city[city1_index].y - city[city2_index].y);
  double c = sqrt((a * a) + (b * b));
  return (int)c;
}



static void city_generate(void)
{
  for (int i = 0; i < CITY_MAX; i++) {

    /* Generate uniqe name. */
    bool name_unique = false;
    while (! name_unique) {
      name_unique = true;
      city[i].name[0] = 0x41 + random() % 26;
      city[i].name[1] = 0x41 + random() % 26;
      city[i].name[2] = 0x41 + random() % 26;
      city[i].name[3] = '\0';
      for (int j = 0; j < i; j++) {
        if (0 == strncmp(city[i].name, city[j].name, strlen(city[i].name))) {
          name_unique = false;
        }
      }
    }

    /* Generate location, but at a minimum distance from others. */
    bool distance_ok = false;
    while (! distance_ok) {
      distance_ok = true;
      city[i].x = random() % CITY_X_MAX;
      city[i].y = random() % CITY_Y_MAX;
      for (int j = 0; j < i; j++) {
        if (city_distance(i, j) < CITY_DISTANCE_MINIMUM) {
          distance_ok = false;
        }
      }
    }

    city[i].size       = 1 + (random() % 255);
    city[i].stinginess = 1 + (random() % 255);
  }

  /* Calculate base demand when all cities are setup. */
  for (int i = 0; i < CITY_MAX; i++) {
    for (int j = 0; j < CITY_MAX; j++) {
      city[i].demand[j] = city[i].size + city[j].size;
      city[i].demand[j] += random() % 64;
    }
  }
}



static int city_index_by_name(const char *city_name)
{
  for (int i = 0; i < CITY_MAX; i++) {
    if (0 == strncmp(city[i].name, city_name, strlen(city[i].name))) {
      return i;
    }
  }
  return -1;
}



static char *city_size_description(int city_index)
{
  uint8_t size = city[city_index].size;

  if (size > 224) {
    return "huge";
  } else if (size > 192) {
    return "large";
  } else if (size > 128) {
    return "medium";
  } else if (size > 64) {
    return "small";
  } else {
    return "tiny";
  }
}



static airplane_type_t airplane_type_by_name(const char *type_name)
{
  for (int i = 0; i < AIRPLANE_TYPE_MAX; i++) {
    if (0 == strncmp(airplane_data[i].name, type_name,
      strlen(airplane_data[i].name))) {
      return i;
    }
  }
  return -1;
}



static void function_quit(int argc, const char *argv[])
{
  game.loop_running = false;
}



static void function_help(int argc, const char *argv[])
{
  for (int i = 0; i < COMMANDS_MAX; i++) {
    printf("%10s ", commands[i].name);
    for (int j = 0; j < COMMAND_ARG_MAX; j++) {
      command_arg_type_t type = commands[i].arg[j];
      if (type == COMMAND_ARG_TYPE_NONE) {
        break;
      }
      if (command_arg_data[type].optional) {
        printf("[");
      } else {
        printf("<");
      }
      printf("%s", command_arg_data[type].name);
      if (command_arg_data[type].optional) {
        printf("] ");
      } else {
        printf("> ");
      }
    }
    printf("\n");
  }
}



static void function_city(int argc, const char *argv[])
{
  if (argc == 1) {
    for (int i = 0; i < CITY_MAX; i++) {
      /* Convert X,Y to Latitude/Longitude. */
      char latitude_direction, longitude_direction;
      double latitude_degrees, longitude_degrees;
      latitude_degrees =  ((double)city[i].x / (double)CITY_X_MAX) * 180;
      longitude_degrees = ((double)city[i].y / (double)CITY_Y_MAX) * 180;
      if (latitude_degrees > 90.0) {
        latitude_direction = 'E';
        latitude_degrees = latitude_degrees - 90.0;
      } else {
        latitude_direction = 'W';
        latitude_degrees = 90.0 - latitude_degrees;
      }
      if (longitude_degrees > 90.0) {
        longitude_direction = 'N';
        longitude_degrees = longitude_degrees - 90.0;
      } else {
        longitude_direction = 'S';
        longitude_degrees = 90.0 - longitude_degrees;
      }
      printf("%s @ %4.1f° %c, %4.1f° %c (%s city)\n", city[i].name,
        longitude_degrees, longitude_direction,
        latitude_degrees, latitude_direction,
        city_size_description(i));
    }

  } else {

    int index = city_index_by_name(argv[1]);
    if (index < 0) {
      printf("City does not exist!\n");
      return;
    }

    for (int i = 0; i < CITY_MAX; i++) {
      if (index == i) {
        continue;
      }
      printf("%s -> %s   Demand: %d\n",
        city[index].name,
        city[i].name,
        city[index].demand[i]);
    }
  }
}



static void function_airplane(int argc, const char *argv[])
{
  int airplanes = 0;

  for (int i = 0; i < AIRPLANE_MAX; i++) {
    if (airplane[i].exists) {
      printf("#%d   ", i + 1);
      printf("Type: %-6s   ", airplane_data[airplane[i].type].name);
      if (airplane[i].parked) {
        printf("Parked\n");
      } else {
        printf("At: %s   Going To: %s   Charging: $%d\n",
          city[airplane[i].src].name,
          city[airplane[i].dst].name,
          airplane[i].ticket_price);
      }
      airplanes++;
    }
  }

  if (0 == airplanes) {
    printf("No airplanes owned, buy one...\n");
  }
}



static void function_buy(int argc, const char *argv[])
{
  if (argc != 2) {
    printf("Please specify airplane type!\n");
    printf("Valid types are: ");
    for (int i = 0; i < AIRPLANE_TYPE_MAX; i++) {
      printf("%s ", airplane_data[i].name);
    }
    printf("\n");
    return;
  }

  airplane_type_t type = airplane_type_by_name(argv[1]);
  if (AIRPLANE_TYPE_INVALID == type) {
    printf("Invalid airplane type specified!\n");
    return;
  }

  if (game.cash < airplane_data[type].price) {
    printf("Not enough cash!\n");
    return;
  }

  for (int i = 0; i < AIRPLANE_MAX; i++) {
    if (! airplane[i].exists) {
      airplane[i].type = type;
      airplane[i].exists = true;
      airplane[i].parked = true;
      airplane[i].ticket_price = 0;
      game.cash -= airplane_data[type].price;

      printf("Airplane #%d with capacity %d bought for $%d.\n", i + 1,
        airplane_data[type].capacity, airplane_data[type].price);
      game_cycle();
      return;
    }
  }

  printf("Cannot buy more airplanes!\n");
}



static void function_route(int argc, const char *argv[])
{
  if (argc != 5) {
    printf("Missing arguments!\n");
    return;
  }

  int airplane_index = atoi(argv[1]);
  airplane_index--; /* ID to index */
  if (airplane_index < 0 || airplane_index >= AIRPLANE_MAX) {
    printf("Invalid airplane ID!\n");
    return;
  }

  if (false == airplane[airplane_index].exists) {
    printf("Airplane #%d does not exist!\n", airplane_index + 1);
    return;
  }

  if (false == airplane[airplane_index].parked) {
    printf("Airplane #%d is in operation (not parked)!\n", airplane_index + 1);
    return;
  }

  int src_index = city_index_by_name(argv[2]);
  if (src_index < 0) {
    printf("Source city does not exist!\n");
    return;
  }

  int dst_index = city_index_by_name(argv[3]);
  if (dst_index < 0) {
    printf("Destination city does not exist!\n");
    return;
  }

  int price = atoi(argv[4]);
  if (price <= 0) {
    printf("Price is invalid!\n");
    return;
  }

  airplane[airplane_index].parked = false;
  airplane[airplane_index].src = src_index;
  airplane[airplane_index].dst = dst_index;
  airplane[airplane_index].ticket_price = price;

  printf("Airplane #%d now flying from %s to %s for $%d.\n",
    airplane_index + 1, city[src_index].name, city[dst_index].name, price);
  game_cycle();
}



static void function_park(int argc, const char *argv[])
{
  if (argc != 2) {
    printf("Please specify airplane ID!\n");
    return;
  }

  int airplane_index = atoi(argv[1]);
  airplane_index--; /* ID to index */
  if (airplane_index < 0 || airplane_index >= AIRPLANE_MAX) {
    printf("Invalid airplane ID!\n");
    return;
  }

  if (false == airplane[airplane_index].exists) {
    printf("Airplane #%d does not exist!\n", airplane_index + 1);
    return;
  }

  if (true == airplane[airplane_index].parked) {
    printf("Airplane #%d already parked!\n", airplane_index + 1);
    return;
  }

  airplane[airplane_index].parked = true;

  printf("Airplane #%d parked.\n", airplane_index + 1);
  game_cycle();
}



static void function_status(int argc, const char *argv[])
{
  int airplanes = 0;

  for (int i = 0; i < AIRPLANE_MAX; i++) {
    if (airplane[i].exists) {
      airplanes++;
    }
  }

  printf("Day        : %d\n", game.day);
  printf("Cash       : $%"PRIi64"\n", game.cash);
  printf("Airplanes  : %d\n", airplanes);
  printf("Fuel price : $%d per kilometer\n", game.current_fuel_price);

  for (int i = 0; i < 3; i++) {
    printf("\n");
    printf("Data for '%s' airplane:\n", airplane_data[i].name);
    printf("  Capacity         : %d passengers\n", airplane_data[i].capacity);
    printf("  Purchase price   : $%d\n", airplane_data[i].price);
    printf("  Upkeep cost      : $%d\n", airplane_data[i].upkeep_cost);
    printf("  Fuel consumption : %d%%\n", 
      (int)(airplane_data[i].fuel_consumption_factor * 100));
  }
}



static void function_sell(int argc, const char *argv[])
{
  if (argc != 2) {
    printf("Please specify airplane ID!\n");
    return;
  }

  int airplane_index = atoi(argv[1]);
  airplane_index--; /* ID to index */
  if (airplane_index < 0 || airplane_index >= AIRPLANE_MAX) {
    printf("Invalid airplane ID!\n");
    return;
  }

  if (false == airplane[airplane_index].exists) {
    printf("Airplane #%d does not exist!\n", airplane_index + 1);
    return;
  }

  if (false == airplane[airplane_index].parked) {
    printf("Airplane #%d is in operation (not parked)!\n", airplane_index + 1);
    return;
  }

  airplane[airplane_index].exists = false;
  game.cash += airplane_data[airplane[airplane_index].type].price;

  printf("Airplane #%d sold for $%d.\n", airplane_index + 1,
    airplane_data[airplane[airplane_index].type].price);
  game_cycle();
}



static void function_wait(int argc, const char *argv[])
{
  game_cycle();
}



#ifdef DEBUG_COMMAND
static void function_debug(int argc, const char *argv[])
{
  printf("Raw City Data:\n");
  for (int i = 0; i < CITY_MAX; i++) {
    printf("%s @ %05hu,%05hu, Size: %d, Stinginess: %d\n",
      city[i].name, city[i].x, city[i].y, city[i].size, city[i].stinginess);
  }

  printf("\n");
  printf("Distances:\n");
  printf("     |");
  for (int i = 0; i < CITY_MAX; i++) {
    printf(" %s |", city[i].name);
  }
  printf("\n");

  for (int i = 0; i < CITY_MAX; i++) {
    printf(" %s |", city[i].name);
    for (int j = 0; j < CITY_MAX; j++) {
      printf("%5d|", city_distance(i, j));
    }
    printf("\n");
  }

  printf("\n");
  printf("Nominal Fuel Price: %d\n", game.nominal_fuel_price);
}
#endif /* DEBUG_COMMAND */



static void game_fluctuate(uint32_t *value,
  int32_t variance_start, int32_t variance_end,
  uint32_t absolute_minimum, uint32_t absolute_maximum)
{
  /* Conversion to signed integers to handle negative values. */
  int32_t variance = (random() % ((variance_end - variance_start) + 1));
  variance += variance_start;
  int32_t new_value = *value;
  new_value += variance;
  if (new_value < (int32_t)absolute_minimum) {
    new_value = absolute_minimum;
  } else if (new_value > (int32_t)absolute_maximum) {
    new_value = absolute_maximum;
  }
  *value = new_value;
}



static void game_cycle(void)
{
  /* Fly airplanes according to routes. */
  for (int i = 0; i < AIRPLANE_MAX; i++) {
    if (airplane[i].exists) {
      if (! airplane[i].parked) {

        /* Calculate passengers willing to fly! */
        int distance = city_distance(airplane[i].src, airplane[i].dst);
        int demand = city[airplane[i].src].demand[airplane[i].dst];

        /* Nominal ticket price is based on having a full flight with a
           medium size airplane with the nominal fuel price
           giving exactly zero profits. */
        double nominal_price = ((distance * game.nominal_fuel_price) /
          airplane_data[AIRPLANE_TYPE_MEDIUM].capacity);

        /* The stinginess of the city gives an additional factor. */
        double stinginess = city[airplane[i].src].stinginess / 32.0;

        /* Passengers are divided into five groups of different size. */
        int group[5];
        group[0] = demand / 20; /*  5% */
        group[1] = demand / 5;  /* 20% */
        group[2] = demand / 2;  /* 50% */
        group[3] = demand / 5;  /* 20% */
        group[4] = demand / 20; /*  5% */

        /* The five groups accepts different prices. */
        double accepts_price[5];
        accepts_price[0] = nominal_price * 0.70 * stinginess;
        accepts_price[1] = nominal_price * 0.95 * stinginess;
        accepts_price[2] = nominal_price * 1.05 * stinginess;
        accepts_price[3] = nominal_price * 1.15 * stinginess;
        accepts_price[4] = nominal_price * 1.30 * stinginess;

#ifdef DEBUG_DEMAND
        printf("%s -> %s, Nominal: $%.2f, Stinginess: %.2f\n",
          city[airplane[i].src].name, city[airplane[i].dst].name,
          nominal_price, stinginess);
        for (int j = 0; j < 5; j++) {
          printf("  Group #%d of %d people accepts anything below $%.0f\n",
            j, group[j], accepts_price[j]);
        }
#endif /* DEBUG_DEMAND */

        int passengers = 0;
        for (int j = 0; j < 5; j++) {
          if (airplane[i].ticket_price < accepts_price[j]) {
            passengers += group[j];
          }
        }

        if (passengers > airplane_data[airplane[i].type].capacity) {
          passengers = airplane_data[airplane[i].type].capacity;
        }

        printf("Airplane #%d flew %d passenger%s from %s to %s\n", i + 1,
          passengers, (passengers == 1) ? "" : "s",
          city[airplane[i].src].name, city[airplane[i].dst].name);

        /* Swap source and destination. */
        int swap = airplane[i].src;
        airplane[i].src = airplane[i].dst;
        airplane[i].dst = swap;

        /* Generate cash from ticket prices. */
        game.cash += passengers * airplane[i].ticket_price;

        /* Subtract fuel price. */
        game.cash -= distance * ((double)game.current_fuel_price *
          airplane_data[airplane[i].type].fuel_consumption_factor);
      }
      /* Subtract upkeep price for airplane, even when parked. */
      game.cash -= airplane_data[airplane[i].type].upkeep_cost;
    }
  }

  /* Fluctuate traveling demand. */
  for (int i = 0; i < CITY_MAX; i++) {
    for (int j = 0; j < CITY_MAX; j++) {
      if (i == j) {
        continue;
      }

      uint32_t variance = city[i].size + city[j].size;
      variance += random() % 64;
      variance /= 10;
      game_fluctuate(&city[i].demand[j], -variance, variance, 0, 1000);
    }
  }

  /* Price/cost fluctuations. */
  game_fluctuate(&game.current_fuel_price, -1, 1, 3, 10);
  game_fluctuate(&airplane_data[0].upkeep_cost, -20, 20, 200, 1000);
  game_fluctuate(&airplane_data[1].upkeep_cost, -50, 50, 500, 2000);
  game_fluctuate(&airplane_data[2].upkeep_cost, -250, 250, 2000, 10000);
  game_fluctuate(&airplane_data[0].price, -500, 500, 20000, 100000);
  game_fluctuate(&airplane_data[1].price, -1000, 1000, 50000, 300000);
  game_fluctuate(&airplane_data[2].price, -5000, 5000, 100000, 800000);

  /* Check and possibly warn about balance. */
  if (game.cash < 0) {
    if (game.days_in_the_red >= GAME_RED_DAYS_MAX) {
      printf("Game over! You went bankrupt...\n");
      game.loop_running = false;
      return;
    }
    printf("WARNING! Cash in the red, you have %d day%s left resolve this.\n",
      GAME_RED_DAYS_MAX - game.days_in_the_red,
      (GAME_RED_DAYS_MAX - game.days_in_the_red == 1) ? "" : "s");
    game.days_in_the_red++;
  } else {
    game.days_in_the_red = 0;
  }

  if (game.cash >= GAME_CASH_GOAL) {
    printf("Congratulations! You reached the goal of %d$ in %d days.\n",
        GAME_CASH_GOAL, game.day);
    game.loop_running = false;
    return;
  }

  game.day++;
}



static void game_init(void)
{
  srandom(time(NULL));
  city_generate();

  for (int i = 0; i < AIRPLANE_MAX; i++) {
    airplane[i].exists = false;
  }

  game.day = 1;
  game.days_in_the_red = 0;
  game.cash = GAME_CASH_START;
  game.current_fuel_price = 4 + (random() % 4);
  game.nominal_fuel_price = game.current_fuel_price;
}



static char *command_arg_completion_city(const char *text, int state)
{
  static int index;

  if (state == 0) {
    index = 0;
  }

  while (index < CITY_MAX) {
    if (0 == strncmp(city[index].name, text, strlen(text))) {
      return strdup(city[index++].name);
    }
    index++;
  }

  return NULL;
}



static char *command_arg_completion_airplane_id(const char *text, int state)
{
  static int index;
  static char airplane_id_string[AIRPLANE_MAX][4];
  static int airplanes;

  if (state == 0) {
    index = 0;
    airplanes = 0;
    /* Generate the list of possible numbers as strings. */
    for (int i = 0; i < AIRPLANE_MAX; i++) {
      if (airplane[i].exists) {
        snprintf(airplane_id_string[i], sizeof(airplane_id_string[i]),
          "%d", i + 1);
        airplanes++;
      }
    }
  }

  if (0 == airplanes) {
    return NULL;
  }

  while (index < AIRPLANE_TYPE_MAX) {
    if (0 == strncmp(airplane_id_string[index], text, strlen(text))) {
      return strdup(airplane_id_string[index++]);
    }
    index++;
  }

  return NULL;
}



static char *command_arg_completion_airplane_type(const char *text, int state)
{
  static int index;

  if (state == 0) {
    index = 0;
  }

  while (index < AIRPLANE_TYPE_MAX) {
    if (0 == strncmp(airplane_data[index].name, text, strlen(text))) {
      return strdup(airplane_data[index++].name);
    }
    index++;
  }

  return NULL;
}



static char *command_completion(const char *text, int state)
{
  static int index;

  if (state == 0) {
    index = 0;
  }

  while (index < COMMANDS_MAX) {
    if (0 == strncmp(commands[index].name, text, strlen(text))) {
      return strdup(commands[index++].name);
    }
    index++;
  }

  return NULL;
}



static char **command_completion_entry(const char *text, int start, int end)
{
  rl_attempted_completion_over = 1;
  if (start == 0) {
    /* Command part. */
    return rl_completion_matches(text, command_completion);

  } else {
    /* Arguments part. */
    for (int i = 0; i < COMMANDS_MAX; i++) {
      if (0 == strncmp(commands[i].name, rl_line_buffer,
                strlen(commands[i].name))) {
        /* Found the command entered... */
        int arg_no = -1;
        bool saw_space = false;
        for (char *p = rl_line_buffer; *p != '\0'; p++) {
          if (*p == ' ') {
            if (! saw_space) {
              arg_no++;
              saw_space = true;
            }
          } else {
            saw_space = false;
          }
        }
        /* ...Found the argument number... */
        if (arg_no < COMMAND_ARG_MAX) {
          command_arg_type_t type = commands[i].arg[arg_no];
          command_arg_completion_function_t function = 
            command_arg_data[type].function;
          if (function != NULL) {
            /* ...Call the specific function. */
            return rl_completion_matches(text, function);
          }
        }
        break;
      }
    }

    return NULL;
  }
}



static bool command_function_call(char *command)
{
  const char *argv[COMMAND_ARG_MAX + 1]; /* Includes the command name. */

  /* Split command into argc and argv. */
  int argc = 0;
  char *token = strtok(command, " ");
  do {
    if (token != NULL) {
      argv[argc] = token;
      argc++;
      if (argc == COMMAND_ARG_MAX + 1) {
        break;
      }
    }
    token = strtok(NULL, " ");
  } while (token != NULL);

  /* Attempt to call matching command. */
  if (argc == 0) {
    return false;
  }
  for (int i = 0; i < COMMANDS_MAX; i++) {
    if (0 == strncmp(commands[i].name, argv[0],
              strlen(commands[i].name))) {
      (commands[i].function(argc, argv));
      return true;
    }
  }

  printf("Unknown command: %s\n", command);
  return false;
}



int main(int argc, const char *argv[])
{
  char *command;
  char prompt[32];

  printf("*** AIRLINE MANAGEMENT SIMULATOR ***\n");
  printf("Reach the goal of %d$\n", GAME_CASH_GOAL);
  printf("Type 'help' or use tab completion.\n");

  game_init();

  rl_attempted_completion_function = command_completion_entry;

  game.loop_running = true;
  while (game.loop_running) {
    snprintf(prompt, sizeof(prompt), "[%u] $%"PRIi64" > ", game.day, game.cash);
    command = readline(prompt);
    if (command == NULL) { /* EOF */
      return 0;
    }
    if (0 == strlen(command)) {
      continue;
    }

    add_history(command);
    command_function_call(command);
    free(command);
  }

  return 0;
}
          


Topic: Scripts and Code, by Kjetil @ 05/02-2022, Article Link