Text Twist Bot
Yesterday I watched someone play a bit of the browser based game Text Twist. Upon trying myself I found that I was awful, so I did what any programmer would do; cheated.
Results of a few hours hacking:
Features:
- Scan for game area and crash and burn if it’s not detected
- (Crudely) detect what letters are shown
- Find all 3, 4, 5 and 6 letter combinations, and then throw them against a basic word list + simple anagram lookup table
- Send key presses to the window to (try and) solve all possible words (and overwrite any work you were doing when the window loses focus)
- Reliably gets all but one or two words for each puzzle, leaving you plenty of time to go crazy trying to finish it
Horrible source for the curious (it should at least help anyone wondering how to send key presses, or capture the current screen in python)
TextTwistBot.py
# Text Twist Bot
# http://games.yahoo.com/game/text-twist
import win32com.client as comclt
from time import sleep
from PIL import ImageGrab
from itertools import combinations
from Board import Board
from Pixels import Pixels
from Anagrams import Anagrams
def sorted_unique_character_groups(letters, length):
c = list(combinations(''.join(letters), length))
c = [''.join(e) for e in c]
c = list(set(c))
c.sort()
return c
anagrams = Anagrams('ispell-enwl-3.1.20/english.all')
pixels = Pixels(ImageGrab.grab())
board = Board(pixels)
letters = board.get_letters()
# letters = ['d', 'e', 'l', 'a', 'p', 'd']
possible_answers = []
lengths = [3,4,5,6]
for length in lengths:
words = sorted_unique_character_groups(letters, length)
for word in words:
possible_answers.extend(anagrams.find(word))
possible_answers = list(set(possible_answers))
print possible_answers
wsh = comclt.Dispatch("WScript.Shell")
wsh.AppActivate("Windows Internet Explorer")
for word in possible_answers:
for letter in word:
wsh.SendKeys(letter)
sleep(0.05)
wsh.SendKeys("\n")
print 'Done'
Pixels.py
class Pixels:
def __init__(self, img):
self.img = img
self.data = list(img.getdata())
(self.width, self.height) = img.size
def at(self, x, y):
offset = (y * self.width) + x
return self.data[offset]
def grab_area(self, x1, y1, x2, y2):
raw = []
for y in range(y1, y2):
for x in range(x1, x2):
raw.append(self.at(x, y))
return raw
Anagrams.py
class Anagrams:
def __init__(self, word_list):
self._load_word_list(word_list)
def _load_word_list(self, filename):
print 'Loading word list...',
lines = open(filename).readlines()
words = [line.strip().lower() for line in lines]
self.lookup = {}
for word in words:
key = self._sort_letters(word)
if key not in self.lookup:
self.lookup[key] = []
if word not in self.lookup[key]:
self.lookup[key].append(word)
print 'Done'
def _sort_letters(self, word):
letters = [l for l in word]
letters.sort()
return ''.join(letters)
def find(self, word):
key = self._sort_letters(word)
if key not in self.lookup:
return []
results = self.lookup[key]
#if word in results:
# results.remove(word)
return results
Board.py (be afraid)
class Board:
def __init__(self, pixels):
# Color of the outside border, used to find the edges of the game
self.border_green = (204, 255, 0)
# Bounding boxes for each letter (including the circle and background)
# Each letter is 45x45
# I've chopped off 8pixels from each side to remove any background
self.letter_coords = [
((161+8, 178+8), (161+45-8, 178+45-8)),
((215+8, 178+8), (215+45-8, 178+45-8)),
((269+8, 178+8), (269+45-8, 178+45-8)),
((323+8, 178+8), (323+45-8, 178+45-8)),
((377+8, 178+8), (377+45-8, 178+45-8)),
((431+8, 178+8), (431+45-8, 178+45-8)),
]
# Pixel data for each letter
self.letter_data = {
# Snipped 300Kb of data
# Yikes, didn't realize it was so big
# If anyone actually cares for the letter data, you
# can reconstruct it from error messages.
#
# Format is:
#
# 'a': [(r, g, b), (r, g, b)....],
# 'b': [(r, g, b), (r, g, b)....],
}
self.pixels = pixels
self._get_edges()
def _get_edges(self):
horz = self._find_pixels(range(0, self.pixels.width), range(0, self.pixels.height, 100), self.border_green)
vert = self._find_pixels(range(0, self.pixels.width, 100), range(0, self.pixels.height), self.border_green)
left = min([x for (x,y) in horz])
right = max([x for (x,y) in horz])
top = min([y for (x,y) in vert])
bottom = max([y for (x,y) in vert])
self.left = left
self.right = right
self.top = top
self.bottom = bottom
def _find_pixels(self, x_range, y_range, color):
edges = []
for y in y_range:
for x in x_range:
if self.pixels.at(x, y) == color:
edges.append((x, y))
return edges
def get_letters(self):
letters = []
for coords in self.letter_coords:
x1 = coords[0][0] + self.left
x2 = coords[1][0] + self.left
y1 = coords[0][1] + self.top
y2 = coords[1][1] + self.top
data = self.pixels.grab_area(x1, y1, x2, y2)
# print 'Got data for pos', coords
found = False
for key in self.letter_data:
if self.letter_data[key] == data:
letters.append(key)
found = True
break
if found == False:
print '\'?\':', data, ','
return letters