├── .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 |
--------------------------------------------------------------------------------