From 00819784893a90a7223c957c7332b336385769c5 Mon Sep 17 00:00:00 2001 From: Atridad Lahiji Date: Mon, 19 Jan 2026 22:33:55 -0700 Subject: [PATCH] Made it more MaRkOvIaN!!!!!! --- command/markov.go | 944 +++++++--------------------------------------- go.mod | 8 +- go.sum | 20 +- 3 files changed, 156 insertions(+), 816 deletions(-) diff --git a/command/markov.go b/command/markov.go index 7a9622a..7a645ba 100644 --- a/command/markov.go +++ b/command/markov.go @@ -13,13 +13,11 @@ import ( "github.com/bwmarrin/discordgo" ) -// MarkovData holds the Markov chain data for different n-gram sizes type MarkovData struct { - // n-gram size -> prefix -> list of suffixes - Chains map[int]map[string][]string + Chain map[string][]string // "word1 word2" -> ["word3", ...] + Starts []string } -// MarkovCache caches chains to avoid rebuilding type MarkovCache struct { data map[string]*MarkovData hashes map[string]string @@ -31,10 +29,8 @@ var ( data: make(map[string]*MarkovData), hashes: make(map[string]string), } - // Regex for cleaning text urlRegex = regexp.MustCompile(`https?://[^\s]+`) mentionRegex = regexp.MustCompile(`<[@#&!][^>]+>`) - emojiRegex = regexp.MustCompile(``) ) func MarkovCommand(s *discordgo.Session, i *discordgo.InteractionCreate) (string, error) { @@ -52,238 +48,33 @@ func MarkovCommand(s *discordgo.Session, i *discordgo.InteractionCreate) (string } } - // Check cache cacheKey := fmt.Sprintf("%s:%d", channelID, numMessages) if data := getCachedChain(cacheKey); data != nil { - newMessage := generateAdvancedMessage(data) - if newMessage != "" { - return newMessage, nil + if msg := generateMessage(data, ""); msg != "" { + return msg, nil } } - // Fetch messages allMessages, err := fetchMessages(s, channelID, numMessages) if err != nil { return "", err } - // Build chain data := buildMarkovChain(allMessages) - - // Cache chain setCachedChain(cacheKey, data, allMessages) - // Generate message - newMessage := generateAdvancedMessage(data) - - // Fallback if empty + newMessage := generateMessage(data, "") if newMessage == "" { - newMessage = "I couldn't generate a message. The channel might be empty or contain no usable text." + newMessage = "Not enough text data to generate a message." } return newMessage, nil } -func getCachedChain(cacheKey string) *MarkovData { - markovCache.mu.RLock() - defer markovCache.mu.RUnlock() - - if data, exists := markovCache.data[cacheKey]; exists { - return data - } - return nil -} - -func setCachedChain(cacheKey string, data *MarkovData, messages []*discordgo.Message) { - hash := hashMessages(messages) - - markovCache.mu.Lock() - defer markovCache.mu.Unlock() - - // Only cache if we have some data - if len(data.Chains[1]) > 10 { - markovCache.data[cacheKey] = data - markovCache.hashes[cacheKey] = hash - - // Simple FIFO cache cleanup - if len(markovCache.data) > lib.AppConfig.MarkovCacheSize { - for k := range markovCache.data { - delete(markovCache.data, k) - delete(markovCache.hashes, k) - break - } - } - } -} - -func hashMessages(messages []*discordgo.Message) string { - var content strings.Builder - for _, msg := range messages { - content.WriteString(msg.ID) - content.WriteString(msg.Content) - } - return fmt.Sprintf("%x", md5.Sum([]byte(content.String()))) -} - -func fetchMessages(s *discordgo.Session, channelID string, numMessages int) ([]*discordgo.Message, error) { - var allMessages []*discordgo.Message - var lastMessageID string - - // Pre-allocate - allMessages = make([]*discordgo.Message, 0, numMessages) - - for len(allMessages) < numMessages { - batchSize := 100 - if numMessages-len(allMessages) < 100 { - batchSize = numMessages - len(allMessages) - } - - batch, err := s.ChannelMessages(channelID, batchSize, lastMessageID, "", "") - if err != nil { - return nil, err - } - - if len(batch) == 0 { - break - } - - // Filter messages - for _, msg := range batch { - if !msg.Author.Bot && len(strings.TrimSpace(msg.Content)) > 0 { - allMessages = append(allMessages, msg) - } - } - - lastMessageID = batch[len(batch)-1].ID - - if len(batch) < 100 { - break - } - } - - return allMessages, nil -} - -// cleanText normalizes text -func cleanText(text string) string { - text = urlRegex.ReplaceAllString(text, "") - text = mentionRegex.ReplaceAllString(text, "") - text = emojiRegex.ReplaceAllString(text, "") - text = strings.Join(strings.Fields(text), " ") - return strings.TrimSpace(text) -} - -// buildMarkovChain creates a Markov chain from messages -func buildMarkovChain(messages []*discordgo.Message) *MarkovData { - data := &MarkovData{ - Chains: make(map[int]map[string][]string), - } - - // Count words - totalWords := 0 - for _, msg := range messages { - cleanedContent := cleanText(msg.Content) - if len(cleanedContent) >= 3 { - words := strings.Fields(cleanedContent) - totalWords += len(words) - } - } - - // Adjust n-gram level based on memory - maxNGram := lib.AppConfig.MarkovMaxNGram - estimatedMemoryMB := estimateMemoryUsage(totalWords, maxNGram) - if estimatedMemoryMB > lib.AppConfig.MarkovMemoryLimit { - for maxNGram > 2 && estimateMemoryUsage(totalWords, maxNGram) > lib.AppConfig.MarkovMemoryLimit { - maxNGram-- - } - } - - // Init maps - for i := 1; i <= maxNGram; i++ { - data.Chains[i] = make(map[string][]string) - } - - for _, msg := range messages { - cleanedContent := cleanText(msg.Content) - if len(cleanedContent) < 3 { - continue - } - - words := strings.Fields(cleanedContent) - if len(words) < 2 { - continue - } - - // Build chains - for n := 1; n <= maxNGram; n++ { - if len(words) <= n { - continue - } - - for i := 0; i < len(words)-n; i++ { - // Validate sequence - validSequence := true - for j := 0; j < n; j++ { - word := words[i+j] - if len(word) < 2 || strings.ContainsAny(word, "!@#$%^&*()[]{}") { - validSequence = false - break - } - } - if !validSequence { - continue - } - - // Build prefix - var prefixBuilder strings.Builder - for j := 0; j < n; j++ { - if j > 0 { - prefixBuilder.WriteString(" ") - } - prefixBuilder.WriteString(strings.ToLower(words[i+j])) - } - prefix := prefixBuilder.String() - - nextWord := words[i+n] - data.Chains[n][prefix] = append(data.Chains[n][prefix], nextWord) - } - } - } - - return data -} - -// estimateMemoryUsage estimates memory usage in MB -func estimateMemoryUsage(wordCount int, maxNGram int) int { - baseMB := wordCount / 2000 - - switch maxNGram { - case 2: - return baseMB * 3 - case 3: - return baseMB * 8 - case 4: - return baseMB * 15 - case 5: - return baseMB * 25 - case 6: - return baseMB * 40 - default: - return baseMB - } -} - -func init() { - // Seed RNG - rand.Seed(time.Now().UnixNano()) -} - -// MarkovQuestionCommand generates an answer func MarkovQuestionCommand(s *discordgo.Session, i *discordgo.InteractionCreate) (string, error) { channelID := i.ChannelID - var question string - var numMessages int = lib.AppConfig.MarkovDefaultMessages + numMessages := lib.AppConfig.MarkovDefaultMessages for _, option := range i.ApplicationCommandData().Options { switch option.Name { @@ -313,639 +104,188 @@ func MarkovQuestionCommand(s *discordgo.Session, i *discordgo.InteractionCreate) if err != nil { return "", err } - data = buildMarkovChain(allMessages) setCachedChain(cacheKey, data, allMessages) } - answer := generateAdvancedQuestionAnswer(data, question) - + answer := generateMessage(data, question) if answer == "" { - answer = "I couldn't generate an answer to that question. The channel might not have enough relevant content." + answer = "I don't have enough context to answer that." } return fmt.Sprintf("**Q:** %s\n**A:** %s", question, answer), nil } -// generateQuestionAnswer generates answer -func generateQuestionAnswer(data *MarkovData, question string) string { - return generateAdvancedQuestionAnswer(data, question) +func getCachedChain(cacheKey string) *MarkovData { + markovCache.mu.RLock() + defer markovCache.mu.RUnlock() + return markovCache.data[cacheKey] } -// categorizeQuestion determines question type -func categorizeQuestion(question string) string { - question = strings.ToLower(question) +func setCachedChain(cacheKey string, data *MarkovData, messages []*discordgo.Message) { + hash := hashMessages(messages) - if strings.Contains(question, "what") { - return "what" - } else if strings.Contains(question, "how") { - return "how" - } else if strings.Contains(question, "why") { - return "why" - } else if strings.Contains(question, "when") { - return "when" - } else if strings.Contains(question, "where") { - return "where" - } else if strings.Contains(question, "who") { - return "who" - } else if strings.Contains(question, "which") { - return "which" - } else if strings.Contains(question, "is") || strings.Contains(question, "are") || strings.Contains(question, "do") || strings.Contains(question, "does") { - return "yesno" - } + markovCache.mu.Lock() + defer markovCache.mu.Unlock() - return "general" -} + if len(data.Starts) > 0 { + markovCache.data[cacheKey] = data + markovCache.hashes[cacheKey] = hash -// WordCandidate holds word score -type WordCandidate struct { - Word string - Score int -} - -// findBestStartingWords scores starting words -func findBestStartingWords(data *MarkovData, questionWords []string, questionType string) []WordCandidate { - candidates := make(map[string]int) - chain := data.Chains[1] - - // Score question words - for _, word := range questionWords { - if len(word) > 2 && !isStopWord(word) { - if nextWords, exists := chain[word]; exists && len(nextWords) > 0 { - candidates[word] += 10 - } - } - } - - // Add context words - contextWords := getContextualWords(questionType) - for _, word := range contextWords { - if nextWords, exists := chain[word]; exists && len(nextWords) > 0 { - candidates[word] += 5 - } - } - - // Add fallback words - for word, nextWords := range chain { - if len(nextWords) >= 3 && len(word) > 2 && !isStopWord(word) { - if _, exists := candidates[word]; !exists { - candidates[word] = len(nextWords) / 2 - } - } - } - - // Sort candidates - var result []WordCandidate - for word, score := range candidates { - result = append(result, WordCandidate{Word: word, Score: score}) - } - - for i := 0; i < len(result)-1; i++ { - for j := i + 1; j < len(result); j++ { - if result[j].Score > result[i].Score { - result[i], result[j] = result[j], result[i] - } - } - } - - // Top 10 - if len(result) > 10 { - result = result[:10] - } - - return result -} - -// getContextualWords returns relevant words -func getContextualWords(questionType string) []string { - switch questionType { - case "what": - return []string{"thing", "something", "object", "idea", "concept", "stuff", "item"} - case "how": - return []string{"way", "method", "process", "steps", "technique", "approach"} - case "why": - return []string{"because", "reason", "cause", "since", "due", "explanation"} - case "when": - return []string{"time", "moment", "day", "hour", "yesterday", "today", "tomorrow", "now", "then"} - case "where": - return []string{"place", "location", "here", "there", "somewhere", "anywhere"} - case "who": - return []string{"person", "people", "someone", "anyone", "everybody", "nobody"} - case "which": - return []string{"choice", "option", "selection", "pick", "prefer"} - case "yesno": - return []string{"yes", "no", "maybe", "definitely", "probably", "possibly", "sure", "absolutely"} - default: - return []string{"think", "believe", "know", "understand", "feel", "seem"} - } -} - -// scoreResponse scores the response -func scoreResponse(response string, questionType string) int { - score := 0 - words := strings.Fields(response) - - // Length score - if len(words) >= 8 && len(words) <= 16 { - score += 10 - } else if len(words) >= 6 && len(words) <= 20 { - score += 5 - } - - // Diversity score - totalLength := 0 - for _, word := range words { - totalLength += len(word) - } - if len(words) > 0 { - avgWordLength := float64(totalLength) / float64(len(words)) - if avgWordLength > 3.5 && avgWordLength < 6.0 { - score += 5 - } - } - - // Content score - contentWords := 0 - for _, word := range words { - if len(word) > 3 && !isStopWord(strings.ToLower(word)) { - contentWords++ - } - } - score += contentWords - - return score -} - -// getPunctuationForQuestionType returns punctuation -func getPunctuationForQuestionType(questionType string) []string { - switch questionType { - case "yesno": - return []string{".", "!", "."} - case "why", "how": - return []string{".", ".", "!"} - default: - return []string{".", ".", "!", "."} - } -} - -// min helper -func min(a, b int) int { - if a < b { - return a - } - return b -} - -// isStopWord checks for common words -func isStopWord(word string) bool { - stopWords := map[string]bool{ - "a": true, "an": true, "and": true, "are": true, "as": true, "at": true, "be": true, "by": true, - "for": true, "from": true, "has": true, "he": true, "in": true, "is": true, "it": true, - "its": true, "of": true, "on": true, "that": true, "the": true, "to": true, "was": true, - "will": true, "with": true, "or": true, "but": true, "if": true, "so": true, "do": true, - } - return stopWords[word] -} - -// generateAdvancedMessage generates a message -func generateAdvancedMessage(data *MarkovData) string { - if len(data.Chains[1]) == 0 { - return "" - } - - // Try multiple attempts - var bestMessage string - bestScore := 0 - - for attempt := 0; attempt < 5; attempt++ { - words := []string{} - var currentWord string - - // Pick start word - attempts := 0 - for word, nextWords := range data.Chains[1] { - if len(nextWords) >= 2 && len(word) > 2 && !isStopWord(word) { - currentWord = word - break - } - attempts++ - if attempts > 50 { - currentWord = word + if len(markovCache.data) > lib.AppConfig.MarkovCacheSize { + for k := range markovCache.data { + delete(markovCache.data, k) + delete(markovCache.hashes, k) break } } + } +} - if currentWord == "" { +func hashMessages(messages []*discordgo.Message) string { + var content strings.Builder + for _, msg := range messages { + content.WriteString(msg.ID) + } + return fmt.Sprintf("%x", md5.Sum([]byte(content.String()))) +} + +func fetchMessages(s *discordgo.Session, channelID string, numMessages int) ([]*discordgo.Message, error) { + var allMessages []*discordgo.Message + var lastMessageID string + + allMessages = make([]*discordgo.Message, 0, numMessages) + + for len(allMessages) < numMessages { + batchSize := 100 + if numMessages-len(allMessages) < 100 { + batchSize = numMessages - len(allMessages) + } + + batch, err := s.ChannelMessages(channelID, batchSize, lastMessageID, "", "") + if err != nil { + return nil, err + } + + if len(batch) == 0 { + break + } + + for _, msg := range batch { + if !msg.Author.Bot && len(strings.TrimSpace(msg.Content)) > 0 { + allMessages = append(allMessages, msg) + } + } + + lastMessageID = batch[len(batch)-1].ID + + if len(batch) < 100 { + break + } + } + + return allMessages, nil +} + +func cleanText(text string) string { + text = urlRegex.ReplaceAllString(text, "") + text = mentionRegex.ReplaceAllString(text, "") + return strings.Join(strings.Fields(text), " ") +} + +func buildMarkovChain(messages []*discordgo.Message) *MarkovData { + data := &MarkovData{ + Chain: make(map[string][]string), + Starts: make([]string, 0), + } + + for _, msg := range messages { + cleaned := cleanText(msg.Content) + if cleaned == "" { continue } - // Generate words - maxWords := 10 + rand.Intn(8) - wordHistory := []string{currentWord} - - for i := 0; i < maxWords; i++ { - // Add word - if i == 0 { - words = append(words, strings.Title(currentWord)) - } else { - words = append(words, currentWord) - } - - var nextWord string - historyLen := len(wordHistory) - - // Try n-grams - for n := 5; n >= 2; n-- { - if historyLen >= n && data.Chains[n] != nil { - // Build prefix - var prefixBuilder strings.Builder - for j := 0; j < n; j++ { - if j > 0 { - prefixBuilder.WriteString(" ") - } - prefixBuilder.WriteString(strings.ToLower(wordHistory[historyLen-n+j])) - } - prefix := prefixBuilder.String() - - if options, exists := data.Chains[n][prefix]; exists && len(options) > 0 { - nextWord = selectBestNextWord(options, wordHistory) - if nextWord != "" { - break - } - } - } - } - - // Fallback to 1-gram - if nextWord == "" { - if nextWords, exists := data.Chains[1][strings.ToLower(currentWord)]; exists && len(nextWords) > 0 { - nextWord = selectBestNextWord(nextWords, wordHistory) - } - } - - // Restart if needed - if nextWord == "" { - found := false - for word, nextWords := range data.Chains[1] { - if len(nextWords) > 0 && len(word) > 2 && !isStopWord(word) { - nextWord = word - found = true - break - } - } - if !found { - break - } - } - - currentWord = nextWord - wordHistory = append(wordHistory, currentWord) - - // Trim history - if len(wordHistory) > 10 { - wordHistory = wordHistory[1:] - } + words := strings.Fields(cleaned) + if len(words) < 3 { + continue } - message := strings.Join(words, " ") + startKey := key(words[0], words[1]) + data.Starts = append(data.Starts, startKey) - // Score message - score := scoreAdvancedMessage(message) - - if score > bestScore { - bestScore = score - bestMessage = message + for i := 0; i < len(words)-2; i++ { + k := key(words[i], words[i+1]) + val := words[i+2] + data.Chain[k] = append(data.Chain[k], val) } } - // Add punctuation - if len(bestMessage) > 0 && !strings.ContainsAny(bestMessage[len(bestMessage)-1:], ".!?") { - punctuation := []string{".", "!", "?", "."} - bestMessage += punctuation[rand.Intn(len(punctuation))] - } - - return bestMessage + return data } -// generateAdvancedQuestionAnswer generates answer -func generateAdvancedQuestionAnswer(data *MarkovData, question string) string { - if len(data.Chains[1]) == 0 { +func generateMessage(data *MarkovData, seed string) string { + if len(data.Starts) == 0 { return "" } - // Analyze question - cleanedQuestion := cleanText(question) - questionWords := strings.Fields(strings.ToLower(cleanedQuestion)) + var w1, w2 string + var currentKey string - // Categorize question - questionType := categorizeQuestion(cleanedQuestion) + // Try to seed based on input question + if seed != "" { + seedWords := strings.Fields(cleanText(seed)) + var candidates []string - // Find starting words - startingCandidates := findBestStartingWords(data, questionWords, questionType) - - if len(startingCandidates) == 0 { - return "" - } - - // Generate response - return generateAdvancedCoherentResponse(data, startingCandidates, questionType) -} - -// generateAdvancedCoherentResponse generates response -func generateAdvancedCoherentResponse(data *MarkovData, candidates []WordCandidate, questionType string) string { - if len(candidates) == 0 { - return "" - } - - // Try multiple attempts - var bestResponse string - bestScore := 0 - - for attempt := 0; attempt < 5; attempt++ { - // Pick candidate - candidateIndex := 0 - if len(candidates) > 1 { - if rand.Float32() > 0.7 && len(candidates) > 1 { - candidateIndex = rand.Intn(min(3, len(candidates))) - } - } - - currentWord := candidates[candidateIndex].Word - words := []string{} - wordHistory := []string{currentWord} - - // Generate response - maxWords := 12 + rand.Intn(10) - - for i := 0; i < maxWords; i++ { - // Add word - if i == 0 { - words = append(words, strings.Title(currentWord)) - } else { - words = append(words, currentWord) - } - - var nextWord string - historyLen := len(wordHistory) - - // Try n-grams - for n := 5; n >= 2; n-- { - if historyLen >= n && data.Chains[n] != nil { - // Build prefix - var prefixBuilder strings.Builder - for j := 0; j < n; j++ { - if j > 0 { - prefixBuilder.WriteString(" ") - } - prefixBuilder.WriteString(strings.ToLower(wordHistory[historyLen-n+j])) - } - prefix := prefixBuilder.String() - - if options, exists := data.Chains[n][prefix]; exists && len(options) > 0 { - nextWord = selectBestNextWord(options, wordHistory) - if nextWord != "" { - break - } - } + for k := range data.Chain { + for _, sw := range seedWords { + if len(sw) > 3 && strings.Contains(strings.ToLower(k), strings.ToLower(sw)) { + candidates = append(candidates, k) } } + } - // Fallback - if nextWord == "" { - if nextWords, exists := data.Chains[1][strings.ToLower(currentWord)]; exists && len(nextWords) > 0 { - nextWord = selectBestNextWord(nextWords, wordHistory) - } - } + if len(candidates) > 0 { + currentKey = candidates[rand.Intn(len(candidates))] + } + } - // Restart - if nextWord == "" { - found := false - for _, candidate := range candidates { - if nextWords, exists := data.Chains[1][candidate.Word]; exists && len(nextWords) > 0 { - nextWord = candidate.Word - found = true - break - } - } - if !found { - break - } - } + if currentKey == "" { + currentKey = data.Starts[rand.Intn(len(data.Starts))] + } - currentWord = nextWord - wordHistory = append(wordHistory, currentWord) + parts := strings.Split(currentKey, " ") + w1, w2 = parts[0], parts[1] - // Trim history - if len(wordHistory) > 10 { - wordHistory = wordHistory[1:] + output := []string{w1, w2} + + for i := 0; i < 40; i++ { + nextOptions, exists := data.Chain[currentKey] + if !exists || len(nextOptions) == 0 { + break + } + + nextWord := nextOptions[rand.Intn(len(nextOptions))] + output = append(output, nextWord) + + w1 = w2 + w2 = nextWord + currentKey = key(w1, w2) + + // Soft stop on punctuation + if i > 5 && strings.ContainsAny(nextWord, ".!?") { + if rand.Float32() > 0.3 { + break } } - - response := strings.Join(words, " ") - - // Score response - score := scoreAdvancedResponse(response, questionType) - - if score > bestScore { - bestScore = score - bestResponse = response - } } - // Add punctuation - if len(bestResponse) > 0 && !strings.ContainsAny(bestResponse[len(bestResponse)-1:], ".!?") { - punctuation := getPunctuationForQuestionType(questionType) - bestResponse += punctuation[rand.Intn(len(punctuation))] - } - - return bestResponse + return strings.Join(output, " ") } -// scoreAdvancedMessage scores message -func scoreAdvancedMessage(message string) int { - score := 0 - words := strings.Fields(message) - - // Length score - if len(words) >= 10 && len(words) <= 16 { - score += 15 - } else if len(words) >= 8 && len(words) <= 18 { - score += 10 - } else if len(words) >= 6 && len(words) <= 20 { - score += 5 - } - - // Diversity score - totalLength := 0 - uniqueWords := make(map[string]bool) - for _, word := range words { - totalLength += len(word) - uniqueWords[strings.ToLower(word)] = true - } - - if len(words) > 0 { - avgWordLength := float64(totalLength) / float64(len(words)) - if avgWordLength > 3.5 && avgWordLength < 6.5 { - score += 8 - } - - // Uniqueness score - uniqueRatio := float64(len(uniqueWords)) / float64(len(words)) - if uniqueRatio > 0.8 { - score += 10 - } else if uniqueRatio > 0.6 { - score += 5 - } - } - - // Content score - contentWords := 0 - for _, word := range words { - if len(word) > 3 && !isStopWord(strings.ToLower(word)) { - contentWords++ - } - } - score += contentWords * 2 - - // Grammar bonus - if !strings.Contains(message, " a a ") && !strings.Contains(message, " the the ") && !strings.Contains(message, " you you ") { - score += 5 - } - - return score +func key(w1, w2 string) string { + return w1 + " " + w2 } -// scoreAdvancedResponse scores response -func scoreAdvancedResponse(response string, questionType string) int { - score := scoreAdvancedMessage(response) // Base score - - // Question bonuses - responseLower := strings.ToLower(response) - switch questionType { - case "yesno": - if strings.Contains(responseLower, "yes") || strings.Contains(responseLower, "no") || - strings.Contains(responseLower, "maybe") || strings.Contains(responseLower, "definitely") { - score += 8 - } - case "why": - if strings.Contains(responseLower, "because") || strings.Contains(responseLower, "reason") || - strings.Contains(responseLower, "since") || strings.Contains(responseLower, "due") { - score += 8 - } - case "how": - if strings.Contains(responseLower, "way") || strings.Contains(responseLower, "method") || - strings.Contains(responseLower, "process") || strings.Contains(responseLower, "steps") { - score += 8 - } - case "when": - if strings.Contains(responseLower, "time") || strings.Contains(responseLower, "day") || - strings.Contains(responseLower, "hour") || strings.Contains(responseLower, "moment") { - score += 8 - } - case "where": - if strings.Contains(responseLower, "place") || strings.Contains(responseLower, "location") || - strings.Contains(responseLower, "here") || strings.Contains(responseLower, "there") { - score += 8 - } - } - - return score -} - -// isValidNextWord checks validity -func isValidNextWord(wordHistory []string, nextWord string) bool { - if len(wordHistory) == 0 { - return true - } - - nextWordLower := strings.ToLower(nextWord) - - // No immediate repetition - if len(wordHistory) >= 1 && strings.ToLower(wordHistory[len(wordHistory)-1]) == nextWordLower { - return false - } - - // No double articles - if len(wordHistory) >= 1 { - lastWord := strings.ToLower(wordHistory[len(wordHistory)-1]) - if (lastWord == "a" || lastWord == "the" || lastWord == "you") && lastWord == nextWordLower { - return false - } - } - - // No triple repetition - if len(wordHistory) >= 3 { - count := 0 - for i := len(wordHistory) - 3; i < len(wordHistory); i++ { - if strings.ToLower(wordHistory[i]) == nextWordLower { - count++ - } - } - if count >= 2 { - return false - } - } - - // Grammar checks - if len(wordHistory) >= 1 { - lastWord := strings.ToLower(wordHistory[len(wordHistory)-1]) - - // No "you a" - if lastWord == "you" && nextWordLower == "a" { - return false - } - - // No double articles - if (lastWord == "a" || lastWord == "an" || lastWord == "the") && - (nextWordLower == "a" || nextWordLower == "an" || nextWordLower == "the") { - return false - } - } - - return true -} - -// selectBestNextWord picks next word -func selectBestNextWord(options []string, wordHistory []string) string { - if len(options) == 0 { - return "" - } - - // Filter invalid - var validOptions []string - for _, option := range options { - if isValidNextWord(wordHistory, option) { - validOptions = append(validOptions, option) - } - } - - // Fallback - if len(validOptions) == 0 { - var fallbackOptions []string - for _, option := range options { - // Avoid repetition - if len(wordHistory) == 0 || !strings.EqualFold(wordHistory[len(wordHistory)-1], option) { - fallbackOptions = append(fallbackOptions, option) - } - } - if len(fallbackOptions) > 0 { - validOptions = fallbackOptions - } else { - validOptions = options - } - } - - // Prefer meaningful words - var goodOptions []string - for _, option := range validOptions { - if len(option) > 2 && !isStopWord(strings.ToLower(option)) { - goodOptions = append(goodOptions, option) - } - } - - if len(goodOptions) > 0 { - return goodOptions[rand.Intn(len(goodOptions))] - } - - return validOptions[rand.Intn(len(validOptions))] +func init() { + rand.Seed(time.Now().UnixNano()) } diff --git a/go.mod b/go.mod index d61e8e9..ff55b0e 100644 --- a/go.mod +++ b/go.mod @@ -5,14 +5,14 @@ go 1.25.5 require ( github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/libsql/sqlite-antlr4-parser v0.0.0-20240721121621-c0bdc870f11c // indirect - golang.org/x/crypto v0.38.0 // indirect - golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect - golang.org/x/sys v0.33.0 // indirect + golang.org/x/crypto v0.47.0 // indirect + golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect + golang.org/x/sys v0.40.0 // indirect ) require ( github.com/bwmarrin/discordgo v0.29.0 github.com/gorilla/websocket v1.5.3 // indirect github.com/joho/godotenv v1.5.1 - github.com/tursodatabase/go-libsql v0.0.0-20250416102726-983f7e9acb0e + github.com/tursodatabase/go-libsql v0.0.0-20251219133454-43644db490ff ) diff --git a/go.sum b/go.sum index db62ba4..5a0be43 100644 --- a/go.sum +++ b/go.sum @@ -13,19 +13,19 @@ github.com/libsql/sqlite-antlr4-parser v0.0.0-20240721121621-c0bdc870f11c h1:WsJ github.com/libsql/sqlite-antlr4-parser v0.0.0-20240721121621-c0bdc870f11c/go.mod h1:gIcFddvsvPcRCO6QDmWH9/zcFd5U26QWWRMgZh4ddyo= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/tursodatabase/go-libsql v0.0.0-20250416102726-983f7e9acb0e h1:DUEcD8ukLWxIlcRWWJSuAX6IbEQln2bc7t9HOT45FFk= -github.com/tursodatabase/go-libsql v0.0.0-20250416102726-983f7e9acb0e/go.mod h1:TjsB2miB8RW2Sse8sdxzVTdeGlx74GloD5zJYUC38d8= +github.com/tursodatabase/go-libsql v0.0.0-20251219133454-43644db490ff h1:Hvxz9W8fWpSg9xkiq8/q+3cVJo+MmLMfkjdS/u4nWFY= +github.com/tursodatabase/go-libsql v0.0.0-20251219133454-43644db490ff/go.mod h1:TjsB2miB8RW2Sse8sdxzVTdeGlx74GloD5zJYUC38d8= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= -golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= -golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI= -golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ= +golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= +golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= +golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU= +golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= -golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=