AI Boggle game (python algorithm)

Image

Author: Tianyi (Bruce) Chen

Image

In this passage, I’ll use python to code to let AI find all solutions of a boggle game quickly.

Introduction:

If you don’t know the rule of boggle game, here’s a brief explanation of how boggle game works: Boggle is a word game played on a NxN grid of lettered dice. Players search for words by connecting adjacent letters (horizontally, vertically, or diagonally) to form words of three letters or more within a time limit, usually two minutes. Points are awarded based on the length of the words found, and the player with the highest score wins. (For specific rules of boggle games: https://en.wikipedia.org/wiki/Boggle)

Image

Design procedure:

To realize the whole function, like all AI program, we should read the dataset first. So there should be function to read through the board and the dictionary which contains all words. Secondly, we should do design some function to pre-process our data. Thirdly, we should feed the data into the searching functions. Last, there should be a overall function to save the output and concatenate all functions together.

Read the board and dictionary

loadBoard() use the line compression to read through the board. The line is read as type ‘String’, the method ‘split()’ is to separate each letter with space.
printBoard() is just to print out the read board.

def loadBoard(board_path):
  return  [line.split() for line in open(board_path)]
def printBoard(board):
  for item in board:
    print(" ".join(item)) # use item(list type) to iterate and concatenate with " "

readDictionary() read through the word dictionary. In ‘with’ loop, f will read each letter, and they are concatenated to form a string until a inline (‘\n’) is detected. When ‘\n’ is detected, it will save the string into list.

def readDictionary(filename):
  with open(filename) as f:
    dictList = []
    text = f.read()
    str = ''
    for i in text:
      if i != '\n':
        str= str + i
      else:
        dictList.append(str)
        str = ''
  return dictList
Image

Basic algorithm functions

In this part, we’ll define few functions to compose the basic function of boggle game.
Firstly, there should be a function to find a list of possible coordinates of any specific position. Here we call it possibleMoves().
Secondly, there should be a function to remove the repeated coordinates and output rest coordinates because one coordinate can only be routed (or passed onece), which we call them illegal coordinates. The function is called legalMoves().
Thirdly, there should be a function to compose the passed coordinates together into a string and another function to check whether the the string is a word in dictionary or not. Here we call them forwWord() and examineState() respectively.

possibleMoves() is to produce the next-movable position of input coordinate. I use the zip packing method and unpack methods here to avoid the specify the axis number.

def possibleMoves(cord,board):
  h = len(board)
  w = len(board[0])
  pos_move = set()
  offsets = [-1,0,1]
  offset_candidates = [(x_offset,y_offset) for x_offset in offsets for y_offset in offsets] #here the iteration of offset create the offset coordinates list of 8 position.  
  offset_candidates.remove((0,0))
  for candidate in offset_candidates:
    x, y = zip(cord, candidate) #pack center cord with one offset
    cord_x, cand_x = x
    cord_y, cand_y =y
    x = cord_x + cand_x    # add center cordinate and offset together in x&y
    y = cord_y + cand_y
    if x >= 0 and x < h and y >= 0 and y<w: #this if statement is to judge the offset + coordinates exceeds the border of boggle or not. 
      possible_moves.append((x,y))
    else:
      continue
  return possible_moves

legalMoves() is to remove the specified coordinates from possible moves.

def legalMoves(moves,path):
  pos = set(moves) - path #set difference
  return pos

formWord() is to locate the corresponding letters according to the coordinates list and concatenate them into string.

def formWord(myBoard, position, path):
  x,y = position
  word = myBoard[x][y]
  for path_item in path:
    i,j = path_item
    word = word + myBoard[i][j]
  return word.lower()

examineState() is to look through the input dictionary of the function to check whether the word is inside it and return the result (True/False).

def formWord(myBoard, position, path):
  x,y = position
  word = myBoard[x][y]
  for path_item in path:
    i,j = path_item
    word = word + myBoard[i][j]
  return word.lower()

Synthesize of functions and optimization

Now, we have all the required functions. And we can construct the boggle function like this: loadBoard-> readDictionary ->possibleMoves->for loop: {formWord->examineState->legalMoves} until there’s no leagalMoves -> next coordinate-> found words.

But the program will be rather slow if we just look through all whole combination from each coordinate. (More than 1 hour for 1 coordinate look through in 4×4 boggle). So, we must implement some methods to optimize it.
After analysis, I found there are large amount of formed string cannot form word when forming in second/third/forth letter. So, there must be a method to pause the deeper searching if the formed string is not possible to form. Moreover, to reduce the computation cost from start (the first letter), I divide the dictionary into small chunks (sub-dictionary), where the program will only look through words corresponds to the first letter. The code to realize these 2 functions is as follows:

subDictionary() is to divide the dictionary (stored as list type) into a dictionary object, where the key is the first letter, value is words start with this letter.

 
def subDictionary(dictionary):
  """
     #This function is to turn the list into a dictionary
     #to let algorithm only search in the first letter are
  """
  sub_dictionary = {}
  sub_list = []
  first_letter = 'a'
  for word in dictionary:
    if first_letter == word[0]:
      sub_list.append(word)
      #print(sub_list)
    else:
      sub_dictionary[first_letter] = sub_list
      sub_list = []
      first_letter = word[0]
      sub_list.append(word)
    sub_dictionary[first_letter] = sub_list
  return sub_dictionary

checkDictionary() is to judge the possibility of current formed string forming words in the dictionary. The function takes sub-dictionary as input and iterates through it. If there is no word can be formed by input string, it will return False. If there are words can be formed, it will record the position of the first formable word in the dictionary and return at the last formable word with True, first location, last location. So that the latter program can utilize the locations to shrink the search zone, which reduces the computation cost.

def checkDictionary(word,dictionary):
  """
    This function is to check current word is a part of words in the dictionary
  """
  length = len(word)
  i = 0
  i_start = -1
  i_end = -1
  for w in dictionary:
    if i_start == -1 and word in w[0:length]:
      #print("True")
      i_start = i       #record the start point of the sub-dictionary
    if i_start !=-1 and (word in w[0:length]) == False:
      i_end = i+1       #record the end point of the sub-dictionary
      return True,i_start,i_end
    i = i+1
  if i_start != -1:
    return True, i_start, i   # the situation that word is iterate through til the end
  return False,-1,-1

findWords() is the function to form paths to look through the formable words with the initial position letter and concatenating above functions.

import pdb
def findWords(board,dictionary,foundWords,moves,availableCords=[],beenCords=[]):
  """
  this function is to iterate through possible path (with 'abandom' algorithm) and
  then form words to search in the corresponding dictionary index
  parameters:
  board:board, list
  dictionary: the dictionary with first letter index
  foundWords: where the found words will be saved, list
  moves: the counter to count how many steps the algorithm has gone
  """
  possible_moves = possibleMoves(beenCords[-1],board)
  legal_moves = legalMoves(possible_moves,set(beenCords))

  for move in legal_moves:

    moves[0] = moves[0] + 1       #use move to record to steps
    availableCords.remove(move)   #remove current move from available move
    beenCords.append(move)
    word = formWord(board,beenCords[0],beenCords[1:])
    state, index_start, index_end = checkDictionary(word,dictionary)
    temp_dictionary = dictionary[index_start: index_end+1]
    status = examineState(word,temp_dictionary)
    ### add one clever here : function judge the formed word is partial of dictionary or not
    #pdb.set_trace()
    if state == False :
      pre_pos = beenCords.pop()
      availableCords.append(pre_pos)
      continue
    ###
    else:
      #print('into lower level function')
      if status == 'YES':
        foundWords.append(word.upper())
      findWords(board,temp_dictionary,foundWords,moves,availableCords,beenCords)
    prev_pos = beenCords.pop()    #after iterate through all posible legal moves, it go delete current path and go back.
    availableCords.append(prev_pos)
  if len(beenCords) == 1:
    return "finish one searching"

runBoard() is the function to synthesize all function together: read the board, dictionary, feed initial position into findWords iteratively and print out total spending time and found words.

import time
def runBoard(board_filename, dictionary_filename):
  start_time = time.time()
  board = loadBoard(board_filename)
  printBoard(board)
  dictionary = readDictionary(dictionary_filename)
  ## clever
  dictionary = subDictionary(dictionary) # create a real dictionary with first letter as index
  ##
  H = len(board)      #number of rows
  W = len(board[0])   #number of columns
  found_words = []
  moves = [0]
  for h in range(H):
    for w in range(W):
      position = (h,w)
      #print("Searching on :")
      #print(position)
      available_cords = [(i,j) for i in range(len(board)) for j in range(len(board[0]))]
      available_cords.remove(position)
      first_letter = board[h][w]
      sub_dictionary = dictionary[first_letter.lower()]
      findWords(board,sub_dictionary,found_words,moves,availableCords = available_cords,beenCords = [position])
  end_time = time.time()
  duration_time = end_time - start_time
  print("And we're off!\nRunning with cleverness  ON\nAll done")
  word_dict = {}
  for word in found_words:
    word_dict[len(word)] = []
  for word in found_words:
    word_dict[len(word)].append(word)
  print("Find words:")
  for key in word_dict:
      print('%d'%key+' -letter words:' + str(word_dict[key]))
  print("The number of find words: ")
  print(len(set(found_words)))
  print("Searched total of %d"%moves[0] + " moves in %f seconds"%duration_time)
  return sorted(set(found_words))
Image

Program running
To run inside python

#add after above code
words_list = runBoard('board.txt','twl06.txt')

To run as script in command line: python board_path dictionary_path python_file_name

#add after above code
import sys
script_py, board, dictionary = sys.argv
words_list = runBoard(board,dictionary)
path = board.split('.')[0] + '_words.txt'
file = open(path,'w')
for word in words_list:
  file.write(word)
  file.write('\n')
file.close()

Future possible development:
To let this boggle game becomes faster, we can divide the sub-dictionary into smaller sub-dictionary when the file which saves word dictionary is big enough.

Design a site like this with WordPress.com
Get started