ttt-demo

Langue: en

Autres versions - même langue

Version: 2006-12-21 (ubuntu - 08/07/09)

Section: 3 (Bibliothèques de fonctions)

NAME

ggtltut - a tutorial introduction to ggtl

SYNOPSIS

This article describes the steps involved in creating a self-playing Tic-Tac-Toe game using an AI provided by GGTL.

The ggtlcb(3) manpage describes the various callback functions GGTL supports. Which ones you have to provide when implementing a game depends on the game in question, so the first thing to do is chose one. I've chosen Tic-Tac-Toe, as it is relatively simple but illustrates what is going on quite well.

Header files

We obviously need to include the GGTL header, ggtl/core.h. The sl/sl.h header is for the sl(3) linked-list library. You're not required to use it, but as it is used internally by GGTL, doing so incurs no extra cost on your memory usage.

   #include <ggtl/core.h>
   #include <sl/sl.h>
   #include <stdio.h>
   #include <stdlib.h>
 
 

Chosing suitable data structures

We first have to define a structure for our states. One of the things that might be a bit hard to get used to at first is that GGTL makes it the responsibility of the states to know which player's turn it is to move. The main reason for this is that it lets the programmer chose how to represent the players. Want to use "a" and "b" for the players? 1 and 2? "x" and "y"? Fine. They're all supported, and there's no extra hoops to jump through.

We will use "x" and "o" for our players. For the board, we'll use a 3-by-3 grid of integers, where a zero means an empty location.

   typedef struct {
     int player;
     int grid[3][3];
   } ttt_state_t;
 
 

Moves are simpler. They consist of simply an x and an y coordinate.

   typedef struct {
     int x, y;
   } ttt_move_t;
 
 

Making moves

Performing a move simply means putting the current player in the grid location specified by the move, then advance the current player. We do that like so:

   void *ttt_move(void *st, void *mv, GGTL *g)
   {
     ttt_state_t *s = st;
     ttt_move_t *m = mv;
 
 
     s->grid[ m->x ][ m->y ] = s->player;
     s->player = s->player == 'x' ? 'o' : 'x';
   
     return s;
   }
 
 

Never do anything you can't undo

GGTL's AI figures out which move to pick by recursively trying out every possible move at the current position and seing where they lead. Needless to say it wouldn't get very far doing this if it didn't know how to undo a move, so let's teach it how to do this.

