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,
# 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 = 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,184 +142,123 @@ 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.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'), # current tile the robot is on
Command('-', color=RED), Command('-', color=BLUE), Command('-', color=WHITE)] 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 # get all events
ev = pygame.event.get() ev = pygame.event.get()
# proceed events # proceed events
for event in ev: for event in ev:
# handle MOUSEBUTTONUP # handle MOUSEBUTTONUP
if event.type == pygame.MOUSEBUTTONUP: if event.type == pygame.MOUSEBUTTONUP:
pos = pygame.mouse.get_pos() 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: if pos[0] >= 325 and pos[0] <= 400 and pos[1] >= 600 and pos[1] <= 650:
print(f"clicked at pos = {pos}") print(f"clicked at pos = {pos}")
self.state = 'running' self.state = 'input'
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) # 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): def render(self, scale_fac, prg_counter_override=None):
prg_counter = 0 """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' :param scale_fac:
:param prg_counter_override:
while self.state == 'running': :return:
cmd = self.cmds[prg_counter] """
prg_counter += 1 prg_counter = self.prg_counter
if prg_counter_override is not None:
# current position prg_counter = prg_counter_override
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):
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()