2 screwdriver = screwdriver or {}
4 local function index_to_xy(idx)
7 local y = (idx - x) / 8
11 local function xy_to_index(x, y)
15 local function get_square(a, b)
16 return (a * 8) - (8 - b)
19 local chat_prefix = minetest.colorize("#FFFF00", "[Chess] ")
20 local letters = {'A','B','C','D','E','F','G','H'}
22 local rowDirs = {-1, -1, -1, 0, 0, 1, 1, 1}
23 local colDirs = {-1, 0, 1, -1, 1, -1, 0, 1}
25 local bishopThreats = {true, false, true, false, false, true, false, true}
26 local rookThreats = {false, true, false, true, true, false, true, false}
27 local queenThreats = {true, true, true, true, true, true, true, true}
28 local kingThreats = {true, true, true, true, true, true, true, true}
30 local function king_attack(color, idx, inv)
31 local threatDetected = false
32 local kill = color == "white"
33 local pawnThreats = {kill, false, kill, false, false, not kill, false, not kill}
36 if not threatDetected then
37 local col, row = index_to_xy(idx)
38 col, row = col + 1, row + 1
41 row = row + rowDirs[dir]
42 col = col + colDirs[dir]
44 if row >= 1 and row <= 8 and col >= 1 and col <= 8 then
45 local square = get_square(row, col)
46 local square_name = inv:get_stack("board", square):get_name()
47 local piece, pieceColor = square_name:match(":(%w+)_(%w+)")
50 if pieceColor ~= color then
51 if piece == "bishop" and bishopThreats[dir] then
53 elseif piece == "rook" and rookThreats[dir] then
55 elseif piece == "queen" and queenThreats[dir] then
59 if piece == "pawn" and pawnThreats[dir] then
62 if piece == "king" and kingThreats[dir] then
78 local function locate_kings(inv)
81 local piece, color = inv:get_stack("board", i):get_name():match(":(%w+)_(%w+)")
82 if piece == "king" then
83 if color == "black" then
95 "realchess:rook_black_1",
96 "realchess:knight_black_1",
97 "realchess:bishop_black_1",
98 "realchess:queen_black",
99 "realchess:king_black",
100 "realchess:bishop_black_2",
101 "realchess:knight_black_2",
102 "realchess:rook_black_2",
103 "realchess:pawn_black_1",
104 "realchess:pawn_black_2",
105 "realchess:pawn_black_3",
106 "realchess:pawn_black_4",
107 "realchess:pawn_black_5",
108 "realchess:pawn_black_6",
109 "realchess:pawn_black_7",
110 "realchess:pawn_black_8",
111 '','','','','','','','','','','','','','','','',
112 '','','','','','','','','','','','','','','','',
113 "realchess:pawn_white_1",
114 "realchess:pawn_white_2",
115 "realchess:pawn_white_3",
116 "realchess:pawn_white_4",
117 "realchess:pawn_white_5",
118 "realchess:pawn_white_6",
119 "realchess:pawn_white_7",
120 "realchess:pawn_white_8",
121 "realchess:rook_white_1",
122 "realchess:knight_white_1",
123 "realchess:bishop_white_1",
124 "realchess:queen_white",
125 "realchess:king_white",
126 "realchess:bishop_white_2",
127 "realchess:knight_white_2",
128 "realchess:rook_white_2"
131 local pieces_str, x = "", 0
132 for i = 1, #pieces do
133 local p = pieces[i]:match(":(%w+_%w+)")
134 if pieces[i]:find(":(%w+)_(%w+)") and not pieces_str:find(p) then
135 pieces_str = pieces_str .. x .. "=" .. p .. ".png,"
139 pieces_str = pieces_str .. "69=mailbox_blank16.png"
144 bgcolor[#080808BB;true]
145 background[0,0;14.7,10;chess_bg.png]
146 list[context;board;0.3,1;8,8;]
147 listcolors[#00000000;#00000000;#00000000;#30434C;#FFF]
148 tableoptions[background=#00000000;highlight=#00000000;border=false]
149 button[10.5,8.5;2,2;new;New game]
150 ]] .. "tablecolumns[image," .. pieces_str ..
151 ";text;color;text;color;text;image," .. pieces_str .. "]"
153 local function get_moves_list(meta, pieceFrom, pieceTo, pieceTo_s, from_x, to_x, from_y, to_y)
154 local moves = meta:get_string("moves")
155 local pieceFrom_s = pieceFrom:match(":(%w+_%w+)")
156 local pieceFrom_si_id = pieces_str:match("(%d+)=" .. pieceFrom_s)
157 local pieceTo_si_id = pieceTo_s ~= "" and pieces_str:match("(%d+)=" .. pieceTo_s) or ""
159 local coordFrom = letters[from_x + 1] .. math.abs(from_y - 8)
160 local coordTo = letters[to_x + 1] .. math.abs(to_y - 8)
162 local new_moves = pieceFrom_si_id .. "," ..
164 (pieceTo ~= "" and "#33FF33" or "#FFFFFF") .. ", > ,#FFFFFF," ..
166 (pieceTo ~= "" and pieceTo_si_id or "69") .. "," ..
169 meta:set_string("moves", new_moves)
172 local function get_eaten_list(meta, pieceTo, pieceTo_s)
173 local eaten = meta:get_string("eaten")
174 if pieceTo ~= "" then
175 eaten = eaten .. pieceTo_s .. ","
178 meta:set_string("eaten", eaten)
180 local eaten_t = string.split(eaten, ",")
184 for i = 1, #eaten_t do
185 local is_white = eaten_t[i]:sub(-5,-1) == "white"
186 local X = (is_white and a or b) % 4
187 local Y = ((is_white and a or b) % 16 - X) / 4
195 eaten_img = eaten_img ..
196 "image[" .. ((X + (is_white and 11.7 or 8.8)) - (X * 0.45)) .. "," ..
197 ((Y + 5.56) - (Y * 0.2)) .. ";1,1;" .. eaten_t[i] .. ".png]"
200 meta:set_string("eaten_img", eaten_img)
203 function realchess.init(pos)
204 local meta = minetest.get_meta(pos)
205 local inv = meta:get_inventory()
207 meta:set_string("formspec", fs)
208 meta:set_string("infotext", "Chess Board")
209 meta:set_string("playerBlack", "")
210 meta:set_string("playerWhite", "")
211 meta:set_string("lastMove", "")
213 meta:set_int("lastMoveTime", 0)
214 meta:set_int("castlingBlackL", 1)
215 meta:set_int("castlingBlackR", 1)
216 meta:set_int("castlingWhiteL", 1)
217 meta:set_int("castlingWhiteR", 1)
219 meta:set_string("moves", "")
220 meta:set_string("eaten", "")
222 inv:set_list("board", pieces)
223 inv:set_size("board", 64)
226 function realchess.move(pos, from_list, from_index, to_list, to_index, _, player)
227 if from_list ~= "board" and to_list ~= "board" then
231 local meta = minetest.get_meta(pos)
232 local playerName = player:get_player_name()
233 local inv = meta:get_inventory()
234 local pieceFrom = inv:get_stack(from_list, from_index):get_name()
235 local pieceTo = inv:get_stack(to_list, to_index):get_name()
236 local lastMove = meta:get_string("lastMove")
237 local playerWhite = meta:get_string("playerWhite")
238 local playerBlack = meta:get_string("playerBlack")
239 local thisMove -- Will replace lastMove when move is legal
241 if pieceFrom:find("white") then
242 if playerWhite ~= "" and playerWhite ~= playerName then
243 minetest.chat_send_player(playerName, chat_prefix .. "Someone else plays white pieces!")
247 if lastMove ~= "" and lastMove ~= "black" then
251 if pieceTo:find("white") then
252 -- Don't replace pieces of same color
256 playerWhite = playerName
259 elseif pieceFrom:find("black") then
260 if playerBlack ~= "" and playerBlack ~= playerName then
261 minetest.chat_send_player(playerName, chat_prefix .. "Someone else plays black pieces!")
265 if lastMove ~= "" and lastMove ~= "white" then
269 if pieceTo:find("black") then
270 -- Don't replace pieces of same color
274 playerBlack = playerName
280 local from_x, from_y = index_to_xy(from_index)
281 local to_x, to_y = index_to_xy(to_index)
284 if pieceFrom:sub(11,14) == "pawn" then
285 if thisMove == "white" then
286 local pawnWhiteMove = inv:get_stack(from_list, xy_to_index(from_x, from_y - 1)):get_name()
287 -- white pawns can go up only
288 if from_y - 1 == to_y then
289 if from_x == to_x then
290 if pieceTo ~= "" then
292 elseif to_index >= 1 and to_index <= 8 then
293 inv:set_stack(from_list, from_index, "realchess:queen_white")
295 elseif from_x - 1 == to_x or from_x + 1 == to_x then
296 if not pieceTo:find("black") then
298 elseif to_index >= 1 and to_index <= 8 then
299 inv:set_stack(from_list, from_index, "realchess:queen_white")
304 elseif from_y - 2 == to_y then
305 if pieceTo ~= "" or from_y < 6 or pawnWhiteMove ~= "" then
314 ensure that destination cell is empty
315 elseif x changed one unit left or right
316 ensure the pawn is killing opponent piece
318 move is not legal - abort
321 if from_x == to_x then
322 if pieceTo ~= "" then
325 elseif from_x - 1 == to_x or from_x + 1 == to_x then
326 if not pieceTo:find("black") then
333 elseif thisMove == "black" then
334 local pawnBlackMove = inv:get_stack(from_list, xy_to_index(from_x, from_y + 1)):get_name()
335 -- black pawns can go down only
336 if from_y + 1 == to_y then
337 if from_x == to_x then
338 if pieceTo ~= "" then
340 elseif to_index >= 57 and to_index <= 64 then
341 inv:set_stack(from_list, from_index, "realchess:queen_black")
343 elseif from_x - 1 == to_x or from_x + 1 == to_x then
344 if not pieceTo:find("white") then
346 elseif to_index >= 57 and to_index <= 64 then
347 inv:set_stack(from_list, from_index, "realchess:queen_black")
352 elseif from_y + 2 == to_y then
353 if pieceTo ~= "" or from_y > 1 or pawnBlackMove ~= "" then
362 ensure that destination cell is empty
363 elseif x changed one unit left or right
364 ensure the pawn is killing opponent piece
366 move is not legal - abort
369 if from_x == to_x then
370 if pieceTo ~= "" then
373 elseif from_x - 1 == to_x or from_x + 1 == to_x then
374 if not pieceTo:find("white") then
385 elseif pieceFrom:sub(11,14) == "rook" then
386 if from_x == to_x then
388 if from_y < to_y then
390 -- Ensure that no piece disturbs the way
391 for i = from_y + 1, to_y - 1 do
392 if inv:get_stack(from_list, xy_to_index(from_x, i)):get_name() ~= "" then
398 -- Ensure that no piece disturbs the way
399 for i = to_y + 1, from_y - 1 do
400 if inv:get_stack(from_list, xy_to_index(from_x, i)):get_name() ~= "" then
405 elseif from_y == to_y then
406 -- Mocing horizontally
407 if from_x < to_x then
409 -- ensure that no piece disturbs the way
410 for i = from_x + 1, to_x - 1 do
411 if inv:get_stack(from_list, xy_to_index(i, from_y)):get_name() ~= "" then
417 -- Ensure that no piece disturbs the way
418 for i = to_x + 1, from_x - 1 do
419 if inv:get_stack(from_list, xy_to_index(i, from_y)):get_name() ~= "" then
425 -- Attempt to move arbitrarily -> abort
429 if thisMove == "white" or thisMove == "black" then
430 if pieceFrom:sub(-1) == "1" then
431 meta:set_int("castlingWhiteL", 0)
432 elseif pieceFrom:sub(-1) == "2" then
433 meta:set_int("castlingWhiteR", 0)
438 elseif pieceFrom:sub(11,16) == "knight" then
440 local dx = from_x - to_x
441 local dy = from_y - to_y
443 -- Get absolute values
444 if dx < 0 then dx = -dx end
445 if dy < 0 then dy = -dy end
448 if dx > dy then dx, dy = dy, dx end
450 -- Ensure that dx == 1 and dy == 2
451 if dx ~= 1 or dy ~= 2 then
454 -- Just ensure that destination cell does not contain friend piece
455 -- ^ It was done already thus everything ok
458 elseif pieceFrom:sub(11,16) == "bishop" then
460 local dx = from_x - to_x
461 local dy = from_y - to_y
463 -- Get absolute values
464 if dx < 0 then dx = -dx end
465 if dy < 0 then dy = -dy end
467 -- Ensure dx and dy are equal
468 if dx ~= dy then return 0 end
470 if from_x < to_x then
471 if from_y < to_y then
473 -- Ensure that no piece disturbs the way
475 if inv:get_stack(from_list, xy_to_index(from_x + i, from_y + i)):get_name() ~= "" then
481 -- Ensure that no piece disturbs the way
483 if inv:get_stack(from_list, xy_to_index(from_x + i, from_y - i)):get_name() ~= "" then
489 if from_y < to_y then
491 -- Ensure that no piece disturbs the way
493 if inv:get_stack(from_list, xy_to_index(from_x - i, from_y + i)):get_name() ~= "" then
499 -- ensure that no piece disturbs the way
501 if inv:get_stack(from_list, xy_to_index(from_x - i, from_y - i)):get_name() ~= "" then
509 elseif pieceFrom:sub(11,15) == "queen" then
510 local dx = from_x - to_x
511 local dy = from_y - to_y
513 -- Get absolute values
514 if dx < 0 then dx = -dx end
515 if dy < 0 then dy = -dy end
517 -- Ensure valid relative move
518 if dx ~= 0 and dy ~= 0 and dx ~= dy then
522 if from_x == to_x then
523 if from_y < to_y then
525 -- Ensure that no piece disturbs the way
527 if inv:get_stack(from_list, xy_to_index(from_x, from_y + i)):get_name() ~= "" then
533 -- Ensure that no piece disturbs the way
535 if inv:get_stack(from_list, xy_to_index(from_x, from_y - i)):get_name() ~= "" then
540 elseif from_x < to_x then
541 if from_y == to_y then
543 -- Ensure that no piece disturbs the way
545 if inv:get_stack(from_list, xy_to_index(from_x + i, from_y)):get_name() ~= "" then
549 elseif from_y < to_y then
551 -- Ensure that no piece disturbs the way
553 if inv:get_stack(from_list, xy_to_index(from_x + i, from_y + i)):get_name() ~= "" then
559 -- Ensure that no piece disturbs the way
561 if inv:get_stack(from_list, xy_to_index(from_x + i, from_y - i)):get_name() ~= "" then
567 if from_y == to_y then
569 -- Ensure that no piece disturbs the way and destination cell does
571 if inv:get_stack(from_list, xy_to_index(from_x - i, from_y)):get_name() ~= "" then
575 elseif from_y < to_y then
577 -- Ensure that no piece disturbs the way
579 if inv:get_stack(from_list, xy_to_index(from_x - i, from_y + i)):get_name() ~= "" then
585 -- Ensure that no piece disturbs the way
587 if inv:get_stack(from_list, xy_to_index(from_x - i, from_y - i)):get_name() ~= "" then
595 elseif pieceFrom:sub(11,14) == "king" then
596 local dx = from_x - to_x
597 local dy = from_y - to_y
600 if thisMove == "white" then
601 if from_y == 7 and to_y == 7 then
603 local castlingWhiteL = meta:get_int("castlingWhiteL")
604 local idx57 = inv:get_stack(from_list, 57):get_name()
606 if castlingWhiteL == 1 and idx57 == "realchess:rook_white_1" then
607 for i = 58, from_index - 1 do
608 if inv:get_stack(from_list, i):get_name() ~= "" then
612 inv:set_stack(from_list, 57, "")
613 inv:set_stack(from_list, 59, "realchess:rook_white_1")
616 elseif to_x == 6 then
617 local castlingWhiteR = meta:get_int("castlingWhiteR")
618 local idx64 = inv:get_stack(from_list, 64):get_name()
620 if castlingWhiteR == 1 and idx64 == "realchess:rook_white_2" then
621 for i = from_index + 1, 63 do
622 if inv:get_stack(from_list, i):get_name() ~= "" then
626 inv:set_stack(from_list, 62, "realchess:rook_white_2")
627 inv:set_stack(from_list, 64, "")
632 elseif thisMove == "black" then
633 if from_y == 0 and to_y == 0 then
635 local castlingBlackL = meta:get_int("castlingBlackL")
636 local idx1 = inv:get_stack(from_list, 1):get_name()
638 if castlingBlackL == 1 and idx1 == "realchess:rook_black_1" then
639 for i = 2, from_index - 1 do
640 if inv:get_stack(from_list, i):get_name() ~= "" then
645 inv:set_stack(from_list, 1, "")
646 inv:set_stack(from_list, 3, "realchess:rook_black_1")
649 elseif to_x == 6 then
650 local castlingBlackR = meta:get_int("castlingBlackR")
651 local idx8 = inv:get_stack(from_list, 1):get_name()
653 if castlingBlackR == 1 and idx8 == "realchess:rook_black_2" then
654 for i = from_index + 1, 7 do
655 if inv:get_stack(from_list, i):get_name() ~= "" then
660 inv:set_stack(from_list, 6, "realchess:rook_black_2")
661 inv:set_stack(from_list, 8, "")
677 if dx > 1 or dy > 1 then
682 if thisMove == "white" then
683 meta:set_int("castlingWhiteL", 0)
684 meta:set_int("castlingWhiteR", 0)
686 local whiteAttacked = king_attack("white", to_index, inv)
687 if whiteAttacked then
691 elseif thisMove == "black" then
692 meta:set_int("castlingBlackL", 0)
693 meta:set_int("castlingBlackR", 0)
695 local blackAttacked = king_attack("black", to_index, inv)
696 if blackAttacked then
704 meta:set_string("lastMove", lastMove)
705 meta:set_int("lastMoveTime", minetest.get_gametime())
706 meta:set_string("playerWhite", playerWhite)
707 meta:set_string("playerBlack", playerBlack)
709 local pieceTo_s = pieceTo ~= "" and pieceTo:match(":(%w+_%w+)") or ""
710 get_moves_list(meta, pieceFrom, pieceTo, pieceTo_s, from_x, to_x, from_y, to_y)
711 get_eaten_list(meta, pieceTo, pieceTo_s)
716 local function timeout_format(timeout_limit)
717 local time_remaining = timeout_limit - minetest.get_gametime()
718 local minutes = math.floor(time_remaining / 60)
719 local seconds = time_remaining % 60
722 return seconds .. " sec."
725 return minutes .. " min. " .. seconds .. " sec."
728 function realchess.fields(pos, _, fields, sender)
729 local playerName = sender:get_player_name()
730 local meta = minetest.get_meta(pos)
731 local timeout_limit = meta:get_int("lastMoveTime") + 300
732 local playerWhite = meta:get_string("playerWhite")
733 local playerBlack = meta:get_string("playerBlack")
734 local lastMoveTime = meta:get_int("lastMoveTime")
735 if fields.quit then return end
737 -- Timeout is 5 min. by default for resetting the game (non-players only)
739 if (playerWhite == playerName or playerBlack == playerName) then
742 elseif lastMoveTime ~= 0 then
743 if minetest.get_gametime() >= timeout_limit and
744 (playerWhite ~= playerName or playerBlack ~= playerName) then
747 minetest.chat_send_player(playerName, chat_prefix ..
748 "You can't reset the chessboard, a game has been started. " ..
749 "If you aren't a current player, try again in " ..
750 timeout_format(timeout_limit))
756 function realchess.dig(pos, player)
761 local meta = minetest.get_meta(pos)
762 local playerName = player:get_player_name()
763 local timeout_limit = meta:get_int("lastMoveTime") + 300
764 local lastMoveTime = meta:get_int("lastMoveTime")
766 -- Timeout is 5 min. by default for digging the chessboard (non-players only)
767 return (lastMoveTime == 0 and minetest.get_gametime() > timeout_limit) or
768 minetest.chat_send_player(playerName, chat_prefix ..
769 "You can't dig the chessboard, a game has been started. " ..
770 "Reset it first if you're a current player, or dig it again in " ..
771 timeout_format(timeout_limit))
774 function realchess.on_move(pos, from_list, from_index)
775 local meta = minetest.get_meta(pos)
776 local inv = minetest.get_meta(pos):get_inventory()
777 inv:set_stack(from_list, from_index, '')
779 local black_king_idx, white_king_idx = locate_kings(inv)
780 local black_king_attacked = king_attack("black", black_king_idx, inv)
781 local white_king_attacked = king_attack("white", white_king_idx, inv)
783 local playerWhite = meta:get_string("playerWhite")
784 local playerBlack = meta:get_string("playerBlack")
786 local moves = meta:get_string("moves")
787 local eaten_img = meta:get_string("eaten_img")
788 local lastMove = meta:get_string("lastMove")
789 local turnBlack = minetest.colorize("#000001", (lastMove == "white" and playerBlack ~= "") and
790 playerBlack .. "..." or playerBlack)
791 local turnWhite = minetest.colorize("#000001", (lastMove == "black" and playerWhite ~= "") and
792 playerWhite .. "..." or playerWhite)
793 local check_s = minetest.colorize("#FF0000", "\\[check\\]")
795 local formspec = fs ..
796 "label[1.9,0.3;" .. turnBlack .. (black_king_attacked and " " .. check_s or "") .. "]" ..
797 "label[1.9,9.15;" .. turnWhite .. (white_king_attacked and " " .. check_s or "") .. "]" ..
798 "table[8.9,1.05;5.07,3.75;moves;" .. moves:sub(1,-2) .. ";1]" ..
801 meta:set_string("formspec", formspec)
806 minetest.register_node(":realchess:chessboard", {
807 description = "Chess Board",
808 drawtype = "nodebox",
810 paramtype2 = "facedir",
811 inventory_image = "chessboard_top.png",
812 wield_image = "chessboard_top.png",
813 tiles = {"chessboard_top.png", "chessboard_top.png", "chessboard_sides.png"},
814 groups = {choppy=3, oddly_breakable_by_hand=2, flammable=3},
815 sounds = default.node_sound_wood_defaults(),
816 node_box = {type = "fixed", fixed = {-.375, -.5, -.375, .375, -.4375, .375}},
817 sunlight_propagates = true,
818 on_rotate = screwdriver.rotate_simple,
819 can_dig = realchess.dig,
820 on_construct = realchess.init,
821 on_receive_fields = realchess.fields,
822 allow_metadata_inventory_move = realchess.move,
823 on_metadata_inventory_move = realchess.on_move,
824 allow_metadata_inventory_take = function() return 0 end
827 local function register_piece(name, count)
828 for _, color in pairs({"black", "white"}) do
830 minetest.register_craftitem(":realchess:" .. name .. "_" .. color, {
831 description = color:gsub("^%l", string.upper) .. " " .. name:gsub("^%l", string.upper),
832 inventory_image = name .. "_" .. color .. ".png",
834 groups = {not_in_creative_inventory=1}
838 minetest.register_craftitem(":realchess:" .. name .. "_" .. color .. "_" .. i, {
839 description = color:gsub("^%l", string.upper) .. " " .. name:gsub("^%l", string.upper),
840 inventory_image = name .. "_" .. color .. ".png",
842 groups = {not_in_creative_inventory=1}
849 register_piece("pawn", 8)
850 register_piece("rook", 2)
851 register_piece("knight", 2)
852 register_piece("bishop", 2)
853 register_piece("queen")
854 register_piece("king")
858 minetest.register_craft({
859 output = "realchess:chessboard",
861 {"dye:black", "dye:white", "dye:black"},
862 {"stairs:slab_wood", "stairs:slab_wood", "stairs:slab_wood"}