It all started with the fact that they sent me a link to the bot in the Telegram with a proposal to play.
It looks like this.
After my first game I earned 28 points, not very impressive result. So you need nothing at all - a program that finds words from the letters of the original word and the base of nouns in Russian words.
For the database I decided to use sqlite3, it is mobile and for this task the most.
The structure of the base looks like this.
CREATE TABLE IF NOT EXISTS words ( word VARCHAR(225) UNIQUE NOT NULL, length INTEGER NOT NULL );
There is a structure, to fill it, I used the list of nouns of Russian words .
It was decided to implement database filling and word search in one code, to separate processing by flags.
Also, the very creation of the base file and the creation of the table are implemented in init ()
func init() { var err error connection, err = sql.Open("sqlite3", "./words.db") if err != nil { log.Fatalf("Failed connection: %v", err) } _, err = connection.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) } }
When adding words, it is necessary to remember that we use Cyrillic, which is why the usual len()
function does not suit us, we use utf8.RuneCountInString()
to correctly calculate the length of words.
Add error checking if err.Error() != "UNIQUE constraint failed: words.word"
- necessary for the possibility of introducing new dictionaries that contain a copy of words from the base.
func insert(word string) error { _, err := connection.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 }
To search for words included in the original, it is necessary to decompose it into letters. A word can contain several identical letters, to account for the number, we use map[rune]int
where int
is the number of found letters in the word.
func decay(word string) map[rune]int { var m = make(map[rune]int) for _, char := range word { m[char]++ } return m }
The search itself is carried out in multi-threaded mode, the number of gorutine = the length of the source word, minus one gorutine Start by searching for words consisting of two or more letters.
With this approach, the program worked too quickly and sent to the chat to the bot the number of answers = gorutine, although each gorutine had time.Sleap(1 * time.Second)
- this led to blocking my Telegram from all devices for 10 minutes. I took this into account and in the current version I set the delay for sending, and I sent the sending itself to a separate gorutine, which communicates with the others through a common channel. The search is carried out as before.
We use waitGroup{}
as a mechanism for ending the search for all words from the database, and then close the channel.
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) }
The search function selects from the database all the words with the desired length and goes through the cycle to check if the word is appropriate. Verification is carried out in several stages. Because of the use of the map
we create a new copy every time we complete the cycle. We need a copy of map
to check for the number of letters in a word, each time a letter matches, we decrement the value by key by one until it decreases to zero, after which if such a letter has a value = 0, we assign the variable ontain=false
and at the end of the cycle, the word will not be added to the channel.
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 } } }
It remains the case for small, so that the program itself sends the answers to the chat. Since a bot with another bot cannot communicate, I had to use my personal account. I decided to use an open source client .
Running it on the port: 9090. We send chat messages to the bot.
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) } }
Install the necessary libraries.
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
Repository cloning
git clone --recursive https://github.com/vysheng/tg.git && cd tg
Execution configuration.
./configure make
Run client on port 9090
bin/telegram-cli -P 9090
In order for the client to find the bot, you need to execute thesearch WordsGame-bot
command in the client, then check the result with themsg WordsGame-bot test
command, if after the actions you didn’t write the text test to the bot, try playing it in person.
In order for the client to start working, do not forget to log in, he will offer when you log in for the first time.
Everything seems to be ready. The program can fill the base as well as play the game with the bot, but only if you yourself will request the word from the bot.
But all this is slow, but we want to immediately take the first line, and for this we need to teach the program to request words from the bot. We will create a connection and send the msg WordsGame-bot /play
command. The bot has a delay, so we wait 5 seconds. After that, we request the last message from the history with the history WordsGame-bot 1
. To read from conn
create the variable reply = make([]byte, 512)
. After we received the entire answer from onn
it looks like this.
history @manymanywords_bot 1 ANSWER 58 [16:10] WordsGame-bot »»»
Create regexp.MustCompile("([-]{1,100})")
to search for Cyrillic words. After that, choose our word.
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) }
But there is a problem, because we closed the channel after we found all the words. To fix this we need the global variable GODMOD
. Add a condition to findSubWords
. Now when we use the -g switch, the GODMOD variable is translated to true and the channel is not closed, and after the completion of the cycle, we request a new word.
if !GODMOD { close(out) }
Now you can look at the result.
Source: https://habr.com/ru/post/432278/
All Articles