Piader v2 – options

We have a home view now lets work on options and option view. We want to be able to set maximum lives and difficulty level. Still I have no idea what would be difference between easy and hard 🙂

Options view – skeleton

We will add option to set number of lives, my choice is 1, 3, 5, 7 and 10. Difficulty will be normal and hard.

Key W/S will move vertically on menu to select option and we will use action button to select its value. That’s two menu items, third is back button.

Lets create view, its widgets and dummy actions. Add new file options.py into views package and copy:

# -*- coding: utf-8 -*-

""" options view
"""

import abstract.view as view
import lcdmanager.widget.pane as pane #pylint: disable=I0011,F0401
import lcdmanager.widget.button as button #pylint: disable=I0011,F0401
import lcdmanager.widget.label as label #pylint: disable=I0011,F0401


class Options(view.View):
    """Options view"""
    def __init__(self, lcdmanager, game):
        """create all widgets"""
        self.manager = lcdmanager
        self.pane = pane.Pane(0, 0, 'options')
        self.pane.width = lcdmanager.width
        self.pane.height = lcdmanager.height
        self.active_button = 0
        self.game = game
        title = label.Label(self.pane.width / 2 - 4, 0, 'title')
        title.label = "Options"
        self.pane.add_widget(title)

        button_lives = button.Button(self.pane.width / 2 - 4, 1, 'btn_lives')
        button_lives.label = " Lives"
        button_lives.pointer_after = ""
        button_lives.callback = self._button_lives
        self.pane.add_widget(button_lives)

        button_difficulty = button.Button(
            self.pane.width / 2 - 5, 2, 'btn_difficulty'
        )
        button_difficulty.label = " Difficulty"
        button_difficulty.pointer_after = ""
        button_difficulty.callback = self._button_difficulty
        self.pane.add_widget(button_difficulty)

        button_back = button.Button(self.pane.width / 2 - 3, 3, 'btn_back')
        button_back.label = " Back "
        button_back.callback = self._button_back
        self.pane.add_widget(button_back)

        self.buttons = [
            button_lives, button_difficulty, button_back
        ]
        self.manager.add_widget(self.pane)

    def hide(self):
        """hide home tab"""
        self.pane.visibility = False

    def show(self):
        """show home tab"""
        self.pane.visibility = True
        self.pane.get_widget('btn_lives').event_focus()

    def loop(self, action):
        """tick"""
        if action == 'move.up':
            self.buttons[self.active_button].event_blur()
            self._prev_button()
            self.buttons[self.active_button].event_focus()

        if action == 'move.down':
            self.buttons[self.active_button].event_blur()
            self._next_button()
            self.buttons[self.active_button].event_focus()

        if action == 'action':
            self.buttons[self.active_button].event_action()

    def _prev_button(self):
        """select previous button"""
        self.active_button -= 1
        if self.active_button < 0: self.active_button = len(self.buttons) - 1 def _next_button(self): """select next button""" self.active_button += 1 if self.active_button > len(self.buttons) - 1:
            self.active_button = 0

    def _button_back(self, widget):
        """back to home tab"""
        self.game.set_tab('home')

    def _button_difficulty(self, widget):
        """set difficulty level"""
        print "difficulty"

    def _button_lives(self, widget):
        """set lives number"""
        print "lives"

This is our basic structure. Now lets consider how will we store options? We may keep it in game class or in separate class. I choose to keep it in another file. Why not in Game class as dict? Because keeping it apart looks nicer 🙂 And we have better code separation.

Configuration and view

We know that we want a separate file for configuration. Lets create configuration.py in main folder and create our class and attributes:

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

""" Piader v2 - configuration
"""


class Configuration(object):
    """Configuraton class"""
    def __init__(self):
        self.lives = 1
        self.difficulty = 'easy'
        self.lives_dict = [1, 3, 5, 7, 10]
        self.difficulty_dict = ['easy', 'hard']

We have two dictionaries to keep allowed values and two attributes to keep actual values.

Now import it in in the Game class:

import configuration as cfg

initialize and and store it (somewhere in __init__):

self.cfg = cfg.Configuration()

