Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Visualizer modification #128

Merged
merged 2 commits into from
Nov 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions visualizer/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,3 @@ def GAME_BOARD_MARGIN_TOP(self) -> int:
@property
def VISUALIZE_HELD_ITEMS(self) -> bool:
return self.__VISUALIZE_HELD_ITEMS



300 changes: 275 additions & 25 deletions visualizer/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,32 +19,273 @@

class ByteVisualiser:

def __init__(self):
def __init__(self, end_time: int = -1, skip_start: bool = False, playback_speed: float = 1.0,
fullscreen: bool = False,
save_video: bool = False, loop_count: int = 1, turn_start: int = 0, turn_end: int = -1):
pygame.init()
self.config: Config = Config()
self.turn_logs: dict[str:dict] = {}
self.size: Vector = self.config.SCREEN_SIZE
self.tile_size: int = self.config.TILE_SIZE

self.screen: pygame.display = pygame.display.set_mode(self.size.as_tuple())
self.fullscreen: bool = fullscreen
self.screen: pygame.Surface = pygame.display.set_mode(self.size.as_tuple(),
pygame.FULLSCREEN if self.fullscreen else pygame.SHOWN)
self.adapter: Adapter = Adapter(self.screen)

self.clock: pygame.time.Clock = pygame.time.Clock()

self.tick: int = 0
self.tick: int = turn_start * self.config.NUMBER_OF_FRAMES_PER_TURN
self.turn_end: int = turn_end
self.bytesprite_factories: dict[int: Callable[[pygame.Surface], ByteSprite]] = {}
self.bytesprite_map: [[[ByteSprite]]] = list()

self.default_frame_rate: int = self.config.FRAME_RATE

self.playback_speed: int = 1
self.playback_speed: float = playback_speed
self.paused: bool = False
self.recording: bool = False
self.recording: bool = save_video

