10.5. A more ambitious example

Finally here is a set of programs designed to cooperate and manipulate a single data file in a coherent, robust fashion.

The programs are intended to help keep track of a ladder of players who compete against each other at some game, squash or chess perhaps.

Each player has a rank from one to n, where n is the number of players who play, one being the highest rank on the ladder. Players lower down the ladder may challenge players above them and, if the lower ranked player wins, he or she moves up taking the rank of the player who loses. The loser in such a situation, and any other players between challenger and loser, are then moved down one rank. If a challenger does not win, the rankings on the ladder remain unchanged.

To provide some measure of equilibrium in the rankings, a player may challenge any higher ranked player, but only wins over players ranked three (or less) higher will allow the challenger to move up the rankings. This ensures that new players added to the bottom of the ladder are forced to play more than one game to reach the top of the ladder!

There are three basic tasks which are required to record all the information needed to keep such a ladder going:

The design to be used here provides a separate program to perform each of these tasks. Having made this decision it is clear that a number of operations needed by each program will be common to all three. For example, all three will need to read player records from the data file, at least two will need to write player records into the data file.

This suggests that a good approach would be to design a ‘library’ of functions which manipulate player records and the data file which may in turn be combined to make up the programs which maintain the ladder.

Before this can be done it will be necessary to define the data structure which represents player records. The minimum information necessary to record for each player consists of player name and rank. However, to allow for more interesting statistics to be compiled about the ladder let us chose to also keep a record of games won, games lost and the time when the last game was played. Clearly this disparate set of information is best collected together in a structure.

The player structure declaration together with the declarations of the player library functions are combined together in the player.h header file. The data file is maintained as lines of text, each line corresponding to a record; this requires input and output conversions to be performed but is a useful technique if the conversions don't cost too much in performance terms.

/*
*
* Declarations and definitions for functions which manipulate player
* records which form the basis of the ladder
*
*/

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

#define NAMELEN 12              /* max. for player name length */

#define LENBUF 256              /* max. for input buffer length */

#define CHALLENGE_RANGE 3       /* number of higher ranked players who may
                                * be challenged to move up in rank
                                */

extern char *OptArg;

typedef struct {
        char    name[NAMELEN+1];
        int     rank;
        int     wins;
        int     losses;
        time_t  last_game;
} player;

#define NULLPLAYER (player *)0

extern const char *LadderFile;

extern const char *WrFmt;       /* used when writing records */
extern const char *RdFmt;       /* used when reading records */

/*
* Declarations for routines used to manipulate the player records
* and the ladder file which are defined in player.c
*
*/

int     valid_records(FILE *);
int     read_records(FILE *, int, player *);
int     write_records(FILE *, player *, int);
player *find_by_name(char *, player *, int);
player *find_by_rank(int, player *, int);
void    push_down(player *, int, int, int);
int     print_records(player *, int);
void    copy_player(player *, player *);
int     compare_name(player *, player *);
int     compare_rank(player *, player *);
void    sort_players(player *, int);
Example 10.4

Here is the code for the player.c file implementing the generic functions which manipulate player records and the data file. These functions can be combined with more specific routines to make up the three programs required to maintain the ladder.

Notice that to manipulate the player records, each program is required to read the entire data file into a dynamically allocated array. Before this array is written back to the data file, it is assumed that the records it contains will have been sorted into rank order. If the records do not remain sorted, the push_down function will produce some ‘interesting’ results!

/*
* Generic functions to manipulate the ladder data file and
* player records.
*
*/

#include "player.h"

const char *LadderFile = "ladder";

const char *WrFmt = "%s %d      %d      %d      %ld\n";
const char *RdFmt = "%s %d      %d      %d      %ld";


/* note use of string-joining */
const char *HeaderLine =
        "Player Rank Won Lost Last Game\n"
        "===============================================\n";

const char *PrtFmt = "%-12s%4d %4d %4d %s\n";

/* return the number of records in the data file */

int valid_records(FILE *fp)
{
        int i = 0;
        long plrs = 0L;
        long tmp = ftell(fp);
        char buf[LENBUF];

        fseek(fp, 0L, SEEK_SET);

        for(i = 0; fgets(buf, LENBUF, fp) != NULL ; i++)
                ;

        /* Restore the file pointer to original state */

        fseek(fp, tmp, SEEK_SET);

        return i;
}

/* read num player records from fp into the array them */

