Typing Tutor Game

City Line

The last object is our city blocks at the bottom of the screen.

I like to call it the cityline because we can draw it it as a contiguous line at the bottom of the screen.

And it is not neccesaary to keep track of every pixel of the line, we can just consider every 10 pixel block.

Let us create our blocks and city iterator and put it in a new script city.py. First our city line that will hold our city blocks in a list:

# city.py import random # Dimensions MARGINX = 5 FONTWIDTH = 9 # based on Consolas Bold 16 pt font size class CityLine(): def __init__(self, screen_width): self._len = int(screen_width / 10) # Initialise city blocks self.city_line = []

And our blocks will have randomly assigned building height, and a flag to indicate its presence or destroyed:

class Block: def __init__(self, flag = True, height = 0): self.flag = flag self.height = height return def get_flag(self): return self.flag def get_height(self): return self.height

We can then randomise the height of each block on initialise of CityLine:

class CityLine:
    def __init__(self, screen_width):
        self._len = int(screen_width / 10)
         # Initialise city blocks
        self.city_line = []
for i in range(self._len): self.city_line.append(Block(height = random.randint(1,20))) self.city_line[-1].del_block() return
class Block:
    def __init__(self, flag = True, height = 0):
        self.flag = flag
        self.height = height
        return
    
def del_block(self): self.flag = False return

We set the last block to be false, so that we do not draw indifinitely, as each block we require start x and end x, which gives us n-1 blocks with cityline of n size.

Iterator

Because our cityline consists of a list of objects, we need to build in our object an iterator to access each block.

class CityLine:
 .
 .
 .
def __iter__(self): return CityIterator(self.city_line) def __len__(self): return self._len
class CityIterator: def __init__(self, city): self.city = city self.index = 0 def __next__(self): try: block = self.city[self.index] except IndexError: raise StopIteration() self.index += 1 return block def __iter__(self): return self

Collision

Remember our y boundary is 40 pixels from screen bottom. So we just need to create method for our game play to call whenever a word goes below the height of our city block.

So we need a method to remove the block/blocks that are hit :

class CityLine():
 . 
 . 
 . 
def del_blocks(self, start_x, str_len): '''Delete associalted block that is hit by untyped words.''' startidx = int((start_x - MARGINX) / 10) endidx = startidx + int((FONTWIDTH * str_len) / 10) if endidx >= self._len: endidx = self.len - 1 for i in range(startidx, endidx+1): self.city_line[i].del_block() return

And we need a method to check if all city blocks are destroyed:

def check_gameover(self): '''Returns true if all blocks are destroyed''' if all(block.get_flag() == False for block in self.city_line): return True return False

Render cityline

Now that we have our cityline object ready, we just need to add to our game objects:

# game objects.py
import string
import random
from word import Word
from city import CityLine
class GameObjects: def __init__(self, screen_width, screen_height): self.screen_width = screen_width self.screen_height = screen_height self.word_list = self.init_words() self.game_words = [] self.bound_y = screen_height - 40
self.city_line = CityLine(screen_width)

And add to our draw method inside render() in play.py:

# play.py
import pygame
from objects import GameObjects

# Colours        R    G    B
BLUE        = (  0,   0, 128)
WHITE       = (255, 255, 255)
GREEN       = (  0, 200,   0)
BLACK       = (  0,   0,   0)
YELLOW      = (255, 255,   0)
LIGHTBLUE = (135, 206, 235) # Dimensions MARGINX = 5
. . . def render(self): '''Render surface''' self.disp_surf.fill(BLUE) # Fill background self.draw_score() # set the words for display for word in self.game_objects.game_words: if word.typed_idx > -1: typed_text = word.text[:word.typed_idx+1] untyped_text = ' '*(word.typed_idx+1) + word.text[word.typed_idx+1:] self.draw_word(untyped_text, WHITE, word.coord()) self.draw_word(typed_text, GREEN, word.coord()) else: self.draw_word(word.text, WHITE, word.coord())
# cityline base_y = self.height - 30 start_x = MARGINX start_y = base_y for block in self.game_objects.city_line: end_x = start_x + 10 end_y = base_y + block.get_height() if block.get_flag(): pygame.draw.lines(self.disp_surf, LIGHTBLUE, False, [(start_x,start_y), (start_x,end_y), (end_x,end_y)], 3) start_y = end_y start_x = end_x
# Blit everything to screen self.screen.blit(self.disp_surf, (0,0)) pygame.display.flip() return

Check collision

Where we check for words falling below the y boundary, we need to remove the corresponding city block, in objects.py

And add a check for gameover while we are editing objects.py.

# objects.py
    def move(self, delta):
        '''Returns Flags if word is removed and if it is in the middle of typing,
        when it has moved beyond boundary Y
        '''
        removed = False
        removed_typing = False 
        
        for word in self.game_words:
            word.move(delta)
            if word.get_y() >= self.bound_y:
                removed = True
                if word.typed_idx >= 0:
                    removed_typing = True
                word.set_remove()
self.city_line.del_blocks(word.get_x(), len(word))
self.clean_up() return removed, removed_typing
def is_gameover(self): return self.city_line.check_gameover()

And to link it up, we check for game over in play.py

# play.py
    def loop(self):
        '''Game play loop'''
        self.check_key_hit()
        
if self.game_objects.is_gameover(): self.playing = False return
# If moved beyond boundary Y removed, removed_typing = self.game_objects.move(self.y_delta) if removed_typing: self.typing_flag = False self.add_word() return
    def add_word(self):
        '''Add new word to drop from top'''
if self.game_objects.is_gameover(): return
# Check if valid to add if not self.game_objects.game_words or ( (pygame.time.get_ticks() - self.last_add) > self.add_timeout): if self.game_objects.add_word(): self.last_add = pygame.time.get_ticks() return

Less Randomised Fun

In order to have a good game progression, we want to increase the chance of words dropping on available city blocks more often than empty blocks.

    def add_word(self):
        '''Add word to game play'''
        # Check that word doesn't duplicate first character in list
        word = self.get_word()
        firstchar = [word.text[0] for word in self.game_words]
        while word[0] in firstchar:
word = self.get_word() # Get the list of city blocks still standing avail_blocks = [] for idx, block in enumerate(self.city_line): if block.get_flag() == True: avail_blocks.append(idx) x_avail = random.choice(avail_blocks) * 10 + MARGINX
# Calculate the max of x position of word max_x = (self.screen_width - MARGINX) - (FONTWIDTH * len(word)) # Some randomised fun x = random.randint(MARGINX,max_x)
x = random.choice([x_avail, x]) x = min(x, max_x)
self.game_words.append(Word(word, x)) return True
def get_word(self): '''Randomly select word from word list for game play''' return random.choice(self.word_list)
AttributeError: 'Block' object has no attribute 'get_height'