We have a configuration, now we need import an options view:

import views.options as options_view

initialization (in __init__):

self.views['options'] = options_view.Options(self.game_manager, self)

and hide this tab (in main_loop at the top):

self.views['options'].hide()

Finally change options_tab body to:

self.views['options'].loop(action)

When you start script you will see that something is wrong. Options tab is shown but it should stay hidden. Quick check and bug was found in manager in render function, we didn’t check visibility attribute 🙂 Such lame error. Fortunately fix is quick, add:

if not widget.visibility:
    continue

at first line in for loop. Now everything is ok.

Switching views

We have two views and we have no way to change between them. Lets fix it. First create new function in Game class:

    def set_tab(self, tab):
        """change views"""
        self.views['home'].hide()
        self.views['options'].hide()
        self.views[tab].show()
        self.option['gui_current_tab'] = tab

Next replace two lines at the top of main_loop functon that sets tabs visbility with:

self.set_tab('home')

Open home view and replace body of _button_options with:

self.game.set_tab('options')

Run a program and see that we can move between home and options ! Nice 🙂
But I found another bug. When we are back from options to home tab menu is wrongly selected. It’s because we are not resetting default button selection. New hide and show functions in home view:

    def hide(self):
        """hide home tab"""
        self.buttons[self.active_button].event_blur()
        self.pane.visibility = False

    def show(self):
        """show home tab"""
        self.pane.visibility = True
        self.active_button = 0
        self.pane.get_widget('btn_start').event_focus()

Options

But we can’t change any settings – not nice. First we will slightly rebuild view in such way that on the left we will have options and on the right values. On action we will change value to next one, after reaching end we will start from beginning.
New Options layout:

    def __init__(self, lcdmanager, game):
        """create all widgets"""
        self.manager = lcdmanager
        self.pane = pane.Pane(0, 0, 'options')
        self.pane.width = lcdmanager.width
        self.pane.height = lcdmanager.height
        self.active_button = 0
        self.game = game
        title = label.Label(self.pane.width / 2 - 4, 0, 'title')
        title.label = "Options"
        self.pane.add_widget(title)

        pos = self.pane.width / 2 - 11
        if pos < 0:
            pos = 0
        button_lives = button.Button(pos + 5, 1, 'btn_lives')
        button_lives.label = " Lives"
        button_lives.pointer_after = ""
        button_lives.callback = self._button_lives
        self.pane.add_widget(button_lives)

        self.label_lives = label.Label(pos + 12, 1)
        self.label_lives.label = str(self.game.cfg.lives)
        self.pane.add_widget(self.label_lives)

        button_difficulty = button.Button(pos, 2, 'btn_difficulty')
        button_difficulty.label = " Difficulty"
        button_difficulty.pointer_after = ""
        button_difficulty.callback = self._button_difficulty
        self.pane.add_widget(button_difficulty)

        self.label_difficulty = label.Label(pos + 12, 2)
        self.label_difficulty.label = str(self.game.cfg.difficulty)
        self.pane.add_widget(self.label_difficulty)

        button_back = button.Button(self.pane.width / 2 - 3, 3, 'btn_back')
        button_back.label = " Back "
        button_back.callback = self._button_back
        self.pane.add_widget(button_back)

        self.buttons = [
            button_lives, button_difficulty, button_back
        ]
        self.manager.add_widget(self.pane)

Running the code now shows that we have a major bug. Only labels are visible, buttons are gone. But when we disable labels buttons are back. Not good. We need to debug this.
Return to LCD Manger and add new test to manager:

    def test_two_widgets_should_render_next_to_each_other(self):
        widget_label1 = label.Label(1, 1)
        widget_label1.label = "Hi "
        widget_label2 = label.Label(4, 1)
        widget_label2.label = "Ho "
        self.lcd_manager.add_widget(widget_label1)
        self.lcd_manager.add_widget(widget_label2)
        self.lcd_manager.render()
        buffer = self.lcd_buffer
        buffer[1] = " Hi Ho              "
        assert_equal(self.lcd_manager.lcd.buffer, buffer)