int read_records(FILE *fp, int num, player *them)
{
        int i = 0;
        long tmp = ftell(fp);

        if(num == 0)
                return 0;

        fseek(fp, 0L, SEEK_SET);

        for(i = 0 ; i < num ; i++){
                if(fscanf(fp, RdFmt, (them[i]).name,
                                &((them[i]).rank),
                                &((them[i]).wins),
                                &((them[i]).losses),
                                &((them[i]).last_game)) != 5)
                        break;          /* error on fscanf! */
        }

        fseek(fp, tmp, SEEK_SET);
        return i;
}

/* write num player records to the file fp from the array them */

int write_records(FILE *fp, player *them, int num)
{
        int i = 0;

        fseek(fp, 0L, SEEK_SET);

        for(i = 0 ; i < num ; i++){
                if(fprintf(fp, WrFmt, (them[i]).name,
                                (them[i]).rank,
                                (them[i]).wins,
                                (them[i]).losses,
                                (them[i]).last_game) < 0)
                        break;          /* error on fprintf! */
        }

        return i;
}

/*
* return a pointer to the player in array them
* whose name matches name
*/

player *find_by_name(char * name, player *them, int num)
{
        player *pp = them;
        int i = 0;

        for(i = 0; i < num; i++, pp++)
                if(strcmp(name, pp->name) == 0)
                        return pp;

        return NULLPLAYER;
}

/*
* return a pointer to the player in array them
* whose rank matches rank
*/

player *find_by_rank(int rank, player *them, int num)
{
        player *pp = them;
        int i = 0;

        for(i = 0; i < num; i++, pp++)
                if(rank == pp->rank)
                        return pp;

        return NULLPLAYER;
}

/*
* reduce by one the ranking of all players in array them
* whose ranks are now between start and end
*/

void push_down(player *them, int number, int start, int end)
{
        int i;
        player *pp;

        for(i = end; i >= start; i--){
        if((pp = find_by_rank(i, them, number)) == NULLPLAYER){
                fprintf(stderr,
                        "error: could not find player ranked %d\n", i);
                free(them);
                exit(EXIT_FAILURE);
        } else
                (pp->rank)++;
        }
}

/* pretty print num player records from the array them */

int print_records(player *them, int num)
{
        int i = 0;

        printf(HeaderLine);

        for(i = 0 ; i < num ; i++){
                if(printf(PrtFmt,
                        (them[i]).name, (them[i]).rank,
                        (them[i]).wins, (them[i]).losses,
                        asctime(localtime(&(them[i]).last_game))) < 0)
                        break;          /* error on printf! */
        }

        return i;
}

/* copy the values from player from to player to */

void copy_player(player *to, player *from)
{
        if((to == NULLPLAYER) || (from == NULLPLAYER))
                return;

        *to = *from;
        return;
}

/* compare the names of player first and player second */

int compare_name(player *first, player *second)
{
        return strcmp(first->name, second->name);
}

/* compare the ranks of player first and player second */

int compare_rank(player *first, player *second)
{
        return (first->rank - second->rank);
}

/* sort num player records in the array them */

void sort_players(player *them, int num)
{
        qsort(them, num, sizeof(player), compare_rank);
}
Example 10.5

This code, when tested, was compiled into an object file which was then linked (together with an object file containing the code for the options function) with one of the following three programs to for the ladder maintenance utilities.

Here is the code for the simplest of those utilities, showlddr which is contained in the file showlddr.c.

This program takes a single option, -f, which you will notice takes an option argument. The purpose of this argument is to allow you to print a ladder data file with a name other than the default file name, ladder.

The player records in the data file should be stored pre-sorted but, just to be safe, showlddr sorts them before it prints them out.

/*
* Program to print the current ladder status.
*
*/

#include "player.h"

const char *ValidOpts = "f:";

const char *Usage = "usage: showlddr [-f ladder_file]\n";

char *OtherFile;

int main(int argc, char *argv[])
{
        int number;
        char ch;
        player *them;
        const char *fname;
        FILE *fp;

        if(argc == 3){
                while((ch = options(argc, argv, ValidOpts)) != -1){
                        switch(ch){
                                case 'f':
                                        OtherFile = OptArg;
                                        break;
                                case '?':
                                        fprintf(stderr, Usage);
                                        break;
                        }
                }
        } else if(argc > 1){
                fprintf(stderr, Usage);
                exit(EXIT_FAILURE);
        }

        fname = (OtherFile == 0)? LadderFile : OtherFile;
        fp = fopen(fname, "r+");

        if(fp == NULL){
                perror("showlddr");
                exit(EXIT_FAILURE);
        }

        number = valid_records (fp);

        them = (player *)malloc((sizeof(player) * number));

        if(them == NULL){
                fprintf(stderr,"showlddr: out of memory\n");
                exit(EXIT_FAILURE);
        }

        if(read_records(fp, number, them) != number){
                fprintf(stderr, "showlddr: error while reading"
                                        " player records\n");
                free(them);
                fclose(fp);
                exit(EXIT_FAILURE);
        }

        fclose(fp);

        sort_players(them, number);

        if(print_records(them, number) != number){
                fprintf(stderr, "showlddr: error while printing"
                                        " player records\n");
                free(them);
                exit(EXIT_FAILURE);
        }

        free(them);
        exit(EXIT_SUCCESS);
}
Example 10.6

