2021-05-23 08:43:31 +02:00
|
|
|
from random import choice, randint
|
2021-05-21 14:00:26 +02:00
|
|
|
|
|
|
|
import project_constants as const
|
|
|
|
|
2021-05-23 05:50:49 +02:00
|
|
|
from assets.display_assets import blit_graphics
|
2021-05-23 13:38:16 +02:00
|
|
|
from algorithms.search import a_star
|
2021-05-21 14:00:26 +02:00
|
|
|
|
|
|
|
from minefield import Minefield
|
|
|
|
|
2021-05-23 18:02:35 +02:00
|
|
|
from objects.mines.mine_models.time_mine import TimeMine
|
2021-05-21 14:00:26 +02:00
|
|
|
|
|
|
|
from ui.ui_components_manager import UiComponentsManager
|
|
|
|
from ui.text_box import TextBox
|
|
|
|
from ui.button import Button
|
|
|
|
from ui.input_box import InputBox
|
|
|
|
|
|
|
|
|
|
|
|
class Game:
|
|
|
|
def __init__(self, turn=0, minefield=None, window=None):
|
|
|
|
# loading game settings
|
|
|
|
self.window = window if window is not None else const.SCREEN
|
|
|
|
|
|
|
|
# setting in game data
|
|
|
|
self.turn = turn
|
|
|
|
self.turn_on_which_last_action_was_taken = turn
|
|
|
|
self.goal = (0, 0)
|
|
|
|
|
|
|
|
# assigning minefield and agent to variables
|
|
|
|
self.minefield = minefield if minefield is not None else Minefield(const.MAP_RANDOM_10x10)
|
|
|
|
self.initial_minefield = self.minefield.__copy__()
|
|
|
|
self.agent = self.minefield.agent
|
|
|
|
self.agent_action = None
|
|
|
|
self.millisecond_timer = 0
|
|
|
|
self.delta_time = 0
|
|
|
|
self.action_timer = 0
|
|
|
|
self.action_delta_time = 0
|
|
|
|
|
|
|
|
# declaring and initializing gui components
|
|
|
|
# ui_component managers
|
|
|
|
self.in_game_gui_components_manager = UiComponentsManager()
|
|
|
|
self.game_over_gui_components_manager = UiComponentsManager()
|
|
|
|
|
|
|
|
# in game gui
|
|
|
|
self.input_box_row = InputBox((0, 0), (0, 0))
|
|
|
|
self.input_box_column = InputBox((0, 0), (0, 0))
|
|
|
|
self.button_auto = Button((0, 0), (0, 0))
|
|
|
|
self.button_random = Button((0, 0), (0, 0))
|
|
|
|
self.button_ok = Button((0, 0), (0, 0))
|
|
|
|
|
|
|
|
# game over screen
|
|
|
|
self.text_box_game_over = TextBox((0, 0), (0, 0))
|
|
|
|
self.button_try_again = Button((0, 0), (0, 0))
|
|
|
|
self.button_close = Button((0, 0), (0, 0))
|
|
|
|
|
|
|
|
# draws minefield and agent instances
|
|
|
|
def draw_minefield(self):
|
|
|
|
blit_graphics(self.minefield)
|
|
|
|
self.agent.update_and_draw(self.window, self.action_delta_time, self.minefield)
|
|
|
|
|
|
|
|
# draws menu
|
|
|
|
def run_in_game_menu_overlay(self, mouse_position, events):
|
|
|
|
# drawing and updating all gui components
|
|
|
|
self.in_game_gui_components_manager.run_all(self.window, mouse_position, events)
|
|
|
|
self.in_game_gui_components_manager.switch_selected_objects_with_arrow_keys(mouse_position, events)
|
|
|
|
|
|
|
|
# setting random goal if random button is clicked
|
|
|
|
if self.button_random.is_clicked(mouse_position, events):
|
|
|
|
self.goal = (randint(0, 9), randint(0, 9))
|
|
|
|
self.input_box_row.set_texts(user_input=str(self.goal[0]))
|
|
|
|
self.input_box_column.set_texts(user_input=str(self.goal[1]))
|
|
|
|
|
|
|
|
# if any input box is empty don't allow player to run game
|
|
|
|
self.button_ok.set_flags(is_active=not (self.input_box_row.empty() or self.input_box_column.empty()))
|
|
|
|
|
|
|
|
# updating goal if input boxes are filled and highlighting chosen tile destination
|
|
|
|
if not (self.input_box_row.empty() or self.input_box_column.empty()):
|
|
|
|
# updating goal
|
|
|
|
self.goal = (min(9, int(self.input_box_row.get_input())), min(9, int(self.input_box_column.get_input())))
|
|
|
|
|
|
|
|
# highlighting chosen tile destination
|
|
|
|
self.window.blit(const.HIGHLIGHT, const.get_tile_coordinates(self.goal))
|
|
|
|
|
|
|
|
# makes agent take next action
|
|
|
|
def agent_take_next_action(self, action_sequence):
|
|
|
|
# give agent an action if there is one
|
|
|
|
self.agent.take_action(self.agent_action)
|
|
|
|
|
|
|
|
# check what the next action is going to be
|
|
|
|
self.agent_action = action_sequence.pop(0)
|
|
|
|
|
|
|
|
# start animating agents next action
|
|
|
|
self.agent.animate(self.agent_action)
|
|
|
|
|
|
|
|
# updates in game time and timers
|
|
|
|
def update_time(self, number_of_seconds_since_last_tick):
|
|
|
|
self.millisecond_timer += number_of_seconds_since_last_tick / 1000
|
|
|
|
self.action_timer = self.millisecond_timer / const.TURN_INTERVAL
|
|
|
|
self.delta_time = number_of_seconds_since_last_tick / 1000
|
|
|
|
self.action_delta_time = self.delta_time / const.TURN_INTERVAL
|
|
|
|
|
|
|
|
# updates number of turns in game
|
|
|
|
def update_turns(self):
|
|
|
|
# if one turns time passed: updating number of turns
|
|
|
|
if self.millisecond_timer >= const.TURN_INTERVAL:
|
|
|
|
self.turn += 1
|
|
|
|
self.minefield.next_turn()
|
|
|
|
|
|
|
|
# resetting timer
|
|
|
|
self.millisecond_timer %= const.TURN_INTERVAL
|
|
|
|
|
|
|
|
# returns turn number
|
|
|
|
def get_turn_number(self):
|
|
|
|
return self.turn
|
|
|
|
|
2021-05-23 08:43:31 +02:00
|
|
|
# draws a random mine to disarm (in auto mode)
|
|
|
|
def set_random_mine_as_target(self):
|
|
|
|
self.goal = choice(self.minefield.get_active_mines()).position
|
|
|
|
|
|
|
|
# display new destination
|
|
|
|
self.input_box_row.set_texts(user_input=str(self.goal[0]))
|
|
|
|
self.input_box_column.set_texts(user_input=str(self.goal[1]))
|
|
|
|
|
|
|
|
# prevents highlighting input_box_row,
|
|
|
|
# couldn't find any better solution w/o major Game class changes
|
|
|
|
self.input_box_row.set_is_selected(False)
|
|
|
|
|
2021-05-21 14:00:26 +02:00
|
|
|
# gets action sequence for agent
|
2021-05-23 08:43:31 +02:00
|
|
|
def get_action_sequence(self, target_type: str = "tile"):
|
2021-05-21 14:00:26 +02:00
|
|
|
return a_star.graphsearch(
|
|
|
|
initial_state=a_star.State(
|
|
|
|
row=self.agent.row,
|
|
|
|
column=self.agent.column,
|
|
|
|
direction=self.agent.direction
|
|
|
|
),
|
|
|
|
minefield=self.minefield,
|
2021-05-23 08:43:31 +02:00
|
|
|
target_type=target_type,
|
2021-05-21 14:00:26 +02:00
|
|
|
tox=self.goal[0],
|
|
|
|
toy=self.goal[1]
|
|
|
|
)
|
|
|
|
|
|
|
|
# initializes attributes before game loop begins
|
|
|
|
def initialize_before_game_loop(self):
|
|
|
|
self.agent_action = None
|
|
|
|
self.millisecond_timer = 0
|
|
|
|
self.action_timer = 0
|
|
|
|
self.delta_time = 0
|
|
|
|
self.action_delta_time = 0
|
|
|
|
|
|
|
|
for component in self.in_game_gui_components_manager.selectable_ui_components:
|
|
|
|
component.set_flags(is_active=False)
|
|
|
|
|
|
|
|
# gives agent last action
|
|
|
|
def agent_take_last_action(self):
|
|
|
|
self.agent.take_action(self.agent_action)
|
|
|
|
|
|
|
|
# cleans up after game loop ends
|
|
|
|
def cleanup_after_game_loop(self):
|
|
|
|
self.agent.update_and_draw(self.window, self.action_delta_time, self.minefield)
|
|
|
|
self.agent.reset_actions()
|
|
|
|
|
|
|
|
for comp in self.in_game_gui_components_manager.selectable_ui_components:
|
|
|
|
comp.set_flags(is_active=True)
|
|
|
|
|
|
|
|
if not any([comp.is_selected for comp in self.in_game_gui_components_manager.selectable_ui_components]):
|
|
|
|
self.input_box_row.set_is_selected(True)
|
|
|
|
|
|
|
|
# returns True if agents should take an action in this turn, or False otherwise
|
|
|
|
def agent_should_take_next_action(self, action_sequence):
|
|
|
|
number_of_turns_since_last_action = self.turn - self.turn_on_which_last_action_was_taken
|
|
|
|
agent_action_cost = 1 if self.agent_action != const.Action.GO else self._get_next_tiles_entering_cost()
|
|
|
|
|
|
|
|
if number_of_turns_since_last_action >= agent_action_cost and any(action_sequence):
|
|
|
|
self.turn_on_which_last_action_was_taken = self.turn
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
# returns True if agent made all actions he was instructed to do, or False otherwise
|
|
|
|
def agent_made_all_actions(self, action_sequence):
|
|
|
|
number_of_turns_since_last_action = self.turn - self.turn_on_which_last_action_was_taken
|
|
|
|
agent_action_cost = 1 if self.agent_action != const.Action.GO else self._get_next_tiles_entering_cost()
|
|
|
|
|
|
|
|
if number_of_turns_since_last_action >= agent_action_cost and not any(action_sequence):
|
|
|
|
self.turn_on_which_last_action_was_taken = self.turn
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
# runs game over screen
|
|
|
|
def run_game_over_screen(self, mouse_position, events):
|
|
|
|
self.window.fill((255, 255, 255))
|
|
|
|
self.game_over_gui_components_manager.run_all(self.window, mouse_position, events)
|
|
|
|
|
|
|
|
# resetting minefield to the original state
|
|
|
|
if self.button_try_again.is_clicked(mouse_position, events):
|
|
|
|
del self.minefield
|
|
|
|
del self.agent
|
|
|
|
self.minefield = self.initial_minefield.__copy__()
|
|
|
|
self.agent = self.initial_minefield.agent
|
|
|
|
|
2021-05-23 06:14:12 +02:00
|
|
|
# initializes old ui components and assigns them to attributes
|
2021-05-21 14:00:26 +02:00
|
|
|
def initialize_gui_components(self):
|
|
|
|
# calculating in game gui coordinates
|
|
|
|
gui_width = const.V_SIDE_MENU_WIDTH
|
|
|
|
ib_height, bt_height = const.V_INPUT_BOX_HEIGHT, const.V_BUTTON_HEIGHT
|
|
|
|
gui_x = const.V_TILE_AREA_WIDTH + 2 * const.V_SCREEN_PADDING + const.V_NUMBER_PADDING
|
|
|
|
gui_y = const.SCREEN.get_height() / 2 - (2 * ib_height + 2 * bt_height + 30) / 2
|
|
|
|
|
|
|
|
# creating in game gui components
|
|
|
|
self.input_box_row = InputBox(
|
|
|
|
position=(gui_x, gui_y),
|
|
|
|
dimensions=(gui_width, ib_height),
|
|
|
|
text="row",
|
|
|
|
box_color=(100, 200, 100),
|
|
|
|
bottom_strip_color=(120, 220, 120),
|
|
|
|
inner_box_color=(120, 220, 120),
|
|
|
|
outline_color=(80, 180, 80),
|
|
|
|
outline_additional_pixel=True,
|
|
|
|
valid_input_characters="1234567890",
|
|
|
|
input_centered=True,
|
|
|
|
clear_input_on_click=True
|
|
|
|
)
|
|
|
|
|
|
|
|
self.input_box_column = InputBox(
|
|
|
|
position=(gui_x, gui_y + ib_height + 10),
|
|
|
|
dimensions=(gui_width, ib_height),
|
|
|
|
text="column",
|
|
|
|
box_color=(100, 200, 100),
|
|
|
|
bottom_strip_color=(120, 220, 120),
|
|
|
|
inner_box_color=(120, 220, 120),
|
|
|
|
outline_color=(80, 180, 80),
|
|
|
|
outline_additional_pixel=True,
|
|
|
|
valid_input_characters="1234567890",
|
|
|
|
input_centered=True,
|
|
|
|
clear_input_on_click=True
|
|
|
|
)
|
|
|
|
|
|
|
|
self.button_auto = Button(
|
|
|
|
position=(gui_x, gui_y + 2 * ib_height + 20),
|
|
|
|
dimensions=(gui_width, bt_height),
|
|
|
|
text="auto",
|
|
|
|
box_color=(100, 200, 100),
|
|
|
|
outline_color=(80, 180, 80),
|
|
|
|
outline_additional_pixel=True
|
|
|
|
)
|
|
|
|
|
|
|
|
self.button_random = Button(
|
|
|
|
position=(gui_x, gui_y + 2 * ib_height + bt_height + 30),
|
|
|
|
dimensions=(gui_width, bt_height),
|
|
|
|
text="random",
|
|
|
|
box_color=(100, 200, 100),
|
|
|
|
outline_color=(80, 180, 80),
|
|
|
|
outline_additional_pixel=True
|
|
|
|
)
|
|
|
|
|
|
|
|
self.button_ok = Button(
|
|
|
|
position=(gui_x, gui_y + 2 * ib_height + 2 * bt_height + 40),
|
|
|
|
dimensions=(gui_width, bt_height),
|
|
|
|
text="ok",
|
|
|
|
box_color=(100, 200, 100),
|
|
|
|
outline_color=(80, 180, 80),
|
|
|
|
outline_additional_pixel=True
|
|
|
|
)
|
|
|
|
|
|
|
|
gui_list = [self.input_box_row, self.input_box_column, self.button_auto, self.button_random, self.button_ok]
|
|
|
|
self.in_game_gui_components_manager = UiComponentsManager(gui_list)
|
|
|
|
|
|
|
|
# creating game over gui components
|
|
|
|
self.text_box_game_over = TextBox(position=(150, 200), dimensions=(500, 100), text="Game Over")
|
|
|
|
self.button_try_again = Button(position=(350, 350), dimensions=(100, 50), text="Try again")
|
|
|
|
self.button_close = Button(position=(350, 450), dimensions=(100, 50), text="Close")
|
|
|
|
|
|
|
|
gui_list = [self.text_box_game_over, self.button_try_again, self.button_close]
|
|
|
|
self.game_over_gui_components_manager = UiComponentsManager(gui_list, select_first_item=False)
|
|
|
|
|
|
|
|
# returns true if there is a time mine that went out of time
|
|
|
|
def time_mine_exploded(self):
|
|
|
|
# auxiliary function that checks if a tile contains a time mine which timer went to 0
|
|
|
|
for i in range(const.V_GRID_HOR_TILES):
|
|
|
|
for j in range(const.V_GRID_VER_TILES):
|
|
|
|
mine = self.minefield.matrix[i][j].mine
|
|
|
|
if mine is not None and isinstance(mine, TimeMine) and mine.timer <= 0:
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
# sets all in game components is_active flags to a given value
|
|
|
|
def set_is_active_flag_for_all_in_game_gui_components(self, is_active):
|
|
|
|
for component in self.in_game_gui_components_manager.selectable_ui_components:
|
|
|
|
component.set_flags(is_active=is_active)
|
|
|
|
|
|
|
|
# returns cost of entering tile in front of the agent
|
|
|
|
def _get_next_tiles_entering_cost(self):
|
|
|
|
row, column = self._get_next_agent_position()
|
|
|
|
|
|
|
|
return self.minefield.matrix[row][column].cost.value
|
|
|
|
|
|
|
|
# returns position in front of agent
|
|
|
|
def _get_next_agent_position(self):
|
|
|
|
# heading either up or down
|
|
|
|
if self.agent_action == const.Action.GO and self.agent.direction.value % 2 == 0:
|
|
|
|
return max(0, min(9, self.agent.row + self.agent.direction.value - 1)), self.agent.column
|
|
|
|
|
|
|
|
# heading either left or right
|
|
|
|
else:
|
|
|
|
return self.agent.row, max(0, min(9, self.agent.column - self.agent.direction.value + 2))
|