Piader – upgrade LCD by game – part 1

Lets do some upgrades to our LCD package. I think the best way to do so is to write a simple game.

We will use both screens to display game objects, on one there will be flying DMO (defined moving object :D), and on second lcd – a player. Enemy and player will be trying to hit each other with bullets. Simple concept but exactly what we need.

Lets name it Piader. 

Source available at the bottom of this post.

Setup

I will use two screens from previous tutorials. Enemy will get bigger lcd (20×4) because.. hmm just like that. And player will move on smaller one (16×2). Bigger screen will go on top, smaller on bottom.

WP_20150627_13_49_01_Pro

Game mechanics

Basic time unit is tick, defined as 1 second. Main loop will do its job, refresh displays and sleep for the remaining time (to 1 s). All object will be stored in array and on each tick they will be called and redraw.

DMO will move randomly in horizontal direction by 1. Will also randomly drop a bomb.

Player will be moving horizontal and firing according to user actions. First we will play with keyboard control.

When enemy is hit it will explode and another one will respawn.

When player is hit the game ends.

Lets start with coding

Download code from previous post, unzip and start your favorite IDE. We will focus on using and rewriting LCD package.

Create game package piader – there we will store game files.

Lets begin with basic things. Main loop, moving enemy and game launcher.

In project root create launcher, I called it piader.py. Its creating LCD instances, passing them to game and starts game loop:

#!/usr/bin/python
# -*- coding: utf-8 -*-

"""Game launcher"""

__author__ = 'Bartosz Kościów'

import RPi.GPIO as GPIO #pylint: disable=I0011,F0401
from charlcd import lcd
from charlcd.drivers.gpio import Gpio
from charlcd.drivers.i2c import I2C
from piader import game

GPIO.setmode(GPIO.BCM)

def main():
    """set lcds and start game"""
    lcd_two = lcd.CharLCD(16, 2, I2C(0x20, 1))
    lcd_one = lcd.CharLCD(20, 4, Gpio())
    my_game = game.Piader([lcd_one, lcd_two])
    my_game.game()

main()

In package piader create game.py. This is game engine, our core class. Lets look at it:

#!/usr/bin/python
# -*- coding: utf-8 -*-

"""Simple game"""
__author__ = 'Bartosz Kościów'

import time
import math
import enemy

class Piader(object):
    """Piader main class"""
    width = 20
    height = 6
    game_tick = 1.0
    objects = []

    def __init__(self, lcds):
        """init class"""
        self.lcds = lcds
        self.init_lcds()
        self.game_on = True

    def init_lcds(self):
        """inits lcds"""
        for l in self.lcds:
            l.init()
        self.objects.append(enemy.Enemy(2, 0, self.width))

    def tick(self):
        """game tick"""
        for item in self.objects:
            item.tick()
            self.draw(item)

    def draw(self, item):
        """draw sprite on screen"""
        lcd = self.lcds[0]
        pos = item.get_position()
        lcd.position_xy(pos[0], pos[1])
        lcd.string(item.get_sprite())

    def game(self):
        """game loop"""
        while self.game_on:
            start = time.time()
            self.tick()

            end = time.time()
            if end - start < self.game_tick:
                t = math.modf(end)
                time.sleep(self.game_tick - t[0])

Function __init__ is receiving array with lcds. They will be used to display game. Its initing them and creating first object/item – our enemy.
We define game size as 20×6 and tick as 1 second.

After init sequence launcher will run a main game loop, called game. For now its calling tick function and sleeps for required time, as long as needed to keep with 1 fps.

Function tick is responsible for all actions. It will iterate over self.objects, call tick on each item and do drawing.
Array self.objects will keep all items that do some actions, like enemy, player, bullet, bomb.

All objects in our game have coords (x, y), we need to know where they are, and sprite, we need to know how they look alike. We need to have an access to those parameters, so few functions are required. Another required function is tick called from main loop. This function is responsible for object AI.
Lets build an interface with required functions:

class Item(object):
    def __init__(self, x, y, max_w):
        """init function"""
        raise NotImplementedError("__init__ not implemented")

    def tick(self):
        """tick function"""
        raise NotImplementedError("tick not implemented")

    def get_sprite(self):
        """get sprite function"""
        raise NotImplementedError("get_sprite not implemented")

    def get_position(self):
        """get position function"""
        raise NotImplementedError("get_position not implemented")

Objects will be created with start coordinates and maximum allowed width. So no object will move out of screen range.

