Microslop Learning Experience
Over the past several months I've been working at an online school as a course author for a course that teaches Python with Minecraft Education (ME). ME is a special version of Minecraft that aims at assisting at teaching and learning such subjects as CS, math, science, etc.
And while I can't tell you how well it works for other subjects, I can rant about how it sucks at being a platform for learning programming. I'll only talk about the Python side of things here.
Table of Contents:
General Problems
The first and probably most annoying problem is the performance. During the lessons we constantly see the problems face the game slowing to a crawl, input lagging, crashes for no apparent reason. Sometimes your code just doesn't run, and you're left wondering if you're doing something wrong, or if the game just quietly died on you.
ME comes with an in-built code editor (seems to be the same one VS Code uses). Sometimes you just can't paste text into it because you're on a platform ME doesn't like. Other times the autocomplete hints stop showing because you wrote a comment in the wrong place. Also, sometimes screen capture software like OBS just doesn't capture the editor, which makes recording videos for students more difficult.
Terrible API
One of the things ME offers over the standard Minecraft is the agent. The agent is a small robot you can program to move, place and destroy blocks, etc. It's a very simple concept, somewhat similar to turtle graphics in principle - you write the code and watch an actor execute the commands. As a concept it's fine for learning, and it's what a large part of the course uses.
What's not fine is the API you use to control the agent and other systems.
Just Bad Naming
Let's start this one with a quiz:
To get the player's position you need to use
player.position(). What do you need to use to get the player's orientation?
Correct, it's player.get_orientation()!
Ok, let's try another one:
To get the agent's orientation you need to use
agent.get_orientation(). What do you need to use to get the agent's position?
Did you apply the logic from the player and say agent.position()? That's cool, but wrong, it's actually agent.get_position()
And one more:
The agent can turn in two directions: left and right. How many functions do you need to control its turning?
Did you say two? Don't be silly, obviously it's four:
agent.turn_left()agent.turn_right()agent.turn(LEFT)agent.turn(RIGHT)
Why? ┐(´ー`)┌
On the topic of function names, here's a couple more.
How do you get a random integer? - With randint(min, max), pretty logical so far, "int" stands for "integer" after all.
How do you get a random floating point number? Also
with randint(min, max). How do you know what you
get? You read the hint that says "If both numbers
are integral, the result is integral." So
randint(1, 10) -> int
randint(1, 10.0) -> int
randint(1, 10.1) -> float
Also, what if you need the code to pause? You use the appropriate pause(ms) function. What namespace is it in? Obviously in the loops.
By the way, to change the weather and enable rain you use gameplay.toggle_downfall(), not toggle_rain which would've been much clearer for the target audience.
Dumb and Inconsistent
Another thing you often need is to teleport the agent to your position. Say, you're at the start of a labyrinth, and you need to write a program for the agent to solve it. So you put
agent.teleport_to_player()
at the start of your code. It teleports to you and goes off in a random direction where there's definitely no labyrinth ahead.
That's because the agent preserves its last orientation, and does not turn to face where the player is facing. To simply teleport with turning, you have to instead write:
agent.teleport(
player.position(),
positions.to_compass_direction(player.get_orientation())
)
Such a basic action shouldn't be so hard to achieve, given how frequently you need it.
The API also has a lot of enums: for blocks, mobs, directions, and so on. Almost all of them also have aliases for ease of use. For example:
Block.STONEbe written as justSTONEAnimalMob.COW->COWSixDirection.FORWARD->FORWARD
So you quickly get used to writing code without enums, and have no incentive to learn them. But then you come to a simple function like agent.detect() which detects if a block exists adjacent to the agent in a certain direction. You write
agent.detect(BLOCK, FORWARD)
and get the "name 'BLOCK' is not defined" error, because for some reason they decided not to give AgentDetection.BLOCK an alias.
Another thing you can do is create chat commands with arguments and bind functions to them. E.g. you write "wall 10 20" in chat, and the agent builds a 10x20 wall.
It looks like this:
def build_wall(x, y):
# wall building code
# that uses x and y
# bind build_wall to the "wall" chat command.
# parameters are passed automatically
player.on_chat("wall", build_wall)
The system has one limitation - the arguments have to be numbers, strings are not supported and get replaced with 0. So this code will print "hello, 0" if you try to invoke it with, say, "greet Bob":
def greet(name):
# player.say just prints to chat
player.say("hello, " + name)
player.on_chat("greet", greet)
However, if you look further, you can also find
player.get_chat_arg(index)
which also extracts arguments. And if you write
def greet(name):
n = player.get_chat_arg(0)
player.say("hello, " + n)
player.on_chat("greet", greet)
"hello, Bob" magically appears on the screen. So the system does support strings, just through an almost undocumented function.
Speaking of something that's just stupid, how about Minecraft-based educational tool messing up the Minecraft blocks?
Some blocks that've been in the game for ages are just not accessible from the code. For example, you may want to use "Chiselled Stone Bricks", but if you tell the agent to place CHISELED_STONE_BRICKS (which btw is spelled with only one L compared to the name in the player inventory), it'll place "Stone Bricks".
If, on the other hand, you tell it to place STONE_BRICKS, it'll also place "Stone Bricks", so "Chiselled Stone Bricks" is either just inaccessible from the code, or hidden behind some other name.
Even funnier is that if you tell the agent to identify the blocks, it returns "Unknown Block" for both "Chiselled Stone Bricks" and "Stone Bricks".
At the same time there are just typos left in the code. There's an option to enable AgentAssist.DESTROY_OBSTACLES. But there's also an option AgentAssist.DETROY_OBSTACLES... They behave the same, one's just missing a letter for some reason.
Not Quite Python
ME doesn't actually run Python scripts directly. Since the code part of ME is actually an embedded MakeCode platform, it functions in the way all MakeCode platforms do. They first translate the code to "Static TypeScript", and only then compile and run it.
Actually, it's not even Python, it's "Static Python" which is a version of Python so dumbed down it's sometimes unusable. And unfortunately, in the game itself it never mentions this.
I learned about this when making a lesson about lists. I wrote this simple piece of code:
nums = []
def add_number():
nums.append(randint(1, 10))
player.on_chat("add", add_number)
The first suspicions arose when, after typing in nums., the autocomplete suggested options like length, push, shift.
Then there was the error message
Variable 'nums' implicitly has an 'any[]' type.
If you've ever written Python, it may look a bit strange to you. And justly so, given that it's not even a Python error, but a TypeScript one, that somehow leaked into the Python editor.
All this was surprising since lists had worked fine previously. After some digging I found that they did indeed work, just not always. For example, you could fix the example above by literally adding one character in the first line
nums = [] -> nums = [1]
Now it can deduce that the list contains numbers, so the TypeScript code does not break. So empty lists are broken, but only in some cases, and non-empty lists work, but only sometimes.
And yes, the lists that accept any types in Python (e.g. you can have a list like [1, "hi", True]) can only have one type here:
nums = [1]
nums.append("s")
^^^
Argument of type '"s"' is not assignable
to parameter of type 'number'.
By the way, to actually create a usable empty list you have to write
nums: List[number] = []
Yes, it's presented as vanilla Python in the game, and does not require any imports. You definitely can still learn programming with such a language, but such hidden limitations lead to confusion when learners step outside the "Static Python" to continue learning "Normal Python".
(¬_¬")