华容道案例

命令行华容道案例

在群里听O大的一个面试题是简单实现一个非图形的华容道,并自定义用户交互。因为手痒自己也想动动手,于是定了一个2小时的jam。当然,因为很简单,估计很多人都不屑于看吧,哈哈。这里用lua实现。

思路

当面对华容道这个题目时,首先想到把它虚拟建模,这里的建模不是3d建模哈,是把实物抽象为数学,从而使用数学工具来控制他们。我们首先来分析一下华容道的棋盘,我们看到了几种棋子,和一个边框。棋子是占一格、横竖两格和方块四格几种。格。。。是什么?矩阵!是的,他们抽象出来实际上跟俄罗斯方块是一个东西啦。我看到了俄罗斯方块的各种落块。如果是俄罗斯就很简单了,我想写程序或者说写游戏的应该多少都实现过俄罗斯吧。如果没有。。。赶紧动动手吧。

俄罗斯和贪吃蛇等等都是所谓的矩阵游戏,图像通过矩阵的点来表示,碰撞就更简单了,对于某一个矩阵节点而言,都有自身的状态,如空,由何种块来占据等。碰撞检测主要是用来看物体是否能够移动。

接下来,很多问题就迎刃而解了,比如华容道的各种棋子就是各种形状的块,块有自身的矩阵,而我们把左上角作为其锚点,用来描述块的位置。然后按照初始状态把块放到棋盘中,然后把块按其位置,把块自身的类别写入到棋盘的矩阵中,用来做碰撞检测的。当块移动时,我们看目标矩阵位置是否是空的,如果是就可以走了。如果曹操走到下方就结束游戏咯。

好啦思路理清了,我们开始设计和实现。

设计和实现

首先我们来设计一下棋子的数据结构

1
2
3
4
5
6
7
8
9
10
11
12
local data = {}
local grid22 = {{1,1},{1,1}}
--[[如果写成这样你就明白了吧
1,1
1,1
]]
data.caocao = {
name = "曹操",--名字,当然以后可以再加要给字段作为图片名
pos = {2,1}, --棋盘的初始网格位置
grid = grid22, --曹操是四方格,也就是四个点组成的矩阵
ctrl = "cc" --控制方式,我们后面说互动的时候再说
}

我们希望把数据和逻辑分开写,这样更加清晰,修改数据或修改逻辑也可以比较明了一些。这里用曹操这个棋子举例。

然后我们来做棋盘矩阵。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
local map = {}
local empty = { --实际上是仿造棋子做的空数据,之所以要这么写主要是为了遍历方便,不报错。
name = " ",
pos = {0,0},
grid = {}
}
local function clearMap() --清空棋盘方法,实际和新建是一样滴
for x = 1, 4 do --华容道的棋盘是4x5的矩阵
map[x] = {}
for y = 1,5 do
map[x][y] = empty --初始值
end
end
end

接下来我们来摆放棋子

1
2
3
4
5
6
7
8
9
10
11
local function setupMap() --摆放棋子
for _,role in pairs(data) do
local roleX = role.pos[1] --棋子在棋盘的矩阵位置
local roleY = role.pos[2]
for y,tab in ipairs(role.grid) do --对于棋子网格中的每一个点,都映射到棋盘网格上。
for x,tab in ipairs(tab) do
map[roleX + x - 1][roleY + y -1] = role --为什么要-1?因为棋子网格初始不是0,0而是1,1
end
end
end
end

既然摆好了,我们来画一下,因为我们不需要图形界面,所以直接用打印输出吧。

1
2
3
4
5
6
7
8
9
10
local function printMap()
local scr = "" --字符串段初始化
for y = 1, 5 do
for x = 1,4 do
scr = scr .. string.sub(map[x][y].name,1,3) --对于棋盘矩阵的每一个点,按其棋子的名字进行打印,只打印第一个字。
end
scr = scr .. "\n" --换行
end
print(scr) --输出
end

实际上,如果你更细致的话,对于棋子矩阵的每个点,都对应一下名字,这样就可以输出完整名字了。不过懒得写了。

