Refactor chess code to access node meta less often

This commit is contained in:
Wuzzy 2024-09-19 13:28:58 +02:00
parent 1156940f99
commit 08991e877c
2 changed files with 161 additions and 84 deletions

View File

@ -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, "")

View File

@ -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