Впервые увидев Словоманию, я запал на нее очень крепко, проводя бесконечные часы отгадывая слова. Суть игры: есть сетка 4х4, в каждой ячейке расположена буква. Задача игрока — составлять слова, последовательно соединяя буквы между собой.
После энного количества времени появилась идея: а что если автоматизировать процесс разгадывания слов? Ведь зачастую я и сам занимаюсь простым перебором комбинаций в попытках сложить слово. Очевидно, что компьютер справится с такой задачей гораздо продуктивнее любого человека. Конечно, за исключением тех случаев, когда слово сразу «видно» на поле, но эта задача уже решена нашим , так что было решено заняться разделением труда между
Сперва необходимо было сделать прототип, чтобы понять, насколько задача поиска слов на поле выполнима с имеющимися на да данный момент вычислительными мощностями (MacBook Core 2 Duo 2.2Ghz Late 2007 с 4 гб памяти и сервер Linode 1024). Поскольку в последнее время я активно занимаюсь разработкой на Ruby On Rails, то было принято решение взять Ruby в качестве тестовой среды. Очевидно, что наша задача требует больших вычислительных мощностей, а в случае с Ruby мы имеем большой оверхед почти на каждой операции, так что для боевой ситуации необходимо использовать нечто другое (например C, но об этом позже). Однако, нельзя не учитывать несомненный плюс — это скорость разработки.
Итак, поехали. Наша программа будет работать следующим образом:
- Перебираем все возможные варианты компоновки букв
- Сопоставляем результаты со словарем
- Если вариант нашелся в словаре — слово угадано, можно показать его игроку
Первым делом необходимо было организовать словарь на который мы будем опираться. После недолгих поисков в Яндексе было найдено несколько словарей в виде обычных текстовых файлов (на каждой строчке отдельное слово). Словари выгружены в базу SQLite. Теперь можно приступать непосредственно к написанию программы. Я буду приводить лишь самые интересные моменты, ссылки на полный исходный код приведены в конце статьи. Прошу заметить, что программа писалась в стиле «сделать как можно быстрее рабочую версию», а не «написать как можно красивее, используя все паттерны проектирования из книги GoF».
В голове сложился следующий алгоритм действий:
- Становимся на клетку с координатами (x,y) (за начало отсчета координат я взял левый нижний угол). Запоминаем позицию текущей клетки в специальном списке координат (пути).
- Идем по очереди в каждую сторону (север, юг, запад, восток, северо-восток, юго-восток, юго-запад и северо-запад) на одну клетку.
- Для каждого пути создаем новый список координат
- По спискам координат формируем слова
- Проверяем, есть ли слова в словаре (если есть, то показываем игроку)
- Для каждой новой позиции переходим к пункту 2 до тех пор, пока не достингем определенной длины слова (т.е. длины списка координат). Длина определяется двумя факторами: максимально возможной длиной слова в игре, либо нашими вычислительными мощностями.
- Переходим к следующей клетке и начинаем все с самого начала.
Таким образом мы рекурсивно обходим все поле, перебирая все возможные комбинации. При этом нужно не забывать, что каждая клетка может быть задействована в каждом слове только один раз, поэтому в пункте (2) необходимо проверить, нет ли в нашем списке координат этой клетки. Также не забываем удостовериться, что мы не ушли за пределы игрового поля.
И еще одна проверка: мы ищем слово в словаре только в том случае, если его длина (т.е. длина пути) больше двух, поскольку по правилам игры слово может состоять минимум из 3 букв.
Вот как это выглядит:
Игровая сетка
@@w =
[
"нцбь",
"алыи",
"етьн",
"прет"
]
Класс координаты
class Coord
attr_accessor :x, :y
def initialize(x,y)
self.x = x
self.y = y
end
end
Обход соседних клеток
def lookup(coords, deep)
print_word coords if deep >= 3
last = coords[-1]
#up
lookup_next coords, Coord.new(last.x + 1, last.y), deep
#up-right
lookup_next coords, Coord.new(last.x + 1, last.y + 1), deep
#right
lookup_next coords, Coord.new(last.x, last.y + 1), deep
#right-down
lookup_next coords, Coord.new(last.x + 1, last.y - 1), deep
#down
lookup_next coords, Coord.new(last.x, last.y - 1), deep
#left-down
lookup_next coords, Coord.new(last.x - 1, last.y - 1), deep
#left
lookup_next coords, Coord.new(last.x - 1, last.y), deep
#left-up
lookup_next coords, Coord.new(last.x - 1, last.y + 1), deep
end
def lookup_next(coords, new_coord, deep)
return if deep > 6
return if new_coord.x < 0 or new_coord.y < 0 or new_coord.x > 3 or new_coord.y > 3
unless coords.find{|c|c.x == new_coord.x and c.y == new_coord.y}
lookup coords.dup + [new_coord], deep + 1
end
end
Поиск слова в словаре и вывод его на печать
def print_word coords
word = ""
coords.each do |c|
word += @@w[3 - c.y].split('')[c.x]
end
return if @@words.include?(word)
@@db.execute( "select * from words where (word)=('#{word}')" ) do |row|
return if @@words.include?(word)
@@words << word
puts word
end
end
Все найденные слова хранятся в глобальной переменной @@words, чтобы избежать вывода на экран одинаковых слов.
Запускаем поиск
0.upto(3) do |x|
0.upto(3) do |y|
lookup [Coord.new(x, y)], 0
end
end
Одним из недостатков данной реализации является то, что поиск начинается с левого нижнего угла сетки, и возможно программе не хватит времени чтобы обойти все поле целиком, поэтому можно в отдельном потоке заняться поиском слов с противоположного угла:
Thread.new {
3.downto(0) do |x|
3.downto(0) do |y|
lookup [Coord.new(x, y)], 0
end
end
}
Даже не смотря на то что мы используем относительно медленный Ruby, наша программа является жизнеспособной и отлично находит слова. В следующей статье я рассмотрю реализацию на C с ncurses, распределенной памятью, семафорами и блекджеком.
Справедливость восторжествовала: теперь победит тот, у кого круче компьютер!
Автор: iAlexey

хорошая игра