[Из песочницы] Вырываемся в top10. Бот для игры в телеграме
Предыстория
Все началось с того, что мне прислали ссылку на бота в Telegram с предложением поиграть.
Выглядит он примерно так.
После моей первой игры я заработал 28 балов, не сильно впечатляющий результат. Значит нужно всего ничего — программа, которая находит слова из букв исходного слова и база существительных русских слов.
Поехали
Для базы решил использовать sqlite3, он мобильный и для этой задачи самое то.
Стурктура базы выглядит так.
CREATE TABLE IF NOT EXISTS words (
word VARCHAR(225) UNIQUE NOT NULL,
length INTEGER NOT NULL
);
- word — из название понятно что это хранимое буквенное значение слова.
- length — символьная длина.
Структура есть, для ее заполнения воспользовался списком существительных русских слов.
Заполнение базы и поиск слов было решено реализовать в одном коде, обработку разделить флагами.
Так же само создание файла базы и создание таблицы реализовываются в init ()
func init() {
var err error
connetion, err = sql.Open("sqlite3", "./words.db")
if err != nil {
log.Fatalf("Failed connection: %v", err)
}
_, err = connetion.Exec(`CREATE TABLE IF NOT EXISTS words (word VARCHAR(225) UNIQUE NOT NULL, length INTEGER NOT NULL);`)
if err != nil {
log.Fatalf("Failed create database table words: %v", err)
}
}
Функция insert ()
При добавление слов необходимо помнить, что мы используем кирилицу, из-за чего обычная функция len()
нам не подходит, воспользуемся utf8.RuneCountInString()
для правильного подсчета длины слов.
Добавляем проверку на ошибку if err.Error() != "UNIQUE constraint failed: words.word"
— необходима для возможности внедрения новых словарей, которые содержат в себе копию слов из базы.
func insert(word string) error {
_, err := connetion.Exec("INSERT INTO words (word,length) VALUES(?,?)", word, utf8.RuneCountInString(word))
if err != nil && err.Error() != "UNIQUE constraint failed: words.word" {
return err
}
return nil
}
Для поиска слов входящих в состав исходного, необходимо его разложить на буквы. В слове может содержаться несколько одинаковых букв, для учёта количества используем map[rune]int
где int
это количество найденых букв в слове.
func decay(word string) map[rune]int {
var m = make(map[rune]int)
for _, char := range word {
m[char]++
}
return m
}
Сам поиск осуществляем в многопоточном режиме, количество gorutine = длине исходного слова, минус одна gorutine т.к. стартуем с поиска слов, состоящих из двух и более букв.
При таком подходе, программа работала слишком быстро и отправляла в чат к боту количество ответов = gorutine, хоть и в каждой gorutine был
time.Sleap(1 * time.Second)
— это привело к блокировки моего Telegram со всех устройств на 10 минут. Я это учел и в текущей версии поставил задержку на отправку, а саму ф-ю отправки вынес в отдельную gorutine которая общается с остальными через общий канал. Поиск же осуществляется как и раньше.
Используем waitGroup{}
как механизм окончания поиска всех слов из базы, после чего закрываем канал.
func findSubWords(word string) {
list := decay(word)
for length := 2; length <= utf8.RuneCountInString(word); length++ {
wg.Add(1)
go func(out chan<- string, length int) {
search(out, list, length)
wg.Done()
fmt.Println("Done: ", length)
}(out, length)
}
wg.Wait()
fmt.Println("search done")
close(out)
}
Функция поиска выбирает из базы все слова с искомой длиной и проходит по циклу проверяя подходит ли слово. Проверка осуществляется в несколько этапов. Из за использования map
создаем новую копию каждый раз как завершаем проход по циклу. Копия map
нам необходима для проверки на количество букв в слове, каждый раз при совпадении буквы мы декрементируем значение по ключу на единицу пока оно не уменьшится до нуля, после чего при совпадении такой буквы у которой значение = 0, мы присвоим переменной сontain=false
и при завершении цикла слово не будет добавлено в канал.
func search(out chan<- string, wordRuneList map[rune]int, length int) {
wordList, err := selects(length)
if err != nil {
log.Printf("fail length %v, error: %v", length, err)
}
for _, word := range wordList {
var (
wordCopyList = make(map[rune]int)
contain = true
)
for k, v := range wordRuneList {
wordCopyList[k] = v
}
for _, r := range word {
if _, ok := wordCopyList[r]; ok && wordCopyList[r] > 0 {
wordCopyList[r]--
} else {
contain = false
break
}
}
if contain {
out <- word
}
}
}
Осталось дело за малым, чтобы программа сама отправляла ответы в чат. Так как бот с другим ботом не может ввести общение мне пришлось использовать свой личный аккаунт. Я решил воспользоваться клиентом с открытым исходным кодом.
Запустив его на порту :9090. Отправляем сообщения в чат к боту.
func send(in <-chan string) {
conn, _ := net.Dial("tcp", "localhost:9090") // conncect to client telegram
for word := range in {
fmt.Fprintf(conn, "msg WordsGame-bot %v\n", word)
time.Sleep(5 * time.Second)
}
}
Команды для быстрого запуска telegram-cli на debian.
Установка необходимых библиотек.
sudo apt install libreadline-dev libconfig-dev libssl-dev lua5.2 liblua5.2-dev libevent-dev libjansson-dev libpython-dev libgcrypt20 libz-dev make git
Клонирование репозитория.
git clone --recursive https://github.com/vysheng/tg.git && cd tg
Выполнение конфигурации.
./configure
make
Запуск клиента на порту 9090
bin/telegram-cli -P 9090
Для того чтобы клиент нашел бота необходимо уже в клиенте выполнить команду
search WordsGame-bot
, после проверте результат командойmsg WordsGame-bot test
, если после действий вы не написали боту в чат текст test, попробуйте сыграть с ним в игру лично.
Чтобы клиент начал работать не забываем авторизоваться, он сам предложит когда вы войдете впервый раз.
Вроде бы все готово. Программа может как и заполнять базу, так и вести игру с ботом, но только если вы сами будете запрашивать слова у бота.
Но все это медленно, а мы ведь хотим сразу занять первую строчку, а для этого нам нужно научить программу запрашивать слова у бота. Создадим подключение и отправим команду msg WordsGame-bot /play
у бота есть задержка по этому ждем 5 сек. После чего запрашиваем последнее сообщение из истории с ботом history WordsGame-bot 1
это будет ответ, а точнее слово которое мы должны использовать в качестве исходного. Для чтения из conn
создадим переменную reply = make([]byte, 512)
. После того как мы получили весь ответ с сonn
он выглядит примерно так.
history @manymanywords_bot 1
ANSWER 58
[16:10] WordsGame-bot »»» дорабатывание
Создадим regexp.MustCompile("([аА-яЯ]{1,100})")
для поиска слов из кирилицы. После чего выбираем наше слово.
else if *god {
go send(out)
for {
var (
conn, _ = net.Dial("tcp", "localhost:9090") // conncect to client telegram
reply = make([]byte, 512)
r = regexp.MustCompile("([аА-яЯ]{1,100})")
)
fmt.Fprintln(conn, "msg WordsGame-bot /play")
time.Sleep(5 * time.Second)
fmt.Fprintln(conn, "history WordsGame-bot 1")
time.Sleep(2 * time.Second)
_, err := conn.Read(reply)
if err != nil {
log.Fatalf("failed read connection %v", err)
}
word := r.FindAllString(string(reply), 1)
if len(word) <= 0 {
log.Fatalf("somthing wrong %s", reply)
}
findSubWords(word[0])
time.Sleep(5 * time.Minute)
}
Но есть проблема, т.к. мы закрывали канал после того как нашли все слова. Чтобы исправить это нам нужна глобальная переменная GODMOD
. Добавим в findSubWords
условие. Теперь когда мы используем ключ -g переменная GODMOD переводится в true и канал не закрывается, а после завершения прохода по циклу мы запрашиваем новое слово.
if !GODMOD {
close(out)
}
Теперь можно посмотреть на результат.