The 5 Simple Rules to the Game of Code

An experimental idea to teach the basics of a Turing machine before teaching loops and branching. Feedback welcome.

By Zed A. Shaw

The 5 Simple Rules to the Game of Code

If you play a game like Go or Chess you know the rules are fairly simple, yet the games they enable are extremely complex. Really good games have this unique quality of simple rules with complex interactions. Programming is also a game with a few simple rules that create complex interactions, and in this exercise we're going to learn what those rules are.

Before we do that, I need to stress that you most likely won't use these rules directly when you code. There are languages that do utilize these rules directly, and your CPU uses them too, but in daily programming you'll rarely use them. If that's the case then why learn the rules?

Because these rules are everywhere, and understanding them will help you understand the code you write. It'll help you debug the code when it goes wrong. If you ever want to know how the code works you'll be able to "disassemble" it down to its basic rules and really see how it works. These rules are a cheat code. Pun totally intended.

I'm also going to warn you that you are not expected to totally understand this right away. Think of this exercise as setting you up for the rest of the exercises in this module. You're expected to study this exercise deeply, and when you get stuck, move on to the next exercises as a break. You want to bounce between this one and the next ones until the concepts "click" and it starts to make sense. You should also study these rules as deeply as you can, but don't get stuck here. Struggle for a few days, move on, come back, and keep trying. As long as you keep trying you can't actually "fail".

Rule 1: Everything is a Sequence of Instructions

All programs are a sequence of instructions which tell a computer to do something. You've seen Python doing this already when you type code like this:

x = 10
y = 20
z = x + y

This code starts at line 1, goes to line 2, and so on until the end. That's a sequence of instructions, but inside Python these 3 lines are converted into another sequence of instructions that look like this:

LOAD_CONST   0 (10) # load the number 10
STORE_NAME   0 (x)  # store that in x

LOAD_CONST   1 (20) # load the number 20
STORE_NAME   1 (y)  # store that in y

LOAD_NAME    0 (x)  # loads x (which is 10)
LOAD_NAME    1 (y)  # loads y (which is 20)
BINARY_ADD          # adds those
STORE_NAME   2 (z)  # store the result in z

That looks totally different from the Python version, but I bet you could probably figure out what this sequence of instructions is doing. I've added comments to explain each instruction, and you should be able to connect it back to the Python code above.

I'm not joking. Take some time right now to connect each line of the Python code to the lines of this "byte code". Using the comments I provided I'm positive you can figure it out, and doing so might turn on a light in your head about the Python code.

It's not necessary to memorize this or even understand each of these instructions. What you should realize is your Python code is being translated into a sequence of simpler instructions that tell the computer to do something. This sequence of instructions is called a "byte code" because it's usually stored in a file as a sequence of numbers a computer understands. The output you see above is usually called an "assembly language" because it's a human "readable" (barely) version of those bytes.

These simpler instructions are processed starting at the top, do one small thing at a time, and go to the end when the program exits. That's just like your Python code but with a simpler syntax of INSTRUCTION OPTIONS. Another way to look at this is each part of x = 10 might become its own instructions in this "byte code."

That's the first rule of The Game of Code: Everything you write eventually becomes a sequence of bytes fed to a computer as instructions for what the computer should do.

How can I get this output?

To get this output yourself, you use a module called dis which stands for "disassemble." This kind of code is traditionally called "byte code" or "assembly language", so dis means to "dis-assemble." To use dis you can import it and use the dis() function like this:

from dis import dis
dis('''
x = 10
y = 20
z = x + y
''')

In this Python code I'm doing the following:

  1. I import the dis() function from the dis module.
  2. I run the dis() function, but I give it a multi-line string using '''.
  3. I then write the Python code I want to disassemble into this multi-line string.
  4. Finally, I end the multi-line string and the dis() function with ''').

When you run this in Jupyter you'll see it dump the byte code like I have above, but maybe with some extras we'll cover in a minute.

Where are these bytes stored?

When you run Python (version 3) these bytes are stored in a directory named __pycache__. If you put this code into a ex19.py file and then run it with python ex19.py you should see this directory.

Looking in this directory you should see a bunch of files ending in .pyc with names similar to the code that generated them. These .pyc files contain your compiled Python code as bytes.

When you run dis() you're printing a human readable version of the numbers in the .pyc file.

Rule 2: Jumps Make the Sequence Non-Linear

A sequence of simple instructions like LOAD_CONST 10 is not very useful. Yay! You can load the number 10! Amazing! Where code starts to become useful is when you add the concept of the "jump" to make this sequence non-linear. Let's look at a new piece of Python code:

while True:
    x = 10