This pass. Next step is to add similar test to pane widget:

    def test_two_widgets_should_render_next_to_each_other(self):
        widget_label1 = label.Label(1, 1)
        widget_label1.label = "Hi "
        widget_label2 = label.Label(4, 1)
        widget_label2.label = "Ho "
        self.pane.add_widget(widget_label1)
        self.pane.add_widget(widget_label2)
        output = [
            "       ",
            " Hi Ho "
        ]
        assert_equal(self.pane.render(), output)

And test fails! Good we know that pane is at fault. And it is easy to find line that makes problems:

output[offset_y] = line.rjust(item.position['x'] +
                    len(line), manager.TRANSPARENCY)

We are overwriting row! This is terrible 🙂 We should check if something is already in line and keep it. New render function:

    def render(self):
        """return view array"""
        output = []
        for item in self.widgets:
            if not item.visibility:
                continue
            view = item.render()
            offset_y = item.position['y']
            for line in view:
                if offset_y >= len(output):
                    for _ in range(0, offset_y - len(output) + 1):
                        output.append('')
                        
                if len(output[offset_y]) >= item.position['x']:
                    output[offset_y] = output[offset_y][0:item.position['x']] + line
                else:
                    output[offset_y] = output[offset_y] + line.rjust(item.position['x'] +
                        len(line) - len(output[offset_y]), manager.TRANSPARENCY)

                offset_y += 1

        return self._crop_to_display(output)

What has changed ? We are considering two cases, one when we can just add another string, and second where we need to fill space between strings. One funny thing, old test starts to fail. Seems I met with this problem but ignored it. I also added visibility checker so it won’t render invisible widgets.
After those changes option view renders correctly we may now work on changing settings.
New callback for lives button:

    def _button_lives(self, widget):
        """set lives number"""
        idx = self.game.cfg.lives_dict.index(self.game.cfg.lives) + 1
        if idx > len(self.game.cfg.lives_dict) - 1:
            idx = 0
        self.game.cfg.lives = self.game.cfg.lives_dict[idx]
        self.label_lives.label = str(self.game.cfg.lives)

We are getting index for current value, increasing it and when over the length of dict set it to 0.
Next we read new value and set it.
Everything looks nicely but when you start program and change lives few times you will see that we have two values of 1. But this is bug, we should have 10 and 1. Something is wrong with label’s autowidth.
So back to label widget, add new test:

    def test_change_label_should_change_width_when_autowidth(self):
        self.label.label = "It's a label"
        assert_equal(self.label.width, 12)
        self.label.label = "The label"
        assert_equal(self.label.autowidth, True)
        assert_equal(self.label.render(), ['The label'])
        assert_equal(self.label.width, 9)

Test fails, we are getting proper output but width is wrong. Seems that after setting label autowidth is disabled. Another bug to fix 🙂
Open label.py find function label and replace

self.width = 

with

self._size['width']

Do the same with height. Now we are back to green and setting lives works good.

Function for setting difficulty:

    def _button_difficulty(self, widget):
        """set difficulty level"""
        idx = self.game.cfg.difficulty_dict.index(self.game.cfg.difficulty) + 1
        if idx > len(self.game.cfg.difficulty_dict) - 1:
            idx = 0
        self.game.cfg.difficulty = self.game.cfg.difficulty_dict[idx]
        self.label_difficulty.label = str(self.game.cfg.difficulty)

One thing left is to fix hide and show functions:

    def hide(self):
        """hide home tab"""
        self.buttons[self.active_button].event_blur()
        self.pane.visibility = False

    def show(self):
        """show home tab"""
        self.active_button = 0
        self.pane.visibility = True
        self.pane.get_widget('btn_lives').event_focus()

 

Stop! Jenkins time!

Quick code check and we have some problems. Once again unused argument widget in callbacks. Ignore it. I also ignore other problems. They are not a serious threat to our code 🙂 And remember that Piader is a side project that helps LCD Manager to improve.

Summary

We added options and option view. We may now select number of lives and difficulty level. And what is the most important we fixed few bugs in LCD Manager.

Download LCDManager

Download Piader

Advertisements

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