#!/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() # 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') 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) # 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_green.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""" 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) 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) 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) 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 # display everything #pygame.display.flip() # 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 ########### ############################################