05 Saving the World

SensorCraft makes it fun to build objects in your world but we currently have no way to save them for use in other worlds. For this exercise, we are going to add some new textures and create a new method that will save the world so we can reload it in the future. The blocks we are going to add will be state of the art United States Air Force (USAF) created blocks that are made out of composite material. Composites are now used in aircraft like the F22 Stealth Fighter and commercial aircraft. To get started with this programming exercise, first copy the file 03_show_current_block_TVR.py python code to a new file 05_saving_the_world_TVR.py but replace TVR with your initials using the following command:

cp 03_show_current_block_TVR.py 05_saving_the_world_TVR.py

First, delete lines 77 - 87 as we no longer need the numbered stone blocks. Next we need to change the file name used for textures on line 76 change the line to TEXTURE_PATH = 'composite_textures.png'. At line 82 you want to add the new composite blocks so after all the changes above line 76 - 89 should look like the following chunk of code:

TEXTURE_PATH = 'composite_textures.png'

GRASS = tex_coords((1, 0), (0, 1), (0, 0))
SAND = tex_coords((1, 1), (1, 1), (1, 1))
BRICK = tex_coords((2, 0), (2, 0), (2, 0))
STONE = tex_coords((2, 1), (2, 1), (2, 1))
COMPOSITE_RED = tex_coords((3, 1), (3, 1), (3, 1))
COMPOSITE_BLUE = tex_coords((3, 0), (3, 0), (3, 0))
COMPOSITE_BLACK = tex_coords((0, 2), (0, 2), (0, 2))
COMPOSITE_GREY = tex_coords((1, 2), (1, 2), (1, 2))
COMPOSITE_GREEN = tex_coords((2, 2), (2, 2), (2, 2))

COMPOSITE = [COMPOSITE_RED, COMPOSITE_BLUE, COMPOSITE_BLACK,
COMPOSITE_GREY, COMPOSITE_GREEN]

Next, we need to remove the code that placed the numbered stone blocks to form the number lines on the axis in exercise 3. Your _initialize method should look like the following after all the code has been deleted:

    def _initialize(self):
        """ Initialize the world by placing all the blocks.

        """
        n = 80  # 1/2 width and height of world
        s = 1  # step size
        y = 0  # initial y height
        for x in xrange(-n, n + 1, s):
            for z in xrange(-n, n + 1, s):
                # create a layer stone an grass everywhere.
                self.add_block((x, y - 2, z), GRASS, immediate=False)
                self.add_block((x, y - 3, z), STONE, immediate=False)
                if x in (-n, n) or z in (-n, n):
                    # create outer walls.
                    for dy in xrange(-2, 3):
                        self.add_block((x, y + dy, z), STONE, immediate=False)

We now have to add the new composite blocks to our character’s inventory. To do so, modify line 490 so it looks like the following:

        # A list of blocks the player can place. Hit num keys to cycle.
        self.inventory = [BRICK, GRASS, SAND, COMPOSITE_RED, COMPOSITE_BLUE, COMPOSITE_BLACK, COMPOSITE_GREY, COMPOSITE_GREEN]

Recall back to the exercise 03_show_current_block and consider what modifications need to be made to the draw_label method. Within the if/else block, you need to make adjustments for the new composite blocks that we added your new draw_label method should be something like:

    def draw_label(self):
        """ Draw the label in the top left of the screen. Label includes
            the current block selection.

        """
        # determines the current block selected
        if self.block == BRICK:
            blockSelectedString = "Brick"
        elif self.block == GRASS:
            blockSelectedString = "Grass"
        elif self.block == SAND:
            blockSelectedString = "Sand"
        elif self.block == COMPOSITE_RED:
            blockSelectedString = "Composite Red"
        elif self.block == COMPOSITE_BLUE:
            blockSelectedString = "Composite Blue"
        elif self.block == COMPOSITE_BLACK:
            blockSelectedString = "Composite Black"
        elif self.block == COMPOSITE_GREY:
            blockSelectedString = "Composite Grey"
        elif self.block == COMPOSITE_GREEN:
            blockSelectedString = "Composite Green"

        x, y, z = self.position
        self.label.text = '%02d (%.2f, %.2f, %.2f) %d / %d Block: %s' % (
            pyglet.clock.get_fps(), x, y, z,
            len(self.model._shown), len(self.model.world),
            blockSelectedString)
        self.label.draw()

How do we tell the game that it can save the world? We could spend time and write a complicated menu system that lets a user enter a file name, but since we are just starting out lets deploy a simple solution. One simple solution is to simply press a key, in this case the O key and the method save_txt will be called from the model class. Below is what the new on_key_press method will look like, take note of the last elif:

    def on_key_press(self, symbol, modifiers):
        """ Called when the player presses a key. See pyglet docs for key
        mappings.

        Parameters
        ----------
        symbol : int
            Number representing the key that was pressed.
        modifiers : int
            Number representing any modifying keys that were pressed.

        """
        if symbol == key.W:
            self.strafe[0] -= 1
        elif symbol == key.S:
            self.strafe[0] += 1
        elif symbol == key.A:
            self.strafe[1] -= 1
        elif symbol == key.D:
            self.strafe[1] += 1
        elif symbol == key.SPACE:
            if self.dy == 0:
                self.dy = JUMP_SPEED
        elif symbol == key.ESCAPE:
            self.set_exclusive_mouse(False)
        elif symbol == key.TAB:
            self.flying = not self.flying
        elif symbol == key.Y:
            self.position = (0, 0, 0)
            dx, dy, dz = self.get_motion_vector()
            self.dy = 0
        elif symbol in self.num_keys:
            index = (symbol - self.num_keys[0]) % len(self.inventory)
            self.block = self.inventory[index]
        elif symbol == key.B:
            self.buildWall()
        elif symbol == key.O:
            self.model.save_txt()

