diff --git a/src/chess.lua b/src/chess.lua index a7db0e2..01b584f 100644 --- a/src/chess.lua +++ b/src/chess.lua @@ -327,11 +327,10 @@ local function en_passant_to_string(double_step) return s_en_passant end -local function can_castle(meta, board, from_list, from_idx, to_idx) +local function can_castle(board, from_idx, to_idx, castlingRights) local from_x, from_y = index_to_xy(from_idx) local to_x, to_y = index_to_xy(to_idx) - local inv = meta:get_inventory() - local kingPiece = inv:get_stack(from_list, from_idx):get_name() + local kingPiece = board[from_idx] local kingColor if kingPiece:find("black") then kingColor = "black" @@ -340,21 +339,21 @@ local function can_castle(meta, board, from_list, from_idx, to_idx) end local possible_castles = { -- white queenside - { y = 7, to_x = 2, rook_idx = 57, rook_goal = 60, acheck_dir = -1, color = "white", meta = "castlingWhiteL", rook_id = 1 }, + { y = 7, to_x = 2, rook_idx = 57, rook_goal = 60, acheck_dir = -1, color = "white", rightName = "castlingWhiteL", rook_id = 1 }, -- white kingside - { y = 7, to_x = 6, rook_idx = 64, rook_goal = 62, acheck_dir = 1, color = "white", meta = "castlingWhiteR", rook_id = 2 }, + { y = 7, to_x = 6, rook_idx = 64, rook_goal = 62, acheck_dir = 1, color = "white", rightName = "castlingWhiteR", rook_id = 2 }, -- black queenside - { y = 0, to_x = 2, rook_idx = 1, rook_goal = 4, acheck_dir = -1, color = "black", meta = "castlingBlackL", rook_id = 1 }, + { y = 0, to_x = 2, rook_idx = 1, rook_goal = 4, acheck_dir = -1, color = "black", rightName = "castlingBlackL", rook_id = 1 }, -- black kingside - { y = 0, to_x = 6, rook_idx = 8, rook_goal = 6, acheck_dir = 1, color = "black", meta = "castlingBlackR", rook_id = 2 }, + { y = 0, to_x = 6, rook_idx = 8, rook_goal = 6, acheck_dir = 1, color = "black", rightName = "castlingBlackR", rook_id = 2 }, } for p=1, #possible_castles do local pc = possible_castles[p] if pc.color == kingColor and pc.to_x == to_x and to_y == pc.y and from_y == pc.y then - local castlingMeta = meta:get_int(pc.meta) - local rookPiece = inv:get_stack(from_list, pc.rook_idx):get_name() - if castlingMeta == 1 and rookPiece == "realchess:rook_"..kingColor.."_"..pc.rook_id then + local castlingRightVal = castlingRights[pc.rightName] + local rookPiece = board[pc.rook_idx] + if castlingRightVal == 1 and rookPiece == "realchess:rook_"..kingColor.."_"..pc.rook_id then -- Check if all squares between king and rook are empty local empty_start, empty_end if pc.acheck_dir == -1 then @@ -367,7 +366,7 @@ local function can_castle(meta, board, from_list, from_idx, to_idx) empty_end = pc.rook_idx - 1 end for i = empty_start, empty_end do - if inv:get_stack(from_list, i):get_name() ~= "" then + if board[i] ~= "" then return false end end @@ -389,15 +388,15 @@ end -- Checks if a square to check if there is a piece that can be captured en passant. Returns true if this -- is the case, false otherwise. -- Parameters: --- * meta: chessboard node metadata +-- * board: chessboard table -- * victim_color: color of the opponent to capture a piece from. "white" or "black". (so in White's turn, pass "black" here) -- * victim_index: board index of the square where you expect the victim to be -local function can_capture_en_passant(meta, victim_color, victim_index) - local inv = meta:get_inventory() - local victimPiece = inv:get_stack("board", victim_index) - local double_step_index = meta:get_int("prevDoublePawnStepTo") - local victim_name = victimPiece:get_name() - if double_step_index ~= 0 and double_step_index == victim_index and victim_name:find(victim_color) and victim_name:sub(11,14) == "pawn" then +-- * prevDoublePawnStepTo: if a pawn did a double-step in the previous halfmove, this is the board index of the destination. +-- if no pawn made a double-step in the previous halfmove, this is nil or 0. +local function can_capture_en_passant(board, victim_color, victim_index, prevDoublePawnStepTo) + local victimPiece = board[victim_index] + local double_step_index = prevDoublePawnStepTo or 0 + if double_step_index ~= 0 and double_step_index == victim_index and victimPiece:find(victim_color) and victimPiece:sub(11,14) == "pawn" then return true end return false @@ -413,7 +412,7 @@ end -- Any key with a numeric value is a possible destination. -- The numeric value is a move rating for the bot and is 0 by default. -- Example: { [4] = 0, [9] = 0 } -- can move to squares 4 and 9 -local function get_theoretical_moves_from(meta, board, from_idx) +local function get_theoretical_moves_from(board, from_idx, prevDoublePawnStepTo, castlingRights) local piece, color = board[from_idx]:match(":(%w+)_(%w+)") if not piece then return {} @@ -450,7 +449,7 @@ local function get_theoretical_moves_from(meta, board, from_idx) can_capture = true else -- en passant - if can_capture_en_passant(meta, "black", xy_to_index(to_x, from_y)) then + if can_capture_en_passant(board, "black", xy_to_index(to_x, from_y), prevDoublePawnStepTo) then can_capture = true en_passant = true end @@ -505,7 +504,7 @@ local function get_theoretical_moves_from(meta, board, from_idx) can_capture = true else -- en passant - if can_capture_en_passant(meta, "white", xy_to_index(to_x, from_y)) then + if can_capture_en_passant(board, "white", xy_to_index(to_x, from_y), prevDoublePawnStepTo) then can_capture = true en_passant = true end @@ -783,11 +782,10 @@ local function get_theoretical_moves_from(meta, board, from_idx) -- KING elseif piece == "king" then - local inv = meta:get_inventory() -- 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 = realchess.board_to_table(inv) + local king_board = table.copy(board) king_board[to_idx] = king_board[from_idx] king_board[from_idx] = "" if realchess.attacked(color, to_idx, king_board) then @@ -805,7 +803,7 @@ local function get_theoretical_moves_from(meta, board, from_idx) end if dx > 1 or dy > 1 then - local cc = can_castle(meta, board, "board", from_idx, to_idx) + local cc = can_castle(board, from_idx, to_idx, castlingRights) if not cc then moves[to_idx] = nil end @@ -847,10 +845,10 @@ 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. -function realchess.get_theoretical_moves_for(meta, board, player) +function realchess.get_theoretical_moves_for(board, player, prevDoublePawnStepTo, castlingRights) local moves = {} for i = 1, 64 do - local possibleMoves = get_theoretical_moves_from(meta, board, i) + local possibleMoves = get_theoretical_moves_from(board, i, prevDoublePawnStepTo, castlingRights) if next(possibleMoves) then local stack_name = board[i] if stack_name:find(player) then @@ -1899,13 +1897,20 @@ local function update_game_result(meta, lastMove) local playerWhite = meta:get_string("playerWhite") local playerBlack = meta:get_string("playerBlack") + local prevDoublePawnStepTo = meta:get_int("prevDoublePawnStepTo") + local castlingRights = { + castlingWhiteR = meta:get_int("castlingWhiteR"), + castlingWhiteL = meta:get_int("castlingWhiteL"), + castlingBlackR = meta:get_int("castlingBlackR"), + castlingBlackL = meta:get_int("castlingBlackL"), + } update_formspec(meta) local blackCanMove = false local whiteCanMove = false - local blackMoves = realchess.get_theoretical_moves_for(meta, board_t, "black") - local whiteMoves = realchess.get_theoretical_moves_for(meta, board_t, "white") + local blackMoves = realchess.get_theoretical_moves_for(board_t, "black", prevDoublePawnStepTo, castlingRights) + local whiteMoves = realchess.get_theoretical_moves_for(board_t, "white", prevDoublePawnStepTo, castlingRights) if next(blackMoves) then blackCanMove = true end @@ -2230,6 +2235,7 @@ function realchess.move(meta, from_list, from_index, to_list, to_index, playerNa local lastMove = meta:get_string("lastMove") local playerWhite = meta:get_string("playerWhite") local playerBlack = meta:get_string("playerBlack") + local prevDoublePawnStepTo = meta:get_int("prevDoublePawnStepTo") local kingMoved = false local thisMove -- Will replace lastMove when move is legal @@ -2346,7 +2352,8 @@ function realchess.move(meta, from_list, from_index, to_list, to_index, playerNa can_capture = true else -- en passant - if can_capture_en_passant(meta, "black", xy_to_index(to_x, from_y)) then + local board = realchess.board_to_table(inv) + if can_capture_en_passant(board, "black", xy_to_index(to_x, from_y), prevDoublePawnStepTo) then can_capture = true en_passant_target = xy_to_index(to_x, from_y) end @@ -2414,7 +2421,8 @@ function realchess.move(meta, from_list, from_index, to_list, to_index, playerNa can_capture = true else -- en passant - if can_capture_en_passant(meta, "white", xy_to_index(to_x, from_y)) then + local board = realchess.board_to_table(inv) + if can_capture_en_passant(board, "white", xy_to_index(to_x, from_y), prevDoublePawnStepTo) then can_capture = true en_passant_target = xy_to_index(to_x, from_y) end @@ -2666,9 +2674,15 @@ function realchess.move(meta, from_list, from_index, to_list, to_index, playerNa local check = true local inv = meta:get_inventory() local board = realchess.board_to_table(inv) + local castlingRights = { + castlingWhiteR = meta:get_int("castlingWhiteR"), + castlingWhiteL = meta:get_int("castlingWhiteL"), + castlingBlackR = meta:get_int("castlingBlackR"), + castlingBlackL = meta:get_int("castlingBlackL"), + } -- 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(board, from_index, to_index, castlingRights) if cc then inv:set_stack(from_list, rook_goal, rook_name) inv:set_stack(from_list, rook_start, "") diff --git a/src/chessbot.lua b/src/chessbot.lua index b467af9..364c811 100644 --- a/src/chessbot.lua +++ b/src/chessbot.lua @@ -3,7 +3,7 @@ local chessbot = {} local realchess = xdecor.chess -- Delay in seconds for a bot moving a piece (excluding choosing a promotion) -local BOT_DELAY_MOVE = 1.0 +local BOT_DELAY_MOVE = 0.2 -- Delay in seconds for a bot promoting a piece local BOT_DELAY_PROMOTE = 1.0 @@ -36,15 +36,58 @@ local function best_move(moves) 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") +function chessbot.choose_move(board_t, meta_t) + local lastMove = meta_t["lastMove"] + local gameResult = meta_t["gameResult"] + local botColor = meta_t["botColor"] + local prevDoublePawnStepTo = meta_t["prevDoublePawnStepTo"] + local castlingRights = { + castlingWhiteR = meta_t["castlingWhiteR"], + castlingWhiteL = meta_t["castlingWhiteL"], + castlingBlackR = meta_t["castlingBlackR"], + castlingBlackL = meta_t["castlingBlackL"], + } + if botColor == "" then return end local currentBotColor, opponentColor + 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 (lastMove == opponentColor or ((botColor == "white" or botColor == "both") and lastMove == "")) and gameResult == "" then + + local moves = realchess.get_theoretical_moves_for(board_t, currentBotColor, prevDoublePawnStepTo, castlingRights) + local safe_moves, safe_moves_count = realchess.get_king_safe_moves(moves, board_t, currentBotColor) + if safe_moves_count == 0 then + -- No safe move: stalemate or checkmate + end + local choice_from, choice_to = best_move(safe_moves) + if choice_from == nil then + -- No best move: stalemate or checkmate + return + end + + return choice_from, choice_to + + end +end + +chessbot.perform_move = function(choice_from, choice_to, meta) + local lastMove = meta:get_string("lastMove") + local botColor = meta:get_string("botColor") + local currentBotColor, opponentColor local botName if botColor == "black" then currentBotColor = "black" @@ -60,62 +103,56 @@ function chessbot.move(inv, meta) currentBotColor = "black" end end + + -- Bot resigns if no move chosen + if not choice_from or not choice_to then + realchess.resign(meta, currentBotColor) + return + 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 safe_moves, safe_moves_count = realchess.get_king_safe_moves(moves, board_t, currentBotColor) - if safe_moves_count == 0 then - -- No safe move: stalemate or checkmate - end - local choice_from, choice_to = best_move(safe_moves) - if choice_from == nil then - -- No best move: stalemate or checkmate - return + 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 + -- Set the bot name if not set already + 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 pieceFrom = inv:get_stack("board", choice_from):get_name() - local pieceTo = inv:get_stack("board", choice_to):get_name() - - 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 - -- Set the bot name if not set already - 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 - - -- Make a move - local 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 - -- Bot resigns if it tried to make an invalid move - if not moveOK then - realchess.resign(meta, currentBotColor) - end - end - end) + -- Make a move + local 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 + -- Bot resigns if it tried to make an invalid move + if not moveOK then + realchess.resign(meta, currentBotColor) + end end end -function chessbot.promote(inv, meta, pawnIndex) +function chessbot.choose_promote(board_t, pawnIndex) + -- Bot always promotes to queen + return "queen" +end + +function chessbot.perform_promote(meta, promoteTo) minetest.after(BOT_DELAY_PROMOTE, function() local lastMove = meta:get_string("lastMove") local color @@ -124,9 +161,35 @@ function chessbot.promote(inv, meta, pawnIndex) else color = "black" end - -- Always promote to queen - realchess.promote_pawn(meta, color, "queen") + realchess.promote_pawn(meta, color, promoteTo) end) end +function chessbot.move(inv, meta) + local board_t = realchess.board_to_table(inv) + local meta_t = { + lastMove = meta:get_string("lastMove"), + gameResult = meta:get_string("gameResult"), + botColor = meta:get_string("botColor"), + prevDoublePawnStepTo = meta:get_int("prevDoublePawnStepTo"), + castlingWhiteL = meta:get_int("castlingWhiteL"), + castlingWhiteR = meta:get_int("castlingWhiteR"), + castlingBlackL = meta:get_int("castlingBlackL"), + castlingBlackR = meta:get_int("castlingBlackR"), + } + local choice_from, choice_to = chessbot.choose_move(board_t, meta_t) + minetest.after(BOT_DELAY_MOVE, function() + chessbot.perform_move(choice_from, choice_to, meta) + end) +end + +function chessbot.promote(inv, meta, pawnIndex) + local board_t = realchess.board_to_table(inv) + local promoteTo = chessbot.choose_promote(board_t, pawnIndex) + if not promoteTo then + promoteTo = "queen" + end + chessbot.perform_promote(meta, promoteTo) +end + return chessbot