Of course the showlddr program works only if there is an existing data file containing player records in the correct format. The program newplyr creates such a file if one does not already exist and then adds a new player record, in the correct format to that file.

Typically, new players are added at the bottom of the rankings but for the odd occasion where this really may not make sense, newplyr also allows a player to be inserted into the middle of the rankings.

A player may only appear once on the ladder (unless a pseudonym is used!) and there can only be one player at any one rank. Thus the program checks for duplicate entries and if the new player is to be inserted into a middling rank, moves other players already on the ladder out of the way.

As with the showlddr program, newplyr recognises a -f option as a request to add the new player to a file named by the option argument rather than the default file, ladder. In addition, newplyr requires two options, -n and -r, each with option arguments to specify both the new player's name and initial ranking respectively.

/*
* Program to add a new player to the ladder.
* You are expected to assign a realistic
* ranking value to the player.
*
*/

#include "player.h"

const char *ValidOpts = "n:r:f:";

char *OtherFile;

static const char *Usage = "usage: newplyr -r rank -n name [-f file]\n";

/* Forward declaration of function defined in this file */

void record(player *extra);

int main(int argc, char *argv[])
{
        char ch;
        player dummy, *new = &dummy;

        if(argc < 5){
                fprintf(stderr, Usage);
                exit(EXIT_FAILURE);
        }

        while((ch = options(argc, argv, ValidOpts)) != -1){
                switch(ch){
                case 'f':
                        OtherFile=OptArg;
                        break;
                case 'n':
                        strncpy(new->name, OptArg, NAMELEN);
                        new->name[NAMELEN] = 0;
                        if(strcmp(new->name, OptArg) != 0)
                                fprintf(stderr,
                                        "Warning: name truncated to %s\n", new->name);
                        break;
                case 'r':
                        if((new->rank = atoi(OptArg)) == 0){
                                fprintf(stderr, Usage);
                        exit(EXIT_FAILURE);
                        }
                        break;
                case '?':
                        fprintf(stderr, Usage);
                        break;
                }
        }

        if((new->rank == 0)){
                fprintf(stderr, "newplyr: bad value for rank\n");
                exit(EXIT_FAILURE);
        }

        if(strlen(new->name) == 0){
                fprintf(stderr,
                        "newplyr: needs a valid name for new player\n");
                exit(EXIT_FAILURE);
        }

        new->wins = new->losses = 0;
        time(& new->last_game); /* make now the time of the "last game" */

        record(new);

        exit(EXIT_SUCCESS);
}

void record(player *extra)
{
        int number, new_number, i;
        player *them;
        const char *fname =(OtherFile==0)?LadderFile:OtherFile;
        FILE *fp;

        fp = fopen(fname, "r+");

        if(fp == NULL){
                if((fp = fopen(fname, "w")) == NULL){
                        perror("newplyr");
                        exit(EXIT_FAILURE);
                }
        }

        number = valid_records (fp);
        new_number = number + 1;

        if((extra->rank <= 0) || (extra->rank > new_number)){
                fprintf(stderr,
                        "newplyr: rank must be between 1 and %d\n",
                        new_number);
                exit(EXIT_FAILURE);
        }

        them = (player *)malloc((sizeof(player) * new_number));

        if(them == NULL){
                fprintf(stderr,"newplyr: out of memory\n");
                exit(EXIT_FAILURE);
        }

        if(read_records(fp, number, them) != number){
                fprintf(stderr,
                        "newplyr: error while reading player records\n");
                free(them);
                exit(EXIT_FAILURE);
        }

        if(find_by_name(extra->name, them, number) != NULLPLAYER){
                fprintf(stderr,
                        "newplyr: %s is already on the ladder\n",
                        extra->name);
                free(them);
                exit(EXIT_FAILURE);
        }

        copy_player(&them[number], extra);

        if(extra->rank != new_number)
                push_down(them, number, extra->rank, number);

        sort_players(them, new_number);

        if((fp = freopen(fname, "w+", fp)) == NULL){
                perror("newplyr");
                free(them);
                exit(EXIT_FAILURE);
        }

        if(write_records(fp, them, new_number) != new_number){
                fprintf(stderr,
                        "newplyr: error while writing player records\n");
                fclose(fp);
                free(them);
                exit(EXIT_FAILURE);
        }
        fclose(fp);
        free(them);
}
Example 10.7

