Thursday, February 26, 2015

Simon Project Completed!

After a little bit of a setback, I managed to put all the pieces together and complete my Simon game. The setback was that I was having trouble getting Python to access the GPIO pins from the desktop. The problem was that I wasn't running it from the root. But I don't want to log in as the root. When I tried to run sudo idle from the terminal, I got the following error:
Invalid MIT-MAGIC-COOKIE-1 keyTraceback (most recent call last):
  File "/usr/bin/idle", line 5, in 
    main()
  File "/usr/lib/python2.7/idlelib/PyShell.py", line 1427, in main
    root = Tk(className="Idle")
  File "/usr/lib/python2.7/lib-tk/Tkinter.py", line 1712, in __init__
    self.tk = _tkinter.create(screenName, baseName, className, interactive, wantobjects, useTk, sync, use)
_tkinter.TclError: couldn't connect to display ":0.0"
I had no clue what that meant. So I asked on the Raspberry Pi forum, and it turns out that I need to use gksudo idle to run it instead. I tried to read up on it, but I still don't really understand what it means. But it worked, so I don't really care that much at the moment.

But after getting that all worked out, I was able to insert the pygame graphics code relatively easily. Here's the final code:
import RPi.GPIO as GPIO
import time
import random
import pygame
GPIO.setmode(GPIO.BCM)

##### Debug Scripts
def log(msg):
    if DebugDisplay == True:
        print msg

##### LED Scripts
def AllLEDsOn():
    for i in range(0,5):
        GPIO.output(LED[i], GPIO.HIGH)
    AllColorsOn()

def AllLEDsOff():
    for i in range(0,5):
        GPIO.output(LED[i], GPIO.LOW)
    ColorOff()

def Flashes(times, pause):
    AllLEDsOff()
    for j in range(0,times):
        AllLEDsOn()
        time.sleep(0.1)
        AllLEDsOff()
        time.sleep(0.1)
    time.sleep(pause)

def LEDFlash(LEDNum, OnTime, Sound):
    GPIO.output(LED[LEDNum], GPIO.HIGH)
    OneColorOn(LEDNum)
    if Sound == 1:
        toneChannel[LEDNum-1].play(tone[LEDNum-1])
        time.sleep(0.1)
    time.sleep(OnTime)
    GPIO.output(LED[LEDNum], GPIO.LOW)
    ColorOff()

def PlayList(Listing):
    for i in Listing:
        LEDFlash(i, 0.5, 1)
        time.sleep(0.1)
    time.sleep(1)

##### Button Scripts

def LookupButton(channel):
    for i in range(0,5):
        if channel == button[i]:
            return i

def ButtonPressed(channel):
    global Pressed, Listening, Busy
    TheButton = LookupButton(channel)
    if (TheButton == 0):
        log("Button 0 Pressed")
        Pressed = 0
        Listening = 0
    elif (TheButton > 0 and Listening == 1):
        log("Button {} Pressed -- Listening".format(TheButton))
        Pressed = TheButton
        Listening = 0
    elif (TheButton > 0 and Listening == 0):
        log("Button {} Pressed -- NOT Listening".format(TheButton))
        Pressed = -1

##### Color Scripts
def OneColorOn(ThisOne):
    if ThisOne == 1:
        pygame.draw.rect(screen, REDON, [0, 0, 100, 100])
        pygame.draw.rect(screen, YELLOW, [100, 0, 100, 100])
        pygame.draw.rect(screen, GREEN, [200, 0, 100, 100])
        pygame.draw.rect(screen, BLUE, [300, 0, 100, 100])
    elif ThisOne == 2:
        pygame.draw.rect(screen, RED, [0, 0, 100, 100])
        pygame.draw.rect(screen, YELLOWON, [100, 0, 100, 100])
        pygame.draw.rect(screen, GREEN, [200, 0, 100, 100])
        pygame.draw.rect(screen, BLUE, [300, 0, 100, 100])
    elif ThisOne == 3:
        pygame.draw.rect(screen, RED, [0, 0, 100, 100])
        pygame.draw.rect(screen, YELLOW, [100, 0, 100, 100])
        pygame.draw.rect(screen, GREENON, [200, 0, 100, 100])
        pygame.draw.rect(screen, BLUE, [300, 0, 100, 100])
    elif ThisOne == 4:
        pygame.draw.rect(screen, RED, [0, 0, 100, 100])
        pygame.draw.rect(screen, YELLOW, [100, 0, 100, 100])
        pygame.draw.rect(screen, GREEN, [200, 0, 100, 100])
        pygame.draw.rect(screen, BLUEON, [300, 0, 100, 100])

    pygame.display.flip()

def ColorOff():
    pygame.draw.rect(screen, RED, [0, 0, 100, 100])
    pygame.draw.rect(screen, YELLOW, [100, 0, 100, 100])
    pygame.draw.rect(screen, GREEN, [200, 0, 100, 100])
    pygame.draw.rect(screen, BLUE, [300, 0, 100, 100])

    pygame.display.flip()

