xdecor-libre/src/chess.lua
2023-07-16 20:16:57 +02:00

2862 lines
85 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
-- Note: Asterisks added to avoid confusion with a player name
-- because asterisks are forbidden in player names.
local BOT_NAME = "*"..S("Weak Computer").."*"
local BOT_DELAY_MOVE = 1.0
local BOT_DELAY_PROMOTE = 1.0
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
-- If true, will show some hidden state for debugging purposes
local CHESS_DEBUG = false
local function index_to_xy(idx)
if not idx then
return nil
end
idx = idx - 1
local x = idx % 8
local y = math.floor((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
-- Given a board index (1..64), returns the color of the square at
-- this position: "light" or "dark".
-- Undefined behavior if given an invalid board index
function get_square_index_color(idx)
local x, y = index_to_xy(idx)
if not x then
return nil
end
if x % 2 == 0 then
if y % 2 == 0 then
return "light"
else
return "dark"
end
else
if y % 2 == 0 then
return "dark"
else
return "light"
end
end
end
local chat_prefix = minetest.colorize("#FFFF00", "["..S("Chess").."] ")
local chat_prefix_debug = minetest.colorize("#FFFF00", "["..S("Chess Debug").."] ")
local letters = {'a','b','c','d','e','f','g','h'}
local function index_to_notation(idx)
local x, y = index_to_xy(idx)
if not x or not y then
return "??"
end
local xstr = letters[x+1] or "?"
local ystr = tostring(9 - (y+1)) or "?"
return xstr .. ystr
end
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
local function get_current_fullmove(meta)
local moves_raw = meta:get_string("moves_raw")
local mrsplit = string.split(moves_raw, ";")
return math.floor(#mrsplit / 2)
end
-- Returns a FEN-style string to represent castling rights
-- Will return a sequence of K, Q, k and q, with each letter
-- representing castling rights:
-- * K: white kingside
-- * Q: white queenside
-- * k: black kingside
-- * q: black queenside
-- If all castling rights are gone, will return "-" instead.
-- The 4 arguments are booleans for each possible castling,
-- true means the castling is possible.
local function castling_to_string(white_kingside, white_queenside, black_kingside, black_queenside)
local s_castling = ""
if white_kingside then
s_castling = s_castling .. "K"
end
if white_queenside then
s_castling = s_castling .. "Q"
end
if black_kingside then
s_castling = s_castling .. "k"
end
if black_queenside then
s_castling = s_castling .. "q"
end
if s_castling == "" then
s_castling = "-"
end
return s_castling
end
-- Returns a FEN-style string to represent the state of a theoretically
-- possible en passant capture on the board (even if no pawn can actually
-- capture). If an en passant capture is possible, returns the square
-- coordinates in algebraic notation of the square the vulnerable pawn
-- has just crossed. If no en passant capture is possible, returns "-".
-- double_step is the board index of the square the vulnerable
-- pawn has just double-stepped to or 0 if there is no such pawn.
local function en_passant_to_string(double_step)
local s_en_passant = "-"
if double_step ~= 0 and double_step ~= nil then
-- write the square crossed by the pawn who made
-- the double step
local dsx, dsy = index_to_xy(double_step)
if dsy == 3 then
dsy = dsy - 1
else
dsy = dsy + 1
end
s_en_passant = index_to_notation(xy_to_index(dsx, dsy))
end
return s_en_passant
end
local function can_castle(meta, board, from_list, from_idx, to_idx)
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 kingColor
if kingPiece:find("black") then
kingColor = "black"
else
kingColor = "white"
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 },
-- white kingside
{ y = 7, to_x = 6, rook_idx = 64, rook_goal = 62, acheck_dir = 1, color = "white", meta = "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 },
-- black kingside
{ y = 0, to_x = 6, rook_idx = 8, rook_goal = 6, acheck_dir = 1, color = "black", meta = "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
-- Check if all squares between king and rook are empty
local empty_start, empty_end
if pc.acheck_dir == -1 then
-- queenside
empty_start = pc.rook_idx + 1
empty_end = from_idx - 1
else
-- kingside
empty_start = from_idx + 1
empty_end = pc.rook_idx - 1
end
for i = empty_start, empty_end do
if inv:get_stack(from_list, i):get_name() ~= "" then
return false
end
end
-- Check if square of king as well the squares that king must cross and reach
-- are NOT attacked
for i = from_idx, from_idx + 2 * pc.acheck_dir, pc.acheck_dir do
if attacked(kingColor, i, board) then
return false
end
end
return true, pc.rook_idx, pc.rook_goal, "realchess:rook_"..kingColor.."_"..pc.rook_id
end
end
end
return false
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
-- * 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
return true
end
return false
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 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 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
if can_capture_en_passant(meta, "black", xy_to_index(to_x, from_y)) then
can_capture = true
en_passant = true
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
if can_capture_en_passant(meta, "white", xy_to_index(to_x, from_y)) then
can_capture = true
en_passant = true
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 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 = board_to_table(inv)
king_board[to_idx] = king_board[from_idx]
king_board[from_idx] = ""
if attacked(color, to_idx, king_board) then
moves[to_idx] = nil
else
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
local cc = can_castle(meta, board, "board", from_idx, to_idx)
if not cc then
moves[to_idx] = nil
end
end
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 the bot.
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
-- Given a table of theoretical moves and the king of the player is attacked,
-- returns true if the player still has at least one move left,
-- return false otherwise.
-- 2nd return value ist table of save moves
-- * theoretical_moves: moves table returned by get_theoretical_moves_for()
-- * board: board table
-- * player: player color ("white" or "black")
local function has_king_safe_move(theoretical_moves, board, player)
local save_moves = {}
local s_board = table.copy(board)
for from_idx, _ in pairs(theoretical_moves) do
for to_idx, value in pairs(_) do
from_idx = tonumber(from_idx)
s_board[to_idx] = s_board[from_idx]
s_board[from_idx] = ""
local black_king_idx, white_king_idx = locate_kings(s_board)
local king_idx
if player == "black" then
king_idx = black_king_idx
else
king_idx = white_king_idx
end
if king_idx then
local playerAttacked = attacked(player, king_idx, s_board)
if not playerAttacked then
save_moves[from_idx] = save_moves[from_idx] or {}
save_moves[from_idx][to_idx] = value
end
end
end
end
if next(save_moves) then
return true, save_moves
else
return false
end
end
-- Given a chessboard, checks whether it is in a "dead position",
-- i.e. a position in which neither player would be able to checkmate.
-- This function does not cover all dead positions, but only
-- the most common ones.
-- NOT checked are dead posisions in which both sides can still move,
-- but cannot capture pieces or checkmate the king
-- Parameters
-- * board: Chessboard table
-- Returns true if the board is in a dead position, false otherwise.
local function is_dead_position(board)
-- Dead position by lack of material
local mat = {} -- material table to count pieces
-- white material
mat.w = {
-- piece counters
pawn = 0,
bishop = 0,
knight = 0,
rook = 0,
queen = 0,
-- for bishops, also record their square color
bishop_square_light = 0,
bishop_square_dark = 0,
}
-- black material
mat.b = table.copy(mat.w)
-- Count material for both players
for b=1, #board do
local piece = board[b]
if piece ~= "" then
local color
if piece:find("white") then
color = "w"
else
color = "b"
end
-- Count all pieces except kings because we can assume
-- the board always has 1 white and 1 black king
if piece:find("pawn") then
mat[color].pawn = mat[color].pawn + 1
elseif piece:find("bishop") then
mat[color].bishop = mat[color].bishop + 1
local sqcolor = get_square_index_color(b)
mat[color]["bishop_square_"..sqcolor] = mat[color]["bishop_square_"..sqcolor] + 1
elseif piece:find("knight") then
mat[color].knight = mat[color].knight + 1
elseif piece:find("rook") then
mat[color].rook = mat[color].rook + 1
elseif piece:find("queen") then
mat[color].queen = mat[color].queen + 1
end
end
end
-- Check well-known dead positions based on insufficient material.
-- If there is any rook, queen or pawn on the board, the material is sufficient.
if mat.w.rook == 0 and mat.w.queen == 0 and mat.w.pawn == 0 and
mat.b.rook == 0 and mat.b.queen == 0 and mat.b.pawn == 0 then
-- King against king
if mat.w.knight == 0 and mat.w.bishop == 0 and mat.b.knight == 0 and mat.b.bishop == 0 then
return true
-- King against king and bishop
elseif mat.w.knight == 0 and mat.b.knight == 0 and
((mat.w.bishop == 1 and mat.b.bishop == 0) or
(mat.w.bishop == 0 and mat.b.bishop == 1)) then
return true
-- King against king and knight
elseif mat.w.bishop == 0 and mat.b.bishop == 0 and
((mat.w.knight == 1 and mat.b.knight == 0) or
(mat.w.knight == 0 and mat.b.knight == 1)) then
return true
-- King and bishop against king and bishop,
-- and both bishops are on squares of the same color
elseif mat.w.knight == 0 and mat.b.knight == 0 and
(mat.w.bishop == 1 and mat.b.bishop == 1) and
(mat.w.bishop_square_color_light == mat.b.bishop_square_color_light) and
(mat.w.bishop_square_color_dark == mat.b.bishop_square_color_dark) then
return true
end
end
return false
end
-- Base names of all Chess pieces (with color)
local pieces_basenames = {
"pawn_white",
"rook_white",
"knight_white",
"bishop_white",
"queen_white",
"king_white",
"pawn_black",
"rook_black",
"knight_black",
"bishop_black",
"queen_black",
"king_black",
}
-- Initial positions of the pieces on the chessboard.
-- The pieces are specified as item names.
-- It starts a8, continues with b8, c8, etc. then continues with a7, b7, etc. etc.
local starting_grid = {
-- rank '8'
"realchess:rook_black_1", -- a8
"realchess:knight_black_1", -- b8
"realchess:bishop_black_1", -- c8
"realchess:queen_black", -- ...
"realchess:king_black",
"realchess:bishop_black_2",
"realchess:knight_black_2",
"realchess:rook_black_2",
-- rank '8'
-- rank '7'
"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",
-- ranks '6' thru '3'
'','','','','','','','',
'','','','','','','','',
'','','','','','','','',
'','','','','','','','',
-- rank '2'
"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",
-- rank '1'
"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"
}
-- Figurine image IDs and file names for the chess notation table.
-- Note: "figurine" refers to the chess notation icon, NOT the chess piece for playing.
local figurines_str = "", 0
local figurines_str_cnt = 0
local MOVES_LIST_SYMBOL_EMPTY = figurines_str_cnt
figurines_str = figurines_str .. MOVES_LIST_SYMBOL_EMPTY .. "=mailbox_blank16.png"
for i = 1, #pieces_basenames do
figurines_str_cnt = figurines_str_cnt + 1
local p = pieces_basenames[i]
figurines_str = figurines_str .. "," .. figurines_str_cnt .. "=chess_figurine_" .. p .. ".png"
end
local function get_figurine_id(piece_itemname)
local piece_s = piece_itemname:match(":(%w+_%w+)")
return figurines_str:match("(%d+)=chess_figurine_" .. piece_s)
end
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]
]]
-- table columns for Chess notation.
-- Columns: move no.; white piece; white halfmove; white promotion; black piece; black halfmove; black promotion
.."tablecolumns[" ..
"text,align=right;"..
"image," .. figurines_str .. ";text;image," .. figurines_str .. ";" ..
--"image,0=mailbox_blank16.png;" ..
"image," .. figurines_str .. ";text;image," .. figurines_str .. "]"
local function add_move_to_moves_list(meta, pieceFrom, pieceTo, 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 .. "," .. 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
-- Returns a list of all positions so far, for the purposes
-- of determining position equality under the "same position
-- repeated X times" draw rule.
-- Each possible position is uniquely identified by a string
-- so equal positions have the same string and unequal positons
-- have a different string.
-- A position string containts the following data:
-- * position of pieces on the board
-- * current player
-- * castling rights
-- * target coords of the square crossed by the pawn who
-- made a double step in the prvious turn (if any)
--
-- Patemeter: meta is the node metadata of the chessboard
--
-- NOTE: The FIDE Laws of Chess (Jan 2023, article 9.2.3.1)
-- are somewhat unclear about en passant here ... Is it important
-- the pawn was only theoretically vulnerable to being
-- captured en passant without being actually threatened
-- that way, or does it only count if the pawn was actually
-- threatened by another pawn to be captured that way?
-- This mod currently interprets the rule in the former way,
-- i.e. a double step by a pawn
local function get_positions_history(meta)
-- Turns a board table to a string.
-- The syntax is inspired by FEN but not identical.
-- It iterates through the table from start
-- to finish and turns every square to a character,
-- representing a piece or an empty square.
local function board_to_string(board)
local str = ""
for b=1, #board do
local piece = board[b]
local append
if piece == "" then
str = str .. "."
elseif piece:find("white") then
if piece:find("pawn") then
str = str .. "P"
elseif piece:find("bishop") then
str = str .. "B"
elseif piece:find("knight") then
str = str .. "N"
elseif piece:find("rook") then
str = str .. "R"
elseif piece:find("queen") then
str = str .. "Q"
elseif piece:find("king") then
str = str .. "K"
end
elseif piece:find("black") then
if piece:find("pawn") then
str = str .. "p"
elseif piece:find("bishop") then
str = str .. "b"
elseif piece:find("knight") then
str = str .. "n"
elseif piece:find("rook") then
str = str .. "r"
elseif piece:find("queen") then
str = str .. "q"
elseif piece:find("king") then
str = str .. "k"
end
end
end
return str
end
local moves_raw = meta:get_string("moves_raw")
local moves_split = string.split(moves_raw, ";")
local positions_list = ""
local board = table.copy(starting_grid)
local castling_state = { true, true, true, true }
local castling_str = castling_to_string(unpack(castling_state))
positions_list = {}
local current_player = "w"
local position_string = board_to_string(board) .. " " ..
current_player .. " " ..
castling_str .. " " ..
en_passant_to_string(nil)
table.insert(positions_list, position_string)
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 from_idx = tonumber(move_split[3])
local to_idx = tonumber(move_split[4])
local special = move_split[5]
if special == "" or special:sub(1,7) == "promo__" then
if current_player == "w" then
current_player = "b"
else
current_player = "w"
end
-- Piece movement
board[to_idx] = board[from_idx]
board[from_idx] = ""
-- Pawn promotion
if special:sub(1, 7) == "promo__" then
local promoSym = special:sub(8)
board[to_idx] = promoSym
end
local from_x, from_y = index_to_xy(from_idx)
local to_x, to_y = index_to_xy(to_idx)
if pieceFrom:sub(11,14) == "king" then
-- Castling (move rook)
if (from_y == 7 and to_y == 7) then
if (from_x == 4 and to_x == 2) then
board[60] = board[57]
board[57] = ""
elseif (from_x == 4 and to_x == 6) then
board[62] = board[64]
board[64] = ""
end
elseif (from_y == 0 and to_y == 0) then
if (from_x == 4 and to_x == 2) then
board[4] = board[1]
board[1] = ""
elseif (from_x == 4 and to_x == 6) then
board[6] = board[8]
board[8] = ""
end
end
-- Lose castling rights on any king move
if pieceFrom:find("white") then
castling_state[1] = false
castling_state[2] = false
else
castling_state[3] = false
castling_state[4] = false
end
-- Lose castling rights on lone rook move
elseif pieceFrom:sub(11,14) == "rook" then
if from_idx == 57 then
-- white queenside
castling_state[2] = false
elseif from_idx == 64 then
-- white kingside
castling_state[1] = false
elseif from_idx == 1 then
-- black queenside
castling_state[4] = false
elseif from_idx == 8 then
-- black kingside
castling_state[3] = false
end
end
local pawn_double_step_index
if pieceTo == "" and pieceFrom:sub(11,14) == "pawn" then
-- En passant (remove captured pawn)
if from_x ~= to_x then
local epp_y
if pieceFrom:find("white") then
epp_y = to_y + 1
else
epp_y = to_y - 1
end
board[xy_to_index(to_x, epp_y)] = ""
-- Double pawn step (record positoin)
elseif math.abs(from_y-to_y) == 2 then
pawn_double_step_index = to_idx
end
end
castling_str = castling_to_string(unpack(castling_state))
local position_string = board_to_string(board) .. " " ..
current_player .. " " ..
castling_str .. " " ..
en_passant_to_string(pawn_double_step_index)
table.insert(positions_list, position_string)
end
end
local p=#positions_list
return positions_list
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 "", 1
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 from_idx = tonumber(move_split[3])
local to_idx = tonumber(move_split[4])
local special = move_split[5]
-- 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..",," .. MOVES_LIST_SYMBOL_EMPTY .. ","
end
end
if special == "whiteWon" then
moves_out = moves_out .. ","..MOVES_LIST_SYMBOL_EMPTY..",10,"..MOVES_LIST_SYMBOL_EMPTY
move_no = move_no + 1
elseif special == "blackWon" then
moves_out = moves_out .. ","..MOVES_LIST_SYMBOL_EMPTY..",01,"..MOVES_LIST_SYMBOL_EMPTY
move_no = move_no + 1
elseif special == "draw" then
moves_out = moves_out .. ","..MOVES_LIST_SYMBOL_EMPTY..",½–½,"..MOVES_LIST_SYMBOL_EMPTY
move_no = move_no + 1
else
local from_x, from_y = index_to_xy(from_idx)
local to_x, to_y = index_to_xy(to_idx)
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 = get_figurine_id(pieceFrom)
end
local pieceTo_si_id = pieceTo ~= "" and get_figurine_id(pieceTo)
local coordFrom = index_to_notation(from_idx)
local coordTo = index_to_notation(to_idx)
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 "000"
moves_out = moves_out .. MOVES_LIST_SYMBOL_EMPTY .. ",000," .. MOVES_LIST_SYMBOL_EMPTY
castling = true
-- kingside castling
elseif from_x == 4 and to_x == 6 then
-- write "00"
moves_out = moves_out .. MOVES_LIST_SYMBOL_EMPTY .. ",00," .. MOVES_LIST_SYMBOL_EMPTY
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
-- Promotion?
if special:sub(1, 7) == "promo__" then
local promoSym = special:sub(8)
moves_out = moves_out .. get_figurine_id(promoSym)
else
moves_out = moves_out .. MOVES_LIST_SYMBOL_EMPTY
end
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 .. ",," .. MOVES_LIST_SYMBOL_EMPTY
end
end
if m ~= #moves_split then
moves_out = moves_out .. ","
end
end
return moves_out, move_no
end
local function add_to_eaten_list(meta, pieceTo)
local eaten = meta:get_string("eaten")
if pieceTo ~= "" then
local pieceTo_s = pieceTo:match(":(%w+_%w+)") or ""
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, mlistlen = 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 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 = ""
if gameResult == "" then
promotion = meta:get_string("promotionActive")
end
local promotion_formstring = ""
local botColor = meta:get_string("botColor")
-- Show promotion prompt to ask player to choose to which piece to promote a pawn to
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]"
if botColor ~= "black" then
-- Hide buttons if computer player promotes
promotion_formstring = promotion_formstring ..
"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;]"
end
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]"
if botColor ~= "white" then
-- Hide buttons if computer player promotes
promotion_formstring = promotion_formstring ..
"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
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 debug_formstring = ""
if CHESS_DEBUG then
-- Write a debug string in the formspec based on FEN
-- to show some hidden state.
-- It uses FEN syntax but without the piece positions
-- current player: b or w
local d_turn = "-"
if lastMove == "white" then
d_turn = "b"
elseif lastMove == "black" or lastMove == "" then
d_turn = "w"
end
-- castling rights
local d_castling = castling_to_string(
meta:get_int("castlingWhiteR") == 1,
meta:get_int("castlingWhiteL") == 1,
meta:get_int("castlingBlackR") == 1,
meta:get_int("castlingBlackL") == 1)
-- en passant possible?
local double_step = meta:get_int("prevDoublePawnStepTo")
local d_en_passant = en_passant_to_string(double_step)
-- The halfmove clock counts for how many consecutive halfmoves
-- have been made with no pawn advancing and no piece being captured
local d_halfmove_clock = meta:get_int("halfmoveClock")
-- fullmove starts at 1 and should count up every time black moves
local d_fullmove = tostring(get_current_fullmove(meta) + 1)
local debug_str = d_turn .. " " .. d_castling .. " " .. d_en_passant .. " " .. d_halfmove_clock .. " " .. d_fullmove
debug_formstring = "label[9.9,10.2;DEBUG: "..debug_str.."]"
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 .. ";"..mlistlen.."]" ..
promotion_formstring ..
eaten_img ..
game_buttons ..
debug_formstring
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
blackCanMove = true
b = b+1
end
b = 0
for k,v in pairs(whiteMoves) do
whiteCanMove = true
b = b+1
end
-- assume lastMove was updated *after* the player moved
local lastMove = meta:get_string("lastMove")
local black_king_idx, white_king_idx = locate_kings(board_t)
if not black_king_idx or not white_king_idx then
return
end
local checkPlayer, king_idx, checkMoves
if lastMove == "white" then
checkPlayer = "black"
checkMoves = blackMoves
king_idx = black_king_idx
else
checkPlayer = "white"
checkMoves = whiteMoves
king_idx = white_king_idx
end
-- King attacked? This reduces the list of available moves,
-- so remove these, too and check if there are still any left.
local isKingAttacked = attacked(checkPlayer, king_idx, board_t)
if isKingAttacked then
meta:set_string(checkPlayer.."Attacked", "true")
local is_safe = has_king_safe_move(checkMoves, board_t, checkPlayer)
-- If not safe moves left, player can't move
if not is_safe then
if checkPlayer == "black" then
blackCanMove = false
else
whiteCanMove = false
end
end
end
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 draw by 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 draw by stalemate")
return
end
end
-- Is this a dead position?
if is_dead_position(board_t) then
meta:set_string("gameResult", "draw")
meta:set_string("gameResultReason", "dead_position")
add_special_to_moves_list(meta, "draw")
minetest.chat_send_player(playerWhite, chat_prefix .. S("The game ended up in a dead position! It's a draw!"))
if playerWhite ~= playerBlack then
minetest.chat_send_player(playerBlack, chat_prefix .. S("The game ended up in dead position! It's a draw!"))
end
minetest.log("action", "[xdecor] Chess: A game between "..playerWhite.." and "..playerBlack.." ended in a draw by dead position")
end
-- 75-moves rule. Automatically draw game if the last 75 moves of EACH player (thus 150 halfmoves)
-- neither moved a pawn or captured a piece.
-- Important: This rule MUST be checked AFTER checkmate because checkmate takes precedence.
if meta:get_int("halfmoveClock") >= 150 then
meta:set_string("gameResult", "draw")
meta:set_string("gameResultReason", "75_moves_rule")
add_special_to_moves_list(meta, "draw")
local msg = S("No piece was captured and no pawn was moved for 75 consecutive moves of each player. It's a draw!")
minetest.chat_send_player(playerWhite, chat_prefix .. msg)
if playerWhite ~= playerBlack then
minetest.chat_send_player(playerBlack, chat_prefix .. msg)
end
minetest.log("action", "[xdecor] Chess: A game between "..playerWhite.." and "..playerBlack.." ended in a draw via the 75-move rule")
end
local repetitionDraw = false
local positions = get_positions_history(meta)
local positions_counter = {}
for p = 1, #positions do
local position = positions[p]
if positions_counter[position] == nil then
positions_counter[position] = 1
else
positions_counter[position] = positions_counter[position] + 1
end
if CHESS_DEBUG and p == #positions then
local msg = chat_prefix_debug .. "Current position: \"" .. position .. "\""
if positions_counter[position] > 1 then
msg = msg .. " (occurred "..positions_counter[position].." times)"
end
minetest.chat_send_player(playerWhite, msg)
if playerWhite ~= playerBlack then
minetest.chat_send_player(playerBlack, msg)
end
end
if positions_counter[position] == 5 then
repetitionDraw = true
break
end
end
if repetitionDraw then
meta:set_string("gameResult", "draw")
meta:set_string("gameResultReason", "same_position_5")
add_special_to_moves_list(meta, "draw")
local msg = S("The exact same position has occured 5 times. It's a draw!")
minetest.chat_send_player(playerWhite, chat_prefix .. msg)
if playerWhite ~= playerBlack then
minetest.chat_send_player(playerBlack, chat_prefix .. msg)
end
minetest.log("action", "[xdecor] Chess: A game between "..playerWhite.." and "..playerBlack.." ended in a draw because the same position has appeared 5 times")
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("botColor", "")
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_int("prevDoublePawnStepTo", 0)
meta:set_int("halfmoveClock", 0)
meta:set_string("moves_raw", "")
meta:set_string("eaten", "")
meta:set_string("mode", "")
inv:set_list("board", starting_grid)
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 a 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, and the game state and UI is upated as well and true
-- is returned.
-- If the move is invalid, nothing happens and false is returned.
-- Note: The move can also be done by a computer player.
--
-- * meta: Chessboard node metadata
-- * 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
-- * playerName: Name of player to move
function realchess.move(meta, from_list, from_index, to_list, to_index, playerName)
if from_list ~= "board" and to_list ~= "board" then
return false
end
local promo = meta:get_string("promotionActive")
if promo ~= "" then
-- Can't move when waiting for selecting a pawn promotion
return false
end
local gameResult = meta:get_string("gameResult")
if gameResult ~= "" then
-- No moves if game is over
return false
end
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 kingMoved = false
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 false
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 false
end
playerWhite = playerName
thisMove = "white"
elseif pieceFrom:find("black") then
if pieceTo:find("black") then
-- Don't replace pieces of same color
return false
end
if lastMove == "black" then
-- let the other invocation decide in case of a capture
return false
end
if playerBlack ~= "" and playerBlack ~= playerName then
minetest.chat_send_player(playerName, chat_prefix .. S("Someone else plays black pieces!"))
return false
end
if lastMove == "" then
-- Nobody has moved yet, and Black cannot move first
return false
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
local doublePawnStep = nil
local en_passant_target = nil
local lostCastlingRightRook = nil
local resetHalfmoveClock = 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 false
elseif to_index >= 1 and to_index <= 8 then
-- activate promotion
promotion = true
end
resetHalfmoveClock = true
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
end
resetHalfmoveClock = true
else
return false
end
elseif from_y - 2 == to_y then
-- double step
if pieceTo ~= "" or from_y < 6 or pawnWhiteMove ~= "" then
return false
end
-- store the destination of this double step in meta (needed for en passant check)
doublePawnStep = to_index
resetHalfmoveClock = true
else
return false
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 false
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
if can_capture_en_passant(meta, "black", xy_to_index(to_x, from_y)) then
can_capture = true
en_passant_target = xy_to_index(to_x, from_y)
end
end
if not can_capture then
return false
end
resetHalfmoveClock = true
else
return false
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 false
elseif to_index >= 57 and to_index <= 64 then
-- activate promotion
promotion = true
end
resetHalfmoveClock = true
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
end
resetHalfmoveClock = true
else
return false
end
elseif from_y + 2 == to_y then
-- double step
if pieceTo ~= "" or from_y > 1 or pawnBlackMove ~= "" then
return false
end
-- store the destination of this double step in meta (needed for en passant check)
doublePawnStep = to_index
resetHalfmoveClock = true
else
return false
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 false
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
if can_capture_en_passant(meta, "white", xy_to_index(to_x, from_y)) then
can_capture = true
en_passant_target = xy_to_index(to_x, from_y)
end
end
if not can_capture then
return false
end
resetHalfmoveClock = true
else
return false
end
else
return false
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 false
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 false
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 false
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 false
end
end
end
else
-- Attempt to move arbitrarily -> abort
return false
end
-- Lose castling right when moving rook
if thisMove == "white" then
if from_index == 57 then
-- queenside white rook
lostCastlingRightRook = "castlingWhiteL"
elseif from_index == 64 then
-- kingside white rook
lostCastlingRightRook = "castlingWhiteR"
end
elseif thisMove == "black" then
if from_index == 1 then
-- queenside black rook
lostCastlingRightRook = "castlingBlackL"
elseif from_index == 8 then
-- kingside black rook
lostCastlingRightRook = "castlingBlackR"
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 false
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 false 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 false
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 false
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 false
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 false
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 false
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 false
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 false
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 false
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 false
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 false
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 false
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 false
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 false
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)
-- Castling
local cc, rook_start, rook_goal, rook_name = can_castle(meta, board, from_list, from_index, to_index)
if cc then
inv:set_stack(from_list, rook_goal, rook_name)
inv:set_stack(from_list, rook_start, "")
check = false
kingMoved = true
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 false
end
end
kingMoved = true
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 false
end
local blackAttacked = attacked("black", black_king_idx, board)
local whiteAttacked = attacked("white", white_king_idx, board)
-- Refuse to move if it would put or leave the own king
-- under attack
if blackAttacked and thisMove == "black" then
return false
end
if whiteAttacked and thisMove == "white" then
return false
end
if pieceTo ~= "" then
resetHalfmoveClock = true
end
-- The halfmove clock counts the number of consecutive halfmoves
-- in which neither a pawn was moved nor a piece was captured.
if resetHalfmoveClock then
meta:set_int("halfmoveClock", 0)
else
meta:set_int("halfmoveClock", meta:get_int("halfmoveClock") + 1)
end
if en_passant_target then
inv:set_stack(to_list, en_passant_target, "")
end
if kingMoved and thisMove == "white" then
meta:set_int("castlingWhiteL", 0)
meta:set_int("castlingWhiteR", 0)
elseif kingMoved and thisMove == "black" then
meta:set_int("castlingBlackL", 0)
meta:set_int("castlingBlackR", 0)
elseif lostCastlingRightRook then
meta:set_int(lostCastlingRightRook, 0)
end
if promotion then
meta:set_string("promotionActive", thisMove)
meta:set_int("promotionPawnFromIdx", from_index)
meta:set_int("promotionPawnToIdx", to_index)
else
realchess.update_state(meta, from_index, to_index, thisMove)
end
if doublePawnStep then
meta:set_int("prevDoublePawnStepTo", doublePawnStep)
else
meta:set_int("prevDoublePawnStepTo", 0)
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 true
end
local function bot_move(inv, meta)
local board_t = board_to_table(inv)
local lastMove = meta:get_string("lastMove")
local gameResult = meta:get_string("gameResult")
local botColor = meta:get_string("botColor")
if botColor == "" then
botColor = "black"
end
local opponentColor
if botColor == "black" then
opponentColor = "white"
else
opponentColor = "black"
end
if (lastMove == opponentColor or (botColor == "white" and lastMove == "")) and gameResult == "" then
update_formspec(meta)
local moves = get_theoretical_moves_for(meta, board_t, botColor)
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 board = board_to_table(inv)
local black_king_idx, white_king_idx = locate_kings(board)
local bot_king_idx
if botColor == "black" then
bot_king_idx = black_king_idx
else
bot_king_idx = white_king_idx
end
local botAttacked = attacked(botColor, bot_king_idx, board)
local kingSafe = true
local bestMoveSaveFrom, bestMoveSaveTo
if botAttacked then
kingSafe = false
meta:set_string(botColor.."Attacked", "true")
local is_safe, safe_moves = has_king_safe_move(moves, board, botColor)
if is_safe then
bestMoveSaveFrom, bestMoveSaveTo = best_move(safe_moves)
end
end
minetest.after(BOT_DELAY_MOVE, function()
local gameResult = meta:get_string("gameResult")
if gameResult ~= "" then
return
end
local lastMove = meta:get_string("lastMove")
local lastMoveTime = meta:get_int("lastMoveTime")
if lastMoveTime > 0 or lastMove == "" then
if botColor == "black" and meta:get_string("playerBlack") == "" then
meta:set_string("playerBlack", BOT_NAME)
elseif botColor == "white" and meta:get_string("playerWhite") == "" then
meta:set_string("playerWhite", BOT_NAME)
end
local moveOK = false
if not kingSafe then
-- Make a move to put the king out of check
if bestMoveSaveTo ~= nil then
moveOK = realchess.move(meta, "board", bestMoveSaveFrom, "board", bestMoveSaveTo, BOT_NAME)
if not moveOK then
minetest.log("error", "[xdecor] Chess: Bot tried to make an invalid move (to protect the king) from "..
index_to_notation(bestMoveSaveFrom).." to "..index_to_notation(bestMoveSaveTo))
end
else
return
end
else
-- Make a regular move
moveOK = realchess.move(meta, "board", choice_from, "board", choice_to, BOT_NAME)
if not moveOK then
minetest.log("error", "[xdecor] Chess: Bot tried to make an invalid move from "..
index_to_notation(choice_from).." to "..index_to_notation(choice_to))
end
end
-- Bot resigns if it made an incorrect move
if not moveOK then
meta:set_string("gameResultReason", "resign")
if botColor == "black" 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
update_formspec(meta)
end
end
end)
else
update_formspec(meta)
end
end
local function bot_promote(inv, meta, pawnIndex)
minetest.after(BOT_DELAY_MOVE, function()
local botColor = meta:get_string("botColor")
-- Always promote to queen
realchess.promote_pawn(meta, botColor, "queen")
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")
local gameResult = meta:get_int("gameResult")
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("botColor", "black")
meta:set_string("playerBlack", BOT_NAME)
elseif fields.single_b then
meta:set_string("botColor", "white")
meta:set_string("playerWhite", BOT_NAME)
local inv = meta:get_inventory()
bot_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 gameResult = meta:get_string("gameResult")
if gameResult ~= "" then
return
end
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 botColor = meta:get_string("botColor")
if botColor == "" then botColor = "black" end
local lastMove = meta:get_string("lastMove")
if lastMove == "" then lastMove = "black" end
-- Let the bot play when it its turn
if meta:get_string("mode") == "single" and lastMove ~= botColor and meta:get_string("gameResult") == "" then
if not promo then
bot_move(inv, meta)
else
bot_promote(inv, meta, to_index)
end
end
end
function realchess.update_state(meta, from_index, to_index, thisMove, promoteFrom, promoteTo)
local inv = meta:get_inventory()
local board = board_to_table(inv)
local pieceTo = board[to_index]
local pieceFrom = promoteFrom or board[from_index]
if not promoteFrom 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 special
if promoteTo then
special = "promo__"..promoteTo
end
add_move_to_moves_list(meta, pieceFrom, pieceTo, from_index, to_index, special)
add_to_eaten_list(meta, pieceTo)
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
pstr = "realchess:" .. pstr
local promoteFrom
if color == "white" then
promoteFrom = inv:get_stack("board", to_idx)
if promoteFrom:get_name():sub(11,14) == "pawn" then
inv:set_stack("board", to_idx, pstr)
promoted = true
end
elseif color == "black" then
promoteFrom = inv:get_stack("board", to_idx)
if promoteFrom:get_name():sub(11,14) == "pawn" then
inv:set_stack("board", to_idx, 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, promoteFrom:get_name(), pstr)
update_formspec(meta)
local botColor = meta:get_string("botColor")
if botColor == "" then botColor = "black" end
local lastMove = meta:get_string("lastMove")
if lastMove == "" then lastMove = "black" end
if meta:get_string("mode") == "single" and lastMove ~= botColor and meta:get_string("gameResult") == "" then
bot_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.
local meta = minetest.get_meta(pos)
local playerName
if player and player:is_player() then
playerName = player:get_player_name()
else
playerName = "<UNKNOWN PLAYER>"
minetest.log("error", "[xdecor] Chess: An unknown player tried to move a piece in the chessboard inventory")
end
realchess.move(meta, from_list, from_index, to_list, to_index, playerName)
-- 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"}
}
})