/* wally.c From: newman@tcgould.TN.CORNELL.EDU (Bill Newman) Subject: another simple-minded PD go-playing program Before gnu-go was announced, I used the ideas in the BYTE article by J.K. Millen (April 1981) to make a go-playing program ("wally") of my own. Wally's only advantage over gnugo is that it plays better than the early gnugo versions that I saw (pulling ahead even before gnugo made its random self-destructive moves in the endgame). Wally uses explicit array addressing instead of pointer arithmetic, so it is slower than it should be, but otherwise I'm not too embarrassed by the details of the code. (though constructive criticism is always appreciated) The program is documented only by comments inside; the only features which may not be obvious are command-line options to create a game record, and to play on a range of board sizes, with or without handicap stones. As noted in the comments, the program is copyrighted but freely available for not-for-profit use, such as anyone can find for it. The program is unlikely to hold the interest of a human opponent very long, especially since it plays an obnoxious endgame, singlemindedly throwing in to the enemy's territory whenever it sees the opportunity to keep the opponent from making the maximum number of eyes. However, it might be of interest to programmers wondering how far you can get with a simple pattern-matching program, or just looking for more patterns for their tables; most of the patterns in the program's table (all those not in Millen's article) are original with me; I started with Millen's shape table, then spent several hours playing against the program (mostly on a 9x9 board with 4-stone handicap) and changing the table to keep the program from doing unnecessarily stupid things. The source code to the program follows the dotted line, and continues to the end of the file. The program compiles and runs with cc under SunOS 4.0.3 and and with Mark Williams C 3.0 on an Atari 520 ST. Bill Newman newman@tcgould.tn.cornell.edu ------------------------------------------------------------------------ */ /* wally.c, a go playing program Copyright (c) 1988 by William H. Newman and distributed as shareware: permission is granted for all non-profit use of the program as long as this notice is included. Send comments to box 172 Cornell U, Ithaca NY 14850, or e-write to newman@batcomputer.tn.cornell.edu. This program was inspired by ideas published by Jonathan K. Millen in BYTE, April 1981. Because an Atari ST is somewhat more powerful than a KIM-1, I have been able to make the program somewhat more sophisticated. However, it still plays very naive go. (30 kyu?) Notable flaws: no lookahead no knowledge of live or dead shape takes advantage of a series of opponent's passes @ the start of the game rather poorly; this points up flaws in the shape table no rules to stop the program from making obnoxious but pointless moves in the endgame; it ought not to once it has passed once. Maybe turn off pattern matching completely after the program's first pass move. #ifdef ADDED Three new options: -d turns on debugging output -mgt writes the output record in a form suitable for mgt -cat writes the output record in a form suitable for cat ... | wally The game score is printed even in the case of a resignation. <-xtx-*> cc -o wally -O -DADDED wally.c Added by Kevin Stock, kstock@encore.fr, 12th July 1990 #endif */ #include /* #include */ #include #ifdef ADDED # define PATCHLEVEL 2 # define WALLY_OUTPUT 0 # define MGT_OUTPUT 1 # define CAT_OUTPUT 2 # define M_LETTER(x) \ ((x < 8) ? lettercol(x) : ((x == 8) ? 'i' : lettercol(x - 1))) int outputmode = WALLY_OUTPUT; #endif #define ASSERT 1 /*flag for whether assert() macros should be */ /* expanded*/ #define BIGU 10000 /*a number higher than any meaningful "urgency" */ /* or move importance*/ #define RESIGN (-2) /*codes for resigning */ #define PASS (-1) /* passing moves*/ #define BOTHPASS (-3) /*code for both sides passed, game is over*/ #define EDGE 23 /*max # intersections on the edge of a go board*/ int edge= 9; /*the number of intersections which we are using (a */ /* command line parameter)*/ #define NPLAYERS 2 /*# players (hence # copies to keep of score &c.)*/ int debugattack= 0; /*flag for printing debugging messages in attack()*/ /*Return the absolute value of i.*/ #define abs(i) ((i)>=0?(i):-(i)) /*Return TRUE if (x,y) corresponds to a point on the board.*/ #define on1board(x) (0<=(x)&&(x)color&&0==g->nliberties) { rv += g->nstones; *capx= g->x; *capy= g->y; sremove(g->x, g->y); } return rv; } int colletter(letter) register int letter; /* Return the column # (0 to edge-1) which corresponds to letter. Due to perversity of tournament organizers etc., 'i' or 'I' is omitted, leading to an increase in complexity of this routine. Return (-1) on illegal input. */ { register int result; if('a'<=letter&&letter<='z') result= letter-'a'; else if('A'<=letter&&letter<='Z') result= letter-'A'; else return (-1); if(8>result) ; else if(8==result) return (-1); else --result; if(result>=edge) return (-1); else return result; } int getnb() /*Read and return the first nonblank character from the standard input.*/ { int c; do c= getchar(); while(' '==c||'\n'==c||TAB==c); return c; } int scanfcoo(x, y) int *x, *y; /* Read a coordinate from the standard input and write it to *x and *y. Like scanf, return 1 for success, 0 for failure, negative numbers for special moves, e.g. PASS and RESIGN. On any input error, the function eats input up to the next newline. */ { int xx, yy, c1, c2; /*Get first nonblank character of input, normally a letter code for a column.*/ c1= getnb(); /*Check for possible resignation (a '.' at the start of the input).*/ if('.'==c1) { while('\n'!=getchar()) /*Skip to end of line.*/ ; return RESIGN; } /*Check for possible "pass".*/ if('p'==lowercase(c1)) { c2= getchar(); if('a'!=lowercase(c2)) ungetc(c2, stdin); else { if((c2=getchar(),'s'!=lowercase(c2))||(c2=getchar(),'s'!=lowercase(c2))) { ungetc(c2, stdin); goto skipquit; } else return PASS; } } xx= colletter(c1); if(1!=scanf("%d", &yy)) { skipquit: /*Skip to the next newline and return failure.*/ while('\n'!=getchar()) ; return 0; } if(0==onboard(xx,yy-1)) /*The -1 converts between C arrays starting */ /* at 0 and human boards counting from 1.*/ goto skipquit; else { *x= xx; *y= yy-1; return 1; } } movedone() /* Do everything which must be done to complete a move after the stone is placed. */ { thegame.pla= nextp(thegame.pla); ++thegame.tur; } count(x, y, thisgroup, scratch, mark) register int x, y; register group *thisgroup; board scratch; int mark; /* Recursively group together connected stones. Three things must be done: (1) count liberties in thisgroup->nliberties, (2) count stones in thisgroup->nstones, and We set scratch[][]= mark for each point and liberty of this group, so that we only count each only once. The calling routine must see to it that scratch[][]!=mark for all points and liberties that it wants counted. */ { register int *bxy, *sxy; /*Common subexpressions to reduce array */ /* arithmetic; they point to theboard[x][y] and scratch[x][y].*/ endrecurse: assert(onboard(x,y)); assert(thisgroup->color==theboard[x][y]); assert(thisgroup->nstones>=0); assert(thisgroup->nliberties>=0); assert(mark!=scratch[x][y]); /*Set common subexpressions.*/ bxy= &(theboard[x][y]); sxy= &(scratch[x][y]); /*Count the point (x,y) and make it a member of the group.*/ ++(thisgroup->nstones); *sxy= mark; /*"Process" (x, y-1): should it be in the group, or a liberty, or */ /* nothing?*/ y= y-1; bxy -= 1; sxy -= 1; assert(&theboard[x][y]==bxy); assert(&scratch[x][y]==sxy); if(y>=0) if(thisgroup->color==*bxy) { if(*sxy!=mark) count(x, y, thisgroup, scratch, mark); } else if(EMPTY==*bxy) { if(*sxy!=mark) { *sxy=mark; ++(thisgroup->nliberties); } } /*Process (x, y+1).*/ y= y+2; bxy += 2; sxy += 2; assert(&theboard[x][y]==bxy); assert(&scratch[x][y]==sxy); if(ycolor==*bxy) { if(*sxy!=mark) count(x, y, thisgroup, scratch, mark); } else if(EMPTY==*bxy) { if(*sxy!=mark) { *sxy=mark; ++(thisgroup->nliberties); } } /*Process (x-1, y).*/ y= y-1; x= x-1; bxy -= EDGE+1; sxy -= EDGE+1; assert(&theboard[x][y]==bxy); assert(&scratch[x][y]==sxy); if(x>=0) if(thisgroup->color==*bxy) { if(*sxy!=mark) count(x, y, thisgroup, scratch, mark); } else if(EMPTY==*bxy) { if(*sxy!=mark) { *sxy=mark; ++(thisgroup->nliberties); } } /*Process (x+1, y).*/ x= x+2; bxy += 2*EDGE; sxy += 2*EDGE; assert(&theboard[x][y]==bxy); assert(&scratch[x][y]==sxy); if(xcolor==*bxy) { if(*sxy!=mark) goto endrecurse; } else if(EMPTY==*bxy) { if(*sxy!=mark) { *sxy=mark; ++(thisgroup->nliberties); } } } makegroups() /*Analyzes groupings of stones on board; sets groups[].*/ { register int x, y; /*coords of point we are now assigning to a group*/ board scratch; /*scratch for count() to use to say if a point is */ /* already counted*/ /*Clear old groups[] table and scratch[][].*/ ngroups= 0; { register int *s; for(s=scratch[0],x=0; xcolor= *bxy; thisgroup->x= x; thisgroup->y= y; thisgroup->nliberties= 0; thisgroup->nstones= 0; count(x, y, thisgroup, scratch, 1+thisgroup-groups); ++thisgroup; ++ngroups; } } } } placestone(x, y, p) int x, y, p; /* Put a stone for player p at (x,y) and sremove all the captures which result. Update ko information. */ { int ncap, capx, capy; assert(onboard(x, y)); assert(BLACK==p||WHITE==p); assert(EMPTY==theboard[x][y]); theboard[x][y]= p; makegroups(); /*Group all stones and count libs.*/ ncap= capture(nextp(p), &capx, &capy);/*Remove p's opponent's dead stones.*/ thegame.kox= thegame.koy= (-1); /*Cancel any old ko.*/ if(1==ncap) /*If exactly one stone was captured, check for new ko.*/ { register int j, *ip; group g; board scratch; for(ip=(int*)scratch,j=EDGE*EDGE; j--; ) *ip++= 0; g.color= theboard[x][y]; g.nstones= 0; g.nliberties= 0; g.x= x; g.y= y; count(x, y, &g, scratch, 1); /*Count stones and libs for capturing group.*/ if(1==g.nstones && 1==g.nliberties) /*If c.g. is a single stone in atari */ { thegame.kox= capx; thegame.koy= capy; } /* then ko, so mark it.*/ } if(ncap) /*If any enemy stones were removed, */ makegroups(); /* recalculate the board.*/ ncap= capture(p, &capx, &capy);/*Remove any still-trapped stones (suicide).*/ assert(WHITE==p||0==ncap); /*Wally knows not to make suicides.*/ } sortv(p, n) group **p; int n; /* Sort the table p[] or pointers to groups in order of vulnerability, most vulnerable first. Currently this is the slow simple bubble sort we all know. */ { register int uns; /*flag that an unsorted pair was found last pass*/ register group *t, /*exchange temporary*/ **ip; /*pointer through p[]*/ register int j; /*loop index through p*/ do { uns= 0; for(j=0,ip=p; jnliberties< (*ip)->nliberties || (t->nliberties==(*ip)->nliberties && t->nstones>(*ip)->nstones) ) { uns= 1; ip[1]= *ip; *ip= t; } } } while(uns); } int subj_lib(x, y) int x, y; /* Return the subjunctive liberties of the group created by a BLACK move at x, y, that is, how many liberties would it have after the move was made? This is done shoddily, without considering the liberties created by the disappearance of any groups captured by the move. */ { board scratch; /*parameter array of flags for count()*/ group t; /*parameter group for count()*/ assert(onboard(x,y)); assert(EMPTY==theboard[x][y]); /*Make sure we can undo by restoring EMPTY.*/ theboard[x][y]= BLACK; /*Temporarily place the stone in question.*/ /*Clear scratch[][] so count() works properly.*/ { register int *s, j; for(j=EDGE*EDGE,s=(int*)scratch; j--; ) *s++= 0; } /*Make a fake group for the new stone to be part of -- this is */ /* a necessary parameter for count().*/ t.color= BLACK; t.x= x; t.y= y; t.nliberties= 0; t.nstones= 0; /*Show this little Potemkin village to the count().*/ count(x, y, &t, scratch, 1); /*Undo the temporary move (x,y).*/ theboard[x][y]= EMPTY; /*Return the result from count().*/ return t.nliberties; } pattern1(u, masks, movehere) int *u; board masks, movehere; /* A routine called by pattern() to match possible moves to tabulated patterns of specific orientation. *u gives the urgency of the best move found so far; we are to ignore moves which are inferior to this. If this move has not already been found (compare to the urgency value in movehere[]) then we set its urgency value in *u and in movehere[], and put it into the table goodmoves[]. If the move is better than any found before, we put it at the beginning of the table; otherwise we put it in at the pointer pgoodmoves. masks[][] is an array which represents the pieces on the board, translated into bit masks by lookup in flookup[]. A late-breaking modification is that only the EMPTY points which have movehere[][] set are tested, so that pattern() can be called to look for contact plays or possibly other restricted sets which make good patterns. An even later modification permits the use of movehere[][] to tell the urgency of the best move which has been made to that point. */ { register int *is, *iis;/*pointers into patterns[]; or scratch*/ register int j; /*& into a particular pattern, # points remaining*/ register int x, y; /*current position we're trying to match pattern to*/ register int xs, ys; /*position of a point from a pattern*/ int ua; /*urgency and adjustment for this move*/ int thispat; /*which pattern in the table are we currently */ /* trying to match?*/ for(is=patterns,thispat=0; PATTERN==*is; is+= 3+3*(is[1]),++thispat) { for(x=0; x edge-1-y (across a mirror plane parallel to x-axis) (2) x <-> edge-1-x ( " y-axis) (3) y <-> edge-1-y ( " x-axis) (4) x <-> y ( " the line y=x) (5) y <-> edge-1-y (across a mirror plane parallel to x-axis) (6) x <-> edge-1-x ( " y-axis) (7) y <-> edge-1-y ( " x-axis) (8) x <-> -y ( " the line y=(-x)) Draw pictures if necessary to see how this works. */ { register int x, y, t, j, *is; board scratch; /*Translate the board to flags for easy comparison with pattern table.*/ for(x=0; xnstones= 0; /*Clear these before */ g->nliberties= 0; /* passing to count, which increments them.*/ count(g->x, g->y, g, scratch, 1); /*Print out scratch[][] as 9x9 table.*/ if(debugattack) { printf("$In attack after count(), board and scratch are:\n"); for(y=edge-1; y>=0; --y) { printf("$"); for(x=0; xnliberties>=0); /*Look for the best adjacent point.*/ edist= edge*10; /*We haven't found a point near the (2.9)-th line yet.*/ for(x=0; x=ml) /* if not too suicidal*/ { register int toedge= x; /*distance to edge of board*/ if(toedge>edge-1-x) toedge= edge-1-x; if(toedge>y) toedge= y; if(toedge>edge-1-y) toedge= edge-1-y; if(debugattack) fprintf(stderr, "$Looking at %d,%d, toedge=%d, abs=%d.\n", x, y, toedge, abs(10*toedge-21)); if(abs(10*toedge-19)nstones= 0; /*Clear these before */ g->nliberties= 0; /* passing to count, which increments them.*/ count(g->x, g->y, g, scratch, 1); /*Check that there are enough liberties to find a contact play.*/ assert(g->nliberties>=0); /*Return any point which succeeds.*/ for(x=0; xcolor) { ++nf; *igt++= g; } for(j=0,g=groups,ne=0,es=igt; jcolor) { ++ne; *igt++= g; } /*Sort the tables in order of vulnerability to attack.*/ sortv(fs, nf); sortv(es, ne); /*Consider extremely urgent pattern moves.*/ if(upattern<16) { movepattern: if(debugattack) printf("$making pattern, pattern #=%d, urgency=%d.\n", patnum, upattern); x= patternx; y= patterny; goto movexy; } /*Capture the enemy!*/ while(ne && 1==(*es)->nliberties) { if(0==attack(*es, &x, &y, 0)) { if(debugattack) printf("$Capturing the enemy.\n"); goto movexy; } else /*(This case comes up if we can't capture because of ko.)*/ { ++es; --ne; } /*Go on to next most vulnerable group.*/ } if(upattern<24) goto movepattern; /*If we can't do that, then try to protect ourself. But as per Dr. Millen, */ /* don't risk everything crawling along the edge without lookahead.*/ /*However, we have a more powerful computer than the good doctor, so we */ /* can afford to check another condition: if crawling connects to a healthy */ /* group immediately (as when the program makes a pattern match with */ /* O O # */ /* . . O . */ /* ~ ~ ~ ~ */ /* where '~' is off the board, then is attacked with */ /* O O # */ /* . . O # */ /* ~ ~ ~ + */ /* it seems to make no sense to prohibit the program from defending with */ /* O O # */ /* . O O # */ /* ~ ~ ~ ~ */ /* .. so we *don't* prohibit it.*/ while(nf && 1==(*fs)->nliberties) { if(escape(*fs, &x, &y)) /*If we can't see how to escape */ { ++fs; --nf; } /* go on to next friendly group, */ else /* otherwise */ { if((0==x||edge-1==x||0==y||edge-1==y) && /* if crawling */ (2>=subj_lib(x,y)) ) /* and not connecting to strong group */ { ++fs; --nf; continue; } /* this is probably not a good move */ else /* but if not crawling or connecting to */ /* a strong group, go for it.*/ if(debugattack) printf("$Protecting a friendly group from imminent capture.\n"); goto movexy; } } if(upattern<34) goto movepattern; /*Try putting the enemy in atari.*/ while(ne && 2==(*es)->nliberties) /*For all vulnerable enemy groups */ { if(attack(*es, &x, &y, 2)) /* if we can't see how to attack */ { ne--; ++es; } /* go on to the next enemy group, */ else /* otherwise */ { if(debugattack) printf("$Putting an enemy group into atari.\n"); goto movexy; /* attack the group.*/ } } if(upattern0) fprintf(file, "(Black wins.)\n"); else fprintf(file, "(White wins.)\n"); } int ist(x, y, n, scratch) register int x, y, n; board scratch; /*This is the recursive part of isterritory().*/ { assert(onboard(x,y)); assert(EMPTY==theboard[x][y]); assert(0==scratch[x][y]); scratch[x][y]= 1; x= x+1; if(onboard(x,y)) { if(EMPTY==theboard[x][y]) { if(0==scratch[x][y] && 0==ist(x, y, n, scratch)) return 0; } else if(n==theboard[x][y]) return 0; } x= x-2; if(onboard(x,y)) { if(EMPTY==theboard[x][y]) { if(0==scratch[x][y] && 0==ist(x, y, n, scratch)) return 0; } else if(n==theboard[x][y]) return 0; } x= x+1; y= y+1; if(onboard(x,y)) { if(EMPTY==theboard[x][y]) { if(0==scratch[x][y] && 0==ist(x, y, n, scratch)) return 0; } else if(n==theboard[x][y]) return 0; } y= y-2; if(onboard(x,y)) { if(EMPTY==theboard[x][y]) { if(0==scratch[x][y] && 0==ist(x, y, n, scratch)) return 0; } else if(n==theboard[x][y]) return 0; } return 1; } int isterritory(x, y, p) register int x, y, p; /* Return TRUE if the EMPTY point x, y is to be counted as territory for player p. This is only called at the end of the game, and I see no reason why it can't be slow. I don't understand the (Ing?) fractional point rules, so I don't implement them. An empty point counts as territory only if only one player has stones adjacent to the block of empty spaces to which it belongs. */ { board scratch; assert(onboard(x,y)); assert(EMPTY==theboard[x][y]); /*Clear scratch[].*/ { register int x, y; for(x=0; xEDGE) fatal("board size out of range on input line"); edge= maybe; } else if(0==strcmp(*argv,"-even")) /*if we are to play an even game*/ evenmode= 1; #ifdef ADDED else if (strcmp(*argv, "-d") == 0) debugattack = 1; else if (strcmp(*argv, "-mgt") == 0) outputmode = MGT_OUTPUT; else if (strcmp(*argv, "-cat") == 0) outputmode = CAT_OUTPUT; #endif else #ifdef ADDED { #else { char hname[80]; #endif if(0!=ofname) fatal("duplicate filenames in command line"); ofname= *argv; ofile= fopen(ofname, "w"); if(0==ofile) fatal("unable to open output file"); #ifndef ADDED printf("What is the name of my opponent? (for the game record)\n"); gets(hname); fprintf(ofile, "%s (black) vs. %s (white)\n", progname, hname); #endif } } #ifdef ADDED if (ofile == 0) { if (outputmode != WALLY_OUTPUT) { fprintf(stderr, "%s: output file needed for -mgt or -cat\n", progname); exit(1); } } else { if (outputmode != CAT_OUTPUT) { printf("What is the name of my opponent? "); gets(hname); } switch (outputmode) { case WALLY_OUTPUT: /* should match that in #ifndef above */ fprintf(ofile, "%s (black) vs. %s (white)\n", progname, hname); break; case MGT_OUTPUT: time(&secs); fprintf(ofile, "(\n;\nGaMe[1]\nSize[%d]\nVieW[]\nBlackSpec[1]\n", edge); fprintf(ofile, "WhiteSpec[1]\nFileFormat[1]\nPLayer[]\n"); fprintf(ofile, "Comment[Black: %s\nWhite: %s\n\nDate: %s", progname, hname, ctime(&secs)); fprintf(ofile, "Size: %d x %d]\n", edge, edge); /* CAT_OUTPUT doesn't require any of this */ } } #endif if(evenmode) nhandicap[edge]= 0; /*Make a brief explanation.*/ printf("%s\n%s\n", "This program plays (poorly) the oriental game of Go, also", "known as wei-chi."); /*Play the game.*/ initgame(); do if(BLACK==thegame.pla) rvalue=mymove(); else if(WHITE==thegame.pla) rvalue=enemymove(); else panic("illegal thegame.pla in main() loop"); while(rvalue!=BOTHPASS && rvalue!=RESIGN); #ifndef ADDED if(BOTHPASS==rvalue) #endif score(); /*Close the output file.*/ if(ofile) #ifdef ADDED { if (outputmode == MGT_OUTPUT) fprintf(ofile, ")\n"); #endif if(fclose(ofile)) fatal("unable to close output file"); #ifdef ADDED } #endif exit(0); }