15 Circuits¶
In this chapter we are going to add some circuit like objects to the game. Before we start talking about the code, let’s look at the idea behind how this system will work.
We’re going to have three types of new blocks: head, tail, and cable. The tail block will always follow the head block and the head block will move to any adjacent cable blocks. Be careful though, if you have too many head blocks next to a cable block it will die out.
Track New Block Types - Part 1¶
First copy the third tutorial “Show Current Block” to a new file with your initials at the end:
cp 03_show_current_block.py 15_circuits_TVR.py
We need to reference a new texture file with the circuit textures. Go to the
line reading TEXTURE_PATH = 'numbered_textures.png'
and change it to read
TEXTURE_PATH = 'story_textures.png'
. Also go to the line reading
def tex_coord(x, y, n=4):
and change it to say n=8
. This is because the
new texture file is larger and has more textures in it, so we need to tell the
program how many pieces to break the file into.
Now we need to add the three new block types. The head blocks will be of type
ELECH
, the tail blocks will be ELECT
, and the cable blocks will simply
be called CABLE
. Delete the numbered stones defined in NUMSTONE
from
the third tutorial. Delete the below section of code (around line 80).
NUMSTONE = []
NUMSTONE.append(tex_coords((3, 0), (3, 0), (3, 0)))
NUMSTONE.append(tex_coords((3, 1), (3, 1), (3, 1)))
NUMSTONE.append(tex_coords((0, 2), (0, 2), (0, 2)))
NUMSTONE.append(tex_coords((1, 2), (1, 2), (1, 2)))
NUMSTONE.append(tex_coords((2, 2), (2, 2), (2, 2)))
NUMSTONE.append(tex_coords((3, 2), (3, 2), (3, 2)))
NUMSTONE.append(tex_coords((0, 3), (0, 3), (0, 3)))
NUMSTONE.append(tex_coords((1, 3), (1, 3), (1, 3)))
NUMSTONE.append(tex_coords((2, 3), (2, 3), (2, 3)))
NUMSTONE.append(tex_coords((3, 3), (3, 3), (3, 3)))
And then add the below code.
CABLE = tex_coords((1, 5), (1, 5), (1, 5))
ELECH = tex_coords((2, 5), (2, 5), (2, 5))
ELECT = tex_coords((0, 5), (0, 5), (0, 5))
After all the above changes are made the code should read as follows from line 52 to line 85
def tex_coord(x, y, n=8):
""" Return the bounding vertices of the texture square.
"""
m = 1.0 / n
dx = x * m
dy = y * m
return dx, dy, dx + m, dy, dx + m, dy + m, dx, dy + m
def tex_coords(top, bottom, side):
""" Return a list of the texture squares for the top, bottom and side.
"""
top = tex_coord(*top)
bottom = tex_coord(*bottom)
side = tex_coord(*side)
result = []
result.extend(top)
result.extend(bottom)
result.extend(side * 4)
return result
TEXTURE_PATH = 'story_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))
CABLE = tex_coords((1, 5), (1, 5), (1, 5))
ELECH = tex_coords((2, 5), (2, 5), (2, 5))
ELECT = tex_coords((0, 5), (0, 5), (0, 5))
Now go down to the Model class __init__
function and delete
the below section.
for i in range(0, 10):
# x axis
self.add_block((i, -2, 0), NUMSTONE[i], immediate=False)
self.add_block((-i, -2, 0), NUMSTONE[i], immediate=False)
# y axis
self.add_block((0, -2 + i, 0), NUMSTONE[i], immediate=False)
self.add_block((0, -2 + -i, 0), NUMSTONE[i], immediate=False)
# z axis
self.add_block((0, -2, i), NUMSTONE[i], immediate=False)
self.add_block((0, -2, -i), NUMSTONE[i], immediate=False)
This removes the numbered axes from the world.
Circuit blocks are going to be special interactive blocks, so let’s track them
separately. We usually track blocks with the world
dictionary which maps
(x, y, z) positions of blocks to the type/texture of that block. Let’s add
another dictionary called circuit
that acts just like world
, but only
stores circuit blocks.
Just below where self.world
is declared, add a definition for
self.circuit
. This section should read as below:
def __init__(self):
# A Batch is a collection of vertex lists for batched rendering.
self.batch = pyglet.graphics.Batch()
# A TextureGroup manages an OpenGL texture.
self.group = TextureGroup(image.load(TEXTURE_PATH).get_texture())
# A mapping from position to the texture of the block at that position.
# This defines all the blocks that are currently in the world.
self.world = {}
self.circuit = {}
# Same mapping as `world` but only contains blocks that are shown.
self.shown = {}
# Mapping from position to a pyglet `VertextList` for all shown blocks.
self._shown = {}
# Mapping from sector to a list of positions inside that sector.
self.sectors = {}
# Simple function queue implementation. The queue is populated with
# _show_block() and _hide_block() calls
self.queue = deque()
self._initialize()
We want to make sure blocks are added to the circuit dictionary only when they
are circuit related. There are two functions used to add and remove blocks:
add_block
and remove_block
. We want to add a section to add_block
that says “if the block is a circuit type, add it to self.circuit
”.
Similarly we then need to add a statement in remove_block
that says “if the
block is of a circuit type, remove it from self.circuit
”. Change these two
functions in the Model
class to read as below.
def add_block(self, position, texture, immediate=True):
""" Add a block with the given `texture` and `position` to the world.
Parameters
----------
position : tuple of len 3
The (x, y, z) position of the block to add.
texture : list of len 3
The coordinates of the texture squares. Use `tex_coords()` to
generate.
immediate : bool
Whether or not to draw the block immediately.
"""
if position in self.world:
self.remove_block(position, immediate)
self.world[position] = texture
self.sectors.setdefault(sectorize(position), []).append(position)
if immediate:
if self.exposed(position):
self.show_block(position)
self.check_neighbors(position)
if texture in [ELECH, CABLE, ELECT]:
self.circuit[position] = texture
def remove_block(self, position, immediate=True):
""" Remove the block at the given `position`.
Parameters
----------
position : tuple of len 3
The (x, y, z) position of the block to remove.
immediate : bool
Whether or not to immediately remove block from canvas.
"""
del self.world[position]
self.sectors[sectorize(position)].remove(position)
if immediate:
if position in self.shown:
self.hide_block(position)
self.check_neighbors(position)
if position in self.circuit:
del self.circuit[position]
Recall how you can press a number key to select which type of block you want to
build. We want to be able to do this with circuit blocks. Go to the line
reading self.inventory = [BRICK, GRASS, SAND]
(around line number 464) and
add the three new types of blocks to the list on the right side of the equal sign.
It should then say self.inventory = [BRICK, GRASS, SAND, CABLE, ELECH, ELECT]
.
Next go to draw_label
. We will need to specify what to display when these
new types of blocks are selected. We do this by defining the value of
blockSelectedString
. Change this function to read as follows.
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 == ELECH):
blockSelectedString = "ELECH"
elif (self.block == ELECT):
blockSelectedString = "ELECT"
elif (self.block == CABLE):
blockSelectedString = "Cable"
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()
Execute the code and now you will be able to add these new types of blocks and see what type of block you have selected at the top of the screen.
Run the Circuits - Part 2¶
In this section we will add the code to make the circuits work. As is our customer first copy the circuit tutorial part 1 to a new file with your initials at the end:
cp 15_circuits_part_1.py 15_circuits_part_2_TVR.py
We are going to break this into two parts: identifying what changes need to be made at each iteration to move the circuit by one block and then actually making those changes.
If a cable block is next to two or fewer head blocks, it needs to become a head block. All head blocks will become tail blocks and all tail blocks will become cable. Basically we know what happens to head and tail blocks, but we need to search around head blocks. Let’s create a function that returns the position of all surrounding blocks.
Just after the function hit_test
in the Model
class add the following:
def neighbor(self, position):
x, y, z = position
local = set()
for dx in [-1, 0, 1]:
for dy in [-1, 0, 1]:
for dz in [-1, 0, 1]:
key = (x + dx, y + dy, z + dz)
local.add(key)
return(local)
Basically this function adds and subtracts one from each coordinate and returns the position of the specified block and all the surrounding blocks as a list.
Below this, add another function called circuit_change
. This function will
actually update the circuit and make it move.
def circuit_change(self):
count_h = {}
# track what contained blocks should change to
to_cable = set()
to_elect = set()
to_elech = set()
elech_safe = set()
# iterate through all circuit blocks
for position in self.circuit:
# if haven't checked this block before
# initialize count to zero
if position not in count_h:
count_h[position] = 0
# all tail blocks will become cable
if self.circuit[position] is ELECT:
to_cable.add(position)
# if it is a head block
elif self.circuit[position] is ELECH:
local = self.neighbor(position)
# iterate through all surrounding blocks
for pos in local:
# if surrounding block is cable
if pos in self.circuit and self.circuit[pos] is CABLE:
# make sure count is initialized
if pos not in count_h:
count_h[pos] = 0
# add one to count
count_h[pos] += 1
# if < 2 surrounding head blocks
# it will become a head block
if count_h[pos] <= 2:
to_elech.add(pos)
# otherwise make sure it doesn't change
elif pos in to_elech:
to_elech.remove(pos)
# the head block will become a tail block
to_elect.add(position)
# go through and make the identified changes
for position in to_elect:
self.add_block(position, ELECT)
for position in to_elech:
self.add_block(position, ELECH)
for position in to_cable:
self.add_block(position, CABLE)
This code looks long, but this is just due to the large number of comments
scattered through the code. Commenting is a important practice in programming
that many people ignore. By commenting your code, it becomes easier for others
to follow your logic and sometimes it even reminds yourself of what you did and
why. Let’s briefly go through the method circuit_change
together.
The variables to_cable
, to_elech
, and to_elect
are sets: groups of
values without any duplicates. These are used to store what will happen to the
contained blocks. For example, blocks in the set to_cable
will become cable
blocks after this function runs.
First we go through all the blocks stored in self.circuit
. count_h
stores the number of surrounding ELECH blocks for the given position. We need
to make sure each checked block has a count and we always start at zero. If the
specified block is a tail block, it will become cable. If we find a block of
type ELECH, we need to check the surrounding blocks, update their count_h
value, and if that value is less than or equal to two, that cable block will be
added to to_elech
to become a head block. The checked ELECH block must then
become a tail block. Finally the three for loops at the end of the function
make the identified changes.
Now jump down to on_key_press
and add a new elif
case that calls
circuit_change
when the C
key is pressed. That is add the following.
elif symbol == key.C:
self.model.circuit_change()
Now if you layout circuit blocks and add a head block followed by a tail block,
you can move the head and tail blocks by pressing C
repeatedly.