控制流
「松饼人物编辑器:从入门到欧耶」系列教程(六)
上一讲的习题参考答案
1)
自己摸牌前输出(false), 下家摸牌前输出(true), 对家摸牌前输出(false), 上家摸牌前输出(false)。
2)
on_draw应为ondraw,
self.right()应为self:right(),
T34:new应为T34.new,
3m应为"3m",
Mount:lighta应为mount:lighta。
3)
变量t的定义出现过早,
who == self的判断也是多余的。
4)
function ondraw()
if who ~= self then
return
end
local t = T34.new("1m")
mount:lighta(t, 100)
end
5) 一种可能的实现:
function ondraw()
local t = nil
if who == self then
t = T34.new("1m")
else
t = T34.new("1p")
end
mount:lighta(t, 1000)
end
6) 方法有很多,比如:
- 在
lighta的上一行打日志,确认每家摸牌前t的值 - 三狗战后观察牌谱
7)
function ondraw()
if who ~= self then
return
end
local t = T34.new(game:getselfwind(self) .. "f")
mount:lighta(t, 2333)
end
8)
function ondraw()
if who ~= self then
return
end
local dealer = game:getdealer()
if dealer == self then
mount:lighta(T34.new("1p"), 2333)
else
mount:lighta(T34.new(game:getroundwind() .. "f"), 2333)
end
end
9)
function ondraw()
if who ~= self then
return
end
mount:lighta(T34.new(game:getselfwind(self:right()) .. "f"), 2333)
end
10)
function ondraw()
if who ~= self then
return
end
local hand = game:gethand(self)
count2drag8(hand, mount, "m")
count2drag8(hand, mount, "p")
count2drag8(hand, mount, "s")
end
function count2drag8(hand, mount, suit)
local two = T34.new(2 .. suit)
if hand:ct(two) > 0 then
local eight = T34.new(8 .. suit)
mount:lighta(eight, 2333)
end
end
11)
function ondraw()
if who ~= self then
return
end
local extra = game:getextraround()
if extra < 9 then
local begin = extra % 3 + 1
drag(mount, begin)
end
end
function drag(mount, begin)
local head = T34.new(begin .. "m")
local middle = T34.new((begin + 3) .. "m")
local tail = T34.new((begin + 6) .. "m")
mount:lighta(head, 2333)
mount:lighta(middle, 2333)
mount:lighta(tail, 2333)
end
分离出drag函数的目的是为了防止ondraw太长不易读。
12) 办法很多,比如:
- 并非采用
game:getextraround()获取真实的本场数, 而是直接「假装」目前为某本场,并测试后续的逻辑。- 例如在上面的答案中,可用
local extra = 3测试三本场时的表现
- 例如在上面的答案中,可用
- 与容易连庄的角色同场测试
多级 if 嵌套
回顾一下第一讲中,自己摸牌输出「哇!」, 别人摸牌输出「emmm…」的那个例子:
function ondraw()
if who == self then
print("哇!")
else
print("emmm...")
end
end
现在我们把这个例子扩展一下, 让它在下家摸牌的输出「鄙视下家」,其余的不变:
function ondraw()
if who == self then
print("哇!")
else
if who == self:right() then
print("鄙视下家")
else
print("emmm...")
end
end
end
这段代码的意思是:
- 如果摸牌的人是自己:
- 输出「哇!」
- 否则:
- 如果摸牌的人是自己的下家:
- 输出「鄙视下家」
- 否则:
- 输出「emmm…」
- 如果摸牌的人是自己的下家:
就像上面的例子所展示的一样,if里面可以嵌套if。
我们不妨再套一层,让它在对家摸牌时输出「嫌弃对家x」。
function ondraw()
if who == self then
print("哇!")
else
if who == self:right() then
print("鄙视下家")
else
if who == self:cross() then
print("嫌弃对家x")
else
print("emmm...")
end
end
end
end
道理很简单,就是一级一级地判断 ——
但是这画风太鬼畜了,看着就晕,尤其是最后的end四连,简直洗脑x
好在 Lua 提供了一种方便的写法 —— elseif:
function ondraw()
if who == self then
print("哇!")
elseif who == self:right() then
print("鄙视下定")
elseif who == self:cross() then
print("嫌弃对家x")
else
print("emmm...")
end
end
通过elseif,代码的逻辑没有变,但看起来舒服了很多,也避免了鬼畜的end四连。
虽然我们已经用过很过次if语句了,
但里面的很多东西其实都没讲清楚,下面详细补充一下。
if 的工作方式
最简单的if语句长这个样子:
if 条件 then
语句1
语句2
语句3
...
end
「条件」的求值结果为「真」,则执行里面的那堆语句(称做「语句体」),
「条件」为「假」时则会跳过语句体,执行end后面的代码。
if 语句可以有 else 部分和/或 elseif 部分:
if 条件1 then
语句体1
elseif 条件2 then
语句体2
elseif 条件3 then
语句体3
else
语句体N
end
这种情况下,会先计算「条件1」。 「条件1」为「真」,则执行「语句体1」。 「条件1」为「假」,则继续计算「条件2」,以此类推。 这里需要注意的是计算的顺序。如果前面的条件为「真」, 后面的条件就不会被计算。
每一个语句体都会开启一个新的局部变量的作用域(即「块」)。
在 Lua 中,boolean 类型的false,和 nil 类型的nil是「假」,
其余的任何类型的任何值都是「真」。
这意味着 number 类型的0,string 类型的""也都是「真」。
逻辑运算 and 及 or
我们可以用and, or运算来做出一些更复杂的判断条件。
例如,下面的代码会在摸牌人是自己,「并且」自风为西时输出「哇!」:
function ondraw()
if who == self and game:getselfwind(self) == 3 then
print("哇!")
end
end
再比如,下面的代码会在摸牌人是自己,「或者」摸牌人是对家时输出「哇!」:
function ondraw()
if who == self or who == self:cross() then
print("哇!")
end
end
and的字面意思是「与」,or的字面意思是「或」,
但它们的具体求值方式有点特别。
and的求值方式是:左手边为假则返回左手边,否则返回右手边;
or的求值方式是:左手边为真则返回左手边,否则返回右手边。
无论是and还是or,都会先计算左手边,再计算右手边;
如果计算左手边后发现结果就是左手边,那么右手边就不会被计算。
这种规则在实现了「与」和「或」的字面意义的同时,
给予了人们「皮一下」的空间。
例如,以下表达式可以在a和b都是 number 类型的情况下,
求出a和b之间的较大值:
a > b and a or b
要理解上面的表达式,首先要明白运算符之间的优先级。
在 Lua 运算符中,or的优先级最低,and排倒数第二。
因此,上面的表达式等同于:
((a > b) and a) or b
所以首先从a > b开始计算:
- 如果
a > b结果为true,表达式等同于(true and a) or b- 因为
and左手边为真,and表达式的结果就是右手边,表达式等同于a or b - 接下来,
or的左手边是真,因此or的结果为左手边a- 只有
false和nil是假,所以 number 类型的a永远都是真
- 只有
- 于是,
a大于b时,整个表达式的结果就是a
- 因为
- 如果
a > b结果为false,表达式等同于(false and a) or b- 因为
and左手边为假,and表达式的结果就是左手边,表达式等同于false or b - 接下来,
or的左手边是假,因此or的结果为右手边b - 于是,
a不大于b时,整个表达式的结果就是b
- 因为
在 Lua 中,这种「条件and结果1or结果2」的写法是很常见的,
遇到的时候要马上反应过来。
逻辑运算 not
我们可以用not实现「非」语义。
例如,下面的技能会在当前手牌有副露时容易摸白板:
function ondraw()
if who ~= self then
return
end
if not game:gethand(self):ismenzen() then
mount:lighta(T34.new("1y"), 1000)
end
end
其中,game:gethand(self):ismenzen()用于判断当前手牌是否为门前清,
如果当前手牌为门前清,其值就是true;有副露则是false(详见 API 文档)。
由于我们在前面加了一个not,整个if条件就会在副路时为true,
门前清时为false。
not的计算结果是 boolean 类型的。
被not的表达式值为假时(即false或nil),not的结果为true;
反之则为false。
需要注意的是,not的优先级高于目前我们见过的所有运算符。
这意味着,对一长串表达式取not时,要加上括号。例如:
not (game:gethand():ismenzen() and game:getround() == 0) -- 表达式 1
not game:gethand():ismenzen() and game:getround() == 0 -- 表达式 2
(not game:gethand():ismenzen()) and game:getround() == 0 -- 表达式 3
上面的三个表达式中,1 和另外两个的意思是完全不同的, 而 2 和 3 的意思则是完全相同的。
练习题(第一组)
1) 表达「摸牌人是自己或对家」的条件能否写成下面的样子?为什么?
function ondraw()
if who == (self or self:cross()) then
print("哇!")
end
end
2) 判断以下表达式真假:
nil == "nil" or nil
nil == ("nil" and nil)
1 + 1 == 2 or nil
1 + 1 ~= (2 or nil)
(0 and true) and T34.new("3y") or false
3) 实现技能:
- 如果谁的手里都没有场风,技能无效果
- 如果刚好有一家的手里有场风,就给他塞白板
- 如果多家手里有场风,给按照摸牌顺序离自己最近的一家塞白板
- 上述逻辑中,塞白板的目标不排除自己
上面的需求写得很啰嗦, 但需求啰嗦不代表代码也一定要复杂。 我们需要尽可能把同样的事情理解得更精辟一些。
for 循环
下面我们要做一个万子染手挂。 原理很简单,就是容易摸到各种万子。
function ondraw()
if who ~= self then
return
end
mount:lighta(T34.new("1m"), 100)
mount:lighta(T34.new("2m"), 100)
mount:lighta(T34.new("3m"), 100)
mount:lighta(T34.new("4m"), 100)
mount:lighta(T34.new("5m"), 100)
mount:lighta(T34.new("6m"), 100)
mount:lighta(T34.new("7m"), 100)
mount:lighta(T34.new("8m"), 100)
mount:lighta(T34.new("9m"), 100)
end
太蠢了。这是病,得治。所以,循环大法好。
function ondraw()
if who ~= self then
return
end
for i = 1, 9 do
mount:lighta(T34.new(i .. "m"), 100)
end
end
如此一改,所做的事情没有变,但代码质量很高了。
这里的i是一个变量。
变量名本是可以随便起的,不叫i,叫cat, dog都可以,
但习惯上循环里面一般都用i。
后面的1, 9是i的循环范围,
每循环一次,i都会自增 1。
循环变量i虽然不带local字样,但也是一个局部变量,
只在for循环内部有效。
除了for循环外,Lua 还有while和repeat循环,
可支持更多样的循环条件。
因为在人物技能里用得不多,就不介绍了。
练习题(第二组)
4) 实现技能:
- 当手里的万子数量处于 3~6 张之间时,容易摸到白板
- 包括刚好 3 张及刚好 6 张的情况
- 计数时不考虑副露及暗杠
5) 实现技能:
- 容易摸到现有手牌中最多的花色
- 现有手牌中万子最多,则容易摸到万子;饼子、索子同理
- 风牌最多,则容易摸风牌;三元牌最多,则容易摸三元牌
- 计数时不考虑副露及暗杠
6) 实现技能:
- 若手里有 (K)m 且无 (K+1)m,则容易摸到 (K+1)m
- 即:有 1m 时招 2m,有 2m 时招 3m,以此类推
- 0 < K < 9
- 若手里有 9m 且无 1m,则容易摸到 1m
7) 实现技能:
- 容易在第 N 巡摸到数值为 N 的数牌
- 第 1 巡容易摸 1m, 1p, 1s;第 2 巡容易摸 2m, 2p, 2s……
- N < 1 或 N > 9 时,技能无效果
- 为方便起见,此处「巡数」单指摸牌次数,不考虑鸣牌
- 摸牌次数可通过
ondraw被调用的次数间接统计
- 摸牌次数可通过
- 庄家的跳牌视为第 0 巡,闲家的第一次摸牌为第 1 巡
下一讲:表