To understand this code we have to foreshadow a later exercise where you learn about the while-loop. The code while True: simply says "Keep running the code under me x = 10 while True is True." Since True will always be True this will loop forever. If you run this in Jupyter it will never end.

What happens when you dis() this code? You see the new instruction JUMP_ABSOLUTE:

dis("while True: x = 10")

     0 LOAD_CONST               1 (10)
     2 STORE_NAME               0 (x)
     4 JUMP_ABSOLUTE            0 (to 0)

You saw the first two instructions when we covered the x = 10 code, but now at the end we have JUMP_ABSOLUTE 0. Notice there's numbers 0, 2, and 4 to the left of these instructions? In the previous code I cut them out so you wouldn't be distracted, but here they're important because they represent locations in the sequence where each instruction lives. All JUMP_ABSOLUTE 0 does is tell Python to "jump to the instruction at position 0" which is LOAD_CONST 1 (10).

With this simple instruction we now have turned boring straight line code into a more complex loop that's not straight anymore. Later we'll see how jumps combine with tests to allow even more complex movements through the sequence of bytes.

Why is this backwards?

You may have noticed that the Python code reads as "while True is True set x equal to 10" but the dis() output reads more like "set x equal to 10, jump to do it again." That's because of Rule #1 which says we have to produce a sequence of bytes only. There is no nested structures, or any syntax more complex than INSTRUCTION OPTIONS allowed.

To follow this rule Python has to figure out how to translate its code into a sequence of bytes that produce the desired output. That means, moving the actual repetition part to the end of the sequence so it will be in a sequence. You'll find this "backwards" nature comes up often when looking at byte codes and assembly language.

Can a JUMP go forward?

Yes, technically a JUMP instruction is simply telling the computer to process a different instruction in the sequence. It can be the next one, a previous one, or one in the future. The way this works is the computer keeps track of the "index" of the current instruction, and it simply increments that index.

When you JUMP you're telling the computer to change this index to a new location in the code. In the code for our while loop (below) the JUMP_ABSOLUTE is at index 4 (see the 4 to the left). After it runs, the index changes to 0 where the LOAD_CONST is located, so the computer runs that instruction again. This loops forever.

     0 LOAD_CONST               1 (10)
     2 STORE_NAME               0 (x)
     4 JUMP_ABSOLUTE            0 (to 0)

Rule 3: Tests Control Jumps

A JUMP is useful for looping, but what about making decisions? A common thing in programming is to ask questions like:

"If x is greater than 0 then set y to 10."

If we write this out in simple Python code it might look like this:

if x > 0:
    y = 10

Once again, this is foreshadowing something you'll learn later, but this is simple enough to figure out:

  1. Python will test if x is greater than > 0.
  2. If it is then Python will run the line y = 10.
  3. You see how that line is indented under the if x > 0:? That is called a "block" and Python uses indentation to say "this indented code is part of the code above it."
  4. If x is NOT greater than 0 then Python will JUMP over the y = 10 line to skip it.

To do this with our Python byte code we need a new instruction that implements the testing part. We have the JUMP. We have variables. We just need a way to compare two things and then a JUMP based on that comparison.

Let's take that code and dis() it to see how Python does this:

dis('''
x = 1
if x > 0:
    y = 10
''')

   0 LOAD_CONST          0 (1)     # load 1
   2 STORE_NAME          0 (x)     # x = 1

   4 LOAD_NAME           0 (x)     # load x
   6 LOAD_CONST          1 (0)     # load 0
   8 COMPARE_OP          4 (>)     # compare x > 0
  10 POP_JUMP_IF_FALSE  10 (to 20) # jump if false

  12 LOAD_CONST          2 (10)    # not false, load 10
  14 STORE_NAME          1 (y)     # y = 10
  16 LOAD_CONST          3 (None)  # done, load None
  18 RETURN_VALUE                  # exit

  # jump here if false
  20 LOAD_CONST          3 (None)  # load none
  22 RETURN_VALUE                  # exit

The key part of this code is the COMPARE_OP and POP_JUMP_IF_FALSE:

   4 LOAD_NAME           0 (x)     # load x
   6 LOAD_CONST          1 (0)     # load 0
   8 COMPARE_OP          4 (>)     # compare x > 0
  10 POP_JUMP_IF_FALSE  10 (to 20) # jump if false

Here's what this code does:

  1. Use LOAD_NAME to load the x variable.
  2. Use LOAD_CONST to load the 0 constant.
  3. Use COMPARE_OP which does the > comparison and leaves a True or False result for later.
  4. Finally, POP_JUMP_IF_FALSE makes the if x > 0 work. It "pops" the True or False value to get it, and if it reads False it will JUMP to instruction 20.
  5. Doing that will jump over the code that set y if the comparison is False, but if the comparison is True then Python just runs the next instruction which starts the y = 10 sequence.

