From 9eea9bd2458cf9e73d9b296c18dcb7248a2ef309 Mon Sep 17 00:00:00 2001 From: Simon Pirkelmann Date: Sun, 5 Sep 2021 23:37:47 +0200 Subject: [PATCH] refactored game into Game class for better maintainability --- gauss-turing/webserver/gauss_turing.py | 493 +++++++++++++++---------- 1 file changed, 293 insertions(+), 200 deletions(-) diff --git a/gauss-turing/webserver/gauss_turing.py b/gauss-turing/webserver/gauss_turing.py index 8c934e7..609a5b2 100644 --- a/gauss-turing/webserver/gauss_turing.py +++ b/gauss-turing/webserver/gauss_turing.py @@ -1,8 +1,7 @@ import numpy as np import random import pygame -import time -import copy + import os BLACK = np.array([0, 0, 0], dtype=np.uint8) @@ -14,27 +13,12 @@ GREEN = np.array([0, 255, 0], dtype=np.uint8) pygame.init() -#os.environ['SDL_VIDEO_WINDOW_POS'] = '1920, 280' - - -pygame.font.init() # you have to call this at the start, - # if you want to use this module. +pygame.font.init() # you have to call this at the start, +# if you want to use this module. myfont = pygame.font.SysFont('Comic Sans MS', 55) myfont_small = pygame.font.SysFont('Comic Sans MS', 45) -P0_text = myfont.render('P0', False, (0, 0, 0)) -game_over_text = myfont.render('GAME OVER', False, RED) -won_text = myfont.render('YOU WON', False, GREEN) -run_text = myfont.render('RUN', False, tuple(BLACK)) -stop_text = myfont_small.render('STOP', False, tuple(BLACK)) -random.seed(42) - -dimx = 7 -dimy = 4 - -scale_fac = 180 -screen = pygame.display.set_mode((dimx*scale_fac,dimy*scale_fac)) -#screen = pygame.display.set_mode((dimx*scale_fac+int(0.1*scale_fac),dimy*scale_fac+300)) +P0_text = myfont.render('P0', False, tuple(BLACK)) tiledt = np.dtype([('x', np.uint8), ('y', np.uint8), ('color', np.uint8, 3), ('star', np.bool)]) @@ -42,24 +26,17 @@ tiledt = np.dtype([('x', np.uint8), ('y', np.uint8), ('color', np.uint8, 3), ('s class Board: valid_colors = [WHITE, RED, BLUE] - def __init__(self, dim_x, dim_y, robot): + def __init__(self, dim_x, dim_y): self.tiles = np.zeros((dim_y, dim_x), dtype=tiledt) for x in range(dim_x): for y in range(dim_y): self.tiles[y, x]['x'] = x self.tiles[y, x]['y'] = y self.tiles[y, x]['color'] = random.choice(Board.valid_colors) - self.robot = robot - def render(self): + def render(self, scale_fac): + dimy, dimx = self.tiles.shape board_surf = pygame.Surface((dimx * scale_fac, dimy * scale_fac)) - robot_surf = pygame.Surface((scale_fac, scale_fac), pygame.SRCALPHA) - # Use the local coordinate system of the surface to draw the lines. - pygame.draw.lines(robot_surf, (0, 0, 0), True, [(0.75 * scale_fac, 0.5 * scale_fac), - (0.25 * scale_fac, 0.25 * scale_fac), - (0.25 * scale_fac, 0.75 * scale_fac)], 3) - # I rotate it so that the arrow is pointing to the right (0° is right). - robot_surf = pygame.transform.rotate(robot_surf, self.robot.get_angle()) star_surf = pygame.Surface((scale_fac, scale_fac), pygame.SRCALPHA) pygame.draw.circle(star_surf, YELLOW, (int(0.5 * scale_fac), int(0.5 * scale_fac)), int(0.25 * scale_fac)) @@ -72,21 +49,19 @@ class Board: (x * scale_fac, y * scale_fac, scale_fac, scale_fac), 1) if self.tiles[y, x]['star']: board_surf.blit(star_surf, (x * scale_fac, y * scale_fac, scale_fac, scale_fac)) - if (x, y) == (self.robot.x, self.robot.y): - board_surf.blit(robot_surf, (x * scale_fac, y * scale_fac, scale_fac, scale_fac)) return board_surf - def __repr__(self): - s = '' - for y in range(self.tiles.shape[0]): - for x in range(self.tiles.shape[1]): - if (x,y) == (self.robot.x, self.robot.y): - s += self.robot.orientation - else: - s += '.' - s += '\n' - return s + # def __repr__(self): + # s = '' + # for y in range(self.tiles.shape[0]): + # for x in range(self.tiles.shape[1]): + # if (x,y) == (self.robot.x, self.robot.y): + # s += self.robot.orientation + # else: + # s += '.' + # s += '\n' + # return s class Robot: @@ -121,6 +96,14 @@ class Robot: angle = {'>': 0, '^': np.pi/2, '<': np.pi, 'v': 3*np.pi/2}[self.orientation] return np.rad2deg(angle) + def render(self, scale_fac): + robot_surf = pygame.Surface((scale_fac, scale_fac), pygame.SRCALPHA) + pygame.draw.lines(robot_surf, (0, 0, 0), True, [(0.75 * scale_fac, 0.5 * scale_fac), + (0.25 * scale_fac, 0.25 * scale_fac), + (0.25 * scale_fac, 0.75 * scale_fac)], 3) + robot_surf = pygame.transform.rotate(robot_surf, self.get_angle()) + return robot_surf + def __repr__(self): return f"({self.y}, {self.x}) - {self.orientation}" @@ -137,7 +120,7 @@ class Command: def __repr__(self): return f"{self.action}: {self.color}" - def render(self): + def render(self, scale_fac): cmd_surf = pygame.Surface((scale_fac, scale_fac)) cmd_surf.fill(tuple(self.color)) @@ -159,184 +142,123 @@ class Command: return cmd_surf +class Programmer: + def __init__(self, prg): + self.prg = prg + self.available_inputs = [Command('forward'), Command('left'), Command('right'), Command('P0'), + Command('-', color=RED), Command('-', color=BLUE), Command('-', color=WHITE)] + self.command_to_edit = 0 + self.screen_rect = None + + def render(self, scale_fac): + """Render surface with possible inputs for the robot. + + :return: surface of the input commands + """ + inp_surf = pygame.Surface((len(self.available_inputs) * scale_fac, 1 * scale_fac)) + for i, inp in enumerate(self.available_inputs): + cmd_surf = inp.render(scale_fac) + inp_surf.blit(cmd_surf, (i * scale_fac, 0, scale_fac, scale_fac)) + return inp_surf + + def update_selected_command(self, pos): + print(f"clicked at pos = {pos}") + xoffset = pos[0] - self.screen_rect.x + selected_input_index = xoffset * len(self.available_inputs) // self.screen_rect.width + selected_input = self.available_inputs[selected_input_index] + + edited_command = self.prg.cmds[self.command_to_edit] + + print("command before edit = ", edited_command) + if selected_input.action != '-': + edited_command.action = selected_input.action + else: + edited_command.color = selected_input.color + print("command after edit = ", edited_command) + class Program: def __init__(self, robot, board, cmds): self.cmds = cmds self.robot = robot self.board = board + self.prg_counter = 0 + self.screen_rect = None - self.initial_pos = (self.robot.x, self.robot.y, self.robot.orientation) + def step(self): + cmd = self.cmds[self.prg_counter] + self.prg_counter += 1 - self.state = 'input' + # current position + x = self.robot.x + y = self.robot.y - self.available_inputs = [Command('forward'), Command('left'), Command('right'), Command('P0'), - Command('-', color=RED), Command('-', color=BLUE), Command('-', color=WHITE)] + # current tile the robot is on + tile = self.board.tiles[y, x] - self.fullscreen = False + # apply next instruction of the program + if np.all(cmd.color == WHITE) or np.all(cmd.color == tile['color']): + # matching color -> execute command + if cmd.action == 'forward': + ynew, xnew = self.robot.get_forward_coordinates() + self.robot.x = xnew + self.robot.y = ynew + elif cmd.action in {'left', 'right'}: + self.robot.orientation = Robot.resulting_orientation[self.robot.orientation][cmd.action] + elif cmd.action == 'P0': + self.prg_counter = 0 + else: + print("color not matching -> skipping command") - def input_program(self): - self.state = 'input' - - selected_cmd = 0 - self.render(selected_cmd) - - while self.state == 'input': # get all events ev = pygame.event.get() - # proceed events for event in ev: # handle MOUSEBUTTONUP if event.type == pygame.MOUSEBUTTONUP: pos = pygame.mouse.get_pos() - - if pos[0] >= 50 and pos[0] <= 50 + 250 and pos[1] >= 600 and pos[1] <= 650: - print(f"clicked at pos = {pos}") - selected_cmd = (pos[0] - 50)//50 - if pos[0] >= 50 and pos[0] <= 50 + 350 and pos[1] >= 700 and pos[1] <= 750: - print(f"clicked at pos = {pos}") - chosen_input = (pos[0] - 50)//50 - chosen_input_cmd = self.available_inputs[chosen_input] - if selected_cmd < len(self.cmds): - edited_cmd = self.cmds[selected_cmd] - if chosen_input_cmd.action is not '-': - edited_cmd.action = chosen_input_cmd.action - else: - edited_cmd.color = chosen_input_cmd.color - else: - self.cmds.append(copy.copy(chosen_input_cmd)) if pos[0] >= 325 and pos[0] <= 400 and pos[1] >= 600 and pos[1] <= 650: print(f"clicked at pos = {pos}") - self.state = 'running' - if event.type == pygame.KEYDOWN: - if event.key == pygame.K_x: - if not self.fullscreen: - os.environ['SDL_VIDEO_WINDOW_POS'] = '1920, 280' - screen = pygame.display.set_mode((dimx * scale_fac, dimy * scale_fac), - pygame.NOFRAME) - self.fullscreen = True - else: - os.environ['SDL_VIDEO_WINDOW_POS'] = '0, 0' - screen = pygame.display.set_mode((dimx * scale_fac, dimy * scale_fac)) - self.fullscreen = False - self.render(selected_cmd) + self.state = 'input' - pygame.time.wait(100) + # update state for new robot position + if (not (0 <= self.robot.x < self.board.tiles.shape[1])) or not (0 <= self.robot.y < self.board.tiles.shape[0]): + # robot leaves the board -> GAME OVER + print("GAME OVER") + return 'game_over' - self.run() + # robot collects star on new tile + tile = self.board.tiles[self.robot.y, self.robot.x] + if tile['star']: + tile['star'] = False + if all([not t['star'] for t in self.board.tiles.flatten()]): + print("YOU WON") + return 'won' + # by default we continue in the running state + return 'running' - def run(self): - prg_counter = 0 + def render(self, scale_fac, prg_counter_override=None): + """Render the current program. This will render all commands and highlight the next command to execute + (determined by self.prg_counter). - self.render(prg_counter) + The prg_counter_override can be used to highlight a different command instead. This is used during input mode + to highlight the command the user will edit. - self.state = 'running' - - while self.state == 'running': - cmd = self.cmds[prg_counter] - prg_counter += 1 - - # current position - x = self.robot.x - y = self.robot.y - - # current tile the robot is on - tile = self.board.tiles[y, x] - - if np.all(cmd.color == WHITE) or np.all(cmd.color == tile['color']): - # matching color -> execute command - if cmd.action == 'forward': - ynew, xnew = r.get_forward_coordinates() - r.x = xnew - r.y = ynew - elif cmd.action in {'left', 'right'}: - r.orientation = Robot.resulting_orientation[self.robot.orientation][cmd.action] - elif cmd.action == 'P0': - prg_counter = 0 - else: - print("color not matching -> skipping command") - - # get all events - ev = pygame.event.get() - # proceed events - for event in ev: - # handle MOUSEBUTTONUP - if event.type == pygame.MOUSEBUTTONUP: - pos = pygame.mouse.get_pos() - if pos[0] >= 325 and pos[0] <= 400 and pos[1] >= 600 and pos[1] <= 650: - print(f"clicked at pos = {pos}") - self.state = 'input' - - self.render(prg_counter) - - if (not (0 <= r.x < self.board.tiles.shape[1])) or not (0 <= r.y < self.board.tiles.shape[0]): - print("GAME OVER") - screen.blit(game_over_text, (50, 00)) - pygame.display.update() - pygame.time.wait(1500) - self.state = 'input' - else: - tile = self.board.tiles[r.y,r.x] - if tile['star']: - tile['star'] = False - - if all([not t['star'] for t in self.board.tiles.flatten()]): - print("YOU WON") - screen.blit(won_text, (50, 00)) - pygame.display.update() - pygame.time.wait(1500) - self.state = 'input' - - pygame.time.wait(100) - - self.robot.x = self.initial_pos[0] - self.robot.y = self.initial_pos[1] - self.robot.orientation = self.initial_pos[2] - self.input_program() - - - def render(self, prg_counter=None): - dx = 0 - dy = 0 - screen.fill(tuple(BLACK)) - board_surf = self.board.render() - screen.blit(board_surf, (dx, dy, dx + dimx * scale_fac, dy + dimy * scale_fac)) - - prg_surf = self.render_prg(prg_counter) - screen.blit(prg_surf, (dx, board_surf.get_height()+2*dy, prg_surf.get_width(), prg_surf.get_height())) - - inp_surf = self.render_inputs() - screen.blit(inp_surf, (dx, board_surf.get_height()+4*dy, inp_surf.get_width(), inp_surf.get_height())) - - btn_surf = pygame.Surface((80, 50)) - if self.state == 'input': - btn_surf.fill(tuple(GREEN)) - btn_surf.blit(run_text, (0, 10)) - elif self.state == 'running': - btn_surf.fill(tuple(RED)) - btn_surf.blit(stop_text, (0, 10)) - screen.blit(btn_surf, (325, board_surf.get_height()+2*scale_fac, btn_surf.get_height(), btn_surf.get_width())) - - pygame.display.update() - - def render_inputs(self): - inp_surf = pygame.Surface((len(self.available_inputs) * scale_fac, 1 * scale_fac)) - - for i, inp in enumerate(self.available_inputs): - cmd_surf = inp.render() - inp_surf.blit(cmd_surf, (i * scale_fac, 0, scale_fac, scale_fac)) - return inp_surf - - def render_prg(self, prg_counter=None): + :param scale_fac: + :param prg_counter_override: + :return: + """ + prg_counter = self.prg_counter + if prg_counter_override is not None: + prg_counter = prg_counter_override prg_surf = pygame.Surface((5 * scale_fac, 1 * scale_fac)) for i in range(5): if i < len(self.cmds): cmd = self.cmds[i] - cmd_surf = cmd.render() + cmd_surf = cmd.render(scale_fac) else: - cmd_surf = pygame.Surface((50,50)) + cmd_surf = pygame.Surface((scale_fac,scale_fac)) cmd_surf.fill(WHITE) if prg_counter is not None and i == prg_counter: pygame.draw.rect(cmd_surf, tuple(GREEN), (0, 0, scale_fac, scale_fac), 5) @@ -344,16 +266,187 @@ class Program: return prg_surf -r = Robot(x=1, y=1, orientation='v') -b = Board(dimx,dimy, r) -b.tiles[3,3]['star'] = True -b.tiles[3,2]['star'] = True - -print(b) + def get_clicked_command(self, pos): + print(f"clicked at pos = {pos}") + xoffset = pos[0] - self.screen_rect.x + clicked_cmd = xoffset * len(self.cmds) // self.screen_rect.width + print("clicked command = ", clicked_cmd) + return clicked_cmd -cmds = [Command('forward'), Command('left', color=RED), Command('left', color=BLUE), Command('P0')] +class Game: + def __init__(self, dimx, dimy, robotx, roboty): + self.robot = Robot(x=robotx, y=roboty, orientation='v') + self.board = Board(dimx, dimy) + self.board.tiles[3,3]['star'] = True + self.board.tiles[3,2]['star'] = True -prg = Program(r, b, cmds) -prg.input_program() -prg.run() + # TODO fix number of commands at 5 + self.cmds = [Command('forward'), Command('left', color=RED), Command('left', color=BLUE), Command('P0'), Command('-')] + self.state = 'input' + + self.prg = Program(self.robot, self.board, self.cmds) + + self.programmer = Programmer(self.prg) + + #self.scale_fac = 180 + self.scale_fac = 125 + self.beamer_mode = False + self.screen = pygame.display.set_mode((int(self.board.tiles.shape[1] * self.scale_fac * 1.1), + int((self.board.tiles.shape[0] + 2) * self.scale_fac * 1.2))) + + self.game_over_text = myfont.render('GAME OVER', False, RED) + self.won_text = myfont.render('YOU WON', False, GREEN) + self.run_text = myfont.render('RUN', False, tuple(BLACK)) + self.stop_text = myfont_small.render('STOP', False, tuple(BLACK)) + + # save initial state + self.initial_pos = (self.robot.x, self.robot.y, self.robot.orientation) + self.inital_board_tiles = self.board.tiles.copy() + + def render(self): + """Render the game screen. + + This will render the board and the robot. + Depending on the display mode it will also render the current program and the input commands for the robot. + + :param prg_counter: + :return: + """ + if self.beamer_mode: + dx = 0 + dy = 0 + else: + dx = int(0.05 * self.screen.get_width()) + dy = int(0.05 * self.screen.get_height()) + self.screen.fill(tuple(BLACK)) + + # render the board + board_surf = self.board.render(self.scale_fac) + + # render robot onto the board surface + robot_surf = self.robot.render(self.scale_fac) + board_surf.blit(robot_surf, (self.robot.x * self.scale_fac, self.robot.y * self.scale_fac, self.scale_fac, self.scale_fac)) + self.screen.blit(board_surf, (dx, dy, dx + self.board.tiles.shape[1] * self.scale_fac, dy + self.board.tiles.shape[0] * self.scale_fac)) + + if not self.beamer_mode: + # if we are not in beamer mode we also show the current program / inputs + + # render program + if self.state == 'input': + # in input mode we highlight the command which is selected for edit + prg_surf = self.prg.render(self.scale_fac, prg_counter_override=self.programmer.command_to_edit) + else: + # in other modes we render the current program counter + prg_surf = self.prg.render(self.scale_fac) + prg_surf = pygame.transform.scale(prg_surf, (self.screen.get_width() * 2 // 3, self.scale_fac * 2 // 3)) + self.prg.screen_rect = pygame.Rect((dx, board_surf.get_height() + 2 * dy, prg_surf.get_width(), prg_surf.get_height())) + self.screen.blit(prg_surf, self.prg.screen_rect) + + # render input fields and buttons + inp_surf = self.programmer.render(self.scale_fac) + inp_surf = pygame.transform.scale(inp_surf, (self.screen.get_width() * 2 // 3, self.scale_fac * 2 // 3)) + self.programmer.screen_rect = pygame.Rect((dx, board_surf.get_height() + prg_surf.get_height() + 3 * dy, inp_surf.get_width(), inp_surf.get_height())) + self.screen.blit(inp_surf, self.programmer.screen_rect) + + btn_surf = pygame.Surface((3 * self.scale_fac//2, self.scale_fac)) + self.btn_rect = pygame.Rect((2 * dx + prg_surf.get_width(), board_surf.get_height() + 2 * dy, + btn_surf.get_height(), btn_surf.get_width())) + if self.state == 'input': + btn_surf.fill(tuple(GREEN)) + btn_surf.blit(self.run_text, (0, 10)) + elif self.state == 'running': + btn_surf.fill(tuple(RED)) + btn_surf.blit(self.stop_text, (0, 10)) + self.screen.blit(btn_surf, self.btn_rect) + + # render messages + if self.state == 'game_over': + self.screen.blit(self.game_over_text, (50, 00)) + pygame.display.update() + pygame.time.wait(1500) + self.state = 'reset' + elif self.state == 'won': + self.screen.blit(self.won_text, (50, 00)) + pygame.display.update() + pygame.time.wait(1500) + self.state = 'reset' + + pygame.display.update() + + def process_inputs(self): + # proceed events + for event in pygame.event.get(): + # handle MOUSEBUTTONUP + if event.type == pygame.MOUSEBUTTONUP: + pos = pygame.mouse.get_pos() + + # select command to edit by the programmer + if self.prg.screen_rect is not None: + if self.prg.screen_rect.collidepoint(pos): + print(f"clicked at pos = {pos}") + self.programmer.command_to_edit = self.prg.get_clicked_command(pos) + + # set the selected command to a new value + if self.programmer.screen_rect is not None: + if self.programmer.screen_rect.collidepoint(pos): + self.programmer.update_selected_command(pos) + + # clicked RUN/STOP button + if self.btn_rect is not None and self.btn_rect.collidepoint(*pos): + print(f"clicked at pos = {pos}") + if self.state == 'running': + self.state = 'input' + elif self.state == 'input': + self.state = 'running' + elif event.type == pygame.KEYDOWN: + if event.key == pygame.K_x: + if not self.beamer_mode: + # switch to beamer mode + os.environ['SDL_VIDEO_WINDOW_POS'] = '1920, 280' + self.scale_fac = 180 + self.screen = pygame.display.set_mode((self.board.tiles.shape[1] * self.scale_fac, + self.board.tiles.shape[0] * self.scale_fac), + pygame.NOFRAME) + self.beamer_mode = True + else: + # switch to normal mode + os.environ['SDL_VIDEO_WINDOW_POS'] = '0, 0' + self.scale_fac = 125 + self.screen = pygame.display.set_mode((self.board.tiles.shape[1] * self.scale_fac, + self.board.tiles.shape[0] * self.scale_fac + 5 * self.scale_fac)) + self.beamer_mode = False + elif event.key == pygame.K_r: + # run program + self.state = 'running' + return self.state + + def reset(self): + self.prg.prg_counter = 0 + self.robot.x = self.initial_pos[0] + self.robot.y = self.initial_pos[1] + self.robot.orientation = self.initial_pos[2] + self.board.tiles = self.inital_board_tiles + return 'input' + + def run(self): + running = True + while running: + self.state = self.process_inputs() + if self.state == 'input': + pass + elif self.state == 'running': + self.state = self.prg.step() + elif self.state == 'reset': + self.state = self.reset() + else: + print("unknown state") + return + + self.render() + pygame.time.wait(100) + +if __name__ == "__main__": + random.seed(42) + game = Game(dimx=7, dimy=4, robotx=3, roboty=1) + game.run() \ No newline at end of file