--!nonstrict --[[ ╔══════════════════════════════════════════════════╗ ║ ║ ║ R O B S C R I P T . C O M ║ ║ ║ ╚══════════════════════════════════════════════════╝ Решатель слов ------------- Авто-ответы для игры в слова (Кот → Тигр → Рыба): 1. Решатель - отвечает за тебя и выбирает первую букву 2. Слова - словарь, свои слова, выученное 3. Журнал - что делает скрипт (по желанию) RightShift - ответить один раз, RightControl - показать или скрыть окно. ]] ---------------------------------------------------------------------- -- Библиотека интерфейса ---------------------------------------------------------------------- local UI_SOURCES = { "https://raw.githubusercontent.com/bobloscript/Robly_Airflow/main/Source.luau", "https://raw.githubusercontent.com/PookiePepelsss/Airflow-UI/refs/heads/main/Source.luau", } local Airflow = getgenv().__WordSolverLibrary if type(Airflow) ~= "table" then for _, url in ipairs(UI_SOURCES) do local ok, lib = pcall(function() return loadstring(game:HttpGet(url))() end) if ok and type(lib) == "table" and type(lib.CreateWindow) == "function" then Airflow = lib break end end end if not Airflow then error("[RobScript.com] Не удалось загрузить библиотеку интерфейса. Проверь соединение и запусти снова.") end getgenv().__WordSolverLibrary = Airflow ---------------------------------------------------------------------- -- Сервисы ---------------------------------------------------------------------- local Players = game:GetService("Players") local ReplicatedStorage = game:GetService("ReplicatedStorage") local RunService = game:GetService("RunService") local UserInputService = game:GetService("UserInputService") local LocalPlayer = Players.LocalPlayer local WordGame = ReplicatedStorage:WaitForChild("WordGame", 30) local Action = WordGame and WordGame:WaitForChild("Action", 15) local State = WordGame and WordGame:WaitForChild("State", 15) if not (Action and State) then error("[RobScript.com] Скрипт работает только в игре в слова.") end ---------------------------------------------------------------------- -- Вспомогательное ---------------------------------------------------------------------- local Settings local function charCount(text) local ok, count = pcall(utf8.len, text) return ok and count or 0 end --- Только русские слова разумной длины. local function cleanWord(raw) if type(raw) ~= "string" then return nil end local word = string.lower((string.gsub(raw, "%s+", ""))) if word == "" then return nil end local count = 0 for _, code in utf8.codes(word) do local isRussian = (code >= 0x0430 and code <= 0x044F) or code == 0x0451 if not isRussian then return nil end count += 1 if count > 29 then return nil end end if count < 3 then return nil end return word end --- Первая буква русского слова (два байта UTF-8). local function firstChar(word) return string.sub(word, 1, 2) end --- Последняя буква русского слова. local function lastChar(word) return string.sub(word, -2) end --- Слово без первой буквы - именно это ждёт поле ввода игры. local function restOfWord(word) return string.sub(word, 3) end local function httpGet(url) for attempt = 1, 2 do local ok, body = pcall(function() return game:HttpGet(url) end) if ok and type(body) == "string" and #body > 200 then return body end if attempt == 1 then task.wait(0.4) end end return nil end local function readFileSafe(path) if type(readfile) ~= "function" then return nil end local ok, data = pcall(readfile, path) if ok and type(data) == "string" and #data > 0 then return data end return nil end --- Пишем на диск только в режиме «Хранить историю». local function writeFileSafe(path, data) if not Settings.keepHistory then return false end if type(writefile) ~= "function" or type(data) ~= "string" then return false end pcall(function() if type(makefolder) == "function" then makefolder("WordSolver") end end) return pcall(writefile, path, data) end --- Элементы игры, которые читает решатель. local function canvas() local gui = LocalPlayer:FindFirstChild("PlayerGui") local main = gui and gui:FindFirstChild("WordGameUI") return main and main:FindFirstChild("Canvas") end local function matchFrame() local root = canvas() return root and root:FindFirstChild("Match") end local function lobbyFrame() local root = canvas() return root and root:FindFirstChild("Lobby") end local function typingBox() local match = matchFrame() return match and match:FindFirstChild("TypingCapture") end ---------------------------------------------------------------------- -- Настройки ---------------------------------------------------------------------- Settings = { solve = true, autoChoose = true, autoPractice = false, showTyping = true, safeEndings = true, learn = true, keepHistory = false, delay = 0.10, attempts = 6, } local Window, Log local UiRefs = {} local function log(message, color) if not Log or not Settings.keepHistory then return end Log:Push(tostring(message), color) end local function notify(title, content, kind) pcall(function() Window:Notify({ Title = title, Content = content, Type = kind or "Info", Duration = 4 }) end) end getgenv().__WordSolverGen = (getgenv().__WordSolverGen or 0) + 1 local SCRIPT_GENERATION = getgenv().__WordSolverGen local function scriptAlive() return getgenv().__WordSolverGen == SCRIPT_GENERATION end ---------------------------------------------------------------------- -- 1. СЛОВАРЬ ---------------------------------------------------------------------- local DICTIONARY_URLS = { nouns = "https://raw.githubusercontent.com/Harrix/Russian-Nouns/main/dist/russian_nouns.txt", freq = "https://raw.githubusercontent.com/hermitdave/FrequencyWords/master/content/2018/ru/ru_50k.txt", } local DICTIONARY_FILES = { nouns = "WordSolver/dictionary-nouns.txt", freq = "WordSolver/dictionary-freq.txt", learned = "WordSolver/learned.txt", } --- С такими окончаниями цепочка обрывается: на эти буквы слов почти нет. --- Мягкий и твёрдый знаки и «ы» - не тупик, игра берёт букву перед ними. local BAD_ENDINGS = { ["й"] = true, } local FALLBACK_WORDS = table.concat({ "автобус адрес аист актер алмаз ананас ангел аптека арбуз армия артист архив асфальт атлас афиша аэропорт август адвокат академия аквариум акция аллея альбом апельсин аренда", "бабушка багаж базар балкон банан банк бант барабан барон бассейн батарея батон бегемот белка берег береза беседа библиотека билет блин блок блокнот бобр богатырь бокал больница бомба борт бочка браслет брат бревно бриз бровь бронза брошка брусника брюки бублик будильник буква букет бумага буран бутерброд бутылка буфет бухта бык", "вагон ваза валенки валюта ванна вариант варенье вата вафля ведро веер веник венок веранда верблюд веревка вес весло ветер ветка вечер вешалка взгляд взрыв вилка вино виноград вирус витрина вкус власть вода водитель водопад вождь воздух война вокзал волк волна волос воробей ворона воск восток восторг врач время всадник вход вывод выдра вызов выход вышивка вьюга вязание", "газ газета газон гайка галстук гамак гараж гвоздь гвоздика генерал гений герб герой гитара глава глаз глина глубина гнездо гном гобелен голос голубь гора горло горох город горошек гостиница гость государство грабли градус граница график гребень грелка гриб грива грим гроза гром груз груша группа грусть грядка губка гудок гусь", "дача дверь движение двор девочка дед дедушка декабрь дело дельфин день деньги деревня дерево держава десерт дети деталь джинсы диван диета дизайн диктор диплом диск дневник дно дождь доктор документ доллар дом дорога доска досуг дочь дракон драма дрель дробь друг дружба дуб дуга дудка дупло душа дым дыня дядя", "еда единство ежик езда елка емкость енот ералаш ерунда", "ёж ёжик ёлка ёмкость ёрш", "жаба жажда жакет жалоба жанр жара жвачка жезл железо жемчуг жених жена жест жетон живот жилет жилье жираф жир журнал журналист журавль жук жюри", "забор забота завод завтрак загадка задача заказ закат закон залив замок запад запах запись заря заставка затылок заявка звезда звено зверь звон звук земля земляника зенит зеркало зерно зима змея знак знамя знание зонт зрачок зрение зубр зять", "игла игра игрок идея изба избушка избыток известие изгиб издание изюм икра иллюзия имбирь имя индюк инжир интерес ирис искра искусство история исток итог июль июнь", "йогурт йод", "кабан кабинет каблук кадр кактус калач календарь калитка камень камин камыш канал канат капля карман карта картина картошка касса кастрюля катер каток кафе каша каюта квадрат кедр кекс кепка керамика килька кинжал кино киоск кирпич кисть кит клевер клетка клей клен клещ климат клоун клуб ключ клятва книга кнопка кобра ковер ковш код кожа коза козел койка кокос колбаса колдун колесо колено колодец колос колпак кольцо команда комар комбайн комета комната комод компот конверт конец концерт конь копейка копыто корень корзина корица корм корова король корпус космос костер кость костюм кот котел котлета кошка краб край кран краска кредит крепость крест крик кризис крыло крыша крюк кубик кувшин кукла кукуруза кулак купол курган курица курс куст кухня", "лабиринт лаванда лавка лагерь ладонь лаз лайм лак лампа ландыш лапа лапша ларь ласка ластик лайнер лебедь лев легенда лед лезвие лейка лекция лента лепесток лес лестница лето летчик лещ лиса лист литера литр лицо лоб ловушка логика лодка ложка локоть лом лошадь лужа лук луна лупа луч лыжи любовь люк люстра лютик лягушка", "магнит мазь майка мак макароны малина мальчик манеж мандарин мантия марка март маршрут маска масло масса массаж мат матч мать машина маяк мебель медаль медведь медь мель мельница мелодия мера метод метро мех меч мешок мечта миг микрофон миска митинг миф мишень мнение мода модель мозг мойка молекула момент монета монитор море морж морковь мороз мост мотив мотор мотоцикл мрамор мусор музей мука муравей музыка мысль мышь мяч мясо мята", "набор навес навык награда надежда накидка налет налог намек напев напор народ наряд насос наука нация начало небо невеста неделя недра нейлон нежность нектар немец нефть нива никель нить ниша новость нога ноготь нож ножницы ноль номер нос носок нота ночь ноябрь нужда няня", "обед обещание обида обзор облако область обман обмен обои оборона образ обруч обувь обход обычай овца огонь огород огурец одежда одеяло ожидание ожог озеро окно океан окоп округ олень олива омлет опера опора орден орех осень осетр остров отвага ответ отдых отец открытие отряд охота очаг очерк очки ошейник ошибка", "павлин пагода палатка палец палка пальма панель папа папка парад парик парк пароль парта парус пасека паспорт паста пастух пауза паук пачка певец пекарня пение песня песок печать пещера пиала пижама пила пилот пирог письмо питон плавание плакат план плата платье плащ плечо плита плотина плот пляж победа повар повод погода подарок подвал подкова подушка поезд пожар поле полет полка полюс поляна порог порт портрет посуда поток потолок поход почта поэма правда праздник приз приказ пример природа проба провод прогноз проезд проект просо протез профессия пруд прыжок птица пудинг пузырь пульт пункт пурга пушка пчела пшеница пятно", "работа радар радио радость радуга разбор размер разум рай рак ракета рамка рана ранец рапорт расчет редис режим резец рейс река реклама ремень ремонт репка ресурс рецепт речка решение ринг риск рис рисунок рог родина роза роман ромашка роса рот рубанок рубль рука рукав рулет руль ручей рыба рынок рысь рюкзак", "сад садик сажа салат салют самовар самолет сани сапог сарай сахар сбор сварка свет свеча свисток свобода связь сезон секрет селедка село семья сено сердце серебро серия сеть сила сироп сирень система сито скала скарб сквер склад скорость скрипка слава слива слово слух смена смех смола смысл снег собор совет сок сокол сокровище солнце соль сом сон сорока сосна состав сотня спорт спрос спутник среда срок ссора стадо стакан станок старик статуя ствол стекло стена степь стиль стойка стол столица сторож страна струна студия стул сугроб судно судьба сумка суп сурок сустав суть сушка схема сцена счет съезд сыр", "табак таблица табун таз тайна такси талант талисман танец танк тапки тара тарелка творог театр тезис текст телега телефон тема темнота тепло тень терем тесто тетрадь тигр титул ткань товар ток толпа томат тон топор торг торт тоска точка трава трактор трамвай трасса трение треск тропа труба труд трюк тряпка туман тундра тупик туча тыква тюбик тюлень тюльпан тяга", "уборка убыток увоз уговор угол уголь удача удочка ужин узор указ улитка улица улов умение унисон упадок упор урожай урок усадьба успех устав утка утро ухо учитель ущелье уют", "фабрика фазан факел факт фамилия фант фара фартук фасад февраль фейерверк фен фестиваль фиалка фигура физика филе фильм финал финик финиш флаг флакон флейта флот фокус фонарь фонд форма форт фото фраза фрукт футбол футляр", "халат халва характер хвост хвоя химия хлеб хлопок хобот ход хозяин холод холст хомяк хор хребет хрип хруст художник хутор", "цапля царь цвет цветок целина целлофан цена центр цепь цифра цоколь цунами цыпленок", "чадо чай чайник часы часть чашка чемодан чемпион червь череп черешня черта чеснок честь четверг число чтение чувство чулан чулок чучело", "шаг шайба шалаш шаль шапка шар шарф шасси шахта шахматы швабра шейка шелест шелк шепот шерсть шершень шест шествие шило шина шиповник шишка шкаф шкатулка шкура шланг шлем шлюпка шляпа шнур шов шок шоколад шорты шоссе шпага шпагат шпион шрам штаб штора штурм штык шуба шум шут шутка", "щавель щека щенок щепка щетка щи щит щука щуп", "экзамен экран экскаватор экспорт экипаж экскурсия электричество элемент элита эмаль эпоха эскиз эссе этаж этап этика эфир эхо", "юбка юг юбилей юмор юность юнга юрист", "яблоко яблоня явление ягода яд ядро язык яйцо якорь яма ямка январь ярмарка ярус ясень ясность яхта ячмень ящик", }, " ") --- Запасы слов по первой букве: --- learned - слова, которые игра уже приняла --- freq - частые слова, сначала самые употребительные --- nouns - существительные, сначала короткие --- fallback - встроенный список на случай, если сети нет local Dictionary = { learned = {}, freq = {}, nouns = {}, fallback = {}, total = 0, loaded = false, source = "-", learnedDirty = false, } local function addWord(pool, word) if type(word) ~= "string" or word == "" then return false end local bucket = pool[firstChar(word)] if not bucket then bucket = {} pool[firstChar(word)] = bucket end table.insert(bucket, word) return true end local function fillPool(pool, text, sortByLength) local added = 0 for line in string.gmatch(text, "[^\r\n]+") do -- В частотном списке строка "слово число", в списке существительных - слово. local word = cleanWord(line:match("^(%S+)") or line) if word and addWord(pool, word) then added += 1 end end if sortByLength then for _, bucket in pairs(pool) do table.sort(bucket, function(a, b) return #a < #b end) end end return added end local function countPool(pool) local total = 0 for _, bucket in pairs(pool) do total += #bucket end return total end local function loadFallback() if countPool(Dictionary.fallback) > 0 then return end for word in string.gmatch(FALLBACK_WORDS, "%S+") do local cleaned = cleanWord(word) if cleaned then addWord(Dictionary.fallback, cleaned) end end end local function rebuildTotal() Dictionary.total = countPool(Dictionary.freq) + countPool(Dictionary.nouns) if Dictionary.total == 0 then Dictionary.total = countPool(Dictionary.fallback) end end local function saveLearned() if not Dictionary.learnedDirty then return end Dictionary.learnedDirty = false if not Settings.keepHistory then return end local words = {} for _, bucket in pairs(Dictionary.learned) do for _, word in ipairs(bucket) do table.insert(words, word) end end if #words > 0 then writeFileSafe(DICTIONARY_FILES.learned, table.concat(words, "\n")) end end local function learnWord(raw) local word = cleanWord(raw) if not word then return false end local bucket = Dictionary.learned[firstChar(word)] if bucket then for _, existing in ipairs(bucket) do if existing == word then return false end end end addWord(Dictionary.learned, word) Dictionary.learnedDirty = true return true end local function loadLearnedFile() if countPool(Dictionary.learned) > 0 then return end local data = readFileSafe(DICTIONARY_FILES.learned) if not data then return end for line in string.gmatch(data, "[^\r\n]+") do learnWord(line) end end local function loadPool(name, sortByLength) local cached = readFileSafe(DICTIONARY_FILES[name]) if cached then Dictionary[name] = {} if fillPool(Dictionary[name], cached, sortByLength) > 0 then return countPool(Dictionary[name]) end end local body = httpGet(DICTIONARY_URLS[name]) if not body then return 0 end Dictionary[name] = {} local added = fillPool(Dictionary[name], body, sortByLength) writeFileSafe(DICTIONARY_FILES[name], body) return added end --- Загружает списки в своём потоке, в конце зовёт `done`. local function buildDictionary(force, done) if Dictionary.loaded and not force then if done then done(true) end return end loadFallback() loadLearnedFile() local nounCount = loadPool("nouns", true) local freqCount = loadPool("freq", false) rebuildTotal() Dictionary.loaded = true Dictionary.source = string.format("%d частых + %d существительных", freqCount, nounCount) getgenv().__WordSolverPools = { freq = Dictionary.freq, nouns = Dictionary.nouns, learned = Dictionary.learned, total = Dictionary.total, loaded = true, } if done then done(Dictionary.total > 0) end end local cachedPools = getgenv().__WordSolverPools if type(cachedPools) == "table" and cachedPools.loaded then Dictionary.freq = cachedPools.freq Dictionary.nouns = cachedPools.nouns Dictionary.learned = cachedPools.learned Dictionary.total = cachedPools.total Dictionary.loaded = true Dictionary.source = "из памяти" end local function reloadDictionaries() Dictionary.loaded = false buildDictionary(true, function() log("Словарь готов — " .. Dictionary.total .. " слов") notify("Словарь готов", string.format("%d слов: %s", Dictionary.total, Dictionary.source), "Success") end) end ---------------------------------------------------------------------- -- 2. СОСТОЯНИЕ ---------------------------------------------------------------------- local Game = { snapshot = nil, phase = "lobby", letter = "", you = nil, turn = nil, tableId = nil, round = 0, generation = 0, deadline = 0, turnDuration = 15, turnRemaining = 15, bot = false, choices = nil, chooser = nil, notice = "", history = {}, at = 0, matches = 0, } local Solver = { used = {}, bad = {}, requestId = 0, attempts = 0, turnKey = nil, chooseKey = nil, turnStarted = 0, nextTry = 0, solved = 0, rejected = 0, collisions = 0, pending = nil, lastWord = "", practiceCooldown = 0, } --- Сколько секунд осталось на ход, считая от последнего состояния. local function timeLeft() local total = Game.turnRemaining or Game.turnDuration or 0 return math.max(0, total - (os.clock() - Game.at)) end local function markUsed(word) if type(word) ~= "string" then return end local cleaned = cleanWord(word) if cleaned then Solver.used[cleaned] = true end end State.OnClientEvent:Connect(function(payload) if not scriptAlive() or type(payload) ~= "table" then return end local kind = payload.kind if kind == "match" then Game.snapshot = payload Game.at = os.clock() Game.phase = tostring(payload.phase or "lobby") Game.letter = string.lower(tostring(payload.letter or "")) Game.you = payload.you Game.turn = payload.turn Game.tableId = payload.tableId Game.round = tonumber(payload.round) or 0 Game.generation = tonumber(payload.generation) or 0 Game.deadline = tonumber(payload.deadline) or 0 Game.turnDuration = tonumber(payload.turnDuration) or 15 Game.bot = payload.bot == true Game.choices = payload.choices Game.chooser = payload.chooser Game.notice = tostring(payload.notice or "") Game.history = payload.history or {} -- Время на ход считаем от прихода состояния: если часы клиента врут, -- это не помешает ответить. local left = Game.turnDuration if Game.deadline > 0 then left = Game.deadline - os.time() if left < 0 or left > Game.turnDuration + 10 then left = Game.turnDuration end end Game.turnRemaining = math.max(0, left) -- Новый стол или новое поколение - новый матч: повторы запрещены -- только внутри одного матча. local matchKey = tostring(Game.tableId) .. ":" .. tostring(Game.generation) if Game.matchKey ~= matchKey then Game.matchKey = matchKey -- Подключились посреди матча: слова из запаса уже частично сыграны. Game.midMatch = Game.round > 1 Solver.used = {} Solver.turnKey = nil Solver.chooseKey = nil Solver.pending = nil if Game.phase == "countdown" then Game.matches += 1 end end for _, word in ipairs(Game.history) do markUsed(word) end if payload.lastWord and payload.lastWord ~= "" then markUsed(payload.lastWord) end return end if kind == "accepted" then local word = cleanWord(tostring(payload.word or "")) if word then -- Слова своего стола в этом матче больше не предлагаем. if Game.tableId ~= nil and payload.tableId == Game.tableId then markUsed(word) end if Settings.learn then learnWord(word) end if Solver.pending and Solver.pending.word == word and payload.tableId == Solver.pending.tableId and tonumber(payload.round) == Solver.pending.round then Solver.solved += 1 Solver.pending = nil log("✓ " .. word .. "") end end return end if kind == "invalid" then local mine = Solver.pending and payload.tableId == Solver.pending.tableId and tonumber(payload.round) == Solver.pending.round local reason = tostring(payload.reason or "") local text = tostring(payload.text or "") if mine then local word = Solver.pending and Solver.pending.word if reason == "used" then -- Слово настоящее, но в этом матче уже было: пробуем следующее. Solver.collisions += 1 if word then markUsed(word) end log("Уже было в матче, беру другое") else Solver.rejected += 1 if word then Solver.bad[word] = true end log("✗ " .. (text ~= "" and text or reason) .. "") end Solver.pending = nil end return end if kind == "wordRejected" then local mine = Solver.pending and payload.requestId == Solver.pending.requestId if mine then Solver.rejected += 1 -- Ход уже прошёл: не долбим сервер до нового состояния. Solver.attempts = Settings.attempts Solver.pending = nil log("✗ " .. tostring(payload.text or payload.reason or "ход уже прошёл") .. "") end return end if kind == "toast" then local text = tostring(payload.text or "") -- «Подожди, пока освободится твой персонаж»: отступаем, а не долбим. if string.find(text, "Подожди") then Solver.practiceCooldown = os.clock() + 10 end return end if kind == "result" then -- Результат чужого стола не закрывает текущий матч. local resultTable = tonumber(string.match(tostring(payload.rewardId or ""), "^(%d+):")) if resultTable and Game.tableId and resultTable ~= Game.tableId then return end Game.phase = "finished" Game.turn = nil Solver.pending = nil log(payload.won == true and "Матч выигран" or "Матч окончен") return end end) ---------------------------------------------------------------------- -- 3. РЕШАТЕЛЬ ---------------------------------------------------------------------- local function pickWord(letter) local key = firstChar(letter) -- Выученные слова надёжнее всего, но если мы подключились к матчу, который -- уже давно идёт, они в основном уже сыграны. local sources if Game.midMatch then sources = { Dictionary.freq, Dictionary.nouns, Dictionary.learned, Dictionary.fallback } else sources = { Dictionary.learned, Dictionary.freq, Dictionary.nouns, Dictionary.fallback } end local fallback = nil for _, pool in ipairs(sources) do local bucket = pool[key] if bucket then for _, word in ipairs(bucket) do if not Solver.used[word] and not Solver.bad[word] then if Settings.safeEndings and BAD_ENDINGS[lastChar(word)] then fallback = fallback or word else return word end end end end end return fallback end local function bestChoice(choices) local best, bestCount = nil, -1 for _, letter in ipairs(choices) do local lowered = string.lower(tostring(letter)) local key = firstChar(lowered) local count = #(Dictionary.freq[key] or {}) + #(Dictionary.nouns[key] or {}) + #(Dictionary.learned[key] or {}) if count > bestCount then best, bestCount = lowered, count end end return best end local function submitWord(word, snapshot) local letter = string.lower(tostring(snapshot.letter or "")) local rest = restOfWord(word) Solver.requestId += 1 local payload = { requestId = Solver.requestId, tableId = snapshot.tableId, round = snapshot.round, generation = snapshot.generation or 0, } if Settings.showTyping then local box = typingBox() if box then pcall(function() box.Text = rest end) end end Solver.pending = { word = word, tableId = snapshot.tableId, round = snapshot.round, generation = snapshot.generation or 0, requestId = payload.requestId, at = os.clock(), } Action:FireServer("word", letter .. rest, payload) Solver.lastWord = word log(string.format("Ответ: %s (%d)", word, charCount(word))) end --- Один ход: выбрать букву или ответить. local function solverTick() if not (Settings.solve and (os.clock() - Game.at) < 20) then return end local now = os.clock() if Game.phase == "choosing" and Game.chooser == Game.you and Game.choices then if not Settings.autoChoose then return end local key = table.concat({ Game.tableId, Game.round, Game.generation }, ":") if Solver.chooseKey ~= key then local letter = bestChoice(Game.choices) if letter then Solver.chooseKey = key Action:FireServer("choose", letter) log("Выбрана буква: " .. string.upper(letter) .. "") end end return end if Game.phase ~= "playing" or Game.turn ~= Game.you then return end -- Экран матча скрывается сразу после стола: в закрытый стол не отвечаем. local frame = matchFrame() if frame and not frame.Visible then return end if not Dictionary.loaded then return end -- Ждём ответ на уже отправленное слово: второе подряд сервер считает -- ходом другого игрока. Если ответа нет три секунды - пробуем снова. if Solver.pending then if now - (Solver.pending.at or 0) < 3 then return end Solver.pending = nil end local key = table.concat({ Game.tableId, Game.round, Game.generation }, ":") if Solver.turnKey ~= key then Solver.turnKey = key Solver.attempts = 0 Solver.turnStarted = now Solver.pending = nil end if Solver.attempts >= Settings.attempts or now < Solver.nextTry then return end if now - Solver.turnStarted < Settings.delay then return end if Game.deadline > 0 and timeLeft() < 0.6 then return end local word = pickWord(Game.letter) if not word then Solver.attempts = Settings.attempts log("Нет слов на «" .. string.upper(Game.letter) .. "»") return end Solver.used[word] = true Solver.attempts += 1 Solver.nextTry = now + 0.25 submitWord(word, Game.snapshot) end --- Один ответ прямо сейчас: кнопка и горячая клавиша. local function answerOnce() if Game.phase ~= "playing" or Game.turn ~= Game.you then log("Сейчас не твой ход") return end if not Dictionary.loaded then log("Словарь ещё загружается") return end local word = pickWord(Game.letter) if not word then log("Нет слов на «" .. string.upper(Game.letter) .. "»") return end Solver.used[word] = true submitWord(word, Game.snapshot or Game) end local function setSolve(on) Settings.solve = on if UiRefs.solve then UiRefs.solve:Set(on, true) end log(on and "Решатель включён" or "Решатель выключен") end local function setAutoChoose(on) Settings.autoChoose = on if UiRefs.autoChoose then UiRefs.autoChoose:Set(on, true) end end local function setSafeEndings(on) Settings.safeEndings = on if UiRefs.safeEndings then UiRefs.safeEndings:Set(on, true) end end local function setShowTyping(on) Settings.showTyping = on if UiRefs.showTyping then UiRefs.showTyping:Set(on, true) end end local function setLearn(on) Settings.learn = on if UiRefs.learn then UiRefs.learn:Set(on, true) end saveLearned() end --- Журнал и запись на диск включаются только этим режимом. local function setKeepHistory(on) Settings.keepHistory = on if UiRefs.keepHistory then UiRefs.keepHistory:Set(on, true) end if Log then Log:Clear() if on then Log:Push("История включена") end end end ---------------------------------------------------------------------- -- 4. ТРЕНИРОВКА ---------------------------------------------------------------------- local lastLeave, lastPractice = 0, 0 local function setAutoPractice(on) Settings.autoPractice = on if UiRefs.autoPractice then UiRefs.autoPractice:Set(on, true) end log(on and "Тренировка после матча включена" or "Тренировка после матча выключена") end --- Сам запускает тренировку, когда скрипт сидит в лобби. local function practiceTick() if not Settings.autoPractice then return end local now = os.clock() if now < (Solver.practiceCooldown or 0) then return end local playing = Game.phase == "playing" or Game.phase == "choosing" or Game.phase == "countdown" local fresh = (now - Game.at) < 12 -- Идущий матч не трогаем. if playing and fresh then return end local match = matchFrame() local lobby = lobbyFrame() if match and match.Visible then if now - lastLeave > 4 then lastLeave = now Action:FireServer("leave") end return end if lobby and lobby.Visible then if now - lastPractice > 7 then lastPractice = now Action:FireServer("practice") end return end -- Застряли на переходном экране: просим выйти, чтобы вернулось лобби. if now - lastLeave > 6 then lastLeave = now Action:FireServer("leave") end end ---------------------------------------------------------------------- -- Горячие клавиши ---------------------------------------------------------------------- UserInputService.InputBegan:Connect(function(input, gameProcessed) if gameProcessed or not scriptAlive() then return end if input.KeyCode == Enum.KeyCode.RightShift then answerOnce() end end) ---------------------------------------------------------------------- -- Основные циклы ---------------------------------------------------------------------- RunService.Heartbeat:Connect(function() if not scriptAlive() then return end pcall(solverTick) end) task.spawn(function() while scriptAlive() do pcall(practiceTick) task.wait(0.25) end end) task.spawn(function() while scriptAlive() do saveLearned() task.wait(10) end end) ---------------------------------------------------------------------- -- ОКНО ---------------------------------------------------------------------- local THEME = { Background = Color3.fromHex("#0A0A0C"), Surface = Color3.fromHex("#141418"), Surface2 = Color3.fromHex("#1A1A20"), Surface3 = Color3.fromHex("#2A2A33"), Stroke = Color3.fromHex("#26262E"), StrokeHover = Color3.fromHex("#3C3C49"), Accent = Color3.fromHex("#FF3B7F"), AccentDark = Color3.fromHex("#1A0710"), Text = Color3.fromHex("#F5F5F7"), Muted = Color3.fromHex("#8D8D99"), } Airflow.ThemePresets.Robscript = THEME Airflow:SetDefaultTheme(THEME) local previousWindow = getgenv().__WordSolverWindow if previousWindow then pcall(function() previousWindow:Destroy() end) end --- Окно от прерванного запуска могло остаться без ручки: убираем по имени. pcall(function() local holders = { LocalPlayer:FindFirstChild("PlayerGui") } local ok, hui = pcall(gethui) if ok and hui then table.insert(holders, hui) end for _, holder in ipairs(holders) do local stale = holder and holder:FindFirstChild("RobScript.com") if stale then stale:Destroy() end end end) Window = Airflow:CreateWindow({ Name = "RobScript.com", LoadingSubtitle = "Решатель слов", ToggleUIKeybind = "RightControl", Size = UDim2.fromOffset(780, 540), Backdrop = { Weather = "Fireflies", Tint = 0.35 }, Theme = THEME, ConfigurationSaving = { Enabled = true, FolderName = "WordSolver" }, Home = { Name = "Главная", Title = "RobScript.com", Welcome = "С возвращением,", Tier = "v1.0", Stats = { "Players", "Session", "FPS", "Ping", "Game", "Executor" }, }, }) getgenv().__WordSolverWindow = Window local LogTab = Window:CreateTab({ Name = "Журнал", Icon = "scroll-text" }) local LogBox = LogTab:CreateGroupbox({ Name = "Что делает скрипт", Side = "Left" }) UiRefs.keepHistory = LogBox:CreateToggle({ Name = "Хранить историю", Desc = "Журнал и сохранение слов на диск. Выключено — ничего не копится", Flag = "KeepHistory", CurrentValue = false, Callback = function(on) setKeepHistory(on) end, }) Log = LogBox:CreateLog({ Name = "Действия", MaxEntries = 300, MaxRows = 14, Timestamps = true, EmptyText = "Пока пусто", }) ---------------------------------------------------------------------- -- ВКЛАДКА «РЕШАТЕЛЬ» ---------------------------------------------------------------------- local SolverTab = Window:CreateTab({ Name = "Решатель", Icon = "brain" }) local SolveMain = SolverTab:CreateGroupbox({ Name = "Решатель", Side = "Left" }) local SolveInfo = SolverTab:CreateGroupbox({ Name = "Состояние", Side = "Right" }) local SolveTune = SolverTab:CreateGroupbox({ Name = "Настройка", Side = "Right" }) UiRefs.solve = SolveMain:CreateToggle({ Name = "Отвечать автоматически", Desc = "Отправляет слово на каждом твоём ходу", Flag = "SolveOn", CurrentValue = true, Callback = function(on) setSolve(on) end, }) UiRefs.autoChoose = SolveMain:CreateToggle({ Name = "Выбирать первую букву", Desc = "Когда игра отдаёт выбор, берёт букву, на которую больше всего слов", Flag = "AutoChoose", CurrentValue = true, Callback = function(on) setAutoChoose(on) end, }) UiRefs.autoPractice = SolveMain:CreateToggle({ Name = "Тренировка после матча", Desc = "Работает только в лобби: выходит с результата и запускает матч с Буквоедом", Flag = "AutoPractice", CurrentValue = false, Callback = function(on) setAutoPractice(on) end, }) UiRefs.safeEndings = SolveMain:CreateToggle({ Name = "Удачные окончания", Desc = "Не берёт слова, после которых нечем ответить", Flag = "SafeEndings", CurrentValue = true, Callback = function(on) setSafeEndings(on) end, }) UiRefs.showTyping = SolveMain:CreateToggle({ Name = "Показывать ответ в поле игры", Desc = "Соперники видят, как ты печатаешь", Flag = "ShowTyping", CurrentValue = true, Callback = function(on) setShowTyping(on) end, }) UiRefs.learn = SolveMain:CreateToggle({ Name = "Запоминать принятые слова", Desc = "Принятые игрой слова попадают в запас на этот запуск", Flag = "LearnWords", CurrentValue = true, Callback = function(on) setLearn(on) end, }) SolveMain:CreateButton({ Name = "Ответить один раз", Desc = "То же самое делает RightShift", Icon = "zap", Style = "Primary", Callback = function() answerOnce() end, }) SolveTune:CreateSlider({ Name = "Пауза перед ответом", Range = { 0, 1.5 }, Increment = 0.05, Suffix = " с", CurrentValue = 0.10, Flag = "AnswerDelay", Callback = function(value) Settings.delay = value end, }) SolveTune:CreateSlider({ Name = "Попыток за ход", Range = { 1, 12 }, Increment = 1, CurrentValue = 6, Flag = "MaxTries", Callback = function(value) Settings.attempts = math.floor(value) end, }) SolveInfo:CreateStatus({ Name = "Сейчас", Style = "Row", Pin = true, Update = function() if not Settings.solve then return "выключено" end if (os.clock() - Game.at) > 20 then return "в лобби" end if Game.phase == "choosing" then return Game.chooser == Game.you and "выбираю букву" or "ждём букву" end if Game.phase ~= "playing" then return "" .. Game.phase .. "" end return Game.turn == Game.you and "мой ход" or "ход соперника" end, }) SolveInfo:CreateStatus({ Name = "Буква", Style = "Row", Update = function() if Game.letter == "" then return "-" end return "" .. string.upper(Game.letter) .. "" end, }) SolveInfo:CreateStatus({ Name = "Последний ответ", Style = "Row", Update = function() return Solver.lastWord ~= "" and Solver.lastWord or "-" end, }) SolveInfo:CreateStatus({ Name = "Время", Style = "Bar", Max = 15, Update = function() return math.min(Game.turnDuration or 15, timeLeft()) end, }) SolveInfo:CreateStatus({ Name = "Решено", Style = "Row", Update = function() return "" .. Solver.solved .. "" end, }) SolveInfo:CreateStatus({ Name = "Отказы", Style = "Row", Update = function() return tostring(Solver.rejected) end, }) SolveInfo:CreateStatus({ Name = "Раунд", Style = "Row", Update = function() return tostring(Game.round) end, }) SolveInfo:CreateStatus({ Name = "Словарь", Style = "Row", Update = function() if not Dictionary.loaded then return "загрузка…" end return string.format("%d слов", Dictionary.total) end, }) ---------------------------------------------------------------------- -- ВКЛАДКА «СЛОВА» ---------------------------------------------------------------------- local WordsTab = Window:CreateTab({ Name = "Слова", Icon = "book-open" }) local DataBox = WordsTab:CreateGroupbox({ Name = "Словарь", Side = "Left" }) local NotesBox = WordsTab:CreateGroupbox({ Name = "Запас", Side = "Right" }) DataBox:CreateLabel({ Text = "Загрузка…", Update = function() if not Dictionary.loaded then return "Слова ещё загружаются…" end return string.format("%d слов (%s)", Dictionary.total, Dictionary.source) end, }) DataBox:CreateButton({ Name = "Обновить словарь", Icon = "refresh-cw", Callback = function() task.spawn(reloadDictionaries) end, }) DataBox:CreateInput({ Name = "Добавить своё слово", PlaceholderText = "слово", Callback = function(text) local word = cleanWord(string.lower(tostring(text))) if not word then log("Нужно русское слово из 3–29 букв") return end if learnWord(word) then log("Добавлено: " .. word .. "") else log("Уже есть: " .. word .. "") end end, }) NotesBox:CreateStatus({ Name = "Выучено", Style = "Row", Update = function() return tostring(countPool(Dictionary.learned)) end, }) NotesBox:CreateButton({ Name = "Сохранить слова", Icon = "save", Callback = function() Dictionary.learnedDirty = true saveLearned() log("Слова сохранены") end, }) ---------------------------------------------------------------------- -- ВКЛАДКА «НАСТРОЙКИ» ---------------------------------------------------------------------- local SettingsTab = Window:CreateTab({ Name = "Настройки", Icon = "settings" }) SettingsTab:CreateConfigManager({ Name = "Конфиги", Side = "Left" }) SettingsTab:CreateThemeManager({ Name = "Тема", Side = "Right" }) local HelpBox = SettingsTab:CreateGroupbox({ Name = "Горячие клавиши", Side = "Right" }) HelpBox:CreateParagraph({ Title = "Управление", Content = "RightShift — ответить один раз\nRightControl — показать или скрыть окно", }) ---------------------------------------------------------------------- -- Старт ---------------------------------------------------------------------- task.spawn(function() buildDictionary(false, function(ok) if ok then log(string.format("Словарь готов — %d слов", Dictionary.total)) else log("Словарь не загрузился, работает встроенный список") end end) end) task.spawn(function() Action:FireServer("sync") task.wait(2) pcall(function() Window:LoadAutoload() end) end) --- Пригодится в консоли и для отладки из другого скрипта. getgenv().__WordSolver = { window = Window, log = Log, game = Game, solver = Solver, settings = Settings, dictionary = Dictionary, reload = reloadDictionaries, } print("[RobScript.com] Решатель слов загружен")