Initial commit

This commit is contained in:
Y. Wang 2023-05-02 14:36:59 +02:00
commit 1a3bbffb57
8 changed files with 471 additions and 0 deletions

31
README.md Normal file
View File

@ -0,0 +1,31 @@
# Xiangqi - Chinese chess for Minetest
## Usage
The board currently does not have a crafting recipe. You currently need
to get it from the inventory in creative mode.
Right-click a figure to select it (or, if it is already selected,
deselect it). The selected figure is marked green, and the possible
target positions are marked, with (in particular) targetable figures
from the opposing side marked blue. Left-click on the target to move
the selected figure to the target position.
The figures face the side that should make a move.
Wikipedia has a page on the rules (and pieces) of Xiangqi.
## Limitations/To-do
* The game does not check whether the move results in the king/general
(帥/將) being captured.
* The game does not check for check(mate)s.
* The player is not notified when the game ends.
* The moves are not recorded.
* The figures may appear weird in certain circumstances.
## Licenses
* The source code is licensed under GNU AGPLv3.
* Textures (see below for exceptions) are licensed under CC0.
* `xiangqi_figures.png` is licensed under SIL OFL.

437
init.lua Normal file
View File

@ -0,0 +1,437 @@
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
})

3
mod.conf Normal file
View File

@ -0,0 +1,3 @@
name=xiangqi
title=Xiangqi
description=Xiangqi (Chinese chess)

BIN
textures/xiangqi_blank.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 B

BIN
textures/xiangqi_board.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 355 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 424 B