# Scale for video saving (division can be adjusted, higher division = lower quality)
self.scaled: tuple[int, int] = (self.size.x // 2, self.size.y // 2)
self.writer: cv2.VideoWriter = cv2.VideoWriter("out.mp4", cv2.VideoWriter_fourcc(*'mp4v'),
self.default_frame_rate, self.scaled)
self.end_time: int = end_time
self.skip_start: bool = skip_start
self.loop_count: int = loop_count

@property
def config(self) -> Config:
return self.__config

@config.setter
def config(self, config: Config) -> None:
if config is None or not isinstance(config, Config):
# Error messaging should look like the message below going forward
raise ValueError(
f'{self.__class__.__name__}.config must be a Config. It is a(n) {type(config)} with the value of {config}')
self.__config: Config = config

@property
def turn_logs(self) -> dict[str:dict]:
return self.__turn_logs

@turn_logs.setter
def turn_logs(self, turn_logs: dict[str:dict]) -> None:
if turn_logs is None or not isinstance(turn_logs, dict):
raise ValueError(
f'{self.__class__.__name__}.turn_logs must be a dict. It is a(n) {type(turn_logs)} with the value of {turn_logs}')
self.__turn_logs: dict = turn_logs

@property
def size(self) -> Vector:
return self.__size

@size.setter
def size(self, size: Vector) -> None:
if size is None or not isinstance(size, Vector):
raise ValueError(
f'{self.__class__.__name__}.size must be a Vector. It is a(n) {type(size)} with the value of {size}')
self.__size: Vector = size

@property
def tile_size(self) -> int:
return self.__tile_size

@tile_size.setter
def tile_size(self, tile_size: int) -> None:
if tile_size is None or not isinstance(tile_size, int):
raise ValueError(
f'{self.__class__.__name__}.tile_size must be an int. It is a(n) {type(tile_size)} with the value of {tile_size}')
self.__tile_size: int = tile_size

@property
def fullscreen(self) -> bool:
return self.__fullscreen

@fullscreen.setter
def fullscreen(self, fullscreen: bool) -> None:
if fullscreen is None or not isinstance(fullscreen, bool):
raise ValueError(
f'{self.__class__.__name__}.fullscreen must be a bool. It is a(n) {type(fullscreen)} with the value of {fullscreen}')
self.__fullscreen: bool = fullscreen

@property
def screen(self) -> pygame.Surface:
return self.__screen

@screen.setter
def screen(self, screen: pygame.Surface) -> None:
if screen is None or not isinstance(screen, pygame.Surface):
raise ValueError(
f'{self.__class__.__name__}.screen must be a pygame.Surface. It is a(n) {type(screen)} with the value of {screen}')
self.__screen: pygame.Surface = screen

@property
def adapter(self) -> Adapter:
return self.__adapter

@adapter.setter
def adapter(self, adapter: Adapter) -> None:
if adapter is None or not isinstance(adapter, Adapter):
raise ValueError(
f'{self.__class__.__name__}.adapter must be an Adapter. It is a(n) {type(adapter)} with the value of {adapter}')
self.__adapter: Adapter = adapter

@property
def clock(self) -> pygame.time.Clock:
return self.__clock

@clock.setter
def clock(self, clock: pygame.time.Clock) -> None:
if clock is None or not isinstance(clock, pygame.time.Clock):
raise ValueError(
f'{self.__class__.__name__}.clock must be a pygame.time.Clock. It is a(n) {type(clock)} with the value of {clock}')
self.__clock: pygame.time.Clock = clock

@property
def tick(self) -> int:
return self.__tick

@tick.setter
def tick(self, tick: int) -> None:
if tick is None or not isinstance(tick, int):
raise ValueError(
f'{self.__class__.__name__}.tick must be an int. It is a(n) {type(tick)} with the value of {tick}')
self.__tick: int = tick

@property
def turn_end(self) -> int:
return self.__turn_end

@turn_end.setter
def turn_end(self, turn_end: int) -> None:
if turn_end is None or not isinstance(turn_end, int):
raise ValueError(
f'{self.__class__.__name__}.turn_end must be an int. It is a(n) {type(turn_end)} with the value of {turn_end}')
self.__turn_end: int = turn_end

@property
def bytesprite_factories(self) -> dict[int: Callable[[pygame.Surface], ByteSprite]]:
return self.__bytesprite_factories

@bytesprite_factories.setter
def bytesprite_factories(self, bytesprite_factories: dict) -> None:
if bytesprite_factories is None or not isinstance(bytesprite_factories, dict):
raise ValueError(
f'{self.__class__.__name__}.bytesprite_factories must be a dict. It is a(n) {type(bytesprite_factories)} with the value of {bytesprite_factories}')
self.__bytesprite_factories: dict = bytesprite_factories

@property
def bytesprite_map(self) -> list:
return self.__bytesprite_map

@bytesprite_map.setter
def bytesprite_map(self, bytesprite_map: list) -> None:
if bytesprite_map is None or not isinstance(bytesprite_map, list):
raise ValueError(
f'{self.__class__.__name__}.bytesprite_map must be a list. It is a(n) {type(bytesprite_map)} with the value of {bytesprite_map}')
self.__bytesprite_map: list = bytesprite_map

@property
def default_frame_rate(self) -> int:
return self.__default_frame_rate

@default_frame_rate.setter
def default_frame_rate(self, default_frame_rate: int) -> None:
if default_frame_rate is None or not isinstance(default_frame_rate, int):
raise ValueError(
f'{self.__class__.__name__}.default_frame_rate must be an int. It is a(n) {type(default_frame_rate)} with the value of {default_frame_rate}')
self.__default_frame_rate: int = default_frame_rate

@property
def playback_speed(self) -> float:
return self.__playback_speed

@playback_speed.setter
def playback_speed(self, playback_speed: float) -> None:
if playback_speed is None or not isinstance(playback_speed, float):
raise ValueError(
f'{self.__class__.__name__}.playback_speed must be a float. It is a(n) {type(playback_speed)} with the value of {playback_speed}')
self.__playback_speed: float = playback_speed

@property
def paused(self) -> bool:
return self.__paused

@paused.setter
def paused(self, paused: bool) -> None:
if paused is None or not isinstance(paused, bool):
raise ValueError(
f'{self.__class__.__name__}.paused must be a bool. It is a(n) {type(paused)} with the value of {paused}')
self.__paused: bool = paused

@property
def recording(self) -> bool:
return self.__recording

@recording.setter
def recording(self, recording: bool) -> None:
if recording is None or not isinstance(recording, bool):
raise ValueError(
f'{self.__class__.__name__}.recording must be a bool. It is a(n) {type(recording)} with the value of {recording}')
self.__recording: bool = recording

@property
def scaled(self) -> tuple[int, int]:
return self.__scaled

@scaled.setter
def scaled(self, scaled: tuple[int, int]) -> None:
if scaled is None or not isinstance(scaled, tuple) or len(scaled) != 2 or any(
map(lambda x: not isinstance(x, int), scaled)):
raise ValueError(
f'{self.__class__.__name__}.scaled must be a tuple[int, int]. It is a(n) {type(scaled)} with the value of {scaled}')
self.__scaled: tuple[int, int] = scaled

@property
def writer(self) -> cv2.VideoWriter:
return self.__writer

@writer.setter
def writer(self, writer: cv2.VideoWriter) -> None:
if writer is None or not isinstance(writer, cv2.VideoWriter):
raise ValueError(
f'{self.__class__.__name__}.writer must be an cv2.VideoWriter. It is a(n) {type(writer)} with the value of {writer}')
self.__writer: cv2.VideoWriter = writer

@property
def end_time(self) -> int:
return self.__end_time

@end_time.setter
def end_time(self, end_time: int) -> None:
if end_time is None or not isinstance(end_time, int):
raise ValueError(
f'{self.__class__.__name__}.end_time must be an int. It is a(n) {type(end_time)} with the value of {end_time}')
self.__end_time: int = end_time

@property
def skip_start(self) -> bool:
return self.__skip_start

@skip_start.setter
def skip_start(self, skip_start: bool) -> None:
if skip_start is None or not isinstance(skip_start, bool):
raise ValueError(
f'{self.__class__.__name__}.skip_start must be a bool. It is a(n) {type(skip_start)} with the value of {skip_start}')
self.__skip_start: bool = skip_start

@property
def loop_count(self) -> int:
return self.__loop_count

@loop_count.setter
def loop_count(self, loop_count: int) -> None:
if loop_count is None or not isinstance(loop_count, int):
raise ValueError(
f'{self.__class__.__name__}.loop_count must be an int. It is a(n) {type(loop_count)} with the value of {loop_count}')
self.__loop_count: int = loop_count

def load(self) -> None:
self.turn_logs: dict = logs_to_dict()
Expand All @@ -60,11 +301,13 @@ def render(self, button_pressed: PlaybackButtons) -> bool:

if self.tick % self.config.NUMBER_OF_FRAMES_PER_TURN == 0:
# NEXT TURN
if self.turn_logs.get(f'turn_{self.tick // self.config.NUMBER_OF_FRAMES_PER_TURN + 1:04d}') is None:
turn: int = self.tick // self.config.NUMBER_OF_FRAMES_PER_TURN+1
if self.turn_logs.get(f'turn_{turn:04d}') is None or \
(self.turn_end != -1 and turn == self.turn_end):
return False
self.recalc_animation(self.turn_logs[f'turn_{self.tick // self.config.NUMBER_OF_FRAMES_PER_TURN + 1:04d}'])
self.recalc_animation(self.turn_logs[f'turn_{turn:04d}'])
self.adapter.recalc_animation(
self.turn_logs[f'turn_{self.tick // self.config.NUMBER_OF_FRAMES_PER_TURN + 1:04d}'])
self.turn_logs[f'turn_{turn:04d}'])

