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 NS = function(s) return 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.
-- 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
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.
-- In case of a bot player, will return a translated
-- bot name.
@ -141,7 +146,7 @@ local function send_message_2(playerName1, playerName2, message, botColor, isDeb
end
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)
if not x or not y then
return "??"
@ -151,7 +156,7 @@ local function index_to_notation(idx)
return xstr .. ystr
end
local function board_to_table(inv)
function realchess.board_to_table(inv)
local t = {}
for i = 1, 64 do
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 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 kill = color == "white"
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
dsy = dsy + 1
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
return s_en_passant
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
-- are NOT attacked
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
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_board simulates the board with the king moved already.
-- 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[from_idx] = ""
if attacked(color, to_idx, king_board) then
if realchess.attacked(color, to_idx, king_board) then
moves[to_idx] = nil
else
local dx = from_x - to_x
@ -798,6 +803,7 @@ local function get_theoretical_moves_from(meta, board, from_idx)
return {}
end
-- Rate the possible moves depending on its piece value
for i in pairs(moves) do
local stack_name = board[tonumber(i)]
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)
-- 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.
local function get_theoretical_moves_for(meta, board, player)
function realchess.get_theoretical_moves_for(meta, board, player)
local moves = {}
for i = 1, 64 do
local possibleMoves = get_theoretical_moves_from(meta, board, i)
@ -839,36 +845,7 @@ local function get_theoretical_moves_for(meta, board, player)
return moves
end
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
local function locate_kings(board)
function realchess.locate_kings(board)
local Bidx, Widx
for i = 1, 64 do
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,
-- return false otherwise.
-- 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
-- * 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 = {}
-- create a virtual 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
v_board[to_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
minetest.log("error", "[xdecor] Chess: Insufficient kings on chessboard!")
return false
@ -918,7 +895,7 @@ local function has_king_safe_move(theoretical_moves, board, player)
else
king_idx = white_king_idx
end
local playerAttacked = attacked(player, king_idx, v_board)
local playerAttacked = realchess.attacked(player, king_idx, v_board)
if not playerAttacked then
safe_moves[from_idx] = safe_moves[from_idx] or {}
safe_moves[from_idx][to_idx] = value
@ -1421,8 +1398,8 @@ local function get_moves_formstring(meta)
end
local pieceTo_si_id = pieceTo ~= "" and get_figurine_id(pieceTo)
local coordFrom = index_to_notation(from_idx)
local coordTo = index_to_notation(to_idx)
local coordFrom = realchess.index_to_notation(from_idx)
local coordTo = realchess.index_to_notation(to_idx)
if curPlayerIsWhite then
move_no = move_no + 1
@ -1492,7 +1469,7 @@ local verify_eaten_list
if CHESS_DEBUG then
verify_eaten_list = function(meta)
local inv = meta:get_inventory()
local board = board_to_table(inv)
local board = realchess.board_to_table(inv)
local whitePiecesLeft = 0
local whitePiecesEaten = 0
local blackPiecesLeft = 0
@ -1744,7 +1721,7 @@ end
local function update_game_result(meta)
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 playerBlack = meta:get_string("playerBlack")
@ -1753,8 +1730,8 @@ local function update_game_result(meta)
local blackCanMove = false
local whiteCanMove = false
local blackMoves = get_theoretical_moves_for(meta, board_t, "black")
local whiteMoves = get_theoretical_moves_for(meta, board_t, "white")
local blackMoves = realchess.get_theoretical_moves_for(meta, board_t, "black")
local whiteMoves = realchess.get_theoretical_moves_for(meta, board_t, "white")
if next(blackMoves) then
blackCanMove = true
end
@ -1765,7 +1742,7 @@ local function update_game_result(meta)
-- assume lastMove was updated *after* the player moved
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
minetest.log("error", "[xdecor] Chess: Insufficient kings on chessboard!")
return
@ -1784,10 +1761,10 @@ local function update_game_result(meta)
-- King attacked? This reduces the list of available moves,
-- 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
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 is_safe 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 check = true
local inv = meta:get_inventory()
local board = board_to_table(inv)
local board = realchess.board_to_table(inv)
-- Castling
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
local board = board_to_table(inv)
local board = realchess.board_to_table(inv)
board[to_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
minetest.log("error", "[xdecor] Chess: Insufficient kings on chessboard!")
return false
end
local blackAttacked = attacked("black", black_king_idx, board)
local whiteAttacked = attacked("white", white_king_idx, board)
local blackAttacked = realchess.attacked("black", black_king_idx, board)
local whiteAttacked = realchess.attacked("white", white_king_idx, board)
-- Refuse to move if it would put or leave the own king
-- under attack
@ -2554,139 +2531,20 @@ function realchess.move(meta, from_list, from_index, to_list, to_index, playerNa
return true
end
local function bot_move(inv, meta)
local board_t = 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
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
-- Causes the player ("white" or "blue") to resign
function realchess.resign(meta, playerColor)
if playerColor == "black" then
meta:set_string("gameResult", "whiteWon")
meta:set_string("gameResultReason", "resign")
add_special_to_moves_list(meta, "whiteWon")
else
update_formspec(meta)
elseif playerColor == "white" then
meta:set_string("gameResult", "blackWon")
meta:set_string("gameResultReason", "resign")
add_special_to_moves_list(meta, "blackWon")
end
update_formspec(meta)
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 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("playerBlack", "*"..BOT_NAME_2.."*")
local inv = meta:get_inventory()
bot_move(inv, meta)
chessbot.move(inv, meta)
elseif fields.single_w then
meta:set_string("mode", "single")
meta:set_string("botColor", "black")
@ -2735,7 +2593,7 @@ function realchess.fields(pos, _, fields, sender)
meta:set_string("botColor", "white")
meta:set_string("playerWhite", "*"..BOT_NAME.."*")
local inv = meta:get_inventory()
bot_move(inv, meta)
chessbot.move(inv, meta)
elseif fields.multi then
meta:set_string("mode", "multi")
end
@ -2792,13 +2650,10 @@ function realchess.fields(pos, _, fields, sender)
whiteWon = true
end
if winner and loser then
meta:set_string("gameResultReason", "resign")
if whiteWon then
meta:set_string("gameResult", "whiteWon")
add_special_to_moves_list(meta, "whiteWon")
realchess.resign(meta, "black")
else
meta:set_string("gameResult", "blackWon")
add_special_to_moves_list(meta, "blackWon")
realchess.resign(meta, "white")
end
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
if (mode == "bot_vs_bot" or (mode == "single" and lastMove ~= botColor)) and gameResult == "" then
if not promo then
bot_move(inv, meta)
chessbot.move(inv, meta)
else
bot_promote(inv, meta, to_index)
chessbot.promote(inv, meta, to_index)
end
end
end
function realchess.update_state(meta, from_index, to_index, thisMove, promoteFrom, promoteTo)
local inv = meta:get_inventory()
local board = board_to_table(inv)
local board = realchess.board_to_table(inv)
local pieceTo = board[to_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] = ""
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
minetest.log("error", "[xdecor] Chess: Insufficient kings on chessboard!")
return
end
local blackAttacked = attacked("black", black_king_idx, board)
local whiteAttacked = attacked("white", white_king_idx, board)
local blackAttacked = realchess.attacked("black", black_king_idx, board)
local whiteAttacked = realchess.attacked("white", white_king_idx, board)
if blackAttacked then
meta:set_string("blackAttacked", "true")
@ -3004,7 +2859,7 @@ function realchess.promote_pawn(meta, color, promoteTo)
local mode = meta:get_string("mode")
local gameResult = meta:get_string("gameResult")
if (mode == "bot_vs_bot" or (mode == "single" and lastMove ~= botColor)) and gameResult == "" then
bot_move(inv, meta)
chessbot.move(inv, meta)
end
else
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