Now lets focus on Enemy class:

#!/usr/bin/python
# -*- coding: utf-8 -*-

"""Enemy class"""

__author__ = 'Bartosz Kościów'
import random
import piader.item as item

class Enemy(item.Item):
    """DMO - defined moving object"""
    def __init__(self, x, y, max_x):
        """init enemy"""
        self.x = x
        self.y = y
        self.max_x = max_x
        self.sprite = '<*>'

    def tick(self):
        """action in tick"""
        self.x += random.choice([-1, 1])
        if self.x < 1:
            self.x = 1
        if self.x > self.max_x - 1:
            self.x = self.max_x - 1

    def get_position(self):
        """enemy position"""
        return self.x, self.y

    def get_sprite(self):
        """get sprite"""
        return self.sprite

Our enemy has simple sprite and simple AI. It will move randomly left or right by 1 field.

First run

When you run the launcher, you will see (probably) a nice DMO moving randomly. But there is a big problem. Lcd is not clearing itself ! Our game is only drawing object but not clearing previous state. And there is this big blinking cursor.

WP_20150627_13_52_42_Pro

Actually we have two problems.

First it will be good idea to add some init parameters to lcd driver. We will be able to set some options like hide a cursor.

Second problem is more complicated. We can manually clear old DMO position and redraw it on new one but its very ineffective. Better idea is to add buffer to our lcd driver.

Lcd will know what is currently displayed on it, we will store this information in array.

Next we wont write anything directly to this array. We will create another array that will be our buffer and all drawing will go to it. After we are ready we will tell driver to update current display from buffer. It will do this a clever way, writing only those chars that differ from current one.

Its good idea for performance. Its rare to change all chars on all displays at the same time. By doing write operations only when needed we greatly increase performance. Imagine, we have two screens, 20*4 + 16*2, it gives 112 write operations for all chars. But our DMO require only four ! One to clear old part, ant three to display it.

We wont delete our old way of accessing lcd but we will rename it as direct. New one will be called buffered.

Adding options

If we look at the documentation of HD44780 we will find that command Display ON/OFF is responsible for setting cursor options. We have two options blink and display. Lets use them.

First add new test in test_lcd.py. I wont digg into testing. This time I’m mentioning about it because its good practice and I’m using TDD approach (only for project package, not Piader). If you want to see all tests look into source code.
New test will check if we are able to set variables correctly:

 def test_init_lcd_without_cursor_and_blinking(self):
        screen = lcd.CharLCD(20, 4, Null(), cursor_visible=0, cursor_blink=0)
        assert_equal(screen.cursor_visible, 0)
        assert_equal(screen.cursor_blink, 0)

We are in red state. Now open lcd.py from charlcd package and change __init__ function to :

def __init__(self, width, height, driver, display_mode=DISPLAY_MODE_DIRECT, cursor_visible=1, cursor_blink=1):

now in body add:

self.cursor_blink = cursor_blink
self.cursor_visible = cursor_visible

Time to tech our code to use this parameters, go to the end of init function and change

self.cmd(0x0C)

to

self.driver.cmd(12 + (self.cursor_visible * 2) + (self.cursor_blink * 1))

And we are green. Now change lcd creation in launcher by adding parameters to turn off cursor and blinking.

First problem solved.

Buffer time

Thats more complicated. We have two ways of displaying on lcd that are totally different. Lets see if we can split them into two classes with current lcd as parent class.
All function that do writing we will move to lcd_direct.py and whats left is common part of two new classes – and it will stay in lcd.

Beginning of lcd_direct goes like this:

import lcd

class CharLCD(lcd.CharLCD):
    """Class for char LCDs"""
    def __init__(self, width, height, driver, cursor_visible=1, cursor_blink=1):
        lcd.CharLCD.__init__(self, width, height, driver, lcd.DISPLAY_MODE_DIRECT, cursor_visible, cursor_blink)

After that we have all write functions.

So our lcd.py is left with __init__ and 5 other functions. One last thing to change, launcher, change lcd to lcd_direct. Nice or not ?
Run tests… yep they will fail.

I wont write about tests rewriting, do it yourself or look into code 🙂

Time has come for main part. Lets think for a second.

What we need is ability to prepare our buffer and when its ready flush it on main screen. Flushing must be clever, just write chars on positions that differ.
We should be able to put char or string at any position and it would be nice to use stream mode. Without coordinates chars will appear one after another. So we need to calculate current position and store it.

Lets get to work.

