Chess: Implement "5-times same position" rule
This rule draws the game if the same position appeared 5 times (FIDE Laws of Chess Jan 2023, article 9.6.1)
This commit is contained in:
parent
2e28acf00e
commit
6c9d60ca1d
313
src/chess.lua
313
src/chess.lua
@ -14,7 +14,7 @@ screwdriver = screwdriver or {}
|
|||||||
local ENABLE_CHESS_GAMES = true
|
local ENABLE_CHESS_GAMES = true
|
||||||
|
|
||||||
-- If true, will show some hidden state for debugging purposes
|
-- If true, will show some hidden state for debugging purposes
|
||||||
local CHESS_DEBUG = true
|
local CHESS_DEBUG = false
|
||||||
|
|
||||||
local function index_to_xy(idx)
|
local function index_to_xy(idx)
|
||||||
if not idx then
|
if not idx then
|
||||||
@ -61,6 +61,7 @@ function get_square_index_color(idx)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local chat_prefix = minetest.colorize("#FFFF00", "["..S("Chess").."] ")
|
local chat_prefix = minetest.colorize("#FFFF00", "["..S("Chess").."] ")
|
||||||
|
local chat_prefix_debug = minetest.colorize("#FFFF00", "["..S("Chess Debug").."] ")
|
||||||
local letters = {'a','b','c','d','e','f','g','h'}
|
local letters = {'a','b','c','d','e','f','g','h'}
|
||||||
|
|
||||||
local function index_to_notation(idx)
|
local function index_to_notation(idx)
|
||||||
@ -177,6 +178,59 @@ local function get_current_fullmove(meta)
|
|||||||
return math.floor(#mrsplit / 2)
|
return math.floor(#mrsplit / 2)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Returns a FEN-style string to represent castling rights
|
||||||
|
-- Will return a sequence of K, Q, k and q, with each letter
|
||||||
|
-- representing castling rights:
|
||||||
|
-- * K: white kingside
|
||||||
|
-- * Q: white queenside
|
||||||
|
-- * k: black kingside
|
||||||
|
-- * q: black queenside
|
||||||
|
-- If all castling rights are gone, will return "-" instead.
|
||||||
|
-- The 4 arguments are booleans for each possible castling,
|
||||||
|
-- true means the castling is possible.
|
||||||
|
local function castling_to_string(white_kingside, white_queenside, black_kingside, black_queenside)
|
||||||
|
local s_castling = ""
|
||||||
|
if white_kingside then
|
||||||
|
s_castling = s_castling .. "K"
|
||||||
|
end
|
||||||
|
if white_queenside then
|
||||||
|
s_castling = s_castling .. "Q"
|
||||||
|
end
|
||||||
|
if black_kingside then
|
||||||
|
s_castling = s_castling .. "k"
|
||||||
|
end
|
||||||
|
if black_queenside then
|
||||||
|
s_castling = s_castling .. "q"
|
||||||
|
end
|
||||||
|
if s_castling == "" then
|
||||||
|
s_castling = "-"
|
||||||
|
end
|
||||||
|
return s_castling
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Returns a FEN-style string to represent the state of a theoretically
|
||||||
|
-- possible en passant capture on the board (even if no pawn can actually
|
||||||
|
-- capture). If an en passant capture is possible, returns the square
|
||||||
|
-- coordinates in algebraic notation of the square the vulnerable pawn
|
||||||
|
-- has just crossed. If no en passant capture is possible, returns "-".
|
||||||
|
-- double_step is the board index of the square the vulnerable
|
||||||
|
-- pawn has just double-stepped to or 0 if there is no such pawn.
|
||||||
|
local function en_passant_to_string(double_step)
|
||||||
|
local s_en_passant = "-"
|
||||||
|
if double_step ~= 0 and double_step ~= nil then
|
||||||
|
-- write the square crossed by the pawn who made
|
||||||
|
-- the double step
|
||||||
|
local dsx, dsy = index_to_xy(double_step)
|
||||||
|
if dsy == 3 then
|
||||||
|
dsy = dsy - 1
|
||||||
|
else
|
||||||
|
dsy = dsy + 1
|
||||||
|
end
|
||||||
|
s_en_passant = index_to_notation(xy_to_index(dsx, dsy))
|
||||||
|
end
|
||||||
|
return s_en_passant
|
||||||
|
end
|
||||||
|
|
||||||
local function can_castle(meta, board, from_list, from_idx, to_idx)
|
local function can_castle(meta, board, from_list, from_idx, to_idx)
|
||||||
local from_x, from_y = index_to_xy(from_idx)
|
local from_x, from_y = index_to_xy(from_idx)
|
||||||
local to_x, to_y = index_to_xy(to_idx)
|
local to_x, to_y = index_to_xy(to_idx)
|
||||||
@ -1009,6 +1063,189 @@ local function add_special_to_moves_list(meta, special)
|
|||||||
add_move_to_moves_list(meta, "", "", "", "", special)
|
add_move_to_moves_list(meta, "", "", "", "", special)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Returns a list of all positions so far, for the purposes
|
||||||
|
-- of determining position equality under the "same position
|
||||||
|
-- repeated X times" draw rule.
|
||||||
|
-- Each possible position is uniquely identified by a string
|
||||||
|
-- so equal positions have the same string and unequal positons
|
||||||
|
-- have a different string.
|
||||||
|
-- A position string containts the following data:
|
||||||
|
-- * position of pieces on the board
|
||||||
|
-- * current player
|
||||||
|
-- * castling rights
|
||||||
|
-- * target coords of the square crossed by the pawn who
|
||||||
|
-- made a double step in the prvious turn (if any)
|
||||||
|
--
|
||||||
|
-- Patemeter: meta is the node metadata of the chessboard
|
||||||
|
--
|
||||||
|
-- NOTE: The FIDE Laws of Chess (Jan 2023, article 9.2.3.1)
|
||||||
|
-- are somewhat unclear about en passant here ... Is it important
|
||||||
|
-- the pawn was only theoretically vulnerable to being
|
||||||
|
-- captured en passant without being actually threatened
|
||||||
|
-- that way, or does it only count if the pawn was actually
|
||||||
|
-- threatened by another pawn to be captured that way?
|
||||||
|
-- This mod currently interprets the rule in the former way,
|
||||||
|
-- i.e. a double step by a pawn
|
||||||
|
local function get_positions_history(meta)
|
||||||
|
-- Turns a board table to a string.
|
||||||
|
-- The syntax is inspired by FEN but not identical.
|
||||||
|
-- It iterates through the table from start
|
||||||
|
-- to finish and turns every square to a character,
|
||||||
|
-- representing a piece or an empty square.
|
||||||
|
local function board_to_string(board)
|
||||||
|
local str = ""
|
||||||
|
for b=1, #board do
|
||||||
|
local piece = board[b]
|
||||||
|
local append
|
||||||
|
if piece == "" then
|
||||||
|
str = str .. "."
|
||||||
|
elseif piece:find("white") then
|
||||||
|
if piece:find("pawn") then
|
||||||
|
str = str .. "P"
|
||||||
|
elseif piece:find("bishop") then
|
||||||
|
str = str .. "B"
|
||||||
|
elseif piece:find("knight") then
|
||||||
|
str = str .. "N"
|
||||||
|
elseif piece:find("rook") then
|
||||||
|
str = str .. "R"
|
||||||
|
elseif piece:find("queen") then
|
||||||
|
str = str .. "Q"
|
||||||
|
elseif piece:find("king") then
|
||||||
|
str = str .. "K"
|
||||||
|
end
|
||||||
|
elseif piece:find("black") then
|
||||||
|
if piece:find("pawn") then
|
||||||
|
str = str .. "p"
|
||||||
|
elseif piece:find("bishop") then
|
||||||
|
str = str .. "b"
|
||||||
|
elseif piece:find("knight") then
|
||||||
|
str = str .. "n"
|
||||||
|
elseif piece:find("rook") then
|
||||||
|
str = str .. "r"
|
||||||
|
elseif piece:find("queen") then
|
||||||
|
str = str .. "q"
|
||||||
|
elseif piece:find("king") then
|
||||||
|
str = str .. "k"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return str
|
||||||
|
end
|
||||||
|
|
||||||
|
local moves_raw = meta:get_string("moves_raw")
|
||||||
|
local moves_split = string.split(moves_raw, ";")
|
||||||
|
local positions_list = ""
|
||||||
|
local board = table.copy(starting_grid)
|
||||||
|
local castling_state = { true, true, true, true }
|
||||||
|
local castling_str = castling_to_string(unpack(castling_state))
|
||||||
|
positions_list = {}
|
||||||
|
|
||||||
|
local current_player = "w"
|
||||||
|
local position_string = board_to_string(board) .. " " ..
|
||||||
|
current_player .. " " ..
|
||||||
|
castling_str .. " " ..
|
||||||
|
en_passant_to_string(nil)
|
||||||
|
table.insert(positions_list, position_string)
|
||||||
|
for m=1, #moves_split do
|
||||||
|
local move_split = string.split(moves_split[m], ",", true)
|
||||||
|
local pieceFrom = move_split[1]
|
||||||
|
local pieceTo = move_split[2]
|
||||||
|
local from_idx = tonumber(move_split[3])
|
||||||
|
local to_idx = tonumber(move_split[4])
|
||||||
|
local special = move_split[5]
|
||||||
|
if special == "" or special:sub(1,7) == "promo__" then
|
||||||
|
|
||||||
|
if current_player == "w" then
|
||||||
|
current_player = "b"
|
||||||
|
else
|
||||||
|
current_player = "w"
|
||||||
|
end
|
||||||
|
-- Piece movement
|
||||||
|
board[to_idx] = board[from_idx]
|
||||||
|
board[from_idx] = ""
|
||||||
|
|
||||||
|
-- Pawn promotion
|
||||||
|
if special:sub(1, 7) == "promo__" then
|
||||||
|
local promoSym = special:sub(8)
|
||||||
|
board[to_idx] = promoSym
|
||||||
|
end
|
||||||
|
|
||||||
|
local from_x, from_y = index_to_xy(from_idx)
|
||||||
|
local to_x, to_y = index_to_xy(to_idx)
|
||||||
|
if pieceFrom:sub(11,14) == "king" then
|
||||||
|
-- Castling (move rook)
|
||||||
|
if (from_y == 7 and to_y == 7) then
|
||||||
|
if (from_x == 4 and to_x == 2) then
|
||||||
|
board[60] = board[57]
|
||||||
|
board[57] = ""
|
||||||
|
elseif (from_x == 4 and to_x == 6) then
|
||||||
|
board[62] = board[64]
|
||||||
|
board[64] = ""
|
||||||
|
end
|
||||||
|
elseif (from_y == 0 and to_y == 0) then
|
||||||
|
if (from_x == 4 and to_x == 2) then
|
||||||
|
board[4] = board[1]
|
||||||
|
board[1] = ""
|
||||||
|
elseif (from_x == 4 and to_x == 6) then
|
||||||
|
board[6] = board[8]
|
||||||
|
board[8] = ""
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- Lose castling rights on any king move
|
||||||
|
if pieceFrom:find("white") then
|
||||||
|
castling_state[1] = false
|
||||||
|
castling_state[2] = false
|
||||||
|
else
|
||||||
|
castling_state[3] = false
|
||||||
|
castling_state[4] = false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Lose castling rights on lone rook move
|
||||||
|
elseif pieceFrom:sub(11,14) == "rook" then
|
||||||
|
if from_idx == 57 then
|
||||||
|
-- white queenside
|
||||||
|
castling_state[2] = false
|
||||||
|
elseif from_idx == 64 then
|
||||||
|
-- white kingside
|
||||||
|
castling_state[1] = false
|
||||||
|
elseif from_idx == 1 then
|
||||||
|
-- black queenside
|
||||||
|
castling_state[4] = false
|
||||||
|
elseif from_idx == 8 then
|
||||||
|
-- black kingside
|
||||||
|
castling_state[3] = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local pawn_double_step_index
|
||||||
|
if pieceTo == "" and pieceFrom:sub(11,14) == "pawn" then
|
||||||
|
-- En passant (remove captured pawn)
|
||||||
|
if from_x ~= to_x then
|
||||||
|
local epp_y
|
||||||
|
if pieceFrom:find("white") then
|
||||||
|
epp_y = to_y + 1
|
||||||
|
else
|
||||||
|
epp_y = to_y - 1
|
||||||
|
end
|
||||||
|
board[xy_to_index(to_x, epp_y)] = ""
|
||||||
|
-- Double pawn step (record positoin)
|
||||||
|
elseif math.abs(from_y-to_y) == 2 then
|
||||||
|
pawn_double_step_index = to_idx
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
castling_str = castling_to_string(unpack(castling_state))
|
||||||
|
local position_string = board_to_string(board) .. " " ..
|
||||||
|
current_player .. " " ..
|
||||||
|
castling_str .. " " ..
|
||||||
|
en_passant_to_string(pawn_double_step_index)
|
||||||
|
table.insert(positions_list, position_string)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local p=#positions_list
|
||||||
|
return positions_list
|
||||||
|
end
|
||||||
|
|
||||||
-- Create the full formspec string for the sequence of moves.
|
-- Create the full formspec string for the sequence of moves.
|
||||||
-- Uses Figurine Algebraic Notation.
|
-- Uses Figurine Algebraic Notation.
|
||||||
local function get_moves_formstring(meta)
|
local function get_moves_formstring(meta)
|
||||||
@ -1281,36 +1518,15 @@ local function update_formspec(meta)
|
|||||||
d_turn = "w"
|
d_turn = "w"
|
||||||
end
|
end
|
||||||
-- castling rights
|
-- castling rights
|
||||||
local d_castling = ""
|
local d_castling = castling_to_string(
|
||||||
if meta:get_int("castlingWhiteR") == 1 then
|
meta:get_int("castlingWhiteR") == 1,
|
||||||
d_castling = d_castling .. "K"
|
meta:get_int("castlingWhiteL") == 1,
|
||||||
end
|
meta:get_int("castlingBlackR") == 1,
|
||||||
if meta:get_int("castlingWhiteL") == 1 then
|
meta:get_int("castlingBlackL") == 1)
|
||||||
d_castling = d_castling .. "Q"
|
|
||||||
end
|
|
||||||
if meta:get_int("castlingBlackR") == 1 then
|
|
||||||
d_castling = d_castling .. "k"
|
|
||||||
end
|
|
||||||
if meta:get_int("castlingBlackL") == 1 then
|
|
||||||
d_castling = d_castling .. "q"
|
|
||||||
end
|
|
||||||
if d_castling == "" then
|
|
||||||
d_castling = "-"
|
|
||||||
end
|
|
||||||
-- en passant possible?
|
-- en passant possible?
|
||||||
local double_step = meta:get_int("prevDoublePawnStepTo")
|
local double_step = meta:get_int("prevDoublePawnStepTo")
|
||||||
local d_en_passant = "-"
|
local d_en_passant = en_passant_to_string(double_step)
|
||||||
if double_step ~= 0 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
|
|
||||||
d_en_passant = index_to_notation(xy_to_index(dsx, dsy))
|
|
||||||
end
|
|
||||||
-- The halfmove clock counts for how many consecutive halfmoves
|
-- The halfmove clock counts for how many consecutive halfmoves
|
||||||
-- have been made with no pawn advancing and no piece being captured
|
-- have been made with no pawn advancing and no piece being captured
|
||||||
local d_halfmove_clock = meta:get_int("halfmoveClock")
|
local d_halfmove_clock = meta:get_int("halfmoveClock")
|
||||||
@ -1469,6 +1685,45 @@ local function update_game_result(meta)
|
|||||||
minetest.log("action", "[xdecor] Chess: A game between "..playerWhite.." and "..playerBlack.." ended in a draw via the 75-move rule")
|
minetest.log("action", "[xdecor] Chess: A game between "..playerWhite.." and "..playerBlack.." ended in a draw via the 75-move rule")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local repetitionDraw = false
|
||||||
|
local positions = get_positions_history(meta)
|
||||||
|
local positions_counter = {}
|
||||||
|
for p = 1, #positions do
|
||||||
|
local position = positions[p]
|
||||||
|
if positions_counter[position] == nil then
|
||||||
|
positions_counter[position] = 1
|
||||||
|
else
|
||||||
|
positions_counter[position] = positions_counter[position] + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
if CHESS_DEBUG and p == #positions then
|
||||||
|
local msg = chat_prefix_debug .. "Current position: \"" .. position .. "\""
|
||||||
|
if positions_counter[position] > 1 then
|
||||||
|
msg = msg .. " (occurred "..positions_counter[position].." times)"
|
||||||
|
end
|
||||||
|
minetest.chat_send_player(playerWhite, msg)
|
||||||
|
if playerWhite ~= playerBlack then
|
||||||
|
minetest.chat_send_player(playerBlack, msg)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if positions_counter[position] == 5 then
|
||||||
|
repetitionDraw = true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if repetitionDraw then
|
||||||
|
meta:set_string("gameResult", "draw")
|
||||||
|
meta:set_string("gameResultReason", "same_position_5")
|
||||||
|
add_special_to_moves_list(meta, "draw")
|
||||||
|
local msg = S("The exact same position has occured 5 times. It's a draw!")
|
||||||
|
minetest.chat_send_player(playerWhite, chat_prefix .. msg)
|
||||||
|
if playerWhite ~= playerBlack then
|
||||||
|
minetest.chat_send_player(playerBlack, chat_prefix .. msg)
|
||||||
|
end
|
||||||
|
minetest.log("action", "[xdecor] Chess: A game between "..playerWhite.." and "..playerBlack.." ended in a draw because the same position has appeared 5 times")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user