Take some time walking through this to try to understand it. If you have a printer, try printing it out and set x to different values manually, then trace through how the code works. What happens when you set x = -1.

What do you mean "pop"?

In the above code I'm skipping over exactly how Python "pops" the value to read it, but it's storing it in something called a "stack." For now just think of it as a temporary storage place that you "push" values into, and then "pop" them off. You really don't need to go much deeper than that at this stage in your learning. Just understand the effect is to get the result of the last instruction.

Wait, aren't tests like COMPARE_OP used in loops too?

Yes, and you could probably figure out how that works right now based on what you know. Try to write a while-loop and see if you can get it to work with what you know now. Don't worry if you can't though as we'll be covering this in later exercises.

Rule 4: Storage Controls Tests

You need some way to keep track of changing data while the code operates, and this is done with "storage." Usually this storage is in the computer's memory and you create names for the data you're storing in memory. You've been doing this when you write code like this:

x = 10
y = 20
z = x + y

In each of the above lines we're making a new piece of data and storing it in memory. We're also giving these pieces of memory the names x, y, and z. We can then use these names to "recall" those values from memory, which is what we do in z = x + y. We're just recalling the value of x and y from memory to add them together.

That's the majority of the story, but the important part of this little rule is that you almost always use memory to control tests.

Sure, you can write code like this:

if 1 < 2:
    print("but...why?")

That's pointless though since it's just running the second line after a pointless test. 1 is always less than 2 so it's useless.

Where tests like COMPARE_OP shine is when you use variables to make the tests dynamic based on calculations. That's why I consider this a "rule of the Game of Code" because code without variables isn't really playing the game.

Take the time to go back through the previous examples and identify the places where LOAD instructions are used to load values, and STORE instructions are used to store values into memory.

Rule 5: Input/Output Controls Storage

The final rule of the Game of Code is how your code interacts with the outside world. Having variables is great, but a program that only has data you've typed into the source file isn't very useful. What you need is input and output.

Input is how you get data into your code from things like files, the keyboard, or the network. You've already used open() and input() to do that in the last module. You accessed input every time you opened a file, read the contents, and did something with them. You also used input when you've use...input() to ask the user a question.

Output is how you save or transmit the results of your program. Output can be to the screen with print(), to a file with file.write(), or even over a network.

The only problem with input and output at this point is the byte code output is a little more complicated. Let's look at a simple one:

from dis import dis
dis("input('Yes? ')")

  0 LOAD_NAME                0 (input)
  2 LOAD_CONST               0 ('Yes? ')
  4 CALL_FUNCTION            1
  6 RETURN_VALUE

This dis() run doesn't help much because now we're getting into an advanced topic we'll cover later called "functions", so let's stop there and pull these rules together.

Putting it All Together

Taking the 5 Rules we have the following Game of Code:

  1. You read data as input to your program (Rule #5).
  2. You store this data in storage (variables) (Rule #4).
  3. You use these variables to perform tests... (Rule #3).
  4. ...so you can JUMP around... (Rule #2)
  5. ...the sequence of instructions... (Rule #1)
  6. ...transforming the data to new variables (Rule #4)...
  7. ...which you then write to output for storage or display. (Rule #5).

While this seems simple these little rules create very complicated software. Video games are a great example of very complicated software that does this. A video game reads your controller or keyboard as input, updates variables that control the models in the scene, and uses advanced instructions that render the scene to your screen as output.

Your next step is to study the Python that uses all of these rules.

The exercises after this will then reference the concepts here to explain the concepts of while-loops, if-statements, boolean logic, and the more advanced applications of these rules. By learning the rules it's hopefully going to make it easier to understand how each thing works, but you tell me. Did this make sense?


More from Learn Code the Hard Way

How Long Does it Take to Learn to Code?

My standard answer when people ask me how long it will take for them to become competent enough to get a job.

OpinionPublished Jun 1, 2024

Exploring the Replacement for C as an Educational Language

My thoughts so far on finding a replacement for C in teaching compiled languages and memory safety.

ResearchPublished Apr 20, 2024

How to Read Programmer Documentation

An excerpt from Learn Python the Hard Way, 5th Edition that explains how I analyze learn from projects with poor or no documentation (which is most of them).

PythonPublished July 29, 2023

The 5 Simple Rules to the Game of Code

An experimental idea to teach the basics of a Turing machine before teaching loops and branching. Feedback welcome.

PythonPublished July 29, 2023