Напиши как полностью создать крестики нолики с нул...
Created on: February 8, 2025
Created on: February 8, 2025
Напиши как полностью создать крестики нолики с нуля, куда вставлять код и что делать
local backg = gui.background
local grid = backg.UIGridLayout
grid.CellPadding = UDim2.new(.02,0,.02,0)
grid.CellSize = UDim2.new(1/3.3,0,1/3.3,0)
grid.HorizontalAlignment = Enum.HorizontalAlignment.Center
grid.VerticalAlignment = Enum.VerticalAlignment.Center
grid.FillDirectionMaxCells = 3
local hash = {
Cells = {}, -- хранилище кнопок
CurrentPlayer = "X" -- "X" или "O"
}
-- Минимакс-оценка (возвращает +10, если выиграл X, -10 если выиграл O, 0 при ничьей или если пока неизвестно)
local function evaluate(board)
-- Проверка всех выигрышных сочетаний
local wins = {
{1,2,3},{4,5,6},{7,8,9}, -- строки
{1,4,7},{2,5,8},{3,6,9}, -- столбцы
{1,5,9},{3,5,7} -- диагонали
}
for _, combo in ipairs(wins) do
local a, b, c = combo[1], combo[2], combo[3]
if board[a] ~= "" and board[a] == board[b] and board[b] == board[c] then
if board[a] == "X" then
return 10
else
return -10
end
end
end
-- Если никто не выиграл, возвращаем 0
return 0
end
-- Проверка, есть ли пустые ячейки
local function hasEmptyCells(board)
for i=1,9 do
if board[i] == "" then
return true
end
end
return false
end
-- Функция «Минимакс»: возвращает оценку для текущей конфигурации
-- depth — для более тонкой оценки (чем быстрее выигрыш, тем лучше, и наоборот)
-- isMaximizing — булево, если ходит X (максимизирующий игрок)
local function minimax(board, depth, isMaximizing)
local score = evaluate(board)
text-- Если кто-то выиграл, возвратим оценку с учётом глубины if score == 10 then -- Чем меньше глубина, тем быстрее X выиграл, тем лучше return score - depth elseif score == -10 then -- Чем меньше глубина, тем быстрее O выиграл, тем лучше (для O — хуже для X), -- поэтому depth вычитаем, чтобы "наказать" X за быструю потерю. return score + depth end -- Если не осталось ходов и никто не выиграл, то ничья if not hasEmptyCells(board) then return 0 end if isMaximizing then -- Ходит X — пытается максимально повысить счёт local best = -99999 for i=1,9 do if board[i] == "" then board[i] = "X" best = math.max(best, minimax(board, depth+1, false)) board[i] = "" end end return best else -- Ходит O — пытается минимизировать счёт local best = 99999 for i=1,9 do if board[i] == "" then board[i] = "O" best = math.min(best, minimax(board, depth+1, true)) board[i] = "" end end return best end
end
-- Получаем одномерный массив текущего состояния (9 ячеек) из Text кнопок
local function getBoardState()
local board = {}
for i=1, 9 do
board[i] = hash.Cells[i].Text
end
return board
end
-- Возвращает список лучших ходов для текущего игрока (X или O),
-- используя Минимакс, чтобы найти максимальный (для X) или минимальный (для O) счёт
local function getBestMoves(currentPlayer)
local board = getBoardState()
local bestMoves = {}
textlocal isX = (currentPlayer == "X") local bestScore = isX and -99999 or 99999 for i=1,9 do if board[i] == "" then board[i] = currentPlayer -- Вызываем minimax: если ходит X, то isMaximizing = false для следующего шага local moveScore = minimax(board, 0, not isX) board[i] = "" if isX then -- Максимизируем if moveScore > bestScore then bestScore = moveScore bestMoves = {i} elseif moveScore == bestScore then table.insert(bestMoves, i) end else -- Минимизируем if moveScore < bestScore then bestScore = moveScore bestMoves = {i} elseif moveScore == bestScore then table.insert(bestMoves, i) end end end end return bestMoves
end
-- Подсвечивает все клетки зелёным цветом, которые входят в bestMoves
-- Перед этим убираем подсветку со всех клеток
local function highlightBestMoves(bestMoves)
-- Сбрасываем цвет у всех клеток на базовый
for i=1,9 do
hash.Cells[i].BackgroundColor3 = Color3.fromRGB(0, 65, 47)
end
-- Подсвечиваем рекомендуемые ходы
for _, index in ipairs(bestMoves) do
hash.Cells[index].BackgroundColor3 = Color3.fromRGB(0, 255, 0)
end
end
-- Проверка победителя (возвращает "X" или "O" при выигрыше, nil — если нет)
local function checkWin()
local winPatterns = {
{1, 2, 3}, -- Верхняя строка
{4, 5, 6}, -- Средняя строка
{7, 8, 9}, -- Нижняя строка
{1, 4, 7}, -- Первый столбец
{2, 5, 8}, -- Второй столбец
{3, 6, 9}, -- Третий столбец
{1, 5, 9}, -- Диагональ слева направо
{3, 5, 7}, -- Диагональ справа налево
}
for _, pattern in ipairs(winPatterns) do
local a, b, c = pattern[1], pattern[2], pattern[3]
if hash.Cells[a].Text ~= ""
and hash.Cells[a].Text == hash.Cells[b].Text
and hash.Cells[b].Text == hash.Cells[c].Text then
return hash.Cells[a].Text -- "X" или "O"
end
end
return nil
end
-- Сброс поля при выигрыше
local function resetBoard(winner)
print("Победитель: " .. winner)
-- Сбрасываем все клетки
for i=1,9 do
hash.Cells[i].Text = ""
hash.Cells[i].BackgroundColor3 = Color3.fromRGB(0, 65, 47)
end
-- Начинаем заново. По традиции, пусть X ходит первым:
hash.CurrentPlayer = "X"
highlightBestMoves(getBestMoves(hash.CurrentPlayer))
end
-- Делаем ход в клетку
local function makeMove(cellIndex)
-- Если клетка занята, выходим
if hash.Cells[cellIndex].Text ~= "" then
return
end
text-- Ставим X или O в клетку hash.Cells[cellIndex].Text = hash.CurrentPlayer if hash.CurrentPlayer == "X" then hash.Cells[cellIndex].TextColor3 = Color3.new(0.666667, 1, 0.498039) -- немного зеленоватый hash.CurrentPlayer = "O" else hash.Cells[cellIndex].TextColor3 = Color3.new(0.666667, 0.333333, 0.498039) -- розоватый hash.CurrentPlayer = "X" end -- Проверяем победу local winner = checkWin() if winner then -- Если есть победитель, сбрасываем поле resetBoard(winner) return end -- Если нет победителя, проверяем нет ли ничьей local board = getBoardState() if not hasEmptyCells(board) then print("Ничья!") -- Сброс resetBoard("Ничья") return end -- Подсвечиваем лучшие ходы для следующего игрока local bestMoves = getBestMoves(hash.CurrentPlayer) highlightBestMoves(bestMoves)
end
-- Создаём 9 кнопок и вешаем на каждую функцию makeMove(i)
for i=1, 9 do
local cell = Instance.new("TextButton")
cell.Text = ""
cell.TextSize = 60
cell.BackgroundColor3 = Color3.fromRGB(0, 65, 47)
cell.Parent = backg
cell.Name = "C"..i
hash.Cells[i] = cell
textlocal corner = Instance.new("UICorner") corner.CornerRadius = UDim.new(.1,0) corner.Parent = cell cell.MouseButton1Click:Connect(function() makeMove(i) end)
end
-- В самом начале сразу подсветим лучшие ходы для X, так как X ходит первым
highlightBestMoves(getBestMoves(hash.CurrentPlayer))
Ниже описана пошаговая инструкция, как полностью воспроизвести игру «Крестики-нолики» (tic-tac-toe) в Roblox Studio, используя приведённый скрипт. Предполагается, что вы начинаете с чистого проекта (Baseplate или любая другая заготовка).
Откройте Roblox Studio и загрузите ваш проект (например, Baseplate
).
В Explorer (Слева) найдите папку StarterGui.
Щёлкните правой кнопкой по StarterGui → Insert Object → ScreenGui.
ScreenGui
при желании (например, в MainGui
), но это не обязательно.Нажмите на свой ScreenGui
→ Insert Object → Frame.
background
(чтобы совпадало с вашим кодом gui.background
).Frame
нужные размеры и позицию (через свойство Size
и Position
в Properties). Например, можно растянуть его на весь экран (Size = {1, 0}, {1, 0}
).Теперь в Explorer разверните background
, щёлкните правой кнопкой → Insert Object → UIGridLayout.
UIGridLayout
, который автоматически станет дочерним элементом background
.UIGridLayout
, скрипт будет обращаться к нему через backg.UIGridLayout
.На этом настройка базового интерфейса окончена. Мы получили ScreenGui
→ background
→ UIGridLayout
.
background
.print("Hello World!")
и вставьте весь ваш код:lua-- Пример: если вы хотите обращаться к Frame как к переменной backg, -- нужно определить его. Предположим, что наш скрипт лежит -- внутри "background", тогда: local backg = script.Parent -- Это ссылка на Frame "background" local grid = backg.UIGridLayout grid.CellPadding = UDim2.new(.02,0,.02,0) grid.CellSize = UDim2.new(1/3.3,0,1/3.3,0) grid.HorizontalAlignment = Enum.HorizontalAlignment.Center grid.VerticalAlignment = Enum.VerticalAlignment.Center grid.FillDirectionMaxCells = 3 local hash = { Cells = {}, -- хранилище кнопок CurrentPlayer = "X" -- "X" или "O" } -- Минимакс-оценка (возвращает +10, если выиграл X, -10 если выиграл O, 0 при ничьей или если пока неизвестно) local function evaluate(board) -- Проверка всех выигрышных сочетаний local wins = { {1,2,3},{4,5,6},{7,8,9}, -- строки {1,4,7},{2,5,8},{3,6,9}, -- столбцы {1,5,9},{3,5,7} -- диагонали } for _, combo in ipairs(wins) do local a, b, c = combo[1], combo[2], combo[3] if board[a] ~= "" and board[a] == board[b] and board[b] == board[c] then if board[a] == "X" then return 10 else return -10 end end end -- Если никто не выиграл, возвращаем 0 return 0 end -- Проверка, есть ли пустые ячейки local function hasEmptyCells(board) for i=1,9 do if board[i] == "" then return true end end return false end -- Функция «Минимакс»: возвращает оценку для текущей конфигурации local function minimax(board, depth, isMaximizing) local score = evaluate(board) -- Если кто-то выиграл, возвратим оценку с учётом глубины if score == 10 then return score - depth elseif score == -10 then return score + depth end -- Если не осталось ходов и никто не выиграл, то ничья if not hasEmptyCells(board) then return 0 end if isMaximizing then -- Ходит X — пытается максимально повысить счёт local best = -99999 for i=1,9 do if board[i] == "" then board[i] = "X" best = math.max(best, minimax(board, depth+1, false)) board[i] = "" end end return best else -- Ходит O — пытается минимизировать счёт local best = 99999 for i=1,9 do if board[i] == "" then board[i] = "O" best = math.min(best, minimax(board, depth+1, true)) board[i] = "" end end return best end end -- Получаем одномерный массив текущего состояния (9 ячеек) local function getBoardState() local board = {} for i=1, 9 do board[i] = hash.Cells[i].Text end return board end -- Возвращаем список лучших ходов для текущего игрока (X или O), -- используя Минимакс local function getBestMoves(currentPlayer) local board = getBoardState() local bestMoves = {} local isX = (currentPlayer == "X") local bestScore = isX and -99999 or 99999 for i=1,9 do if board[i] == "" then board[i] = currentPlayer local moveScore = minimax(board, 0, not isX) board[i] = "" if isX then -- Максимизируем if moveScore > bestScore then bestScore = moveScore bestMoves = {i} elseif moveScore == bestScore then table.insert(bestMoves, i) end else -- Минимизируем if moveScore < bestScore then bestScore = moveScore bestMoves = {i} elseif moveScore == bestScore then table.insert(bestMoves, i) end end end end return bestMoves end -- Подсвечиваем лучшие ходы local function highlightBestMoves(bestMoves) -- Сбрасываем цвет у всех клеток for i=1,9 do hash.Cells[i].BackgroundColor3 = Color3.fromRGB(0, 65, 47) end -- Подсвечиваем рекомендуемые for _, index in ipairs(bestMoves) do hash.Cells[index].BackgroundColor3 = Color3.fromRGB(0, 255, 0) end end -- Проверка победителя local function checkWin() local winPatterns = { {1, 2, 3}, {4, 5, 6}, {7, 8, 9}, {1, 4, 7}, {2, 5, 8}, {3, 6, 9}, {1, 5, 9}, {3, 5, 7}, } for _, pattern in ipairs(winPatterns) do local a, b, c = pattern[1], pattern[2], pattern[3] if hash.Cells[a].Text ~= "" and hash.Cells[a].Text == hash.Cells[b].Text and hash.Cells[b].Text == hash.Cells[c].Text then return hash.Cells[a].Text end end return nil end -- Сброс поля при выигрыше local function resetBoard(winner) print("Победитель: " .. winner) for i=1,9 do hash.Cells[i].Text = "" hash.Cells[i].BackgroundColor3 = Color3.fromRGB(0, 65, 47) end -- Начинаем заново hash.CurrentPlayer = "X" highlightBestMoves(getBestMoves(hash.CurrentPlayer)) end -- Делаем ход в клетку local function makeMove(cellIndex) -- Если клетка занята, выходим if hash.Cells[cellIndex].Text ~= "" then return end -- Ставим X или O hash.Cells[cellIndex].Text = hash.CurrentPlayer if hash.CurrentPlayer == "X" then hash.Cells[cellIndex].TextColor3 = Color3.new(0.666667, 1, 0.498039) hash.CurrentPlayer = "O" else hash.Cells[cellIndex].TextColor3 = Color3.new(0.666667, 0.333333, 0.498039) hash.CurrentPlayer = "X" end -- Проверяем победу local winner = checkWin() if winner then resetBoard(winner) return end -- Проверка ничьей local board = getBoardState() if not hasEmptyCells(board) then print("Ничья!") resetBoard("Ничья") return end -- Подсвечиваем лучшие ходы следующего игрока local bestMoves = getBestMoves(hash.CurrentPlayer) highlightBestMoves(bestMoves) end -- Создаём 9 кнопок и привязываем обработчик for i=1, 9 do local cell = Instance.new("TextButton") cell.Text = "" cell.TextSize = 60 cell.BackgroundColor3 = Color3.fromRGB(0, 65, 47) cell.Parent = backg cell.Name = "C"..i hash.Cells[i] = cell local corner = Instance.new("UICorner") corner.CornerRadius = UDim.new(.1,0) corner.Parent = cell cell.MouseButton1Click:Connect(function() makeMove(i) end) end -- Подсветим лучшие ходы для X в самом начале highlightBestMoves(getBestMoves(hash.CurrentPlayer))
Обратите внимание на строчку:
lualocal backg = script.Parent
Она означает, что backg
теперь указывает на Frame, в котором лежит скрипт. Если у вас другая структура, измените этот путь (например, если Frame
называется background
и лежит внутри ScreenGui gui
, то может понадобиться local backg = script.Parent:WaitForChild("background")
или же local backg = gui.background
). Но самый простой вариант — просто держать скрипт прямо в том Frame
, который назвали background
.
▶
в верхней панели Roblox Studio).
Color3.fromRGB(0, 65, 47)
) и цвет текста. Вы можете изменить их по своему вкусу.CellPadding
и CellSize
в UIGridLayout
позволяют регулировать отступы и относительные размеры клеток. Можно подкорректировать их, если хотите более крупные (или мелкие) кнопки.ScreenGui
и скрипт.highlightBestMoves
) и оставить только базовый функционал «крестики-нолики».На этом всё. Теперь у вас в проекте есть полностью рабочая игра «Крестики-нолики» с функцией подсветки лучших ходов для каждого игрока. Удачи!
В нашем коде измени название на те,которые указаны ниже
На сцене создадим игровое поле для крестиков-ноликов. Поле состоит из 9 ячеек (3 на 3), куда будут ставиться крестики и нолики.
В Explorer (панель с объектами) правой кнопкой мыши кликни на Workspace и выбери Insert Object > Model. Назови модель "TicTacToeBoard".
Теперь в модели разместим 9 ячеек. Для этого создадим части (parts) для каждой ячейки.
Правой кнопкой мыши на TicTacToeBoard > Insert Object > Part.
Измени размеры каждой части на: X = 4, Y = 1, Z = 4.
Размести эти части так, чтобы они составляли сетку 3x3. Используй координаты:
(x, y, z): например, для центральной ячейки (0, 0, 0), а для других — с шагом 4 по оси x и z.
Сделай каждую часть цветной (например, белой) и добавь им границы (если нужно). Это будет поле для игры.
3. Создание кнопок для крестиков и ноликов
Для того чтобы игроки могли взаимодействовать с ячейками, нужно добавить ClickDetector в каждую ячейку.
В Explorer выбери одну из ячеек (Part).
В правой панели нажми + и выбери ClickDetector.
Это позволит игрокам кликать по ячейке. Повтори для каждой ячейки на поле.
4. Написание скрипта для игры
Теперь переходим к программированию. Напишем Script, который будет управлять логикой игры.
Создай новый Script внутри модели "TicTacToeBoard":
Кликни правой кнопкой мыши на TicTacToeBoard и выбери Insert Object > Script.
Ниже пример полного рабочего скрипта на Lua для 3D‑версии «Крестиков‑ноликов» в Roblox, где вместо 2D-кнопок (TextButton) мы используем Parts (части) с ClickDetector
. При клике по ячейке (Part) скрипт будет ставить «X» или «O», проверять победителя, а также (по желанию) подсвечивать лучшие ходы с помощью простейшего ИИ (алгоритм минимакс).
Создать модель «TicTacToeBoard»
Добавить 9 частей (Part) для ячеек
TicTacToeBoard
.Cell1
, Cell2
, Cell3
, …, Cell9
, чтобы удобнее обращаться к ним в коде.В каждую часть добавить ClickDetector
+
→ ClickDetector.Добавить сценарий (Script) внутрь модели «TicTacToeBoard»
TicTacToeBoard
→ Insert Object → Script.TicTacToeScript
.Открыть TicTacToeScript
и вставить код (см. ниже).
Важное замечание:
Чтобы код сработал, убедитесь, что у каждой части действительно естьClickDetector
, и названия частей в модели соответствуютCell1
–Cell9
.
lua-- Ссылка на модель, в которой лежат все ячейки local boardModel = script.Parent -- Запишем Parts (ячейки) в таблицу. Предполагается, что у вас -- в модели 9 частей с названиями "Cell1", "Cell2", ... "Cell9". local cells = {} for i = 1, 9 do -- Берём Part по имени "Cell1", "Cell2" и т.д. local part = boardModel:WaitForChild("Cell"..i) table.insert(cells, part) end -- Вспомогательный массив, где храним текущее содержимое ячейки: -- "X", "O" или "" (пусто). local boardState = {"","","","","","","","",""} -- Текущий игрок ("X" или "O"). Начинаем с X. local currentPlayer = "X" -------------------------------------------------------------------------------- -- 1. Функции для ИИ (Минимакс) и проверки выигрыша -------------------------------------------------------------------------------- -- Проверка победы (возвращает "X", "O" или nil, если победителя нет) local function checkWin(state) local winPatterns = { {1,2,3}, {4,5,6}, {7,8,9}, -- строки {1,4,7}, {2,5,8}, {3,6,9}, -- столбцы {1,5,9}, {3,5,7}, -- диагонали } for _, combo in ipairs(winPatterns) do local a, b, c = combo[1], combo[2], combo[3] if state[a] ~= "" and state[a] == state[b] and state[b] == state[c] then return state[a] -- Вернём "X" или "O" end end return nil end -- Проверка, остались ли пустые клетки local function hasEmptyCells(state) for i = 1, 9 do if state[i] == "" then return true end end return false end -- Оценка позиции: +10, если выиграл X, -10, если выиграл O, 0 - если ничья или игра ещё не окончена local function evaluate(state) local winner = checkWin(state) if winner == "X" then return 10 elseif winner == "O" then return -10 end return 0 -- Нет победителя end -- Функция Минимакс local function minimax(state, depth, isMaximizing) local score = evaluate(state) -- Если уже кто-то выиграл, учитываем глубину (чем быстрее - тем лучше) if score == 10 then return score - depth elseif score == -10 then return score + depth end -- Если нет пустых клеток, ничья if not hasEmptyCells(state) then return 0 end if isMaximizing then -- Ход X, пытается максимизировать счёт local best = -99999 for i = 1, 9 do if state[i] == "" then state[i] = "X" best = math.max(best, minimax(state, depth + 1, false)) state[i] = "" end end return best else -- Ход O, пытается минимизировать счёт local best = 99999 for i = 1, 9 do if state[i] == "" then state[i] = "O" best = math.min(best, minimax(state, depth + 1, true)) state[i] = "" end end return best end end -- Функция, возвращающая список индексов клеток, которые для текущего игрока -- являются лучшими (по оценке Минимакс). local function getBestMoves(player) local isX = (player == "X") local bestScore = isX and -99999 or 99999 local bestMoves = {} for i = 1, 9 do if boardState[i] == "" then boardState[i] = player local moveScore = minimax(boardState, 0, not isX) boardState[i] = "" if isX then -- Максимизируем if moveScore > bestScore then bestScore = moveScore bestMoves = { i } elseif moveScore == bestScore then table.insert(bestMoves, i) end else -- Минимизируем if moveScore < bestScore then bestScore = moveScore bestMoves = { i } elseif moveScore == bestScore then table.insert(bestMoves, i) end end end end return bestMoves end -------------------------------------------------------------------------------- -- 2. Отрисовка (цвет) на реальном Part -------------------------------------------------------------------------------- -- Функция для установки цвета ячейки в зависимости от того, что там стоит: -- "" - пустая (белый цвет), -- "X" - крестик (например, красный), -- "O" - нолик (синий). local function updateCellColor(cellIndex) local value = boardState[cellIndex] local part = cells[cellIndex] if value == "X" then part.BrickColor = BrickColor.new("Bright red") elseif value == "O" then part.BrickColor = BrickColor.new("Bright blue") else part.BrickColor = BrickColor.new("Institutional white") end end -- Подсветка лучших ходов (для пустых клеток) local function highlightBestMoves(bestMoves) -- Сначала сбросим цвет у всех клеток в соответствии со значением (X, O или "") for i = 1, 9 do updateCellColor(i) end -- Затем, для лучших ходов (пустых), покрасим в зелёный for _, idx in ipairs(bestMoves) do if boardState[idx] == "" then cells[idx].BrickColor = BrickColor.new("Lime green") end end end -------------------------------------------------------------------------------- -- 3. Основная логика хода и сброс игры -------------------------------------------------------------------------------- -- Сброс всей игры (очистка ячеек и цвет, выставление игрока X первым) local function resetGame(winner) if winner then print("Победил: " .. winner) else print("Ничья!") end -- Очистим поле for i = 1, 9 do boardState[i] = "" updateCellColor(i) end currentPlayer = "X" highlightBestMoves(getBestMoves(currentPlayer)) end -- Обработка клика по ячейке local function onCellClicked(cellIndex, player) -- Если ячейка уже занята, выходим if boardState[cellIndex] ~= "" then return end -- Записываем значение ("X" или "O") в массив boardState boardState[cellIndex] = currentPlayer -- Обновляем цвет части updateCellColor(cellIndex) -- Проверяем, не выиграл ли кто-то local winner = checkWin(boardState) if winner then resetGame(winner) return end -- Проверяем, не ничья ли if not hasEmptyCells(boardState) then resetGame(nil) -- передадим nil, значит ничья return end -- Меняем игрока if currentPlayer == "X" then currentPlayer = "O" else currentPlayer = "X" end -- Подсвечиваем лучшие ходы для нового игрока highlightBestMoves(getBestMoves(currentPlayer)) end -------------------------------------------------------------------------------- -- 4. Подключаем обработчики клика (ClickDetector.MouseClick) -------------------------------------------------------------------------------- for i, part in ipairs(cells) do local clickDetector = part:FindFirstChild("ClickDetector") if clickDetector then clickDetector.MouseClick:Connect(function(playerWhoClicked) onCellClicked(i, playerWhoClicked) end) else warn("Нет ClickDetector в ячейке: ".. part.Name) end end -- В самом начале подсветим лучшие ходы для X highlightBestMoves(getBestMoves(currentPlayer))
boardState
хранит состояние 9 клеток (что там: "X"
, "O"
или ""
).MouseClick
) на ту или иную ячейку (Part) вызывается onCellClicked(i)
.
boardState[i]
символ текущего игрока, красим Part в соответствующий цвет.currentPlayer
(X → O или O → X) и подсвечиваем лучшие ходы нового игрока.highlightBestMoves
использует алгоритм Минимакс, чтобы найти клетки, которые выгоднее занять. Те клетки, которые ИИ считает равными «лучшими», окрашиваются в зелёный (Lime green
).resetGame
вызывается при обнаружении победителя или ничьей. Очищает все ячейки до исходного состояния.Cell1
, Cell2
, …, Cell9
. Можно, конечно, и по-другому, но тогда нужно подкорректировать код, чтобы он корректно их искал."X"
) используется Bright red
, для нолика ("O"
) — Bright blue
, а для подсветки лучшего хода — Lime green
. При желании меняйте на любые другие.highlightBestMoves(...)
. Тогда игра будет чисто PvP (два игрока).BrickColor
+ храним «X»/«O» в скрипте.На этом всё! Теперь у вас есть базовая заготовка для 3D‑крестиков-ноликов с логикой минимакса (подсветкой лучших ходов). При запуске в Roblox Studio и клике по ячейкам будет запускаться данный скрипт. Удачи в разработке!
Сделай так,чтобы он подсвечивал 1 самый лучший ход
Ниже приводится пример обновлённого кода, в котором вместо подсветки всех лучших ходов (если у минимакса несколько вариантов с равными оценками) мы выделяем только один (например, случайно выбранный из нескольких равнозначных).
Главная разница — в функции highlightBestMove(bestMoves)
:
Ниже полный код для 3D‑крестиков-ноликов в TicTacToeScript (Script внутри модели с 9 Part’ами). Если вы уже реализовали проект по предыдущей инструкции, замените в своём скрипте соответствующие части.
lua-------------------------------------------------------------------------------- -- 0. Подготовка -------------------------------------------------------------------------------- local boardModel = script.Parent -- Собираем ссылки на 9 ячеек в модели local cells = {} for i = 1, 9 do local part = boardModel:WaitForChild("Cell"..i) table.insert(cells, part) end -- Массив, хранящий текущее состояние каждой ячейки: "X", "O" или "" local boardState = {"","","","","","","","",""} -- Кто ходит сейчас ("X" или "O") local currentPlayer = "X" -------------------------------------------------------------------------------- -- 1. Логика проверки выигрыша и Минимакс -------------------------------------------------------------------------------- local function checkWin(state) local winPatterns = { {1,2,3}, {4,5,6}, {7,8,9}, -- строки {1,4,7}, {2,5,8}, {3,6,9}, -- столбцы {1,5,9}, {3,5,7}, -- диагонали } for _, combo in ipairs(winPatterns) do local a, b, c = combo[1], combo[2], combo[3] if state[a] ~= "" and state[a] == state[b] and state[b] == state[c] then return state[a] -- Вернём "X" или "O" end end return nil end local function hasEmptyCells(state) for i = 1, 9 do if state[i] == "" then return true end end return false end local function evaluate(state) local winner = checkWin(state) if winner == "X" then return 10 elseif winner == "O" then return -10 end return 0 -- Нет победителя end local function minimax(state, depth, isMaximizing) local score = evaluate(state) -- Если уже кто-то выиграл, учитываем глубину (чем быстрее - тем лучше для X) if score == 10 then return score - depth elseif score == -10 then return score + depth end -- Ничья? if not hasEmptyCells(state) then return 0 end if isMaximizing then -- Ход X (максимизирует результат) local best = -99999 for i = 1, 9 do if state[i] == "" then state[i] = "X" best = math.max(best, minimax(state, depth + 1, false)) state[i] = "" end end return best else -- Ход O (минимизирует результат) local best = 99999 for i = 1, 9 do if state[i] == "" then state[i] = "O" best = math.min(best, minimax(state, depth + 1, true)) state[i] = "" end end return best end end local function getBestMoves(player) local isX = (player == "X") local bestScore = isX and -99999 or 99999 local bestMoves = {} for i = 1, 9 do if boardState[i] == "" then boardState[i] = player local moveScore = minimax(boardState, 0, not isX) boardState[i] = "" if isX then if moveScore > bestScore then bestScore = moveScore bestMoves = { i } elseif moveScore == bestScore then table.insert(bestMoves, i) end else if moveScore < bestScore then bestScore = moveScore bestMoves = { i } elseif moveScore == bestScore then table.insert(bestMoves, i) end end end end return bestMoves end -------------------------------------------------------------------------------- -- 2. Работа с цветами деталей -------------------------------------------------------------------------------- -- Устанавливаем цвет Part в зависимости от того, что там стоит: -- "X" -> красный -- "O" -> синий -- "" -> белый (пусто) local function updateCellColor(cellIndex) local value = boardState[cellIndex] local part = cells[cellIndex] if value == "X" then part.BrickColor = BrickColor.new("Bright red") elseif value == "O" then part.BrickColor = BrickColor.new("Bright blue") else part.BrickColor = BrickColor.new("Institutional white") end end -- Подсвечиваем ровно ОДИН лучший ход (если есть несколько равных - выбираем случайно) local function highlightBestMove(bestMoves) -- Сначала сбросим цвет у всех клеток к тому, что соответствует boardState for i = 1, 9 do updateCellColor(i) end if #bestMoves == 0 then return end -- Выбираем любой один индекс из списка лучших ходов (случайным образом) local chosenIndex = bestMoves[math.random(#bestMoves)] -- Подсвечиваем его, только если он действительно пустой if boardState[chosenIndex] == "" then cells[chosenIndex].BrickColor = BrickColor.new("Lime green") end end -------------------------------------------------------------------------------- -- 3. Основная логика хода и сброс игры -------------------------------------------------------------------------------- local function resetGame(winner) if winner then print("Победил: " .. winner) else print("Ничья!") end -- Сброс состояния for i = 1, 9 do boardState[i] = "" updateCellColor(i) end currentPlayer = "X" highlightBestMove(getBestMoves(currentPlayer)) end local function onCellClicked(cellIndex, playerWhoClicked) if boardState[cellIndex] ~= "" then return end boardState[cellIndex] = currentPlayer updateCellColor(cellIndex) local winner = checkWin(boardState) if winner then resetGame(winner) return end if not hasEmptyCells(boardState) then resetGame(nil) -- ничья return end -- Меняем игрока if currentPlayer == "X" then currentPlayer = "O" else currentPlayer = "X" end -- Подсвечиваем ровно один лучший ход highlightBestMove(getBestMoves(currentPlayer)) end -------------------------------------------------------------------------------- -- 4. Подключаем обработчики клика (ClickDetector) -------------------------------------------------------------------------------- for i, part in ipairs(cells) do local clickDetector = part:FindFirstChild("ClickDetector") if clickDetector then clickDetector.MouseClick:Connect(function(player) onCellClicked(i, player) end) else warn("Нет ClickDetector в ячейке: " .. part.Name) end end -- Сразу при запуске подсветим один лучший ход для X highlightBestMove(getBestMoves(currentPlayer))
Обратите внимание на функцию:
lualocal function highlightBestMove(bestMoves) -- ... local chosenIndex = bestMoves[math.random(#bestMoves)] cells[chosenIndex].BrickColor = BrickColor.new("Lime green") end
bestMoves
(если их несколько, выбираем случайно).Если вы хотите каждый раз подсвечивать первую клетку (а не случайную), замените строчку:
lualocal chosenIndex = bestMoves[math.random(#bestMoves)]
на
lualocal chosenIndex = bestMoves[1]
Тогда будет всегда подсвечиваться первая из лучших, а не случайная.
Теперь при каждом ходе игрока будет находиться едиственный лучший ход для следующего игрока и подсвечиваться клетка только одна (либо случайно из множества равных, либо самая первая — решать вам). Всё остальное (логика минимакса, проверка победы и т.п.) остаётся без изменений.
Пусть ии проанализирует все выигрышные матчи до начала игры и будет советовать самые лучшие ходы, которые практически невозможно обыграть
Ниже приведён пример скрипта, в котором мы заранее (ещё до начала партии) полностью просчитываем все возможные развития игры в «Крестики‑нолики» (полное построение дерева игры). Благодаря такому «предварительному анализу» (precomputation) игра будет иметь оптимальную стратегию и станет «непроходимой» для человека, ведь классические крестики-нолики при безошибочной игре заканчиваются вничью.
Главная идея:
"X_O__O__X"
).solutions
) лучшую оценку и лучшие ходы для каждой конфигурации.minimax
.Так мы фактически «анализируем все выигрышные матчи до начала игры», ведь всё дерево перебирается один раз при инициализации.
Ниже приведён вариант кода для 3D‑крестиков‑ноликов, аналогичный предыдущим примерам, но с учётом полного предвычисления.
TicTacToeBoard
в Workspace.Cell1
, Cell2
, …, Cell9
.
ClickDetector
, чтобы игрок мог кликать мышью.Script
в модель TicTacToeBoard
и вставляем туда код ниже.lua-------------------------------------------------------------------------------- -- 0. Подготовка -------------------------------------------------------------------------------- -- Ссылка на модель с 9 ячейками local boardModel = script.Parent -- Соберём ссылки на ячейки (Part) в массив `cells` local cells = {} for i = 1, 9 do local part = boardModel:WaitForChild("Cell"..i) table.insert(cells, part) end -- Массив, где мы храним текущее состояние доски: -- "X", "O" или "" (пусто) для i-й ячейки. local boardState = {"","","","","","","","",""} -- Текущий игрок ("X" или "O") local currentPlayer = "X" -- Словарь (таблица) для уже рассчитанных конфигураций доски: -- ключ: строка, описывающая состояние + чей ход -- значение: объект вида { -- score: (число) оптимальная оценка для X (10, -10, 0 и т.п.), -- bestMoves: (таблица) список индексов лучших ходов -- } local solutions = {} -------------------------------------------------------------------------------- -- 1. Сериализация и десериализация -------------------------------------------------------------------------------- -- Превращаем массив boardState + info о текущем игроке в строку, -- чтобы uniquely идентифицировать положение. local function boardToString(state, player) -- Пример: "X,O,,,O,,X," + "_X" (кто ходит). -- Или же более простая форма без запятых. -- Лишь бы был стабильный формат. return table.concat(state, "") .. "_" .. player end -- (Опционально) можно сделать и обратный процесс, но нам важнее -- именно ключ для словаря. Обратно мы обычно не десериализуем, -- т.к. в реальном времени у нас уже есть `boardState`. -------------------------------------------------------------------------------- -- 2. Проверка выигрыша и вспомогательные функции -------------------------------------------------------------------------------- local function checkWin(state) local winPatterns = { {1,2,3}, {4,5,6}, {7,8,9}, -- строки {1,4,7}, {2,5,8}, {3,6,9}, -- столбцы {1,5,9}, {3,5,7}, -- диагонали } for _, combo in ipairs(winPatterns) do local a,b,c = combo[1], combo[2], combo[3] if state[a] ~= "" and state[a] == state[b] and state[b] == state[c] then return state[a] -- "X" или "O" end end return nil end local function hasEmptyCells(state) for i = 1, 9 do if state[i] == "" then return true end end return false end -------------------------------------------------------------------------------- -- 3. Рекурсивный перебор всех состояний (аналог Минимакса, но с кешированием) -------------------------------------------------------------------------------- -- Возвращает объект {score=?, bestMoves=?} для текущей конфигурации (state, player). -- score > 0 выгодно X, -- score < 0 выгодно O, -- score = 0 ничья или неопределённо в равных вариантах. -- -- bestMoves — список ходов (индексов клеток), которые ведут к лучшему результату -- для игрока (player). -- -- Мы используем глобальный `solutions` для хранения уже рассчитанных состояний. local function computeBestMoves(state, player) -- 1) Пытаемся найти готовый ответ в solutions local key = boardToString(state, player) if solutions[key] then return solutions[key] end -- 2) Проверяем, есть ли победитель local winner = checkWin(state) if winner then if winner == "X" then -- Возвращаем большую +10 solutions[key] = {score=10, bestMoves={}} return solutions[key] else -- O выиграл -> -10 solutions[key] = {score=-10, bestMoves={}} return solutions[key] end end -- 3) Проверка ничьей if not hasEmptyCells(state) then -- Ничья -> 0 solutions[key] = {score=0, bestMoves={}} return solutions[key] end -- 4) Если игра не окончена, перебираем все возможные ходы local isX = (player == "X") local bestScore = isX and -99999 or 99999 local bestMoves = {} for i = 1, 9 do if state[i] == "" then -- Делает ход state[i] = player local nextPlayer = (player == "X") and "O" or "X" local result = computeBestMoves(state, nextPlayer) local moveScore = result.score -- Возвращаем ход обратно state[i] = "" if isX then -- Максимизируем score if moveScore > bestScore then bestScore = moveScore bestMoves = {i} elseif moveScore == bestScore then table.insert(bestMoves, i) end else -- Минимизируем score if moveScore < bestScore then bestScore = moveScore bestMoves = {i} elseif moveScore == bestScore then table.insert(bestMoves, i) end end end end -- Запомним результат в словаре solutions[key] = { score = bestScore, bestMoves = bestMoves } return solutions[key] end -- Упрощённая обёртка: возвращает массив лучших ходов local function getBestMoves(state, player) local result = computeBestMoves(state, player) return result.bestMoves end -------------------------------------------------------------------------------- -- 4. Работа с цветами деталей -------------------------------------------------------------------------------- local function updateCellColor(cellIndex) local value = boardState[cellIndex] local part = cells[cellIndex] if value == "X" then part.BrickColor = BrickColor.new("Bright red") elseif value == "O" then part.BrickColor = BrickColor.new("Bright blue") else part.BrickColor = BrickColor.new("Institutional white") -- пустая ячейка end end -- Подсвечиваем ОДИН лучший ход (можно сделать, чтобы подсвечивалось несколько). -- Если у лучших ходов одинаковая оценка, выберем один случайно. local function highlightOneBestMove(bestMoves) -- Сбрасываем все ячейки по их реальному состоянию X/O/пусто for i = 1, 9 do updateCellColor(i) end if #bestMoves == 0 then return end local chosen = bestMoves[math.random(#bestMoves)] if boardState[chosen] == "" then cells[chosen].BrickColor = BrickColor.new("Lime green") end end -------------------------------------------------------------------------------- -- 5. Основная логика хода и сброс игры -------------------------------------------------------------------------------- local function resetGame(winner) if winner then print("Победил:", winner) else print("Ничья!") end for i = 1, 9 do boardState[i] = "" updateCellColor(i) end currentPlayer = "X" highlightOneBestMove(getBestMoves(boardState, currentPlayer)) end local function onCellClicked(cellIndex) if boardState[cellIndex] ~= "" then return end boardState[cellIndex] = currentPlayer updateCellColor(cellIndex) local w = checkWin(boardState) if w then resetGame(w) return end if not hasEmptyCells(boardState) then resetGame(nil) return end -- Меняем игрока currentPlayer = (currentPlayer == "X") and "O" or "X" -- Подсвечиваем лучший ход highlightOneBestMove(getBestMoves(boardState, currentPlayer)) end -------------------------------------------------------------------------------- -- 6. Инициализация: запускаем полный перебор ещё до начала -------------------------------------------------------------------------------- -- Построим всё дерево для стартового пустого поля, передав "X" как первого игрока. -- Это заполнит словарь solutions всеми состояниями (их <= 3^9 = 19683, что не страшно). computeBestMoves(boardState, currentPlayer) print("Готово! Все возможные партии просчитаны:", #solutions, "записей.") -- Вешаем обработчики клика для каждой из 9 ячеек for i, part in ipairs(cells) do local cd = part:FindFirstChild("ClickDetector") if cd then cd.MouseClick:Connect(function(whoClicked) onCellClicked(i) end) else warn("Нет ClickDetector в:", part.Name) end end -- В самом начале подсветим лучший ход для X highlightOneBestMove(getBestMoves(boardState, currentPlayer))
computeBestMoves
— рекурсивная функция, которая перебирает все возможные расстановки, начиная с текущей.
solutions
(кеш).score
, bestMoves
), не вычисляя заново.computeBestMoves(boardState, "X")
вызывается один раз в конце скрипта.
solutions
заполнится значениями для всех возможных состояний, которые могут появиться в партии (включая и те, где уже ходит O, и X, и т.д.).getBestMoves(boardState, currentPlayer)
, получаем из solutions
готовый результат и подсвечиваем 1 лучший ход (или можно было бы показать все).Output
число «записей» — это сколько состояний (конфигураций + чей ход) реально встретилось при полном переборе (обычно несколько тысяч при учёте всех промежуточных вариантов).В итоге ИИ «знает» всё о крестиках-ноликах, так как игра слишком маленькая, чтобы это занимало ощутимое время или память. И если человек не допустит ошибок, итогом будет ничья, но обыграть «машину» с такой оптимальной стратегией не получится.
solutions
.Таким образом, получаем оптимальную (и очень быструю) реализацию, в которой ИИ «прокачан» за счёт полного перебора всех исходов заранее.
как добавить хранилище для этих записей?
Чтобы добавить хранилище для записей, таких как решения игры, можно использовать несколько подходов в Roblox. В данном случае, для хранения данных между сессиями игры (например, чтобы сохранённые вычисления не терялись при перезапуске игры), вы можете использовать DataStore. Этот сервис позволяет сохранять данные на сервере Roblox и загружать их при следующем запуске игры.
Мы будем использовать DataStore для сохранения и загрузки наших записей в словаре solutions
, чтобы они сохранялись даже после перезапуска игры.
TicTacToeSolutions
).Откройте сервис DataStore:
Создайте новый скрипт в ServerScriptService
:
lualocal DataStoreService = game:GetService("DataStoreService") local ticTacToeDataStore = DataStoreService:GetDataStore("TicTacToeSolutions") -------------------------------------------------------------------------------- -- 0. Подготовка -------------------------------------------------------------------------------- local boardModel = script.Parent local cells = {} for i = 1, 9 do local part = boardModel:WaitForChild("Cell"..i) table.insert(cells, part) end local boardState = {"","","","","","","","",""} local currentPlayer = "X" local solutions = {} -------------------------------------------------------------------------------- -- 1. Сериализация и десериализация -------------------------------------------------------------------------------- local function boardToString(state, player) return table.concat(state, "") .. "_" .. player end local function loadSolutions() -- Загружаем решения из DataStore local success, savedSolutions = pcall(function() return ticTacToeDataStore:GetAsync("solutions") end) if success and savedSolutions then solutions = savedSolutions else print("Не удалось загрузить решения из хранилища или оно пусто.") end end local function saveSolutions() -- Сохраняем решения в DataStore local success, errorMessage = pcall(function() ticTacToeDataStore:SetAsync("solutions", solutions) end) if success then print("Решения сохранены в DataStore.") else warn("Ошибка при сохранении решений: " .. errorMessage) end end -------------------------------------------------------------------------------- -- 2. Проверка выигрыша и вспомогательные функции -------------------------------------------------------------------------------- local function checkWin(state) local winPatterns = { {1,2,3}, {4,5,6}, {7,8,9}, {1,4,7}, {2,5,8}, {3,6,9}, {1,5,9}, {3,5,7} } for _, combo in ipairs(winPatterns) do local a,b,c = combo[1], combo[2], combo[3] if state[a] ~= "" and state[a] == state[b] and state[b] == state[c] then return state[a] end end return nil end local function hasEmptyCells(state) for i = 1, 9 do if state[i] == "" then return true end end return false end -------------------------------------------------------------------------------- -- 3. Рекурсивный перебор всех состояний (аналог Минимакса, но с кешированием) -------------------------------------------------------------------------------- local function computeBestMoves(state, player) local key = boardToString(state, player) if solutions[key] then return solutions[key] end local winner = checkWin(state) if winner then if winner == "X" then solutions[key] = {score=10, bestMoves={}} return solutions[key] else solutions[key] = {score=-10, bestMoves={}} return solutions[key] end end if not hasEmptyCells(state) then solutions[key] = {score=0, bestMoves={}} return solutions[key] end local isX = (player == "X") local bestScore = isX and -99999 or 99999 local bestMoves = {} for i = 1, 9 do if state[i] == "" then state[i] = player local nextPlayer = (player == "X") and "O" or "X" local result = computeBestMoves(state, nextPlayer) local moveScore = result.score state[i] = "" if isX then if moveScore > bestScore then bestScore = moveScore bestMoves = {i} elseif moveScore == bestScore then table.insert(bestMoves, i) end else if moveScore < bestScore then bestScore = moveScore bestMoves = {i} elseif moveScore == bestScore then table.insert(bestMoves, i) end end end end solutions[key] = {score=bestScore, bestMoves=bestMoves} return solutions[key] end local function getBestMoves(state, player) local result = computeBestMoves(state, player) return result.bestMoves end -------------------------------------------------------------------------------- -- 4. Работа с цветами деталей -------------------------------------------------------------------------------- local function updateCellColor(cellIndex) local value = boardState[cellIndex] local part = cells[cellIndex] if value == "X" then part.BrickColor = BrickColor.new("Bright red") elseif value == "O" then part.BrickColor = BrickColor.new("Bright blue") else part.BrickColor = BrickColor.new("Institutional white") end end local function highlightOneBestMove(bestMoves) for i = 1, 9 do updateCellColor(i) end if #bestMoves == 0 then return end local chosen = bestMoves[math.random(#bestMoves)] if boardState[chosen] == "" then cells[chosen].BrickColor = BrickColor.new("Lime green") end end -------------------------------------------------------------------------------- -- 5. Основная логика хода и сброс игры -------------------------------------------------------------------------------- local function resetGame(winner) if winner then print("Победил:", winner) else print("Ничья!") end for i = 1, 9 do boardState[i] = "" updateCellColor(i) end currentPlayer = "X" highlightOneBestMove(getBestMoves(boardState, currentPlayer)) end local function onCellClicked(cellIndex) if boardState[cellIndex] ~= "" then return end boardState[cellIndex] = currentPlayer updateCellColor(cellIndex) local w = checkWin(boardState) if w then resetGame(w) saveSolutions() -- сохраняем решения после завершения игры return end if not hasEmptyCells(boardState) then resetGame(nil) saveSolutions() -- сохраняем решения после завершения игры return end currentPlayer = (currentPlayer == "X") and "O" or "X" highlightOneBestMove(getBestMoves(boardState, currentPlayer)) end -------------------------------------------------------------------------------- -- 6. Инициализация: загрузка решений и запуск -------------------------------------------------------------------------------- loadSolutions() -- Загружаем решения из DataStore -- Построим всё дерево для стартового пустого поля, передав "X" как первого игрока. computeBestMoves(boardState, currentPlayer) print("Готово! Все возможные партии просчитаны:", #solutions, "записей.") for i, part in ipairs(cells) do local cd = part:FindFirstChild("ClickDetector") if cd then cd.MouseClick:Connect(function() onCellClicked(i) end) else warn("Нет ClickDetector в:", part.Name) end end highlightOneBestMove(getBestMoves(boardState, currentPlayer))
Загрузка решений (loadSolutions
):
"solutions"
.Сохранение решений (saveSolutions
):
solutions
в DataStore, чтобы оно не потерялось после перезапуска.DataStore:
DataStoreService
для хранения и получения данных, что позволяет сохранять решения между сессиями игры.SetAsync
слишком часто (например, в каждом ходе), можно столкнуться с ограничениями. Чтобы избежать этого, сохраняйте данные после завершения игры или по другим условиям.pcall
для обработки ошибок при сохранении и загрузке данных. Если возникает ошибка, она будет выведена в консоль Roblox Studio, и игра продолжит работать, как обычно.Теперь ваши решения будут сохраняться между сессиями, и даже если игрок выйдет из игры или перезагрузит её, данные о возможных ходах и решениях сохранятся.
Как добавить solutions
Ниже кратко опишу, что такое solutions
и как именно его «добавить» (или использовать) в коде на практике. В более ранних примерах у нас была большая таблица solutions
, где мы храним все просмотренные конфигурации доски (состояния) и их результаты (лучшая оценка и лучшие ходы).
solutions
solutions
— это словарь (или ассоциативный массив) вида:
lualocal solutions = {}
"X__O_____X"
, где _
— пустые клетки, а в конце "_O"
может указывать, что ходит O. В коде мы обычно составляем её с помощью boardToString(state, currentPlayer)
.solutions[key]
— это таблица, где мы храним:
score
— числовая оценка (для X) этой конфигурации (например, 10, 0, -10).bestMoves
— список (массив) индексов клеток (1..9), которые при оптимальной игре дадут наилучший результат для игрока, чей ход сейчас.Пример:
luasolutions["X__O_____X_O"] = { score = 10, bestMoves = {3,7} }
Это означает: в конфигурации, описываемой строкой "X__O_____X_O"
, наилучшая оценка +10 (выигрыш для X), и индексы лучших ходов — 3 и 7.
solutions
В скрипте мы просто объявляем solutions
как глобальную (или локальную в основном модуле) таблицу:
lualocal solutions = {}
boardState
, currentPlayer
и т.п.Мы пишем две основные функции:
boardToString(state, player)
— превращает текущее состояние (state
массива из 9 клеток) и инфо о том, кто ходит (player
= "X" или "O") в строку (ключ).computeBestMoves(state, player)
— в этой функции мы проверяем:
solutions[key]
? Если есть, то сразу возвращаем, не пересчитывая заново.score
и лучшие ходы bestMoves
и записываем в solutions[key]
.Пример кода (упрощённый фрагмент):
lua-- Глобальная таблица local solutions = {} local function boardToString(state, player) return table.concat(state, "") .. "_" .. player end local function computeBestMoves(state, player) local key = boardToString(state, player) if solutions[key] then -- Если уже есть готовое решение, вернём его return solutions[key] end -- ... Тут идёт логика проверки победителя, заполненности поля и т.п. ... -- Когда мы вычислим bestScore и bestMoves: solutions[key] = {score=bestScore, bestMoves=bestMoves} return solutions[key] end
Таким образом, при первом вызове computeBestMoves(state, player)
для какой-то конфигурации мы заполняем solutions[key]
. При повторном вызове для той же конфигурации — моментально получаем результат.
solutions
в существующий кодЕсли в вашем скрипте пока нет такого словаря:
lualocal solutions = {}
computeBestMoves
) вы используете этот solutions
для кеширования результатов.solutions
и в другие функции, если вы хотите вести учёт вне computeBestMoves
. Но обычно достаточно держать его в одной области видимости скрипта.Пример итоговой структуры (минимальный шаблон):
lualocal solutions = {} -- 1) объявили глобальный словарь -- Превращает состояние в строку (ключ) local function boardToString(state, player) return table.concat(state, "") .. "_" .. player end local function checkWin(state) -- ... проверка ... end local function hasEmptyCells(state) -- ... проверка ... end local function computeBestMoves(state, player) local key = boardToString(state, player) if solutions[key] then return solutions[key] -- Если есть, просто вернуть end -- 1. Проверяем выигрыш, ничью ... -- 2. Если есть победитель -> score=10 (X) / -10 (O) / 0 -- 3. Иначе перебираем все пустые клетки, считаем rec для следующего игрока -- Пример: solutions[key] = { score = bestScore, bestMoves = bestMoves } return solutions[key] end
lualocal result = computeBestMoves(boardState, currentPlayer) local bestMoves = result.bestMoves
bestMoves
используете для подсветки или выбора хода.solutions
в DataStoreЧтобы решения не «пропадали» при перезапуске игры, можно хранить их между сессиями с помощью DataStore (как в предыдущем сообщении). Для этого понадобится:
solutions
из DataStore при запуске (loadSolutions
).saveSolutions
), или по любому другому триггеру.Таким образом, вы получите полноценное хранилище. Но если вас устраивает «вычислять заново» при каждом запуске (ведь крестики-нолики — маленькая игра), можно DataStore не использовать, а просто пересчитывать с нуля.
solutions
— это таблица (dictionary), в которой ключом является строка (состояние + игрок), а значением — результат анализа (оценка и лучшие ходы).solutions
значит просто завести у себя в скрипте переменную local solutions = {}
, а в функции перебора computeBestMoves
записывать в неё результаты.solutions
.