{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "Some breif notes from a lazy Sunday morning spent exploring the [minecraft coding environment](/blog/2018/minecraft-python-2.html) that I set up using [Raspberry Jam](http://www.instructables.com/id/Python-coding-for-Minecraft/) earlier. I'm taking my notes in [Jupyter](http://jupyter-notebook-beginner-guide.readthedocs.io/en/latest/what_is_jupyter.html) while I explore the Minecraft API and poke around with one of the sample programs. Since my blogging engine [Nikola](https://getnikola.com/) also supports Jupyter Notebooks as one of it's import formats, I found that I could do [Litterate Programming](http://www.literateprogramming.com/) for Minecraft quite nicely!\n", "" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "----\n", "\n", "# More about the API\n", "\n", "The *Stuff About Code* web site has some really useful detail for the methods included in the API:\n", "\n", " * [MCPI API tutorial](http://www.stuffaboutcode.com/2013/04/minecraft-pi-edition-api-tutorial.html) and [basics](http://www.stuffaboutcode.com/2013/01/raspberry-pi-minecraft-api-basics.html)\n", " * [MCPI API reference](http://www.stuffaboutcode.com/p/minecraft-api-reference.html)\n", " * [Minecraft Stuff documentation](http://minecraft-stuff.readthedocs.io/en/latest/minecraftdrawing.html) (higher-level primatives and turtle-graphics)\n", " * And heaps of example projects including in-world Snake and a Cannon that fires blocks to blow things up, which show in detail how to use the API\n", " \n", "\n", "# Exploring in a Notebook\n", "\n", "The [PythonTool Mod documentation](https://ngcm.github.io/PythonTool-Mod/startcoding/) shows using Jupyter Notebooks to explore Minecraft interactively. Let's try that, since Nikola also uses Notebooks.\n", "\n", "## Nikola/Python setup\n", "\n", "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.\n", "\n", "First, add the necessary Python libraries if not already installed:\n", "\n", "```\n", "activate nikola\n", "pip install jupyter-notebook minecraftstuff\n", "```\n", "\n", "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.\n", "\n", "```\n", "[src][mjl@milo:~/hax/net/blog/milosophical.me]\n", "[17:11](nikola)$ nikola new_post -d -2 -f ipynb\n", "Creating New Post\n", "-----------------\n", "\n", "Title: minecraft-python-3\n", "Scanning posts..........done!\n", "[2018-02-10T10:50:09Z] NOTICE: compile_ipynb: No kernel specified, assuming \"python3\".\n", "[2018-02-10T10:50:09Z] INFO: new_post: Your post's metadata is at: posts/2018/minecraft-python-3.meta\n", "[2018-02-10T10:50:09Z] INFO: new_post: Your post's text is at: posts/2018/minecraft-python-3.ipynb\n", "[src:?][mjl@milo:~/hax/net/blog/milosophical.me]\n", "[21:50](nikola)$\n", "```\n", "\n", "I also need to link to the MCPIPY library files, so that I can `import` them in Jupyter:\n", "\n", "```\n", "cd posts/2018\n", "ln -s ~/fun/minecaft/mcpipy/mcpi mcpi\n", "git ignore mcpi #I don't want this in my blog code\n", "```\n", "\n", "Now start the notebook:\n", "\n", "```\n", "[src:!?][mjl@milo:~/Grid/MEGA/Projects/blog/milosophical.me/posts/2018]\n", "[08:10](nikola)$ jupyter notebook\n", "[I 08:10:32.921 NotebookApp] Serving notebooks from local directory: /Users/mjl/Grid/MEGA/Projects/blog/milosophical.me/posts/2018\n", "[I 08:10:32.921 NotebookApp] 0 active kernels\n", "[I 08:10:32.921 NotebookApp] The Jupyter Notebook is running at: http://localhost:8888/?token=08f71c4de52c71b408c44442bea7774169095a902db68336\n", "[I 08:10:32.921 NotebookApp] Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).\n", "[C 08:10:32.923 NotebookApp]\n", "\n", " Copy/paste this URL into your browser when you connect for the first time,\n", " to login with a token:\n", " http://localhost:8888/?token=08f71c4de52c71b408c44442bea7774169095a902db68336\n", "[I 08:10:32.999 NotebookApp] Accepting one-time-token-authenticated connection from ::1\n", "```\n", "\n", "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." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Hacking an example\n", "\n", "Let's play with the **zhuowei_rainbow.py** example code (you can see the [original code on github](https://github.com/arpruss/raspberryjammod/blob/master/mcpipy/zhuowei_rainbow.py) (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:\n", "\n", "```python\n", "#!/usr/bin/env python\n", "\n", "#\n", "# mcpipy.com retrieved from URL below, written by zhuowei\n", "# http://www.minecraftforum.net/topic/1638036-my-first-script-for-minecraft-pi-api-a-rainbow/\n", "\n", "import mcpi.minecraft as minecraft\n", "import mcpi.block as block\n", "from math import *\n", "import server\n", "\n", "colors = [14, 1, 4, 5, 3, 11, 10]\n", "\n", "mc = minecraft.Minecraft.create(server.address)\n", "height = 60\n", "\n", "mc.setBlocks(-64,0,0,64,height + len(colors),0,0)\n", "for x in range(0, 128):\n", " for colourindex in range(0, len(colors)):\n", " y = sin((x / 128.0) * pi) * height + colourindex\n", " mc.setBlock(x - 64, int(y), 0, block.WOOL.id, colors[len(colors) - 1 - colourindex])\n", "```\n", "\n", "This is a nice simple program that has a few things I'd like to change.\n", "\n", " * I'd like to use STAINED_GLASS blocks instead of WOOL, so that the rainbow is transparent\n", " * It has a bug where it clears all the blocks under it to AIR which breaks anything that you have underneath the rainbow:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "![](/pixels/minecraft/broken-donut.png)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ " * It'd be nice to be able to place a rainbow near where the player is, instead of allways at 0,0,0\n", " * 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\n", " \n", "\n", "## Start up Minecraft with the Forge mods and play a Single-player game\n", " \n", "Okay, now I can edit inline of this post, I'll just copy-paste the code from the file into here. Let's hack!" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Import the Minecraft library objects" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "ename": "ModuleNotFoundError", "evalue": "No module named 'server'", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mModuleNotFoundError\u001b[0m Traceback (most recent call last)", "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[1;32m 2\u001b[0m \u001b[0;32mimport\u001b[0m \u001b[0mmcpi\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mblock\u001b[0m \u001b[0;32mas\u001b[0m \u001b[0mblock\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 3\u001b[0m \u001b[0;32mfrom\u001b[0m \u001b[0mmath\u001b[0m \u001b[0;32mimport\u001b[0m \u001b[0;34m*\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 4\u001b[0;31m \u001b[0;32mimport\u001b[0m \u001b[0mserver\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[0;31mModuleNotFoundError\u001b[0m: No module named 'server'" ] } ], "source": [ "import mcpi.minecraft as minecraft\n", "import mcpi.block as block\n", "from math import *\n", "import server" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Oh, whoops!\n", "\n", "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):" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "/Users/mjl/Grid/MEGA/fun/minecraft/mcpipy\n" ] }, { "data": { "text/plain": [ "['', 'address = \"127.0.0.1\"', '', '', 'is_pi = False']" ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "%cd ~/fun/minecraft/mcpipy/\n", "!!egrep -v '^#' server.py" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Connect to the game\n", "\n", "(This also sets some values used later)" ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "collapsed": true }, "outputs": [], "source": [ "colors = [14, 1, 4, 5, 3, 11, 10]\n", "\n", "mc = minecraft.Minecraft.create()\n", "height = 60\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Start experimenting\n", "\n", "The rest of the zhouwhei_rainbow code looks like this:\n", "\n", "```python\n", "mc.setBlocks(-64,0,0,64,height + len(colors),0,0)\n", "for x in range(0, 128):\n", " for colourindex in range(0, len(colors)):\n", " y = sin((x / 128.0) * pi) * height + colourindex\n", " mc.setBlock(x - 64, int(y), 0, block.WOOL.id, colors[len(colors) - 1 - colourindex])\n", "```\n", "\n", "The first line:\n", "\n", "```python\n", "mc.setBlocks(-64,0,0,64,height + len(colors),0,0)\n", "```\n", "\n", "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" ] }, { "cell_type": "code", "execution_count": 4, "metadata": { "collapsed": true }, "outputs": [], "source": [ "for x in range(0, 128):\n", " for colourindex in range(0, len(colors)):\n", " y = sin((x / 128.0) * pi) * height + colourindex\n", " mc.setBlock(x - 64, int(y), 0, block.WOOL.id, colors[len(colors) - 1 - colourindex])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "![](/pixels/minecraft/zhouwei_rainbow.png)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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.\n", "\n", "The first thing to do is change it to STAINED_GLASS instead. I'll just repeat this loop and replace the existing blocks:" ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "collapsed": true }, "outputs": [], "source": [ "for x in range(0, 128):\n", " for colourindex in range(0, len(colors)):\n", " y = sin((x / 128.0) * pi) * height + colourindex\n", " mc.setBlock(x - 64, int(y), 0, block.STAINED_GLASS.id, colors[len(colors) - 1 - colourindex])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "![](/pixels/minecraft/rainbow-glass.png)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "That's better. You can see the little village showing through the rainbow now.\n", "\n", "Let's change the `setBlock` call so that it sweeps over the *Z* axis insted of *X*:" ] }, { "cell_type": "code", "execution_count": 6, "metadata": { "collapsed": true }, "outputs": [], "source": [ "for z in range(0, 128):\n", " for colourindex in range(0, len(colors)):\n", " y = sin((z / 128.0) * pi) * height + colourindex\n", " mc.setBlock(0, int(y), z - 64, block.STAINED_GLASS.id, colors[len(colors) - 1 - colourindex])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "![](/pixels/minecraft/rainbow-cross.png)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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.\n", "\n", "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](/jargon/html/M/magic-number.html) (like 128 and 64). Let's fix that by defining some values:" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Vec3(-1436.9899566384854,74.0,-278.2069795419402)" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "height = 60\n", "width = 128.0\n", "pos = mc.player.getPos()\n", "pX=int(pos.x)\n", "pZ=int(pos.z)\n", "\n", "pos" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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.\n", "\n", "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:" ] }, { "cell_type": "code", "execution_count": 8, "metadata": { "collapsed": true }, "outputs": [], "source": [ "for z in range(0, int(width)):\n", " for idx in range(0, len(colors)):\n", " mc.setBlock(pX,\n", " int(sin((z / width) * pi) * height + idx),\n", " pZ - int(width/2) + z,\n", " block.STAINED_GLASS.id,\n", " colors[len(colors) - 1 - idx])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "![](/pixels/minecraft/rainbow-village.png)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Sweet! That was fun, next to try doing this with an actual kid.... ;-)" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.6.4" } }, "nbformat": 4, "nbformat_minor": 2 }