├── .coveragerc ├── .gitignore ├── .travis-ci.sh ├── .travis.yml ├── AUTHORS.txt ├── CONTRIBUTING.md ├── CONTROLS.txt ├── HOW_TO_RUN.txt ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── mcpi ├── __init__.py ├── block.py ├── connection.py ├── event.py ├── exceptions.py ├── minecraft.py ├── mock_server.py ├── util.py └── vec3.py ├── pytest.ini ├── setup.py ├── tests ├── test_block.py ├── test_event.py ├── test_minecraft.py ├── test_usage.py └── test_vec3.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = 3 | minecraft 4 | 5 | [report] 6 | exclude_lines = 7 | # Don't complain about missing debug-only code: 8 | def __repr__ 9 | if self\.debug 10 | 11 | # Don't complain if tests don't hit defensive assertion code: 12 | raise AssertionError 13 | raise NotImplementedError 14 | 15 | # Don't complain if non-runnable code isn't run: 16 | if 0: 17 | if __name__ == .__main__.: 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.swp 3 | .idea/ 4 | .cache/ 5 | .tox/ 6 | *.egg-info/ 7 | .coverage 8 | -------------------------------------------------------------------------------- /.travis-ci.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Based on a test script from avsm/ocaml repo https://github.com/avsm/ocaml 3 | 4 | CHROOT_DIR=/tmp/arm-chroot 5 | MIRROR=http://archive.raspbian.org/raspbian 6 | VERSION=wheezy 7 | CHROOT_ARCH=armhf 8 | 9 | # Debian package dependencies for the host 10 | HOST_DEPENDENCIES="debootstrap qemu-user-static binfmt-support sbuild" 11 | 12 | # Debian package dependencies for the chrooted environment 13 | GUEST_DEPENDENCIES="sudo python2.7 python3 python-pip python3-pip git" 14 | 15 | # Command used to run the tests 16 | 17 | function setup_arm_chroot { 18 | # Host dependencies 19 | sudo apt-get update 20 | sudo apt-get install -qq -y ${HOST_DEPENDENCIES} 21 | 22 | # Create chrooted environment 23 | sudo mkdir ${CHROOT_DIR} 24 | sudo debootstrap --foreign --no-check-gpg --include=fakeroot,build-essential \ 25 | --arch=${CHROOT_ARCH} ${VERSION} ${CHROOT_DIR} ${MIRROR} 26 | sudo cp /usr/bin/qemu-arm-static ${CHROOT_DIR}/usr/bin/ 27 | sudo chroot ${CHROOT_DIR} ./debootstrap/debootstrap --second-stage 28 | sudo sbuild-createchroot --arch=${CHROOT_ARCH} --foreign --setup-only \ 29 | ${VERSION} ${CHROOT_DIR} ${MIRROR} 30 | 31 | # Create file with environment variables which will be used inside chrooted 32 | # environment 33 | echo "export ARCH=${ARCH}" > envvars.sh 34 | echo "export TOXENV=${TOXENV}" > envvars.sh 35 | echo "export TRAVIS_BUILD_DIR=${TRAVIS_BUILD_DIR}" >> envvars.sh 36 | chmod a+x envvars.sh 37 | 38 | # Install dependencies inside chroot 39 | sudo chroot ${CHROOT_DIR} apt-get update 40 | sudo chroot ${CHROOT_DIR} apt-get --allow-unauthenticated install \ 41 | -qq -y ${GUEST_DEPENDENCIES} 42 | 43 | # Create build dir and copy travis build files to our chroot environment 44 | sudo mkdir -p ${CHROOT_DIR}/${TRAVIS_BUILD_DIR} 45 | sudo rsync -av ${TRAVIS_BUILD_DIR}/ ${CHROOT_DIR}/${TRAVIS_BUILD_DIR}/ 46 | 47 | # Indicate chroot environment has been set up 48 | sudo touch ${CHROOT_DIR}/.chroot_is_done 49 | 50 | # Call ourselves again which will cause tests to run 51 | sudo chroot ${CHROOT_DIR} bash -c "cd ${TRAVIS_BUILD_DIR} && ./.travis-ci.sh" 52 | } 53 | 54 | function run_tests { 55 | if [ -f "./envvars.sh" ]; then 56 | . ./envvars.sh 57 | fi 58 | 59 | echo "--- Running tests" 60 | echo "--- Environment: $(uname -a)" 61 | echo "--- Working directory: $(pwd)" 62 | ls -la 63 | 64 | sudo pip install tox 65 | tox 66 | } 67 | 68 | if [ "${ARCH}" = "arm" ]; then 69 | if [ -e "/.chroot_is_done" ]; then 70 | echo "--- Running inside chrooted environment" 71 | 72 | run_tests 73 | else 74 | echo "--- Setting up chrooted ARM environment" 75 | setup_arm_chroot 76 | fi 77 | else 78 | sudo apt-get update 79 | sudo apt-get install -qq -y python-pip # Because we might not be running in a Pythonic environment 80 | 81 | run_tests 82 | fi 83 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: erlang 2 | env: 3 | - ARCH=arm TOXENV=py27 4 | - ARCH=arm TOXENV=py32 5 | - TOXENV=cov 6 | - TOXENV=flake8 7 | script: 8 | - bash -ex .travis-ci.sh 9 | matrix: 10 | fast_finish: true 11 | -------------------------------------------------------------------------------- /AUTHORS.txt: -------------------------------------------------------------------------------- 1 | # Original Authors at Mojang 2 | 3 | - Aron Nieminen 4 | 5 | # py3minepi Developers 6 | 7 | - George Hickman (@ghickman) 8 | - Jørn Lomax (@jvlomax) 9 | - Kristian Glass (@doismellburning) 10 | - Jonathan Fine (@jonathanfine) 11 | - Ben Nuttall (@bennuttall) 12 | - Miles Gould (@pozorvlak) 13 | - Danilo Bargen (@dbrgn) 14 | 15 | # Contributors 16 | 17 | - ... 18 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributors Guidelines 2 | 3 | Contributions to py3minepi are welcome! Please adhere to the following 4 | contribution guidelines though: 5 | 6 | - Please follow the [coding 7 | guidelines](https://github.com/py3minepi/py3minepi#coding-guidelines). 8 | - Use meaningful commit messages: First line of your commit message should be a 9 | very short summary (ideally 50 characters or less). After the first line of 10 | the commit message, add a blank line and then a more detailed explanation (when 11 | relevant). [This](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) 12 | is a nice blog post concerning git commit messages. 13 | - Add yourself to the [AUTHORS.txt 14 | file](https://github.com/py3minepi/py3minepi/blob/master/AUTHORS.txt). 15 | - Even if you have write access to the repository, never push commits directly 16 | to master. Always create a branch or a fork and post a pull request. The pull 17 | request will then be merged by someone other than you after at least 1 18 | approving comment by a py3minepi organization member. 19 | 20 | If you want to make sure that your changes didn't break anything, you may also 21 | run the [test suite](https://github.com/py3minepi/py3minepi#testing) before 22 | committing and pushing your changes. (If you don't, the changes will still be 23 | automatically tested though on Travis CI.) 24 | 25 | Thanks for your contribution! 26 | -------------------------------------------------------------------------------- /CONTROLS.txt: -------------------------------------------------------------------------------- 1 | === KEYBOARD === 2 | W,A,S,D - Move (navigate inventory) 3 | SPACE - Jump, double tap to start/stop flying, hold to fly higher 4 | SHIFT - Sneak, hold to fly lower 5 | E - Open inventory 6 | 1-8 - Select inventory slot item to use 7 | ESC - Show/hide menu 8 | TAB - Release mouse without showing menu 9 | ENTER - Confirm menu selection 10 | 11 | === Mouse === 12 | Steer - Look/turn around 13 | Left mouse button - Remove block (hold) 14 | Right mouse button - Place block, hit block with sword 15 | Mouse wheel - Select inventory slot item to use 16 | 17 | -------------------------------------------------------------------------------- /HOW_TO_RUN.txt: -------------------------------------------------------------------------------- 1 | 1. If XWindows isn't started yet, start it by typing "startx". 2 | 2. Launch LXTerminal by clicking the icon on the desktop 3 | 3. cd to the "mcpi" folder 4 | 4. Launch Minecraft - Pi Edition by typing "./minecraft-pi". 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | *** The real license isn't finished yet, here's what goes in plain english *** 2 | 3 | You may execute the minecraft-pi binary on a Raspberry Pi or an emulator 4 | You may use any of the source code included in the distribution for any purpose (except evil) 5 | 6 | You may not redistribute any modified binary parts of the distribution 7 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst AUTHORS.txt LICENSE.txt 2 | recursive-exclude * *.pyc 3 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | README 2 | ====== 3 | 4 | .. image:: https://secure.travis-ci.org/py3minepi/py3minepi.png?branch=master 5 | :alt: Build status 6 | :target: https://travis-ci.org/py3minepi/py3minepi 7 | 8 | .. image:: https://coveralls.io/repos/py3minepi/py3minepi/badge.png?branch=master 9 | :alt: Coverage 10 | :target: https://coveralls.io/r/py3minepi/py3minepi 11 | 12 | .. image:: https://landscape.io/github/py3minepi/py3minepi/master/landscape.png 13 | :alt: Code Health 14 | :target: https://landscape.io/github/py3minepi/py3minepi 15 | 16 | 17 | `Minecraft: Pi Edition `__ is awesome. 18 | 19 | However it uses Python 2. We're moving it to Python 3 (without any official 20 | approval) and offering it for download here. 21 | 22 | We hope this makes people's lives easier. 23 | 24 | 25 | Goals 26 | ----- 27 | 28 | - [x] Python 3 29 | - [ ] TESTS (pytest, tox, flake8, coverage) 30 | - [ ] More intuitive API focusing on getting some mining done and hiding implementation details 31 | - [ ] Backwards compatibility with the existing codebase (with `__init__` foo) so existing scripts will continue to work 32 | - [ ] Connection backends (socket, in memory for testing) 33 | - [ ] Clever socket usage so disconnects can be dealt with 34 | - [ ] Make the code base more readable and thus maintainable 35 | - [ ] A CI test suite running an rPi emulator (with Travis) 36 | - [ ] Improve code documentation both in the code base and with a RTD page 37 | - [ ] Find missing functions that are in the java API but not described in the python API 38 | 39 | 40 | Coding Guidelines 41 | ----------------- 42 | 43 | All code (except legacy API compatibility code) should adhere to `PEP8 44 | `_ with some exceptions: 45 | 46 | - Try to keep your line length below 80, but if it looks better then use up to 47 | 99 characters per line. 48 | - You can ignore the following three PEP8 rules: E126 (continuation line 49 | over-indented for hanging indent), E127 (continuation line over-indented for 50 | visual indent), E128 (continuation line under-indented for visual indent). 51 | 52 | You can check the code style for example by using `flake8 53 | `_. 54 | 55 | Some other things you should keep in mind: 56 | 57 | - Function names should mirror ingame commands where possible. 58 | - Group imports into three groups: First stdlib imports, then third party 59 | imports, then local imports. Put an empty line between each group. 60 | - Backwards compatibility must be maintained unless you have a very compelling 61 | reason. 62 | - KISS! 63 | 64 | For backwards compatibility with Python 2, please insert this header in every 65 | Python module:: 66 | 67 | # -*- coding: utf-8 -*- 68 | from __future__ import print_function, division, absolute_import, unicode_literals 69 | 70 | 71 | Testing 72 | ------- 73 | 74 | Testing for py3minepi is set up using `Tox `_ and 75 | `pytest `_. Violations of the `coding guidelines 76 | <#coding-guidelines>`__ are counted as test fails. 77 | 78 | The only requirement to run the tests is tox:: 79 | 80 | $ pip install tox 81 | 82 | **Running tests** 83 | 84 | To run the tests on all supported Python versions, simply issue :: 85 | 86 | $ tox 87 | 88 | To test only a single Python version, use the ``-e`` parameter:: 89 | 90 | $ tox -e py32 91 | 92 | To see the test coverage, use the ``cov`` testenv (which uses Python 3.2 by 93 | default):: 94 | 95 | $ tox -e cov 96 | 97 | All Python versions you need to test on need to be installed of course. 98 | 99 | 100 | Links 101 | ----- 102 | 103 | - `Raspberry Pi `_ 104 | - `Minecraft Pi `_ 105 | - `Minecraft Pi Usage page `_ 106 | - `Original API reference `_ 107 | - `Martin O'Hanlon GitHub `_ (useful test projects) 108 | -------------------------------------------------------------------------------- /mcpi/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/py3minepi/py3minepi-legacy/9f3bfba3ef6854b91fc223f2008b3cd84376768e/mcpi/__init__.py -------------------------------------------------------------------------------- /mcpi/block.py: -------------------------------------------------------------------------------- 1 | class Block: 2 | """Minecraft PI block description. Can be sent to Minecraft.setBlock/s""" 3 | def __init__(self, id, data=0): 4 | self.id = id 5 | self.data = data 6 | 7 | def __hash__(self): 8 | return (self.id << 8) + self.data 9 | 10 | def withData(self, data): 11 | return Block(self.id, data) 12 | 13 | def __iter__(self): 14 | """Allows a Block to be sent whenever id [and data] is needed""" 15 | return iter((self.id, self.data)) 16 | 17 | def __repr__(self): 18 | return "Block(%d, %d)" % (self.id, self.data) 19 | 20 | AIR = Block(0) 21 | STONE = Block(1) 22 | GRASS = Block(2) 23 | DIRT = Block(3) 24 | COBBLESTONE = Block(4) 25 | WOOD_PLANKS = Block(5) 26 | SAPLING = Block(6) 27 | BEDROCK = Block(7) 28 | WATER_FLOWING = Block(8) 29 | WATER = WATER_FLOWING 30 | WATER_STATIONARY = Block(9) 31 | LAVA_FLOWING = Block(10) 32 | LAVA = LAVA_FLOWING 33 | LAVA_STATIONARY = Block(11) 34 | SAND = Block(12) 35 | GRAVEL = Block(13) 36 | GOLD_ORE = Block(14) 37 | IRON_ORE = Block(15) 38 | COAL_ORE = Block(16) 39 | WOOD = Block(17) 40 | LEAVES = Block(18) 41 | GLASS = Block(20) 42 | LAPIS_LAZULI_ORE = Block(21) 43 | LAPIS_LAZULI_BLOCK = Block(22) 44 | SANDSTONE = Block(24) 45 | BED = Block(26) 46 | COBWEB = Block(30) 47 | GRASS_TALL = Block(31) 48 | WOOL = Block(35) 49 | FLOWER_YELLOW = Block(37) 50 | FLOWER_CYAN = Block(38) 51 | MUSHROOM_BROWN = Block(39) 52 | MUSHROOM_RED = Block(40) 53 | GOLD_BLOCK = Block(41) 54 | IRON_BLOCK = Block(42) 55 | STONE_SLAB_DOUBLE = Block(43) 56 | STONE_SLAB = Block(44) 57 | BRICK_BLOCK = Block(45) 58 | TNT = Block(46) 59 | BOOKSHELF = Block(47) 60 | MOSS_STONE = Block(48) 61 | OBSIDIAN = Block(49) 62 | TORCH = Block(50) 63 | FIRE = Block(51) 64 | STAIRS_WOOD = Block(53) 65 | CHEST = Block(54) 66 | DIAMOND_ORE = Block(56) 67 | DIAMOND_BLOCK = Block(57) 68 | CRAFTING_TABLE = Block(58) 69 | FARMLAND = Block(60) 70 | FURNACE_INACTIVE = Block(61) 71 | FURNACE_ACTIVE = Block(62) 72 | DOOR_WOOD = Block(64) 73 | LADDER = Block(65) 74 | STAIRS_COBBLESTONE = Block(67) 75 | DOOR_IRON = Block(71) 76 | REDSTONE_ORE = Block(73) 77 | SNOW = Block(78) 78 | ICE = Block(79) 79 | SNOW_BLOCK = Block(80) 80 | CACTUS = Block(81) 81 | CLAY = Block(82) 82 | SUGAR_CANE = Block(83) 83 | FENCE = Block(85) 84 | GLOWSTONE_BLOCK = Block(89) 85 | BEDROCK_INVISIBLE = Block(95) 86 | STONE_BRICK = Block(98) 87 | GLASS_PANE = Block(102) 88 | MELON = Block(103) 89 | FENCE_GATE = Block(107) 90 | GLOWING_OBSIDIAN = Block(246) 91 | NETHER_REACTOR_CORE = Block(247) 92 | -------------------------------------------------------------------------------- /mcpi/connection.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import errno 3 | import select 4 | import sys 5 | 6 | from . import exceptions 7 | from .util import flatten_parameters_to_string 8 | 9 | 10 | class RequestError(Exception): 11 | pass 12 | 13 | 14 | class Connection: 15 | """ 16 | Connection to a Minecraft Pi game. 17 | """ 18 | RequestFailed = "Fail" 19 | 20 | def __init__(self, address, port): 21 | """ 22 | Initialize TCP socket connection. 23 | """ 24 | self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 25 | try: 26 | self.socket.connect((address, port)) 27 | except socket.error as e: 28 | if e.errno != errno.ECONNREFUSED: 29 | # Not the error we are looking for, re-raise 30 | raise e 31 | msg = 'Could not connect to Minecraft server at %s:%s (connection refused).' 32 | raise exceptions.ConnectionError(msg % (address, port)) 33 | 34 | self.lastSent = '' 35 | 36 | def drain(self): 37 | """ 38 | Drains the socket of incoming data. 39 | """ 40 | while True: 41 | readable, _, _ = select.select([self.socket], [], [], 0.0) 42 | if not readable: 43 | break 44 | data = self.socket.recv(1500) 45 | e = 'Drained Data: <{}>\n'.format(data.strip()) 46 | e += 'Last Message: <{}>\n'.format(self.lastSent.strip()) 47 | sys.stderr.write(e) 48 | 49 | def send(self, f, *data): 50 | """ 51 | Sends data. Note that a trailing newline '\n' is added here. 52 | """ 53 | s = "%s(%s)\n" % (f, flatten_parameters_to_string(data)) 54 | 55 | self._send(s) 56 | 57 | def _send(self, s): 58 | """ 59 | The actual socket interaction from self.send, extracted for easier mocking 60 | and testing 61 | """ 62 | self.drain() 63 | self.lastSent = s 64 | 65 | self.socket.sendall(s.encode()) 66 | 67 | def receive(self): 68 | """ 69 | Receives data. Note that the trailing newline '\n' is trimmed. 70 | """ 71 | s = self.socket.makefile("r").readline().rstrip("\n") 72 | if s == Connection.RequestFailed: 73 | raise RequestError('{} failed'.format(self.lastSent.strip())) 74 | return s 75 | 76 | def sendReceive(self, *data): 77 | """ 78 | Sends and receive data. 79 | """ 80 | self.send(*data) 81 | return self.receive() 82 | -------------------------------------------------------------------------------- /mcpi/event.py: -------------------------------------------------------------------------------- 1 | from .vec3 import Vec3 2 | 3 | 4 | class BlockEvent: 5 | """An Event related to blocks (e.g. placed, removed, hit)""" 6 | HIT = 0 7 | 8 | def __init__(self, type, x, y, z, face, entityId): 9 | self.type = type 10 | self.pos = Vec3(x, y, z) 11 | self.face = face 12 | self.entityId = entityId 13 | 14 | def __repr__(self): 15 | # TODO: untangle .HIT and .Hit 16 | sType = { 17 | BlockEvent.HIT: "BlockEvent.HIT" 18 | }.get(self.type, "???") 19 | 20 | args = ( 21 | sType, 22 | self.pos.x, 23 | self.pos.y, 24 | self.pos.z, 25 | self.face, 26 | self.entityId 27 | ) 28 | return 'BlockEvent({}, {:.2f}, {:.2f}, {:.2f}, {:.2f}, {})'.format(*args) 29 | 30 | @staticmethod 31 | def Hit(x, y, z, face, entityId): 32 | return BlockEvent(BlockEvent.HIT, x, y, z, face, entityId) 33 | -------------------------------------------------------------------------------- /mcpi/exceptions.py: -------------------------------------------------------------------------------- 1 | class ConnectionError(RuntimeError): 2 | """ 3 | Raised for connection-related errors. 4 | """ 5 | pass 6 | -------------------------------------------------------------------------------- /mcpi/minecraft.py: -------------------------------------------------------------------------------- 1 | """ 2 | Minecraft PI low level api v0.1_1 3 | 4 | Note: many methods have the parameter *arg. This solution makes it 5 | simple to allow different types, and variable number of arguments. 6 | The actual magic is a mix of flatten_parameters() and __iter__. Example: 7 | A Cube class could implement __iter__ to work in Minecraft.setBlocks(c, id). 8 | 9 | (Because of this, it's possible to "erase" arguments. CmdPlayer removes 10 | entityId, by injecting [] that flattens to nothing) 11 | 12 | @author: Aron Nieminen, Mojang AB 13 | 14 | """ 15 | from .connection import Connection 16 | from .vec3 import Vec3 17 | from .event import BlockEvent 18 | from .block import Block 19 | from .util import flatten 20 | import math 21 | import warnings 22 | 23 | 24 | def intFloor(*args): 25 | """ 26 | Run math.floor on each argument passed in 27 | 28 | Arguments passed in are expected to be x, y & z coordinates. 29 | 30 | Returns integers (int). 31 | """ 32 | return [int(math.floor(a)) for a in flatten(args)] 33 | 34 | 35 | class CmdPositioner(object): 36 | """Methods for setting and getting positions""" 37 | def __init__(self, connection, packagePrefix): 38 | self.conn = connection 39 | self.pkg = packagePrefix 40 | 41 | def getPos(self, id): 42 | """Get entity position (entityId:int) => Vec3""" 43 | s = self.conn.sendReceive(self.pkg + ".getPos", id) 44 | return Vec3(*map(float, s.split(","))) 45 | 46 | def setPos(self, id, *args): 47 | """Set entity position (entityId:int, x,y,z)""" 48 | self.conn.send(self.pkg + ".setPos", id, args) 49 | 50 | def getTilePos(self, id): 51 | """Get entity tile position (entityId:int) => Vec3""" 52 | s = self.conn.sendReceive(self.pkg + ".getTile", id) 53 | return Vec3(*map(float, s.split(","))) 54 | 55 | def setTilePos(self, id, *args): 56 | """Set entity tile position (entityId:int, x,y,z)""" 57 | self.conn.send(self.pkg + ".setTile", id, intFloor(*args)) 58 | 59 | def setting(self, setting, status): 60 | """Set a player setting (setting, status). keys: autojump""" 61 | self.conn.send(self.pkg + ".setting", setting, 1 if bool(status) else 0) 62 | 63 | 64 | class CmdEntity(CmdPositioner): 65 | """Methods for entities""" 66 | def __init__(self, connection): 67 | super(CmdEntity, self).__init__(connection, "entity") 68 | 69 | 70 | class CmdPlayer(CmdPositioner): 71 | """Methods for the host (Raspberry Pi) player""" 72 | def __init__(self, connection): 73 | super(CmdPlayer, self).__init__(connection, "player") 74 | self.conn = connection 75 | 76 | def getPos(self): 77 | return CmdPositioner.getPos(self, []) 78 | 79 | def setPos(self, *args): 80 | return CmdPositioner.setPos(self, [], args) 81 | 82 | def getTilePos(self): 83 | return CmdPositioner.getTilePos(self, []) 84 | 85 | def setTilePos(self, *args): 86 | return CmdPositioner.setTilePos(self, [], args) 87 | 88 | 89 | class CmdCamera: 90 | def __init__(self, connection): 91 | self.conn = connection 92 | 93 | def setNormal(self, *args): 94 | """Set camera mode to normal Minecraft view ([entityId])""" 95 | self.conn.send("camera.mode.setNormal", args) 96 | 97 | def setFixed(self): 98 | """Set camera mode to fixed view""" 99 | self.conn.send("camera.mode.setFixed") 100 | 101 | def setFollow(self, *args): 102 | """Set camera mode to follow an entity ([entityId])""" 103 | self.conn.send("camera.mode.setFollow", args) 104 | 105 | def setPos(self, *args): 106 | """Set camera entity position (x,y,z)""" 107 | self.conn.send("camera.setPos", args) 108 | 109 | 110 | class CmdEvents: 111 | """Events""" 112 | def __init__(self, connection): 113 | self.conn = connection 114 | 115 | def clearAll(self): 116 | """Clear all old events""" 117 | self.conn.send("events.clear") 118 | 119 | def pollBlockHits(self): 120 | """Only triggered by sword => [BlockEvent]""" 121 | s = self.conn.sendReceive("events.block.hits") 122 | events = [e for e in s.split("|") if e] 123 | return [BlockEvent.Hit(*map(int, e.split(","))) for e in events] 124 | 125 | 126 | class Minecraft: 127 | """The main class to interact with a running instance of Minecraft Pi.""" 128 | def __init__(self, address="localhost", port=4711): 129 | self._conn = Connection(address, port) 130 | 131 | self.camera = CmdCamera(self._conn) 132 | self.entity = CmdEntity(self._conn) 133 | self.player = CmdPlayer(self._conn) 134 | self.events = CmdEvents(self._conn) 135 | 136 | self.getHeight = self.getGroundHeight 137 | 138 | def getBlock(self, *args): 139 | """Get block (x,y,z) => id:int""" 140 | return int(self._conn.sendReceive("world.getBlock", intFloor(args))) 141 | 142 | def getBlockWithData(self, *args): 143 | """Get block with data (x,y,z) => Block""" 144 | 145 | ans = self._conn.sendReceive("world.getBlockWithData", intFloor(args)) 146 | return Block(*map(int, ans.split(","))) 147 | 148 | """ 149 | @TODO (What?) 150 | """ 151 | def getBlocks(self, *args): 152 | """Get a cuboid of blocks (x0,y0,z0,x1,y1,z1) => [id:int]""" 153 | 154 | return int(self._conn.sendReceive("world.getBlocks", intFloor(args))) 155 | 156 | def setBlock(self, *args): 157 | """Set block (x,y,z,id,[data])""" 158 | self._conn.send("world.setBlock", intFloor(args)) 159 | 160 | def setBlocks(self, *args): # leaving thisone alone for now 161 | """Set a cuboid of blocks (x0,y0,z0,x1,y1,z1,id,[data])""" 162 | self._conn.send("world.setBlocks", intFloor(args)) 163 | 164 | def getGroundHeight(self, *args): 165 | """Get the height of the world (x,z) => int""" 166 | 167 | return int(self._conn.sendReceive("world.getHeight", intFloor(args))) 168 | 169 | def getPlayerEntityIds(self): 170 | """Get the entity ids of the connected players => [id:int]""" 171 | ids = self._conn.sendReceive("world.getPlayerIds") 172 | return list(map(int, ids.split("|"))) 173 | 174 | def saveCheckpoint(self): 175 | """Save a checkpoint that can be used for restoring the world""" 176 | self._conn.send("world.checkpoint.save") 177 | 178 | def restoreCheckpoint(self): 179 | """Restore the world state to the checkpoint""" 180 | self._conn.send("world.checkpoint.restore") 181 | 182 | def postToChat(self, msg): 183 | """Post a message to the game chat""" 184 | self._conn.send("chat.post", msg) 185 | 186 | def setting(self, setting, status): 187 | """Set a world setting (setting, status). keys: world_immutable, nametags_visible""" 188 | self._conn.send("world.setting", setting, 1 if bool(status) else 0) 189 | 190 | @staticmethod 191 | def create(address="localhost", port=4711): 192 | warnings.warn( 193 | "The `mc = Minecraft.create(address,port)` style is deprecated; " + 194 | "please use the more Pythonic `mc = Minecraft(address, port)` style " + 195 | " (or just `mc = Minecraft()` for the default address/port)", 196 | DeprecationWarning) 197 | return Minecraft(address, port) 198 | 199 | if __name__ == "__main__": 200 | mc = Minecraft() 201 | mc.postToChat("Hello, Minecraft!") 202 | -------------------------------------------------------------------------------- /mcpi/mock_server.py: -------------------------------------------------------------------------------- 1 | import socketserver 2 | import threading 3 | 4 | 5 | class ThreadedRequestHandler(socketserver.BaseRequestHandler): 6 | def handle(self): 7 | data = str(self.request.recv(1024), "ascii") 8 | cur_thread = threading.current_thread() 9 | response = bytes("{}: {}".format(cur_thread.name, data), "ascii") 10 | print(data) 11 | self.request.sendall(response) 12 | 13 | 14 | class ThreadedServer(socketserver.ThreadingMixIn, socketserver.TCPServer): 15 | pass 16 | 17 | if __name__ == "__main__": 18 | PORT = 4711 19 | server = ThreadedServer(("localhost", PORT), ThreadedRequestHandler) 20 | ip, port = server.server_address 21 | server_thread = threading.Thread(target=server.serve_forever) 22 | server_thread.daemon = False 23 | server_thread.start() 24 | print("server is now running on port {}".format(PORT)) 25 | -------------------------------------------------------------------------------- /mcpi/util.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | try: 4 | basestring 5 | except NameError: 6 | basestring = (str, bytes) 7 | 8 | 9 | def flatten(l): 10 | for e in l: 11 | if isinstance(e, collections.Iterable) and not isinstance(e, basestring): 12 | for ee in flatten(e): 13 | yield ee 14 | else: 15 | yield e 16 | 17 | 18 | def flatten_parameters_to_string(l): 19 | return ",".join(map(str, flatten(l))) 20 | -------------------------------------------------------------------------------- /mcpi/vec3.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | 4 | class Vec3: 5 | def __init__(self, x=0, y=0, z=0): 6 | self.x = x 7 | self.y = y 8 | self.z = z 9 | 10 | def __add__(self, rhs): 11 | c = self.clone() 12 | c += rhs 13 | return c 14 | 15 | def __iadd__(self, rhs): 16 | self.x += rhs.x 17 | self.y += rhs.y 18 | self.z += rhs.z 19 | return self 20 | 21 | def length(self): 22 | return self.lengthSqr() ** .5 23 | 24 | def lengthSqr(self): 25 | return self.x * self.x + self.y * self.y + self.z * self.z 26 | 27 | def __mul__(self, k): 28 | c = self.clone() 29 | c *= k 30 | return c 31 | 32 | def __imul__(self, k): 33 | self.x *= k 34 | self.y *= k 35 | self.z *= k 36 | return self 37 | 38 | def clone(self): 39 | return Vec3(self.x, self.y, self.z) 40 | 41 | def __neg__(self): 42 | return Vec3(-self.x, -self.y, -self.z) 43 | 44 | def __sub__(self, rhs): 45 | return self.__add__(-rhs) 46 | 47 | def __isub__(self, rhs): 48 | return self.__iadd__(-rhs) 49 | 50 | def __repr__(self): 51 | return 'Vec3({},{},{})'.format(self.x, self.y, self.z) 52 | 53 | def __iter__(self): 54 | return iter((self.x, self.y, self.z)) 55 | 56 | def _map(self, func): 57 | self.x = func(self.x) 58 | self.y = func(self.y) 59 | self.z = func(self.z) 60 | 61 | def __eq__(self, other): 62 | return all([self.x == other.x, self.y == other.y, self.z == other.z]) 63 | 64 | def __ne__(self, other): 65 | return not (self == other) 66 | 67 | def iround(self): 68 | self._map(lambda v: int(v + 0.5)) 69 | 70 | def ifloor(self): 71 | self._map(int) 72 | 73 | def rotateLeft(self): 74 | self.x, self.z = self.z, -self.x 75 | 76 | def rotateRight(self): 77 | self.x, self.z = -self.z, self.x 78 | 79 | def distanceTo(self, other): 80 | x_dist = other.x - self.x 81 | y_dist = other.y - self.y 82 | z_dist = other.z - self.z 83 | return math.sqrt(x_dist ** 2 + y_dist ** 2 + z_dist ** 2) 84 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = --tb=short --doctest-glob='*.rst' --pep8 3 | python_files = test_*.py 4 | norecursedirs = .* VIRTUAL build docs 5 | pep8ignore = 6 | *.py E126 E127 E128 7 | setup.py ALL 8 | */tests/* ALL 9 | */docs/* ALL 10 | pep8maxlinelength = 99 11 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import io 3 | from setuptools import setup, find_packages 4 | import sys 5 | 6 | with io.open('README.rst', mode='r', encoding='utf8') as f: 7 | readme = f.read() 8 | 9 | 10 | dependencies = [] 11 | if sys.version_info[:2] < (3, 4): 12 | dependencies.append('enum34') 13 | 14 | 15 | setup(name='py3minepi', 16 | version='0.0.1', 17 | description='A better minecraft pi library.', 18 | url='https://github.com/py3minepi/py3minepi', 19 | packages=find_packages(exclude=['*.tests', '*.tests.*', 'tests.*', 'tests']), 20 | zip_safe=True, 21 | include_package_data=True, 22 | keywords='minecraft raspberry pi mcpi py3minepi', 23 | long_description=readme, 24 | install_requires=dependencies, 25 | classifiers=[ 26 | 'Development Status :: Development Status :: 3 - Alpha', 27 | 'Environment :: X11 Applications', 28 | 'Intended Audience :: Education', 29 | 'Intended Audience :: Developers', 30 | 'License :: OSI Approved :: Other/Proprietary License', # TODO fix 31 | 'Operating System :: POSIX', 32 | 'Operating System :: POSIX :: Linux', 33 | 'Programming Language :: Python :: 3', 34 | 'Programming Language :: Python :: 3.2', 35 | 'Programming Language :: Python :: 3.3', 36 | 'Programming Language :: Python :: 3.4', 37 | ], 38 | ) 39 | -------------------------------------------------------------------------------- /tests/test_block.py: -------------------------------------------------------------------------------- 1 | """ 2 | Bare-bones but we don't even import block in any tests yet 3 | """ 4 | 5 | from mcpi import block 6 | 7 | block.Block(block.AIR) 8 | -------------------------------------------------------------------------------- /tests/test_event.py: -------------------------------------------------------------------------------- 1 | from mcpi.event import BlockEvent 2 | 3 | 4 | class TestEvent(): 5 | 6 | def test_instantiation(self): 7 | event_type = 0 8 | pos = [14, 15, 16] 9 | face = 2 10 | entity = 1 11 | event = BlockEvent(event_type, pos[0], pos[1], pos[2], face, entity) 12 | assert event.type == event_type 13 | assert event.pos.x == pos[0] 14 | assert event.pos.y == pos[1] 15 | assert event.pos.z == pos[2] 16 | assert event.face == face 17 | assert event.entityId == entity 18 | 19 | def test_representation(self): 20 | data = [0, 14, 15, 16, 1, 1] 21 | event = BlockEvent(data[0], data[1], data[2], 22 | data[3], data[4], data[5]) 23 | # block hit event has integer number converted in rep to text 24 | expected = "BlockEvent(BlockEvent.HIT, 14.00, 15.00, 16.00, 1.00, 1)" 25 | rep = repr(event) 26 | assert rep == expected 27 | 28 | def test_static_hit(self): 29 | x = 89 30 | y = -34 31 | z = 30 32 | event_type = 0 33 | face = 3 34 | entity = 1 35 | # test the variable HIT 36 | event = BlockEvent(BlockEvent.HIT, x, y, z, face, entity) 37 | assert event.type == event_type 38 | assert event.pos.x == x 39 | assert event.pos.y == y 40 | assert event.pos.z == z 41 | assert event.face == face 42 | assert event.entityId == entity 43 | 44 | # test the static function 45 | event_from_static = BlockEvent.Hit(x, y, z, face, entity) 46 | assert event_from_static.type == event.type 47 | assert event_from_static.pos.x == event.pos.x 48 | assert event_from_static.pos.y == event.pos.y 49 | assert event_from_static.pos.z == event.pos.z 50 | assert event_from_static.face == event.face 51 | assert event_from_static.entityId == event.entityId 52 | -------------------------------------------------------------------------------- /tests/test_minecraft.py: -------------------------------------------------------------------------------- 1 | from mcpi.minecraft import intFloor 2 | 3 | 4 | def test_int_floor_id(): 5 | intlist = [1, 2, 3] 6 | 7 | assert intFloor(intlist) == intlist 8 | 9 | 10 | def test_int_floor_floats(): 11 | 12 | assert type(intFloor([1.0])[0]) == int 13 | -------------------------------------------------------------------------------- /tests/test_usage.py: -------------------------------------------------------------------------------- 1 | """ 2 | API compatibility tests based on http://www.raspberrypi.org/documentation/usage/minecraft/ 3 | 4 | We do not want to break this API - we do not want to be responsible for sad children 5 | (and adults) whose awesome Minecraft code no longer works. 6 | 7 | Ergo this suite is a translation of that usage guide 8 | 9 | Currently it doesn't actually test the __success__ of any of these commands, but it at 10 | least verifies that the commands still exist, which is the most likely cause of breakage 11 | """ 12 | 13 | import pytest 14 | 15 | from mcpi import minecraft 16 | from mcpi import block 17 | from mcpi.vec3 import Vec3 18 | from time import sleep 19 | 20 | 21 | @pytest.fixture(autouse=True) 22 | def mc(monkeypatch): 23 | monkeypatch.setattr("socket.socket.connect", lambda x, y: None) 24 | monkeypatch.setattr("socket.socket.sendall", lambda x, y: None) 25 | 26 | def dummy_send(self, command): 27 | """ 28 | Log, don't send, the command 29 | """ 30 | 31 | self.last_command_sent = command 32 | 33 | monkeypatch.setattr("mcpi.connection.Connection._send", dummy_send) 34 | monkeypatch.setattr("mcpi.minecraft.CmdPositioner.getPos", lambda x, y: Vec3(0.1, 0.1, 0.1)) 35 | return minecraft.Minecraft.create() 36 | 37 | 38 | def test_hello_world(mc): 39 | mc.postToChat("Hello world") 40 | 41 | assert mc._conn.last_command_sent == "chat.post(Hello world)\n" 42 | 43 | 44 | def test_get_pos(mc): 45 | x, y, z = mc.player.getPos() 46 | 47 | 48 | def test_teleport(mc): 49 | x, y, z = mc.player.getPos() 50 | mc.player.setPos(x, y + 100, z) 51 | 52 | 53 | def test_set_block(mc): 54 | x, y, z = mc.player.getPos() 55 | mc.setBlock(x + 1, y, z, 1) 56 | 57 | assert mc._conn.last_command_sent == "world.setBlock(%d,%d,%d,%d)\n" % (x + 1, y, z, 1) 58 | 59 | 60 | def test_blocks_as_variables(mc): 61 | x, y, z = mc.player.getPos() 62 | 63 | dirt = block.DIRT.id 64 | mc.setBlock(x, y, z, dirt) 65 | 66 | 67 | def test_special_blocks(mc): 68 | x, y, z = mc.player.getPos() 69 | 70 | wool = 35 71 | mc.setBlock(x, y, z, wool, 1) 72 | 73 | 74 | def test_set_blocks(mc): 75 | stone = 1 76 | x, y, z = mc.player.getPos() 77 | mc.setBlocks(x + 1, y + 1, z + 1, x + 11, y + 11, z + 11, stone) 78 | 79 | 80 | def test_dropping_blocks_as_you_walk(mc): 81 | """ 82 | 'The following code will drop a flower behind you wherever you walk' 83 | 84 | We're not walking, and we don't want the infinite loop from the example, but this should do 85 | 86 | Note that the actual example uses xrange which is not in Python 3, so lets test with range 87 | """ 88 | 89 | flower = 38 90 | 91 | for i in range(10): 92 | x, y, z = mc.player.getPos() 93 | mc.setBlock(x, y, z, flower) 94 | sleep(0.1) 95 | -------------------------------------------------------------------------------- /tests/test_vec3.py: -------------------------------------------------------------------------------- 1 | from mcpi.vec3 import Vec3 2 | 3 | 4 | class TestVec3(): 5 | """ Test the functions of the Vec3 class """ 6 | 7 | def test_instantiation(self): 8 | expect_x = -1.0 9 | expect_y = 4.0 10 | expect_z = 6.0 11 | v = Vec3(expect_x, expect_y, expect_z) 12 | assert v.x == expect_x 13 | assert v.y == expect_y 14 | assert v.z == expect_z 15 | 16 | vector3 = Vec3(1, -2, 3) 17 | assert vector3.x == 1 18 | assert vector3.y == -2 19 | assert vector3.z == 3 20 | 21 | assert vector3.x != -1 22 | assert vector3.y != +2 23 | assert vector3.z != -3 24 | 25 | def test_representation(self): 26 | # Test repr 27 | v1 = Vec3(2, -3, 8) 28 | expected_string = "Vec3({},{},{})".format(v1.x, v1.y, v1.z) 29 | rep = repr(v1) 30 | assert rep == expected_string 31 | e = eval(repr(v1)) 32 | assert e == v1 33 | 34 | def test_iteration(self): 35 | coords = [1, 9, 6] 36 | v = Vec3(coords[0], coords[1], coords[2]) 37 | for index, pos in enumerate(v): 38 | assert pos == coords[index] 39 | 40 | def test_equality(self): 41 | v1 = Vec3(2, -3, 8) 42 | v_same = Vec3(2, -3, 8) 43 | v_diff = Vec3(22, 63, 88) 44 | v_x_larger = Vec3(5, -3, 8) 45 | v_x_smaller = Vec3(0, -3, 8) 46 | v_y_larger = Vec3(2, 9, 8) 47 | v_y_smaller = Vec3(2, -10, 8) 48 | v_z_larger = Vec3(2, -3, 12) 49 | v_z_smaller = Vec3(2, -3, 4) 50 | 51 | assert v1 == v_same 52 | assert not v1 == v_diff 53 | assert v1 != v_diff 54 | 55 | otherVectors = [v_x_larger, v_y_larger, v_z_larger, 56 | v_x_smaller, v_y_smaller, v_z_smaller] 57 | 58 | for other in otherVectors: 59 | assert v1 != other 60 | 61 | for other in otherVectors: 62 | assert not v1 == other 63 | 64 | def test_cloning(self): 65 | v = Vec3(2, -3, 8) 66 | v_clone = v.clone() 67 | assert v == v_clone 68 | v.x += 1 69 | assert v != v_clone 70 | 71 | def test_negation(self): 72 | v1 = Vec3(2, -3, 8) 73 | v_inverse = -v1 74 | assert v1.x == -v_inverse.x 75 | assert v1.y == -v_inverse.y 76 | assert v1.z == -v_inverse.z 77 | 78 | def test_addition(self): 79 | a = Vec3(10, -3, 4) 80 | b = Vec3(-7, 1, 2) 81 | c = a + b 82 | totV = Vec3(3, -2, 6) 83 | assert c == totV 84 | assert c - a == b 85 | assert c - b == a 86 | 87 | def test_subtraction(self): 88 | a = Vec3(10, -3, 4) 89 | b = Vec3(5, 3, 5) 90 | assert (a - a) == Vec3(0, 0, 0) 91 | assert (a + (-a)) == Vec3(0, 0, 0) 92 | assert (a - b) == Vec3(5, -6, -1) 93 | 94 | def test_multiplication(self): 95 | a = Vec3(2, -3, 8) 96 | assert (a + a) == (a * 2) 97 | k = 4 98 | a *= k 99 | assert a == Vec3(2 * k, -3 * k, 8 * k) 100 | 101 | def test_length(self): 102 | v = Vec3(2, -3, 8) 103 | length = v.length() 104 | expect_length = (((2 * 2) + (-3 * -3) + (8 * 8)) ** 0.5) 105 | assert length == expect_length 106 | 107 | def test_length_sqr(self): 108 | v = Vec3(2, -3, 8) 109 | ls = v.lengthSqr() 110 | assert ls == ((2 * 2) + (-3 * -3) + (8 * 8)) 111 | 112 | def test_distance_to(self): 113 | coords_one = [2, -3, 8] 114 | coords_two = [-4, 5, 12] 115 | v1 = Vec3(coords_one[0], coords_one[1], coords_one[2]) 116 | v2 = Vec3(coords_two[0], coords_two[1], coords_two[2]) 117 | expect_dist = ( 118 | ((coords_two[0] - coords_one[0]) ** 2) + 119 | ((coords_two[1] - coords_one[1]) ** 2) + 120 | ((coords_two[2] - coords_one[2]) ** 2) 121 | ) ** 0.5 122 | dist = v1.distanceTo(v2) 123 | assert dist == expect_dist 124 | 125 | def test_iround(self): 126 | v = Vec3(2.3, -3.7, 8.8) 127 | v.iround() 128 | expect_vec = Vec3(2, -3, 9) 129 | assert v == expect_vec 130 | 131 | def test_ifloor(self): 132 | v = Vec3(2.3, -3.7, 8.8) 133 | v.ifloor() 134 | expect_vec = Vec3(2, -3, 8) 135 | assert v == expect_vec 136 | 137 | def test_rotate_left(self): 138 | v = Vec3(2, -3, 8) 139 | v.rotateLeft() 140 | expect_vec = Vec3(8, -3, -2) 141 | assert v == expect_vec 142 | 143 | def test_rotate_right(self): 144 | v = Vec3(2, -3, 8) 145 | v.rotateRight() 146 | expect_vec = Vec3(-8, -3, 2) 147 | assert v == expect_vec 148 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | ############ Base configuration ############ 2 | [tox] 3 | envlist = 4 | py27,py32,py33,py34,cov,flake8 5 | 6 | [testenv] 7 | deps = 8 | pytest 9 | pytest-pep8 10 | commands = 11 | pip install -e . 12 | py.test 13 | 14 | ############ Special Cases ############ 15 | 16 | [testenv:cov] 17 | basepython=python2.7 18 | deps = 19 | {[testenv]deps} 20 | coverage>=3.6,<3.999 21 | coveralls 22 | commands = 23 | pip install -e . 24 | coverage run --source mcpi -m py.test 25 | coverage report 26 | coveralls 27 | 28 | [flake8] 29 | ignore = E126, E127, E128 30 | 31 | [testenv:flake8] 32 | basepython = python2.7 33 | deps = 34 | flake8 35 | commands = 36 | flake8 --max-line-length 100 --exclude=.tox,setup.py,build,dist 37 | --------------------------------------------------------------------------------