下面是移动棋子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
local function move(x,y)
local tx,ty = selected.pos[1] + x,selected.pos[2] + y --目标位置
local coll --是否碰撞标志
for y,tab in ipairs(selected.grid) do --对于选定棋子的每个点
for x,tab in ipairs(tab) do
if not map[tx + x - 1] --如果无次点说明超出棋盘范围了
or ( map[tx + x - 1][ty + y -1] ~= empty --如果是空值是自身那么就不碰撞可以走
and map[tx + x - 1][ty + y -1] ~= selected) then --否则就不能移动跳出循环
coll = true
break
end
end
end
if coll then
print("不能这样移动哦!") --不能移动的提示
else
selected.pos[1] = tx --如果移动了就把棋子的位置移动到目标位置
selected.pos[2] = ty
updateMap() --更新地图信息即清空棋盘、摆放棋子
checkWin() --暂时不说,因为只有移动的时候才可能胜利
end
end

接下来我们考虑一下互动的问题。
一般而言,命令行程序的互动的方法是命令+参数的形式。而对于我们的华容道而言,可能的效果是这样:
move caocao 1,0
move bing 0,-1
然后我们可以通过split方法来切割命令和参数。最后解析出我们想要的行为。
不过我们发现move一直在重复,而且对于棋子的控制一般我们会选中一个连续移动,移动的方式也只有上下左右四种,因此,为了简便操作我们换了另一种方案。
选中物体,然后adws来控制上下左右,这样仅仅通过简单的按键就可以实现目的了。对于棋子,我们希望用他们的简称,于是就有其ctrl字段的意义了。
下面是实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
local moveDir = {
w = {0,-1},
s = {0,1},
a = {-1,0},
d = {1,0}
}
local function interactive() --互动
printMap() --显示器盘
print("输入help获得帮助;输入quit退出") -- 显示帮助
print(string.format("当前选中\"%s\"",selected.name)) --显示当前选中
local cmd = io.read() --等待用户输入
for k,role in pairs(data) do --如果用户指令是任意棋子的控制指令,那么就将当前的控制棋子切换到它
if role.ctrl == cmd then
selected = role
printMap()
return interactive() --继续互动
end
end
if cmd == "help" then --显示帮助信息
print(help)
elseif cmd == "quit" then --退出,命令行嘛,直接返回互动模式就退出了。
return
elseif cmd == "show" then --再次显示棋盘
printMap()
elseif cmd == "reset" then --重置
data = loadfile("data.lua")() --这里是一个lua的技巧,后文讲解
selected = data.bing
updateMap()
elseif moveDir[cmd] then --移动控制命令,这里也是一个小技巧
move(unpack(moveDir[cmd]))
else
print("未知命令")
end
return interactive() --尾调用 实际上就是实现了个无限循环而已。
end

最后就是程序的初始化与循环了。

1
2
3
4
5
6
7
8
9
10
local data =require "data" --读取棋子信息
local function updateMap() --更新棋盘是个组合技,清空棋盘及摆棋子
clearMap()
setupMap()
end
print("欢迎来到命令行华容道") --欢迎信息
selected = data.bing --设置一个当前选中的棋子
updateMap() -- 初始化一下棋盘
interactive() --开始互动模式

以上就是华容道的全部代码啦,很简单?

重点

下面将一下一些lua的特性以及上面需要注意的地方。

  1. 一定要记住,lua是解释型的语言,因此在一些定义和调用上要注意顺序。否则会报未定义的错误。
  2. 数据和逻辑分开写有利于维护。
  3. 尽量使用局部变量。当然,这与你语句块的层数有关,如果仅仅是一层的话就无所谓了。
  4. 有一些表可以重复利用的,没必要复制那么多,比如棋子数据中的grid。
  5. Lua中oop的所谓单例模式就是一个表啦,我们这里的所有棋子实际上是通过数据模板做出的单例,因为我们在使用中改变了棋子的位置。
  6. 一定要注意,require得到的永远是第一次require文件的返回值,无论多少次都是这样,因此,你重新用require对棋子进行重置是不行的,需要用到一个用法:loadfile(“data.lua”)() 或者 dofile(“data.lua”)。这样每次得到的都是新的实例。实际上如果你愿意,也可以使用这种特性来做oop。
  7. lua并不提供switch方法,所以感觉不太舒服,不过table的键值中的值可以是function,所以可以用table来做switch。
  8. 还有就是遍历中的提前退出机制,比如在碰撞检测时,我们检测其任意地点有障碍而非所有地点没有障碍就是为了减少循环次数。

