We learn to write an application with XForms by simulating a game with 2 players and 2 actions.
Last month we began this series on XForms by explaining how to install the forms library and include file. We also took a stab at programming with XForms by writing a couple of simple programs. In this month’s article, we’re going to write a fully fledged application. We’ll start with an explanation of the project, and then see how to implement it with XForms.
The Project: A Game Theory Simulator
Our task is to implement a game theory simulator. If you don’t happen to have a doctorate in mathematics, you might want to have a look at A Primer on Game Theory which appears on page 52 of this magazine. We’re attacking a non-trivial programming task in order to get a handle on how to do “real-world” programming with XForms. It’s not important that you understand every nuance of game theory, since our main goal is to figure out XForms.
In a formal game, there are two main entities we have to consider: players and payoffs. So our simulator should allow us to set relevant values for these elements. Players, for example, are defined by actions and strategies. Similarly, payoffs are just a set of values that players receive when they play the game. As a great simplifier, we’ll assume there are only two types of players and only two possible actions. This reduces the dimensionality of our programming problem. A good exercise for readers would be to try and relax the two strategy limitation, but make sure you understand the initial program fairly well before trying to modify it.
Since we’re creating a graphical application, we’ll want a point-and-click interface for setting up our players. The method adopted is to think of players as having a finite number of states. In every state, there is an action to be taken. Since we’ve limited ourselves to just two possible actions, let’s call these A and B. Our simulator will be used for repeated games, so we want to be able to design players who can change their actions. That is, move to another state which tells them to play a different action. There are only two types of players, Column Players and Row Players, and two actions, so the choice of which action to play can be affected only by what the other player did.
Let’s say you want to design a player who always plays action A. That’s simple; just set the action in each state to A. A more complicated example would be to design a player who plays A if the other player chose A last period, but B if the other player chose B. This is easier to see with a diagram:
Here we see that the transitions from state to state can be made contingent on the behavior of other players. So to implement an interface for designing players, we have to be able to specify the action in each state and the transitions to perform, i.e., which state to jump to, given the behavior of the other player. We cannot make one player’s choices contingent on what the other player does in the current period. This would violate one of the tenets of standard game theory: simultaneous choice of action. Here we show only two states, but we’ll allow for more complicated player strategies in the actual program.
We’ll also want several players of each type to exist and to be randomly matched against players of the other type. This sounds more difficult than it is, since we still have to design only two types of players. The population of Row Players, for example, should differ only in what state they are currently in, not in the overall design of their strategy.
Like player design, we’ll want an easy way for the user to set and edit payoffs. This is a little simpler, since we just need a graphical representation of the payoff table and a method for letting the user change these values. We’ll want both of these features to appear in their own windows so we can pop them up when we need them.
Once the user has specified player strategies and payoffs, we’ll also need a way to actually run the simulation. This routine should match the players, allocate their payoffs, and handle the transitions from state to state. It should also let us set how long the game should run, and give us some nice visual feedback on the progress of play.
All of this input, interaction and editing may seem very complicated. It would probably require a fairly cumbersome set of menus or a command language, if we were going to program this project on a simple terminal window. But with XForms, we can easily create windows, input fields and other graphical elements required to implement our game theory simulator. If this is all a little hazy, it may be a good idea to play with the running program a little (see below), and then come back to this section.
The xgtsim Program
/* xgtsim.c -- An XForms Game Theory Simulator On a "standard" Linux system, this program can be compiled with the command: gcc -lX11 -lforms -lm xgtsim.c -o xgtsim If that gives you problems, you are probably using a RedHat system. In that case, try: gcc -lforms -L/usr/X11R6/lib -lXpm -lX11 -lm xgtsim.c -o xgtsim */ /* We need a few include files, the most important of which is forms.h (which declares all the functions available in libforms) */ #include <forms.h> /* XForms include file */ #include <stdlib.h> /* Standard C stuff */ #include <time.h> /* Need time to seed for random numbers */ /* A few definitions */ #define NUMB_TYPES 2 /* Two types of players */ #define NUMB_PLAYERS 20 /* You can change this parameter to 1 if you want just one player of each type */ #define NUMB_ACTIONS 2 /* Two possible actions */ #define NUMB_STATES 10 /* You can change this if you want to allow more complex (or simpler) players */ #define ACTION_A 0 /* Value to represent A */ #define ACTION_B 1 /* Value to represent B */ /* Some global forms variables for the windows we'll be using */ FL_FORM *main_window; FL_FORM *player_window; FL_FORM *payoff_window; FL_FORM *run_window; /* We also need to make some forms objects global, since they are accessed by different functions */ FL_OBJECT *action_choices[NUMB_STATES]; FL_OBJECT *transition_inputs[NUMB_ACTIONS][NUMB_STATES]; FL_OBJECT *payoff_inputs[NUMB_ACTIONS][NUMB_ACTIONS][NUMB_TYPES]; FL_OBJECT *go_button; FL_OBJECT *stop_button; FL_OBJECT *column_chart; FL_OBJECT *row_chart; FL_OBJECT *column_browser; FL_OBJECT *row_browser; /* Here are a few normal variables to store information about players, payoffs, and other parameters */ float payoffs[NUMB_ACTIONS][NUMB_ACTIONS][NUMB_TYPES]; int state_actions[NUMB_TYPES][NUMB_STATES]; int state_transitions[NUMB_TYPES][NUMB_ACTIONS][NUMB_STATES]; int row_or_column; int numb_iterations; int abort_flag; /* This routine sets some startup values */ void set_defaults() { int i,j,k; time_t curtime; int seedval; /* (Pseudo-)randomly seed the (pseudo-)random number generator */ curtime = time(NULL); seedval = curtime; srand(seedval); /* This variable tells us whether changes in the player window should affect the column players or the row players Default is to edit column players */ row_or_column = 0; /* We'll do 1000 iterations of the game by default */ numb_iterations = 1000; /* Set Random Payoffs */ for(i = 0; i < NUMB_ACTIONS; i++) { for(j=0; j < NUMB_ACTIONS; j++) { for(k = 0; k < NUMB_TYPES; k++) { payoffs[i][j][k] = (rand() % 1000) / 1000.0; } } } /* Set random strategies and simple transitions */ for(i = 0; i < NUMB_TYPES; i++) { for(j = 0; j < NUMB_STATES; j++) { k = rand() % 1000; if(k < 500) state_actions[i][j] = ACTION_A; else state_actions[i][j] = ACTION_B; for(k = 0; k < NUMB_ACTIONS; k++) { state_transitions[i][k][j] = j+2; if(state_transitions[i][k][j] > NUMB_STATES) state_transitions[i][k][j] = 1; } } } } /* The function set_player_values() is called whenever the user changes anything in the player window. Note that variable row_or_column is already set, so we only change the relevant values */ void set_player_values(FL_OBJECT *obj, long argument) { int i; char a_string[10]; for(i = 0; i < NUMB_STATES; i++) { if(fl_get_choice(action_choices[i]) == 1) state_actions[row_or_column][i] = ACTION_A; else state_actions[row_or_column][i] = ACTION_B; istate_transitions[row_or_column][0][i] = atoi(fl_get_input(transition_inputs[0][i])); /* Here we check to make sure the user did not specify a state value which is less than 1 or greater than NUMB_STATES */ if(state_transitions[row_or_column][0][i] < 1) { state_transitions[row_or_column][0][i] = 1; sprintf(a_string, "1"); fl_set_input(transition_inputs[0][i], a_string); } if(state_transitions[row_or_column][0][i] > NUMB_STATES) { state_transitions[row_or_column][0][i] = NUMB_STATES; sprintf(a_string, "%d", NUMB_STATES); fl_set_input(transition_inputs[0][i], a_string); } state_transitions[row_or_column][1][i] = atoi(fl_get_input(transition_inputs[1][i])); if(state_transitions[row_or_column][1][i] < 1) { state_transitions[row_or_column][1][i] = 1; sprintf(a_string, "1"); fl_set_input(transition_inputs[1][i], a_string); } if(state_transitions[row_or_column][1][i] > NUMB_STATES) { state_transitions[row_or_column][1][i] = NUMB_STATES; sprintf(a_string, "%d", NUMB_STATES); fl_set_input(transition_inputs[1][i], a_string); } } } /* Here we read the values from the payoffs window into the payoffs array whenever a value is edited by the user */ void set_payoffs(FL_OBJECT *obj, long argument) { int i,j,k; for(i = 0; i < NUMB_ACTIONS; i++) { for(j = 0; j < NUMB_ACTIONS; j++) { for(k = 0; k < NUMB_TYPES; k++) { payoffs[i][j][k] = atof(fl_get_input(payoff_inputs[i][j][k])); } } } } /* This changes the number of iterations */ void set_iterations(FL_OBJECT *obj, long argument) { numb_iterations = fl_get_counter_value(obj); } /* Whenever a button is pushed on the main window, this routine is called to make the relevant window appear */ void display_forms(FL_OBJECT *obj, long which_form) { if(which_form == 1) fl_show_form(player_window,FL_PLACE_FREE,FL_FULLBORDER, "Player Design"); if(which_form == 2) fl_show_form(payoff_window,FL_PLACE_FREE,FL_FULLBORDER, "Edit Payoffs"); if(which_form == 3) fl_show_form(run_window,FL_PLACE_FREE,FL_FULLBORDER, "Play the Game"); } /* close_forms() tells XForms that it's OK to close a window when if the window manager receives that signal (ie. if the user clicked on their window manager's close window icon */ int close_forms(FL_FORM *form, void *argument) { /* We always want to let the user close windows If we wanted to prevent a window from being closed in this way, we could return FL_IGNORE instead */ return(FL_OK); } /* Are we editing row or column players in the players window? */ void set_row_or_column(FL_OBJECT *obj, long argument) { int i; char a_string[10]; /* Set row_or_column */ row_or_column = argument; /* Now update all the objects in the player window so that they show correct values */ for(i = 0; i < NUMB_STATES; i++) { if(state_actions[row_or_column][i] == ACTION_A) fl_set_choice_text(action_choices[i], "A"); else fl_set_choice_text(action_choices[i], "B"); sprintf(a_string,"%d", state_transitions[row_or_column][0][i]); fl_set_input(transition_inputs[0][i], a_string) sprintf(a_string,"%d", state_transitions[row_or_column][1][i]); fl_set_input(transition_inputs[1][i], a_string); } } /* Shuts down the program */ void quit_xgtsim(FL_OBJECT *obj, long argument) { fl_finish(); exit(0); } /* When the Stop button is pushed, we set this flag to 1. */ void stop_the_game(FL_OBJECT *obj, long argument) { abort_flag = 1; } /* This routine runs the game by matching players, and updates information in the run window */ void play_the_game(FL_OBJECT *obj, long argument) { int i,j,k,l; float column_average, row_average; char a_string[256]; double smallest_payoff, largest_payoff; int to_match[NUMB_PLAYERS]; int column_action; int row_action; int which_player, players_left; int current_state[NUMB_TYPES][NUMB_PLAYERS]; float current_payoff[NUMB_TYPES][NUMB_PLAYERS]; /* Start all players off in a random state */ for(i = 0; i < NUMB_TYPES; i++) { for(j = 0; j < NUMB_PLAYERS; j++) { current_state[i][j] = rand() % NUMB_STATES; } } /* Turn off the abort flag, then make the stop button appear so the user can abort the run */ abort_flag = 0; fl_hide_object(go_button); fl_show_object(stop_button); /* Clear the charts */ fl_clear_chart(column_chart); fl_clear_chart(row_chart); /* Run the simulation */ for (i = 0; i < numb_iterations; i++) { /* Each time through, we want to scale the charts */ smallest_payoff = payoffs[0][0][0]; largest_payoff = payoffs[0][0][0]; for(j = 0; j < NUMB_ACTIONS; j++) { for(k = 0; k < NUMB_ACTIONS; k++) { for(l = 0; l < NUMB_TYPES; l++) { if(smallest_payoff > payoffs[j][k][l]) smallest_payoff = payoffs[j][k][l]; if(largest_payoff < payoffs[j][k][l]) largest_payoff = payoffs[j][k][l]; } } } smallest_payoff = smallest_payoff - 0.05 * (largest_payoff - smallest_payoff); largest_payoff = largest_payoff + 0.05 * (largest_payoff - smallest_payoff); fl_set_chart_bounds(column_chart, smallest_payoff, largest_payoff); fl_set_chart_bounds(row_chart, smallest_payoff, largest_payoff); /* Now we randomly match column players against row players, calculate their payoffs, and change their states according to their transitions */ for(j = 0; j < NUMB_PLAYERS; j++) { to_match[j] = j; } column_average = 0.0; row_average = 0.0; players_left = NUMB_PLAYERS; for(j = 0; j < NUMB_PLAYERS; j++) { which_player = rand() % players_left; column_action = state_actions[0][current_state[0][j]]; row_action = state_actions[1][current_state[1] [to_match[which_player]]]; current_payoff[0][j] = payoffs[state_actions[0] [current_state[0][j]]] [state_actions[1][current_state[1] [to_match[which_player]]]] [0]; column_average = column_average + current_payoff[0][j]; current_payoff[1][to_match[which_player]] = payoffs[state_actions[0][current_state[0][j]]] [state_actions[1][current_state[1] [to_match[which_player]]]] [1]; row_average = row_average + current_payoff[1] [to_match[which_player]]; current_state[0][j] = state_transitions[0] [row_action] [current_state[0][j]] - 1; current_state[1][to_match[which_player]] = state_transitions[1] [column_action] [current_state[1][to_match[which_player]]] - 1; for(k = which_player; k < players_left - 1; k++) { to_match[k] = to_match[k+1]; } players_left--; } column_average = column_average / NUMB_PLAYERS; row_average = row_average / NUMB_PLAYERS; /* Here's where we update the display with the new information from the lastest round of the game */ fl_add_chart_value(column_chart, column_average, "", FL_RED); sprintf(a_string,"%f", column_average); fl_clear_browser(column_browser); fl_add_browser_line(column_browser,a_string); fl_add_chart_value(row_chart, row_average, "", FL_RED); sprintf(a_string,"%f", row_average); fl_clear_browser(row_browser); fl_add_browser_line(row_browser,a_string); /* Now call fl_check_forms() to see if any information has changed (in particular, was the stop button pushed?) */ fl_check_forms(); if(abort_flag == 1) { i = numb_iterations; } } /* We're done this run, so hide the Stop button and show the Go button */ fl_hide_object(stop_button); fl_show_object(go_button); } /* This routine creates all our windows/forms and all the graphical elements on them */ void create_forms() { /* The "obj" pointer is used to create elements on all the various forms. We also need a group object to handle groupings */ FL_OBJECT *obj; FL_OBJECT *group; int i,j,k; char a_string[10]; /* The main window is used to access all the other windows, so we set the callbacks to the display_forms() function (except for Quit). */ main_window = fl_bgn_form(FL_NO_BOX, 290, 50); obj = fl_add_box(FL_UP_BOX,0,0,290,50,""); obj = fl_add_button(FL_NORMAL_BUTTON,10,10,60,30,"Players"); fl_set_object_callback(obj, display_forms, 1); obj = fl_add_button(FL_NORMAL_BUTTON,80,10,60,30,"Payoffs"); fl_set_object_callback(obj, display_forms, 2); obj = fl_add_button(FL_NORMAL_BUTTON,150,10,60,30,"Run"); fl_set_object_callback(obj, display_forms, 3); obj = fl_add_button(FL_NORMAL_BUTTON,220,10,60,30,"Quit"); fl_set_object_callback(obj, quit_xgtsim, 1); fl_end_form(); /* The Player Window allows the user to set up player actions and strategies */ player_window = fl_bgn_form(FL_NO_BOX, 270, 100 + 20 * NUMB_STATES); obj = fl_add_box(FL_UP_BOX,0,0,270,100 + 20 * NUMB_STATES,""); obj = fl_add_box(FL_UP_BOX,10,50,250,10,""); /* This loop creates 10 text labels (one for each state) on the player window */ for(i = 0; i < NUMB_STATES; i++) { sprintf(a_string,"%d", i+1); obj = fl_add_text(FL_NORMAL_TEXT,20,90 + i * 20,20,20,i a_string); fl_set_object_lalign(obj,FL_ALIGN_LEFT|FL_ALIGN_INSIDE); } /* Now we label this column of state numbers */ obj = fl_add_text(FL_NORMAL_TEXT,10,70,40,20,"States"); fl_set_object_lalign(obj,FL_ALIGN_LEFT|FL_ALIGN_INSIDE); /* For each state, we need a choice object to select the action in that state */ for(i = 0; i < NUMB_STATES; i++) { action_choices[i] = fl_add_choice(FL_NORMAL_CHOICE,60,90+i*20,50,20,""); fl_set_object_boxtype(action_choices[i],FL_EMBOSSED_BOX); fl_addto_choice(action_choices[i],"A"); fl_addto_choice(action_choices[i],"B"); if(state_actions[0][i] == ACTION_A) fl_set_choice_text(action_choices[i], "A"); else fl_set_choice_text(action_choices[i], "B"); fl_set_object_callback(action_choices[i], set_player_values, 0); } /* Label this column of actions */ obj = fl_add_text(FL_NORMAL_TEXT,60,70,50,20,"Actions"); fl_set_object_lalign(obj,FL_ALIGN_LEFT|FL_ALIGN_INSIDE); /* We also need two lists of which states to jump to while agents are "playing" the game */ for(i = 0; i < NUMB_STATES; i++) { transition_inputs[0][i] = fl_add_input(FL_INT_INPUT,i 140,90 + i * 20,30,20,""); sprintf(a_string,"%d", state_transitions[0][0][i]); fl_set_input(transition_inputs[0][i], a_string); fl_set_object_callback(transition_inputs[0][i], set_player_values, 0); transition_inputs[1][i] = fl_add_input(FL_INT_INPUT, 210,90 + i * 20,30,20,""); sprintf(a_string,"%d", state_transitions[0][1][i]); fl_set_input(transition_inputs[1][i], a_string); fl_set_object_callback(transition_inputs[1][i], set_player_values, 0); } obj = fl_add_text(FL_NORMAL_TEXT,120,70,70,20, "A Transitions"); fl_set_object_lalign(obj, FL_ALIGN_LEFT|FL_ALIGN_INSIDE); obj = fl_add_text(FL_NORMAL_TEXT,190,70,70,20, "B Transitions"); fl_set_object_lalign(obj, FL_ALIGN_LEFT|FL_ALIGN_INSIDE); /* Finally (for this form) we need two buttons to choose between column players and row players. Note that we set the button type to FL_RADIO button so that XForms will ensure that only one is selected at a time */ group = fl_bgn_group(); obj = fl_add_lightbutton(FL_RADIO_BUTTON,10,10,110,30, "Column Players"); fl_set_button(obj, 1); fl_set_object_callback(obj, set_row_or_column, 0); obj = fl_add_lightbutton(FL_RADIO_BUTTON,150,10,110,30, "Row Players"); fl_set_object_callback(obj, set_row_or_column, 1); fl_end_group(); fl_end_form(); /* When the user clicks on the close window button in the title bar, run the following */ fl_set_form_atclose(player_window, close_forms, 0); /* Now we create the window for editing payoffs */ payoff_window = fl_bgn_form(FL_NO_BOX, 260, 180); obj = fl_add_box(FL_UP_BOX,0,0,260,180,""); /* We have two types of players, each of which can play 1 of 2 actions, giving us four possible outcomes. For each outcome, we need a payoff for each type of player. */ for(i = 0; i < NUMB_ACTIONS; i++) { for(j =0; j < NUMB_ACTIONS; j++) { for(k=0; k < NUMB_TYPES; k++) { payoff_inputs[i][j][k] = i fl_add_input(FL_FLOAT_INPUT, 110 + 70 * i, 60 + j * 50 + k * 20, 60, 20,""); sprintf(a_string,"%5.5f", payoffs[i][j][k]); fl_set_input(payoff_inputs[i][j][k], a_string); fl_set_object_callback(payoff_inputs[i][j][k], set_payoffs, 0); } } } /* Here we add some text so that the user understands what the values in all those input fields represent */ obj = fl_add_text(FL_NORMAL_TEXT,130,10,110,20, "Column Players"); fl_set_object_lalign(obj, FL_ALIGN_LEFT|FL_ALIGN_INSIDE); obj = fl_add_text(FL_NORMAL_TEXT,10,100,70,20, "Row Players"); fl_set_object_lalign(obj, FL_ALIGN_LEFT|FL_ALIGN_INSIDE); obj = fl_add_text(FL_NORMAL_TEXT,80,70,20,20,"A"); fl_set_object_lalign(obj, FL_ALIGN_LEFT|FL_ALIGN_INSIDE); obj = fl_add_text(FL_NORMAL_TEXT,130,30,20,20,"A"); fl_set_object_lalign(obj, FL_ALIGN_LEFT|FL_ALIGN_INSIDE); obj = fl_add_text(FL_NORMAL_TEXT,200,30,20,20,"B"); fl_set_object_lalign(obj, FL_ALIGN_LEFT|FL_ALIGN_INSIDE); obj = fl_add_text(FL_NORMAL_TEXT,80,130,20,20,"B"); fl_set_object_lalign(obj, FL_ALIGN_LEFT|FL_ALIGN_INSIDE); fl_end_form(); fl_set_form_atclose(payoff_window, close_forms, 0); /* The run window lets us set the number of iterations, start/stop the game, and gives us visual feedback on a running game. */ run_window = fl_bgn_form(FL_NO_BOX, 420, 180); obj = fl_add_box(FL_UP_BOX,0,0,420,180,""); /* We want a line graph for both column players and row players, so we create charts of type FL_LINE_CHART */ column_chart = fl_add_chart(FL_LINE_CHART,10,30,190,90, "Column Players"); fl_set_object_lalign(column_chart, FL_ALIGN_TOP|FL_ALIGN_LEFT); fl_set_chart_maxnumb(column_chart, 100); row_chart = fl_add_chart(FL_LINE_CHART,220,30,190,90, "Row Players"); fl_set_object_lalign(row_chart, FL_ALIGN_TOP|FL_ALIGN_LEFT); fl_set_chart_maxnumb(row_chart, 100); /* We also create two browsers to give us a place to display numerical feedback */ column_browser = fl_add_browser(FL_NORMAL_BROWSER, 130, 5, 70, 21,""); row_browser = fl_add_browser(FL_NORMAL_BROWSER, 340, 5, 70, 21,""); /* Now we add a counter to let us set the number of iterations for the game */ obj = fl_add_counter(FL_NORMAL_COUNTER,60,140,140,30, "Iterations"); fl_set_object_lalign(obj,FL_ALIGN_LEFT); fl_set_counter_precision(obj, 0); fl_set_counter_bounds(obj, 1, 100000); fl_set_counter_step(obj, 1, 100); fl_set_counter_value(obj, numb_iterations); fl_set_object_callback(obj, set_iterations, 0); /* We need buttons to start (Go) and stop (Stop) the game. we draw them on top of each other, but then hide the Stop button. The Go button starts the game running by calling play_the_game(). In that routine, we hide the Go button and show the Stop button. */ go_button = fl_add_button(FL_NORMAL_BUTTON,220,140,190,30, "Go!"); fl_set_object_callback(go_button, play_the_game, 0); stop_button = fl_add_button(FL_NORMAL_BUTTON,220,140,190,30, "Stop!"); fl_set_object_callback(stop_button, stop_the_game, 0); fl_hide_object(stop_button); fl_end_form(); fl_set_form_atclose(run_window, close_forms, 0); } int main(int argc, char *argv[]) { /* The first call is to fl_initialize(), which sets up XForms and handles relevant command line options */ fl_initialize(&argc, argv,"xldlas", 0, 0); /* We call set_defaults() to assign initial payoffs and strategies, then create_forms() sets up all the windows for the program (but doesn't make any of them actually appear on the display) */ set_defaults(); create_forms(); /* Now we show the main window and pass control over to the user with fl_do_forms(); */ fl_show_form(main_window,FL_PLACE_FREE,FL_FULLBORDER, "An XForms Game Theory Simulator"); fl_do_forms(); return(0); }
Let’s just plunge right in. We’ll get an example up and running right away, and then use the rest of this article to explain how it works. The program is called xgtsim and the C source code can be found in Listing 1.1 Although you’re welcome to type it in, it’s also available on the web site for this series (see http://a42.com/~thor/xforms, Listing 1). It should compile with the command:
gcc -lX11 -lforms -lm xgtsim.c -o xgtsim
From within the X Window System, you should be able to run the program by typing ./xgtsim in an xterm window. If you have problems, you may want to go back and review last month’s article on installing XForms. With all possible windows open, the running program should look something like Figure 2.
If you want to play around with the program before continuing with the rest of this article, one useful exercise would be to set up a prisoner’s dilemma. Just use the payoff editor to set values the same as they appear in the primer, and then try some different player strategies. In particular, try and figure out what happens when two Tit-for-Tat strategies come up against each other. Does it matter what initial strategies they play?
The Flow of the Program
Last month, we saw that the basic steps to designing an XForms program are as follows:
- Include forms.h to access the XForms routines
- Call fl_initialize() as soon as possible
- Set up your graphical interface by creating forms
- Assign actions to relevant objects by setting callbacks
- Show one or more forms
- Turn control over to fl_do_forms()
We use this approach in xgtsim. Like all C programs, execution begins in the main() routine, which is at the very end of the source code. First we call fl_initialize() to set up XForms, and allow it to parse command line arguments. Next, we call set_defaults() which seeds the random number generator, and sets some default values for our payoffs and player design variables: payoffs[][][], state_actions[][] and state_transitions[][][].
A call to create_forms() is then made, which sets up all of our windows, graphic elements and callbacks. We’ll go into more detail shortly, but let’s look at how this works for the simplest case: quitting the program. Within the create_forms() code, we use main_window (a variable of type FL_FORM) to create a window which will be shown when the program starts up. This window has four buttons on it, called Players, Payoffs, Run and Quit. Notice that the Quit button is set to call the function quit_xgtsim() with the command:
fl_set_object_callback(obj, quit_xgtsim, 1);
This means whenever the mouse is clicked on the button labeled Quit, the quit routine will be called. This function, in turn, simply calls fl_finish() and then exits.
To return to the flow of the main() function, after setting up all of our windows, buttons and so on with create_forms(), we then make our main_window appear with a call to fl_show_form(). Then we turn control over to the user by calling fl_do_forms().
It’s crucial to understand that setting up our forms in create_forms() does more than just decide how graphics should be laid out on the screen. By setting callbacks to link button pushes and data inputs with specific actions, we’ve actually set up the whole flow of the program. When the user pushes the Payoffs button, it is XForms (via fl_do_forms()) which calls the relevant routine to make the Payoffs window appear, and to handle subsequent interaction with that window. In fact, if we’ve set all our callbacks correctly, execution never returns to main(). The fl_do_forms() routine returns only if the user activates an object which does not have a callback associated with it.
Some Details
Since create_forms() is so important, lets look at it in more detail. We use and re-use a generic pointer called *obj, which is of type FL_OBJECT, to create many of our graphical elements. This can be a little confusing, but we’ll clarify things as we go.
The first form created in create_forms() is main_window. This is a global pointer variable which we declare early in the source code. We tell XForms it is a window which should be 290 pixels wide and 50 pixels high with the assignment:
main_window = fl_bgn_form(FL_NO_BOX, 290, 50);
In the following nine lines of code, we create four buttons which will be used to pop up windows for user interaction and to quit the program. Each time we need a new graphical element, we just use obj, which saves memory and keeps things simple. Just remember that whenever we reassign obj, all subsequent functions passing obj as a value will affect the most recent assignment. The Players, Payoffs and Run buttons are all linked by a callback to a routine called display_forms(), but they are set to call that routine with the values 1, 2 and 3 respectively. The display_forms() routine, in turn, uses these values to decide which window to display. After creating the Quit button, we tell XForms we’re done adding elements to this form by calling fl_end_form().
We then go on to create the player_window, payoff_window and run_window. These all follow the same general pattern; declare the dimensions of the window with fl_bgn_form(), add as many objects as we want (assigning callbacks as we go), and then finish with fl_end_form(). We’ll look at the run_window in detail, since it’s the simplest. Once you have it figured out, you’ll probably want to look over the other two on your own.
Since we want visual feedback from the game, we create two charts in the run_window. We make these into line charts by specifying FL_LINE_CHART, and we set the dimensions by including 4 integer values. The first two values represent where the upper left corner of our chart should appear, with 0,0 being the very top left corner of the form the object is being created on. The next values describe the width and height of the object. Finally, we supply a string to give the chart a label:
column_chart = fl_add_chart(FL_LINE_CHART, 10, 30, 190, 90, "Column Players");
You may be wondering why we assign this function call to a variable called column_chart instead of using our generic obj variable. This is done because column_chart is declared as a global variable, which is accessible to all the routines in xgtsim. In particular, when the game is actually being run, the play_the_game() routine uses this global variable to add values to the chart we just created—look for the function fl_add_chart_value().
With the label “Column Players” assigned to our chart, the default behavior is for it to appear below the chart. We move it to the top left corner with the call to fl_set_object_lalign(). Then we limit the number of items which can be displayed with fl_set_chart_maxnumb(). We then create an almost identical chart to display information about Row Players.
In addition to chart feedback, we create two browsers to display numerical data. This is accomplished with calls to fl_add_browser(). Browsers are very useful objects in XForms, and they can be used in many different ways. Our implementation here is very simple, but you can learn more about them in the XForms documentation.
To allow the user to set the number of iterations the game should run, we create a counter, and set lots of options. First we align the label to appear on the left, then set the precision to 0. This just means we want our counter to hold integer data, since you can’t really perform half an iteration. A standard counter appears on the screen with two sets of arrows. Whenever they are pushed, they change the value of the numerical data the counter is holding. We set bounds on this data with a call to fl_set_counter_bounds(), and then make one set of buttons change the value by 1 and the other set change the value by 100 by setting the counter step rates. We also set the starting value in the counter to a default value (stored in numb_iterations), and then record a callback. Whenever the counter object is changed, the routine set_iterations() is called which sets the variable numb_iterations.
The run window also contains two buttons, one to start the game running and one to stop it. Notice that we create these two buttons in exactly the same place on the form, so that they are on top of each other. Before finishing, though, we hide the stop_button to ensure the go_button is visible. When the go_button is pushed, it calls play_the_game(), which hides the go_button and makes the stop_button appear. The ability to call fl_hide_object() and fl_show_object() makes form design in XForms very flexible, since you can design windows where objects appear and disappear according to any number of conditions. When an object is hidden, it is impossible for the user to activate it.
Once fl_end_form() is called, we are nearly done with this window. Immediately afterwards though, we call:
fl_set_form_atclose(run_window, close_forms, 0);
This tells XForms what routine to run when the window manager sends the close window signal. On most window managers, this signal is sent when the user clicks on a close icon in the title bar of the window in question. This is like a callback, but the format is slightly different. In a normal callback, the declaration is of the form:
fl_set_object_callback(the_object, the_function, an_argument);
The function pointed to by the_function must accept two arguments, an FL_OBJECT pointer and a long, as in:
the_function(FL_OBJECT *obj, long an_argument);
This function must return void. When the window close signal is sent, however, it applies to an entire window/form, not a particular object on that form. So the first argument to fl_set_form_atclose() must be a pointer of type FL_FORM, as in:
close_forms(FL_FORM *form, void *an_argument);
This function must return an integer, and in particular, it should return FL_OK if you want the window to actually close, and FL_IGNORE if you want the window to remain visible.
Having looked at main() and create_forms(), the rest of the source code is fairly easy to follow. The most complicated part is how the player_window uses the row_or_column variable to edit both types of players on a single form. The general idea is as follows. The global variables state_actions[][] and state_transitions[][][] hold all the data on the current state of both types of players, i.e., Column Players and Row Players. On the Player window, there are two buttons allowing the user to choose which type of player they want to edit. Whenever these are pushed, the Player window must be updated to reflect the state of these variables. This is done by the set_row_or_column() routine, which reads values from state_actions[][] and state_transitions[][][] into the relevant objects on the Player window, which are action_choices[] and transition_inputs[][].
With the window updated to reflect the current state of the relevant set of players, the user can now edit these values. This is accomplished via the set_player_values() function, which is called whenever any of these on-screen objects are changed. We do not bother trying to figure out which object is changed, but simply read all values on the Player window into state_actions[][] and state_transitions[][][].
The only remaining subtleties in the program are the use of the abort_flag variable and the call to fl_check_forms() in the iteration loop of play_the_game(). When the Go button is activated in the Run window, play_the_game() is called. One of the first things done in that routine is to set abort_flag to zero. Players are matched, payoffs made and the charts on the Run window are updated. At the bottom of the iteration loop, we check to see if abort_flag has been changed from 0 to 1, and if it has, we stop the run. You may be confused as to how this flag’s value could have possibly changed within this algorithm.
The key lies in the call to fl_check_forms(). This is a non-blocking routine that works just like fl_do_forms(), except that it exits immediately if no objects were activated. This exacts a small performance penalty, since the program is effectively monitoring all its objects while the game is running, but it is well worth it. Since we set a callback to the Stop button to change abort_flag to one (via stop_the_game()), clicking on the Stop button will cause the current game to be aborted.
This has the added benefit of allowing us to modify all our data while the simulation is running. For example, we can alter payoff values and immediately see how this changes the unfolding game via the visual feedback in the Run window. Similarly, we can change player strategies on the fly, and watch how this affects their performance. This probing and runtime editing of parameters is often very difficult to achieve with standard C running on a console, but with a few global variables, a little sensible design and a call to fl_check_forms(), XForms makes it almost trivial.
Things to Try
If you’ve managed to pour over xgtsim and get a good feel for what’s going on, you may want to try altering the source code to test your understanding. One thing to attempt would be adding an extra button to the Main window that randomizes all the current variables. That is, suppose the user has set payoffs and strategies, but wants to scramble these values. You’ll not only have to add the button and set up the callback, but you’ll have to update any currently displayed windows to reflect these changes.
The charts in the Run window currently provide only average feedback on the two types of players. Try adding more charts or other elements to display information on the best and worst players in each category.
If you’re feeling really ambitious, then try altering xgtsim to allow for more actions and more complicated strategies. This can get very complicated, since elements like the payoff matrix will have to grow and shrink depending on how many actions are currently possible. This can be accomplished by dynamically creating new objects and forms, something we haven’t covered so far.
In playing with xgtsim, you may find a set of strategies and payoffs that generate interesting results. Currently there’s no way to save this state of the game, because we have no file-based input and output. We’ll be adding that next month, by using XForms pre-built file requester routine. It’s just part of a whole set of “goodies” that XForms includes, and we’ll be looking at most of them.
We’ll also spiff up our application with some pixmaps, learn how to set gravity parameters to control window resizing, and look at a few other interesting features of XForms.