From 6f69b471e5c65b2329b497807541b39b8d0d6a99 Mon Sep 17 00:00:00 2001 From: Patrick Tschuchnig Date: Fri, 13 Sep 2019 08:18:54 +0200 Subject: [PATCH] added next major revision with cleanups and documentation --- gametest_rev2.py | 769 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 769 insertions(+) create mode 100644 gametest_rev2.py diff --git a/gametest_rev2.py b/gametest_rev2.py new file mode 100644 index 0000000..a90bf6b --- /dev/null +++ b/gametest_rev2.py @@ -0,0 +1,769 @@ +#!/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: + + * 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. + + * 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 + + * 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. + +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 Up + Use: Start pin. If this is active, the game starts. + +pin_stop + + Type: Input, Pull Up + Use: Stop pin. If this is active, the game ends + +pin_error + + Type: Input, Pull Up + 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 Up + 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: + +32: pull up +33: pull up +36: pull up +37: pull up + +Make sure leds are on pull-up default pins +so when their state is undefined, the led +strips are switched off! + +22: pull up +18: pull up +16: pull up + +Credits: +Based on a script made by TODO: source, although it has been heavily altered +Changes made by: Clima Philip, Krajnc Moris, Glantschnig Raphael + +Hosted on: +https://git.wolfsberg.local/philip.clima/heisser_draht + +""" + + +############################################ +############# START OF IMPORTS ############# +############################################ +import signal +import sys +import os +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 + +# screen settings +screen_size_x = 1920 +screen_size_y = 1080 + +# name length for highscores +max_name_length = 10 + +# preset highscores +hs1_name = "Fritz" +hs2_name = "Bernd" +hs3_name = "Max" +hs1_time = 100 +hs2_time = 200 +hs3_time = 300 + +############################################ +############ 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() + +# 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_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 toggle_fullscreen(): + """switches the game to fullscreen""" + screen = pygame.display.get_surface() + tmp = screen.convert() + caption = pygame.display.get_caption() + cursor = pygame.mouse.get_cursor() # Duoas 16-04-2007 + + screen_width, screen_height = screen.get_width(), screen.get_height() + flags = screen.get_flags() + bits = screen.get_bitsize() + + pygame.display.quit() + pygame.display.init() + + screen = pygame.display.set_mode((screen_width, screen_height), flags^FULLSCREEN, bits) + screen.blit(tmp, (0, 0)) + pygame.display.set_caption(*caption) + + pygame.key.set_mods(0) # HACK: work-a-round for a SDL bug?? + + pygame.mouse.set_cursor(*cursor) # Duoas 16-04-2007 + return screen + +def clear_screen(): + """fills the screen with a colour""" + screen.fill(pygame_color_white) + +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 GPIO.INPUT(pin_shutdown): + shutdown_raspberry() + +def exit_application(): + """ exit the application with proper cleanup""" + pygame.quit() + GPIO.cleanup() + sys.exit() + +def shutdown_raspberry(): + """shutdown the system with proper cleanup""" + 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, 250) + + 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, 400) + + textbox_surface = pygame_font_2.render(str(name), True, pygame_font_main_color) + textbox_rectangle = textbox_surface.get_rect() + textbox_rectangle.topleft = (800, 480) + + # 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 + # none of the following may be needed + # TODO: Testing + # 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_border(): + """draw logos""" + screen.blit(img_itlablogo_image, (screen_size_x-img_itlablogo_imagex-20, screen_size_y-img_itlablogo_imagey-20)) + # TODO: Metallic Logo + +def draw_logos(): + """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 show_debug(): + """show debug information""" + print('#############') + print('Game running: ' + str(game_running)) + print('Game ending: ' + str(game_ending)) + print('Pin status: ') + print('Start pin: ' + str(GPIO.INPUT(pin_start))) + print('Error pin: ' + str(GPIO.INPUT(pin_error))) + print('Shutdown pin: ' + str(GPIO.input(pin_shutdown))) + 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) + +# TODO: Metallic Logo einfügen +#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) + +# 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() + print(str(pygame.time.get_ticks())) + + # 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(0, 200, 0) + 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, 250) + + 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 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 GPIO.input(pin_error): + if not error_added: + errors += 1 + error_added = True + error_cooldown_start = pygame.time.get_ticks() + 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 GPIO.input(pin_stop): + # change led colour to red + change_led_colour(200, 0, 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 ########### +############################################