xdecor-libre/src/chess.lua

3222 lines
99 KiB
Lua
Raw Normal View History

2023-07-17 16:17:52 +03:00
-- Init stuff
if minetest.get_modpath("realchess") ~= nil then
-- If the 'realchess' mod was found, don't use any of this mod's Chess code
minetest.log("action", "[xdecor] 'realchess' mod detected. Disabling X-Decor-libre's Chess module in favor of realchess")
return
end
2023-07-17 16:54:07 +03:00
-- For making some functions available for the chessbot.
-- It is NOT recommended to use the public chess function outside of this mod!
xdecor.chess = {}
local realchess = xdecor.chess -- just an alias
-- Load chess bot code
2023-07-17 16:17:52 +03:00
local chessbot = dofile(minetest.get_modpath(minetest.get_current_modname()).."/src/chessbot.lua")
2023-07-17 16:54:07 +03:00
2023-07-17 16:17:52 +03:00
screwdriver = screwdriver or {}
-- Translation init
local S = minetest.get_translator("xdecor")
local NS = function(s) return s end
local FS = function(...) return minetest.formspec_escape(S(...)) end
2023-07-17 16:17:52 +03:00
-- 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
-- and enables a "Bot vs Bot" gamemode for testing the bot
local CHESS_DEBUG = false
2023-07-16 12:48:50 +03:00
-- Bot names
local BOT_NAME = NS("Weak Computer")
-- Bot names in Bot vs Bot mode
local BOT_NAME_1 = NS("Weak Computer 1")
local BOT_NAME_2 = NS("Weak Computer 2")
2023-07-16 12:48:50 +03:00
2023-07-17 14:50:03 +03:00
-- Timeout in seconds to allow resetting the game or digging the chessboard.
-- If no move was made for this time, everyone can reset the game
-- and remove the chessboard.
local TIMEOUT = 300
2023-07-17 16:17:52 +03:00
local ALPHA_OPAQUE = minetest.features.use_texture_alpha_string_modes and "opaque" or false
2023-07-16 13:55:07 +03:00
-- Returns the player name for the given player color.
-- In case of a bot player, will return a translated
-- bot name.
local function get_display_player_name(meta, playerColor)
local botColor = meta:get_string("botColor")
local displayName
if playerColor == botColor and (botColor == "white" or botColor == "black") then
return "*"..S(BOT_NAME).."*"
elseif botColor == "both" then
if playerColor == "white" then
return "*"..S(BOT_NAME_1).."*"
else
return "*"..S(BOT_NAME_2).."*"
end
elseif playerColor == "white" then
return meta:get_string("playerWhite")
elseif playerColor == "black" then
return meta:get_string("playerBlack")
else
return ""
end
end
2016-02-18 00:53:00 +03:00
local function index_to_xy(idx)
2019-07-14 01:48:36 +03:00
if not idx then
return nil
end
2019-07-23 15:03:20 +03:00
2016-02-18 00:53:00 +03:00
idx = idx - 1
2019-07-23 15:03:20 +03:00
2016-02-18 00:53:00 +03:00
local x = idx % 8
local y = math.floor((idx - x) / 8)
2018-08-30 22:30:04 +03:00
return x, y
end
local function xy_to_index(x, y)
return x + y * 8 + 1
end
2018-08-24 20:38:49 +03:00
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
local 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").."] ")
-- Send a chat message to a player with a prefix.
-- If you pass playerColor and botColor, this function will also
-- check if the player is a bot, in which case the message
-- is not sent.
-- If you only pass playerName and message, the message will always
-- be sent (if the player exists).
--
-- Parameters:
-- * playerName: Name of player to send message to (can be a bot name)
-- * message: The message text
-- * playerColor: "white", "black" or nil (if color is irrelevant)
-- * botColor: optional color of the bot(s):
-- * "white", "black": white or black
-- * "both": Both players are bots
-- * "": No player is a bot (default)
-- * isDebug: if true, message will only be shown in Chess Debug Mode (default: false)
local function send_message(playerName, message, playerColor, botColor, isDebug)
local prefix
if isDebug then
if not CHESS_DEBUG then
return
end
prefix = chat_prefix_debug
else
prefix = chat_prefix
end
minetest.chat_send_player(playerName, prefix .. message)
end
-- Send a message to both players of the chess game (except bots, if botColor is provided)
-- * playerName1: White player name
-- * playerName2: Black player name
-- * message: Message text
-- * botColor: See `send_message`
-- * isDebug: See `send_message`
local function send_message_2(playerName1, playerName2, message, botColor, isDebug)
send_message(playerName1, message, "white", botColor, isDebug)
if playerName2 ~= playerName1 then
send_message(playerName2, message, "black", botColor, isDebug)
end
end
local notation_letters = {'a','b','c','d','e','f','g','h'}
2023-07-17 16:17:52 +03:00
function realchess.index_to_notation(idx)
2023-07-14 18:30:40 +03:00
local x, y = index_to_xy(idx)
if not x or not y then
2023-07-14 18:30:40 +03:00
return "??"
end
local xstr = notation_letters[x+1] or "?"
local ystr = tostring(9 - (y+1)) or "?"
2023-07-14 18:30:40 +03:00
return xstr .. ystr
end
2023-07-17 16:17:52 +03:00
function realchess.board_to_table(inv)
2018-08-25 13:55:03 +03:00
local t = {}
for i = 1, 64 do
t[#t + 1] = inv:get_stack("board", i):get_name()
end
return t
end
2018-08-30 22:30:04 +03:00
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}
2023-07-17 16:17:52 +03:00
function realchess.attacked(color, idx, board)
local threatDetected = false
local kill = color == "white"
local pawnThreats = {kill, false, kill, false, false, not kill, false, not kill}
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
2023-07-16 13:55:07 +03:00
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
2023-07-17 16:17:52 +03:00
s_en_passant = realchess.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
2023-07-17 16:17:52 +03:00
if realchess.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.
2023-07-16 21:16:57 +03:00
-- 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)
2019-03-11 01:50:16 +03:00
local piece, color = board[from_idx]:match(":(%w+)_(%w+)")
if not piece then
return {}
end
2018-08-30 22:30:04 +03:00
local moves = {}
local from_x, from_y = index_to_xy(from_idx)
for i = 1, 64 do
2019-03-11 01:50:16 +03:00
local stack_name = board[i]
2018-08-30 22:30:04 +03:00
if stack_name:find((color == "black" and "white" or "black")) or
2019-03-11 01:50:16 +03:00
stack_name == "" then
2018-08-30 22:30:04 +03:00
moves[i] = 0
end
end
for to_idx in pairs(moves) do
2019-03-11 01:50:16 +03:00
local pieceTo = board[to_idx]
2018-08-30 22:30:04 +03:00
local to_x, to_y = index_to_xy(to_idx)
-- PAWN
if piece == "pawn" then
if color == "white" then
2019-03-11 01:50:16 +03:00
local pawnWhiteMove = board[xy_to_index(from_x, from_y - 1)]
local en_passant = false
2018-08-30 22:30:04 +03:00
-- 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
2018-08-30 22:30:04 +03:00
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
2018-08-30 22:30:04 +03:00
moves[to_idx] = nil
end
else
moves[to_idx] = nil
end
elseif color == "black" then
2019-03-11 01:50:16 +03:00
local pawnBlackMove = board[xy_to_index(from_x, from_y + 1)]
local en_passant = false
2018-08-30 22:30:04 +03:00
-- 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
2018-08-30 22:30:04 +03:00
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
2018-08-30 22:30:04 +03:00
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
2019-03-11 01:50:16 +03:00
if board[xy_to_index(from_x, i)] ~= "" then
2018-08-30 22:30:04 +03:00
moves[to_idx] = nil
end
end
else
-- Moving up
2018-08-30 22:30:04 +03:00
-- Ensure that no piece disturbs the way
for i = to_y + 1, from_y - 1 do
2019-03-11 01:50:16 +03:00
if board[xy_to_index(from_x, i)] ~= "" then
2018-08-30 22:30:04 +03:00
moves[to_idx] = nil
end
end
end
elseif from_y == to_y then
-- Moving horizontally
2018-08-30 22:30:04 +03:00
if from_x < to_x then
-- moving right
2018-08-30 22:30:04 +03:00
-- ensure that no piece disturbs the way
for i = from_x + 1, to_x - 1 do
2019-03-11 01:50:16 +03:00
if board[xy_to_index(i, from_y)] ~= "" then
2018-08-30 22:30:04 +03:00
moves[to_idx] = nil
end
end
else
-- Moving left
2018-08-30 22:30:04 +03:00
-- Ensure that no piece disturbs the way
for i = to_x + 1, from_x - 1 do
2019-03-11 01:50:16 +03:00
if board[xy_to_index(i, from_y)] ~= "" then
2018-08-30 22:30:04 +03:00
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
2019-03-11 01:50:16 +03:00
if board[xy_to_index(from_x + i, from_y + i)] ~= "" then
2018-08-30 22:30:04 +03:00
moves[to_idx] = nil
end
end
else
-- Moving right-up
-- Ensure that no piece disturbs the way
for i = 1, dx - 1 do
2019-03-11 01:50:16 +03:00
if board[xy_to_index(from_x + i, from_y - i)] ~= "" then
2018-08-30 22:30:04 +03:00
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
2019-03-11 01:50:16 +03:00
if board[xy_to_index(from_x - i, from_y + i)] ~= "" then
2018-08-30 22:30:04 +03:00
moves[to_idx] = nil
end
end
else
-- Moving left-up
-- ensure that no piece disturbs the way
for i = 1, dx - 1 do
2019-03-11 01:50:16 +03:00
if board[xy_to_index(from_x - i, from_y - i)] ~= "" then
2018-08-30 22:30:04 +03:00
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
2019-03-11 01:50:16 +03:00
if board[xy_to_index(from_x, i)] ~= "" then
2018-08-30 22:30:04 +03:00
moves[to_idx] = nil
end
end
else
-- Moving up
2018-08-30 22:30:04 +03:00
-- Ensure that no piece disturbs the way
for i = to_y + 1, from_y - 1 do
2019-03-11 01:50:16 +03:00
if board[xy_to_index(from_x, i)] ~= "" then
2018-08-30 22:30:04 +03:00
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
2019-03-11 01:50:16 +03:00
if board[xy_to_index(from_x + i, from_y)] ~= "" then
2018-08-30 22:30:04 +03:00
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
2019-03-11 01:50:16 +03:00
if board[xy_to_index(from_x + i, from_y + i)] ~= "" then
2018-08-30 22:30:04 +03:00
moves[to_idx] = nil
end
end
else
-- Goes right-up
-- Ensure that no piece disturbs the way
for i = 1, dx - 1 do
2019-03-11 01:50:16 +03:00
if board[xy_to_index(from_x + i, from_y - i)] ~= "" then
2018-08-30 22:30:04 +03:00
moves[to_idx] = nil
end
end
end
else
if from_y == to_y then
-- Moving horizontally
2018-08-30 22:30:04 +03:00
if from_x < to_x then
-- moving right
2018-08-30 22:30:04 +03:00
-- ensure that no piece disturbs the way
for i = from_x + 1, to_x - 1 do
2019-03-11 01:50:16 +03:00
if board[xy_to_index(i, from_y)] ~= "" then
2018-08-30 22:30:04 +03:00
moves[to_idx] = nil
end
end
else
-- Moving left
2018-08-30 22:30:04 +03:00
-- Ensure that no piece disturbs the way
for i = to_x + 1, from_x - 1 do
2019-03-11 01:50:16 +03:00
if board[xy_to_index(i, from_y)] ~= "" then
2018-08-30 22:30:04 +03:00
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
2019-03-11 01:50:16 +03:00
if board[xy_to_index(from_x - i, from_y + i)] ~= "" then
2018-08-30 22:30:04 +03:00
moves[to_idx] = nil
end
end
else
-- Goes left-up
-- Ensure that no piece disturbs the way
for i = 1, dx - 1 do
2019-03-11 01:50:16 +03:00
if board[xy_to_index(from_x - i, from_y - i)] ~= "" then
2018-08-30 22:30:04 +03:00
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
2023-07-17 16:17:52 +03:00
local king_board = realchess.board_to_table(inv)
king_board[to_idx] = king_board[from_idx]
king_board[from_idx] = ""
2023-07-17 16:17:52 +03:00
if realchess.attacked(color, to_idx, king_board) then
moves[to_idx] = nil
else
local dx = from_x - to_x
local dy = from_y - to_y
2018-08-30 22:30:04 +03:00
if dx < 0 then
dx = -dx
end
2018-08-30 22:30:04 +03:00
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
2018-08-30 22:30:04 +03:00
end
end
if not next(moves) then
return {}
end
2018-08-30 22:30:04 +03:00
2023-07-17 16:17:52 +03:00
-- Rate the possible moves depending on its piece value
2023-07-17 16:54:07 +03:00
-- TODO: Move this to chessbot.lua
2018-08-30 22:30:04 +03:00
for i in pairs(moves) do
2019-03-11 01:50:16 +03:00
local stack_name = board[tonumber(i)]
2018-08-30 22:30:04 +03:00
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.
2023-07-16 21:16:57 +03:00
-- r1, r2, r3 ... are numeric values (normally 0) to "rate" this square for the bot.
2023-07-17 16:17:52 +03:00
function realchess.get_theoretical_moves_for(meta, board, player)
local moves = {}
for i = 1, 64 do
local possibleMoves = get_theoretical_moves_from(meta, board, i)
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
2023-07-17 16:17:52 +03:00
function realchess.locate_kings(board)
2018-08-24 20:38:49 +03:00
local Bidx, Widx
for i = 1, 64 do
2018-08-25 13:55:03 +03:00
local piece, color = board[i]:match(":(%w+)_(%w+)")
2018-08-24 20:38:49 +03:00
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 is table of save moves
2023-07-17 16:17:52 +03:00
-- * theoretical_moves: moves table returned by realchess.get_theoretical_moves_for()
-- * board: board table
-- * player: player color ("white" or "black")
2023-07-17 16:17:52 +03:00
function realchess.has_king_safe_move(theoretical_moves, board, player)
local safe_moves = {}
-- create a virtual board
local v_board = table.copy(board)
for from_idx, _ in pairs(theoretical_moves) do
for to_idx, value in pairs(_) do
from_idx = tonumber(from_idx)
-- save the old board values before manipulating them
local bak_to = v_board[to_idx]
local bak_from = v_board[from_idx]
-- move the piece on the virtual board
v_board[to_idx] = v_board[from_idx]
v_board[from_idx] = ""
2023-07-17 16:17:52 +03:00
local black_king_idx, white_king_idx = realchess.locate_kings(v_board)
if not black_king_idx or not white_king_idx then
minetest.log("error", "[xdecor] Chess: Insufficient kings on chessboard!")
return false
end
local king_idx
if player == "black" then
king_idx = black_king_idx
else
king_idx = white_king_idx
end
2023-07-17 16:17:52 +03:00
local playerAttacked = realchess.attacked(player, king_idx, v_board)
if not playerAttacked then
safe_moves[from_idx] = safe_moves[from_idx] or {}
safe_moves[from_idx][to_idx] = value
end
-- restore the old state of the virtual board
v_board[to_idx] = bak_to
v_board[from_idx] = bak_from
end
end
if next(safe_moves) then
return true, safe_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
2023-07-15 11:06:24 +03:00
-- 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",
}
2023-07-15 11:06:24 +03:00
-- 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'
2023-07-15 11:06:24 +03:00
"realchess:rook_black_1", -- a8
"realchess:knight_black_1", -- b8
"realchess:bishop_black_1", -- c8
"realchess:queen_black", -- ...
2018-08-15 02:54:56 +03:00
"realchess:king_black",
"realchess:bishop_black_2",
"realchess:knight_black_2",
"realchess:rook_black_2",
-- rank '8'
-- rank '7'
2018-08-15 02:54:56 +03:00
"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'
2018-08-15 02:54:56 +03:00
"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'
2018-08-15 02:54:56 +03:00
"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"
}
2023-07-15 11:06:24 +03:00
-- 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"
2018-08-15 02:54:56 +03:00
end
2023-07-15 11:06:24 +03:00
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
2018-08-15 02:54:56 +03:00
local fs_gamemode_x
if CHESS_DEBUG then
fs_gamemode_x = 10.2
else
fs_gamemode_x = 11.5
end
2019-03-11 01:50:16 +03:00
local fs_init = [[
2023-07-12 14:35:54 +03:00
formspec_version[2]
size[16,10.7563;]
2019-03-11 01:50:16 +03:00
no_prepend[]
]]
2023-07-12 12:04:26 +03:00
.."bgcolor[#080808BB;true]"
2023-07-12 14:35:54 +03:00
.."background[0,0;16,10.7563;chess_bg.png;true]"
.."style_type[button,image_button,item_image_button;bgcolor=#8f3000]"
2023-07-12 14:35:54 +03:00
.."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")).."]"
.."label["..fs_gamemode_x..",1.8;"..FS("Select a mode:").."]"
.."button["..fs_gamemode_x.."10.2,2.1;2.5,0.8;single_w;"..FS("Singleplayer (white)").."]"
.."button["..fs_gamemode_x.."10.2,2.95;2.5,0.8;single_b;"..FS("Singleplayer (black)").."]"
.."button["..fs_gamemode_x.."10.2,4.1;2.5,0.8;multi;"..FS("Multiplayer").."]"
if CHESS_DEBUG then
fs_init = fs_init .."button["..(fs_gamemode_x+2.5)..",2.1;2.5,0.8;bot_vs_bot;"..FS("Bot vs Bot").."]"
end
2019-03-11 01:50:16 +03:00
2018-08-15 02:54:56 +03:00
local fs = [[
2023-07-12 14:35:54 +03:00
formspec_version[2]
size[16,10.7563;]
2018-08-15 02:54:56 +03:00
no_prepend[]
bgcolor[#080808BB;true]
2023-07-12 14:35:54 +03:00
background[0,0;16,10.7563;chess_bg.png;true]
style_type[button,image_button,item_image_button;bgcolor=#8f3000]
2023-07-12 14:35:54 +03:00
style_type[list;spacing=0.1;size=0.975]
2018-08-15 02:54:56 +03:00
listcolors[#00000000;#00000000;#00000000;#30434C;#FFF]
2023-07-12 14:35:54 +03:00
list[context;board;0.47,1.155;8,8;]
2018-08-15 02:54:56 +03:00
tableoptions[background=#00000000;highlight=#00000000;border=false]
]]
2023-07-15 18:43:25 +03:00
-- table columns for Chess notation.
-- Columns: move no.; white piece; white halfmove; white promotion; black piece; black halfmove; black promotion
.."tablecolumns[" ..
"text,align=right;"..
2023-07-15 18:43:25 +03:00
"image," .. figurines_str .. ";text;image," .. figurines_str .. ";" ..
--"image,0=mailbox_blank16.png;" ..
"image," .. figurines_str .. ";text;image," .. figurines_str .. "]"
2018-08-15 02:54:56 +03:00
2023-07-15 11:06:24 +03:00
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
2023-07-12 21:56:06 +03:00
if not special then
special = ""
end
2023-07-15 11:06:24 +03:00
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)
2023-07-15 11:06:24 +03:00
add_move_to_moves_list(meta, "", "", "", "", special)
end
-- abbreviation for each piece for the string
-- representation of the board (like in FEN)
local piece_letters = {
white = {
pawn = "P",
knight = "N",
bishop = "B",
rook = "R",
queen = "Q",
king = "K",
},
black = {
pawn = "p",
knight = "n",
bishop = "b",
rook = "r",
queen = "q",
king = "k",
},
}
local function piece_to_letter(piece)
if piece == "" then
return "."
elseif piece:find("white") then
for k,v in pairs(piece_letters.white) do
if piece:find(k) then
return v
end
end
elseif piece:find("black") then
for k,v in pairs(piece_letters.black) do
if piece:find(k) then
return v
end
end
end
end
local function letter_to_piece(letter)
if letter == "." then
return ""
else
for k,v in pairs(piece_letters.white) do
if v == letter then
return v
end
end
for k,v in pairs(piece_letters.black) do
if v == letter then
return v
end
end
end
return nil
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
str = str .. piece_to_letter(piece)
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
-- Returns the highest number of positions that are repeated
-- in the given positions history list.
-- Arguments:
-- * positions: positions history list returned by get_position_history()
-- * stop_counting_at: stop counting when this many repetitons have been found (optional)
local function count_repeated_positions(positions, stop_counting_at)
-- Count how often each position occurred
local positions_counter = {}
local maxRepeatedPositions = 0
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 positions_counter[position] > maxRepeatedPositions then
maxRepeatedPositions = positions_counter[position]
end
if stop_counting_at and maxRepeatedPositions >= stop_counting_at then
break
end
end
return maxRepeatedPositions
end
-- Create the full formspec string for the sequence of moves.
-- Uses Figurine Algebraic Notation.
2023-07-11 11:50:06 +03:00
local function get_moves_formstring(meta)
local moves_raw = meta:get_string("moves_raw")
if moves_raw == "" then
2023-07-15 18:43:25 +03:00
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]
2023-07-15 11:06:24 +03:00
local from_idx = tonumber(move_split[3])
local to_idx = tonumber(move_split[4])
local special = move_split[5]
2023-07-12 21:56:06 +03:00
-- 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
2023-07-15 18:43:25 +03:00
moves_out = moves_out .. ""..MOVES_LIST_SYMBOL_EMPTY..",," .. MOVES_LIST_SYMBOL_EMPTY .. ","
2023-07-12 21:56:06 +03:00
end
end
if special == "whiteWon" then
moves_out = moves_out .. ","..MOVES_LIST_SYMBOL_EMPTY..",10,"..MOVES_LIST_SYMBOL_EMPTY
move_no = move_no + 1
2023-07-12 21:56:06 +03:00
elseif special == "blackWon" then
moves_out = moves_out .. ","..MOVES_LIST_SYMBOL_EMPTY..",01,"..MOVES_LIST_SYMBOL_EMPTY
move_no = move_no + 1
2023-07-12 21:56:06 +03:00
elseif special == "draw" then
moves_out = moves_out .. ","..MOVES_LIST_SYMBOL_EMPTY..",½–½,"..MOVES_LIST_SYMBOL_EMPTY
move_no = move_no + 1
2023-07-12 21:56:06 +03:00
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
2023-07-15 11:06:24 +03:00
pieceFrom_si_id = get_figurine_id(pieceFrom)
end
2023-07-15 11:06:24 +03:00
local pieceTo_si_id = pieceTo ~= "" and get_figurine_id(pieceTo)
2023-07-17 16:17:52 +03:00
local coordFrom = realchess.index_to_notation(from_idx)
local coordTo = realchess.index_to_notation(to_idx)
if curPlayerIsWhite then
move_no = move_no + 1
2023-07-15 17:54:58 +03:00
-- Add move number (e.g. "3.")
moves_out = moves_out .. string.format("%d.", move_no) .. ","
end
2023-07-11 12:06:29 +03:00
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
2023-07-11 10:16:13 +03:00
-- normal capture
2023-07-11 12:06:29 +03:00
betweenCoordsSymbol = "×"
2023-07-11 10:16:13 +03:00
elseif pieceTo == "" and pieceFrom:sub(11,14) == "pawn" and from_x ~= to_x then
-- 'en passant' capture
2023-07-11 12:06:29 +03:00
betweenCoordsSymbol = "×"
2023-07-15 17:54:58 +03:00
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
2023-07-11 08:01:58 +03:00
-- queenside castling
if from_x == 4 and to_x == 2 then
2023-07-11 12:06:29 +03:00
-- write "000"
2023-07-15 18:43:25 +03:00
moves_out = moves_out .. MOVES_LIST_SYMBOL_EMPTY .. ",000," .. MOVES_LIST_SYMBOL_EMPTY
castling = true
2023-07-11 08:01:58 +03:00
-- kingside castling
elseif from_x == 4 and to_x == 6 then
2023-07-11 12:06:29 +03:00
-- write "00"
2023-07-15 18:43:25 +03: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
2023-07-11 12:06:29 +03:00
coordFrom .. betweenCoordsSymbol .. coordTo .. -- coords in long algebraic notation, e.g. "e2e3"
2023-07-15 18:43:25 +03:00
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
2023-07-15 18:43:25 +03:00
moves_out = moves_out .. "," .. MOVES_LIST_SYMBOL_EMPTY .. ",," .. MOVES_LIST_SYMBOL_EMPTY
end
2023-07-12 21:56:06 +03:00
end
if m ~= #moves_split then
moves_out = moves_out .. ","
end
end
return moves_out, move_no
2018-08-24 20:38:49 +03:00
end
-- Verify eaten list
local verify_eaten_list
if CHESS_DEBUG then
verify_eaten_list = function(meta)
local inv = meta:get_inventory()
2023-07-17 16:17:52 +03:00
local board = realchess.board_to_table(inv)
local whitePiecesLeft = 0
local whitePiecesEaten = 0
local blackPiecesLeft = 0
local blackPiecesEaten = 0
for b=1, 64 do
local piece = board[b]
if piece:find("white") then
whitePiecesLeft = whitePiecesLeft + 1
elseif piece:find("black") then
blackPiecesLeft = blackPiecesLeft + 1
end
end
local eaten = meta:get_string("eaten")
local eaten_split = string.split(eaten, ",")
for e=1, #eaten_split do
local piece = eaten_split[e]
if piece:find("white") then
whitePiecesEaten = whitePiecesEaten + 1
elseif piece:find("black") then
blackPiecesEaten = blackPiecesEaten + 1
end
end
local eatenError = false
if whitePiecesLeft + whitePiecesEaten ~= 16 then
minetest.log("error", "[xdecor] Chess: Incorrect number of white pieces in eaten list! pieces="..whitePiecesLeft.."; eaten="..whitePiecesEaten)
eatenError = true
elseif blackPiecesLeft + blackPiecesEaten ~= 16 then
minetest.log("error", "[xdecor] Chess: Incorrect number of black pieces in eaten list! pieces="..blackPiecesLeft.."; eaten="..blackPiecesEaten)
eatenError = true
end
if eatenError then
-- halt bots
local mode = meta:get_string("mode")
if mode == "bot_vs_bot" then
meta:set_string("botColor", "")
end
end
end
end
-- Reports that a piece was "eaten" (=captured).
-- Must be called right after the board inventory was updated
-- on which the piece is already removed
-- * meta: Chessboard node metadata
-- * piece: The itemname of the piece that was captured
local function add_to_eaten_list(meta, piece)
if piece ~= "" then
local eaten = meta:get_string("eaten")
if eaten ~= "" then
eaten = eaten .. ","
end
local piece_s = piece:match(":(%w+_%w+)") or ""
eaten = eaten .. piece_s
meta:set_string("eaten", eaten)
if CHESS_DEBUG then
verify_eaten_list(meta)
end
2018-08-24 20:38:49 +03:00
end
2023-07-11 11:50:06 +03:00
end
2018-08-24 20:38:49 +03:00
2023-07-11 11:50:06 +03:00
local function get_eaten_formstring(meta)
local eaten = meta:get_string("eaten")
2018-08-24 20:38:49 +03:00
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 ..
2023-07-12 14:35:54 +03:00
"image[" .. ((X + (is_white and 12.82 or 9.72)) - (X * 0.44)) .. "," ..
((Y + 6) - (Y * 0.12)) .. ";1,1;" .. eaten_t[i] .. ".png]"
2018-08-24 20:38:49 +03:00
end
2023-07-11 11:50:06 +03:00
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 botColor = meta:get_string("botColor")
2018-08-24 20:38:49 +03:00
2023-07-11 11:50:06 +03:00
local playerWhite = meta:get_string("playerWhite")
local playerBlack = meta:get_string("playerBlack")
-- Translate the bot names for the display
-- (internally, the bot names are still English-only)
local playerWhiteDisplay = get_display_player_name(meta, "white")
local playerBlackDisplay = get_display_player_name(meta, "black")
2023-07-11 11:50:06 +03:00
local moves_raw = meta:get_string("moves_raw")
local moves, mlistlen = get_moves_formstring(meta)
2023-07-11 11:50:06 +03:00
local eaten_img = get_eaten_formstring(meta)
local lastMove = meta:get_string("lastMove")
local gameResult = meta:get_string("gameResult")
2023-07-12 21:56:06 +03:00
local grReason = meta:get_string("gameResultReason")
local mode = meta:get_string("mode")
2023-07-11 11:50:06 +03:00
-- arrow to show whose turn it is
2023-07-12 14:35:54 +03:00
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", playerBlackDisplay)
local turnWhite = minetest.colorize("#000001", playerWhiteDisplay)
-- 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").."]")
2023-07-12 21:56:06 +03:00
-- player has resigned
local resign_s = minetest.colorize("#FF0000", "["..S("resigned").."]")
-- player has won
local win_s = minetest.colorize("#26AB2B", "["..S("winner").."]")
2023-07-12 21:56:06 +03:00
-- player has lost
local lose_s = minetest.colorize("#FF0000", "["..S("loser").."]")
-- player has a draw
local draw_s = minetest.colorize("#FF00FF", "["..S("draw").."]")
2023-07-11 11:50:06 +03:00
local status_black = ""
local status_white = ""
if gameResult == "blackWon" then
2023-07-12 21:56:06 +03:00
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
2023-07-12 21:56:06 +03:00
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
2023-07-15 09:28:18 +03:00
local promotion = ""
if gameResult == "" then
promotion = meta:get_string("promotionActive")
end
local promotion_formstring = ""
2023-07-16 12:48:50 +03:00
-- Show promotion prompt to ask player to choose to which piece to promote a pawn to
if promotion == "black" then
eaten_img = ""
promotion_formstring =
2023-07-12 14:35:54 +03:00
"label[10.1,6.35;"..FS("PROMOTION\nFOR BLACK!").."]" ..
2023-07-16 12:48:50 +03:00
"animated_image[10.05,7.2;2,2;p_img_white;pawn_black_promo_anim.png;5;100]"
if botColor ~= "black" and botColor ~= "both" then
2023-07-16 12:48:50 +03:00
-- Hide buttons if computer player promotes
promotion_formstring = promotion_formstring ..
2023-07-12 14:35:54 +03:00
"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;]"
2023-07-16 12:48:50 +03:00
end
elseif promotion == "white" then
eaten_img = ""
promotion_formstring =
2023-07-12 14:35:54 +03:00
"label[10.1,6.35;"..FS("PROMOTION\nFOR WHITE!").."]" ..
2023-07-16 12:48:50 +03:00
"animated_image[10.05,7.2;2,2;p_img_white;pawn_white_promo_anim.png;5;100]"
if botColor ~= "white" and botColor ~= "both" then
2023-07-16 12:48:50 +03:00
-- Hide buttons if computer player promotes
promotion_formstring = promotion_formstring ..
2023-07-12 14:35:54 +03:00
"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;]"
2023-07-16 12:48:50 +03:00
end
end
-- Resign / Start new game
local game_buttons = ""
game_buttons = game_buttons .. "button[13.36,0.26;2,0.8;new;"..FS("New game").."]"
local playerActionsAvailable = mode ~= "bot_vs_bot" and gameResult == ""
if playerActionsAvailable and (playerWhite ~= "" and playerBlack ~= "") then
game_buttons = game_buttons .. "image_button[14.56,9.7;0.8,0.8;chess_resign.png;resign;]" ..
"tooltip[resign;"..FS("Resign").."]"
end
local drawClaim = meta:get_string("drawClaim")
if playerActionsAvailable and drawClaim == "" then
-- 50-move rule
local halfmoveClock = meta:get_int("halfmoveClock")
if halfmoveClock == 99 then
-- When the 50 moves without capture / pawn move is about to occur.
-- Will trigger "draw claim" mode in which player must do the final move that triggers the draw
game_buttons = game_buttons .. "image_button[13.36,9.7;0.8,0.8;chess_draw_50move_next.png;draw_50_moves;]"..
"tooltip[draw_50_moves;"..
FS("Invoke the 50-move rule and try to draw the game in your next move.").."\n"..
FS("If your next move is the 50th consecutive move of both players where no pawn moved and no piece was captured, the game will be drawn.").."]"
elseif halfmoveClock >= 100 then
-- When the 50 moves without capture / pawn move have occured occur.
-- Will insta-draw.
game_buttons = game_buttons .. "image_button[13.36,9.7;0.8,0.8;chess_draw_50move.png;draw_50_moves;]"..
"tooltip[draw_50_moves;"..
FS("Invoke the 50-move rule and draw the game.").."\n"..
FS("(In the last 50 moves of both players, no pawn moved and no piece was captured.)").."]"
end
-- "same position has occured 3 times" rule
-- Count how often each position occurred
local positions = get_positions_history(meta)
local maxRepeatedPositions = count_repeated_positions(positions, 3)
if maxRepeatedPositions == 2 then
-- If the same position is about to occur 3 times.
-- Will trigger "draw claim" mode in which player must do the final move that triggers the draw.
game_buttons = game_buttons .. "image_button[12.36,9.7;0.8,0.8;chess_draw_repeat3_next.png;draw_repeat_3;]"..
"tooltip[draw_repeat_3;"..
FS("Invoke the 'same position' rule and try to draw the game in your next move.").."\n"..
FS("If your next move causes the same position to occur a 3rd time, the game will be drawn.").."]"
elseif maxRepeatedPositions >= 3 then
-- If the same position has already occured 3 times
-- Will insta-draw.
game_buttons = game_buttons .. "image_button[12.36,9.7;0.8,0.8;chess_draw_repeat3.png;draw_repeat_3;]"..
"tooltip[draw_repeat_3;"..
FS("Invoke the 'same position' rule and draw the game.").."\n"..
FS("(The same position has occured 3 times.)").."]"
end
2023-07-12 21:56:06 +03:00
end
local s_draw_claim = ""
-- Change interface when in "draw claim" mode to make the move
-- that triggers the draw condition
if drawClaim == "50_move_rule" then
eaten_img = ""
s_draw_claim = "" ..
"image[13.36,9.7;0.8,0.8;chess_draw_50move_next.png]" ..
"tooltip[13.36,9.7;0.8,0.8;"..
FS("Draw claim active: 50-move rule").."\n"..
FS("If the next move satisfies the 50-move rule, the game is drawn.").."]"
elseif drawClaim == "same_position_3" then
eaten_img = ""
s_draw_claim = "" ..
"image[12.36,9.7;0.8,0.8;chess_draw_repeat3_next.png]" ..
"tooltip[12.36,9.7;0.8,0.8;"..
FS("Draw claim active: Same position").."\n"..
FS("If the next move satisfies the same position rule, the game is drawn.").."]"
end
2023-07-16 13:55:07 +03:00
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)
2023-07-16 13:55:07 +03:00
-- 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")
2023-07-16 13:55:07 +03:00
-- 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[6.9,10.2;DEBUG: "..debug_str.."]"
2023-07-16 13:55:07 +03:00
end
2023-07-11 11:50:06 +03:00
local formspec = fs ..
2023-07-12 14:35:54 +03:00
"label[2.2,0.652;" .. turnBlack .. minetest.formspec_escape(status_black) .. "]" ..
2023-07-11 11:50:06 +03:00
blackArr ..
2023-07-12 14:35:54 +03:00
"label[2.2,10.21;" .. turnWhite .. minetest.formspec_escape(status_white) .. "]" ..
2023-07-11 11:50:06 +03:00
whiteArr ..
"table[9.9,1.25;5.45,4;moves;" .. moves .. ";"..mlistlen.."]" ..
s_draw_claim ..
promotion_formstring ..
2023-07-12 21:56:06 +03:00
eaten_img ..
2023-07-16 13:55:07 +03:00
game_buttons ..
debug_formstring
2023-07-11 11:50:06 +03:00
meta:set_string("formspec", formspec)
2018-08-24 20:38:49 +03:00
end
local function update_game_result(meta)
local inv = meta:get_inventory()
2023-07-17 16:17:52 +03:00
local board_t = realchess.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
2023-07-17 16:17:52 +03:00
local blackMoves = realchess.get_theoretical_moves_for(meta, board_t, "black")
local whiteMoves = realchess.get_theoretical_moves_for(meta, board_t, "white")
if next(blackMoves) then
blackCanMove = true
end
if next(whiteMoves) then
whiteCanMove = true
end
-- assume lastMove was updated *after* the player moved
local lastMove = meta:get_string("lastMove")
2023-07-17 16:17:52 +03:00
local black_king_idx, white_king_idx = realchess.locate_kings(board_t)
if not black_king_idx or not white_king_idx then
minetest.log("error", "[xdecor] Chess: Insufficient kings on chessboard!")
return
end
local checkPlayer, king_idx, checkMoves
if lastMove == "black" or lastMove == "" then
checkPlayer = "white"
checkMoves = whiteMoves
king_idx = white_king_idx
else
checkPlayer = "black"
checkMoves = blackMoves
king_idx = black_king_idx
end
-- King attacked? This reduces the list of available moves,
-- so remove these, too and check if there are still any left.
2023-07-17 16:17:52 +03:00
local isKingAttacked = realchess.attacked(checkPlayer, king_idx, board_t)
if isKingAttacked then
meta:set_string(checkPlayer.."Attacked", "true")
2023-07-17 16:17:52 +03:00
local is_safe = realchess.has_king_safe_move(checkMoves, board_t, checkPlayer)
-- If not safe moves left, player can't move
if not is_safe then
if checkPlayer == "black" then
blackCanMove = false
else
whiteCanMove = false
end
end
end
local playerWhiteDisplay = get_display_player_name(meta, "white")
local playerBlackDisplay = get_display_player_name(meta, "black")
local botColor = meta:get_string("botColor")
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")
send_message(playerWhite, S("You have checkmated @1. You win!", playerBlackDisplay), "white", botColor)
send_message(playerBlack, S("You were checkmated by @1. You lose!", playerWhiteDisplay), "black", botColor)
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")
send_message_2(playerWhite, playerBlack, S("The game ended up in a stalemate! It's a draw!"), botColor)
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")
send_message(playerBlack, S("You have checkmated @1. You win!", playerWhiteDisplay), "white", botColor)
send_message(playerWhite, S("You were checkmated by @1. You lose!", playerBlackDisplay), "white", botColor)
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")
send_message_2(playerWhite, playerBlack, S("The game ended up in a stalemate! It's a draw!"), botColor)
minetest.log("action", "[xdecor] Chess: A game between "..playerWhite.." and "..playerBlack.." ended in a draw by stalemate")
return
end
end
2023-07-16 15:55:25 +03:00
-- 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")
send_message_2(playerWhite, playerBlack, S("The game ended up in a dead position! It's a draw!"), botColor)
minetest.log("action", "[xdecor] Chess: A game between "..playerWhite.." and "..playerBlack.." ended in a draw by dead position")
end
2023-07-16 15:55:25 +03:00
-- 50-move rule, after a player issued a draw claim when the halfmove clock was at 99.
-- If no pawn moved nor a piece was captured for >= 100 halfmoves, the game is drawn.
if meta:get_int("halfmoveClock") >= 100 and meta:get_string("drawClaim") == "50_move_rule" then
meta:set_string("drawClaim", "")
meta:set_string("gameResult", "draw")
meta:set_string("gameResultReason", "50_move_rule")
add_special_to_moves_list(meta, "draw")
update_formspec(meta)
local claimer, other
if lastMove == "black" or lastMove == "" then
claimer = playerWhite
other = playerBlack
else
claimer = playerBlack
other = playerWhite
end
send_message(claimer, S("You have drawn the game by invoking the 50-move rule."), botColor)
if claimer ~= other then
send_message(other, S("@1 has drawn the game by invoking the 50-move rule.", claimer), botColor)
end
minetest.log("action", "[xdecor] Chess: A game between "..playerWhite.." and "..playerBlack.." ended in a draw because "..claimer.." has applied the 50-move rule")
-- 75-move rule. Automatically draw game if the last 75 moves of EACH player (thus 150 halfmoves)
2023-07-16 15:55:25 +03:00
-- neither moved a pawn or captured a piece.
-- Important: This rule MUST be checked AFTER checkmate because checkmate takes precedence.
elseif meta:get_int("halfmoveClock") >= 150 then
2023-07-16 15:55:25 +03:00
meta:set_string("gameResult", "draw")
meta:set_string("gameResultReason", "75_move_rule")
2023-07-16 15:55:25 +03:00
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!")
send_message_2(playerWhite, playerBlack, msg, botColor)
2023-07-16 15:55:25 +03:00
minetest.log("action", "[xdecor] Chess: A game between "..playerWhite.." and "..playerBlack.." ended in a draw via the 75-move rule")
end
-- Draw if same position appeared >= 5 times
-- or if it appeared >= 3 times and the player claimed the draw previously
-- First, generate the position history
local forceRepetitionDraw = false
local chosenRepetitionDraw = false
local positions = get_positions_history(meta)
-- Then count the repeated positions
local maxRepeatedPositions = count_repeated_positions(positions, 5)
if maxRepeatedPositions >= 3 then
chosenRepetitionDraw = true
end
if maxRepeatedPositions >= 5 then
forceRepetitionDraw = true
end
if CHESS_DEBUG then
-- Show last position
local last_position = positions[#positions]
local msg = "Current position: \"" .. last_position .. "\""
send_message_2(playerWhite, playerBlack, msg, botColor)
-- Compare the last position with the actual chessboard
-- to automatically test if the position history is still valid
local pos_split = last_position:split(" ")
local p_board = pos_split[1]
local p_player = pos_split[2]
local p_castling = pos_split[3]
local p_en_passant = pos_split[4]
local errors = 0
for b=1, #board_t do
local piece = board_t[b]
local letter_real = piece_to_letter(piece)
local letter_pos = string.sub(p_board, b, b)
if letter_real ~= letter_pos then
minetest.log("error", "[xdecor] Chess: Position history inconsistency on board index "..b..": '"..letter_pos.."' seen but '"..letter_real.."' expected")
end
end
-- Compare current player
if (lastMove == "black" or lastMove == "") and p_player ~= "w" then
minetest.log("error", "[xdecor] Chess: Position history inconsistency: Wrong player! '"..p_player.."' seen but 'w' expected")
elseif (lastMove == "white") and p_player ~= "b" then
minetest.log("error", "[xdecor] Chess: Position history inconsistency: Wrong player! '"..p_player.."' seen but 'b' expected")
end
-- Compare 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)
if d_castling ~= p_castling then
minetest.log("error", "[xdecor] Chess: Position history inconsistency: Castling rights mismatch! '"..p_castling.."' seen but '"..d_castling.."' expected")
end
-- Compare en passant status
local double_step = meta:get_int("prevDoublePawnStepTo")
local d_en_passant = en_passant_to_string(double_step)
if d_en_passant ~= p_en_passant then
minetest.log("error", "[xdecor] Chess: Position history inconsistency: En passant status mismatch! '"..p_en_passant.."' seen but '"..d_en_passant.."' expected")
end
end
if forceRepetitionDraw 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!")
send_message_2(playerWhite, playerBlack, msg, botColor)
minetest.log("action", "[xdecor] Chess: A game between "..playerWhite.." and "..playerBlack.." ended in a draw because the same position has appeared 5 times")
elseif chosenRepetitionDraw and meta:get_string("drawClaim") == "same_position_3" then
meta:set_string("drawClaim", "")
meta:set_string("gameResult", "draw")
meta:set_string("gameResultReason", "same_position_3")
add_special_to_moves_list(meta, "draw")
update_formspec(meta)
local claimer, other
if lastMove == "black" or lastMove == "" then
claimer = playerWhite
other = playerBlack
else
claimer = playerBlack
other = playerWhite
end
send_message(claimer, S("You have drawn the game by invoking the same position rule."), botColor)
if claimer ~= other then
send_message(other, S("@1 has drawn the game by invoking the same position rule.", claimer), botColor)
end
minetest.log("action", "[xdecor] Chess: A game between "..playerWhite.." and "..playerBlack.." ended in a draw because "..claimer.." has applied the same position rule")
end
meta:set_string("drawClaim", "")
end
function realchess.init(pos)
local meta = minetest.get_meta(pos)
2018-08-24 20:38:49 +03:00
local inv = meta:get_inventory()
2016-12-30 17:04:57 +03:00
2019-03-11 01:50:16 +03:00
meta:set_string("formspec", fs_init)
meta:set_string("infotext", S("Chess Board"))
meta:set_string("playerBlack", "")
meta:set_string("playerWhite", "")
2023-07-16 21:16:57 +03:00
meta:set_string("botColor", "")
2018-08-24 20:38:49 +03:00
meta:set_string("lastMove", "")
meta:set_string("gameResult", "")
2023-07-12 21:56:06 +03:00
meta:set_string("gameResultReason", "")
meta:set_string("drawClaim", "")
2018-08-25 15:28:52 +03:00
meta:set_string("blackAttacked", "")
meta:set_string("whiteAttacked", "")
meta:set_string("promotionActive", "")
2016-02-18 00:53:00 +03:00
2018-08-24 20:38:49 +03:00
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", "")
2018-08-15 02:54:56 +03:00
meta:set_string("eaten", "")
2019-03-11 01:50:16 +03:00
meta:set_string("mode", "")
2016-02-18 00:53:00 +03:00
2023-07-15 11:06:24 +03:00
inv:set_list("board", starting_grid)
2016-02-18 00:53:00 +03:00
inv:set_size("board", 64)
2023-07-11 11:50:06 +03:00
-- 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
2023-07-02 02:36:38 +03:00
end
local promo = meta:get_string("promotionActive")
if promo ~= "" then
-- Can't move when waiting for selecting a pawn promotion
return false
end
2023-07-12 21:56:06 +03:00
local gameResult = meta:get_string("gameResult")
if gameResult ~= "" then
-- No moves if game is over
return false
2023-07-12 21:56:06 +03:00
end
2018-08-24 20:38:49 +03:00
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
2018-08-24 20:38:49 +03:00
local thisMove -- Will replace lastMove when move is legal
if pieceFrom:find("white") then
2023-07-02 02:36:38 +03:00
if pieceTo:find("white") then
-- Don't replace pieces of same color
return false
2016-12-30 17:04:57 +03:00
end
2018-08-24 20:38:49 +03:00
2023-07-02 02:36:38 +03:00
if lastMove == "white" then
-- let the other invocation decide in case of a capture
return pieceTo == "" and 0 or 1
end
2018-08-24 20:38:49 +03:00
2023-07-02 02:36:38 +03:00
if playerWhite ~= "" and playerWhite ~= playerName then
send_message(playerName, S("Someone else plays white pieces!"))
return false
end
2018-08-24 20:38:49 +03:00
playerWhite = playerName
thisMove = "white"
2018-08-24 20:38:49 +03:00
elseif pieceFrom:find("black") then
2023-07-02 02:36:38 +03:00
if pieceTo:find("black") then
-- Don't replace pieces of same color
return false
end
2018-08-24 20:38:49 +03:00
2023-07-02 02:36:38 +03:00
if lastMove == "black" then
-- let the other invocation decide in case of a capture
return false
end
2018-08-24 20:38:49 +03:00
2023-07-02 02:36:38 +03:00
if playerBlack ~= "" and playerBlack ~= playerName then
send_message(playerName, S("Someone else plays black pieces!"))
return false
end
2018-08-24 20:38:49 +03:00
2023-07-11 13:03:21 +03:00
if lastMove == "" then
-- Nobody has moved yet, and Black cannot move first
return false
2023-07-11 13:03:21 +03:00
end
playerBlack = playerName
thisMove = "black"
end
2018-08-24 20:38:49 +03:00
-- MOVE LOGIC
local from_x, from_y = index_to_xy(from_index)
2018-08-24 20:38:49 +03:00
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
2018-08-24 20:38:49 +03:00
-- PAWN
2016-02-18 00:53:00 +03:00
if pieceFrom:sub(11,14) == "pawn" then
if thisMove == "white" then
2016-02-18 00:53:00 +03:00
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
2023-07-11 10:16:13 +03:00
-- 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
2023-07-11 10:16:13 +03:00
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
2023-07-11 10:16:13 +03:00
-- double step
2016-02-18 00:53:00 +03:00
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
2018-03-08 23:21:20 +03:00
2018-08-24 20:38:49 +03:00
--[[
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
]]
2018-03-08 23:21:20 +03:00
if from_x == to_x then
if pieceTo ~= "" then
return false
2018-03-08 23:21:20 +03:00
end
elseif from_x - 1 == to_x or from_x + 1 == to_x then
2023-07-11 10:16:13 +03:00
-- 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)
2023-07-11 10:16:13 +03:00
end
end
if not can_capture then
return false
2018-03-08 23:21:20 +03:00
end
resetHalfmoveClock = true
2018-03-08 23:21:20 +03:00
else
return false
2018-03-08 23:21:20 +03:00
end
elseif thisMove == "black" then
2016-02-18 00:53:00 +03:00
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
2023-07-11 10:16:13 +03:00
-- 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
2023-07-11 10:16:13 +03:00
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
2023-07-11 10:16:13 +03:00
-- double step
2016-02-18 00:53:00 +03:00
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
2018-08-24 20:38:49 +03:00
--[[
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
2023-07-11 10:16:13 +03:00
-- 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)
2023-07-11 10:16:13 +03:00
end
end
if not can_capture then
return false
end
resetHalfmoveClock = true
else
return false
end
else
return false
end
2018-08-24 20:38:49 +03:00
-- ROOK
2016-02-18 00:53:00 +03:00
elseif pieceFrom:sub(11,14) == "rook" then
if from_x == to_x then
2018-08-24 20:38:49 +03:00
-- Moving vertically
if from_y < to_y then
2018-08-24 20:38:49 +03:00
-- 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
2018-08-24 20:38:49 +03:00
-- 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
2018-08-24 20:38:49 +03:00
-- 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
2018-08-24 20:38:49 +03:00
-- 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
2018-08-24 20:38:49 +03:00
-- KNIGHT
2016-02-18 00:53:00 +03:00
elseif pieceFrom:sub(11,16) == "knight" then
2018-08-24 20:38:49 +03:00
-- Get relative pos
local dx = from_x - to_x
local dy = from_y - to_y
2018-08-24 20:38:49 +03:00
-- Get absolute values
2016-02-18 00:53:00 +03:00
if dx < 0 then dx = -dx end
if dy < 0 then dy = -dy end
2018-08-24 20:38:49 +03:00
-- Sort x and y
2016-02-18 00:53:00 +03:00
if dx > dy then dx, dy = dy, dx end
2018-08-24 20:38:49 +03:00
-- Ensure that dx == 1 and dy == 2
if dx ~= 1 or dy ~= 2 then
return false
end
2018-08-24 20:38:49 +03:00
-- Just ensure that destination cell does not contain friend piece
-- ^ It was done already thus everything ok
2018-08-24 20:38:49 +03:00
-- BISHOP
2016-02-18 00:53:00 +03:00
elseif pieceFrom:sub(11,16) == "bishop" then
2018-08-24 20:38:49 +03:00
-- Get relative pos
local dx = from_x - to_x
local dy = from_y - to_y
2018-08-24 20:38:49 +03:00
-- Get absolute values
2016-02-18 00:53:00 +03:00
if dx < 0 then dx = -dx end
if dy < 0 then dy = -dy end
2018-08-24 20:38:49 +03:00
-- 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
2018-08-24 20:38:49 +03:00
-- Moving right-down
-- Ensure that no piece disturbs the way
for i = 1, dx - 1 do
2018-08-30 22:30:04 +03:00
if inv:get_stack(
from_list, xy_to_index(from_x + i, from_y + i)):get_name() ~= "" then
return false
end
end
else
2018-08-24 20:38:49 +03:00
-- Moving right-up
-- Ensure that no piece disturbs the way
for i = 1, dx - 1 do
2018-08-30 22:30:04 +03:00
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
2018-08-24 20:38:49 +03:00
-- Moving left-down
-- Ensure that no piece disturbs the way
for i = 1, dx - 1 do
2018-08-30 22:30:04 +03:00
if inv:get_stack(
from_list, xy_to_index(from_x - i, from_y + i)):get_name() ~= "" then
return false
end
end
else
2018-08-24 20:38:49 +03:00
-- Moving left-up
-- ensure that no piece disturbs the way
for i = 1, dx - 1 do
2018-08-30 22:30:04 +03:00
if inv:get_stack(
from_list, xy_to_index(from_x - i, from_y - i)):get_name() ~= "" then
return false
end
end
end
end
2018-08-24 20:38:49 +03:00
-- QUEEN
2016-02-18 00:53:00 +03:00
elseif pieceFrom:sub(11,15) == "queen" then
local dx = from_x - to_x
local dy = from_y - to_y
2018-08-24 20:38:49 +03:00
-- Get absolute values
2016-02-18 00:53:00 +03:00
if dx < 0 then dx = -dx end
if dy < 0 then dy = -dy end
2018-08-24 20:38:49 +03:00
-- 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
2018-08-24 20:38:49 +03:00
-- Goes down
-- Ensure that no piece disturbs the way
2021-02-28 23:02:06 +03:00
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
2018-08-24 20:38:49 +03:00
-- Goes up
-- Ensure that no piece disturbs the way
2021-02-28 23:02:06 +03:00
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
2016-12-30 17:04:57 +03:00
end
elseif from_x < to_x then
if from_y == to_y then
2018-08-24 20:38:49 +03:00
-- Goes right
-- Ensure that no piece disturbs the way
2021-02-28 23:02:06 +03:00
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
2018-08-24 20:38:49 +03:00
-- Goes right-down
-- Ensure that no piece disturbs the way
for i = 1, dx - 1 do
2018-08-30 22:30:04 +03:00
if inv:get_stack(
from_list, xy_to_index(from_x + i, from_y + i)):get_name() ~= "" then
return false
end
end
else
2018-08-24 20:38:49 +03:00
-- Goes right-up
-- Ensure that no piece disturbs the way
for i = 1, dx - 1 do
2018-08-30 22:30:04 +03:00
if inv:get_stack(
from_list, xy_to_index(from_x + i, from_y - i)):get_name() ~= "" then
return false
end
end
2016-12-30 17:04:57 +03:00
end
else
if from_y == to_y then
2018-08-24 20:38:49 +03:00
-- Goes left
-- Ensure that no piece disturbs the way and destination cell does
2021-02-28 23:02:06 +03:00
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
2018-08-24 20:38:49 +03:00
-- Goes left-down
-- Ensure that no piece disturbs the way
for i = 1, dx - 1 do
2018-08-30 22:30:04 +03:00
if inv:get_stack(
from_list, xy_to_index(from_x - i, from_y + i)):get_name() ~= "" then
return false
end
end
else
2018-08-24 20:38:49 +03:00
-- Goes left-up
-- Ensure that no piece disturbs the way
for i = 1, dx - 1 do
2018-08-30 22:30:04 +03:00
if inv:get_stack(
from_list, xy_to_index(from_x - i, from_y - i)):get_name() ~= "" then
return false
end
end
2016-12-30 17:04:57 +03:00
end
end
2018-08-24 20:38:49 +03:00
-- KING
2016-02-18 00:53:00 +03:00
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()
2023-07-17 16:17:52 +03:00
local board = realchess.board_to_table(inv)
2016-12-30 17:04:57 +03:00
-- 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
2018-08-24 20:38:49 +03:00
if dx < 0 then
dx = -dx
end
if dy < 0 then
dy = -dy
end
if dx > 1 or dy > 1 then
return false
2018-08-24 20:38:49 +03:00
end
end
kingMoved = true
2016-12-30 17:04:57 +03:00
end
2023-07-17 16:17:52 +03:00
local board = realchess.board_to_table(inv)
2023-07-12 09:29:26 +03:00
board[to_index] = board[from_index]
board[from_index] = ""
2023-07-17 16:17:52 +03:00
local black_king_idx, white_king_idx = realchess.locate_kings(board)
2023-07-12 09:29:26 +03:00
if not black_king_idx or not white_king_idx then
minetest.log("error", "[xdecor] Chess: Insufficient kings on chessboard!")
return false
2023-07-12 09:29:26 +03:00
end
2023-07-17 16:17:52 +03:00
local blackAttacked = realchess.attacked("black", black_king_idx, board)
local whiteAttacked = realchess.attacked("white", white_king_idx, board)
2023-07-12 09:29:26 +03:00
-- Refuse to move if it would put or leave the own king
-- under attack
2023-07-12 09:29:26 +03:00
if blackAttacked and thisMove == "black" then
return false
2023-07-12 09:29:26 +03:00
end
if whiteAttacked and thisMove == "white" then
return false
2023-07-12 09:29:26 +03:00
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
-- Capture pawn en passant
local capturedPiece = inv:get_stack(to_list, en_passant_target):get_name()
inv:set_stack(to_list, en_passant_target, "")
add_to_eaten_list(meta, capturedPiece)
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
2023-07-16 12:48:50 +03:00
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)
2018-08-25 15:28:52 +03:00
end
2023-07-16 12:48:50 +03:00
if doublePawnStep then
meta:set_int("prevDoublePawnStepTo", doublePawnStep)
else
meta:set_int("prevDoublePawnStepTo", 0)
end
2018-08-25 15:28:52 +03:00
2018-08-30 22:30:04 +03:00
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
2023-07-17 16:17:52 +03:00
-- Causes the player ("white" or "blue") to resign
function realchess.resign(meta, playerColor)
if playerColor == "black" then
meta:set_string("gameResult", "whiteWon")
meta:set_string("gameResultReason", "resign")
add_special_to_moves_list(meta, "whiteWon")
2018-08-30 22:30:04 +03:00
update_formspec(meta)
2023-07-17 16:17:52 +03:00
elseif playerColor == "white" then
meta:set_string("gameResult", "blackWon")
meta:set_string("gameResultReason", "resign")
add_special_to_moves_list(meta, "blackWon")
2018-08-30 22:30:04 +03:00
update_formspec(meta)
end
2019-03-11 01:50:16 +03:00
end
2015-11-21 23:00:54 +03:00
local function timeout_format(timeout_limit)
local time_remaining = timeout_limit - minetest.get_gametime()
2018-08-24 20:38:49 +03:00
local minutes = math.floor(time_remaining / 60)
local seconds = time_remaining % 60
2015-11-21 23:00:54 +03:00
2018-08-15 02:54:56 +03:00
if minutes == 0 then
2023-07-12 12:11:48 +03:00
-- number of seconds
return S("@1 s", seconds)
2018-08-15 02:54:56 +03:00
end
2023-07-12 12:11:48 +03:00
-- number of minutes and seconds
return S("@1 min @2 s", minutes, seconds)
2015-11-21 23:00:54 +03:00
end
2016-02-18 00:53:00 +03:00
function realchess.fields(pos, _, fields, sender)
2018-08-24 20:38:49 +03:00
local playerName = sender:get_player_name()
local meta = minetest.get_meta(pos)
2023-07-17 14:50:03 +03:00
local timeout_limit = meta:get_int("lastMoveTime") + TIMEOUT
2018-08-24 20:38:49 +03:00
local playerWhite = meta:get_string("playerWhite")
local playerBlack = meta:get_string("playerBlack")
local lastMoveTime = meta:get_int("lastMoveTime")
2023-07-15 09:28:18 +03:00
local gameResult = meta:get_int("gameResult")
if fields.quit then return end
if fields.single_w or fields.single_b or fields.multi or fields.bot_vs_bot then
if fields.bot_vs_bot then
if not CHESS_DEBUG then
-- Bot vs Bot only allowed in Chess Debug Mode
return
end
meta:set_string("mode", "bot_vs_bot")
meta:set_string("botColor", "both")
-- Add asterisk to bot names so it can't collide with a player name
-- (asterisk is forbidden in player names)
meta:set_string("playerWhite", "*"..BOT_NAME_1.."*")
meta:set_string("playerBlack", "*"..BOT_NAME_2.."*")
local inv = meta:get_inventory()
2023-07-17 16:17:52 +03:00
chessbot.move(inv, meta)
elseif fields.single_w then
meta:set_string("mode", "single")
2023-07-16 21:16:57 +03:00
meta:set_string("botColor", "black")
meta:set_string("playerBlack", "*"..BOT_NAME.."*")
elseif fields.single_b then
meta:set_string("mode", "single")
2023-07-16 21:16:57 +03:00
meta:set_string("botColor", "white")
meta:set_string("playerWhite", "*"..BOT_NAME.."*")
local inv = meta:get_inventory()
2023-07-17 16:17:52 +03:00
chessbot.move(inv, meta)
elseif fields.multi then
meta:set_string("mode", "multi")
end
2019-03-11 01:50:16 +03:00
update_formspec(meta)
return
end
local mode = meta:get_string("mode")
2023-07-17 14:50:03 +03:00
-- If the game is ongoing and no move was made for TIMEOUT seconds,
-- the game can be aborted by everyone.
-- Also allow instant reset before White and Black moved,
-- as well as in Bot vs Bot mode
2018-08-15 02:54:56 +03:00
if fields.new then
if mode == "bot_vs_bot" or (playerWhite == playerName or playerBlack == playerName or playerWhite == "" or playerBlack == "") then
2018-08-15 02:54:56 +03:00
realchess.init(pos)
2018-08-24 20:38:49 +03:00
2018-08-30 22:30:04 +03:00
elseif lastMoveTime > 0 then
2018-08-24 20:38:49 +03:00
if minetest.get_gametime() >= timeout_limit and
(playerWhite ~= playerName or playerBlack ~= playerName) then
realchess.init(pos)
else
send_message(playerName,
2023-07-12 12:30:15 +03:00
S("You can't reset the chessboard, a game has been started. Try again in @1.",
timeout_format(timeout_limit)))
2018-08-24 20:38:49 +03:00
end
2018-08-15 02:54:56 +03:00
end
return
end
if fields.resign and mode ~= "bot_vs_bot" then
2023-07-12 21:56:06 +03:00
local lastMove = meta:get_string("lastMove")
if playerWhite == "" and playerBlack == "" or lastMove == "" then
send_message(playerName, S("Resigning is not possible yet."))
2023-07-12 21:56:06 +03:00
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
if whiteWon then
2023-07-17 16:17:52 +03:00
realchess.resign(meta, "black")
2023-07-12 21:56:06 +03:00
else
2023-07-17 16:17:52 +03:00
realchess.resign(meta, "white")
2023-07-12 21:56:06 +03:00
end
send_message(loser, S("You have resigned."))
2023-07-12 21:56:06 +03:00
if playerWhite ~= playerBlack then
send_message(winner, S("@1 has resigned. You win!", loser))
2023-07-12 21:56:06 +03:00
end
minetest.log("action", "[xdecor] Chess: "..loser.." has resigned from the game against "..winner)
update_formspec(meta)
else
send_message(playerName, S("You can't resign, you're not playing in this game."))
2023-07-12 21:56:06 +03:00
end
return
end
-- Claim or declare draw via the 50-move rule
if fields.draw_50_moves then
local botColor = meta:get_string("botColor")
local lastMove = meta:get_string("lastMove")
if playerWhite == "" and playerBlack == "" or lastMove == "" then
return
end
local currentPlayer
if lastMove == "black" or lastMove == "" then
currentPlayer = "white"
else
currentPlayer = "black"
end
local claimer, other
if (currentPlayer == "white" and playerWhite == playerName) then
claimer = playerWhite
other = playerBlack
elseif (currentPlayer == "black" and playerBlack == playerName) then
claimer = playerBlack
other = playerWhite
else
send_message(playerName, S("You can't claim a draw, it's not your turn!"))
return
end
local halfmoveClock = meta:get_int("halfmoveClock")
if halfmoveClock == 99 then
meta:set_string("drawClaim", "50_move_rule")
update_formspec(meta)
elseif halfmoveClock >= 100 then
meta:set_string("gameResult", "draw")
meta:set_string("gameResultReason", "50_move_rule")
add_special_to_moves_list(meta, "draw")
update_formspec(meta)
send_message(claimer, S("You have drawn the game by invoking the 50-move rule."), botColor)
if claimer ~= other then
send_message(other, S("@1 has drawn the game by invoking the 50-move rule.", claimer), botColor)
end
minetest.log("action", "[xdecor] Chess: A game between "..playerWhite.." and "..playerBlack.." ended in a draw because "..claimer.." has applied the 50-move rule")
else
send_message(claimer, S("Your draw claim is invalid!"), botColor)
end
return
end
-- Claim or declare draw via the same position rule (same position occured >= 3 times)
if fields.draw_repeat_3 then
local botColor = meta:get_string("botColor")
local lastMove = meta:get_string("lastMove")
if playerWhite == "" and playerBlack == "" or lastMove == "" then
return
end
local currentPlayer
if lastMove == "black" or lastMove == "" then
currentPlayer = "white"
else
currentPlayer = "black"
end
local claimer, other
if (currentPlayer == "white" and playerWhite == playerName) then
claimer = playerWhite
other = playerBlack
elseif (currentPlayer == "black" and playerBlack == playerName) then
claimer = playerBlack
other = playerWhite
else
send_message(playerName, S("You can't claim a draw, it's not your turn!"))
return
end
local positions = get_positions_history(meta)
local maxRepeatedPositions = count_repeated_positions(positions, 3)
if maxRepeatedPositions == 2 then
meta:set_string("drawClaim", "same_position_3")
update_formspec(meta)
elseif maxRepeatedPositions >= 3 then
meta:set_string("gameResult", "draw")
meta:set_string("gameResultReason", "same_position_3")
add_special_to_moves_list(meta, "draw")
update_formspec(meta)
send_message(claimer, S("You have drawn the game by invoking the same position rule."), botColor)
if claimer ~= other then
send_message(other, S("@1 has drawn the game by invoking the same position rule.", claimer), botColor)
end
minetest.log("action", "[xdecor] Chess: A game between "..playerWhite.." and "..playerBlack.." ended in a draw because "..claimer.." has applied the same position rule")
else
send_message(claimer, S("Your draw claim is invalid!"), botColor)
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
2023-07-15 11:15:45 +03:00
local gameResult = meta:get_string("gameResult")
2023-07-15 09:28:18 +03:00
if gameResult ~= "" then
return
end
local promo = promotions[p]
if fields["p_"..promo] then
if not (playerName == playerWhite or playerName == playerBlack) then
send_message(playerName, 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
send_message(playerName, S("This isn't the time for promotion."))
return
elseif activePromo ~= pcolor then
send_message(playerName, 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
send_message(playerName, S("It's not your turn! This promotion is meant for the other player."))
return
end
end
2018-08-15 02:54:56 +03:00
end
end
function realchess.dig(pos, player)
if not player then
return false
end
2018-08-15 02:54:56 +03:00
2018-08-24 20:38:49 +03:00
local meta = minetest.get_meta(pos)
local playerName = player:get_player_name()
2023-07-17 14:50:03 +03:00
local timeout_limit = meta:get_int("lastMoveTime") + TIMEOUT
2018-08-24 20:38:49 +03:00
local lastMoveTime = meta:get_int("lastMoveTime")
2023-07-12 12:30:15 +03:00
local playerWhite = meta:get_string("playerWhite")
local playerBlack = meta:get_string("playerBlack")
local botColor = meta:get_string("botColor")
2023-07-17 14:50:03 +03:00
-- If the game is ongoing and no move was made for TIMEOUT seconds,
-- the board is free to be dug
2023-07-12 12:30:15 +03:00
if (lastMoveTime == 0 and minetest.get_gametime() > timeout_limit) then
return true
else
if playerName == playerWhite or playerName == playerBlack or botColor == "both" then
send_message(playerName,
2023-07-12 12:30:15 +03:00
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
send_message(playerName,
2023-07-12 12:30:15 +03:00
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()
local pieceTo = inv:get_stack(to_list, to_index):get_name()
-- Update inventory slots
inv:set_stack(from_list, from_index, "")
inv:set_stack(to_list, to_index, pieceFrom)
-- Report the eaten piece
if pieceTo ~= "" then
add_to_eaten_list(meta, pieceTo)
end
local promo = meta:get_string("promotionActive") ~= ""
if not promo then
update_game_result(meta)
end
update_formspec(meta)
2023-07-16 21:16:57 +03:00
local botColor = meta:get_string("botColor")
if botColor == "" then botColor = "black" end
2023-07-13 00:53:31 +03:00
local lastMove = meta:get_string("lastMove")
if lastMove == "" then lastMove = "black" end
local mode = meta:get_string("mode")
local gameResult = meta:get_string("gameResult")
2023-07-16 21:16:57 +03:00
-- Let the bot play when it its turn
if (mode == "bot_vs_bot" or (mode == "single" and lastMove ~= botColor)) and gameResult == "" then
2023-07-16 12:48:50 +03:00
if not promo then
2023-07-17 16:17:52 +03:00
chessbot.move(inv, meta)
2023-07-16 12:48:50 +03:00
else
2023-07-17 16:17:52 +03:00
chessbot.promote(inv, meta, to_index)
2023-07-16 12:48:50 +03:00
end
end
end
2023-07-15 18:43:25 +03:00
function realchess.update_state(meta, from_index, to_index, thisMove, promoteFrom, promoteTo)
local inv = meta:get_inventory()
2023-07-17 16:17:52 +03:00
local board = realchess.board_to_table(inv)
local pieceTo = board[to_index]
2023-07-15 18:43:25 +03:00
local pieceFrom = promoteFrom or board[from_index]
2023-07-15 18:43:25 +03:00
if not promoteFrom then
2023-07-12 09:29:26 +03:00
board[to_index] = board[from_index]
board[from_index] = ""
end
2023-07-17 16:17:52 +03:00
local black_king_idx, white_king_idx = realchess.locate_kings(board)
if not black_king_idx or not white_king_idx then
minetest.log("error", "[xdecor] Chess: Insufficient kings on chessboard!")
return
end
2023-07-17 16:17:52 +03:00
local blackAttacked = realchess.attacked("black", black_king_idx, board)
local whiteAttacked = realchess.attacked("white", white_king_idx, board)
if blackAttacked then
2023-07-12 09:29:26 +03:00
meta:set_string("blackAttacked", "true")
else
meta:set_string("blackAttacked", "")
end
if whiteAttacked then
2023-07-12 09:29:26 +03:00
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())
2023-07-15 18:43:25 +03:00
local special
if promoteTo then
special = "promo__"..promoteTo
end
add_move_to_moves_list(meta, pieceFrom, pieceTo, from_index, to_index, special)
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
2023-07-15 18:43:25 +03:00
pstr = "realchess:" .. pstr
local promoteFrom
if color == "white" then
2023-07-15 18:43:25 +03:00
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
2023-07-15 18:43:25 +03:00
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)
2023-07-15 18:43:25 +03:00
realchess.update_state(meta, from_idx, to_idx, color, promoteFrom:get_name(), pstr)
update_formspec(meta)
2023-07-16 21:16:57 +03:00
local botColor = meta:get_string("botColor")
if botColor == "" then botColor = "black" end
2023-07-13 00:53:31 +03:00
local lastMove = meta:get_string("lastMove")
if lastMove == "" then lastMove = "black" end
local mode = meta:get_string("mode")
local gameResult = meta:get_string("gameResult")
if (mode == "bot_vs_bot" or (mode == "single" and lastMove ~= botColor)) and gameResult == "" then
2023-07-17 16:17:52 +03:00
chessbot.move(inv, meta)
end
else
minetest.log("error", "[xdecor] Chess: Could not find pawn to promote!")
end
end
2023-07-01 10:34:49 +03:00
function realchess.blast(pos)
minetest.remove_node(pos)
end
2023-07-01 17:23:50 +03:00
local chessboarddef = {
description = S("Chess Board"),
drawtype = "nodebox",
paramtype = "light",
paramtype2 = "facedir",
inventory_image = "chessboard_top.png",
wield_image = "chessboard_top.png",
2015-11-22 19:02:58 +03:00
tiles = {"chessboard_top.png", "chessboard_top.png", "chessboard_sides.png"},
2022-02-26 14:03:20 +03:00
use_texture_alpha = ALPHA_OPAQUE,
2015-11-16 00:55:12 +03:00
groups = {choppy=3, oddly_breakable_by_hand=2, flammable=3},
is_ground_content = false,
sounds = default.node_sound_wood_defaults(),
2015-11-22 19:02:58 +03:00
node_box = {type = "fixed", fixed = {-.375, -.5, -.375, .375, -.4375, .375}},
sunlight_propagates = true,
on_rotate = screwdriver.rotate_simple,
2023-07-01 17:23:50 +03:00
}
if ENABLE_CHESS_GAMES then
-- Extend chess board node definition if chess games are enabled
2023-07-10 20:52:38 +03:00
chessboarddef._tt_help = S("Play a game of Chess against another player or the computer")
2023-07-01 17:23:50 +03:00
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
2023-07-01 17:23:50 +03:00
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.
2023-07-01 17:23:50 +03:00
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
2023-07-01 17:23:50 +03:00
local meta = minetest.get_meta(pos)
if meta:get_string("formspec", "") then
realchess.init(pos)
end
end,
})
else
minetest.register_lbm({
2023-07-01 17:29:39 +03:00
label = "Clear chessboard formspec+infotext+inventory (disable Chess games)",
2023-07-01 17:23:50 +03:00
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", "")
2023-07-01 17:29:39 +03:00
local inv = meta:get_inventory()
inv:set_size("board", 0)
2023-07-01 17:23:50 +03:00
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
2018-08-15 02:54:56 +03:00
minetest.register_craftitem(":realchess:" .. name .. "_" .. color, {
description = (color == "black") and black_desc or white_desc,
2018-08-15 02:54:56 +03:00
inventory_image = name .. "_" .. color .. ".png",
stack_max = 1,
groups = {not_in_creative_inventory=1}
})
else
for i = 1, count do
2018-08-15 02:54:56 +03:00
minetest.register_craftitem(":realchess:" .. name .. "_" .. color .. "_" .. i, {
description = (color == "black") and black_desc or white_desc,
2018-08-15 02:54:56 +03:00
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"}
}
})