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 chat_prefix = minetest.colorize("#FFFF00", "[Chess] ")
16 local letters = {'A','B','C','D','E','F','G','H'}
19 "realchess:rook_black_1",
20 "realchess:knight_black_1",
21 "realchess:bishop_black_1",
22 "realchess:queen_black",
23 "realchess:king_black",
24 "realchess:bishop_black_2",
25 "realchess:knight_black_2",
26 "realchess:rook_black_2",
27 "realchess:pawn_black_1",
28 "realchess:pawn_black_2",
29 "realchess:pawn_black_3",
30 "realchess:pawn_black_4",
31 "realchess:pawn_black_5",
32 "realchess:pawn_black_6",
33 "realchess:pawn_black_7",
34 "realchess:pawn_black_8",
35 '','','','','','','','','','','','','','','','',
36 '','','','','','','','','','','','','','','','',
37 "realchess:pawn_white_1",
38 "realchess:pawn_white_2",
39 "realchess:pawn_white_3",
40 "realchess:pawn_white_4",
41 "realchess:pawn_white_5",
42 "realchess:pawn_white_6",
43 "realchess:pawn_white_7",
44 "realchess:pawn_white_8",
45 "realchess:rook_white_1",
46 "realchess:knight_white_1",
47 "realchess:bishop_white_1",
48 "realchess:queen_white",
49 "realchess:king_white",
50 "realchess:bishop_white_2",
51 "realchess:knight_white_2",
52 "realchess:rook_white_2"
55 local pieces_str, x = "", 0
57 local p = pieces[i]:match(":(%w+_%w+)")
58 if pieces[i]:find(":(%w+)_(%w+)") and not pieces_str:find(p) then
59 pieces_str = pieces_str .. x .. "=" .. p .. ".png,"
63 pieces_str = pieces_str .. "69=mailbox_blank16.png"
68 bgcolor[#080808BB;true]
69 background[0,0;14.7,10;chess_bg.png]
70 list[context;board;0.3,1;8,8;]
71 listcolors[#00000000;#00000000;#00000000;#30434C;#FFF]
72 tableoptions[background=#00000000;highlight=#00000000;border=false]
73 button[10.5,8.5;2,2;new;New game]
74 ]] .. "tablecolumns[image," .. pieces_str ..
75 ";text;color;text;color;text;image," .. pieces_str .. "]"
77 function realchess.init(pos)
78 local meta = minetest.get_meta(pos)
79 local inv = meta:get_inventory()
81 meta:set_string("formspec", fs)
82 meta:set_string("infotext", "Chess Board")
83 meta:set_string("playerBlack", "")
84 meta:set_string("playerWhite", "")
85 meta:set_string("lastMove", "")
86 meta:set_string("winner", "")
88 meta:set_int("lastMoveTime", 0)
89 meta:set_int("castlingBlackL", 1)
90 meta:set_int("castlingBlackR", 1)
91 meta:set_int("castlingWhiteL", 1)
92 meta:set_int("castlingWhiteR", 1)
94 meta:set_string("moves", "")
95 meta:set_string("eaten", "")
97 inv:set_list("board", pieces)
98 inv:set_size("board", 64)
101 function realchess.move(pos, from_list, from_index, to_list, to_index, _, player)
102 if from_list ~= "board" and to_list ~= "board" then
106 local playerName = player:get_player_name()
107 local meta = minetest.get_meta(pos)
109 if meta:get_string("winner") ~= "" then
113 local inv = meta:get_inventory()
114 local pieceFrom = inv:get_stack(from_list, from_index):get_name()
115 local pieceTo = inv:get_stack(to_list, to_index):get_name()
116 local lastMove = meta:get_string("lastMove")
117 local thisMove -- will replace lastMove when move is legal
118 local playerWhite = meta:get_string("playerWhite")
119 local playerBlack = meta:get_string("playerBlack")
121 if pieceFrom:find("white") then
122 if playerWhite ~= "" and playerWhite ~= playerName then
123 minetest.chat_send_player(playerName, chat_prefix .. "Someone else plays white pieces!")
126 if lastMove ~= "" and lastMove ~= "black" then
129 if pieceTo:find("white") then
130 -- Don't replace pieces of same color
133 playerWhite = playerName
135 elseif pieceFrom:find("black") then
136 if playerBlack ~= "" and playerBlack ~= playerName then
137 minetest.chat_send_player(playerName, chat_prefix .. "Someone else plays black pieces!")
140 if lastMove ~= "" and lastMove ~= "white" then
143 if pieceTo:find("black") then
144 -- Don't replace pieces of same color
147 playerBlack = playerName
151 -- DETERMINISTIC MOVING
153 local from_x, from_y = index_to_xy(from_index)
154 local to_x, to_y = index_to_xy(to_index)
156 if pieceFrom:sub(11,14) == "pawn" then
157 if thisMove == "white" then
158 local pawnWhiteMove = inv:get_stack(from_list, xy_to_index(from_x, from_y - 1)):get_name()
159 -- white pawns can go up only
160 if from_y - 1 == to_y then
161 if from_x == to_x then
162 if pieceTo ~= "" then
164 elseif to_index >= 1 and to_index <= 8 then
165 inv:set_stack(from_list, from_index, "realchess:queen_white")
167 elseif from_x - 1 == to_x or from_x + 1 == to_x then
168 if not pieceTo:find("black") then
170 elseif to_index >= 1 and to_index <= 8 then
171 inv:set_stack(from_list, from_index, "realchess:queen_white")
176 elseif from_y - 2 == to_y then
177 if pieceTo ~= "" or from_y < 6 or pawnWhiteMove ~= "" then
185 -- ensure that destination cell is empty
186 -- elseif x changed one unit left or right
187 -- ensure the pawn is killing opponent piece
189 -- move is not legal - abort
191 if from_x == to_x then
192 if pieceTo ~= "" then
195 elseif from_x - 1 == to_x or from_x + 1 == to_x then
196 if not pieceTo:find("black") then
203 elseif thisMove == "black" then
204 local pawnBlackMove = inv:get_stack(from_list, xy_to_index(from_x, from_y + 1)):get_name()
205 -- black pawns can go down only
206 if from_y + 1 == to_y then
207 if from_x == to_x then
208 if pieceTo ~= "" then
210 elseif to_index >= 57 and to_index <= 64 then
211 inv:set_stack(from_list, from_index, "realchess:queen_black")
213 elseif from_x - 1 == to_x or from_x + 1 == to_x then
214 if not pieceTo:find("white") then
216 elseif to_index >= 57 and to_index <= 64 then
217 inv:set_stack(from_list, from_index, "realchess:queen_black")
222 elseif from_y + 2 == to_y then
223 if pieceTo ~= "" or from_y > 1 or pawnBlackMove ~= "" then
231 -- ensure that destination cell is empty
232 -- elseif x changed one unit left or right
233 -- ensure the pawn is killing opponent piece
235 -- move is not legal - abort
237 if from_x == to_x then
238 if pieceTo ~= "" then
241 elseif from_x - 1 == to_x or from_x + 1 == to_x then
242 if not pieceTo:find("white") then
252 elseif pieceFrom:sub(11,14) == "rook" then
253 if from_x == to_x then
255 if from_y < to_y then
257 -- ensure that no piece disturbs the way
258 for i = from_y + 1, to_y - 1 do
259 if inv:get_stack(from_list, xy_to_index(from_x, i)):get_name() ~= "" then
265 -- ensure that no piece disturbs the way
266 for i = to_y + 1, from_y - 1 do
267 if inv:get_stack(from_list, xy_to_index(from_x, i)):get_name() ~= "" then
272 elseif from_y == to_y then
273 -- mocing horizontally
274 if from_x < to_x then
276 -- ensure that no piece disturbs the way
277 for i = from_x + 1, to_x - 1 do
278 if inv:get_stack(from_list, xy_to_index(i, from_y)):get_name() ~= "" then
284 -- ensure that no piece disturbs the way
285 for i = to_x + 1, from_x - 1 do
286 if inv:get_stack(from_list, xy_to_index(i, from_y)):get_name() ~= "" then
292 -- attempt to move arbitrarily -> abort
296 if thisMove == "white" or thisMove == "black" then
297 if pieceFrom:sub(-1) == "1" then
298 meta:set_int("castlingWhiteL", 0)
299 elseif pieceFrom:sub(-1) == "2" then
300 meta:set_int("castlingWhiteR", 0)
304 elseif pieceFrom:sub(11,16) == "knight" then
306 local dx = from_x - to_x
307 local dy = from_y - to_y
309 -- get absolute values
310 if dx < 0 then dx = -dx end
311 if dy < 0 then dy = -dy end
314 if dx > dy then dx, dy = dy, dx end
316 -- ensure that dx == 1 and dy == 2
317 if dx ~= 1 or dy ~= 2 then
320 -- just ensure that destination cell does not contain friend piece
321 -- ^ it was done already thus everything ok
323 elseif pieceFrom:sub(11,16) == "bishop" then
325 local dx = from_x - to_x
326 local dy = from_y - to_y
328 -- get absolute values
329 if dx < 0 then dx = -dx end
330 if dy < 0 then dy = -dy end
332 -- ensure dx and dy are equal
333 if dx ~= dy then return 0 end
335 if from_x < to_x then
336 if from_y < to_y then
338 -- ensure that no piece disturbs the way
340 if inv:get_stack(from_list, xy_to_index(from_x + i, from_y + i)):get_name() ~= "" then
346 -- ensure that no piece disturbs the way
348 if inv:get_stack(from_list, xy_to_index(from_x + i, from_y - i)):get_name() ~= "" then
354 if from_y < to_y then
356 -- ensure that no piece disturbs the way
358 if inv:get_stack(from_list, xy_to_index(from_x - i, from_y + i)):get_name() ~= "" then
364 -- ensure that no piece disturbs the way
366 if inv:get_stack(from_list, xy_to_index(from_x - i, from_y - i)):get_name() ~= "" then
373 elseif pieceFrom:sub(11,15) == "queen" then
374 local dx = from_x - to_x
375 local dy = from_y - to_y
377 -- get absolute values
378 if dx < 0 then dx = -dx end
379 if dy < 0 then dy = -dy end
381 -- ensure valid relative move
382 if dx ~= 0 and dy ~= 0 and dx ~= dy then
386 if from_x == to_x then
387 if from_y < to_y then
389 -- ensure that no piece disturbs the way
391 if inv:get_stack(from_list, xy_to_index(from_x, from_y + i)):get_name() ~= "" then
397 -- ensure that no piece disturbs the way
399 if inv:get_stack(from_list, xy_to_index(from_x, from_y - i)):get_name() ~= "" then
404 elseif from_x < to_x then
405 if from_y == to_y then
407 -- ensure that no piece disturbs the way
409 if inv:get_stack(from_list, xy_to_index(from_x + i, from_y)):get_name() ~= "" then
413 elseif from_y < to_y then
415 -- ensure that no piece disturbs the way
417 if inv:get_stack(from_list, xy_to_index(from_x + i, from_y + i)):get_name() ~= "" then
423 -- ensure that no piece disturbs the way
425 if inv:get_stack(from_list, xy_to_index(from_x + i, from_y - i)):get_name() ~= "" then
431 if from_y == to_y then
433 -- ensure that no piece disturbs the way and destination cell does
435 if inv:get_stack(from_list, xy_to_index(from_x - i, from_y)):get_name() ~= "" then
439 elseif from_y < to_y then
441 -- ensure that no piece disturbs the way
443 if inv:get_stack(from_list, xy_to_index(from_x - i, from_y + i)):get_name() ~= "" then
449 -- ensure that no piece disturbs the way
451 if inv:get_stack(from_list, xy_to_index(from_x - i, from_y - i)):get_name() ~= "" then
458 elseif pieceFrom:sub(11,14) == "king" then
459 local dx = from_x - to_x
460 local dy = from_y - to_y
463 if thisMove == "white" then
464 if from_y == 7 and to_y == 7 then
466 local castlingWhiteL = meta:get_int("castlingWhiteL")
467 local idx57 = inv:get_stack(from_list, 57):get_name()
469 if castlingWhiteL == 1 and idx57 == "realchess:rook_white_1" then
470 for i = 58, from_index - 1 do
471 if inv:get_stack(from_list, i):get_name() ~= "" then
475 inv:set_stack(from_list, 57, "")
476 inv:set_stack(from_list, 59, "realchess:rook_white_1")
479 elseif to_x == 6 then
480 local castlingWhiteR = meta:get_int("castlingWhiteR")
481 local idx64 = inv:get_stack(from_list, 64):get_name()
483 if castlingWhiteR == 1 and idx64 == "realchess:rook_white_2" then
484 for i = from_index + 1, 63 do
485 if inv:get_stack(from_list, i):get_name() ~= "" then
489 inv:set_stack(from_list, 62, "realchess:rook_white_2")
490 inv:set_stack(from_list, 64, "")
495 elseif thisMove == "black" then
496 if from_y == 0 and to_y == 0 then
498 local castlingBlackL = meta:get_int("castlingBlackL")
499 local idx1 = inv:get_stack(from_list, 1):get_name()
501 if castlingBlackL == 1 and idx1 == "realchess:rook_black_1" then
502 for i = 2, from_index - 1 do
503 if inv:get_stack(from_list, i):get_name() ~= "" then
507 inv:set_stack(from_list, 1, "")
508 inv:set_stack(from_list, 3, "realchess:rook_black_1")
511 elseif to_x == 6 then
512 local castlingBlackR = meta:get_int("castlingBlackR")
513 local idx8 = inv:get_stack(from_list, 1):get_name()
515 if castlingBlackR == 1 and idx8 == "realchess:rook_black_2" then
516 for i = from_index + 1, 7 do
517 if inv:get_stack(from_list, i):get_name() ~= "" then
521 inv:set_stack(from_list, 6, "realchess:rook_black_2")
522 inv:set_stack(from_list, 8, "")
530 if dx < 0 then dx = -dx end
531 if dy < 0 then dy = -dy end
532 if dx > 1 or dy > 1 then return 0 end
535 if thisMove == "white" then
536 meta:set_int("castlingWhiteL", 0)
537 meta:set_int("castlingWhiteR", 0)
538 elseif thisMove == "black" then
539 meta:set_int("castlingBlackL", 0)
540 meta:set_int("castlingBlackR", 0)
544 meta:set_string("playerWhite", playerWhite)
545 meta:set_string("playerBlack", playerBlack)
547 meta:set_string("lastMove", lastMove)
548 meta:set_int("lastMoveTime", minetest.get_gametime())
550 if pieceTo:sub(11,14) == "king" then
551 meta:set_string("winner", thisMove)
554 local moves = meta:get_string("moves")
555 local pieceFrom_s = pieceFrom:match(":(%w+_%w+)")
556 local pieceFrom_si_id = pieces_str:match("(%d+)=" .. pieceFrom_s)
557 local pieceTo_s = pieceTo_s ~= "" and pieceTo:match(":(%w+_%w+)") or ""
558 local pieceTo_si_id = pieceTo_s ~= "" and pieces_str:match("(%d+)=" .. pieceTo_s) or ""
560 moves = pieceFrom_si_id .. "," ..
561 letters[from_x + 1] .. (from_y + 1) .. "," ..
562 (pieceTo ~= "" and "#33FF33" or "#FFFFFF") .. ", > ,#FFFFFF," ..
563 letters[to_x + 1] .. (to_y + 1) .. "," ..
564 (pieceTo ~= "" and pieceTo_si_id or "69") .. "," ..
567 meta:set_string("moves", moves)
569 local eaten = meta:get_string("eaten")
570 if pieceTo ~= "" then
571 eaten = eaten .. pieceTo_s .. ","
574 meta:set_string("eaten", eaten)
576 local eaten_t = string.split(eaten, ",")
580 for i = 1, #eaten_t do
581 local is_white = eaten_t[i]:sub(-5,-1) == "white"
582 local X = (is_white and a or b) % 4
583 local Y = ((is_white and a or b) % 16 - X) / 4
591 eaten_img = eaten_img ..
592 "image[" .. ((X + (is_white and 11.7 or 8.8)) - (X * 0.45)) .. "," ..
593 ((Y + 5.56) - (Y * 0.2)) .. ";1,1;" .. eaten_t[i] .. ".png]"
596 local black_win = lastMove == "black" and pieceTo:sub(11,14) == "king"
597 local white_win = lastMove == "white" and pieceTo:sub(11,14) == "king"
599 local formspec = fs ..
600 "label[2,0.3;" .. (black_win and
601 minetest.colorize("#00FF00", playerBlack .. " has win") or
602 minetest.colorize("#000001",
603 (lastMove == "white" and playerBlack ~= "" and not white_win) and
604 playerBlack .. "..." or playerBlack)) .. "]" ..
605 "label[2,9.15;" .. (white_win and
606 minetest.colorize("#00FF00", playerWhite .. " has win") or
607 minetest.colorize("#000001",
608 (lastMove == "black" and playerWhite ~= "" and not black_win) and
609 playerWhite .. "..." or playerWhite)) .. "]" ..
610 "table[8.9,1.05;5.07,3.75;moves;" .. moves:sub(1,-2) .. ";1]" ..
613 meta:set_string("formspec", formspec)
618 local function timeout_format(timeout_limit)
619 local time_remaining = timeout_limit - minetest.get_gametime()
620 local minutes = math.floor(time_remaining / 60)
621 local seconds = time_remaining % 60
624 return seconds .. " sec."
627 return minutes .. " min. " .. seconds .. " sec."
630 function realchess.fields(pos, _, fields, sender)
631 local playerName = sender:get_player_name()
632 local meta = minetest.get_meta(pos)
633 local timeout_limit = meta:get_int("lastMoveTime") + 300
634 local playerWhite = meta:get_string("playerWhite")
635 local playerBlack = meta:get_string("playerBlack")
636 local lastMoveTime = meta:get_int("lastMoveTime")
637 if fields.quit then return end
639 -- timeout is 5 min. by default for resetting the game (non-players only)
641 if (playerWhite == playerName or playerBlack == playerName) then
644 minetest.chat_send_player(playerName, chat_prefix ..
645 "You can't reset the chessboard, a game has been started.\n" ..
646 "If you are not a current player, try again in " ..
647 timeout_format(timeout_limit))
651 if fields.new and lastMoveTime ~= 0 and minetest.get_gametime() >= timeout_limit and
652 (playerWhite ~= playerName or playerBlack ~= playerName) then
657 function realchess.dig(pos, player)
662 local meta = minetest.get_meta(pos)
663 local playerName = player:get_player_name()
664 local timeout_limit = meta:get_int("lastMoveTime") + 300
665 local lastMoveTime = meta:get_int("lastMoveTime")
667 -- timeout is 5 min. by default for digging the chessboard (non-players only)
668 return (lastMoveTime == 0 and minetest.get_gametime() > timeout_limit) or
669 minetest.chat_send_player(playerName, chat_prefix ..
670 "You can't dig the chessboard, a game has been started.\n" ..
671 "Reset it first if you're a current player, or dig it again in " ..
672 timeout_format(timeout_limit))
675 function realchess.on_move(pos, from_list, from_index)
676 local inv = minetest.get_meta(pos):get_inventory()
677 inv:set_stack(from_list, from_index, '')
681 minetest.register_node(":realchess:chessboard", {
682 description = "Chess Board",
683 drawtype = "nodebox",
685 paramtype2 = "facedir",
686 inventory_image = "chessboard_top.png",
687 wield_image = "chessboard_top.png",
688 tiles = {"chessboard_top.png", "chessboard_top.png", "chessboard_sides.png"},
689 groups = {choppy=3, oddly_breakable_by_hand=2, flammable=3},
690 sounds = default.node_sound_wood_defaults(),
691 node_box = {type = "fixed", fixed = {-.375, -.5, -.375, .375, -.4375, .375}},
692 sunlight_propagates = true,
693 on_rotate = screwdriver.rotate_simple,
694 can_dig = realchess.dig,
695 on_construct = realchess.init,
696 on_receive_fields = realchess.fields,
697 allow_metadata_inventory_move = realchess.move,
698 on_metadata_inventory_move = realchess.on_move,
699 allow_metadata_inventory_take = function() return 0 end
702 local function register_piece(name, count)
703 for _, color in pairs({"black", "white"}) do
705 minetest.register_craftitem(":realchess:" .. name .. "_" .. color, {
706 description = color:gsub("^%l", string.upper) .. " " .. name:gsub("^%l", string.upper),
707 inventory_image = name .. "_" .. color .. ".png",
709 groups = {not_in_creative_inventory=1}
713 minetest.register_craftitem(":realchess:" .. name .. "_" .. color .. "_" .. i, {
714 description = color:gsub("^%l", string.upper) .. " " .. name:gsub("^%l", string.upper),
715 inventory_image = name .. "_" .. color .. ".png",
717 groups = {not_in_creative_inventory=1}
724 register_piece("pawn", 8)
725 register_piece("rook", 2)
726 register_piece("knight", 2)
727 register_piece("bishop", 2)
728 register_piece("queen")
729 register_piece("king")
733 minetest.register_craft({
734 output = "realchess:chessboard",
736 {"dye:black", "dye:white", "dye:black"},
737 {"stairs:slab_wood", "stairs:slab_wood", "stairs:slab_wood"}