def AllColorsOn():
    pygame.draw.rect(screen, REDON, [0, 0, 100, 100])
    pygame.draw.rect(screen, YELLOWON, [100, 0, 100, 100])
    pygame.draw.rect(screen, GREENON, [200, 0, 100, 100])
    pygame.draw.rect(screen, BLUEON, [300, 0, 100, 100])

    pygame.display.flip()

##### Text Display
def ShowText(Text):
    pygame.draw.rect(screen, BLACK, [0, 100, 400, 100])
    text = font.render(Text, True, WHITE)
    textpos = text.get_rect()
    textpos.centerx = screen.get_rect().centerx
    textpos.y = 110
    screen.blit(text, textpos)

    pygame.display.flip()


##### Color codes
RED = (255,0,0)
GREEN = (0,255,0)
BLUE = (0,0,255)
YELLOW = (255,255,0)

REDON = (255,127,127)
GREENON = (127,255,127)
BLUEON = (127,127,255)
YELLOWON = (255,255,127)

WHITE = (255,255,255)
BLACK = (0,0,0)

##### Pygame setup
pygame.init()

font = pygame.font.SysFont('Calibri', 36, False, False)

screen = pygame.display.set_mode((400,150))
pygame.display.set_caption("Simon")

done = False
clock = pygame.time.Clock()

pygame.draw.rect(screen, RED, [0, 0, 100, 100])
pygame.draw.rect(screen, YELLOW, [100, 0, 100, 100])
pygame.draw.rect(screen, GREEN, [200, 0, 100, 100])
pygame.draw.rect(screen, BLUE, [300, 0, 100, 100])

ShowText("Welcome")

pygame.display.flip()

##### MAIN GAME LOOP
# Game Constants
DebugDisplay = False
button = [4, 12, 16, 20, 21]
LED = [22, 5, 6, 13, 19]

pygame.mixer.init(48000, -16, 1, 1024)
tone = [pygame.mixer.Sound("RedTone.ogg"), \
        pygame.mixer.Sound("YellowTone.ogg"), \
        pygame.mixer.Sound("GreenTone.ogg"), \
        pygame.mixer.Sound("BlueTone.ogg")]

toneChannel = [pygame.mixer.Channel(1), \
               pygame.mixer.Channel(2), \
               pygame.mixer.Channel(3), \
               pygame.mixer.Channel(4) ]

# Variables
Pressed = -1
Listening = 1
GameState = 1
HighScore = 0

# Set GPIO pins
for i in range (0,5):
    GPIO.setup(button[i], GPIO.IN, pull_up_down=GPIO.PUD_UP)
    GPIO.setup(LED[i], GPIO.OUT)
    GPIO.add_event_detect(button[i], GPIO.FALLING, callback=ButtonPressed, bouncetime=200)

# LED loop in waiting mode
while True:
    Listening = 1
    if GameState == 0:
        log("GameState = 0, Exiting Game")
        break
    elif GameState == 1:
        log("GameState = 1, Pressed = {}".format(Pressed))
        if Pressed == 0:
            GameState = 0
            break
        else:
            for i in range(1,5):
                LEDFlash(i,0.2,0)
                if Pressed == 0:
                    GameState = 0
                    break
                elif Pressed > 0:
                    GameState = 2
                    break
            Pressed = -1
    elif GameState == 2:
        log("GameState = 2, Pressed = {}".format(Pressed))
        ShowText("Get Ready...")
        Pressed = -1
        Listening = 0
        Simon = [random.randint(1,4)]
        Flashes(5, 2)
        while GameState == 2:
            log("Starting a round, length = {}".format(len(Simon)))
            ShowText("Round {}".format(len(Simon)))
            Listening = 0
            PlayList(Simon)
            Flashes(1,0)
            for Levels in range(0, len(Simon)):
                log("Waiting for user input, correct answer = {}".format(Simon[Levels]))
                ShowText("Go!")
                GPIO.output(LED[0], GPIO.HIGH)
                Listening = 1
                Pressed = -1
                for timerloop in range(0,500):
                    if Pressed > -1:
                        break
                    time.sleep(0.01)
                if Pressed == Simon[Levels]:
                    LEDFlash(Pressed, 0.5, 1)
                else:
                    toneChannel[0].play(tone[0])
                    toneChannel[1].play(tone[1])
                    toneChannel[2].play(tone[2])
                    toneChannel[3].play(tone[3])
                    Flashes(5,2)
                    GameState = 1
                    if HighScore < len(Simon):
                        HighScore = len(Simon)
                    ShowText("High Score: {}".format(HighScore))
                    break
            GPIO.output(LED[0], GPIO.LOW)
            if (GameState == 1 and Pressed != 0):
                PlayList(Simon[0:Levels])
                for i in range(0,10):
                    LEDFlash(Simon[Levels], 0.1, 1)
                    time.sleep(0.1)
                time.sleep(1)
            elif GameState == 2:
                Simon.append(random.randint(1,4))
                time.sleep(0.2)
                Flashes(2,0)
                time.sleep(1)
        GameState = 1
        Pressed = -1