拓展

  1. 刚才已经说了,对名字的显示进行处理的话可以显示出全名来。
  2. 把命令行的形式改成图形界面。
  3. 自定义其他棋盘形状和兵种。
  4. 自动解正常版华容道
  5. 通过自动解任意华容道来自动设计随机华容道。(筛选中步长较多但有解的棋盘)

全部代码

main.lua

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
local map = {}
local data =require "data"
local empty = {
name = " ",
pos = {0,0},
grid = {}
}
local selected = nil
local function clearMap()
for x = 1, 4 do
map[x] = {}
for y = 1,5 do
map[x][y] = empty
end
end
end
local function setupMap()
for _,role in pairs(data) do
local roleX = role.pos[1]
local roleY = role.pos[2]
for y,tab in ipairs(role.grid) do
for x,tab in ipairs(tab) do
map[roleX + x - 1][roleY + y -1] = role
end
end
end
end
local function printMap()
local scr = ""
for y = 1, 5 do
for x = 1,4 do
scr = scr .. string.sub(map[x][y].name,1,3)
end
scr = scr .. "\n"
end
print(scr)
end
local function updateMap()
clearMap()
setupMap()
end
local function checkWin()
local pos = data.caocao.pos
if pos[1] == 2 and pos[2] == 4 then
print("混蛋,你让曹操跑了!")
end
end
local help = [[
请输入人物简码选中人物,按回车键确定;
按a,w,s,d来对选中人物进行移动,按回车键确定;
人物为姓名前两字缩写,士兵为数字1,2,3,4
输入quit退出游戏;
输入show显示棋牌;
输入reset重启游戏;
输入help游戏帮助
]]
local function move(x,y)
local tx,ty = selected.pos[1] + x,selected.pos[2] + y
local coll
for y,tab in ipairs(selected.grid) do
for x,tab in ipairs(tab) do
if not map[tx + x - 1]
or ( map[tx + x - 1][ty + y -1] ~= empty
and map[tx + x - 1][ty + y -1] ~= selected) then
coll = true
break
end
end
end
if coll then
print("不能这样移动哦!")
else
selected.pos[1] = tx
selected.pos[2] = ty
updateMap()
checkWin()
end
end
local moveDir = {
w = {0,-1},
s = {0,1},
a = {-1,0},
d = {1,0}
}
local function interactive()
printMap()
print("输入help获得帮助;输入quit退出")
print(string.format("当前选中\"%s\"",selected.name))
local cmd = io.read()
for k,role in pairs(data) do
if role.ctrl == cmd then
selected = role
printMap()
return interactive()
end
end
if cmd == "help" then
print(help)
elseif cmd == "quit" then
return
elseif cmd == "show" then
printMap()
elseif cmd == "reset" then
data = dofile("data.lua")
selected = data.bing
updateMap()
elseif moveDir[cmd] then
move(unpack(moveDir[cmd]))
else
print("未知命令")
end
return interactive()
end
-----go------
print("欢迎来到命令行华容道")
selected = data.bing
updateMap()
interactive()

data.lua

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
local data = {}
local grid12 = {{1},{1}}
local grid21 = {{1,1}}
local grid11 = {{1}}
local grid22 = {{1,1},{1,1}}
data.caocao = {
name = "曹操",
pos = {2,1},
grid = grid22,
ctrl = "cc"
}
data.zhangfei = {
name = "张飞",
pos = {1,1},
grid = grid12
}
data.zhaoyun = {
name = "赵云",
pos = {1,3},
grid = grid12,
ctrl = "zy"
}
data.huangzhong = {
name = "黄忠",
pos = {4,1},
grid = grid12,
ctrl = "hz"
}
data.machao = {
name = "马超",
pos = {4,3},
grid = grid12,
ctrl = "mc"
}
data.guanyu = {
name = "关羽",
pos = {2,3},
grid = grid21,
ctrl = "gy"
}
data.bing = {
name = "兵",
pos = {1,5},
grid = grid11,
ctrl = "1"
}
data.zu = {
name = "卒",
pos = {4,5},
grid = grid11,
ctrl = "2"
}
data.shi = {
name = "士",
pos = {2,4},
grid = grid11,
ctrl = "3",
}
data.zuo = {
name = "佐",
pos = {3,4},
grid = grid11,
ctrl = "4"
}
return data