Exploring Minecraft code in Jupyter

Some breif notes from a lazy Sunday morning spent exploring the minecraft coding environment that I set up using Raspberry Jam earlier. I'm taking my notes in Jupyter while I explore the Minecraft API and poke around with one of the sample programs. Since my blogging engine Nikola also supports Jupyter Notebooks as one of it's import formats, I found that I could do Litterate Programming for Minecraft quite nicely!


More about the API

The Stuff About Code web site has some really useful detail for the methods included in the API:

Exploring in a Notebook

The PythonTool Mod documentation shows using Jupyter Notebooks to explore Minecraft interactively. Let's try that, since Nikola also uses Notebooks.

Nikola/Python setup

I'm using a Python Virtual Environment to run my Nikola blog engine locally. So I'll make changes in there, but it's also possible to do this in a separate "minecraft" venv.

First, add the necessary Python libraries if not already installed:

activate nikola
pip install jupyter-notebook minecraftstuff

When that's ready, I made a new post in Nikola, but you can skip that and just create a new Notebook after starting Jupyter.

[src][[email protected]:~/hax/net/blog/milosophical.me]
[17:11](nikola)$ nikola new_post -d -2 -f ipynb
Creating New Post
-----------------

Title: minecraft-python-3
Scanning posts..........done!
[2018-02-10T10:50:09Z] NOTICE: compile_ipynb: No kernel specified, assuming "python3".
[2018-02-10T10:50:09Z] INFO: new_post: Your post's metadata is at: posts/2018/minecraft-python-3.meta
[2018-02-10T10:50:09Z] INFO: new_post: Your post's text is at: posts/2018/minecraft-python-3.ipynb
[src:?][[email protected]:~/hax/net/blog/milosophical.me]
[21:50](nikola)$

I also need to link to the MCPIPY library files, so that I can import them in Jupyter:

cd posts/2018
ln -s ~/fun/minecaft/mcpipy/mcpi mcpi
git ignore mcpi   #I don't want this in my blog code

Now start the notebook:

[src:!?][[email protected]:~/Grid/MEGA/Projects/blog/milosophical.me/posts/2018]
[08:10](nikola)$ jupyter notebook
[I 08:10:32.921 NotebookApp] Serving notebooks from local directory: /Users/mjl/Grid/MEGA/Projects/blog/milosophical.me/posts/2018
[I 08:10:32.921 NotebookApp] 0 active kernels
[I 08:10:32.921 NotebookApp] The Jupyter Notebook is running at: http://localhost:8888/?token=08f71c4de52c71b408c44442bea7774169095a902db68336
[I 08:10:32.921 NotebookApp] Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).
[C 08:10:32.923 NotebookApp]

    Copy/paste this URL into your browser when you connect for the first time,
    to login with a token:
        http://localhost:8888/?token=08f71c4de52c71b408c44442bea7774169095a902db68336
[I 08:10:32.999 NotebookApp] Accepting one-time-token-authenticated connection from ::1

This will launch your computer's default web browser and automatically log it into the Jupyter system with the token. You can then navigate to the post or create a new Notebook by using the New button.

Hacking an example

Let's play with the zhuowei_rainbow.py example code (you can see the original code on github (arpruss' copy, with changes for Python3). You should probably open that link in a new window so you can compare with this post. Here it is in case the link ever goes stale:

#!/usr/bin/env python

#
# mcpipy.com retrieved from URL below, written by zhuowei
# http://www.minecraftforum.net/topic/1638036-my-first-script-for-minecraft-pi-api-a-rainbow/

import mcpi.minecraft as minecraft
import mcpi.block as block
from math import *
import server

colors = [14, 1, 4, 5, 3, 11, 10]

mc = minecraft.Minecraft.create(server.address)
height = 60

mc.setBlocks(-64,0,0,64,height + len(colors),0,0)
for x in range(0, 128):
        for colourindex in range(0, len(colors)):
                y = sin((x / 128.0) * pi) * height + colourindex
                mc.setBlock(x - 64, int(y), 0, block.WOOL.id, colors[len(colors) - 1 - colourindex])

This is a nice simple program that has a few things I'd like to change.

  • I'd like to use STAINED_GLASS blocks instead of WOOL, so that the rainbow is transparent
  • It has a bug where it clears all the blocks under it to AIR which breaks anything that you have underneath the rainbow:

  • It'd be nice to be able to place a rainbow near where the player is, instead of allways at 0,0,0
  • The rainbow is sweeping over the X axis (West-East), but I'd rather it went over the Z access (South-North) so that it's in the right orrientation: 90 degrees offset relative to the Sun

