438 lines
10 KiB
Lua
438 lines
10 KiB
Lua
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
|
|
})
|