Depending on personal preference (or attributes of the game you're implementing) you may chose to implement either the "unmove()" or "copy_state()" callback. For Tic-Tac-Toe we could chose either, but let's pick the "unmove()" method. It is very similar to the "move()" callback:

   void *ttt_unmove(void *st, void *mv, GGTL *g)
   {
     ttt_state_t *s = st;
     ttt_move_t *m = mv;
 
 
     s->grid[ m->x ][ m->y ] = 0;
     s->player = s->player == 'x' ? 'o' : 'x';
   
     return s;
   }
 
 

We now have everything needed for using GGTL simply as a history manager, keeping track of the current position whilst providing unlimited undo.

Working out your options

GGTL's AI needs help finding out what moves are available at each state. We do this by giving it a callback function that returns a list of available moves for the current player. Any grid location not currently occupied is a legal move, so this is really quite simple. We can use ggtl's cache to avoid gratuitous calls to "malloc()".

Note that in the interest of brevity and simplicity I've taken the liberty to exclude error checking from the below code. You should really check the return value of "malloc()".

   GGTL_MOVE *ttt_getmoves(void *st, GGTL *g)
   {
     ttt_state_t *s = st;
     int x, y;
     GGTL_MOVE *moves = NULL;
 
 
     for (x = 0; x < 3; x++) {
       for (y = 0; y < 3; y++) {
         if (!s->grid[x][y]) {
           ttt_move_t *m = ggtl_uncache_move_raw(g);
           if (!m) {
             m = malloc(sizeof *m);
           }
           m->x = x; 
           m->y = y;
           moves = sl_push(moves, ggtl_wrap_move(g, m));
         }
       }
     }
   
     return moves;
   }
 
 

Evaluating your options

When searching for the best move, GGTL's AI needs to know which states are better than others. We will give it a simple function that returns 0 in the case of a draw or an unfinished game, and +1 or -1 if the current state is a win or a loss for the current player.

A much better evaluation function would not only indicate wins and losses but return a fitness value for intermediate states too. This would allow GGTL to make better decisions even if it doesn't have time to search through all the positions to the end of the game. This is left as an exercise for the reader (as it happens, Tic-Tac-Toe has such a small search space that searching through all positions is not a problem; hence, this doesn't matter if we let GGTL's AI look far enough ahead).

   int ttt_eval(void *st, GGTL *g)
   {
     ttt_state_t *s = st;
     int i, player;
 
 
     for (i = 0; i < 3; i++) {
       player = s->grid[i][0];
       if (player &&
           player == s->grid[i][1] &&
           player == s->grid[i][2]) {
         goto have_winner;
       }
   
       player = s->grid[0][i];
       if (player &&
           player == s->grid[1][i] &&
           player == s->grid[2][i]) {
         goto have_winner;
       }
     }
   
     player = s->grid[0][0];
     if (player &&
         player == s->grid[1][1] &&
         player == s->grid[2][2]) {
       goto have_winner;
     }
    
     player = s->grid[0][2];
     if (player &&
         player == s->grid[1][1] &&
         player == s->grid[2][0]) {
       goto have_winner;
     }
   
     return 0;
   
   have_winner:
     return player == s->player ? 1 : -1;
   }
 
 

An exercise in visualisation

We now have all the components GGTL needs to provide an AI player for Tic-Tac-Toe. However, to enable us to visualize the game, we'll create a function that prints out the game state. Let's keep it simple though.

   void ttt_draw(ttt_state_t *s)
   {
     int x, y;
     for (x = 0; x < 3; x++) {
       for (y = 0; y < 3; y++) {
         int c = s->grid[x][y];
         putchar(c ? c : '.');
       }
       if (x < 2) putchar('\n');
     }
     printf(" - %c to move\n\n", s->player);
   }
 
 

All together now

We're now ready to create our "main()" function. It is fairly standard and consists of setting up the inital state of our game, initializing GGTL, playing through the game, reporting who won, and cleaning up the mess (freeing memory etc).

Note that, once again, for brevity and clarity I've omitted error checking.

   int main(void)
   {
     GGTL *g = ggtl_new();
     ttt_state_t *s = calloc(1, sizeof *s);
     s->player = 'x';
   
     ggtl_init(g, s);
   
     /* set the callback functions */
     ggtl_vtab(g)->move = ttt_move;
     ggtl_vtab(g)->unmove = ttt_unmove;
     ggtl_vtab(g)->get_moves = ttt_getmoves;
     ggtl_vtab(g)->eval = ttt_eval;
     
     ggtl_set(g, TYPE, FIXED); /* fixed-depth alpha-beta search */
     ggtl_set(g, PLY, 9);      /* nine-move look-ahead */
   
     do {
       ttt_draw(s);
       s = ggtl_ai_move(g);
     } while (s);
   
     s = ggtl_peek_state(g);
     if (!ttt_eval(s, NULL)) {
       puts("The only winning move is not to play.");
     }
     else {
       printf("Player %d %s. This should never happen.\n", 
         s->player, 
         ttt_eval(s, NULL) > 0 ? "won" : "lost");
     }
     ggtl_free(g);
   
     return 0;
   }
 
 

And that's all it takes. We now have a self-playing Tic-Tac-Toe game. This program is bundled with GGTL and should be installed as "ttt-demo" if you have GGTL installed.

AUTHOR

Stig Brautaset <stig@brautaset.org>

THANKS

Kari Pahula (<kaol@iki.fi>) for providing a documentation patch. Copyright (C) 2005 Stig Brautaset

This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.