Initial commit
This commit is contained in:
commit
1a3bbffb57
31
README.md
Normal file
31
README.md
Normal 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
437
init.lua
Normal 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
3
mod.conf
Normal file
@ -0,0 +1,3 @@
|
||||
name=xiangqi
|
||||
title=Xiangqi
|
||||
description=Xiangqi (Chinese chess)
|
BIN
textures/xiangqi_blank.png
Normal file
BIN
textures/xiangqi_blank.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 82 B |
BIN
textures/xiangqi_board.png
Normal file
BIN
textures/xiangqi_board.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 355 B |
BIN
textures/xiangqi_figure_frame.png
Normal file
BIN
textures/xiangqi_figure_frame.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 89 B |
BIN
textures/xiangqi_figure_side.png
Normal file
BIN
textures/xiangqi_figure_side.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 82 B |
BIN
textures/xiangqi_figures.png
Normal file
BIN
textures/xiangqi_figures.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 424 B |
Loading…
x
Reference in New Issue
Block a user