So first lest write test to check if after init we have buffer and screen variable with proper size. We have a failing test so lets go green. Create lcd_buffered.py and start with:

#!/usr/bin/python
# -*- coding: utf-8 -*-

"""Lcd class for buffered input"""

__author__ = 'Bartosz Kościów'

import lcd

class CharLCD(lcd.CharLCD):
    """Class for char LCDs"""
    screen = []
    buffer = []
    current_pos = {'x': 0,
                   'y': 0}

    def __init__(self, width, height, driver, cursor_visible=1, cursor_blink=1):
        lcd.CharLCD.__init__(self, width, height, driver, lcd.DISPLAY_MODE_BUFFERED, cursor_visible, cursor_blink)

    def init(self):
        """screen init and buffer init"""
        lcd.CharLCD.init(self)
        self.screen = [" ".ljust(self.width, " ") for i in range(0, self.height)]
        self.buffer = [" ".ljust(self.width, " ") for i in range(0, self.height)]

Time for write function. I wrote few test and after red->green cycles ended with code:

    def write(self, content, x=None, y=None):
        """writes content into buffer at position(x,y) or current"""
        if x is None:
            x = self.current_pos['x']
        if y is None:
            y = self.current_pos['y']

        if x >= self.width:
            raise IndexError
        if y >= self.height:
            raise IndexError

        line = self.buffer[y]
        new_line = line[0:x] + content + line[x + len(content):]
        line = new_line[:self.width]
        self.buffer[y] = line

        self.current_pos = {'x': x + len(content),
                            'y': y}

Its good idea to add function to set, get current position and to clear buffer:

def set_xy(self, x, y):
    """sets current cursor position
    Args:
        x: x position
        y: y position
    """
    self.current_pos['x'] = x
    self.current_pos['y'] = y

def get_xy(self):
    """return current cursor position"""
    return self.current_pos;
def buffer_clear(self):
    """clears buffer"""
    self.buffer = [" ".ljust(self.width, " ")
                   for i in range(0, self.height)]

And finally add flush function:

def flush(self):
    """Flush buffer to screen, skips chars that didn't change"""
    bag = zip(range(0, self.get_height()), self.buffer, self.screen)
    for line, line_new, line_current in bag:
        if line_new != line_current:
            i = 0
            last_i = -1
            for char_new, char_current in zip(line_new, line_current):
                if char_new != char_current:
                    if last_i != i:
                        self._cmd(lcd.LCD_LINES[line] + i)
                        last_i = i
                    self._char(char_new)
                    last_i += 1
                i += 1
            self.screen[line] = line_new

def _cmd(self, cmd):
    self.driver.cmd(cmd)

def _char(self, char):
    self.driver.char(char)

Few words about it. We iterate on three things at the same time, line number, line in buffer and line in current. If buffer and current line are different we iterate over chars on each line. If chars are the same, we increase internal pointer. If chars are different we check if we are in required position and if not we move lcd cursor, and then we send char to lcd.
Last thing to do is update screen line with buffer line.

Rewrite game to use buffer

Change our launcher, replace all lcd_direct with lcd_buffered.
Open game.py and change tick and draw to:

def tick(self):
    """game tick"""
    self.lcds[0].buffer_clear()
    for item in self.objects:
        item.tick()
        self.draw(item)

    self.lcds[0].flush()

def draw(self, item):
    """draw sprite on screen"""
    lcd = self.lcds[0]
    pos = item.get_position()
    lcd.write(item.get_sprite(), pos[0], pos[1])

Modified tick function clears buffer, enters loop and flushes buffer. In draw function we write to buffer. Small changes and we are ready.

Now start script and you will see DMO moving randomly – this time without leaving traces.

WP_20150630_17_02_00_Pro

 

 

 

There is still one bug. DMO will move out of screen to the right. Bug is in line that checks its position. Fix is simple and just do it yourself (or look into code):)

Stop! Jenkins time !

We have some stable source of our game. Its good idea to run Jenkins test. As I mentioned I wrote required tests. They are in source package.

Thing that I messed up were:
– variable names
– line to long
– for loops without variable usage

Summary

In part 1 we have:
– created enemy and abstract class for all items
– added simple AI to enemy
– added buffered output
– create main game loop
– 1 fps 🙂
– fixed code with Jenkins help

What next ?
– add player
– add player controls
– add shooting
– add bullet
– add bomb
– collision

Stay tuned for next part.

Download source code.

Advertisements

3 comments

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s