We have just two things left. After that we are back to main goal – upgrading LCD class.
First we will go with shooting. Next small refactor of our controls and finally adding missile class and space as fire button. After that we will add collision detection.
Download source code.
Before adding missile lets change one thing. Our ctrl_keyboard is sending keys that were pressed and game is parsing them. I think is better to change this to event names like player.move.left, so our next controls will just send event and not some undefined variable that will be parsed by game.
Change while loop to:
while True: key = getch() if key == 'a': conn_socket.send('player.move.left') if key == 'd': conn_socket.send('player.move.right') if key == ' ': conn_socket.send('player.move.fire')
Open game.py, change keyboard ifs to:
event = self.queue.get(True, 0.05) if event == "player.move.left": self.player.move_left() if event == "player.move.right": self.player.move_right() if event == "player.move.fire": self.player.fire()
In Player class add: (don’t forget about importing Missile class)
def fire(self): """fire a missile""" if self.missile['current'] < self.missile['max']: self.missile['current'] += 1 if self.heading == "right": self.objects.append(missile.Missile(self.pos_x, self.pos_y, self)) else: self.objects.append(missile.Missile(self.pos_x + 1, self.pos_y, self))
Finally create Missile class in missile.py file:
import piader.item as item class Missile(item.Item): sprite = "|" """Missile""" def __init__(self, x, y, player): """init bomb""" self.pos_x = x self.pos_y = y self.player = player def tick(self): """action in tick""" self.pos_y -= 1 def get_position(self): """missile position""" return self.pos_x, self.pos_y def get_sprite(self): """get sprite""" return self.sprite def event_discard(self): """discard missile, decrease Player bomb counter""" self.player.missile['current'] -= 1
When you start game and press space a missile will go up but after going out of screen script will crash. We need to add missile removal. Find draw function in game and modify position y check to:
if position_y > 5 or position_y < 0:
Thats all, we can now retaliate against enemy. There is one funny thing, char | is used as missile sprite but my smalles lcd can’t display it 🙂
We have all pieces on board, enemy can move and drop a bomb. Player can move and fire a missile. It would be nice if all those things could interact with each other.
Lets see what kind of collisions we have:
- missile hits enemy
- bomb hits player
Not much so lets implement them.
We need to know if object can hit something and can be hit by something. Add those functions to item.py:
def is_hit(self, target): """check if object is hit by target""" raise NotImplementedError("is_hit not implemented") def can_hit(self): """object can hit something else""" raise NotImplementedError("can_hit not implemented")
Implementation in bomb.py and missile.py:
def can_hit(self): """bomb can hit something""" return True def is_hit(self, target): """bomb cant be hit""" return False
Those elements can hit others but can’t be hit by anything.
In player.py: (don’t forget about bomb import )
def can_hit(self): """Player can't hit anything""" return False def is_hit(self, target): """Player can be hit""" if isinstance(target, bomb.Bomb): target_position = target.get_position() if target_position == self.pos_y and \ self.pos_x <= target_position <= self.pos_x + 1: return True return False
Player can be hit only with bomb. Without this check player can destroy himself 🙂
And in enemy.py: (this time don’t forget to add missile import)
def can_hit(self): """enemy can't hit anything""" return False def is_hit(self, target): """enemy can be hit""" if isinstance(target, missile.Missile): target_position = target.get_position() if target_position == self.pos_y and \ self.pos_x <= target_position <= self.pos_x + 2: return True return False
Enemy can be hit only with missile.
We have hit capabilities and we should use them, open game.py and add collision checks:
def collision_check(self): """checks for collisions""" for source in self.objects: if source.can_hit(): for target in self.objects: if target.is_hit(source): if isinstance(target, player.Player): self.game_over() if isinstance(target, enemy.Enemy): self.enemy_hit(source, target)
I think best place to call this function is at the end of tick function.
Now if player is hit the games is over. We will display string and exit script. As for enemy, when its down we will spawn new at random location.
def game_over(self): """displays game over and stops game""" self.game_on = False self.lcds.buffer_clear() self.lcds.buffer_clear() self.lcds.write("Game Over", 5, 2) self.lcds.flush() self.lcds.flush() self.event_server.join() def enemy_hit(self, source, target): """event enemy hit""" target.event_discard() source.event_discard() self.objects.remove(source) self.objects.remove(target) self.objects.append(enemy.Enemy(random.randint(1, self.width - 2), random.randint(0, 3), self.width, self.objects ))
And its done ! Game is playable but not very dynamic. Lets speed it a little
Speed it up
Having speed at 1 fps is quite boring. We can easily change it. First replace main game loop with:
def game(self): """game loop""" try: while self.game_on: start = time.time() self.tick() end = time.time() if end - start < self.game_tick: t_delta = end - start time.sleep(max(0, self.game_tick - t_delta)) finally: self.event_server.join()
We changed how delay is calculated. It allows us to change game_tick to 0.5. Now game is more challenging. Remember that we can’t go lower that time required to calculate all actions in one tick.
If we look at the item, enemy, missile, bomb and player, we can see that those classes are not optimal. Code repeats itself (like in get_position, get_sprite). We should avoid repetition and put shared code in base class Item, so lets change get_position to:
def get_position(self): """get position""" return self.pos_x, self.pos_y
and remove this function from all subclasses.
Almost same with get_sprite, just don’t delete it from Player class.
Now change __init__, extract common part: pos_x and pos_y.
def __init__(self, x, y): """init function""" self.pos_x = x self.pos_y = y
Modify __init__ in all subclasses by replacing pos assignments with:
item.Item.__init__(self, x, y)
We can do some optimizations in tick functions in Enemy. Now we are changing position and then do checks. Lets change it so first we will do checks and then change position.
New tick function in enemy:
def tick(self): """action in tick""" if self.bombs['max'] > self.bombs['current'] and \ random.randint(1, 100) < self.bombs['chance']: self.bombs['current'] += 1 self.objects.append(bomb.Bomb(self.pos_x + 1, self.pos_y, self)) direction =  if self.pos_x > 2: direction.append(-1) if self.pos_x < self.max_x - len(self.sprite) - 1: direction.append(1) self.pos_x += random.choice(direction)
Next I have done something similar in Player class. Changed move_right and move_left that checks will be first.
Last thing I changed was adding move_* functions to Item and change subclasses accordingly.
Stop! Jenkins time!
This time no more pylint violations but few cpd one. But I will just ignore them 🙂
We have finished our game, from now on we will focus on expanding Lcd classes. We will add virtual lcd, menu and widgets.
Download source code.