序
今天,我尝试了编程农场这个有趣的游戏。
它是一个以类python语言为蓝本的自动化处理游戏,在游戏中,玩家需要收集资源,解锁科技树。
值得注意的是,在这个游戏中,性能也是很重要的一环,需要考虑游戏规则和无人机性能。
can_harvest()
如果作物没有长好就直接选择收割,则什么也得不到,需要先进行判断:
if can_harvest(): harvest()Tree
种植树可以获得木头,但是如果树的十字格周围,则生长速度会降低:
树喜欢保持一定的空间,相邻的两棵树生长速度会减慢。每在其正北、正东、正西或正南方向地块上种植一棵树,都会使其生长时间翻倍。所以如果在每个地块上都种上树,它们的生长时间将是原来的 222*2 = 16 倍。
也就是,一棵树的周围,只能种别的,比如草。
看起来很复杂,其实很简单,只需要让 偶数行的奇数格 和 奇数行的偶数格 种树就好了。
也就是:
#规定x,y是无人机的当前位置,可以使用get_post_xy获得,这里简化。if x%2 == 0 and y %2 != 0:
if x%2 != 0 and y %2 == 0:这么写不太优雅,我们可以优化成一条if
if (x%2) != (y%2):原理很简单,奇数行的偶数格,和偶数行的奇数格,本质上是x和y的奇偶性相反。
种树和种胡萝卜,用key来进行解耦约束吧!
因为我还要种胡萝卜,所以定义了一个key进行规范约束:
key = 3while True: # 如果在key以内,那么执行种树 if x <= key: pass # 否则,我们就执行种胡萝卜 else: passpass有点类似godot,先跳过这里的执行,我们先不需要研究具体的细节。
种间种植:草与木
在刚才的说明中,我们知道, Tree 的上下左右周围有别的树,效率会非常低,因此需要考虑种间种植。由于种植胡萝卜需要消耗草,权衡了一下,我打算种草。
种草非常简单,在土地上直接收割,会自动长出来。
# 没了!就这么多if can_harvest(): harvest()种树则相对复杂一些,由于我的无人机等级比较高,导致无人机一轮的时候,树木还没有成熟,会空转一轮(性能会被严重消耗),我们可以浇水让它长得更快:
if can_harvest(): harvest()plant(Entities.Tree)# 直接硬编码 0.8 进去,是比较不好的写法。后续维护会比较麻烦,我们可以定义一个water_key,但是这里就不多写了。if get_water() < 0.8: use_item(Items.Water)初始化:让土地回归初始
有时候,代码写错了,导致整个土地非常杂乱。
如果手动一个一个修改,就非常麻烦。我们需要编写一个初始化的代码,可以让所有地块回归到普通土地上。
首先,我们需要对整个农场进行遍历,这意味着未来农场大小更新了,这个代码依然是可以复用的(尽管效率不高!未来再优化吧!)
# 获得地图大小,比较有趣的是,在很多系统中的get size获取的值是一个二维数组或容器,例如[2,3],<2,3># 但是,编程农场的地图始终是正方形的(至少我目前玩到的部分是),因此get size获取的值,可以同时给x和y。x_max = get_world_size()y_max = get_world_size()遍历:
for _ in range(y_max): for _ in range(x_max):for xxx in range 是一个python的语法,好吧,我在c++里可没见过。
它的完整写法是:for xxx in range(a,b,c) ,你或许注意到了 _ ,有点类似JavaScript的匿名函数,这是一个匿名变量,代表我们只使用它参与循坏,而不进行其他使用,非常方便。
这句代码的意思是,使一个变量(_)在遍历中,从a到b,每次+c
说白了,这不是就是 for( int x = a; x < b; x += c ) 吗?
其实我还是觉得c的语法比较优美,python系的语法看起来怪怪的(以及我感觉 || 比 or 帅多了!)
遍历完毕了!现在我们要进行初始化了,对于每一块土地,我们应该进行这样的判定:
- 如果有作物->收获
- 如果本格是耕作过的土地(Soil),进行耕作,恢复成原本的样子。
代码也非常简单:
x_max = get_world_size()y_max = get_world_size()
while True: for _ in range(y_max): for _ in range(x_max): x = get_pos_x() y = get_pos_y() if can_harvest(): harvest() if get_ground_type() == Grounds.Soil: till() move(East) if x >= x_max-1: move(North)不过,这段代码写的比较糟糕,维护性也比较差。
其实可以把固定的移动次数和地图大小完全解耦:
x_max = get_world_size()y_max = get_world_size()
while True: x = get_pos_x() y = get_pos_y() if can_harvest(): harvest() if get_ground_type() == Grounds.Soil: till() move(East) if x >= x_max-1: move(North)此外,AI给了另一种版本:
x_max = get_world_size()y_max = get_world_size()
while True: for y in range(y_max): for x in range(x_max): if can_harvest(): harvest() if get_ground_type() == Grounds.Soil: till() if x < x_max-1: move(East) if y < y_max: move(East) move(North)AI给的代码中,不仅减少了 get_world_size() 的调用频率(仅剩1次),而且对x和y的移动进行了解耦。
但是,这段代码就完全没有问题吗?
事实上,尽管性能更加出色,但我更喜欢上面的版本,因为每次都要获取x和y的实时位置,这意味着无论无人机在什么位置开始,位置都是正确的。
而第二个版本的代码,则严格要求必须在(0,0)开始才正确,否则会出错:
由于遍历是x_max次(即地图长度),在目前的地图中,固定为6次。但如果坐标是从(2,2)开始,那么在第一个循环中:
# 这里用了一种伪代码的写法:# 在这个循环中,x需要位移6次for y=0 in range(y_max): for x=0 in range(x_max): # 但实际上,2+6 = 2,进行完毕后x回到2 if x < x_max-1: pass # 但由于 move north嵌在x循环中,所以y的偏移也随着错了。 # 这意味着后续的所有x和y都会错位。 if y < y_max: pass而第一个版本的代码,每次循环的时候,都会立刻获取一次x和y的位置。并且使用匿名变量,不关心这个变量的变化,因此不受到影响。
而get size的性能开销,对于这个游戏来说可以完全忽略了。
附代码:自动化种间种植。
# 先想办法获得地图的大小x_max = get_world_size()y_max = get_world_size()key = 4
while True: for y in range(y_max + 1): for x in range(x_max + 1): y = get_pos_y() x = get_pos_x() if x <= key: if (x%2) != (y%2): if can_harvest(): harvest() plant(Entities.Tree) if get_water() < 0.8: use_item(Items.Water) #elif y%2 == 0: #if can_harvest(): #harvest() #plant(Entities.Bush) else: if can_harvest(): harvest() else: if get_ground_type() != Grounds.Soil: till() if can_harvest(): harvest() plant(Entities.Carrot) if get_water() < 0.8: use_item(Items.Water) move(East) if x >= x_max-1: move(North)