refactored game into Game class for better maintainability

This commit is contained in:
Simon Pirkelmann 2021-09-05 23:37:47 +02:00
parent 945833c8ac
commit 9eea9bd245

View File

@ -1,8 +1,7 @@
import numpy as np import numpy as np
import random import random
import pygame import pygame
import time
import copy
import os import os
BLACK = np.array([0, 0, 0], dtype=np.uint8) BLACK = np.array([0, 0, 0], dtype=np.uint8)
@ -14,27 +13,12 @@ GREEN = np.array([0, 255, 0], dtype=np.uint8)
pygame.init() pygame.init()
#os.environ['SDL_VIDEO_WINDOW_POS'] = '1920, 280'
pygame.font.init() # you have to call this at the start, pygame.font.init() # you have to call this at the start,
# if you want to use this module. # if you want to use this module.
myfont = pygame.font.SysFont('Comic Sans MS', 55) myfont = pygame.font.SysFont('Comic Sans MS', 55)
myfont_small = pygame.font.SysFont('Comic Sans MS', 45) 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) P0_text = myfont.render('P0', False, tuple(BLACK))
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))
tiledt = np.dtype([('x', np.uint8), ('y', np.uint8), ('color', np.uint8, 3), ('star', np.bool)]) 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: class Board:
valid_colors = [WHITE, RED, BLUE] 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) self.tiles = np.zeros((dim_y, dim_x), dtype=tiledt)
for x in range(dim_x): for x in range(dim_x):
for y in range(dim_y): for y in range(dim_y):
self.tiles[y, x]['x'] = x self.tiles[y, x]['x'] = x
self.tiles[y, x]['y'] = y self.tiles[y, x]['y'] = y
self.tiles[y, x]['color'] = random.choice(Board.valid_colors) 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)) 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) 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)) 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) (x * scale_fac, y * scale_fac, scale_fac, scale_fac), 1)
if self.tiles[y, x]['star']: if self.tiles[y, x]['star']:
board_surf.blit(star_surf, (x * scale_fac, y * scale_fac, scale_fac, scale_fac)) 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 return board_surf
def __repr__(self): # def __repr__(self):
s = '' # s = ''
for y in range(self.tiles.shape[0]): # for y in range(self.tiles.shape[0]):
for x in range(self.tiles.shape[1]): # for x in range(self.tiles.shape[1]):
if (x,y) == (self.robot.x, self.robot.y): # if (x,y) == (self.robot.x, self.robot.y):
s += self.robot.orientation # s += self.robot.orientation
else: # else:
s += '.' # s += '.'
s += '\n' # s += '\n'
return s # return s
class Robot: class Robot:
@ -121,6 +96,14 @@ class Robot:
angle = {'>': 0, '^': np.pi/2, '<': np.pi, 'v': 3*np.pi/2}[self.orientation] angle = {'>': 0, '^': np.pi/2, '<': np.pi, 'v': 3*np.pi/2}[self.orientation]
return np.rad2deg(angle) 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): def __repr__(self):
return f"({self.y}, {self.x}) - {self.orientation}" return f"({self.y}, {self.x}) - {self.orientation}"
@ -137,7 +120,7 @@ class Command:
def __repr__(self): def __repr__(self):
return f"{self.action}: {self.color}" return f"{self.action}: {self.color}"
def render(self): def render(self, scale_fac):
cmd_surf = pygame.Surface((scale_fac, scale_fac)) cmd_surf = pygame.Surface((scale_fac, scale_fac))
cmd_surf.fill(tuple(self.color)) cmd_surf.fill(tuple(self.color))
@ -159,84 +142,51 @@ class Command:
return cmd_surf 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: class Program:
def __init__(self, robot, board, cmds): def __init__(self, robot, board, cmds):
self.cmds = cmds self.cmds = cmds
self.robot = robot self.robot = robot
self.board = board 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.state = 'input' self.prg_counter += 1
self.available_inputs = [Command('forward'), Command('left'), Command('right'), Command('P0'),
Command('-', color=RED), Command('-', color=BLUE), Command('-', color=WHITE)]
self.fullscreen = False
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)
pygame.time.wait(100)
self.run()
def run(self):
prg_counter = 0
self.render(prg_counter)
self.state = 'running'
while self.state == 'running':
cmd = self.cmds[prg_counter]
prg_counter += 1
# current position # current position
x = self.robot.x x = self.robot.x
@ -245,16 +195,17 @@ class Program:
# current tile the robot is on # current tile the robot is on
tile = self.board.tiles[y, x] tile = self.board.tiles[y, x]
# apply next instruction of the program
if np.all(cmd.color == WHITE) or np.all(cmd.color == tile['color']): if np.all(cmd.color == WHITE) or np.all(cmd.color == tile['color']):
# matching color -> execute command # matching color -> execute command
if cmd.action == 'forward': if cmd.action == 'forward':
ynew, xnew = r.get_forward_coordinates() ynew, xnew = self.robot.get_forward_coordinates()
r.x = xnew self.robot.x = xnew
r.y = ynew self.robot.y = ynew
elif cmd.action in {'left', 'right'}: elif cmd.action in {'left', 'right'}:
r.orientation = Robot.resulting_orientation[self.robot.orientation][cmd.action] self.robot.orientation = Robot.resulting_orientation[self.robot.orientation][cmd.action]
elif cmd.action == 'P0': elif cmd.action == 'P0':
prg_counter = 0 self.prg_counter = 0
else: else:
print("color not matching -> skipping command") print("color not matching -> skipping command")
@ -269,74 +220,45 @@ class Program:
print(f"clicked at pos = {pos}") print(f"clicked at pos = {pos}")
self.state = 'input' self.state = 'input'
self.render(prg_counter) # 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]):
if (not (0 <= r.x < self.board.tiles.shape[1])) or not (0 <= r.y < self.board.tiles.shape[0]): # robot leaves the board -> GAME OVER
print("GAME OVER") print("GAME OVER")
screen.blit(game_over_text, (50, 00)) return 'game_over'
pygame.display.update()
pygame.time.wait(1500) # robot collects star on new tile
self.state = 'input' tile = self.board.tiles[self.robot.y, self.robot.x]
else:
tile = self.board.tiles[r.y,r.x]
if tile['star']: if tile['star']:
tile['star'] = False tile['star'] = False
if all([not t['star'] for t in self.board.tiles.flatten()]): if all([not t['star'] for t in self.board.tiles.flatten()]):
print("YOU WON") print("YOU WON")
screen.blit(won_text, (50, 00)) return 'won'
pygame.display.update()
pygame.time.wait(1500)
self.state = 'input'
pygame.time.wait(100) # by default we continue in the running state
return 'running'
self.robot.x = self.initial_pos[0] def render(self, scale_fac, prg_counter_override=None):
self.robot.y = self.initial_pos[1] """Render the current program. This will render all commands and highlight the next command to execute
self.robot.orientation = self.initial_pos[2] (determined by self.prg_counter).
self.input_program()
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.
def render(self, prg_counter=None): :param scale_fac:
dx = 0 :param prg_counter_override:
dy = 0 :return:
screen.fill(tuple(BLACK)) """
board_surf = self.board.render() prg_counter = self.prg_counter
screen.blit(board_surf, (dx, dy, dx + dimx * scale_fac, dy + dimy * scale_fac)) if prg_counter_override is not None:
prg_counter = prg_counter_override
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):
prg_surf = pygame.Surface((5 * scale_fac, 1 * scale_fac)) prg_surf = pygame.Surface((5 * scale_fac, 1 * scale_fac))
for i in range(5): for i in range(5):
if i < len(self.cmds): if i < len(self.cmds):
cmd = self.cmds[i] cmd = self.cmds[i]
cmd_surf = cmd.render() cmd_surf = cmd.render(scale_fac)
else: else:
cmd_surf = pygame.Surface((50,50)) cmd_surf = pygame.Surface((scale_fac,scale_fac))
cmd_surf.fill(WHITE) cmd_surf.fill(WHITE)
if prg_counter is not None and i == prg_counter: if prg_counter is not None and i == prg_counter:
pygame.draw.rect(cmd_surf, tuple(GREEN), (0, 0, scale_fac, scale_fac), 5) pygame.draw.rect(cmd_surf, tuple(GREEN), (0, 0, scale_fac, scale_fac), 5)
@ -344,16 +266,187 @@ class Program:
return prg_surf return prg_surf
r = Robot(x=1, y=1, orientation='v') def get_clicked_command(self, pos):
b = Board(dimx,dimy, r) print(f"clicked at pos = {pos}")
b.tiles[3,3]['star'] = True xoffset = pos[0] - self.screen_rect.x
b.tiles[3,2]['star'] = True clicked_cmd = xoffset * len(self.cmds) // self.screen_rect.width
print("clicked command = ", clicked_cmd)
print(b) 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) # TODO fix number of commands at 5
prg.input_program() self.cmds = [Command('forward'), Command('left', color=RED), Command('left', color=BLUE), Command('P0'), Command('-')]
prg.run() 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()