2023-05-02 14:36:59 +02:00

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
})