else:
# NEXT ANIMATION FRAME
Expand Down Expand Up @@ -111,11 +354,11 @@ def __playback_controls(self, button_pressed: PlaybackButtons) -> None:
if PlaybackButtons.PAUSE_BUTTON in button_pressed:
self.paused = not self.paused
if PlaybackButtons.NORMAL_SPEED_BUTTON in button_pressed:
self.playback_speed = 1
self.playback_speed = 1.0
if PlaybackButtons.FAST_SPEED_BUTTON in button_pressed:
self.playback_speed = 2
self.playback_speed = 2.0
if PlaybackButtons.FASTEST_SPEED_BUTTON in button_pressed:
self.playback_speed = 4
self.playback_speed = 4.0

# Method to deal with saving game to mp4 (called in render if save button pressed)
def save_video(self) -> None:
Expand Down Expand Up @@ -186,7 +429,8 @@ def __create_bytesprite(self, x: int, y: int, z: int, temp_tile: dict | None) ->
if len(self.bytesprite_factories) == 0:
raise ValueError(f'must provide bytesprite factories for visualization!')
# Check that a bytesprite template exists for current object type
factory_function: Callable[[pygame.Surface], ByteSprite] | None = self.bytesprite_factories.get(temp_tile['object_type'])
factory_function: Callable[[pygame.Surface], ByteSprite] | None = self.bytesprite_factories.get(
temp_tile['object_type'])
if factory_function is None:
raise ValueError(
f'Must provide a bytesprite for each object type! Missing object_type: {temp_tile["object_type"]}')
Expand All @@ -210,22 +454,24 @@ def postrender(self) -> None:
self.clock.tick(self.default_frame_rate * self.playback_speed)

def loop(self) -> None:
thread: Thread = Thread(target=self.load)
thread.start()
for _ in range(self.loop_count):
thread: Thread = Thread(target=self.load)
thread.start()

# Start Menu loop
in_phase: bool = True
self.__start_menu_loop(in_phase)
# Start Menu loop
in_phase: bool = True
if not self.skip_start:
self.__start_menu_loop(in_phase)

thread.join()
thread.join()

# Playback Menu loop
in_phase = True
self.__play_back_menu_loop(in_phase)
# Playback Menu loop
in_phase = True
self.__play_back_menu_loop(in_phase)

# Results
in_phase = True
self.__results_loop(in_phase)
# Results
in_phase = True
self.__results_loop(in_phase)

if self.recording:
self.writer.release()
Expand Down Expand Up @@ -279,6 +525,7 @@ def __play_back_menu_loop(self, in_phase: bool) -> None:
# Results loop method ran in loop method
def __results_loop(self, in_phase: bool) -> None:
self.adapter.results_load(self.turn_logs['results'])
ticks: int = 0
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT: sys.exit()
Expand All @@ -297,11 +544,14 @@ def __results_loop(self, in_phase: bool) -> None:
if self.recording:
self.save_video()

if not in_phase:
if not in_phase or (self.end_time != -1 and ticks >= self.end_time * math.floor(
self.default_frame_rate * self.playback_speed)):
break
self.clock.tick(math.floor(self.default_frame_rate * self.playback_speed))
ticks += 1
self.writer.release()


if __name__ == '__main__':
byte_visualiser: ByteVisualiser = ByteVisualiser()
byte_visualiser.loop()