AllLEDsOff()
GPIO.cleanup()
pygame.quit()
The only thing that I might have left to do with this is to generate a little bit of documentation so that in a year or two when I look back at this, I'll know what I did and be able to go back through the whole thing. I'm not sure if I'm disciplined enough to do that, but that's really what I should do at this point.

Monday, February 16, 2015

Simple Graphics

After a short hiatus, I'm back to goofing around on the Simon project. I finally took the leap into learning Pygame. I'm not following any particular tutorial, but just cobbling things together with what I can find on the internet. My primary source right now is the following website: http://programarcadegames.com/index.php?lang=en. It's very well organized with a lot of comments, which is very helpful.

My only task tonight was to just get a basic window up with some colors and some text. This is just the basic type of task I need to understand in order to finish making the game. So I decided to just put together a basic color test that shows all the colors I was going to use in the project.

There are all sorts of new commands I needed learn. I've put them into groups based on concept, as opposed to the specific order they're called in:
  • General commands:
    • import pygame: This just calls up the Pygame module.
    • for event in pygame.event.get(): This cycles through the various things that the user has done in the window. I will probably need to explore this more if I wanted to make the Simon game also respond to clicks. But I don't want to do that in the current project. The current project is much more about physical buttons and lights.
    • if event.type == pygame.QUIT: This is what happens when the X in the window.
  • Font commands:
    • font = pygame.font.SysFont('Calibri', 36, False, False): This defines the font that the program will use. The pattern is font style, size, bold, and italics. Presumably, you can set multiple fonts because you call this command when you actually create text.
    • text = font.render("Welcome", True, WHITE): This renders a piece of text. The text is in the quotes, the next parameter has to do with anti-aliasing (which is something about smoothing things out), and then the last parameter is the text color.
    • Other commands: There are some commands I took off of a website. It has to do with grabbing and setting different attributes. I'm not going to go through them one at a time because I'm feeling lazy.
    • screen.blit(text, textpos): This actually puts the text into the window. But it won't appear until the screen is flipped, just like any other drawing command.
  • Window commands:
    • screen = pygame.display.set_mode((400,250)): This creates a window that is referred to as screen. The dimensions of the window are set inside of the parentheses.
    • pygame.display.set_caption("Simon"): This names the window. I'm not entirely sure how this works in the sense that it doesn't call the screen by its name. But it works.
    • Colors: Colors in Pygame are defined by RGB codes. I needed two version of the four main colors: one for when the light is on and one for when the light is off. To calculate the color-on value, I just averaged the color-off value with the white value.
    • pygame.draw.rect(screen, RED, [0, 0, 100, 100]): This draws a rectangle on the screen of the given color. The first two coordinates give the top left corner and the second two coordinates give the dimensions.
    • pygame.display.flip(): This redraws the screen. I'm not sure why it's called a flip instead of a redraw, but that's what it is.
  • Clock commands:
    • clock = pygame.time.Clock(): This establishes a clock that is used later in the code to control the refresh rate.
    • clock.tick(60): This limits the program to running only 60 frames per second. The purpose of this is just to slow down the refresh rate to not eat up too much CPU power.
The full code is below. What it does is draw a window with 8 colored squares and a little bit of text at the bottom.
import pygame

RED = (255,0,0)
GREEN = (0,255,0)
BLUE = (0,0,255)
YELLOW = (255,255,0)

REDON = (255,127,127)
GREENON = (127,255,127)
BLUEON = (127,127,255)
YELLOWON = (255,255,127)

WHITE = (255,255,255)

pygame.init()

font = pygame.font.SysFont('Calibri', 36, False, False)

screen = pygame.display.set_mode((400,250))
pygame.display.set_caption("Simon")

done = False
clock = pygame.time.Clock()

while not done:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            done = True

    pygame.draw.rect(screen, RED, [0, 0, 100, 100])
    pygame.draw.rect(screen, YELLOW, [100, 0, 100, 100])
    pygame.draw.rect(screen, GREEN, [200, 0, 100, 100])
    pygame.draw.rect(screen, BLUE, [300, 0, 100, 100])

    pygame.draw.rect(screen, REDON, [0, 100, 100, 100])
    pygame.draw.rect(screen, YELLOWON, [100, 100, 100, 100])
    pygame.draw.rect(screen, GREENON, [200, 100, 100, 100])
    pygame.draw.rect(screen, BLUEON, [300, 100, 100, 100])

    text = font.render("Welcome", True, WHITE)
    textpos = text.get_rect()
    textpos.centerx = screen.get_rect().centerx
    textpos.y = 210
    screen.blit(text, textpos)

    pygame.display.flip()
    clock.tick(60)

pygame.quit()
My next task will be to get the actual game play into the program. I don't think it should be too difficult. But I've said that type of thing before and been completely wrong. So we'll see what happens.