The only remaining utility required is one for recording the results of games played. The result program performs this task.

As with the previous two utilities, result will accept a -f option together with a file name to specify an alternative to the default player record file.

Unlike the newplyr utility, result interactively prompts the user for the names of the winning and losing players. The program insists that the names supplied should be those of existing players.

Given a valid pair of names, a check is then made to see if the loser is higher ranked than winner and whether or not the winner is ranked close enough for the victory to alter the rankings.

If a change in the standings is in order, the victor takes the loser's rank and the loser (as well as any other player on an intervening rank) is demoted one rank.

Here is the code for the result utility.

/*
* Program to record a result in the ladder
*
*/

#include "player.h"

/* Forward declarations for functions defined in this file */

char *read_name(char *, char *);
void move_winner(player *, player *, player *, int);

const char *ValidOpts = "f:";

const char *Usage = "usage: result [-f file]\n";

char *OtherFile;

int main(int argc, char *argv[])
{
        player *winner, *loser, *them;
        int number;
        FILE *fp;
        const char *fname;
        char buf[LENBUF], ch;

        if(argc == 3){
                while((ch = options(argc, argv, ValidOpts)) != -1){
                        switch(ch){
                                case 'f':
                                        OtherFile = OptArg;
                                        break;
                                case '?':
                                        fprintf(stderr, Usage);
                                        break;
                        }
                }
        } else if(argc > 1){
                fprintf(stderr, Usage);
                exit(EXIT_FAILURE);
        }

        fname = (OtherFile == 0)? LadderFile : OtherFile;
        fp = fopen(fname, "r+");

        if(fp == NULL){
                perror("result");
                exit(EXIT_FAILURE);
        }

        number = valid_records (fp);

        them = (player *)malloc((sizeof(player) * number));

        if(them == NULL){
                fprintf(stderr,"result: out of memory\n");
                exit(EXIT_FAILURE);
        }

        if(read_records(fp, number, them) != number){
                fprintf(stderr,
                        "result: error while reading player records\n");
                fclose(fp);
                free(them);
                exit(EXIT_FAILURE);
        }

        fclose(fp);

        if((winner = find_by_name(read_name(buf, "winner"), them, number))
                == NULLPLAYER){
                fprintf(stderr,"result: no such player %s\n",buf);
                free(them);
                exit(EXIT_FAILURE);
        }

        if((loser = find_by_name(read_name(buf, "loser"), them, number))
                == NULLPLAYER){
                fprintf(stderr,"result: no such player %s\n",buf);
                free(them);
                exit(EXIT_FAILURE);
        }

        winner->wins++;
        loser->losses++;

        winner->last_game = loser->last_game = time(0);

        if(loser->rank < winner->rank)
                if((winner->rank - loser->rank) <= CHALLENGE_RANGE)
                        move_winner(winner, loser, them, number);

        if((fp = freopen(fname, "w+", fp)) == NULL){
                perror("result");
                free(them);
                exit(EXIT_FAILURE);
        }

        if(write_records(fp, them, number) != number){
                fprintf(stderr,"result: error while writing player records\n");
                free(them);
                exit(EXIT_FAILURE);
        }
        fclose(fp);
        free(them);
        exit(EXIT_SUCCESS);
}

void move_winner(player *ww, player *ll, player *them, int number)
{
        int loser_rank = ll->rank;

        if((ll->rank - ww->rank) > 3)
                return;

        push_down(them, number, ll->rank, (ww->rank - 1));
        ww->rank = loser_rank;
        sort_players(them, number);
        return;
}

char *read_name(char *buf, char *whom)
{
        for(;;){
                char *cp;
                printf("Enter name of %s : ",whom);
                if(fgets(buf, LENBUF, stdin) == NULL)
                        continue;
                /* delete newline */
                cp = &buf[strlen(buf)-1];
                if(*cp == '\n')
                        *cp = 0;
                /* at least one char? */
                if(cp != buf)
                        return buf;
        }
}
Example 10.8