// Currently being worked on. with USE_GAMESTATE *not* defined, // scores appear to have gone haywire... // "USE_GAMESTATE" replaces playword with recursive_playword // which passes all the game state in as a parameter. We should // switch to that one, but it doesn't yet have all the global // analysis tweaks that the regular playword has. // global analysis code is now being developed separately in // subdirectory global_analysis, in file global.c - will // eventually need to be merged with this. /* http://www.gtoal.com/cgi-bin/allmoves.cgi?savedgame=14511 #..-...#...-..D .+...=..GLYCINE ..+...-.-.R.+.R -..+.TOAsTIE..M ....NAB...V.... .=.KOP...=D.H=. ..-.PEAGE...U.. AMATE.WUSS.-R.# ..-...-E-I..D.. .F...=.R.XI.E=. .O..+..I..L.N.. VERECUND.GITS.- IT+...-O-LAH+.. AA...=.n.I.E.JO <- 14 EL.-...S.T.WOOF ^ D place abore (drawprob 0.03%) at 14D across ([rzebqynaoiun][rzebqynaoiun][rzebqynaoiun][rzebqynaoiun]?) => abore row 14 col 4: add 1 to hscore for a row 14 col 5: add 3 to hscore for b row 14 col 6: add 1*3L to hscore for o row 14 col 7: add 1 to hscore for r add 0 to hscore for ? board[row 14][col 8] = n{?} assertion failed: nexttile=4 placed=5 It would appear that the move evaluator has not noticed the blank as 'n' on the board... I think this is now fixed. Next problem: http://www.gtoal.com/cgi-bin/allmoves.cgi?savedgame=14522 P("ern", "einr") = 0.0 ??? No, it's 1.0 ! This comes from the 3rd parameter, racksize, which is set to 7. It should reduce in the endgame when 0 tiles left in bag and player's rack shrinks. */ #define USE_GAMESTATE //#define GLOBALTEST // TO DO: when doing global analysis, need to take the remaining // tiles that you hold in your rackas givens. // Currently can only do that test with *all* unplayed tiles - // no account taken of rackleave from previous play /* This is a relatively small and simple word-puzzle solver, (or at least it was; it seems to have grown a little more complex as some bugs were fixed) which takes a board layout and finds all possible plays (it is currently up to the user to chose the best one). This could be used for other games such as "Crossword Anagrams" or "Wordy" as it has no special knowlege of any one game. The current setting of the board size, of course, makes it useful for Scrabble, but that's a 2-line parameter that can be easily changed. (actually since I first wrote that, the code has become more Scrabble-oriented) I wrote this while on vacation at a Wingate hotel which has T1 ethernet to the room. Very nice. It originally took about 6 hours to get it working, over two evenings, although I've put a little more time into it since. I wrote it specifically because although the Wordgame Programmers Archive (www.gtoal.com/wordgames/) has several Scrabble sources, there was none that was basic enough to take and modify for use in various utilities. I expect this program will be developed, but I am specifically freezing this version as a short and simple example program. Although there are two procedures in here which dive into the guts of a DAWG data struture, the general framework of the source is actually completely independent of DAWGs and any other data structure could be used (right down to a linear search through a file of strings, if you feel so inclined!). The two aforementioned procedures are not especially high-quality code and are specifically *not* part of the instructional purpose of this program. Just pay attention to the interface procedures which wrap them, and ignore the internal details entirely - they are not critical to the general algorithm. You'll need this data file: --- #..-...#.J.-..# .+...=...E...+. ..+...-.-S..+.. -..+...-.U.+..- ....+....I+.... .=...=...T.KOR. ..-...-.-I.IRE. #..-...ALCADE.# ..-...-.-...-.. .=...=...=...=. ....+.....+.... -..+...-...+..- ..+...-.-...+.. .+...=...=...+. #..-...#...-..# GIMSST* 0 0 --- To generate a dawg dictionary file, you'll need the "token" program from ../spell */ #include <stdio.h> #include <stdlib.h> #include <ctype.h> #include <string.h> #include <assert.h> #include <time.h> /* for randomising the shuffle */ int irand(int max) /* assert: max > 0 */ /* used in shuffle */ { long l = random(); if (l < 0) l = -l; l = l % max; /* assert: 0 <= l < max */ return((int)l); } #ifndef FALSE #define TRUE (0==0) #define FALSE (0!=0) #endif int spell_verbose = TRUE; static int superquit = FALSE; FILE *errfile = NULL; #include "global_analysis/latin1.c" #include "global_analysis/bitsigs.c" /* fast data structure based on Ars Magna anagram program */ #include "global_analysis/rackgen.c" /* fast internal unique rack generator */ #ifdef MEMDEBUG #include "mnemosyne.h" /* Transparent utility to debug malloc */ #endif /* Only externals used here are my 'dawg' library, and even that is only for loading the dict file. However in a later iteration I may move the two dawg-based procedures here to the library */ #include "splib.h" /* load dictionary */ static int debug = FALSE; static int megaanalysis = FALSE; /* If doing global_analysis */ static int opt_html = TRUE; /* Generate html or text? */ #define MIN(a, b) (a < b ? a : b) #define BLANKCH '?' /* We're making the assumption that the ascii code for '?' is not going to be used for a letter. See the explanation in gameconfig about charset portability */ #include "gameconfig.c" /* Board shape, scores, alphabet etc */ static int bestscore = 0; #define HORIZONTAL 1 #define VERTICAL 2 //------------------------------------------------------ START /* This is the format that we use to store potential plays in the first hack at global analysis. Below this is a work-in-progress struct which will be needed for multi-level lookahead, but which is not yet in use. Need to migrate from this structure to that one... */ typedef struct playfm { char place[8]; char pos[4]; char dirn; int score; int freq; int cumfreq; bitsig sig; } playfm; /* I'm being lazy here... */ #define MAX_PLAYS (176915 * 7) playfm play[MAX_PLAYS]; static int nextfree = 0; #include "global_analysis/sortfn.c" /* auxilliary function for sorting playfm array */ #define MAX_PLAYSCORE 2000 /* surely enough - has to be > highest score you can make on one play */ static double playscore[MAX_PLAYSCORE]; /* eg playscore[87] would contain the probability (count of the # of ways) of making a play worth 87. */ //------------------------------------------------------ END static int report = TRUE; typedef struct GAMESTATE { // THIS IS THE ALTERNATIVE TO THE ABOVE char board[HEIGHT+2][WIDTH+2]; // **UNDER DEVELOPMENT** char apparent_letter[HEIGHT+2][WIDTH+2]; int tiles_held /* = 0*/; char tiles[/*RACKSIZE*/MAX_TILES+1]; /* Empty, for static analysis */ char remaining_tiles[MAX_TILES+1]; /* Full, for static analysis */ int myscore /*= 0*/; int oppscore /*= 0*/; int vertical_score[HEIGHT+2][WIDTH+2]; char fullrack[MAX_TILES+1]; char *crosschecks[HEIGHT+2][WIDTH+2]; char *vertical_word[HEIGHT+2][WIDTH+2]; int vindex[HEIGHT+2][WIDTH+2]; char *boardfile /* = NULL*/; /* This should be in "main()" but for hacky reasons (HTML in playword()) it is global *for now* */ int bestscore; char playtiles[/*RACKSIZE*/MAX_TILES+1]; /* Empty, for static analysis */ char playword[/*RACKSIZE*/MAX_TILES+1]; /* Empty, for static analysis */ char playtilesleft[/*RACKSIZE*/MAX_TILES+1]; /* Empty, for static analysis */ char playtemplate[/*RACKSIZE*/MAX_TILES+1]; /* Empty, for static analysis */ int playplaced; int playLetter; int playnumber; int playorientation; int movesconsidered; } GAMESTATE; void display_form(GAMESTATE *game) { { int row, col; fprintf(stdout, "<FORM ACTION='/cgi-bin/allmoves.cgi' METHOD='POST'>\n"); fprintf(stdout, "It's your turn. Enter your tiles and I'll work out the possible moves and likely replies.<BR><BR><BR>\n"); fprintf(stdout, "<input name=\"dict\" type=\"hidden\" value=\"sowpods\">"); fprintf(stdout, "Enter your rack here:<BR>\n"); fprintf(stdout, "<select name=\"R0\" size=\"1\"><option>a<option>b<option>c<option>d<option>e<option>f<option>g<option>h<option>i<option>j<option>k<option>l<option>m<option>n<option>o<option>p<option>q<option>r<option>s<option>t<option>u<option>v<option>w<option>x<option>y<option>z<option>BLANK<option selected>No tile</SELECT>"); fprintf(stdout, "<select name=\"R1\" size=\"1\"><option>a<option>b<option>c<option>d<option>e<option>f<option>g<option>h<option>i<option>j<option>k<option>l<option>m<option>n<option>o<option>p<option>q<option>r<option>s<option>t<option>u<option>v<option>w<option>x<option>y<option>z<option>BLANK<option selected>No tile</SELECT>"); fprintf(stdout, "<select name=\"R2\" size=\"1\"><option>a<option>b<option>c<option>d<option>e<option>f<option>g<option>h<option>i<option>j<option>k<option>l<option>m<option>n<option>o<option>p<option>q<option>r<option>s<option>t<option>u<option>v<option>w<option>x<option>y<option>z<option>BLANK<option selected>No tile</SELECT>"); fprintf(stdout, "<select name=\"R3\" size=\"1\"><option>a<option>b<option>c<option>d<option>e<option>f<option>g<option>h<option>i<option>j<option>k<option>l<option>m<option>n<option>o<option>p<option>q<option>r<option>s<option>t<option>u<option>v<option>w<option>x<option>y<option>z<option>BLANK<option selected>No tile</SELECT>"); fprintf(stdout, "<select name=\"R4\" size=\"1\"><option>a<option>b<option>c<option>d<option>e<option>f<option>g<option>h<option>i<option>j<option>k<option>l<option>m<option>n<option>o<option>p<option>q<option>r<option>s<option>t<option>u<option>v<option>w<option>x<option>y<option>z<option>BLANK<option selected>No tile</SELECT>"); fprintf(stdout, "<select name=\"R5\" size=\"1\"><option>a<option>b<option>c<option>d<option>e<option>f<option>g<option>h<option>i<option>j<option>k<option>l<option>m<option>n<option>o<option>p<option>q<option>r<option>s<option>t<option>u<option>v<option>w<option>x<option>y<option>z<option>BLANK<option selected>No tile</SELECT>"); fprintf(stdout, "<select name=\"R6\" size=\"1\"><option>a<option>b<option>c<option>d<option>e<option>f<option>g<option>h<option>i<option>j<option>k<option>l<option>m<option>n<option>o<option>p<option>q<option>r<option>s<option>t<option>u<option>v<option>w<option>x<option>y<option>z<option>BLANK<option selected>No tile</select><br>\n"); fprintf(stdout, "\n"); fprintf(stdout, "Score: <INPUT NAME=score VALUE=\"\"><BR>\n"); fprintf(stdout, "Number of tiles left: <INPUT NAME=remaining VALUE=\"\"><BR>\n"); fprintf(stdout, "\n"); fprintf(stdout, "<TABLE BORDER Align='center'>\n"); fprintf(stdout, "\n"); fprintf(stdout, "Enter this play in <em>lower case</em>. Enter blanks in UPPER CASE.\n"); fprintf(stdout, "\n"); fprintf(stdout, "<TR Align='center'><TD></TD>\n"); fprintf(stdout, "\n"); fprintf(stdout, "<TD>A</TD><TD>B</TD><TD>C</TD><TD>D</TD><TD>E</TD><TD>F</TD><TD>G</TD><TD>H</TD><TD>I</TD><TD>J</TD><TD>K</TD><TD>L</TD><TD>M</TD><TD>N</TD><TD>O</TD></TR>\n"); for (col = 1; col <= 15; col++) { int letter; fprintf(stdout, "<TR Align='center'><TD>%d</TD>\n", col); for (row = 1; row <= 15; row++) { letter = game->board[col][row]; if (isupper(letter)) { // needs work for i18n version (latin1) letter = tolower(letter); } else { letter = toupper(letter); } fprintf(stdout, "<TD><INPUT TYPE='Text' NAME='%c%d' VALUE='%c' SIZE=1 MAXLENGTH=1></TD>\n", col-1+'a', row, letter); } fprintf(stdout, "</TR>\n"); fprintf(stdout, "\n"); } fprintf(stdout, "</TABLE><BR>\n"); fprintf(stdout, "\n"); fprintf(stdout, "<INPUT TYPE='submit' VALUE='Find all moves!'>\n"); fprintf(stdout, "</FORM><BR>\n"); } } /* Probability function used in both global analysis and rackleave calc */ #include "global_analysis/probfn.c" //#include "global_analysis/movegen.c" // IN-PROGRESS - currently uses playword. However the // recursive_playword below is needed for multi-level minimax. // need to merge the two procedures static char lookahead[8096]; void recursive_playword(GAMESTATE *game, char *tiles, char *word, char *tilesleft, char *template, int placed, int Letter, int number, int orientation) { int hscore = 0, vscore = 0; int row, col; int tmp, tmpch, i, nexttile = 0; int wmult = 1; char *s = word; char outbuff[1024]; char *outbuffp = outbuff; int ga_score, confidence; tiles[placed] = '\0'; if (superquit) return; /* we treat the board as horizontal even if it's not */ row = (orientation == HORIZONTAL ? number : Letter-'A'+1); col = (orientation == HORIZONTAL ? Letter-'A'+1 : number); if (report) outbuffp += sprintf(outbuffp, (orientation == VERTICAL ? "place %s at %c%d %s (%s) => " : "place %s at %d%c %s (%s) => "), tiles, (orientation == VERTICAL ? Letter : number), (orientation == VERTICAL ? number : Letter), (orientation == VERTICAL ? "down" : "across"), template); assert((word != NULL) && (game->boardfile != NULL) && (game->fullrack != NULL) && (tiles != NULL)); if (report) { if (opt_html) { outbuffp += sprintf(outbuffp, "<A HREF=\"play?word=%s&board=%s&bagwas=%s&rackwas=%s%s&letter=%c&number=%d&orientation=%d&game->myscore=%d&game->oppscore=%d\">%s</A>", word, game->boardfile, game->fullrack, tiles,tilesleft, Letter, number, orientation, game->myscore, game->oppscore, word); } else { outbuffp += sprintf(outbuffp, "%s", word); } } i = col; for (;;) { if (*s == '\0') break; if (game->board[row][i] == 0) { /* Place first letter from "tiles" here */ hscore += ((tmp = score[tmpch = tiles[nexttile++]]) * lettermult[row][i]); if (debug) outbuffp += sprintf(outbuffp, "row %d col %d: add %d%s to hscore for %c\n", row, i, tmp, (lettermult[row][i] == 2 ? "*2L" : ( lettermult[row][i] == 3 ? "*3L" : "") ), tmpch); wmult *= wordmult[row][i]; // BUG: when we call playword again just to print the best scoring move, // it is failing to add in the vscore component. Don't yet know why... // it MAY be because the orientation has changed (best score was across, // we've since also done the downs, and perhaps the move evaluator is // looking at some global state that is not stored in the playlist. // perhaps I need to keep 2 copies of the cross-check info and index // into them by orientation. if (game->vertical_word[row][i] != NULL) { tmp = (game->vertical_score[row][i] + (score[tmpch] * lettermult[row][i])) * wordmult[row][i]; vscore += tmp; game->vertical_word[row][i][game->vindex[row][i]] = *s; /* Plug in the letter to the vword */ if (report) outbuffp += sprintf(outbuffp, "/%s(%d", game->vertical_word[row][i], tmp); if (wordmult[row][i] != 1) if (report) outbuffp += sprintf(outbuffp, ":%dWS", wordmult[row][i]); if (report) outbuffp += sprintf(outbuffp, ")"); game->vertical_word[row][i][game->vindex[row][i]] = '?'; /* restore */ } if (debug) if (game->vertical_word[row][i] != NULL) { outbuffp += sprintf(outbuffp, "Intersects with %s ", game->vertical_word[row][i]); game->vertical_word[row][i][game->vindex[row][i]] = *s; /* Plug in the letter to the vword */ outbuffp += sprintf(outbuffp, "forming %s, worth %d", game->vertical_word[row][i], tmp); if (wordmult[row][i] != 1) outbuffp += sprintf(outbuffp, " (%dWS)", wordmult[row][i]); game->vertical_word[row][i][game->vindex[row][i]] = '?'; /* restore */ outbuffp += sprintf(outbuffp, "\n"); } } else { /* The tile is already placed - game->apparent_letter takes care of blanks */ /* no bonuses, AND DO NOT ADD IN VSCORE!!! */ tmp = score[tmpch = game->apparent_letter[row][i]]; hscore += tmp; if (debug) outbuffp += sprintf(outbuffp, "add %d to hscore for %c\n", tmp, tmpch); } s += 1; i += 1; } /* simple hscore, and vscores now all added */ if (debug) if (wmult != 1) outbuffp += sprintf(outbuffp, "Multiply score by %d\n", wmult); hscore *= wmult; assert(nexttile == placed); if (placed == RACKSIZE) { hscore += BINGO; if (debug) outbuffp += sprintf(outbuffp, "Add 50 for a bingo!\n"); } if (megaanalysis) { if (report) outbuffp += sprintf(outbuffp, ". SCORE %d\n", hscore+vscore); } else { if (report) { char do_global_analysis[1024]; FILE *ga; int rc; outbuffp += sprintf(outbuffp, ", leaving %s. SCORE %d. ", tilesleft, hscore+vscore); // --word \"%s\" // --board "%s\" // --bagwas \"%s\" // --rackwas \"%s%s\" // --letter \"%c\" // --number \"%d\" // --orientation \"%d\" // --myscore \"%d\" // --oppscore \"%d\" sprintf(do_global_analysis, "~/gtoal.com/wordgames/scrabble_solver/global_analysis/play" " --word \"%s\"" // " --board \"/home/gtoal/gtoal.com/wordgames/boardgen/temp/wsc_%s.dat\"" " --board \"%s\"" " --bagwas \"%s\"" /* THIS IS PASSING THE WRONG VALUE FOR USED BLANKS (tiles) */ " --rackwas \"%s%s\"" " --letter \"%c\"" " --number \"%d\"" " --orientation \"%d\"" " --myscore \"%d\"" " --oppscore \"%d\"", word, game->boardfile, game->fullrack, tiles, tilesleft, Letter, number, orientation, game->myscore+hscore+vscore, game->oppscore ); ga = popen(do_global_analysis, "r"); rc = fscanf(ga, "%d", &ga_score); if (rc != 1) ga_score = 0; outbuffp += sprintf(outbuffp, "(- <= %d with %d%% confidence => %d)\n", ga_score, confidence, hscore+vscore-ga_score); pclose(ga); } } if (megaanalysis && report) { static int printed = 0, local_best_score = 0, moves = 0; moves += 1; if (moves == 1000000) { outbuffp = outbuff; // throw current output away outbuffp += sprintf(outbuffp, "\nOK, I\'m quitting after 1,000,000 moves so as not to be antisocial to my ISP!\n\n"); outbuffp += sprintf(outbuffp, "\nIt might be better if you don't get into 'mega analysis' mode until after the\n"); outbuffp += sprintf(outbuffp, "board has been closed up a bit and there aren't so many choices of move available...\n"); outbuffp += sprintf(outbuffp, "(I.e. go back to the previous page and enter your tiles, rather than leaving them\n"); outbuffp += sprintf(outbuffp, " all set to \'No Tile\')\n\n"); superquit = TRUE; *outbuffp = '\0'; fprintf(stdout, "%s", outbuff); outbuffp = outbuff; } if ((printed < 21) || (hscore+vscore > local_best_score)) { static int announced = 0; if ((announced == 0) && (printed >= 21)) { announced = 1; outbuffp += sprintf(outbuffp, "\nToo many results are being returned. From now on we'll only list ones with better scores...\n\n"); } local_best_score = hscore+vscore; *outbuffp = '\0'; fprintf(stdout, "%sThe probability of playing this move is P(\"%s\", \"%s\") = %1.7f\n", outbuff, tiles, game->fullrack, RackProb(tiles, game->fullrack, 7 /*BUG*/)); printed += 1; } fflush(stdout); if ((strlen(word) + strlen(game->boardfile) + strlen(game->fullrack) + strlen(tiles) + strlen(tilesleft)) > 7000) exit(1); // hacking attempt on web interface? sprintf(lookahead, "~gtoal/gtoal.com/wordgames/scrabble_solver/global_analysis/play" " --word \"%s\"" " --board \"%s\"" " --bagwas \"%s\"" " --rackwas \"%s%s\"" " --letter \"%c\"" " --number \"%d\"" " --orientation \"%d\"" " --myscore \"%d\"" " --oppscore \"%d\"", word, game->boardfile, game->fullrack, tiles, tilesleft, Letter, number, orientation, game->myscore, game->oppscore, word); //system(lookahead); // and we want a single numeric value returned as a result! //fflush(stdout); } else { *outbuffp = '\0'; fprintf(stdout, "%s", outbuff); } outbuffp = outbuff; game->movesconsidered += 1; if (hscore+vscore > game->bestscore) { game->bestscore = hscore+vscore; strcpy(game->playtiles, tiles); strcpy(game->playword, word); strcpy(game->playtilesleft, tilesleft); strcpy(game->playtemplate, template); game->playplaced = placed; game->playLetter = Letter; game->playnumber = number; game->playorientation = orientation; } /* I guess the recursive call to 'minimax()' should go here??? */ } static int myscore = 0; static int oppscore = 0; static char *dict = NULL; static int vertical_score[HEIGHT+2][WIDTH+2]; static int tiles_held = 0; static char tiles[/*RACKSIZE*/MAX_TILES+1]; static char fullrack[MAX_TILES+1]; static char *crosschecks[HEIGHT+2][WIDTH+2]; static char *vertical_word[HEIGHT+2][WIDTH+2]; static int vindex[HEIGHT+2][WIDTH+2]; static char remaining_tiles[MAX_TILES+1]; static char *boardfile = NULL; /* This should be in "main()" but for hacky reasons (HTML in playword()) it is global *for now* */ static int orientation = 0; static NODE *dawg; static INDEX nedges; /*--------------------- crosscheck code -------------------------*/ /* When placing a word horizontally, if it abuts to any tiles either above or below, we will have generated a wildcard string consisting of the tiles above, a "?", and the tiles below, from which we then find all the valid words allowed which match that wildcard string. From that we find which letters were valid at that position, and we return the set of valid letters to the scrabble code, so that the search for legal plays can be trimmed for speed. */ /* External interface procedure follows this internal procedure, which needs a lot of extra parameters that the interface isn't really interested in */ #include "crosscheck.c" /* This is a 'black box' procedure which can be replaced by anything which behaves the same even though implemented differently. In fact the internal code of this function is a 'quick hack' and probably should be replaced by something neater. INTERFACE: inputs: word list, wild-card string containing one "?" output: string of letters which the "? could represent or NULL if none found. (i.e this square is blocked) example: twl98, "c?t" -> "auo" (cat, cut and cot all matching the pattern) */ char *convert_wildcard_to_permitted_set(NODE *dawg, char *word) { char result[MAX_WORD_LEN]; static char set[MAX_WORD_LEN]; int i = 0; *set = '\0'; if (debug) fprintf(stderr, "wildcard: %s\n", word); (void)crosscheck_internal(dawg, (INDEX)ROOT_NODE, word, result, set, 0, &i); if (i == 0) return(NULL); if (debug) fprintf(stderr, " -> %s\n", set); return(set); } /*--------------------- placement code -------------------------*/ /* This finds words which can be played at this row,column position of the length requested. It does a wildcard match of a string such as "c[aiou]t[etrains]" which in this instance might return cats, cots, cute & cuts. The search is pruned in two ways - the first, by which letters are allowed on certain squares, eg [aiou] may be a constraint from cross-check words (see above); and the second by the tiles in your rack (eg "etrains"). If you hold a blank, the wildcard might look like "c[aiou]t?" instead. The decision to do separate searches for different lengths of words was a deliberate one, to simplify the code. Some scrabble programs would prefer to do searches which looked more like "c[aiou]t*" allowing more letters to be added at the right. I chose not to do this for simplicity, and also because some other optimisations are possible when you are using fixed-length searches (although those have not yet been done). This procedure is also passed the tiles which we hold, because the wildcard itself does not describe only valid words. For instance, if we hold the tiles "act" then the wildcard might be "[act][act][act]" - but "tat" could not be played even though wildcard expansion listed it. By removing the tiles played at each point for a wild letter, we end up playing only "act" and "cat" here. */ #ifdef NEVER void playword(char *tiles, char *word, char *tilesleft, char *template, int placed, int Letter, int number, int orientation) { int hscore = 0, vscore = 0; int row, col; int tmp, tmpch, i, nexttile = 0; int wmult = 1; char *s = word; double prob; tiles[placed] = '\0'; /* we treat the board as horizontal even if it's not */ row = (orientation == HORIZONTAL ? number : Letter-'A'+1), col = (orientation == HORIZONTAL ? Letter-'A'+1 : number), prob = RackProb(tiles, fullrack, 7 /* BUG: TESTING. ADJUST LATER */); /* error if any wanted tile in "Tiles" does not exist in "Bag" */ // tiles-to-play where how resulting-words score no-of-ways-of-picking-tiles // nounier 3A across nounier/etape(7)/rob(6) 79 12 #ifdef GLOBALTEST #else fprintf(stdout, (orientation == VERTICAL ? "place %s (drawprob %1.2f%%) at %c%d %s (%s) => " : "place %s (drawprob %1.2f%%) at %d%c %s (%s) => "), tiles, prob, (orientation == VERTICAL ? Letter : number), (orientation == VERTICAL ? number : Letter), (orientation == VERTICAL ? "down" : "across"), template); #endif assert((word != NULL) && (boardfile != NULL) && (fullrack != NULL) && (tiles != NULL)); #ifndef GLOBALTEST if (opt_html) { fprintf(stdout, "<A HREF=\"play?word=%s&board=%s&bagwas=%s+&rackwas=%s%s&letter=%c&number=%d&orientation=%d&myscore=%d&oppscore=%d&dict=%s\">%s</A>", word, boardfile, fullrack, tiles,tilesleft, Letter, number, orientation, myscore, oppscore, dict == NULL ? "" : dict, word); } else { fprintf(stdout, "%s", word); } fflush(stdout); #endif i = col; for (;;) { if (*s == '\0') break; if (board[row][i] == 0) { /* Place first letter from "tiles" here */ hscore += ((tmp = score[tmpch = tiles[nexttile++]]) * lettermult[row][i]); if (debug) fprintf(stdout, "row %d col %d: add %d%s to hscore for %c\n", row, i, tmp, (lettermult[row][i] == 2 ? "*2L" : ( lettermult[row][i] == 3 ? "*3L" : "") ), tmpch); wmult *= wordmult[row][i]; if (vertical_word[row][i] != NULL) { tmp = (vertical_score[row][i] + (score[tmpch] * lettermult[row][i])) * wordmult[row][i]; vscore += tmp; vertical_word[row][i][vindex[row][i]] = *s; /* Plug in the letter to the vword */ #ifndef GLOBALTEST fprintf(stdout, "/%s(%d", vertical_word[row][i], tmp); if (wordmult[row][i] != 1) fprintf(stdout, ":%dWS", wordmult[row][i]); fprintf(stdout, ")"); fflush(stdout); #endif vertical_word[row][i][vindex[row][i]] = BLANKCH; /* restore */ } if (debug) if (vertical_word[row][i] != NULL) { fprintf(stdout, "Intersects with %s ", vertical_word[row][i]); vertical_word[row][i][vindex[row][i]] = *s; /* Plug in the letter to the vword */ fprintf(stdout, "forming %s, worth %d", vertical_word[row][i], tmp); if (wordmult[row][i] != 1) fprintf(stdout, " (%dWS)", wordmult[row][i]); vertical_word[row][i][vindex[row][i]] = BLANKCH; /* restore */ fprintf(stdout, "\n"); } } else { /* The tile is already placed - apparent_letter takes care of blanks */ /* no bonuses */ tmp = score[tmpch = apparent_letter[row][i]]; hscore += tmp; if (debug) fprintf(stdout, "add %d to hscore for %c\n", tmp, tmpch); if (debug) fprintf(stdout, "board[row %d][col %d] = %c{%c}\n", row, i, board[row][i], apparent_letter[row][i]); } s += 1; i += 1; } /* simple hscore, and vscores now all added */ if (debug) if (wmult != 1) fprintf(stdout, "Multiply score by %d\n", wmult); hscore *= wmult; if (nexttile != placed) { //would appear not to have been changed anywhere? fflush(stdout); fprintf(stderr, "\n\nassertion failed: nexttile=%d placed=%d\n", nexttile, placed); exit(0); } if (placed == RACKSIZE) { hscore += BINGO; if (debug) fprintf(stdout, "Add 50 for a bingo!\n"); } if (megaanalysis) { strcpy(play[nextfree].place, tiles); sprintf(play[nextfree].pos, "%c%0d", Letter, number); play[nextfree].dirn = orientation; play[nextfree].score = hscore+vscore; play[nextfree].freq = prob; makeonesig(play[nextfree].place, play[nextfree].sig); if (nextfree >= MAX_PLAYS) { fprintf(stderr, "Too many plays - increase MAX_PLAYS\n"); exit(0); } nextfree += 1; fprintf(stdout, ". SCORE %d\n", hscore+vscore); } else { fprintf(stdout, ", leaving %s. SCORE %d\n", tilesleft, hscore+vscore); } // NO LONGER DONE HERE playscore[hscore+vscore] += prob; fflush(stdout); } #endif /* External interface procedure follows this internal procedure, which needs a lot of extra parameters that the interface isn't really interested in */ #include "fix_scrabble.c" /* This is a 'black box' procedure which can be replaced by anything which behaves the same even though implemented differently. In fact the internal code of this function (above) is a 'quick hack' and probably should be replaced by something neater. INTERFACE: inputs: word list, wild-card, rack, row & column outputs: should be list of words to play. (Currently the output is printed, not returned) The wild card can contain only fixed letters, [abcd...] (sets of letters), or "?" (single-character wild-card). There are no multi- character wild-cards (eg "*") - the word length is fixed. IMPORTANT NOTE: "cat?" is not the same as "c[a]t?" - the former expects ONE tile to be placed, the latter exects TWO. I.e. simple letters represent tiles already placed on the board. A cleaned-up interface would not need L/n (letter/number, ie row/col in format such as "H7 across" or "G5 down". This procedure should just return (or action) the list of words, and doesn't need to know where they are placed if it is not doing the placement itself. */ int scrabble_match( #ifdef USE_GAMESTATE GAMESTATE *game, #endif NODE *dawg, char *word, char *tiles, int L, int n, int orientation) { char result[MAX_WORD_LEN]; char placedtiles[MAX_WORD_LEN]; int i = 0; //fprintf(stderr, "match 1\n"); result[0] = '\0'; placedtiles[0] = '\0'; fix_scrabble( #ifdef USE_GAMESTATE game, #endif dawg, (INDEX)ROOT_NODE, word, word, result, tiles, placedtiles, 0, 0, &i, L, n, orientation); //fprintf(stderr, "match 2\n"); return(i); } /*--------------------------------------------------------------*/ /* This reads a board in the simple file format which Kevin Cowtan uses in his board-graphic image generator software. I just happened to have some cgi web page software which generates this format so I plan to reuse it to create a graphical front-end to solving scrabble problems. */ #include "readboard.c" /*--------------------------------------------------------------*/ /* This looks at the place where a tile is likely to be played, and whether there are tiles abutting above or below it, which would permit or deny a word to be placed there. This generates a wild-card string which is then expanded in the code above to generate the set of letters which are valid here. It is that set which is stored, not the wild card string or the words themselves. */ #ifdef USE_GAMESTATE char *create_crosscheck_wildcard(GAMESTATE *game, int r, int c) { char crosscheck[WIDTH+2]; char *s; int rp; game->vertical_score[r][c] = 0; /* Already a tile here so crosscheck is meaningless */ if (game->board[r][c] != 0) return(NULL); /* none above and none below? */ if ((game->board[r-1][c] == 0) && (game->board[r+1][c] == 0)) return(NULL); /* what's above? */ rp = r-1; while (game->board[rp][c] != 0) rp -= 1; rp += 1; /* row of 1st letter in crosscheck word */ s = crosscheck; while (rp != r) { *s = game->apparent_letter[rp][c]; game->vertical_score[r][c] += score[*s]; /* 0 if blank */ if (*s == '?') *s = board[rp][c]; // FIX FOR BUG IN "wildcard:" s += 1; rp += 1; } *s++ = '?'; /* r */ game->vindex[r][c] = s-crosscheck-1; /* what's below? */ rp = r+1; while (game->board[rp][c] != 0) { *s = game->apparent_letter[rp][c]; game->vertical_score[r][c] += score[*s]; /* 0 if blank */ if (*s == '?') *s = board[rp][c]; // FIX FOR BUG IN "wildcard:" s += 1; rp += 1; } *s = '\0'; game->vertical_word[r][c] = strdup(crosscheck); return(strdup(crosscheck)); } #else char *create_crosscheck_wildcard(int r, int c) { char crosscheck[WIDTH+2]; char *s; int rp; vertical_score[r][c] = 0; /* Already a tile here so crosscheck is meaningless */ if (board[r][c] != 0) return(NULL); /* none above and none below? */ if ((board[r-1][c] == 0) && (board[r+1][c] == 0)) return(NULL); /* what's above? */ rp = r-1; while (board[rp][c] != 0) rp -= 1; rp += 1; /* row of 1st letter in crosscheck word */ s = crosscheck; while (rp != r) { *s = apparent_letter[rp][c]; vertical_score[r][c] += score[*s]; /* 0 if blank */ if (*s == '?') *s = board[rp][c]; // FIX FOR BUG IN "wildcard:" s += 1; rp += 1; } *s++ = BLANKCH; /* r */ vindex[r][c] = s-crosscheck-1; /* what's below? */ rp = r+1; while (board[rp][c] != 0) { *s = apparent_letter[rp][c]; vertical_score[r][c] += score[*s]; /* 0 if blank */ if (*s == '?') *s = board[rp][c]; // FIX FOR BUG IN "wildcard:" s += 1; rp += 1; } *s = '\0'; vertical_word[r][c] = strdup(crosscheck); return(strdup(crosscheck)); } #endif /*--------------------------------------------------------------*/ /* Can we slot a word into the board at this row,col and length(*)? If so, then the very last thing we do before returning is to actually generate the words. This is not really good design - we should pass the wildcard which this generates up a level and have that level do the search. This procedure generates a wild-card string using fixed letters, "[...]" letter sets, and wildcards ("?") which when expanded returns valid words which can be played here. *: Note 'length' here doesn't mean word length. It means number of tiles to be placed. The word may be longer because of tiles already on the board that we are placing around. */ void slot_internal( GAMESTATE *game, #define GAME game, #define STATE game-> int orig_r, int orig_c, int r, int c, int still_to_place, int placed, char *pattern, int touches_center, int touches_horiz, int touches_vert, int orientation) { int valid; int pp = strlen(pattern); int opp = strlen(pattern); char *s; //fprintf(stderr, "slot 1\n"); /* No more loop. Just place one, and recurse. If I had done this with the scrabble program to begin with, I wouldn't have had the stupid bug to do with playing blanks as the same letter as another letter you hold in the rack... */ /* Ran off end of board with tiles still to place? */ if ((c > WIDTH) && (still_to_place > 0)) return; /* Covered with an earlier hook? */ if ((placed == 0) && (STATE board[r][c-1] != 0) && (c != 1)) return; /* (special case for first col ... */ //fprintf(stderr, "slot 2\n"); while (STATE board[r][c] != 0) { /* There is already a tile on the board here. Careful with blanks */ pattern[pp++] = STATE apparent_letter[r][c++]; pattern[pp] = '\0'; } if ((c) > WIDTH) return; /* Oops, we went over the edge */ /* If center square was empty this must be the first move */ if ((STATE board[WIDTH/2+1][HEIGHT/2+1] == 0) && (((WIDTH/2)+1 == r) && (c == (WIDTH/2)+1)) ) { touches_center = (touches_center || TRUE); } if (STATE board[r][c-1] != 0 || STATE board[r][c+1] != 0) touches_horiz = (touches_horiz || TRUE); if (STATE board[r-1][c] != 0 || STATE board[r+1][c] != 0) touches_vert = (touches_vert || TRUE); //fprintf(stderr, "slot 3\n"); if (STATE crosschecks[r][c] == NULL) { /* no tiles above or below in this col */ if (strchr(STATE tiles, '?') != NULL) { /* We hold a blank so it could be anything */ sprintf(pattern+pp, "?"); } else { /* restrict search space when possible, ie only those letters for which we hold tiles */ sprintf(pattern+pp, "[%s]", STATE tiles); } } else if (*STATE crosschecks[r][c] == '!') { /* If no valid crosscheck at all, then reject this slot - square is blocked */ pattern[opp] = '\0'; /* undo recursion */ return; } else if ((strcmp(STATE crosschecks[r][c], "?") == 0) && (strchr(STATE tiles, '?') == NULL)) { /* we don't hold a blank, so do a reduction-in-strength of a wild-card "?" to [abcdefg] (i.e. only the tiles we hold) */ sprintf(pattern+pp, "[%s]", STATE tiles); } else if (strcmp(STATE crosschecks[r][c], "?") == 0) { /* we do hold a blank, so allow anything */ sprintf(pattern+pp, "%s", STATE crosschecks[r][c]); } else { /* This letter is constrained by specific cross-checks */ sprintf(pattern+pp, "[%s]", STATE crosschecks[r][c]); } //fprintf(stderr, "slot 4\n"); pp = strlen(pattern); pattern[pp] = '\0'; placed += 1; still_to_place -= 1; if (still_to_place != 0) { /* Should this test be moved up before some of the code above? */ //fprintf(stderr, "slot 5\n"); slot_internal(GAME orig_r, orig_c, r, c+1, still_to_place, placed, pattern, touches_center, touches_horiz, touches_vert, orientation); //fprintf(stderr, "slot 5a\n"); pattern[opp] = '\0'; /* undo recursion */ //fprintf(stderr, "slot 5b\n"); return; } /* after placing all tiles, are there still some letters abutting to the right? If so, add them to the string to be matched */ {int p = c+1; while (STATE board[r][p] != 0) { /*hscore += ...[r][p];*/ touches_horiz = TRUE; pattern[pp++] = STATE apparent_letter[r][p++]; /* Careful with blanks */ }} pattern[pp] = '\0'; //fprintf(stderr, "slot 6\n"); valid = (touches_horiz || (touches_vert && (placed > 1)) || (touches_center && (placed > 1))); //fprintf(stderr, "slot 7\n"); if (valid) scrabble_match(GAME dawg, pattern, STATE tiles, (orientation == HORIZONTAL ? orig_c : orig_r)-1+'A', (orientation == HORIZONTAL ? orig_r : orig_c), orientation); /* the x/y param could be done more intelligently */ //fprintf(stderr, "slot 8\n"); } #undef STATE #undef GAME int slot( GAMESTATE *game, int r, int c, int l, int orientation) { char pattern[(WIDTH*(256+2))+1]; pattern[0] = '\0'; slot_internal( #ifdef USE_GAMESTATE game, #endif r, c, r, c, l, 0, pattern, FALSE, FALSE, FALSE, orientation); //fprintf(stderr, "slot returned\n"); return(0); /* Needs to be true if any were placed */ } int main(int argc, char **argv) { int i; int row = 0, col = 0; int orientation = 0; int length = 0; char *dict = NULL; GAMESTATE *game; if (debug) report = TRUE; if (opt_html) report = TRUE; game = calloc(1, sizeof(struct GAMESTATE)); if (argc != 3) { if (argc <= 2) { dict = "SOWPODS"; } else { fprintf(stderr, "syntax: %s board.dat dict?\n", argv[0]); exit(1); } } else dict = argv[2]; /* Default word list is TWL98 */ /* default param when debugging makes running gbd easier */ if (argv[1] == NULL) game->boardfile = "test.txt"; else game->boardfile = argv[1]; if (!dawg_init(dict, &dawg, &nedges)) { fprintf(stderr, "%s: cannot open wordlist '%s'\n", argv[0], dict); exit(2); } /* Clear the arrays */ assert(WIDTH == HEIGHT); for (row = 0; row < (HEIGHT+2); row++) { for (col = 0; col < (WIDTH+2); col++) { game->board[row][col] = 0; game->apparent_letter[row][col] = 0; game->crosschecks[row][col] = NULL; game->vertical_word[row][col] = NULL; game->vindex[row][col] = 0; } } for (i = 0; i < 256; i++) { tot[i] = 2; /* Blank ;-) */ score[i] = 0; totprob[i] = 0.0; watkins_rackvalue[i] = 25.0; } for (i = 0; i < 26; i++) { tot['a'+i] = itot[i]; score['a'+i] = iscore[i]; watkins_rackvalue['a'+i] = iwatkins_rackvalue[i]; } /* hand-coded random board. This could be replaced by a more complex format which includes tile distributions, values, language etc */ strcpy(game->fullrack, FULLRACK); /* If no tiles given in file, do megamove analysis */ read_board(game, game->boardfile); display_form(game); if (game->tiles[0] == '\0') { megaanalysis = TRUE; game->tiles_held = strlen(game->fullrack); strcpy(game->tiles, game->fullrack); } fprintf(stdout, "\nUnseen tiles are: %s\n\n", game->fullrack); for (orientation = HORIZONTAL; orientation <= VERTICAL; orientation++) { /* We assume throughout that we are examining only horizontal plays, and just flip the board to handle vertical plays. We flip it back at the end, in case we want to update it with a play and write it back to file. (but we don't do that yet) */ for (row = 1; row <= HEIGHT; row++) { /* calculate game->crosschecks for this row before we look at placements for this row. We could actually do this for the whole board first, but it isn't necessary */ for (col = 1; col <= WIDTH; col++) { char *s; /* Clean up from previous tries */ /* This is about our only use of the heap and I bet if we were to make these fixed size statics it would speed up the code... */ if (game->crosschecks[row][col] != NULL) free(game->crosschecks[row][col]); if (game->vertical_word[row][col] != NULL) free(game->vertical_word[row][col]); game->vertical_word[row][col] = NULL; /* first of all get wildcard pattern, eg "w?rd" : */ game->crosschecks[row][col] = create_crosscheck_wildcard(game, row, col); /* Then convert it to a set of valid letters */ if (game->crosschecks[row][col] == NULL) { /* game->crosschecks don't make sense here */ game->crosschecks[row][col]= strdup("?"); } else { s = convert_wildcard_to_permitted_set(dawg, game->crosschecks[row][col]); free(game->crosschecks[row][col]); if (s == NULL) { /* No letter can currently be placed in this square legally */ game->crosschecks[row][col] = strdup("!"); /* "!" is easier to debug than empty string */ } else { game->crosschecks[row][col] = strdup(s); } } } /* Now do placements: try every square on this row as position for first tile to be placed down. Rest follow automatically */ for (col = 1; col <= WIDTH; col++) { /* if (debug) fprintf(stderr, "Testing row %d col %d\n", row, col); */ for (length = 1; length <= MIN(game->tiles_held, 7); length++) { /* Can I legally place this many tiles onto the board here? */ if (slot(game, row, col, length, orientation)) { /* would be nice to pass the wildcard string back to here and then expand it/search for plays, and make the plays separately. The hack above was for convenience only. */ /* record row,col,length and wildcard for move generator (maybe) */ } } } } /* Try every row on the board */ /* flip the board around the x=y diagonal */ for (row = 1; row <= HEIGHT; row++) { for (col = 1; col <= WIDTH; col++) { int i; if (row > col) { /* avoid doing twice (no-op) */ /* swap board and game->apparent_letter at row,col */ i = game->board[row][col]; game->board[row][col] = game->board[col][row]; game->board[col][row] = i; i = game->apparent_letter[row][col]; game->apparent_letter[row][col] = game->apparent_letter[col][row]; game->apparent_letter[col][row] = i; } } } } /* end of horiz/vert placement loop */ /* If want to make best play on board and save it, do it here */ report = TRUE; // OUTSTANDING BUG: when playword displays this move again, it forgets to add in the vscore component!??! if (game->bestscore != 0) { /* if orientation == VERTICAL it expects the board to have already been swapped... quick hack... */ if (game->playorientation == VERTICAL) { /* flip the board around the x=y diagonal */ for (row = 1; row <= HEIGHT; row++) { for (col = 1; col <= WIDTH; col++) { int i; if (row > col) { /* avoid doing twice (no-op) */ /* swap board and game->apparent_letter at row,col */ i = game->board[row][col]; game->board[row][col] = game->board[col][row]; game->board[col][row] = i; i = game->apparent_letter[row][col]; game->apparent_letter[row][col] = game->apparent_letter[col][row]; game->apparent_letter[col][row] = i; } } } } // redo crosschecks: THIS DUPLICATED CODE SHOULD BE MADE INTO A PROCEDURE for (row = 1; row <= HEIGHT; row++) { /* calculate game->crosschecks for this row before we look at placements for this row. We could actually do this for the whole board first, but it isn't necessary */ for (col = 1; col <= WIDTH; col++) { char *s; /* Clean up from previous tries */ /* This is about our only use of the heap and I bet if we were to make these fixed size statics it would speed up the code... */ if (game->crosschecks[row][col] != NULL) free(game->crosschecks[row][col]); if (game->vertical_word[row][col] != NULL) free(game->vertical_word[row][col]); game->vertical_word[row][col] = NULL; /* first of all get wildcard pattern, eg "w?rd" : */ game->crosschecks[row][col] = create_crosscheck_wildcard(game, row, col); /* Then convert it to a set of valid letters */ if (game->crosschecks[row][col] == NULL) { /* game->crosschecks don't make sense here */ game->crosschecks[row][col]= strdup("?"); } else { s = convert_wildcard_to_permitted_set(dawg, game->crosschecks[row][col]); free(game->crosschecks[row][col]); if (s == NULL) { /* No letter can currently be placed in this square legally */ game->crosschecks[row][col] = strdup("!"); /* "!" is easier to debug than empty string */ } else { game->crosschecks[row][col] = strdup(s); } } } } fprintf(stdout, "\n"); recursive_playword(game, game->playtiles, game->playword, game->playtilesleft, game->playtemplate, game->playplaced, game->playLetter, game->playnumber, game->playorientation); } fprintf(stdout, "%d moves were considered for this play\n", game->movesconsidered); exit(0); }