local realchess = {} local S = minetest.get_translator("xdecor") local FS = function(...) return minetest.formspec_escape(S(...)) end local ALPHA_OPAQUE = minetest.features.use_texture_alpha_string_modes and "opaque" or false local MOVES_LIST_SYMBOL_EMPTY = 69 local AI_NAME = S("Dumb AI") 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. local ENABLE_CHESS_GAMES = true local function index_to_xy(idx) if not idx then return nil end idx = idx - 1 local x = idx % 8 local y = (idx - x) / 8 return x, y end local function xy_to_index(x, y) return x + y * 8 + 1 end local function get_square(a, b) return (a * 8) - (8 - b) end local chat_prefix = minetest.colorize("#FFFF00", "["..S("Chess").."] ") local letters = {'a','b','c','d','e','f','g','h'} local function board_to_table(inv) local t = {} for i = 1, 64 do t[#t + 1] = inv:get_stack("board", i):get_name() end return t end local piece_values = { pawn = 10, knight = 30, bishop = 30, rook = 50, queen = 90, king = 900 } local rowDirs = {-1, -1, -1, 0, 0, 1, 1, 1} local colDirs = {-1, 0, 1, -1, 1, -1, 0, 1} local rowDirsKnight = { 2, 1, 2, 1, -2, -1, -2, -1} local colDirsKnight = {-1, -2, 1, 2, 1, 2, -1, -2} local bishopThreats = {true, false, true, false, false, true, false, true} 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) local threatDetected = false local kill = color == "white" local pawnThreats = {kill, false, kill, false, false, not kill, false, not kill} for dir = 1, 8 do if not threatDetected then local col, row = index_to_xy(idx) col, row = col + 1, row + 1 for step = 1, 8 do row = row + rowDirs[dir] col = col + colDirs[dir] if row >= 1 and row <= 8 and col >= 1 and col <= 8 then local square = get_square(row, col) local square_name = board[square] local piece, pieceColor = square_name:match(":(%w+)_(%w+)") if piece then if pieceColor ~= color then if piece == "bishop" and bishopThreats[dir] then threatDetected = true elseif piece == "rook" and rookThreats[dir] then threatDetected = true elseif piece == "queen" and queenThreats[dir] then threatDetected = true else if step == 1 then if piece == "pawn" and pawnThreats[dir] then threatDetected = true end if piece == "king" and kingThreats[dir] then threatDetected = true end end end end break end end end local colK, rowK = index_to_xy(idx) colK, rowK = colK + 1, rowK + 1 rowK = rowK + rowDirsKnight[dir] colK = colK + colDirsKnight[dir] if rowK >= 1 and rowK <= 8 and colK >= 1 and colK <= 8 then local square = get_square(rowK, colK) local square_name = board[square] local piece, pieceColor = square_name:match(":(%w+)_(%w+)") if piece and pieceColor ~= color and piece == "knight" then threatDetected = true end end end end return threatDetected end local function get_current_halfmove(meta) local moves_raw = meta:get_string("moves_raw") local mrsplit = string.split(moves_raw, ";") return #mrsplit end -- Returns all theoretically possible moves from a given -- square, according to the piece it occupies. Ignores restrictions like check, etc. -- If the square is empty, no moves are returned. -- Parameters: -- * board: chessboard table -- * from_idx: -- returns: table with the keys used as destination indices -- Any key with a numeric value is a possible destination. -- The numeric value is a move rating for AI 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 piece, color = board[from_idx]:match(":(%w+)_(%w+)") if not piece then return {} end local moves = {} local from_x, from_y = index_to_xy(from_idx) for i = 1, 64 do local stack_name = board[i] if stack_name:find((color == "black" and "white" or "black")) or stack_name == "" then moves[i] = 0 end end for to_idx in pairs(moves) do local pieceTo = board[to_idx] local to_x, to_y = index_to_xy(to_idx) -- PAWN if piece == "pawn" then if color == "white" then local pawnWhiteMove = board[xy_to_index(from_x, from_y - 1)] local en_passant = false -- white pawns can go up only if from_y - 1 == to_y then if from_x == to_x then if pieceTo ~= "" then moves[to_idx] = nil end elseif from_x - 1 == to_x or from_x + 1 == to_x then local can_capture = false if pieceTo:find("black") then can_capture = true else -- en passant local inv = meta:get_inventory() local enPassantPiece = inv:get_stack("board", xy_to_index(to_x, from_y)) local epp_meta = enPassantPiece:get_meta() local epp_name = enPassantPiece:get_name() if epp_name:find("black") and epp_name:sub(11,14) == "pawn" then local pawn_no = epp_name:sub(-1) local double_step_halfmove = meta:get_int("doublePawnStepB"..pawn_no) local current_halfmove = get_current_halfmove(meta) if double_step_halfmove ~= 0 and double_step_halfmove == current_halfmove then can_capture = true en_passant = true end end end if not can_capture then moves[to_idx] = nil end else moves[to_idx] = nil end elseif from_y - 2 == to_y then if pieceTo ~= "" or from_y < 6 or pawnWhiteMove ~= "" then moves[to_idx] = nil end else moves[to_idx] = nil end --[[ if x not changed ensure that destination cell is empty elseif x changed one unit left or right ensure the pawn is killing opponent piece else move is not legal - abort ]] if from_x == to_x then if pieceTo ~= "" then moves[to_idx] = nil end elseif from_x - 1 == to_x or from_x + 1 == to_x then if not pieceTo:find("black") and not en_passant then moves[to_idx] = nil end else moves[to_idx] = nil end elseif color == "black" then local pawnBlackMove = board[xy_to_index(from_x, from_y + 1)] local en_passant = false -- black pawns can go down only if from_y + 1 == to_y then if from_x == to_x then if pieceTo ~= "" then moves[to_idx] = nil end elseif from_x - 1 == to_x or from_x + 1 == to_x then local can_capture = false if pieceTo:find("white") then can_capture = true else -- en passant local inv = meta:get_inventory() local enPassantPiece = inv:get_stack("board", xy_to_index(to_x, from_y)) local epp_meta = enPassantPiece:get_meta() local epp_name = enPassantPiece:get_name() if epp_name:find("white") and epp_name:sub(11,14) == "pawn" then local pawn_no = epp_name:sub(-1) local double_step_halfmove = meta:get_int("doublePawnStepW"..pawn_no) local current_halfmove = get_current_halfmove(meta) if double_step_halfmove ~= 0 and double_step_halfmove == current_halfmove then can_capture = true en_passant = true end end end if not can_capture then moves[to_idx] = nil end else moves[to_idx] = nil end elseif from_y + 2 == to_y then if pieceTo ~= "" or from_y > 1 or pawnBlackMove ~= "" then moves[to_idx] = nil end else moves[to_idx] = nil end --[[ if x not changed ensure that destination cell is empty elseif x changed one unit left or right ensure the pawn is killing opponent piece else move is not legal - abort ]] if from_x == to_x then if pieceTo ~= "" then moves[to_idx] = nil end elseif from_x - 1 == to_x or from_x + 1 == to_x then if not pieceTo:find("white") and not en_passant then moves[to_idx] = nil end else moves[to_idx] = nil end else moves[to_idx] = nil end -- ROOK elseif piece == "rook" then if from_x == to_x then -- Moving vertically if from_y < to_y then -- Moving down -- Ensure that no piece disturbs the way for i = from_y + 1, to_y - 1 do if board[xy_to_index(from_x, i)] ~= "" then moves[to_idx] = nil end end else -- Moving up -- Ensure that no piece disturbs the way for i = to_y + 1, from_y - 1 do if board[xy_to_index(from_x, i)] ~= "" then moves[to_idx] = nil end end end elseif from_y == to_y then -- Moving horizontally if from_x < to_x then -- moving right -- ensure that no piece disturbs the way for i = from_x + 1, to_x - 1 do if board[xy_to_index(i, from_y)] ~= "" then moves[to_idx] = nil end end else -- Moving left -- Ensure that no piece disturbs the way for i = to_x + 1, from_x - 1 do if board[xy_to_index(i, from_y)] ~= "" then moves[to_idx] = nil end end end else -- Attempt to move arbitrarily -> abort moves[to_idx] = nil end -- KNIGHT elseif piece == "knight" then -- Get relative pos local dx = from_x - to_x local dy = from_y - to_y -- Get absolute values if dx < 0 then dx = -dx end if dy < 0 then dy = -dy end -- Sort x and y if dx > dy then dx, dy = dy, dx end -- Ensure that dx == 1 and dy == 2 if dx ~= 1 or dy ~= 2 then moves[to_idx] = nil end -- Just ensure that destination cell does not contain friend piece -- ^ It was done already thus everything ok -- BISHOP elseif piece == "bishop" then -- Get relative pos local dx = from_x - to_x local dy = from_y - to_y -- Get absolute values if dx < 0 then dx = -dx end if dy < 0 then dy = -dy end -- Ensure dx and dy are equal if dx ~= dy then moves[to_idx] = nil end if from_x < to_x then if from_y < to_y then -- Moving right-down -- Ensure that no piece disturbs the way for i = 1, dx - 1 do if board[xy_to_index(from_x + i, from_y + i)] ~= "" then moves[to_idx] = nil end end else -- Moving right-up -- Ensure that no piece disturbs the way for i = 1, dx - 1 do if board[xy_to_index(from_x + i, from_y - i)] ~= "" then moves[to_idx] = nil end end end else if from_y < to_y then -- Moving left-down -- Ensure that no piece disturbs the way for i = 1, dx - 1 do if board[xy_to_index(from_x - i, from_y + i)] ~= "" then moves[to_idx] = nil end end else -- Moving left-up -- ensure that no piece disturbs the way for i = 1, dx - 1 do if board[xy_to_index(from_x - i, from_y - i)] ~= "" then moves[to_idx] = nil end end end end -- QUEEN elseif piece == "queen" then local dx = from_x - to_x local dy = from_y - to_y -- Get absolute values if dx < 0 then dx = -dx end if dy < 0 then dy = -dy end -- Ensure valid relative move if dx ~= 0 and dy ~= 0 and dx ~= dy then moves[to_idx] = nil end if from_x == to_x then -- Moving vertically if from_y < to_y then -- Moving down -- Ensure that no piece disturbs the way for i = from_y + 1, to_y - 1 do if board[xy_to_index(from_x, i)] ~= "" then moves[to_idx] = nil end end else -- Moving up -- Ensure that no piece disturbs the way for i = to_y + 1, from_y - 1 do if board[xy_to_index(from_x, i)] ~= "" then moves[to_idx] = nil end end end elseif from_x < to_x then if from_y == to_y then -- Goes right -- Ensure that no piece disturbs the way for i = 1, dx - 1 do if board[xy_to_index(from_x + i, from_y)] ~= "" then moves[to_idx] = nil end end elseif from_y < to_y then -- Goes right-down -- Ensure that no piece disturbs the way for i = 1, dx - 1 do if board[xy_to_index(from_x + i, from_y + i)] ~= "" then moves[to_idx] = nil end end else -- Goes right-up -- Ensure that no piece disturbs the way for i = 1, dx - 1 do if board[xy_to_index(from_x + i, from_y - i)] ~= "" then moves[to_idx] = nil end end end else if from_y == to_y then -- Moving horizontally if from_x < to_x then -- moving right -- ensure that no piece disturbs the way for i = from_x + 1, to_x - 1 do if board[xy_to_index(i, from_y)] ~= "" then moves[to_idx] = nil end end else -- Moving left -- Ensure that no piece disturbs the way for i = to_x + 1, from_x - 1 do if board[xy_to_index(i, from_y)] ~= "" then moves[to_idx] = nil end end end elseif from_y < to_y then -- Goes left-down -- Ensure that no piece disturbs the way for i = 1, dx - 1 do if board[xy_to_index(from_x - i, from_y + i)] ~= "" then moves[to_idx] = nil end end else -- Goes left-up -- Ensure that no piece disturbs the way for i = 1, dx - 1 do if board[xy_to_index(from_x - i, from_y - i)] ~= "" then moves[to_idx] = nil end end end end -- KING elseif piece == "king" then local dx = from_x - to_x local dy = from_y - to_y if dx < 0 then dx = -dx end if dy < 0 then dy = -dy end if dx > 1 or dy > 1 then moves[to_idx] = nil end if attacked(color, xy_to_index(to_x, to_y), board) then moves[to_idx] = nil end end end if not next(moves) then return {} end for i in pairs(moves) do local stack_name = board[tonumber(i)] if stack_name ~= "" then for p, value in pairs(piece_values) do if stack_name:find(p) then moves[i] = value end end end end return moves end -- returns all theoretically possible moves on the board for a player -- Parameters: -- * board: chessboard table -- * player: "black" or "white" -- returns: table of this format: -- { -- [origin_index_1] = { [destination_index_1] = r1, [destination_index_2] = r2 }, -- [origin_index_2] = { [destination_index_3] = r3 }, -- ... -- } -- 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 AI. local function get_theoretical_moves_for(meta, board, player) local moves = {} for i = 1, 64 do local possibleMoves = get_theoretical_moves_from(meta, board, i) if next(possibleMoves) then local stack_name = board[i] if stack_name:find(player) then moves[tostring(i)] = possibleMoves end end end 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) local Bidx, Widx for i = 1, 64 do local piece, color = board[i]:match(":(%w+)_(%w+)") if piece == "king" then if color == "black" then Bidx = i else Widx = i end end end return Bidx, Widx end local pieces = { "realchess:rook_black_1", "realchess:knight_black_1", "realchess:bishop_black_1", "realchess:queen_black", "realchess:king_black", "realchess:bishop_black_2", "realchess:knight_black_2", "realchess:rook_black_2", "realchess:pawn_black_1", "realchess:pawn_black_2", "realchess:pawn_black_3", "realchess:pawn_black_4", "realchess:pawn_black_5", "realchess:pawn_black_6", "realchess:pawn_black_7", "realchess:pawn_black_8", '','','','','','','','','','','','','','','','', '','','','','','','','','','','','','','','','', "realchess:pawn_white_1", "realchess:pawn_white_2", "realchess:pawn_white_3", "realchess:pawn_white_4", "realchess:pawn_white_5", "realchess:pawn_white_6", "realchess:pawn_white_7", "realchess:pawn_white_8", "realchess:rook_white_1", "realchess:knight_white_1", "realchess:bishop_white_1", "realchess:queen_white", "realchess:king_white", "realchess:bishop_white_2", "realchess:knight_white_2", "realchess:rook_white_2" } local pieces_str, x = "", 0 for i = 1, #pieces do local p = pieces[i]:match(":(%w+_%w+)") if pieces[i]:find(":(%w+)_(%w+)") and not pieces_str:find(p) then pieces_str = pieces_str .. x .. "=" .. p .. ".png," x = x + 1 end end pieces_str = pieces_str .. MOVES_LIST_SYMBOL_EMPTY .. "=mailbox_blank16.png" local fs_init = [[ formspec_version[2] size[16,10.7563;] no_prepend[] ]] .."bgcolor[#080808BB;true]" .."background[0,0;16,10.7563;chess_bg.png;true]" .."style_type[button,item_image_button;bgcolor=#8f3000]" .."label[11.5,1.8;"..FS("Select a mode:").."]" .."button[11.5,2.1;2.5,0.8;single_w;"..FS("Singleplayer (white)").."]" .."button[11.5,2.95;2.5,0.8;single_b;"..FS("Singleplayer (black)").."]" .."button[11.5,4.1;2.5,0.8;multi;"..FS("Multiplayer").."]" .."label[2.2,0.652;"..minetest.colorize("#404040", FS("Select a game mode")).."]" .."label[2.2,10.21;"..minetest.colorize("#404040", FS("Select a game mode")).."]" local fs = [[ formspec_version[2] size[16,10.7563;] no_prepend[] bgcolor[#080808BB;true] background[0,0;16,10.7563;chess_bg.png;true] style_type[button,item_image_button;bgcolor=#8f3000] style_type[list;spacing=0.1;size=0.975] listcolors[#00000000;#00000000;#00000000;#30434C;#FFF] list[context;board;0.47,1.155;8,8;] tableoptions[background=#00000000;highlight=#00000000;border=false] ]] -- move; white piece; white halfmove; black piece; black halfmove .."tablecolumns[text;image," .. pieces_str .. ";text;image," .. pieces_str .. ";text]" local function add_move_to_moves_list(meta, pieceFrom, pieceTo, pieceTo_s, from_idx, to_idx, special) local moves_raw = meta:get_string("moves_raw") if moves_raw ~= "" then moves_raw = moves_raw .. ";" end if not special then special = "" end moves_raw = moves_raw .. pieceFrom .. "," .. pieceTo .. "," .. pieceTo_s .. "," .. from_idx .. "," .. to_idx .. "," .. special meta:set_string("moves_raw", moves_raw) end local function add_special_to_moves_list(meta, special) add_move_to_moves_list(meta, "", "", "", "", "", special) end -- Create the full formspec string for the sequence of moves. -- Uses Figurine Algebraic Notation. local function get_moves_formstring(meta) local moves_raw = meta:get_string("moves_raw") if moves_raw == "" then return ","..MOVES_LIST_SYMBOL_EMPTY..",,"..MOVES_LIST_SYMBOL_EMPTY.."," end local moves_split = string.split(moves_raw, ";") local moves_out = "" local move_no = 0 for m=1, #moves_split do local move_split = string.split(moves_split[m], ",", true) local pieceFrom = move_split[1] local pieceTo = move_split[2] local pieceTo_s = move_split[3] local from_idx = tonumber(move_split[4]) local to_idx = tonumber(move_split[5]) local special = move_split[6] -- true if White plays, false if Black plays local curPlayerIsWhite = m % 2 == 1 if special == "whiteWon" or special == "blackWon" or special == "draw" then if not curPlayerIsWhite then moves_out = moves_out .. ""..MOVES_LIST_SYMBOL_EMPTY..",," end end if special == "whiteWon" then moves_out = moves_out .. ","..MOVES_LIST_SYMBOL_EMPTY..",1–0,"..MOVES_LIST_SYMBOL_EMPTY elseif special == "blackWon" then moves_out = moves_out .. ","..MOVES_LIST_SYMBOL_EMPTY..",0–1,"..MOVES_LIST_SYMBOL_EMPTY elseif special == "draw" then moves_out = moves_out .. ","..MOVES_LIST_SYMBOL_EMPTY..",½–½,"..MOVES_LIST_SYMBOL_EMPTY else local from_x, from_y = index_to_xy(from_idx) local to_x, to_y = index_to_xy(to_idx) local pieceFrom_s = pieceFrom:match(":(%w+_%w+)") local pieceFrom_si_id -- Show no piece icon for pawn if pieceFrom:sub(11,14) == "pawn" then pieceFrom_si_id = MOVES_LIST_SYMBOL_EMPTY else pieceFrom_si_id = pieces_str:match("(%d+)=" .. pieceFrom_s) end local pieceTo_si_id = pieceTo_s ~= "" and pieces_str:match("(%d+)=" .. pieceTo_s) or "" local coordFrom = letters[from_x + 1] .. math.abs(from_y - 8) local coordTo = letters[to_x + 1] .. math.abs(to_y - 8) if curPlayerIsWhite then move_no = move_no + 1 -- Add move number (e.g. " 3.") moves_out = moves_out .. string.format("% d.", move_no) .. "," end local betweenCoordsSymbol = "–" -- to be inserted between source and destination coords -- dash for normal moves, × for capturing moves local enPassantSymbol = "" -- symbol for en passant captures if pieceTo ~= "" then -- normal capture betweenCoordsSymbol = "×" elseif pieceTo == "" and pieceFrom:sub(11,14) == "pawn" and from_x ~= to_x then -- 'en passant' capture betweenCoordsSymbol = "×" enPassantSymbol = " e. p." end ---- Add halfmove of current player -- Castling local castling = false if pieceFrom:sub(11,14) == "king" and ((curPlayerIsWhite and from_y == 7 and to_y == 7) or (not curPlayerIsWhite and from_y == 0 and to_y == 0)) then -- queenside castling if from_x == 4 and to_x == 2 then -- write "0–0–0" moves_out = moves_out .. MOVES_LIST_SYMBOL_EMPTY .. ",0–0–0" castling = true -- kingside castling elseif from_x == 4 and to_x == 6 then -- write "0–0" moves_out = moves_out .. MOVES_LIST_SYMBOL_EMPTY .. ",0–0" castling = true end end -- Normal halfmove if not castling then moves_out = moves_out .. pieceFrom_si_id .. "," .. -- piece image ID coordFrom .. betweenCoordsSymbol .. coordTo .. -- coords in long algebraic notation, e.g. "e2e3" enPassantSymbol -- written in case of an 'en passant' capture end -- If White moved, fill up the rest of the row with empty space. -- Required for validity of the table if curPlayerIsWhite and m == #moves_split then moves_out = moves_out .. "," .. MOVES_LIST_SYMBOL_EMPTY end end if m ~= #moves_split then moves_out = moves_out .. "," end end return moves_out end local function add_to_eaten_list(meta, pieceTo, pieceTo_s) local eaten = meta:get_string("eaten") if pieceTo ~= "" then eaten = eaten .. pieceTo_s .. "," end meta:set_string("eaten", eaten) end local function get_eaten_formstring(meta) local eaten = meta:get_string("eaten") local eaten_t = string.split(eaten, ",") local eaten_img = "" local a, b = 0, 0 for i = 1, #eaten_t do local is_white = eaten_t[i]:sub(-5,-1) == "white" local X = (is_white and a or b) % 4 local Y = ((is_white and a or b) % 16 - X) / 4 if is_white then a = a + 1 else b = b + 1 end eaten_img = eaten_img .. "image[" .. ((X + (is_white and 12.82 or 9.72)) - (X * 0.44)) .. "," .. ((Y + 6) - (Y * 0.12)) .. ";1,1;" .. eaten_t[i] .. ".png]" end return eaten_img end local function update_formspec(meta) local black_king_attacked = meta:get_string("blackAttacked") == "true" local white_king_attacked = meta:get_string("whiteAttacked") == "true" local playerWhite = meta:get_string("playerWhite") local playerBlack = meta:get_string("playerBlack") local moves_raw = meta:get_string("moves_raw") local moves = get_moves_formstring(meta) local eaten_img = get_eaten_formstring(meta) local lastMove = meta:get_string("lastMove") local gameResult = meta:get_string("gameResult") local grReason = meta:get_string("gameResultReason") -- arrow to show whose turn it is local blackArr = (gameResult == "" and lastMove == "white" and "image[1.2,0.252;0.7,0.7;chess_turn_black.png]") or "" local whiteArr = (gameResult == "" and (lastMove == "" or lastMove == "black") and "image[1.2,9.81;0.7,0.7;chess_turn_white.png]") or "" local turnBlack = minetest.colorize("#000001", playerBlack) local turnWhite = minetest.colorize("#000001", playerWhite) -- several status words for the player -- player is in check local check_s = minetest.colorize("#FF8000", "["..S("check").."]") -- player has been checkmated local mate_s = minetest.colorize("#FF0000", "["..S("checkmate").."]") -- player has resigned local resign_s = minetest.colorize("#FF0000", "["..S("resigned").."]") -- player has won local win_s = minetest.colorize("#26AB2B", "["..S("winner").."]") -- player has lost local lose_s = minetest.colorize("#FF0000", "["..S("loser").."]") -- player has a draw local draw_s = minetest.colorize("#FF00FF", "["..S("draw").."]") local mrsplit = string.split(moves_raw, ";") local m_sel_idx = math.ceil(#mrsplit / 2) local status_black = "" local status_white = "" if gameResult == "blackWon" then if grReason == "resign" then status_white = " " .. resign_s elseif grReason == "checkmate" then status_white = " " .. mate_s else status_white = " " .. lose_s end status_black = " " .. win_s elseif gameResult == "draw" then status_black = " " .. draw_s status_white = " " .. draw_s elseif gameResult == "whiteWon" then if grReason == "resign" then status_black = " " .. resign_s elseif grReason == "checkmate" then status_black = " " .. mate_s else status_black = " " .. lose_s end status_white = " " .. win_s else if black_king_attacked then status_black = " " .. check_s end if white_king_attacked then status_white = " " .. check_s end end local promotion = meta:get_string("promotionActive") local promotion_formstring = "" if promotion == "black" then eaten_img = "" promotion_formstring = "label[10.1,6.35;"..FS("PROMOTION\nFOR BLACK!").."]" .. "animated_image[10.05,7.2;2,2;p_img_white;pawn_black_promo_anim.png;5;100]" .. "label[13.15,6.35;"..FS("Promote pawn to:").."]" .. "item_image_button[13.15,7.2;1,1;realchess:queen_black;p_queen_black;]" .. "item_image_button[14.15,7.2;1,1;realchess:rook_black_1;p_rook_black;]" .. "item_image_button[13.15,8.2;1,1;realchess:bishop_black_1;p_bishop_black;]" .. "item_image_button[14.15,8.2;1,1;realchess:knight_black_1;p_knight_black;]" elseif promotion == "white" then eaten_img = "" promotion_formstring = "label[10.1,6.35;"..FS("PROMOTION\nFOR WHITE!").."]" .. "animated_image[10.05,7.2;2,2;p_img_white;pawn_white_promo_anim.png;5;100]" .. "label[13.15,6.35;"..FS("Promote pawn to:").."]" .. "item_image_button[13.15,7.2;1,1;realchess:queen_white;p_queen_white;]" .. "item_image_button[14.15,7.2;1,1;realchess:rook_white_1;p_rook_white;]" .. "item_image_button[13.15,8.2;1,1;realchess:bishop_white_1;p_bishop_white;]" .. "item_image_button[14.15,8.2;1,1;realchess:knight_white_1;p_knight_white;]" end local game_buttons if gameResult == "" and (playerWhite ~= "" and playerBlack ~= "") then game_buttons = "button[13.36,0.26;2,0.8;resign;"..FS("Resign").."]" else game_buttons = "button[13.36,0.26;2,0.8;new;"..FS("New game").."]" end local formspec = fs .. "label[2.2,0.652;" .. turnBlack .. minetest.formspec_escape(status_black) .. "]" .. blackArr .. "label[2.2,10.21;" .. turnWhite .. minetest.formspec_escape(status_white) .. "]" .. whiteArr .. "table[9.9,1.25;5.45,4;moves;" .. moves .. ";"..m_sel_idx.."]" .. promotion_formstring .. eaten_img .. game_buttons meta:set_string("formspec", formspec) end local function update_game_result(meta) local inv = meta:get_inventory() local board_t = board_to_table(inv) local playerWhite = meta:get_string("playerWhite") local playerBlack = meta:get_string("playerBlack") update_formspec(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 b = 0 for k,v in pairs(blackMoves) do b = b + 1 blackCanMove = true end b = 0 for k,v in pairs(whiteMoves) do b = b + 1 whiteCanMove = true end -- assume lastMove was updated *after* the player moved local lastMove = meta:get_string("lastMove") if lastMove == "white" and not blackCanMove then if meta:get_string("blackAttacked") == "true" then -- black was checkmated meta:set_string("gameResult", "whiteWon") meta:set_string("gameResultReason", "checkmate") add_special_to_moves_list(meta, "whiteWon") minetest.chat_send_player(playerWhite, chat_prefix .. S("You have checkmated @1. You win!", playerBlack)) minetest.chat_send_player(playerBlack, chat_prefix .. S("You were checkmated by @1. You lose!", playerWhite)) minetest.log("action", "[xdecor] Chess: "..playerWhite.." won against "..playerBlack.." by checkmate") return else -- stalemate meta:set_string("gameResult", "draw") meta:set_string("gameResultReason", "stalemate") add_special_to_moves_list(meta, "draw") minetest.chat_send_player(playerWhite, chat_prefix .. S("The game ended up in a stalemate! It's a draw!")) if playerWhite ~= playerBlack then minetest.chat_send_player(playerBlack, chat_prefix .. S("The game ended up in a stalemate! It's a draw!")) end minetest.log("action", "[xdecor] Chess: A game between "..playerWhite.." and "..playerBlack.." ended in a stalemate") return end end if lastMove == "black" and not whiteCanMove then if meta:get_string("whiteAttacked") == "true" then -- white was checkmated meta:set_string("gameResult", "blackWon") meta:set_string("gameResultReason", "checkmate") add_special_to_moves_list(meta, "blackWon") minetest.chat_send_player(playerBlack, chat_prefix .. S("You have checkmated @1. You win!", playerWhite)) minetest.chat_send_player(playerWhite, chat_prefix .. S("You were checkmated by @1. You lose!", playerBlack)) minetest.log("action", "[xdecor] Chess: "..playerBlack .." won against "..playerWhite.." by checkmate") return else -- stalemate meta:set_string("gameResult", "draw") meta:set_string("gameResultReason", "stalemate") add_special_to_moves_list(meta, "draw") minetest.chat_send_player(playerWhite, chat_prefix .. S("The game ended up in a stalemate! It's a draw!")) if playerWhite ~= playerBlack then minetest.chat_send_player(playerBlack, chat_prefix .. S("The game ended up in a stalemate! It's a draw!")) end minetest.log("action", "[xdecor] Chess: A game between "..playerWhite.." and "..playerBlack.." ended in a stalemate") return end end end function realchess.init(pos) local meta = minetest.get_meta(pos) local inv = meta:get_inventory() meta:set_string("formspec", fs_init) meta:set_string("infotext", S("Chess Board")) meta:set_string("playerBlack", "") meta:set_string("playerWhite", "") meta:set_string("aiColor", "") meta:set_string("lastMove", "") meta:set_string("gameResult", "") meta:set_string("gameResultReason", "") meta:set_string("blackAttacked", "") meta:set_string("whiteAttacked", "") meta:set_string("promotionActive", "") meta:set_int("lastMoveTime", 0) meta:set_int("castlingBlackL", 1) meta:set_int("castlingBlackR", 1) meta:set_int("castlingWhiteL", 1) meta:set_int("castlingWhiteR", 1) meta:set_int("promotionPawnFromIdx", 0) meta:set_int("promotionPawnToIdx", 0) meta:set_string("moves_raw", "") meta:set_string("eaten", "") meta:set_string("mode", "") inv:set_list("board", pieces) inv:set_size("board", 64) -- Clear legacy metadata meta:set_string("moves", "") meta:set_string("eaten_img", "") end -- The move logic of Chess. -- This is meant to be called when the player *ATTEMPTS* to move a piece -- from one slot of the inventory to another one and reacts accordingly. -- If the move is valid, the inventory is changed to reflect the new -- situation. If the move is invalid, nothing happens. -- -- * pos: Chessboard node pos -- * from_list: Inventory list of source square -- * from_index: Inventory index of source square -- * to_list: Inventory list of destination square -- * to_index: Inventory list of destination square function realchess.move(pos, from_list, from_index, to_list, to_index, player) if from_list ~= "board" and to_list ~= "board" then return end local meta = minetest.get_meta(pos) local promo = meta:get_string("promotionActive") if promo ~= "" then -- Can't move when waiting for selecting a pawn promotion return end local gameResult = meta:get_string("gameResult") if gameResult ~= "" then -- No moves if game is over return end local playerName = player:get_player_name() local inv = meta:get_inventory() local pieceFrom = inv:get_stack(from_list, from_index):get_name() local pieceTo = inv:get_stack(to_list, to_index):get_name() local lastMove = meta:get_string("lastMove") local playerWhite = meta:get_string("playerWhite") local playerBlack = meta:get_string("playerBlack") local thisMove -- Will replace lastMove when move is legal if pieceFrom:find("white") then if pieceTo:find("white") then -- Don't replace pieces of same color return end if lastMove == "white" then -- let the other invocation decide in case of a capture return pieceTo == "" and 0 or 1 end if playerWhite ~= "" and playerWhite ~= playerName then minetest.chat_send_player(playerName, chat_prefix .. S("Someone else plays white pieces!")) return end playerWhite = playerName thisMove = "white" elseif pieceFrom:find("black") then if pieceTo:find("black") then -- Don't replace pieces of same color return end if lastMove == "black" then -- let the other invocation decide in case of a capture return end if playerBlack ~= "" and playerBlack ~= playerName then minetest.chat_send_player(playerName, chat_prefix .. S("Someone else plays black pieces!")) return end if lastMove == "" then -- Nobody has moved yet, and Black cannot move first return end playerBlack = playerName thisMove = "black" end -- MOVE LOGIC local from_x, from_y = index_to_xy(from_index) local to_x, to_y = index_to_xy(to_index) local promotion = false -- PAWN if pieceFrom:sub(11,14) == "pawn" then if thisMove == "white" then local pawnWhiteMove = inv:get_stack(from_list, xy_to_index(from_x, from_y - 1)):get_name() -- white pawns can go up only if from_y - 1 == to_y then -- single step if from_x == to_x then if pieceTo ~= "" then return elseif to_index >= 1 and to_index <= 8 then -- activate promotion promotion = true meta:set_string("promotionActive", "white") meta:set_int("promotionPawnFromIdx", from_index) meta:set_int("promotionPawnToIdx", to_index) end elseif from_x - 1 == to_x or from_x + 1 == to_x then if to_index >= 1 and to_index <= 8 and pieceTo:find("black") then -- activate promotion promotion = true meta:set_string("promotionActive", "white") meta:set_int("promotionPawnFromIdx", from_index) meta:set_int("promotionPawnToIdx", to_index) end else return end elseif from_y - 2 == to_y then -- double step if pieceTo ~= "" or from_y < 6 or pawnWhiteMove ~= "" then return end -- store this double step in meta (needed for en passant check) local pawn_no = pieceFrom:sub(-1) local moves_raw = meta:get_string("moves_raw") local mrsplit = string.split(moves_raw, ";") local halfmove_no = #mrsplit + 1 meta:set_int("doublePawnStepW"..pawn_no, halfmove_no) else return end --[[ if x not changed ensure that destination cell is empty elseif x changed one unit left or right ensure the pawn is killing opponent piece else move is not legal - abort ]] if from_x == to_x then if pieceTo ~= "" then return end elseif from_x - 1 == to_x or from_x + 1 == to_x then -- capture local can_capture = false if pieceTo:find("black") then -- normal capture can_capture = true else -- en passant local enPassantPiece = inv:get_stack(to_list, xy_to_index(to_x, from_y)) local epp_meta = enPassantPiece:get_meta() local epp_name = enPassantPiece:get_name() if epp_name:find("black") and epp_name:sub(11,14) == "pawn" then local pawn_no = epp_name:sub(-1) local double_step_halfmove = meta:get_int("doublePawnStepB"..pawn_no) local current_halfmove = get_current_halfmove(meta) if double_step_halfmove ~= 0 and double_step_halfmove == current_halfmove then can_capture = true inv:set_stack(to_list, xy_to_index(to_x, from_y), "") end end end if not can_capture then return end else return end elseif thisMove == "black" then local pawnBlackMove = inv:get_stack(from_list, xy_to_index(from_x, from_y + 1)):get_name() -- black pawns can go down only if from_y + 1 == to_y then -- single step if from_x == to_x then if pieceTo ~= "" then return elseif to_index >= 57 and to_index <= 64 then -- activate promotion promotion = true meta:set_string("promotionActive", "black") meta:set_int("promotionPawnFromIdx", from_index) meta:set_int("promotionPawnToIdx", to_index) end elseif from_x - 1 == to_x or from_x + 1 == to_x then if to_index >= 57 and to_index <= 64 and pieceTo:find("white") then -- activate promotion promotion = true meta:set_string("promotionActive", "black") meta:set_int("promotionPawnFromIdx", from_index) meta:set_int("promotionPawnToIdx", to_index) end else return end elseif from_y + 2 == to_y then -- double step if pieceTo ~= "" or from_y > 1 or pawnBlackMove ~= "" then return end -- store this double step in meta (needed for en passant check) local pawn_no = pieceFrom:sub(-1) local moves_raw = meta:get_string("moves_raw") local mrsplit = string.split(moves_raw, ";") local halfmove_no = #mrsplit + 1 meta:set_int("doublePawnStepB"..pawn_no, halfmove_no) else return end --[[ if x not changed ensure that destination cell is empty elseif x changed one unit left or right ensure the pawn is killing opponent piece else move is not legal - abort ]] if from_x == to_x then if pieceTo ~= "" then return end elseif from_x - 1 == to_x or from_x + 1 == to_x then -- capture local can_capture = false if pieceTo:find("white") then -- normal capture can_capture = true else -- en passant local enPassantPiece = inv:get_stack(to_list, xy_to_index(to_x, from_y)) local epp_meta = enPassantPiece:get_meta() local epp_name = enPassantPiece:get_name() if epp_name:find("white") and epp_name:sub(11,14) == "pawn" then local pawn_no = epp_name:sub(-1) local double_step_halfmove = meta:get_int("doublePawnStepW"..pawn_no) local current_halfmove = get_current_halfmove(meta) if double_step_halfmove ~= 0 and double_step_halfmove == current_halfmove then can_capture = true inv:set_stack(to_list, xy_to_index(to_x, from_y), "") end end end if not can_capture then return end else return end else return end -- ROOK elseif pieceFrom:sub(11,14) == "rook" then if from_x == to_x then -- Moving vertically if from_y < to_y then -- Moving down -- Ensure that no piece disturbs the way for i = from_y + 1, to_y - 1 do if inv:get_stack(from_list, xy_to_index(from_x, i)):get_name() ~= "" then return end end else -- Moving up -- Ensure that no piece disturbs the way for i = to_y + 1, from_y - 1 do if inv:get_stack(from_list, xy_to_index(from_x, i)):get_name() ~= "" then return end end end elseif from_y == to_y then -- Moving horizontally if from_x < to_x then -- moving right -- ensure that no piece disturbs the way for i = from_x + 1, to_x - 1 do if inv:get_stack(from_list, xy_to_index(i, from_y)):get_name() ~= "" then return end end else -- Moving left -- Ensure that no piece disturbs the way for i = to_x + 1, from_x - 1 do if inv:get_stack(from_list, xy_to_index(i, from_y)):get_name() ~= "" then return end end end else -- Attempt to move arbitrarily -> abort return end if thisMove == "white" or thisMove == "black" then if pieceFrom:sub(-1) == "1" then meta:set_int("castlingWhiteL", 0) elseif pieceFrom:sub(-1) == "2" then meta:set_int("castlingWhiteR", 0) end end -- KNIGHT elseif pieceFrom:sub(11,16) == "knight" then -- Get relative pos local dx = from_x - to_x local dy = from_y - to_y -- Get absolute values if dx < 0 then dx = -dx end if dy < 0 then dy = -dy end -- Sort x and y if dx > dy then dx, dy = dy, dx end -- Ensure that dx == 1 and dy == 2 if dx ~= 1 or dy ~= 2 then return end -- Just ensure that destination cell does not contain friend piece -- ^ It was done already thus everything ok -- BISHOP elseif pieceFrom:sub(11,16) == "bishop" then -- Get relative pos local dx = from_x - to_x local dy = from_y - to_y -- Get absolute values if dx < 0 then dx = -dx end if dy < 0 then dy = -dy end -- Ensure dx and dy are equal if dx ~= dy then return end if from_x < to_x then if from_y < to_y then -- Moving right-down -- Ensure that no piece disturbs the way for i = 1, dx - 1 do if inv:get_stack( from_list, xy_to_index(from_x + i, from_y + i)):get_name() ~= "" then return end end else -- Moving right-up -- Ensure that no piece disturbs the way for i = 1, dx - 1 do if inv:get_stack( from_list, xy_to_index(from_x + i, from_y - i)):get_name() ~= "" then return end end end else if from_y < to_y then -- Moving left-down -- Ensure that no piece disturbs the way for i = 1, dx - 1 do if inv:get_stack( from_list, xy_to_index(from_x - i, from_y + i)):get_name() ~= "" then return end end else -- Moving left-up -- ensure that no piece disturbs the way for i = 1, dx - 1 do if inv:get_stack( from_list, xy_to_index(from_x - i, from_y - i)):get_name() ~= "" then return end end end end -- QUEEN elseif pieceFrom:sub(11,15) == "queen" then local dx = from_x - to_x local dy = from_y - to_y -- Get absolute values if dx < 0 then dx = -dx end if dy < 0 then dy = -dy end -- Ensure valid relative move if dx ~= 0 and dy ~= 0 and dx ~= dy then return end if from_x == to_x then if from_y < to_y then -- Goes down -- Ensure that no piece disturbs the way for i = from_y + 1, to_y - 1 do if inv:get_stack(from_list, xy_to_index(from_x, i)):get_name() ~= "" then return end end else -- Goes up -- Ensure that no piece disturbs the way for i = to_y + 1, from_y - 1 do if inv:get_stack(from_list, xy_to_index(from_x, i)):get_name() ~= "" then return end end end elseif from_x < to_x then if from_y == to_y then -- Goes right -- Ensure that no piece disturbs the way for i = from_x + 1, to_x - 1 do if inv:get_stack(from_list, xy_to_index(i, from_y)):get_name() ~= "" then return end end elseif from_y < to_y then -- Goes right-down -- Ensure that no piece disturbs the way for i = 1, dx - 1 do if inv:get_stack( from_list, xy_to_index(from_x + i, from_y + i)):get_name() ~= "" then return end end else -- Goes right-up -- Ensure that no piece disturbs the way for i = 1, dx - 1 do if inv:get_stack( from_list, xy_to_index(from_x + i, from_y - i)):get_name() ~= "" then return end end end else if from_y == to_y then -- Goes left -- Ensure that no piece disturbs the way and destination cell does for i = to_x + 1, from_x - 1 do if inv:get_stack(from_list, xy_to_index(i, from_y)):get_name() ~= "" then return end end elseif from_y < to_y then -- Goes left-down -- Ensure that no piece disturbs the way for i = 1, dx - 1 do if inv:get_stack( from_list, xy_to_index(from_x - i, from_y + i)):get_name() ~= "" then return end end else -- Goes left-up -- Ensure that no piece disturbs the way for i = 1, dx - 1 do if inv:get_stack( from_list, xy_to_index(from_x - i, from_y - i)):get_name() ~= "" then return end end end end -- KING elseif pieceFrom:sub(11,14) == "king" then local dx = from_x - to_x local dy = from_y - to_y local check = true local inv = meta:get_inventory() local board = board_to_table(inv) if thisMove == "white" then if from_y == 7 and to_y == 7 then if to_x == 2 then local castlingWhiteL = meta:get_int("castlingWhiteL") local idx57 = inv:get_stack(from_list, 57):get_name() if castlingWhiteL == 1 and idx57 == "realchess:rook_white_1" then for i = 58, from_index - 1 do if inv:get_stack(from_list, i):get_name() ~= "" then return end end for i = from_index, from_index - 2, -1 do if attacked("white", i, board) then return end end inv:set_stack(from_list, 57, "") inv:set_stack(from_list, 60, "realchess:rook_white_1") check = false end elseif to_x == 6 then local castlingWhiteR = meta:get_int("castlingWhiteR") local idx64 = inv:get_stack(from_list, 64):get_name() if castlingWhiteR == 1 and idx64 == "realchess:rook_white_2" then for i = from_index + 1, 63 do if inv:get_stack(from_list, i):get_name() ~= "" then return end end for i = from_index, from_index + 2, 1 do if attacked("white", i, board) then return end end inv:set_stack(from_list, 62, "realchess:rook_white_2") inv:set_stack(from_list, 64, "") check = false end end end elseif thisMove == "black" then if from_y == 0 and to_y == 0 then if to_x == 2 then local castlingBlackL = meta:get_int("castlingBlackL") local idx1 = inv:get_stack(from_list, 1):get_name() if castlingBlackL == 1 and idx1 == "realchess:rook_black_1" then for i = 2, from_index - 1 do if inv:get_stack(from_list, i):get_name() ~= "" then return end end for i = from_index, from_index - 2, -1 do if attacked("black", i, board) then return end end inv:set_stack(from_list, 1, "") inv:set_stack(from_list, 4, "realchess:rook_black_1") check = false end elseif to_x == 6 then local castlingBlackR = meta:get_int("castlingBlackR") local idx8 = inv:get_stack(from_list, 8):get_name() if castlingBlackR == 1 and idx8 == "realchess:rook_black_2" then for i = from_index + 1, 7 do if inv:get_stack(from_list, i):get_name() ~= "" then return end end for i = from_index, from_index + 2, 1 do if attacked("black", i, board) then return end end inv:set_stack(from_list, 6, "realchess:rook_black_2") inv:set_stack(from_list, 8, "") check = false end end end end if check then if dx < 0 then dx = -dx end if dy < 0 then dy = -dy end if dx > 1 or dy > 1 then return end end if thisMove == "white" then meta:set_int("castlingWhiteL", 0) meta:set_int("castlingWhiteR", 0) elseif thisMove == "black" then meta:set_int("castlingBlackL", 0) meta:set_int("castlingBlackR", 0) end end local board = board_to_table(inv) board[to_index] = board[from_index] board[from_index] = "" local black_king_idx, white_king_idx = locate_kings(board) if not black_king_idx or not white_king_idx then return end local blackAttacked = attacked("black", black_king_idx, board) local whiteAttacked = attacked("white", white_king_idx, board) if blackAttacked and thisMove == "black" then return end if whiteAttacked and thisMove == "white" then return end if not promotion then realchess.update_state(meta, from_index, to_index, thisMove) end if meta:get_string("playerWhite") == "" then meta:set_string("playerWhite", playerWhite) elseif meta:get_string("playerBlack") == "" then meta:set_string("playerBlack", playerBlack) end realchess.move_piece(meta, pieceFrom, from_list, from_index, to_list, to_index) return end local function ai_move(inv, meta) local board_t = board_to_table(inv) local lastMove = meta:get_string("lastMove") local gameResult = meta:get_string("gameResult") local aiColor = meta:get_string("aiColor") if aiColor == "" then aiColor = "black" end local opponentColor if aiColor == "black" then opponentColor = "white" else opponentColor = "black" end if (lastMove == opponentColor or (aiColor == "white" and lastMove == "")) and gameResult == "" then update_formspec(meta) local moves = get_theoretical_moves_for(meta, board_t, aiColor) 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 pieceTo_s = pieceTo ~= "" and pieceTo:match(":(%w+_%w+)") or "" local board = board_to_table(inv) local black_king_idx, white_king_idx = locate_kings(board) local ai_king_idx if aiColor == "black" then ai_king_idx = black_king_idx else ai_king_idx = white_king_idx end local aiAttacked = attacked(aiColor, ai_king_idx, board) local kingSafe = true local bestMoveSaveFrom, bestMoveSaveTo if aiAttacked then kingSafe = false meta:set_string(aiColor.."Attacked", "true") local save_moves = {} for from_idx, _ in pairs(moves) do for to_idx, value in pairs(_) do from_idx = tonumber(from_idx) local from_idx_bak, to_idx_bak = board[from_idx], board[to_idx] board[to_idx] = board[from_idx] board[from_idx] = "" black_king_idx, white_king_idx = locate_kings(board) if aiColor == "black" then ai_king_idx = black_king_idx else ai_king_idx = white_king_idx end if ai_king_idx then aiAttacked = attacked(aiColor, ai_king_idx, board) if not aiAttacked then save_moves[from_idx] = save_moves[from_idx] or {} save_moves[from_idx][to_idx] = value end end board[from_idx], board[to_idx] = from_idx_bak, to_idx_bak end end if next(save_moves) then bestMoveSaveFrom, bestMoveSaveTo = best_move(save_moves) end end minetest.after(1.0, function() local gameResult = meta:get_string("gameResult") if gameResult ~= "" then return end local lastMove = meta:get_string("lastMove") local lastMoveTime = meta:get_int("lastMoveTime") if lastMoveTime > 0 or lastMove == "" then if not kingSafe then if bestMoveSaveTo ~= nil then inv:set_stack("board", bestMoveSaveTo, board[bestMoveSaveFrom]) inv:set_stack("board", bestMoveSaveFrom, "") meta:set_string(aiColor.."Attacked", "") else return end else if aiColor == "black" and pieceFrom:find("pawn") and choice_to >= 57 and choice_to <= 64 then inv:set_stack("board", choice_to, "realchess:queen_black") elseif aiColor == "white" and pieceFrom:find("pawn") and choice_to >= 1 and choice_to <= 8 then inv:set_stack("board", choice_to, "realchess:queen_white") else inv:set_stack("board", choice_to, pieceFrom) end inv:set_stack("board", choice_from, "") end board = board_to_table(inv) black_king_idx, white_king_idx = locate_kings(board) local opponent_king_idx if opponentColor == "white" then opponent_king_idx = white_king_idx else opponent_king_idx = black_king_idx end local opponentAttacked = attacked(opponentColor, opponent_king_idx, board) if opponentAttacked then meta:set_string(opponentColor.."Attacked", "true") end if aiColor == "black" and meta:get_string("playerBlack") == "" then meta:set_string("playerBlack", AI_NAME) elseif aiColor == "white" and meta:get_string("playerWhite") == "" then meta:set_string("playerWhite", AI_NAME) end meta:set_string("lastMove", aiColor) meta:set_int("lastMoveTime", minetest.get_gametime()) add_move_to_moves_list(meta, pieceFrom, pieceTo, pieceTo_s, choice_from, choice_to) add_to_eaten_list(meta, pieceTo, pieceTo_s) update_game_result(meta) update_formspec(meta) else end end) else update_formspec(meta) end end local function timeout_format(timeout_limit) local time_remaining = timeout_limit - minetest.get_gametime() local minutes = math.floor(time_remaining / 60) local seconds = time_remaining % 60 if minutes == 0 then -- number of seconds return S("@1 s", seconds) end -- number of minutes and seconds return S("@1 min @2 s", minutes, seconds) end function realchess.fields(pos, _, fields, sender) local playerName = sender:get_player_name() local meta = minetest.get_meta(pos) local timeout_limit = meta:get_int("lastMoveTime") + 300 local playerWhite = meta:get_string("playerWhite") local playerBlack = meta:get_string("playerBlack") local lastMoveTime = meta:get_int("lastMoveTime") if fields.quit then return end if fields.single_w or fields.single_b or fields.multi then meta:set_string("mode", ((fields.single_w or fields.single_b) and "single" or "multi")) if fields.single_w then meta:set_string("aiColor", "black") meta:set_string("playerBlack", AI_NAME) elseif fields.single_b then meta:set_string("aiColor", "white") meta:set_string("playerWhite", AI_NAME) local inv = meta:get_inventory() ai_move(inv, meta) end update_formspec(meta) return end -- Timeout is 5 min. by default for resetting the game (non-players only) -- Also allow instant reset before White and Black moved if fields.new then if (playerWhite == playerName or playerBlack == playerName or playerWhite == "" or playerBlack == "") then realchess.init(pos) elseif lastMoveTime > 0 then if minetest.get_gametime() >= timeout_limit and (playerWhite ~= playerName or playerBlack ~= playerName) then realchess.init(pos) else minetest.chat_send_player(playerName, chat_prefix .. S("You can't reset the chessboard, a game has been started. Try again in @1.", timeout_format(timeout_limit))) end end return end if fields.resign then local lastMove = meta:get_string("lastMove") if playerWhite == "" and playerBlack == "" or lastMove == "" then minetest.chat_send_player(playerName, chat_prefix .. S("Resigning is not possible yet.")) return end local winner, loser, whiteWon if playerWhite == playerBlack and playerWhite == playerName and playerWhite ~= "" then if lastMove == "black" then winner = playerBlack loser = playerWhite whiteWon = false else winner = playerWhite loser = playerBlack whiteWon = true end elseif playerName == playerWhite and playerWhite ~= "" then winner = playerBlack loser = playerWhite whiteWon = false elseif playerName == playerBlack and playerBlack ~= "" then winner = playerWhite loser = playerBlack 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") else meta:set_string("gameResult", "blackWon") add_special_to_moves_list(meta, "blackWon") end minetest.chat_send_player(loser, chat_prefix .. S("You have resigned.")) if playerWhite ~= playerBlack then minetest.chat_send_player(winner, chat_prefix .. S("@1 has resigned. You win!", loser)) end minetest.log("action", "[xdecor] Chess: "..loser.." has resigned from the game against "..winner) update_formspec(meta) else minetest.chat_send_player(playerName, chat_prefix .. S("You can't resign, you're not playing in this game.")) end return end local promotions = { "queen_white", "rook_white", "bishop_white", "knight_white", "queen_black", "rook_black", "bishop_black", "knight_black", } for p=1, #promotions do local promo = promotions[p] if fields["p_"..promo] then if not (playerName == playerWhite or playerName == playerBlack) then minetest.chat_send_player(playerName, chat_prefix .. S("You're only a spectator in this game of Chess.")) return end local pcolor = promo:sub(-5) local activePromo = meta:get_string("promotionActive") if activePromo == "" then minetest.chat_send_player(playerName, chat_prefix .. S("This isn't the time for promotion.")) return elseif activePromo ~= pcolor then minetest.chat_send_player(playerName, chat_prefix .. S("It's not your turn! This promotion is meant for the other player.")) return end if pcolor == "white" and playerName == playerWhite or pcolor == "black" and playerName == playerBlack then realchess.promote_pawn(meta, pcolor, promo:sub(1, -7)) return else minetest.chat_send_player(playerName, chat_prefix .. S("It's not your turn! This promotion is meant for the other player.")) return end end end end function realchess.dig(pos, player) if not player then return false end local meta = minetest.get_meta(pos) local playerName = player:get_player_name() local timeout_limit = meta:get_int("lastMoveTime") + 300 local lastMoveTime = meta:get_int("lastMoveTime") local playerWhite = meta:get_string("playerWhite") local playerBlack = meta:get_string("playerBlack") -- Timeout is 5 min. by default for digging the chessboard (non-players only) if (lastMoveTime == 0 and minetest.get_gametime() > timeout_limit) then return true else if playerName == playerWhite or playerName == playerBlack then minetest.chat_send_player(playerName, chat_prefix .. S("You can't dig the chessboard, a game has been started. " .. "Reset it first or dig it again in @1.", timeout_format(timeout_limit))) else minetest.chat_send_player(playerName, chat_prefix .. S("You can't dig the chessboard, a game has been started. " .. "Try it again in @1.", timeout_format(timeout_limit))) end return false end end -- Helper function for realchess.move. -- To be called when a valid normal move should be taken. -- Will also update the state for the Chessboard. function realchess.move_piece(meta, pieceFrom, from_list, from_index, to_list, to_index) local inv = meta:get_inventory() inv:set_stack(from_list, from_index, "") inv:set_stack(to_list, to_index, pieceFrom) local promo = meta:get_string("promotionActive") ~= "" if not promo then update_game_result(meta) end update_formspec(meta) local aiColor = meta:get_string("aiColor") if aiColor == "" then aiColor = "black" end local lastMove = meta:get_string("lastMove") if lastMove == "" then lastMove = "black" end -- The AI always plays black; make sure it doesn't move twice in the case of a swap: -- Only let it play if it didn't already play. if meta:get_string("mode") == "single" and lastMove ~= aiColor and meta:get_string("gameResult") == "" and not promo then ai_move(inv, meta) end end function realchess.update_state(meta, from_index, to_index, thisMove, promotionOverride) local inv = meta:get_inventory() local board = board_to_table(inv) local pieceTo = board[to_index] local pieceFrom = promotionOverride or board[from_index] local pieceTo_s = pieceTo ~= "" and pieceTo:match(":(%w+_%w+)") or "" if not promotionOverride then board[to_index] = board[from_index] board[from_index] = "" end local black_king_idx, white_king_idx = locate_kings(board) if not black_king_idx or not white_king_idx then return end local blackAttacked = attacked("black", black_king_idx, board) local whiteAttacked = attacked("white", white_king_idx, board) if blackAttacked then meta:set_string("blackAttacked", "true") else meta:set_string("blackAttacked", "") end if whiteAttacked then meta:set_string("whiteAttacked", "true") else meta:set_string("whiteAttacked", "") end local lastMove = thisMove meta:set_string("lastMove", lastMove) meta:set_int("lastMoveTime", minetest.get_gametime()) local pieceTo_s = pieceTo ~= "" and pieceTo:match(":(%w+_%w+)") or "" add_move_to_moves_list(meta, pieceFrom, pieceTo, pieceTo_s, from_index, to_index) add_to_eaten_list(meta, pieceTo, pieceTo_s) end function realchess.promote_pawn(meta, color, promoteTo) local inv = meta:get_inventory() local pstr = promoteTo .. "_" .. color local promoted = false local to_idx = meta:get_int("promotionPawnToIdx") local from_idx = meta:get_int("promotionPawnFromIdx") if to_idx < 1 or from_idx < 1 then return end if promoteTo ~= "queen" then pstr = pstr .. "_1" end local stack if color == "white" then stack = inv:get_stack("board", to_idx) if stack:get_name():sub(11,14) == "pawn" then inv:set_stack("board", to_idx, "realchess:"..pstr) promoted = true end elseif color == "black" then stack = inv:get_stack("board", to_idx) if stack:get_name():sub(11,14) == "pawn" then inv:set_stack("board", to_idx, "realchess:"..pstr) promoted = true end end if promoted then meta:set_string("promotionActive", "") meta:set_int("promotionPawnFromIdx", 0) meta:set_int("promotionPawnToIdx", 0) realchess.update_state(meta, from_idx, to_idx, color, stack:get_name()) update_formspec(meta) local aiColor = meta:get_string("aiColor") if aiColor == "" then aiColor = "black" end local lastMove = meta:get_string("lastMove") if lastMove == "" then lastMove = "black" end if meta:get_string("mode") == "single" and lastMove ~= aiColor and meta:get_string("gameResult") == "" then ai_move(inv, meta) end else minetest.log("error", "[xdecor] Chess: Could not find pawn to promote!") end end function realchess.blast(pos) minetest.remove_node(pos) end local chessboarddef = { description = S("Chess Board"), drawtype = "nodebox", paramtype = "light", paramtype2 = "facedir", inventory_image = "chessboard_top.png", wield_image = "chessboard_top.png", tiles = {"chessboard_top.png", "chessboard_top.png", "chessboard_sides.png"}, use_texture_alpha = ALPHA_OPAQUE, groups = {choppy=3, oddly_breakable_by_hand=2, flammable=3}, sounds = default.node_sound_wood_defaults(), node_box = {type = "fixed", fixed = {-.375, -.5, -.375, .375, -.4375, .375}}, sunlight_propagates = true, on_rotate = screwdriver.rotate_simple, } if ENABLE_CHESS_GAMES then -- Extend chess board node definition if chess games are enabled chessboarddef._tt_help = S("Play a game of Chess against another player or the computer") chessboarddef.on_blast = realchess.blast chessboarddef.can_dig = realchess.dig chessboarddef.on_construct = realchess.init chessboarddef.on_receive_fields = realchess.fields -- The move logic of Chess is here (at least for players) chessboarddef.allow_metadata_inventory_move = function(pos, from_list, from_index, to_list, to_index, _, player) -- Normally, the allow function is meant to just check if an inventory move -- is allowed but instead we abuse it to detect where the player is *attempting* -- to move pieces to. -- This function may manipulate the inventory. This is a bit dirty -- because this is not really what the allow function is meant to do. realchess.move(pos, from_list, from_index, to_list, to_index, player) -- We always return 0 to disable all *builtin* inventory moves, since -- we do it ourselves. This should be fine because there shouldn't be a -- conflict between this mod and Minetest then. return 0 end chessboarddef.allow_metadata_inventory_take = function() return 0 end chessboarddef.allow_metadata_inventory_put = function() return 0 end -- Note: There is no on_move function because we put the entire move handling -- into the allow function above. The reason for this is of Minetest's -- awkward behavior when swapping items. minetest.register_lbm({ label = "Re-initialize chessboard (enable Chess games)", name = "xdecor:chessboard_reinit", nodenames = {"realchess:chessboard"}, run_at_every_load = true, action = function(pos, node) -- Init chessboard only if neccessary local meta = minetest.get_meta(pos) if meta:get_string("formspec", "") then realchess.init(pos) end end, }) else minetest.register_lbm({ label = "Clear chessboard formspec+infotext+inventory (disable Chess games)", name = "xdecor:chessboard_clear", nodenames = {"realchess:chessboard"}, run_at_every_load = true, action = function(pos, node) local meta = minetest.get_meta(pos) meta:set_string("formspec", "") meta:set_string("infotext", "") local inv = meta:get_inventory() inv:set_size("board", 0) end, }) end minetest.register_node(":realchess:chessboard", chessboarddef) local function register_piece(name, white_desc, black_desc, count) for _, color in pairs({"black", "white"}) do if not count then minetest.register_craftitem(":realchess:" .. name .. "_" .. color, { description = (color == "black") and black_desc or white_desc, inventory_image = name .. "_" .. color .. ".png", stack_max = 1, groups = {not_in_creative_inventory=1} }) else for i = 1, count do minetest.register_craftitem(":realchess:" .. name .. "_" .. color .. "_" .. i, { description = (color == "black") and black_desc or white_desc, inventory_image = name .. "_" .. color .. ".png", stack_max = 1, groups = {not_in_creative_inventory=1} }) end end end end register_piece("pawn", S("White Pawn"), S("Black Pawn"), 8) register_piece("rook", S("White Rook"), S("Black Rook"), 2) register_piece("knight", S("White Knight"), S("Black Knight"), 2) register_piece("bishop", S("White Bishop"), S("Black Bishop"), 2) register_piece("queen", S("White Queen"), S("Black Queen")) register_piece("king", S("White King"), S("Black King")) -- Recipes minetest.register_craft({ output = "realchess:chessboard", recipe = { {"dye:black", "dye:white", "dye:black"}, {"stairs:slab_wood", "stairs:slab_wood", "stairs:slab_wood"} } })