local S = minetest.get_translator "xiangqi" local yoff = -15/32 local vstep = 16/177 local figheight = 1/2+yoff local function objects_in_cube(pos, r) return minetest.get_objects_in_area(vector.offset(pos, -r, -r, -r), vector.offset(pos, r, r, r)) end local v2d = {} local v2d_mt = { __add = function(a, b) return v2d.new(a.x+b.x, a.y+b.y) end, __sub = function(a, b) return v2d.new(a.x-b.x, a.y-b.y) end, __unm = function(v) return v2d.new(-v.x, -v.y) end, __eq = function(a, b) return a.x == b.x and a.y == b.y end, __index = v2d, } function v2d.new(x, y) return setmetatable({x = x, y = y}, v2d_mt) end function v2d:copy() return v2d.new(self.x, self.y) end function v2d:transpose() return v2d.new(self.y, self.x) end function v2d:to3d(base) local row, col = self.y, self.x local v = vector.new((col-4)*vstep, yoff+figheight/2, (row-4.5)*vstep) if base then return base + v else return v end end function v2d:in_bound() return self.x >= 0 and self.x <= 8 and self.y >= 0 and self.y <= 9 end function v2d:assert_bound() if self:in_bound() then return self end return nil end function v2d.raynext(step, pos) return (step+pos):assert_bound() end function v2d.forray(pos, step) return v2d.raynext, step, pos end local game = {} local game_mt = {__index = game} function game.new(pos) return setmetatable({ basepos = pos, }, game_mt) end local function spawn_piece(...) local p0, row, col = ... local obj = minetest.add_entity(v2d.new(col, row):to3d(p0), "xiangqi:figure", minetest.serialize{...}) return obj end local function spawn_target_marker(p0, row, col) return minetest.add_entity(v2d.new(col, row):to3d(p0), "xiangqi:target_marker", minetest.serialize{p0, row, col}) end function game:setup() self:set_done(false) local pos = self.basepos for ptype, rmod in pairs{ [false] = function(x) return x end, [true] = function(x) return 9-x end, } do for _, cmod in pairs { function(x) return x end, function(x) return 8-x end, } do for _, t in pairs { {0, 0, "chariot"}, {0, 1, "horse"}, {0, 2, "elephant"}, {0, 3, "advisor"}, {2, 1, "cannon"}, {3, 0, "pawn"}, {3, 2, "pawn"}, } do spawn_piece(pos, rmod(t[1]), cmod(t[2]), t[3], ptype) end end for r, fig in pairs { [0] = "king", [3] = "pawn", } do spawn_piece(pos, rmod(r), 4, fig, ptype) end end end function game:teardown() for _, obj in pairs(objects_in_cube(self.basepos, 0.5)) do local ename = obj:get_entity_name() if ename == "xiangqi:figure" or ename == "xiangqi:target_marker" then obj:remove() end end end function game:node_meta() local meta = minetest.get_meta(self.basepos) self.node_meta = function() return meta end return meta end function game:get_done() return minetest.is_yes(self:node_meta():get_int("gameend")) end function game:set_done(st) self:node_meta():set_int("gameend", st and 1 or 0) end function game:get_side() return minetest.is_yes(self:node_meta():get_int("pturn")) end function game:set_side(s) self:node_meta():set_int("pturn", s and 1 or 0) end function game:flip_side() return self:set_side(not self:get_side()) end function game:get_selection() local meta = self:node_meta() local row, col = meta:get_int("selrow"), meta:get_int("selcol") return v2d.new(col-1, row-1):assert_bound() end function game:set_selection(pos) local meta = self:node_meta() local row, col = 0, 0 if pos then row, col = pos.y+1, pos.x+1 end meta:set_int("selrow", row) meta:set_int("selcol", col) end function game:get_figure(pos) if not pos then return nil end for _, obj in pairs(minetest.get_objects_inside_radius(pos:to3d(self.basepos), vstep/2)) do if obj:get_entity_name() == "xiangqi:figure" then return obj:get_luaentity() end end return nil end function game:get_selected_figure() return self:get_figure(self:get_selection()) end function game:update_board() for _, obj in pairs(objects_in_cube(self.basepos, 0.5)) do local ename = obj:get_entity_name() if ename == "xiangqi:figure" then local ent = obj:get_luaentity() ent._reachable = false ent:_update_appearance() elseif ename == "xiangqi:target_marker" then obj:remove() end end local selfig = self:get_selected_figure() if selfig then for _, pos in pairs(selfig:_list_reachable()) do local fig = self:get_figure(pos) if fig then if fig._pside ~= selfig._pside then fig._reachable = true fig:_update_appearance() end else spawn_target_marker(self.basepos, pos.y, pos.x) end end end end function game:move(fig, tpos) if not fig then return false end local tfig = self:get_figure(tpos) if tfig then if tfig._ptype == "king" then self:set_done(true) end tfig.object:remove() end fig._row, fig._col, fig._pos = tpos.y, tpos.x, tpos fig.object:set_pos(tpos:to3d(self.basepos)) return true end function game:gamemove(tpos) local selfig = self:get_selected_figure() self:set_selection() self:update_board() minetest.after(0, function() if self:move(selfig, tpos) then self:flip_side() self:update_board() end end) end local figstex = "xiangqi_figure_side.png" minetest.register_entity("xiangqi:target_marker", { visual = "cube", visual_size = {x = vstep, y = figheight}, selectionbox = {-vstep/2, -figheight/2, -vstep/2, vstep/2, figheight/2, vstep/2}, textures = {figstex, figstex, figstex, figstex, figstex, figstex}, on_activate = function(self, staticdata) self._p0, self._row, self._col = unpack(minetest.deserialize(staticdata or {})) self._game = game.new(self._p0) self._pos = v2d.new(self._col, self._row) self.object:set_armor_groups{immortal = 100} end, on_punch = function(self) self._game:gamemove(self._pos) end, }) minetest.register_entity("xiangqi:figure", { initial_properties = { visual = "cube", visual_size = {x = vstep, y = figheight}, selectionbox = {-vstep/2, -figheight/2, -vstep/2, vstep/2, figheight/2, vstep/2} }, on_activate = function(self, staticdata) self._p0, self._row, self._col, self._ptype, self._pside = unpack(minetest.deserialize(staticdata or {})) self.object:set_armor_groups{immortal = 100} self._game = game.new(self._p0) self._pos = v2d.new(self._col, self._row) self:_update_appearance() end, get_staticdata = function(self) return minetest.serialize{self._p0, self._row, self._col, self._ptype, self._pside} end, on_rightclick = function(self, clicker) if self._game:get_side() ~= self._pside then if clicker:is_player() then minetest.chat_send_player(clicker:get_player_name(), S("It is not yet your turn.")) end return end if self:_is_selected() then self._game:set_selection() elseif self._game:get_done() then if clicker:is_player() then minetest.chat_send_player(clicker:get_player_name(), S("The game has ended")) end else self._game:set_selection(self._pos) end self._game:update_board() end, on_punch = function(self) if self._reachable then self._game:gamemove(self._pos) end end, _list_reachable = function(self) local game = self._game local t = {} local fourdirs = {v2d.new(0, 1), v2d.new(0, -1), v2d.new(1, 0), v2d.new(-1, 0)} local diagdirs = {v2d.new(1, 1), v2d.new(1, -1), v2d.new(-1, 1), v2d.new(-1, -1)} if self._ptype == "king" then local midrow = self._pside and 8 or 1 for _, step in pairs(fourdirs) do local pos = self._pos+step if math.abs(pos.x-4) <= 1 and math.abs(pos.y-midrow) <= 1 then table.insert(t, pos) end for pos in self._pos:forray(step) do local fig = game:get_figure(pos) if fig then if fig._ptype == "king" then table.insert(t, pos) end break end end end elseif self._ptype == "advisor" then local midrow = self._pside and 8 or 1 for _, step in pairs(diagdirs) do local pos = self._pos+step if math.abs(pos.x-4) <= 1 and math.abs(pos.y-midrow) <= 1 then table.insert(t, pos) end end elseif self._ptype == "elephant" then for _, step in pairs(diagdirs) do local p1 = self._pos+step if (not game:get_figure(p1)) then local tar = p1+step if (tar.y > 4.5) == self._pside then table.insert(t, tar:assert_bound()) end end end elseif self._ptype == "horse" then for _, s1 in pairs(fourdirs) do local p1 = self._pos+s1 if not game:get_figure(p1) then local p2 = p1+s1 local n = s1:transpose() for _, s2 in pairs{n, -n} do table.insert(t, (p2+s2):assert_bound()) end end end elseif self._ptype == "chariot" then for _, step in pairs(fourdirs) do for pos in self._pos:forray(step) do table.insert(t, pos) if game:get_figure(pos) then break end end end elseif self._ptype == "cannon" then for _, step in pairs(fourdirs) do local o = 0 for pos in self._pos:forray(step) do if game:get_figure(pos) then o = o+1 if o == 2 then table.insert(t, pos) break end end if o == 0 then table.insert(t, pos) end end end elseif self._ptype == "pawn" then table.insert(t, (self._pos+v2d.new(0, self._pside and -1 or 1)):assert_bound()) if (self._pos.y <= 4.5) == self._pside then for _, d in pairs{v2d.new(1, 0), v2d.new(-1, 0)} do table.insert(t, (self._pos+d):assert_bound()) end end end return t end, _is_selected = function(self) return self._game:get_selection() == self._pos end, _update_appearance = function(self) local tex = { king = 0, advisor = 1, elephant = 2, horse = 3, chariot = 4, cannon = 5, pawn = 6, } local tex = tex[self._ptype] if tex then tex = ("xiangqi_figures.png^[sheet:7x2:%d,%d"):format(tex, self._pside and 1 or 0) else tex = figstex end if self:_is_selected() then tex = tex .. "^(xiangqi_figure_frame.png^[multiply:green)" elseif self._reachable then tex = tex .. "^(xiangqi_figure_frame.png^[multiply:blue)" end self.object:set_properties { textures = {tex, figstex, figstex, figstex, figstex, figstex}, } if self._game:get_side() then self.object:set_yaw(math.pi) else self.object:set_yaw(0) end end, }) local blankbg = "xiangqi_blank.png" minetest.register_node("xiangqi:board", { description = "Xiangqi board", drawtype = "nodebox", node_box = { type = "fixed", fixed = {-1/2, -1/2, -1/2, 1/2, yoff, 1/2} }, tiles = {"xiangqi_board.png", blankbg, blankbg, blankbg, blankbg, blankbg}, inventory_image = "xiangqi_board.png", groups = {cracky=1}, on_construct = function(pos) game.new(pos):setup() end, on_destruct = function(pos) game.new(pos):teardown() end })