heisser_draht/gametest_rev2.py
2020-07-16 10:58:01 +02:00

873 lines
29 KiB
Python

#!/usr/bin/env python
#coding=utf-8
############################################
########### GENERAL INFORMATION ############
############################################
"""
This is the software for the project heisser_draht from the bfi itlabs st.stefan
The idea of the game is to follow a wire with a ring, without touching it.
The ones with the fastest times to reach the end (and with the least errors)
are written onto the highscore table. Said table is temporary, and not saved on exit!
The main game loop is running in 4 states which are changed depending on what inputs the raspberry receives.
In each game state, a different part of the main game loop is run repeatedly, checking for signals and taking
corresponding actions.
If the right signals are detected, the state will change accordingly.
* The game is not running and not ending (State 0)
This state is the default "starting" page of the game.
The screen shows the highscore table, and the game waits for a start signal.
If the raspberry detects a signal on the start pin, the game state changes to State 1
* The game is running and not ending (State 1)
This is the game running. A timer is displayed on screen.
Errors are shown on screen aswell.
The game is waiting for error signals and stop signals.
If the raspberry detects a signal on the error pin, errors are added to the time.
If the raspberry detects a signal on the stop pin, the game state changes to State 2
* The game is not running and ending (State 2)
Once the stop signal has been reached in the running game, this state is reached.
The game checks for highscores and asks for a name, should a highscore have been made.
This state automatically ends after a few seconds and returns the game to State 0.
* The game is neither running nor ending (State 3)
This state is currently invalid and does nothing. It should never be reached.
There's no functionality assigned to this, and this state is never run, but included
for completeness sake.
If during any of these states the shutdown pin is detected, the raspberry will shutdown entirely.
The game also features a (not entirely interactive) LED lighting control. The LEDs are supposed
to be driven by a small MOSFET driver circuit. DO NOT ATTACH LEDS DIRECTLY!
The correct type of LEDs are the ones that have a common 12V rail per segment, and are switched
by clamping the ground. Those ground clamps are driven by the Raspberry via a PWM signal.
Pin explanation:
pin_blue, pin_green, pin_red
Type: PWM Output
Use: These pins are for controlling the lighting. They are to be connected to a MOSFET driver stage
that clamps the ground.
pin_start
Type: Input, Pull Down
Use: Start pin. If this is active, the game starts.
pin_stop
Type: Input, Pull Down
Use: Stop pin. If this is active, the game ends
pin_error
Type: Input, Pull Down
Use: This is to be attached to the game wire. If the wire is touched,
the pin is pulled up and an error is registered.
pin_shutdown
Type: Input, Pull Down
Use: Triggering this input causes a system shutdown.
GPIO Pinout for assembly:
3V3 (1) (2) 5V
- (3) (4) 5V
- (5) (6) GND
- (7) (8) -
GND (9) (10) -
- (11) (12) -
- (13) (14) GND
- (15) (16) pin_blue
3V3 (17) (18) pin_green
- (19) (20) GND
- (21) (22) pin_red
- (23) (24) -
GND (25) (26) -
- (27) (28) -
- (29) (30) GND
- (31) (32) pin_start
pin_error (33) (34) GND
- (35) (36) pin_shutdown
pin_stop (37) (38) -
GND (39) (40) -
Default state of pins:
P_32: pull up
P_33: pull up
P_36: pull up
P_37: pull up
Make sure leds are on pull-up default pins
so when their state is undefined, the led
strips are switched off!
P_22: pull up
P_18: pull up
P_16: pull up
Circuit diagrams for the contact and button electronics:
All buttons are close-contact.
+3.3V DC ---+-----------+-----------+-----------+-------->
| | | |
R2 R4 R6 R8
| | | |
+----+ +----+ +----+ +----+
| | | | | | | |
| P_32 | P_33 | P_36 | P_37
\ \ \ \
\ \ \ \
\ \ \ \
* * * *
| | | |
GND --------+-----------+-----------+-----------+-------->
LED Strip controller and driver:
A power MOSFET per channel is driven by the GPIO output of the raspberry to
control the flow from the LED Strip contact on the connector to the ground rail.
P_22 ---+ LED_STRIP_RED-----+
| |
R9 Q1 | |--+ D
| |
+----------------| |<-+
| G | |
R10 | |--+ S
| |
| |
GND ----+---------------------+----->
P_18 ---+ LED_STRIP_GRN-----+
| |
R11 Q2 | |--+ D
| |
+----------------| |<-+
| G | |
R12 | |--+ S
| |
| |
GND ----+---------------------+----->
P_16 ---+ LED_STRIP_BLU-----+
| |
R13 Q3 | |--+ D
| |
+----------------| |<-+
| G | |
R14 | |--+ S
| |
| |
GND ----+---------------------+----->
LED Strip:
The LED Strip has a 4 contact connector, one being the 12V supply rail
and the other 3 the ground connections for the corresponding LED colour.
+12V DC ----+---------------------------->
|
| a series of LEDs and
| resistors, as determined
| by the LED strip.
|
+--( ->|- -[|||]- ->|- )--+
|
|
LED_STRIP_RED/GRN/BLU
Part list:
Resistors:
R2...........10,000 Ohm
R4...........10,000 Ohm
R6...........10,000 Ohm
R8...........10,000 Ohm
R9............1,000 Ohm
R10..........10,000 Ohm
R11...........1,000 Ohm
R12..........10,000 Ohm
R13...........1,000 Ohm
R14..........10,000 Ohm
Transistors:
Q1...........IRLZ34N Enhancement Mode n-channel MOSFET
Q2...........IRLZ34N Enhancement Mode n-channel MOSFET
Q3...........IRLZ34N Enhancement Mode n-channel MOSFET
Credits:
Based on a script made by TODO: source, although it has been heavily altered
Changes made by: Clima Philip, Krajnc Moris, Cooke Thomas, Glantschnig Raphael
Hosted on:
https://git.wolfsberg.local/philipp.clima/heisser_draht
"""
############################################
############# START OF IMPORTS #############
############################################
import signal
import sys
import os
import time
import pygame
from PIL import Image
from pygame.locals import *
import RPi.GPIO as GPIO
############################################
############## END OF IMPORTS ##############
############################################
############################################
########### START OF DEFINITIONS ###########
############################################
# base pygame settings
pygame_fps = 30 #frames per second setting
pygame_clock = pygame.time.Clock()
# enable border drawing
cfg_borders_enabled = False
# GPIO for Buttons
pin_start = 32
pin_stop = 37
pin_error = 33
pin_shutdown = 36
# GPIO for LED
pin_red = 22
pin_green = 18
pin_blue = 16
# other constants
# every time the wire is touched, some time is added as penalty.
# number is in ms
time_per_error = 5000
# name length for highscores
max_name_length = 10
# preset highscores
hs1_name = "Fritz"
hs2_name = "Bernd"
hs3_name = "Max"
hs1_time = 100000
hs2_time = 200000
hs3_time = 300000
############################################
############ END OF DEFINITIONS ############
############################################
############################################
####### START OF PRE-INITIALISATION ########
############################################
# here goes stuff that is requires by functions
# so they can be properly defined
# initialise the game
pygame.init()
# screen settings - autodetermined based on the initialised pygame video instance
screen_size_x = pygame.display.Info().current_w
screen_size_y = pygame.display.Info().current_h
# load sound effects
#error_sound = pygame.mixer.Sound('snd/buzz.wav')
error_sound = pygame.mixer.Sound('snd/nope.wav')
logoff_sound = pygame.mixer.Sound('snd/winxplogoff.wav')
# fonts
pygame_font_1 = pygame.font.Font('freesansbold.ttf', 90)
pygame_font_2 = pygame.font.Font('freesansbold.ttf', 65)
pygame_font_3 = pygame.font.Font('freesansbold.ttf', 45)
pygame_font_4 = pygame.font.Font('freesansbold.ttf', 16)
# colors
pygame_color_green = pygame.Color(42, 217, 13)
pygame_color_black = pygame.Color(0, 0, 0)
pygame_color_white = pygame.Color(255, 255, 255)
pygame_color_yellow = pygame.Color(255, 215, 0)
pygame_color_grey = pygame.Color(196, 202, 206)
pygame_color_brown = pygame.Color(177, 86, 15)
# font colors
pygame_font_main_color = pygame_color_black
# initialise gpio
GPIO.setmode(GPIO.BOARD)
GPIO.setup(pin_start, GPIO.IN)
GPIO.setup(pin_stop, GPIO.IN)
GPIO.setup(pin_error, GPIO.IN)
GPIO.setup(pin_shutdown, GPIO.IN)
# predefinition of led variables, for use in functions
led_red = 0
led_green = 0
led_blue = 0
############################################
######## END OF PRE-INITIALISATION #########
############################################
############################################
###### START OF FUNCTION DEFINITIONS #######
############################################
def get_image_width(filepath):
"""get the width of an image"""
return (Image.open(filepath).size)[0]
def get_image_height(filepath):
"""get the height of an image"""
return (Image.open(filepath).size)[1]
def signal_handler(sig, frame):
"""
signal handler, later called to interpret
ctrl+c as "quit" rather than "abort"
"""
print('Programm exiting')
exit_application()
def led_init():
"""
initialises the LED pins
mind that the pins for the LEDs are pullup by default,
which is on purpose, since this causes the LED strip to
be turned off when the program isnt running or their state
is undefined
set the led pins to be outputs
careful: these pins dont drive the
led strip themselves, but rather 3
n-channel mosfets, type IRLZ34N
"""
global led_red, led_green, led_blue
GPIO.setup(pin_red, GPIO.OUT)
GPIO.setup(pin_green, GPIO.OUT)
GPIO.setup(pin_blue, GPIO.OUT)
# set the pwm frequency to 100
# if you experience strobing, try changing
# the number
led_red = GPIO.PWM(pin_red, 100)
led_blue = GPIO.PWM(pin_blue, 100)
led_green = GPIO.PWM(pin_green, 100)
# start pwm generation with a duty cycle of 0
led_green.start(0)
led_blue.start(0)
led_red.start(0)
def change_red(amount):
"""
takes an input from 0 to 255 and converts it to Duty Cycle
valid duty cycle values are between 0 and 100
stores the new value in led_brightness_COLOUR
this value is later read by the led_handler to set the
corresponding duty cycles to allow fast interactive updating
to be used by change_led_colour
"""
global led_red
factor = 100/255
led_red.ChangeDutyCycle(amount*factor)
def change_green(amount):
"""
takes an input from 0 to 255 and converts it to Duty Cycle
valid duty cycle values are between 0 and 100
stores the new value in led_brightness_COLOUR
this value is later read by the led_handler to set the
corresponding duty cycles to allow fast interactive updating
to be used by change_led_colour
"""
global led_green
factor = 100/255
led_green.ChangeDutyCycle(amount*factor)
def change_blue(amount):
"""
takes an input from 0 to 255 and converts it to Duty Cycle
valid duty cycle values are between 0 and 100
stores the new value in led_brightness_COLOUR
this value is later read by the led_handler to set the
corresponding duty cycles to allow fast interactive updating
to be used by change_led_colour
"""
global led_blue
factor = 100/255
led_blue.ChangeDutyCycle(amount*factor)
def change_led_colour(red_amount, green_amount, blue_amount):
"""
takes an rgb value as input
and converts it to duty cycles
and then applies that.
"""
global led_red, led_green, led_blue
# sets an RGB value for the led strip.
if red_amount < 0 or red_amount > 255:
# valid rgb values should be between 0 and 255
print('change_led_colour: invalid red value received: ' + str(red_amount))
return False
if green_amount < 0 or green_amount > 255:
# valid rgb values should be between 0 and 255
print('change_led_colour: invalid green value received: '+ str(green_amount))
return False
if blue_amount < 0 or blue_amount > 255:
# valid rgb values should be between 0 and 255
print('change_led_colour: invalid blue value received: ' + str(blue_amount))
return False
change_red(red_amount)
change_blue(blue_amount)
change_green(green_amount)
return True
def clear_screen():
"""fills the screen with a colour and draws the background"""
screen.fill(pygame_color_white)
draw_background()
def handle_events():
"""looks for things to do every tick"""
for event in pygame.event.get():
if event.type == KEYDOWN and event.key == K_ESCAPE:
exit_application()
# shutdown raspberry if shutdown pin is detected
if not GPIO.input(pin_shutdown):
shutdown_raspberry()
def exit_application():
""" exit the application with proper cleanup"""
logoff_sound.play()
time.sleep(3)
pygame.quit()
GPIO.cleanup()
sys.exit()
def shutdown_raspberry():
"""shutdown the system with proper cleanup"""
logoff_sound.play()
time.sleep(3)
pygame.quit()
GPIO.cleanup()
os.system("sudo shutdown -h now")
def enter_name():
"""asks for a name on screen and then returns that name"""
clear_screen()
print('text entry started')
name = ''
while True:
clear_screen()
# add all needed surfaces
highscore_surface = pygame_font_1.render('High Score!', True, pygame_font_main_color)
highscore_rectangle = highscore_surface.get_rect()
highscore_rectangle.topleft = (700, 350)
screen.blit(highscore_surface, highscore_rectangle)
textbox_text_surface = pygame_font_2.render('Enter Name:', True, pygame_font_main_color)
textbox_text_rectangle = textbox_text_surface.get_rect()
textbox_text_rectangle.topleft = (700, 500)
textbox_surface = pygame_font_2.render(str(name), True, pygame_font_main_color)
textbox_rectangle = textbox_surface.get_rect()
textbox_rectangle.topleft = (800, 580)
# draw everything
screen.blit(textbox_text_surface, textbox_text_rectangle)
screen.blit(textbox_surface, textbox_rectangle)
# add the border and logos
draw_border()
draw_logos()
# display everything
pygame.display.flip()
# handle key events
for event in pygame.event.get():
# read new keys for as long as the max length for the name hasnt been reached
if len(name) < max_name_length:
if event.type == KEYDOWN:
# if someone presses the erase key, erase 1 letter
if event.key == pygame.K_BACKSPACE:
name = name[:-1]
# if enter is pressed with at least 1 letter as the name,
# return the name
elif event.key == pygame.K_RETURN and len(name) > 0:
clear_screen()
return name
# if enter is pressed without any letters in the name,
# dont return a name. do nothing instead.
elif event.key == pygame.K_RETURN:
# do nothing
pass
# if escape is pressed, exit.
# this prevents lockups since the main game loop is not running inside this function.
elif event.key == pygame.K_ESCAPE:
exit_application()
# for any other key, add the letter to the name
# Caution: this includes all sorts of weird letters, control sequences, and other unprintable chars.
else:
name += event.unicode
# if someone presses the erase key, erase 1 letter
elif event.key == pygame.K_BACKSPACE:
name = name[:-1]
#
elif event.type == KEYDOWN and event.key == pygame.K_RETURN:
clear_screen()
return name
# this part of the code should be unreachable, and only here in case the event handling goes wrong horribly.
clear_screen()
name = 'Error'
return name
def check_highscores(time):
"""
check if a new highscore has been made
this is coded in a horrible way and adding more than 3 high scores with this sorta system would be too much work.
"""
global hs1_time, hs2_time, hs3_time, hs1_name, hs2_name, hs3_name
if time <= hs1_time:
print('new high score:'+str(time))
# make the second the third
hs3_time = hs2_time
hs3_name = hs2_name
# make the first the second
hs2_time = hs1_time
hs2_name = hs1_name
# new high score
hs1_time = time
hs1_name = enter_name()
return True
elif time <= hs2_time:
print('new second:'+str(time))
# make the second the third
hs3_time = hs2_time
hs3_name = hs2_name
# new second time
hs2_time = time
hs2_name = enter_name()
return True
elif time <= hs3_time:
print('new third:'+str(time))
hs3_time = time
hs3_name = enter_name()
return True
else:
return False
def draw_background():
"""draw background image"""
screen.blit(img_background_image, (0,0))
def draw_border():
"""draw rectangle border around everything"""
if cfg_borders_enabled:
pygame.draw.rect(screen, pygame_color_black, (500, 200, (screen_size_x-500*2), (screen_size_y-200*2)), 5)
def draw_logos():
"""draw logos"""
screen.blit(img_itlablogo_image, (screen_size_x-img_itlablogo_imagex-20, screen_size_y-img_itlablogo_imagey-20))
screen.blit(img_metalliclogo_image, (20, screen_size_y-img_metalliclogo_imagey-20))
def show_debug():
"""show debug information"""
print('#############')
print('Game running: ' + str(game_running))
print('Game ending: ' + str(game_ending))
print('-------------')
print('Pin status: ')
print('Start pin: ' + str(GPIO.input(pin_start)))
print('Stop pin: ' + str(GPIO.input(pin_stop)))
print('Error pin: ' + str(GPIO.input(pin_error)))
print('Shutdown pin: ' + str(GPIO.input(pin_shutdown)))
print('-------------')
print('Tick: ' + str(pygame.time.get_ticks()))
print('#############')
############################################
####### END OF FUNCTION DEFINITIONS ########
############################################
############################################
######### START OF INITIALISATION ##########
############################################
# signal handing setup for controlled exit via ctrl+c
signal.signal(signal.SIGINT, signal_handler)
# hide mouse from screen
pygame.mouse.set_visible(False)
# start fullscreen mode with defined screen geometrics
screen = pygame.display.set_mode((screen_size_x, screen_size_y), pygame.FULLSCREEN)
#pygame.display.set_caption('Heisser Draht') # not required for fullscreen application
# currently unused because its not needed
#screen = toggle_fullscreen()
# define image variables
img_itlablogo = 'img/itlablogo.png'
img_itlablogo_image = pygame.image.load(img_itlablogo).convert_alpha()
img_itlablogo_imagex = get_image_width(img_itlablogo)
img_itlablogo_imagey = get_image_height(img_itlablogo)
img_metalliclogo = 'img/metalliclogo.png'
img_metalliclogo_image = pygame.image.load(img_metalliclogo).convert_alpha()
img_metalliclogo_imagex = get_image_width(img_metalliclogo)
img_metalliclogo_imagey = get_image_height(img_metalliclogo)
img_background = 'img/bg.jpg'
img_background_image = pygame.image.load(img_background).convert()
img_background_imagex = get_image_width(img_background)
img_background_imagey = get_image_height(img_background)
# initialise led strip
led_init()
# set the start state
game_running = False
game_ending = False
# enable the one-shot for changing the LEDs
game_just_started = True
# set start inhibitor to false by default
pin_start_inhibit = False
############################################
########## END OF INITIALISATION ###########
############################################
############################################
######### START OF MAIN GAME LOOP ##########
############################################
while True:
# default actions to be done every cycle
clear_screen()
handle_events()
show_debug()
# State 0
if not game_running and not game_ending:
# one shot for changing the led colour
if game_just_started:
print('game just started, changed colour of leds to green')
change_led_colour(200, 0, 200)
game_just_started = False
# reset all the surfaces to be empty
time_surface = 0
errors_surface = 0
time_rectangle = 0
errors_rectangle = 0
# fill all surfaces again to contain the proper text
header_surface = pygame_font_1.render('Dr' + u'ü' + 'cken Sie Start!', True, pygame_font_main_color)
header_rectangle = header_surface.get_rect()
header_rectangle.topleft = (560, 270)
highscore_header_surface = pygame_font_2.render('Highscores:', True, pygame_font_main_color)
highscore_header_rectangle = highscore_header_surface.get_rect()
highscore_header_rectangle = (770, 410)
score1_surface = pygame_font_3.render(hs1_name + ": " + str(hs1_time/1000) + 's', True, pygame_color_yellow)
score1_rectangle = score1_surface.get_rect()
score1_rectangle.topleft = (820, 540)
score2_surface = pygame_font_3.render(hs2_name + ": " + str(hs2_time/1000) + 's', True, pygame_color_grey)
score2_rectangle = score2_surface.get_rect()
score2_rectangle.topleft = (820, 630)
score3_surface = pygame_font_3.render(hs3_name + ": " + str(hs3_time/1000) + 's', True, pygame_color_brown)
score3_rectangle = score3_surface.get_rect()
score3_rectangle.topleft = (820, 720)
# draw headline, highscore headline, the 3 scores, as well as the logos to the screen
screen.blit(header_surface, header_rectangle)
screen.blit(highscore_header_surface, highscore_header_rectangle)
screen.blit(score1_surface, score1_rectangle)
screen.blit(score2_surface, score2_rectangle)
screen.blit(score3_surface, score3_rectangle)
# event handling for if the start button is pushed
#wait for the user to press start button
if not GPIO.input(pin_start) and not pin_start_inhibit:
# change game state to running and not ending
game_running = True
game_ending = False
# set an inhibitor bit to stop the contact from possibly vibrating and
# causing a stop of the game immediately after start
pin_start_inhibit = True
timer_pin_start_inhibit_starttime = pygame.time.get_ticks()
# reset errors
errors = 0
error_added = False
error_cooldown_start = 0
# change the headline
header_surface = pygame_font_1.render('Spiel l' + u'ä' + 'uft!', True, pygame_font_main_color)
# start the timer
timer_game_running_start = pygame.time.get_ticks()
# change led colour to blue
change_led_colour(0, 0, 200)
# State 1
if game_running and not game_ending:
# reset the highscore check
highscore_checked = False
# if the start pin is inhibited and at least 3 seconds have passed,
# one shot: stop blocking the pin
if pin_start_inhibit:
if (pygame.time.get_ticks()-timer_pin_start_inhibit_starttime) > 3000:
pin_start_inhibit = False
# add errors if error pin is detected, only once per 500 ms
if not GPIO.input(pin_error):
if not error_added:
errors += 1
error_added = True
error_cooldown_start = pygame.time.get_ticks()
error_sound.play()
change_led_colour(200, 0, 0)
# if an error happened, error_added is set to true - which prohibits adding another error.
# after 500 ms error_added is set to false again.
# this provides a cooldown
if error_added and (pygame.time.get_ticks()-error_cooldown_start) > 500:
error_added = False
change_led_colour(0, 0, 200)
# calculate the current time
timer_game_running = (pygame.time.get_ticks() - timer_game_running_start) + (errors * time_per_error)
# fill the surface with the current time and errors
time_surface = pygame_font_2.render('Zeit: ' + str(timer_game_running/1000) + 's', True, pygame_font_main_color)
time_rectangle = time_surface.get_rect()
time_rectangle.topleft = (640, 480)
errors_surface = pygame_font_2.render('Fehler: ' + str(errors), True, pygame_font_main_color)
errors_rectangle = errors_surface.get_rect()
errors_rectangle.topleft = (640, 560)
clear_screen()
# draw errors, time and headline to the screen
screen.blit(errors_surface, errors_rectangle) #errors
screen.blit(time_surface, time_rectangle) #time
screen.blit(header_surface, header_rectangle) #header
fps = pygame_font_4.render(str(int(pygame_clock.get_fps())), True, pygame.Color('white'))
screen.blit(fps, (50, 50))
# if another push of start is detected (i.e. the game is ending!)
if not GPIO.input(pin_stop):
# change led colour to red
change_led_colour(0, 200, 200)
# reset the one shot latch for the end of game timer
timer_game_ending_started = False
# change the state to not running and ending
game_running = False
game_ending = True
# State 2
if not game_running and game_ending:
# check highscores only once when game ended
if not highscore_checked:
check_highscores(timer_game_running)
highscore_checked = True
# timer for ending the game after 5 seconds
if not timer_game_ending_started:
timer_game_ending_timout = pygame.time.get_ticks()
timer_game_ending_started = True
clear_screen()
# change the headline to game over
header_surface = pygame_font_1.render('Game over!', True, pygame_font_main_color)
# draw errors, time, and new headline to the screen
screen.blit(errors_surface, errors_rectangle) #errors
screen.blit(time_surface, time_rectangle) #time
screen.blit(header_surface, header_rectangle) #header
# if 5 seconds have passed since the game state changed to ending (see: timer_game_ending_timeout)
if ((pygame.time.get_ticks()-timer_game_ending_timout)/1000) > 5:
# reset game_just_started flag to enable the one shot led change in the first state
game_just_started = True
# change state
game_running = False
game_ending = False
# reset highscore one shot
highscore_checked = False
# reset timer started one shot
timer_game_ending_started = False
# unblock the start pin unconditionally
pin_start_inhibit = False
# State 3
if game_running and game_ending:
# something went horribly wrong...
# this state is never supposed to be reached
pass
# overlaying structure - this is to be drawn no matter what's below
draw_border()
draw_logos()
# flip everything, update display, do a tick
pygame.display.flip()
pygame.display.update()
pygame_clock.tick(pygame_fps)
############################################
########## END OF MAIN GAME LOOP ###########
############################################