使用 Python 游玩我的世界。

参考资料来源:各种网络资料和《零基础学 Minecraft 编程》,人民邮电出版社。

37-0.png
图书的两位作者

开始

环境配置教程:https://www.bilibili.com/video/BV1FG4y1X7SQ

注意在安装 java -jar BuildTools.jar 的时候会打印一大大大坨信息,整个过程大约6分钟,需要梯子。出现如下信息则表示成功:

37-1.png
37-1.png

启动服务器:

cd D:\mcserver
java -Xms1024M -Xmx1024M -jar spigot-1.19.4.jar

多人游戏,服务器地址127.0.0.1

op username   # 给权限

这个时候就可以使用作弊码了。作弊码附在文章末尾。

# hellofrompy.py
from mcpi.minecraft import Minecraft
import mcpi.block as block
import time

if __name__ == '__main__':
    time.sleep(3)
    print('连接MC...')
    mc = Minecraft.create()

    mc.postToChat("hello frome Python")
    x, y, z = mc.player.getTilePos()
    print(x, y, z)
    mc.setBlocks(x, y, z+5, x, y, z+5, 1, 0)

生成方块

$${\scriptstyle\text{摧毁和放置方块教程}}$$

Minecraft 坐标信息:

  • 正东:x轴正方向
  • 正南:z轴正方向
  • 正上:y轴正方向
# 16blocks.py
from mcpi.minecraft import Minecraft
import mcpi.block as block
import time

if __name__ == '__main__':
    time.sleep(3)
    
    # 连接到 MC
    mc = Minecraft.create()

    # 得到角色当前位置
    x, y, z = mc.player.getTilePos()
    
    # 羊毛
    for i in range(16):
        mc.setBlock(x+3, y, z+i, 35, i)
        # 35是羊毛的代号,0是白色
        time.sleep(0.5) # 加个延时以演示

效果如下:

37-2.png
37-2.png

采用更多层循环以生成平面和立体。

多个方块同时放置的函数:

mc.setBlocks(x1, y1, z1, x2, y2, z2, blockid, someindex)

(x1, y1, z1)(x2, y2, z2)对应一个长方体的两个对角顶点。根据这两个顶点我们可以绘制出一个由指定方块组成的长方体。

想要一个空心的?缩小一圈用air方块填充。

简单的例子

实时显示玩家的位置:

# showpos.py
from mcpi.minecraft import Minecraft
import time

mc = Minecraft.create()

while True:
    time.sleep(1)
    pos = mc.player.getTilePos()
    mc.postToChat("x="+str(pos.x)+" y="+str(pos.y)+" z="+str(pos.z))

注意:若坐标错误,需输入/setworldspawn 0 0 0

玩家进入某一区域,超过3秒弹出:

# bump2sky.py
from mcpi.minecraft import Minecraft
import time

x1 = 60   # y = 80
x2 = 63
z1 = -17
z2 = -15
infield = 0

def bump2sky(x, y, z):
    for t in range(8):
        y += 0.90*t
        mc.player.setPos(x+5, y, z)

time.sleep(3)
mc = Minecraft.create()
while True:
    time.sleep(1)
    pos = mc.player.getTilePos()
    if pos.x>=x1 and pos.x<=x2 and pos.z>=z1 and pos.z<=z2:
        mc.postToChat("warnning")
        infield += 1
    else:
        infield = 0
    if infield > 3:
        mc.postToChat("too slow!")
        bump2sky(pos.x, pos.y, pos.z)

效果如下:

37-3.gif
37-3.gif

绘制任意图像

知道了如何生成方块,就知道了如何绘制任意图像。

37-4.png
37-4.png

对于一张图片,如果我们能根据每一个像素点的 RGB 值找到对应颜色的 Minecraft 方块,就有可能在游戏中绘制该图像。

然而,并不是所有颜色都可以找到对应的 Minecraft 方块。对于找不到的颜色,我们希望有一个替代颜色,这个颜色应当在人眼视觉上相近。

为此,我们需要一个寻找相近颜色的算法。

已经知道,颜色可用 RGB 表示,是否相近的 RGB 值表示的颜色在视觉上相似呢?答案是否定的。但可喜可贺,还是有以 RGB 值计算颜色距离的公式(具体参考此链接(该内容已备份)):

$$\bar{r}=\frac{C_{1,R}+C_{2,R}}{2}$$
$$\Delta R=C_{1,R}-C_{2,R}$$
$$\Delta G=C_{1,G}-C_{2,G}$$
$$\Delta B=C_{1,B}-C_{2,B}$$
$$\Delta C=\sqrt{(2+\frac{\bar{r}}{256})\times \Delta R^{2}+4\Delta G^2+(2+\frac{255-\bar{r}}{256})\times \Delta B^2}$$

