Chess: Move bot code to its own file

This commit is contained in:
Wuzzy 2023-07-17 15:17:52 +02:00
parent fbd55700e0
commit 6a19130042
2 changed files with 232 additions and 218 deletions

View File

@ -1,25 +1,17 @@
local realchess = {} -- Init stuff
if minetest.get_modpath("realchess") ~= nil then
-- If the 'realchess' mod was found, don't use any of this mod's Chess code
minetest.log("action", "[xdecor] 'realchess' mod detected. Disabling X-Decor-libre's Chess module in favor of realchess")
return
end
realchess = {}
local chessbot = dofile(minetest.get_modpath(minetest.get_current_modname()).."/src/chessbot.lua")
screwdriver = screwdriver or {}
-- Translation init
local S = minetest.get_translator("xdecor") local S = minetest.get_translator("xdecor")
local NS = function(s) return s end local NS = function(s) return s end
local FS = function(...) return minetest.formspec_escape(S(...)) end local FS = function(...) return minetest.formspec_escape(S(...)) end
local ALPHA_OPAQUE = minetest.features.use_texture_alpha_string_modes and "opaque" or false
-- Bot names
local BOT_NAME = NS("Weak Computer")
-- Bot names in Bot vs Bot mode
local BOT_NAME_1 = NS("Weak Computer 1")
local BOT_NAME_2 = NS("Weak Computer 2")
-- Delay in seconds for a bot moving a piece (excluding choosing a promotion)
local BOT_DELAY_MOVE = 1.0
-- Delay in seconds for a bot promoting a piece
local BOT_DELAY_PROMOTE = 1.0
-- Timeout in seconds to allow resetting the game or digging the chessboard.
-- If no move was made for this time, everyone can reset the game
-- and remove the chessboard.
local TIMEOUT = 300
screwdriver = screwdriver or {}
-- Chess games are disabled because they are currently too broken. -- Chess games are disabled because they are currently too broken.
-- Set this to true to enable this again and try your luck. -- Set this to true to enable this again and try your luck.
@ -29,6 +21,19 @@ local ENABLE_CHESS_GAMES = true
-- and enables a "Bot vs Bot" gamemode for testing the bot -- and enables a "Bot vs Bot" gamemode for testing the bot
local CHESS_DEBUG = false local CHESS_DEBUG = false
-- Bot names
local BOT_NAME = NS("Weak Computer")
-- Bot names in Bot vs Bot mode
local BOT_NAME_1 = NS("Weak Computer 1")
local BOT_NAME_2 = NS("Weak Computer 2")
-- Timeout in seconds to allow resetting the game or digging the chessboard.
-- If no move was made for this time, everyone can reset the game
-- and remove the chessboard.
local TIMEOUT = 300
local ALPHA_OPAQUE = minetest.features.use_texture_alpha_string_modes and "opaque" or false
-- Returns the player name for the given player color. -- Returns the player name for the given player color.
-- In case of a bot player, will return a translated -- In case of a bot player, will return a translated
-- bot name. -- bot name.
@ -141,7 +146,7 @@ local function send_message_2(playerName1, playerName2, message, botColor, isDeb
end end
local notation_letters = {'a','b','c','d','e','f','g','h'} local notation_letters = {'a','b','c','d','e','f','g','h'}
local function index_to_notation(idx) function realchess.index_to_notation(idx)
local x, y = index_to_xy(idx) local x, y = index_to_xy(idx)
if not x or not y then if not x or not y then
return "??" return "??"
@ -151,7 +156,7 @@ local function index_to_notation(idx)
return xstr .. ystr return xstr .. ystr
end end
local function board_to_table(inv) function realchess.board_to_table(inv)
local t = {} local t = {}
for i = 1, 64 do for i = 1, 64 do
t[#t + 1] = inv:get_stack("board", i):get_name() t[#t + 1] = inv:get_stack("board", i):get_name()
@ -180,7 +185,7 @@ local rookThreats = {false, true, false, true, true, false, true, false}
local queenThreats = {true, true, true, true, true, true, true, true} local queenThreats = {true, true, true, true, true, true, true, true}
local kingThreats = {true, true, true, true, true, true, true, true} local kingThreats = {true, true, true, true, true, true, true, true}
local function attacked(color, idx, board) function realchess.attacked(color, idx, board)
local threatDetected = false local threatDetected = false
local kill = color == "white" local kill = color == "white"
local pawnThreats = {kill, false, kill, false, false, not kill, false, not kill} local pawnThreats = {kill, false, kill, false, false, not kill, false, not kill}
@ -302,7 +307,7 @@ local function en_passant_to_string(double_step)
else else
dsy = dsy + 1 dsy = dsy + 1
end end
s_en_passant = index_to_notation(xy_to_index(dsx, dsy)) s_en_passant = realchess.index_to_notation(xy_to_index(dsx, dsy))
end end
return s_en_passant return s_en_passant
end end
@ -354,7 +359,7 @@ local function can_castle(meta, board, from_list, from_idx, to_idx)
-- Check if square of king as well the squares that king must cross and reach -- Check if square of king as well the squares that king must cross and reach
-- are NOT attacked -- are NOT attacked
for i = from_idx, from_idx + 2 * pc.acheck_dir, pc.acheck_dir do for i = from_idx, from_idx + 2 * pc.acheck_dir, pc.acheck_dir do
if attacked(kingColor, i, board) then if realchess.attacked(kingColor, i, board) then
return false return false
end end
end end
@ -767,10 +772,10 @@ local function get_theoretical_moves_from(meta, board, from_idx)
-- King can't move to any attacked square -- King can't move to any attacked square
-- king_board simulates the board with the king moved already. -- king_board simulates the board with the king moved already.
-- Required for the attacked() check to work -- Required for the attacked() check to work
local king_board = board_to_table(inv) local king_board = realchess.board_to_table(inv)
king_board[to_idx] = king_board[from_idx] king_board[to_idx] = king_board[from_idx]
king_board[from_idx] = "" king_board[from_idx] = ""
if attacked(color, to_idx, king_board) then if realchess.attacked(color, to_idx, king_board) then
moves[to_idx] = nil moves[to_idx] = nil
else else
local dx = from_x - to_x local dx = from_x - to_x
@ -798,6 +803,7 @@ local function get_theoretical_moves_from(meta, board, from_idx)
return {} return {}
end end
-- Rate the possible moves depending on its piece value
for i in pairs(moves) do for i in pairs(moves) do
local stack_name = board[tonumber(i)] local stack_name = board[tonumber(i)]
if stack_name ~= "" then if stack_name ~= "" then
@ -825,7 +831,7 @@ end
-- origin_index is the board index for the square to start the piece from (as string) -- origin_index is the board index for the square to start the piece from (as string)
-- and this is the key for a list of destination indixes. -- and this is the key for a list of destination indixes.
-- r1, r2, r3 ... are numeric values (normally 0) to "rate" this square for the bot. -- r1, r2, r3 ... are numeric values (normally 0) to "rate" this square for the bot.
local function get_theoretical_moves_for(meta, board, player) function realchess.get_theoretical_moves_for(meta, board, player)
local moves = {} local moves = {}
for i = 1, 64 do for i = 1, 64 do
local possibleMoves = get_theoretical_moves_from(meta, board, i) local possibleMoves = get_theoretical_moves_from(meta, board, i)
@ -839,36 +845,7 @@ local function get_theoretical_moves_for(meta, board, player)
return moves return moves
end end
local function best_move(moves) function realchess.locate_kings(board)
local value, choices = 0, {}
for from, _ in pairs(moves) do
for to, val in pairs(_) do
if val > value then
value = val
choices = {{
from = from,
to = to
}}
elseif val == value then
choices[#choices + 1] = {
from = from,
to = to
}
end
end
end
if #choices == 0 then
return nil
end
local random = math.random(1, #choices)
local choice_from, choice_to = choices[random].from, choices[random].to
return tonumber(choice_from), choice_to
end
local function locate_kings(board)
local Bidx, Widx local Bidx, Widx
for i = 1, 64 do for i = 1, 64 do
local piece, color = board[i]:match(":(%w+)_(%w+)") local piece, color = board[i]:match(":(%w+)_(%w+)")
@ -888,10 +865,10 @@ end
-- returns true if the player still has at least one move left, -- returns true if the player still has at least one move left,
-- return false otherwise. -- return false otherwise.
-- 2nd return value is table of save moves -- 2nd return value is table of save moves
-- * theoretical_moves: moves table returned by get_theoretical_moves_for() -- * theoretical_moves: moves table returned by realchess.get_theoretical_moves_for()
-- * board: board table -- * board: board table
-- * player: player color ("white" or "black") -- * player: player color ("white" or "black")
local function has_king_safe_move(theoretical_moves, board, player) function realchess.has_king_safe_move(theoretical_moves, board, player)
local safe_moves = {} local safe_moves = {}
-- create a virtual board -- create a virtual board
local v_board = table.copy(board) local v_board = table.copy(board)
@ -907,7 +884,7 @@ local function has_king_safe_move(theoretical_moves, board, player)
-- move the piece on the virtual board -- move the piece on the virtual board
v_board[to_idx] = v_board[from_idx] v_board[to_idx] = v_board[from_idx]
v_board[from_idx] = "" v_board[from_idx] = ""
local black_king_idx, white_king_idx = locate_kings(v_board) local black_king_idx, white_king_idx = realchess.locate_kings(v_board)
if not black_king_idx or not white_king_idx then if not black_king_idx or not white_king_idx then
minetest.log("error", "[xdecor] Chess: Insufficient kings on chessboard!") minetest.log("error", "[xdecor] Chess: Insufficient kings on chessboard!")
return false return false
@ -918,7 +895,7 @@ local function has_king_safe_move(theoretical_moves, board, player)
else else
king_idx = white_king_idx king_idx = white_king_idx
end end
local playerAttacked = attacked(player, king_idx, v_board) local playerAttacked = realchess.attacked(player, king_idx, v_board)
if not playerAttacked then if not playerAttacked then
safe_moves[from_idx] = safe_moves[from_idx] or {} safe_moves[from_idx] = safe_moves[from_idx] or {}
safe_moves[from_idx][to_idx] = value safe_moves[from_idx][to_idx] = value
@ -1421,8 +1398,8 @@ local function get_moves_formstring(meta)
end end
local pieceTo_si_id = pieceTo ~= "" and get_figurine_id(pieceTo) local pieceTo_si_id = pieceTo ~= "" and get_figurine_id(pieceTo)
local coordFrom = index_to_notation(from_idx) local coordFrom = realchess.index_to_notation(from_idx)
local coordTo = index_to_notation(to_idx) local coordTo = realchess.index_to_notation(to_idx)
if curPlayerIsWhite then if curPlayerIsWhite then
move_no = move_no + 1 move_no = move_no + 1
@ -1492,7 +1469,7 @@ local verify_eaten_list
if CHESS_DEBUG then if CHESS_DEBUG then
verify_eaten_list = function(meta) verify_eaten_list = function(meta)
local inv = meta:get_inventory() local inv = meta:get_inventory()
local board = board_to_table(inv) local board = realchess.board_to_table(inv)
local whitePiecesLeft = 0 local whitePiecesLeft = 0
local whitePiecesEaten = 0 local whitePiecesEaten = 0
local blackPiecesLeft = 0 local blackPiecesLeft = 0
@ -1744,7 +1721,7 @@ end
local function update_game_result(meta) local function update_game_result(meta)
local inv = meta:get_inventory() local inv = meta:get_inventory()
local board_t = board_to_table(inv) local board_t = realchess.board_to_table(inv)
local playerWhite = meta:get_string("playerWhite") local playerWhite = meta:get_string("playerWhite")
local playerBlack = meta:get_string("playerBlack") local playerBlack = meta:get_string("playerBlack")
@ -1753,8 +1730,8 @@ local function update_game_result(meta)
local blackCanMove = false local blackCanMove = false
local whiteCanMove = false local whiteCanMove = false
local blackMoves = get_theoretical_moves_for(meta, board_t, "black") local blackMoves = realchess.get_theoretical_moves_for(meta, board_t, "black")
local whiteMoves = get_theoretical_moves_for(meta, board_t, "white") local whiteMoves = realchess.get_theoretical_moves_for(meta, board_t, "white")
if next(blackMoves) then if next(blackMoves) then
blackCanMove = true blackCanMove = true
end end
@ -1765,7 +1742,7 @@ local function update_game_result(meta)
-- assume lastMove was updated *after* the player moved -- assume lastMove was updated *after* the player moved
local lastMove = meta:get_string("lastMove") local lastMove = meta:get_string("lastMove")
local black_king_idx, white_king_idx = locate_kings(board_t) local black_king_idx, white_king_idx = realchess.locate_kings(board_t)
if not black_king_idx or not white_king_idx then if not black_king_idx or not white_king_idx then
minetest.log("error", "[xdecor] Chess: Insufficient kings on chessboard!") minetest.log("error", "[xdecor] Chess: Insufficient kings on chessboard!")
return return
@ -1784,10 +1761,10 @@ local function update_game_result(meta)
-- King attacked? This reduces the list of available moves, -- King attacked? This reduces the list of available moves,
-- so remove these, too and check if there are still any left. -- so remove these, too and check if there are still any left.
local isKingAttacked = attacked(checkPlayer, king_idx, board_t) local isKingAttacked = realchess.attacked(checkPlayer, king_idx, board_t)
if isKingAttacked then if isKingAttacked then
meta:set_string(checkPlayer.."Attacked", "true") meta:set_string(checkPlayer.."Attacked", "true")
local is_safe = has_king_safe_move(checkMoves, board_t, checkPlayer) local is_safe = realchess.has_king_safe_move(checkMoves, board_t, checkPlayer)
-- If not safe moves left, player can't move -- If not safe moves left, player can't move
if not is_safe then if not is_safe then
if checkPlayer == "black" then if checkPlayer == "black" then
@ -2451,7 +2428,7 @@ function realchess.move(meta, from_list, from_index, to_list, to_index, playerNa
local dy = from_y - to_y local dy = from_y - to_y
local check = true local check = true
local inv = meta:get_inventory() local inv = meta:get_inventory()
local board = board_to_table(inv) local board = realchess.board_to_table(inv)
-- Castling -- Castling
local cc, rook_start, rook_goal, rook_name = can_castle(meta, board, from_list, from_index, to_index) local cc, rook_start, rook_goal, rook_name = can_castle(meta, board, from_list, from_index, to_index)
@ -2479,17 +2456,17 @@ function realchess.move(meta, from_list, from_index, to_list, to_index, playerNa
end end
local board = board_to_table(inv) local board = realchess.board_to_table(inv)
board[to_index] = board[from_index] board[to_index] = board[from_index]
board[from_index] = "" board[from_index] = ""
local black_king_idx, white_king_idx = locate_kings(board) local black_king_idx, white_king_idx = realchess.locate_kings(board)
if not black_king_idx or not white_king_idx then if not black_king_idx or not white_king_idx then
minetest.log("error", "[xdecor] Chess: Insufficient kings on chessboard!") minetest.log("error", "[xdecor] Chess: Insufficient kings on chessboard!")
return false return false
end end
local blackAttacked = attacked("black", black_king_idx, board) local blackAttacked = realchess.attacked("black", black_king_idx, board)
local whiteAttacked = attacked("white", white_king_idx, board) local whiteAttacked = realchess.attacked("white", white_king_idx, board)
-- Refuse to move if it would put or leave the own king -- Refuse to move if it would put or leave the own king
-- under attack -- under attack
@ -2554,139 +2531,20 @@ function realchess.move(meta, from_list, from_index, to_list, to_index, playerNa
return true return true
end end
local function bot_move(inv, meta) -- Causes the player ("white" or "blue") to resign
local board_t = board_to_table(inv) function realchess.resign(meta, playerColor)
local lastMove = meta:get_string("lastMove") if playerColor == "black" then
local gameResult = meta:get_string("gameResult")
local botColor = meta:get_string("botColor")
if botColor == "" then
return
end
local currentBotColor, opponentColor
local botName
if botColor == "black" then
currentBotColor = "black"
opponentColor = "white"
elseif botColor == "white" then
currentBotColor = "white"
opponentColor = "black"
elseif botColor == "both" then
opponentColor = lastMove
if lastMove == "black" or lastMove == "" then
currentBotColor = "white"
else
currentBotColor = "black"
end
end
if currentBotColor == "white" then
botName = meta:get_string("playerWhite")
else
botName = meta:get_string("playerBlack")
end
if (lastMove == opponentColor or ((botColor == "white" or botColor == "both") and lastMove == "")) and gameResult == "" then
update_formspec(meta)
local moves = get_theoretical_moves_for(meta, board_t, currentBotColor)
local choice_from, choice_to = best_move(moves)
if choice_from == nil then
-- No best move: stalemate or checkmate
return
end
local pieceFrom = inv:get_stack("board", choice_from):get_name()
local pieceTo = inv:get_stack("board", choice_to):get_name()
local black_king_idx, white_king_idx = locate_kings(board_t)
local bot_king_idx
if currentBotColor == "black" then
bot_king_idx = black_king_idx
else
bot_king_idx = white_king_idx
end
local botAttacked = attacked(currentBotColor, bot_king_idx, board_t)
local kingSafe = true
local bestMoveSaveFrom, bestMoveSaveTo
if botAttacked then
kingSafe = false
meta:set_string(currentBotColor.."Attacked", "true")
local is_safe, safe_moves = has_king_safe_move(moves, board_t, currentBotColor)
if is_safe then
bestMoveSaveFrom, bestMoveSaveTo = best_move(safe_moves)
end
end
minetest.after(BOT_DELAY_MOVE, function()
local gameResult = meta:get_string("gameResult")
if gameResult ~= "" then
return
end
local botColor = meta:get_string("botColor")
if botColor == "" then
return
end
local lastMove = meta:get_string("lastMove")
local lastMoveTime = meta:get_int("lastMoveTime")
if lastMoveTime > 0 or lastMove == "" then
if currentBotColor == "black" and meta:get_string("playerBlack") == "" then
meta:set_string("playerBlack", botName)
elseif currentBotColor == "white" and meta:get_string("playerWhite") == "" then
meta:set_string("playerWhite", botName)
end
local moveOK = false
if not kingSafe then
-- Make a move to put the king out of check
if bestMoveSaveTo ~= nil then
moveOK = realchess.move(meta, "board", bestMoveSaveFrom, "board", bestMoveSaveTo, botName)
if not moveOK then
minetest.log("error", "[xdecor] Chess: Bot tried to make an invalid move (to protect the king) from "..
index_to_notation(bestMoveSaveFrom).." to "..index_to_notation(bestMoveSaveTo))
end
else
-- No safe move left: checkmate or stalemate
return
end
else
-- Make a regular move
moveOK = realchess.move(meta, "board", choice_from, "board", choice_to, botName)
if not moveOK then
minetest.log("error", "[xdecor] Chess: Bot tried to make an invalid move from "..
index_to_notation(choice_from).." to "..index_to_notation(choice_to))
end
end
-- Bot resigns if it made an incorrect move
if not moveOK then
meta:set_string("gameResultReason", "resign")
if currentBotColor == "black" then
meta:set_string("gameResult", "whiteWon") meta:set_string("gameResult", "whiteWon")
meta:set_string("gameResultReason", "resign")
add_special_to_moves_list(meta, "whiteWon") add_special_to_moves_list(meta, "whiteWon")
else update_formspec(meta)
elseif playerColor == "white" then
meta:set_string("gameResult", "blackWon") meta:set_string("gameResult", "blackWon")
meta:set_string("gameResultReason", "resign")
add_special_to_moves_list(meta, "blackWon") add_special_to_moves_list(meta, "blackWon")
end
update_formspec(meta) update_formspec(meta)
end end
end end
end)
else
update_formspec(meta)
end
end
local function bot_promote(inv, meta, pawnIndex)
minetest.after(BOT_DELAY_PROMOTE, function()
local lastMove = meta:get_string("lastMove")
local color
if lastMove == "black" or lastMove == "" then
color = "white"
else
color = "black"
end
-- Always promote to queen
realchess.promote_pawn(meta, color, "queen")
end)
end
local function timeout_format(timeout_limit) local function timeout_format(timeout_limit)
local time_remaining = timeout_limit - minetest.get_gametime() local time_remaining = timeout_limit - minetest.get_gametime()
@ -2725,7 +2583,7 @@ function realchess.fields(pos, _, fields, sender)
meta:set_string("playerWhite", "*"..BOT_NAME_1.."*") meta:set_string("playerWhite", "*"..BOT_NAME_1.."*")
meta:set_string("playerBlack", "*"..BOT_NAME_2.."*") meta:set_string("playerBlack", "*"..BOT_NAME_2.."*")
local inv = meta:get_inventory() local inv = meta:get_inventory()
bot_move(inv, meta) chessbot.move(inv, meta)
elseif fields.single_w then elseif fields.single_w then
meta:set_string("mode", "single") meta:set_string("mode", "single")
meta:set_string("botColor", "black") meta:set_string("botColor", "black")
@ -2735,7 +2593,7 @@ function realchess.fields(pos, _, fields, sender)
meta:set_string("botColor", "white") meta:set_string("botColor", "white")
meta:set_string("playerWhite", "*"..BOT_NAME.."*") meta:set_string("playerWhite", "*"..BOT_NAME.."*")
local inv = meta:get_inventory() local inv = meta:get_inventory()
bot_move(inv, meta) chessbot.move(inv, meta)
elseif fields.multi then elseif fields.multi then
meta:set_string("mode", "multi") meta:set_string("mode", "multi")
end end
@ -2792,13 +2650,10 @@ function realchess.fields(pos, _, fields, sender)
whiteWon = true whiteWon = true
end end
if winner and loser then if winner and loser then
meta:set_string("gameResultReason", "resign")
if whiteWon then if whiteWon then
meta:set_string("gameResult", "whiteWon") realchess.resign(meta, "black")
add_special_to_moves_list(meta, "whiteWon")
else else
meta:set_string("gameResult", "blackWon") realchess.resign(meta, "white")
add_special_to_moves_list(meta, "blackWon")
end end
send_message(loser, S("You have resigned.")) send_message(loser, S("You have resigned."))
@ -2912,16 +2767,16 @@ function realchess.move_piece(meta, pieceFrom, from_list, from_index, to_list, t
-- Let the bot play when it its turn -- Let the bot play when it its turn
if (mode == "bot_vs_bot" or (mode == "single" and lastMove ~= botColor)) and gameResult == "" then if (mode == "bot_vs_bot" or (mode == "single" and lastMove ~= botColor)) and gameResult == "" then
if not promo then if not promo then
bot_move(inv, meta) chessbot.move(inv, meta)
else else
bot_promote(inv, meta, to_index) chessbot.promote(inv, meta, to_index)
end end
end end
end end
function realchess.update_state(meta, from_index, to_index, thisMove, promoteFrom, promoteTo) function realchess.update_state(meta, from_index, to_index, thisMove, promoteFrom, promoteTo)
local inv = meta:get_inventory() local inv = meta:get_inventory()
local board = board_to_table(inv) local board = realchess.board_to_table(inv)
local pieceTo = board[to_index] local pieceTo = board[to_index]
local pieceFrom = promoteFrom or board[from_index] local pieceFrom = promoteFrom or board[from_index]
@ -2930,13 +2785,13 @@ function realchess.update_state(meta, from_index, to_index, thisMove, promoteFro
board[from_index] = "" board[from_index] = ""
end end
local black_king_idx, white_king_idx = locate_kings(board) local black_king_idx, white_king_idx = realchess.locate_kings(board)
if not black_king_idx or not white_king_idx then if not black_king_idx or not white_king_idx then
minetest.log("error", "[xdecor] Chess: Insufficient kings on chessboard!") minetest.log("error", "[xdecor] Chess: Insufficient kings on chessboard!")
return return
end end
local blackAttacked = attacked("black", black_king_idx, board) local blackAttacked = realchess.attacked("black", black_king_idx, board)
local whiteAttacked = attacked("white", white_king_idx, board) local whiteAttacked = realchess.attacked("white", white_king_idx, board)
if blackAttacked then if blackAttacked then
meta:set_string("blackAttacked", "true") meta:set_string("blackAttacked", "true")
@ -3004,7 +2859,7 @@ function realchess.promote_pawn(meta, color, promoteTo)
local mode = meta:get_string("mode") local mode = meta:get_string("mode")
local gameResult = meta:get_string("gameResult") local gameResult = meta:get_string("gameResult")
if (mode == "bot_vs_bot" or (mode == "single" and lastMove ~= botColor)) and gameResult == "" then if (mode == "bot_vs_bot" or (mode == "single" and lastMove ~= botColor)) and gameResult == "" then
bot_move(inv, meta) chessbot.move(inv, meta)
end end
else else
minetest.log("error", "[xdecor] Chess: Could not find pawn to promote!") minetest.log("error", "[xdecor] Chess: Could not find pawn to promote!")

159
src/chessbot.lua Normal file
View File

@ -0,0 +1,159 @@
local chessbot = {}
-- Delay in seconds for a bot moving a piece (excluding choosing a promotion)
local BOT_DELAY_MOVE = 1.0
-- Delay in seconds for a bot promoting a piece
local BOT_DELAY_PROMOTE = 1.0
local function best_move(moves)
local value, choices = 0, {}
for from, _ in pairs(moves) do
for to, val in pairs(_) do
if val > value then
value = val
choices = {{
from = from,
to = to
}}
elseif val == value then
choices[#choices + 1] = {
from = from,
to = to
}
end
end
end
if #choices == 0 then
return nil
end
local random = math.random(1, #choices)
local choice_from, choice_to = choices[random].from, choices[random].to
return tonumber(choice_from), choice_to
end
function chessbot.move(inv, meta)
local board_t = realchess.board_to_table(inv)
local lastMove = meta:get_string("lastMove")
local gameResult = meta:get_string("gameResult")
local botColor = meta:get_string("botColor")
if botColor == "" then
return
end
local currentBotColor, opponentColor
local botName
if botColor == "black" then
currentBotColor = "black"
opponentColor = "white"
elseif botColor == "white" then
currentBotColor = "white"
opponentColor = "black"
elseif botColor == "both" then
opponentColor = lastMove
if lastMove == "black" or lastMove == "" then
currentBotColor = "white"
else
currentBotColor = "black"
end
end
if currentBotColor == "white" then
botName = meta:get_string("playerWhite")
else
botName = meta:get_string("playerBlack")
end
if (lastMove == opponentColor or ((botColor == "white" or botColor == "both") and lastMove == "")) and gameResult == "" then
local moves = realchess.get_theoretical_moves_for(meta, board_t, currentBotColor)
local choice_from, choice_to = best_move(moves)
if choice_from == nil then
-- No best move: stalemate or checkmate
return
end
local pieceFrom = inv:get_stack("board", choice_from):get_name()
local pieceTo = inv:get_stack("board", choice_to):get_name()
local black_king_idx, white_king_idx = realchess.locate_kings(board_t)
local bot_king_idx
if currentBotColor == "black" then
bot_king_idx = black_king_idx
else
bot_king_idx = white_king_idx
end
local botAttacked = realchess.attacked(currentBotColor, bot_king_idx, board_t)
local kingSafe = true
local bestMoveSaveFrom, bestMoveSaveTo
if botAttacked then
kingSafe = false
meta:set_string(currentBotColor.."Attacked", "true")
local is_safe, safe_moves = realchess.has_king_safe_move(moves, board_t, currentBotColor)
if is_safe then
bestMoveSaveFrom, bestMoveSaveTo = best_move(safe_moves)
end
end
minetest.after(BOT_DELAY_MOVE, function()
local gameResult = meta:get_string("gameResult")
if gameResult ~= "" then
return
end
local botColor = meta:get_string("botColor")
if botColor == "" then
return
end
local lastMove = meta:get_string("lastMove")
local lastMoveTime = meta:get_int("lastMoveTime")
if lastMoveTime > 0 or lastMove == "" then
if currentBotColor == "black" and meta:get_string("playerBlack") == "" then
meta:set_string("playerBlack", botName)
elseif currentBotColor == "white" and meta:get_string("playerWhite") == "" then
meta:set_string("playerWhite", botName)
end
local moveOK = false
if not kingSafe then
-- Make a move to put the king out of check
if bestMoveSaveTo ~= nil then
moveOK = realchess.move(meta, "board", bestMoveSaveFrom, "board", bestMoveSaveTo, botName)
if not moveOK then
minetest.log("error", "[xdecor] Chess: Bot tried to make an invalid move (to protect the king) from "..
realchess.index_to_notation(bestMoveSaveFrom).." to "..realchess.index_to_notation(bestMoveSaveTo))
end
else
-- No safe move left: checkmate or stalemate
return
end
else
-- Make a regular move
moveOK = realchess.move(meta, "board", choice_from, "board", choice_to, botName)
if not moveOK then
minetest.log("error", "[xdecor] Chess: Bot tried to make an invalid move from "..
realchess.index_to_notation(choice_from).." to "..realchess.index_to_notation(choice_to))
end
end
-- Bot resigns if it made an incorrect move
if not moveOK then
realchess.resign(meta, currentBotColor)
end
end
end)
end
end
function chessbot.promote(inv, meta, pawnIndex)
minetest.after(BOT_DELAY_PROMOTE, function()
local lastMove = meta:get_string("lastMove")
local color
if lastMove == "black" or lastMove == "" then
color = "white"
else
color = "black"
end
-- Always promote to queen
realchess.promote_pawn(meta, color, "queen")
end)
end
return chessbot