Start up Minecraft with the Forge mods and play a Single-player game

Okay, now I can edit inline of this post, I'll just copy-paste the code from the file into here. Let's hack!

Import the Minecraft library objects

In [1]:
import mcpi.minecraft as minecraft
import mcpi.block as block
from math import *
import server
---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
<ipython-input-1-dbfb1a3ebb83> in <module>()
      2 import mcpi.block as block
      3 from math import *
----> 4 import server

ModuleNotFoundError: No module named 'server'

Oh, whoops!

Well, okay, Jupyter can't find the server library... hmm. It's not part of mcpi, that's why. What does that code actually do? I'll take a look (just the code please, no comments):

In [2]:
%cd ~/fun/minecraft/mcpipy/
!!egrep -v '^#' server.py
/Users/mjl/Grid/MEGA/fun/minecraft/mcpipy
Out[2]:
['', 'address = "127.0.0.1"', '', '', 'is_pi = False']

That is code to specify which minecraft server to connect to. A lot of the mcpipy examples include it. It's designed so that you keep a server.py together with your scripts and edit it to change where to connect to. I'm going to just skip it for running within Jupyter. I'll ignore that error, and replace any mention of server.address or server.is_pi with appropriate values.

Connect to the game

(This also sets some values used later)

In [3]:
colors = [14, 1, 4, 5, 3, 11, 10]

mc = minecraft.Minecraft.create()
height = 60

Start experimenting

The rest of the zhouwhei_rainbow code looks like this:

mc.setBlocks(-64,0,0,64,height + len(colors),0,0)
for x in range(0, 128):
        for colourindex in range(0, len(colors)):
                y = sin((x / 128.0) * pi) * height + colourindex
                mc.setBlock(x - 64, int(y), 0, block.WOOL.id, colors[len(colors) - 1 - colourindex])

The first line:

mc.setBlocks(-64,0,0,64,height + len(colors),0,0)

is what's clearing all the blocks to AIR (block id 0). It's not necessary, so I won't do that, but I will run the rest

In [4]:
for x in range(0, 128):
        for colourindex in range(0, len(colors)):
                y = sin((x / 128.0) * pi) * height + colourindex
                mc.setBlock(x - 64, int(y), 0, block.WOOL.id, colors[len(colors) - 1 - colourindex])

I nice wooly rainbow. The code loops over the X axis and calculates nice curves for each colour in the rainbow, using the sin function. Each block in the curve is set to WOOL with a data value for the colour, taken from the colors array.

The first thing to do is change it to STAINED_GLASS instead. I'll just repeat this loop and replace the existing blocks:

In [5]:
for x in range(0, 128):
        for colourindex in range(0, len(colors)):
                y = sin((x / 128.0) * pi) * height + colourindex
                mc.setBlock(x - 64, int(y), 0, block.STAINED_GLASS.id, colors[len(colors) - 1 - colourindex])

That's better. You can see the little village showing through the rainbow now.

Let's change the setBlock call so that it sweeps over the Z axis insted of X:

In [6]:
for z in range(0, 128):
        for colourindex in range(0, len(colors)):
                y = sin((z / 128.0) * pi) * height + colourindex
                mc.setBlock(0, int(y), z - 64, block.STAINED_GLASS.id, colors[len(colors) - 1 - colourindex])

That was simple — just swap the position of the arguments to setBlock. I also renamed the loop variable to z so that it's clear what it's doing.

It would be nice to have a rainbow over one of those villages. To do that, the code needs to use a different starting point to the hard-coded 0,0-64,0 in code above. Actually the code uses a lot of magic numbers (like 128 and 64). Let's fix that by defining some values:

In [7]:
height = 60
width = 128.0
pos = mc.player.getPos()
pX=int(pos.x)
pZ=int(pos.z)

pos
Out[7]:
Vec3(-1436.9899566384854,74.0,-278.2069795419402)

Okay, that shows a nice way to debug Minecraft hacks from within a Notebook too: you can just get Jupyter to run a cell and the last value (the pos at the end) will be output like above, in the Notebook.

Using these variables in place of the numbers should be enough to solve our other issues. I'll reformat the code a bit so that it fits within the page width nicer too:

In [8]:
for z in range(0, int(width)):
    for idx in range(0, len(colors)):
        mc.setBlock(pX,
                    int(sin((z / width) * pi) * height + idx),
                    pZ - int(width/2) + z,
                    block.STAINED_GLASS.id,
                    colors[len(colors) - 1 - idx])

Sweet! That was fun, next to try doing this with an actual kid.... ;-)