可以根据以上公式编写代码计算两个颜色的距离。不过,我们采用另一种更好的方案。

CIEDE2000算法是CIE(国际照明委员会)于2000年提出的,它是Delta E算法的改进版本。CIEDE2000算法考虑了人眼感知颜色的非线性特性,并将颜色差异分解为亮度、色相和饱和度三个因素,从而更准确地计算颜色之间的相似度。CIEDE2000算法还考虑了颜色对比度的影响,因此在低对比度颜色之间的比较中表现更好。

这个算法已经封装好了,安装 colormath 库:

pip install colormath

使用此库需要初步了解颜色空间,并大致知道一些函数接受什么,返回什么,做了什么。

「动态类型一时爽,代码重构火葬场。」

colormath 官方文档:

注意,如果提示:

AttributeError: module 'numpy' has no attribute 'asscalar'. Did you mean: 'isscalar'? 

这是因为:

  • numpy的版本过高,需要降版本(高情商)
  • numpy你真就不考虑历史兼容呗(低情商)

降版本的命令:

pip uninstall numpy
pip install -U numpy==1.22.4

OK,颜色替代的问题解决了。但我们还缺一个映射方案。我们需要将颜色编码映射到具体的 Minecraft 方块的编号上。这方面的资料找起来比较麻烦。我直接给出链接:

以上链接只是名字到编号的映射,我们总不能用眼睛看出物品的颜色编码吧?因此需要配合颜色到名字的映射表(该内容已备份):

注意有些方块是不能用的,它可能是个半砖,可能会随重力掉落,可能会自爆(例如仙人掌,不过仙人掌另有它用,之后再说),甚至可能根本不是一个方块。为了准确性,还需要在游戏里对比,这个比对过程是痛苦的,为了让世界 no more pain,我决定贴出初步整理的映射表:

ffffff,80
dcdcdc,35
dcd9d3,155
d5c98c,216
909090,42
faee4d,41
b0a836,19:1
c5c52c,19
2c4199,35:11
8d909e,82
4f4f4f,1:6
848484,35:8
6db015,35:5
d06d8e,35:6
414141,35:7
ffc125,35:4
ba6d2c,35:1
9941ba,35:2
6699d8,35:3
486c98,251:3
416d84,35:9
6d3699,35:10
58412c,35:12
586d2c,35:13
842c2c,35:14
151515,35:15
4b3f26,5:5
7b663e,25
4fbcb7,57
8a8adc,79
667f33,251:13

这个映射表非常粗糙,如果希望有更丰富的色彩表现,请手动修改之。

注意:一些魔改的mcpi版本并不需要此映射表,而可以直接使用物品名称。但是这种mcpi版本与本文不兼容,也不与《零基础学 Minecraft 编程》兼容。更重要的,它们主要是通过某盘传播(而不是pip安装),如果你和我一样抵制某盘,不想在上面花钱,尽量不要倒向这个版本。

下面放出完整代码:

from colormath.color_objects import sRGBColor, LabColor
from colormath.color_diff import delta_e_cie2000
from colormath import color_conversions
from imageio import imread
from mcpi.minecraft import Minecraft

def rgb2hex(red, green, blue):
    """
    返回一个 HexColorString
    """
    return '{:02x}{:02x}{:02x}'.format(red, green, blue)

def hex2lab(hx):
    color_srbg = sRGBColor.new_from_rgb_hex(hx)
    color_lab = color_conversions.convert_color(color_srbg, LabColor)
    return color_lab

def closest_color(hexStr):
    """
    接受一个 HexColorString
    返回列表中与该颜色最接近的颜色
    """
    if hexStr in colorhave: return colorhave[hexStr]

    closest_color = None
    closest_distance = None
    labStr = hex2lab(hexStr)
    
    for color_code, color_name in colorhave.items():
        color_c = hex2lab(color_code)
        distance = delta_e_cie2000(labStr, color_c)
        if closest_distance is None or distance < closest_distance:
            closest_distance = distance
            closest_color = color_name

    return closest_color

def wip(r, g, b):
    s = closest_color(rgb2hex(r, g, b))
    tmpls = []
    if ':' in s:
        tmpls.append(eval(s.split(":")[0]))
        tmpls.append(eval(s.split(":")[1]))
    else:
        tmpls.append(eval(s))
    return tmpls

fo = open("D:\mypycode\mccode\colormap.txt")
ls = []
for line in fo:
    line = line.replace("\n","")
    ls.append(line.split(","))
fo.close()

colorhave = {}
for i in range(len(ls)):
    colorhave[ls[i][0]] = ls[i][1]

im = imread('D:\mypycode\imageiostuff\\rmdpic.jpg')
h, w, _ = im.shape