Finally, we implement the method that will save our world, or parts of the world we care about. First a little background: Python has this incredibly powerful data structure called a dictionary. Dictionaries allow a programmer to associate a key to any given value. In our case the key is X, Y, Z position of the block and the value is the type of block like COMPOSITE_RED, COMPOSITE_BLUE, COMPOSITE_BLACK, SAND, GRASS, BRICK, etc. You can learn more about dictionaries in Python by reading the data structure dictionary documentation page.

Go to line 88 where you defined textured blocks and add another line as below:

COMPOSITE = [COMPOSITE_RED, COMPOSITE_BLUE, COMPOSITE_BLACK,
COMPOSITE_GREY, COMPOSITE_GREEN]

The code should look like below:

TEXTURE_PATH = 'composite_textures.png'

GRASS = tex_coords((1, 0), (0, 1), (0, 0))
SAND = tex_coords((1, 1), (1, 1), (1, 1))
BRICK = tex_coords((2, 0), (2, 0), (2, 0))
STONE = tex_coords((2, 1), (2, 1), (2, 1))
COMPOSITE_RED = tex_coords((3, 1), (3, 1), (3, 1))
COMPOSITE_BLUE = tex_coords((3, 0), (3, 0), (3, 0))
COMPOSITE_BLACK = tex_coords((0, 2), (0, 2), (0, 2))
COMPOSITE_GREY = tex_coords((1, 2), (1, 2), (1, 2))
COMPOSITE_GREEN = tex_coords((2, 2), (2, 2), (2, 2))

COMPOSITE = [COMPOSITE_RED, COMPOSITE_BLUE, COMPOSITE_BLACK,
COMPOSITE_GREY, COMPOSITE_GREEN]

This creates a list that has all the types of composite blocks. These are the only types of blocks we are interested in saving, you could easily add more blocks to the list as is required for your game.

We want the file we save to to be as small as possible so we can share our saved worlds with friends. Let’s think about the necessary information needed to define a block. Every block has a location in the world in the form x, y, z and a texture that defines how it looks. We have an indexed list defining the textures so each composite block type has a corresponding number. As we defined it, we can say COMPOSITE[0] corresponds to COMPOSITE_RED, COMPOSITE[1] is COMPOSITE_BLUE, and so on through the end of the list. Hence we can store each block as four numbers: x, y, and z coordinates and the number corresponding to the index in the list that has the block’s texture.

Let’s use this logic to write a new function that translates between the literal texture type and the numerical equivalence in the COMPOSITE list. At the end of the model class copy and paste the below function.

    def get_block_index(self, num="", type=""):
        """
        If given a number (num), returns composite block at that index.
        If given a composite block type (type), returns the index of that type.
        """
        if not isinstance(num, str):
            return COMPOSITE[int(num)]
        elif not isinstance(type, str):
            for i in xrange(0, len(COMPOSITE)):
                if type == COMPOSITE[i]:
                    return i
        print("Invalid Call")

This function behaves slightly differently than previous methods we’ve implemented. In the first line we have two parameters num="", type="", but notice the parameters are set equal to default values. So if you make a call to the function as just model.get_block_index(), these variables will default to "", n empty string. The function isinstance is a way of checking the type of object. So now if we walk through the function, we can see that it checks if num is an integer and if it is, returns the texture at that index in the COMPOSITE list. If instead the type argument is provided (the specific texture), the function will return the corresponding integer index.

Now let’s add the function that actually writes to an external file to save our blocks. Just above get_block_index, add the following.

    def save_txt(self):
        """
        Save composite blocks to a .txt file
        """
        file_name = 'composite_world.txt'
        with open(file_name, 'w') as output:
            for pos, type in self.world.items():
                if type in COMPOSITE:
                    output.write(str(pos[0]) + " " + str(pos[1]) + " " + str(pos[2])
                                 + " " + str(self.get_block_index(type=type)) + "\n")
        print("world is saved to file name: %s" % file_name)

This function finds all the composite blocks in the world and writes the position and texture type as an integer to a file called composite_world.txt. If you open this file, you will see lines with four numbers on them, the first three corresponding to a block’s position and the last the index of the block’s texture. Run the program with IDLE then place a few red composite bricks, finally push the O key (not the zero key but the O key) to save the composite blocks to a file called composite_world.txt so we can use the file in the next chapter to load a saved world. You can now share these save files with friends via email attachment.