mc = Minecraft.create()
x0, y0, z0 = mc.player.getTilePos()
x0 += 5

for y in range(h):
    for x in range(w):
        r, g, b = im[y][x]
        flagls = wip(r, g, b)
        if len(flagls)==1:
            mc.setBlock(x0+x, y0+h-y, z0, flagls[0])
        else:
            mc.setBlock(x0+x, y0+h-y, z0, flagls[0], flagls[1])

一个简单的优化是:把最相近颜色存储起来。

from colormath.color_objects import sRGBColor, LabColor
from colormath.color_diff import delta_e_cie2000
from colormath import color_conversions
from imageio.v2 import imread
from mcpi.minecraft import Minecraft

def rgb2hex(red, green, blue):
    """
    返回一个 HexColorString
    """
    return '{:02x}{:02x}{:02x}'.format(red, green, blue)

def hex2lab(hx):
    color_srbg = sRGBColor.new_from_rgb_hex(hx)
    color_lab = color_conversions.convert_color(color_srbg, LabColor)
    return color_lab

closestcolor = {}
def closest_color(hexStr):
    """
    接受一个 HexColorString
    返回列表中与该颜色最接近的颜色
    """
    if hexStr in colorhave: return colorhave[hexStr]
    if hexStr in closestcolor: return closestcolor[hexStr]

    closest_color = None
    closest_distance = None
    labStr = hex2lab(hexStr)
    
    for color_code, color_name in colorhave.items():
        color_c = hex2lab(color_code)
        distance = delta_e_cie2000(labStr, color_c)
        if closest_distance is None or distance < closest_distance:
            closest_distance = distance
            closest_color = color_name

    closestcolor[hexStr] = closest_color
    return closestcolor[hexStr]

def wip(r, g, b):
    s = closest_color(rgb2hex(r, g, b))
    tmpls = []
    if ':' in s:
        tmpls.append(eval(s.split(":")[0]))
        tmpls.append(eval(s.split(":")[1]))
    else:
        tmpls.append(eval(s))
    return tmpls

fo = open("D:\mypycode\mccode\colormap.txt")
ls = []
for line in fo:
    line = line.replace("\n","")
    ls.append(line.split(","))
fo.close()

colorhave = {}
for i in range(len(ls)):
    colorhave[ls[i][0]] = ls[i][1]

im = imread('D:\mypycode\imageiostuff\delisha2.jpg')
h, w, _ = im.shape

mc = Minecraft.create()
x0, y0, z0 = mc.player.getTilePos()
x0 += 5

for y in range(h):
    for x in range(w):
        r, g, b = im[y][x]
        flagls = wip(r, g, b)
        if len(flagls)==1:
            mc.setBlock(x0+x, y0+h-y, z0, flagls[0])
        else:
            mc.setBlock(x0+x, y0+h-y, z0, flagls[0], flagls[1])

当然,更好的方案是在命令中传入要绘制的图片,而不是每次绘制都要手动修改代码。

看一下效果(图片太大了两边加载不出来):

37-5.png
37-5.png

作为对比,给出原图:

37-5dot5.jpg
高240 宽430

头发的细节:

37-6.png
37-6.png

考虑如下生成的图片:

37-7.png
37-7.png

我们希望只保留这个谜之柴犬,那么可以在数据文件中添加仙人掌的记录。因仙人掌自爆的特性,只会留下柴犬本犬:

37-8.png
37-8.png

对于更大的图片,可能需要在平坦的世界绘制,或者铺在地上。对于像素更高的图片,可能需要事先压缩,或者转换为像素画形式。普通图片转换为像素画形式可以使用 Python 实现,不再赘述。

37-9.png
ΤΗΙ ΚΑΛΛΙΣΤΗΙ

从上图可以看出,这个颜色映射表还是非常简陋的。但我没耐心再一个个地比对了。

作弊码

/gamerule doDaylightCycle false  # 永久白天
/gamemode creative               # 创造 
/gamemode survival               # 生存
/gamemode adventure              # 冒险
/gamemode spectator              # 旁观
/spawnpoint                      # 重生点设置为当前位置
/seed                            # 种子
/give username minecraft:diamond_ore 64      # 钻石
/give username minecraft:emerald_block 64    # 绿宝石
/tp username {x} {y} {z}                     # 传送
/time set 0                                  # morning 6
/time set 9500                               # noon
/time set 12000                              # 黄昏
/xp {int}                                    # 给经验
/effect username {type} {time} {level}       # 给药
/gamerule keepInventory true                 # 死亡不掉落
/summon minecraft:zombie                     # 召唤僵尸
/setblock {x} {y} {z} minecraft:redstone     # 设置红石方块