From cfc6054f255f8e9bf3f5475c7c553623ca226085 Mon Sep 17 00:00:00 2001 From: Giovani Rodriguez Date: Thu, 15 Jul 2021 18:58:27 -0400 Subject: [PATCH] wip: add stack and piece communication --- config.yaml | 4 +- tetri5/entity.py | 36 ++++++++------ tetri5/online.py | 127 ++++++++++++++++++++++++++++++++--------------- tetri5/scene.py | 78 ++++++++++++++++++----------- 4 files changed, 160 insertions(+), 85 deletions(-) diff --git a/config.yaml b/config.yaml index 52830f8..9001ca7 100644 --- a/config.yaml +++ b/config.yaml @@ -48,10 +48,8 @@ position: score-value-player-2: [540, 40] lines-label-player-1: [100, 540] lines-label-player-2: [500, 540] - next-label-player-1: [340, 120] + next-label-player-1: [360, 120] next-piece-player-1: [360, 160] - next-label-player-2: [340, 420] - next-piece-player-2: [360, 460] spawn-piece-player-1: [-200, -60] spawn-piece-player-2: [200, -360] diff --git a/tetri5/entity.py b/tetri5/entity.py index f54d084..bb5a7ab 100644 --- a/tetri5/entity.py +++ b/tetri5/entity.py @@ -148,11 +148,13 @@ class Piece(Entity): self._applying_drop_delay = not self._applying_lock_delay - def draw(self, surface: pygame.Surface, well: Well = None, stack: "Stack" = None) -> None: + def draw(self, surface: pygame.Surface, well: Well = None, stack: "Stack" = None, ghost_piece_off: bool = False) -> None: # ghost piece - if well and stack: - for square in self._get_ghost_piece_points(well, stack): - pygame.draw.polygon(surface, pygame.Color(self._ghost_piece_color), square, max(self._tile_size // 6, 1)) # TODO add white to the yaml + if well and stack and not ghost_piece_off: + ghost_piece_points = self._get_ghost_piece_points(well, stack) + if ghost_piece_points is not None: + for square in ghost_piece_points: + pygame.draw.polygon(surface, pygame.Color(self._ghost_piece_color), square, max(self._tile_size // 6, 1)) # TODO add white to the yaml super().draw(surface) @@ -313,6 +315,7 @@ class Piece(Entity): def _get_ghost_piece_points(self, well: Well, stack: "Stack") -> List: current_points = copy.deepcopy(self._points) + prior_points = None while not well._collide(current_points) and not stack._collide(current_points): prior_points = copy.deepcopy(current_points) for square in current_points: @@ -369,7 +372,7 @@ class Stack(Entity): def add_piece(self, piece: Piece) -> None: self._points += piece._points - self._square_designs += [_SquareDesign(piece._color, piece._inner_border_color, piece._border_color) for _ in range(len(piece._points))] + self._square_designs += [SquareDesign(piece._color, piece._inner_border_color, piece._border_color) for _ in range(len(piece._points))] # TODO refactor into multiple functions def _complete_rows(self) -> int: @@ -417,7 +420,7 @@ class Stack(Entity): """ TODO description """ -class _SquareDesign: +class SquareDesign: def __init__(self, base_color: str, inner_color: str, outer_color: str): self.base_color = base_color self.inner_color = inner_color @@ -428,17 +431,20 @@ class _SquareDesign: """ class PieceGenerator: - _bucket_one = [] - _bucket_two = [] + _bucket = [] @classmethod - def get_piece(cls, position: Tuple, is_player_two: bool = False) -> Piece: - bucket = cls._bucket_one if not is_player_two else cls._bucket_two - if len(bucket) == 0: - cls._generate_bucket(bucket) + def get_piece(cls, position: Tuple) -> Piece: + if len(cls._bucket) == 0: + cls._generate_bucket(cls._bucket) - base_color, inner_border_color, outer_border_color = cls._get_piece_color(is_player_two) - return Piece(cls._get_piece_shape(bucket.pop()), position, base_color, inner_border_color, outer_border_color) + base_color, inner_border_color, outer_border_color = cls._get_piece_color() + return Piece(cls._get_piece_shape(cls._bucket.pop()), position, base_color, inner_border_color, outer_border_color) + + @classmethod + def get_opponent_piece(cls) -> Piece: + base_color, inner_border_color, outer_border_color = cls._get_piece_color(True) + return Piece(Piece.Z_SHAPE, (-250, -250), base_color, inner_border_color, outer_border_color) @classmethod def _generate_bucket(cls, bucket: List) -> None: @@ -467,7 +473,7 @@ class PieceGenerator: return None @classmethod - def _get_piece_color(cls, is_player_two: bool) -> Tuple: + def _get_piece_color(cls, is_player_two: bool = False) -> Tuple: random_number = random.randint(1, 3) player_mod = "player-1" if not is_player_two else "player-2" diff --git a/tetri5/online.py b/tetri5/online.py index 5f73a78..8f65bc7 100644 --- a/tetri5/online.py +++ b/tetri5/online.py @@ -3,18 +3,22 @@ import websockets import json import queue # refer to https://docs.python.org/3/library/queue.html for cross threading communication import uuid -from typing import Dict +from typing import Dict, List from threading import Thread class MultiplayerService(): _thread = None - _receive_piece_queue = queue.Queue() - _send_piece_queue = queue.Queue() - _receive_message_queue = queue.Queue() - _send_message_queue = queue.Queue() _current_game_id = None _client_id = str(uuid.uuid4()) + """ QUEUES """ + _receive_piece_queue = queue.Queue() + _send_piece_queue = queue.Queue() + _receive_stack_queue = queue.Queue() + _send_stack_queue = queue.Queue() + _receive_message_queue = queue.Queue() + _send_message_queue = queue.Queue() + WAIT_FOR_OPPONENT = "wait_for_opponent" START_GAME = "start_game" @@ -24,19 +28,20 @@ class MultiplayerService(): args=(_NetworkConnectionService.init(),)) thread.start() - @classmethod - def enter_game(cls, game_id: str) -> None: - cls._current_game_id = game_id - _NetworkConnectionService._join_game = True # TODO: change this to a function - + """ SEND """ @classmethod def send_piece(cls, piece: "PieceDto") -> None: cls._send_piece_queue.put(piece) + @classmethod + def send_stack(cls, stack: "StackDto") -> None: + cls._send_stack_queue.put(stack) + @classmethod def send_message(cls, message: str) -> None: cls._send_message_queue.put(message) + """ RECEIVE """ @classmethod def try_receive_piece(cls) -> "PieceDto": if cls._receive_piece_queue.empty(): @@ -46,6 +51,15 @@ class MultiplayerService(): cls._receive_piece_queue.task_done() return result + @classmethod + def try_receive_stack(cls) -> "StackDto": + if cls._receive_stack_queue.empty(): + return None + + result = cls._receive_stack_queue.get() + cls._receive_stack_queue.task_done() + return result + @classmethod def try_receive_message(cls) -> str: if cls._receive_message_queue.empty(): @@ -55,6 +69,12 @@ class MultiplayerService(): cls._receive_message_queue.task_done() return result + """ MISC """ + @classmethod + def enter_game(cls, game_id: str) -> None: + cls._current_game_id = game_id + _NetworkConnectionService._join_game = True # TODO: change this to a function + @classmethod def quit(cls) -> None: _NetworkConnectionService.close_connection() @@ -70,26 +90,8 @@ class _NetworkConnectionService(): async def init(cls) -> None: await cls._connect_to_server() await cls._run_network_loop() - - @classmethod - def close_connection(cls) -> None: - cls._is_closed = True - - @classmethod - async def _run_network_loop(cls) -> None: - while True: - await asyncio.sleep(16e-3) # TODO add clock tick instead - - await cls._try_enter_game() - await cls._try_send_piece() - await cls._try_send_message() - await cls._try_receive_message() - - # if conection is closed, exit loop - if cls._is_closed: - await cls._websocket.close() - break + """ NETWORK SEND """ @classmethod async def _try_enter_game(cls) -> None: if not cls._join_game: @@ -119,6 +121,24 @@ class _NetworkConnectionService(): await cls._websocket.send(json_message) MultiplayerService._send_piece_queue.task_done() + @classmethod + async def _try_send_stack(cls) -> None: + # if no messages to proccess, return + if MultiplayerService._send_stack_queue.empty(): + return + + # get next piece to send and send to server + stack = MultiplayerService._send_stack_queue.get() + + # construct json message + json_message = json.dumps({"action": "send_stack",\ + "clientId": MultiplayerService._client_id,\ + "gameId": MultiplayerService._current_game_id,\ + "stack": stack.__dict__}) + + await cls._websocket.send(json_message) + MultiplayerService._send_stack_queue.task_done() + @classmethod async def _try_send_message(cls) -> None: # if no messages to proccess, return @@ -131,11 +151,13 @@ class _NetworkConnectionService(): await cls._websocket.send(message) MultiplayerService._send_message_queue.task_done() + """ NETWORK RECEIVE """ + # todo refactor @classmethod async def _try_receive_message(cls) -> None: try: task = cls._pending_receive_task or asyncio.create_task(cls._websocket.recv()) - done, pending = await asyncio.wait({task}, timeout=8e-3) # TODO experiment with the timeout + done, pending = await asyncio.wait({task}, timeout=4e-3) # TODO experiment with the timeout if task in done: json_str = await task @@ -145,15 +167,14 @@ class _NetworkConnectionService(): return data = json.loads(json_str) - print(data) - if data["type"] == "wait_for_opponent": MultiplayerService._receive_message_queue.put(MultiplayerService.WAIT_FOR_OPPONENT) if data["type"] == "start_game": MultiplayerService._receive_message_queue.put(MultiplayerService.START_GAME) if data["type"] == "receive_piece": - print("Receive a piece!") MultiplayerService._receive_piece_queue.put(PieceDto.create(data["piece"])) + if data["type"] == "receive_stack": + MultiplayerService._receive_stack_queue.put(StackDto.create(data["stack"])) if data["type"] == "exit_game": print("Exit the game!") cls.close_connection() @@ -162,7 +183,24 @@ class _NetworkConnectionService(): elif len(pending): cls._pending_receive_task = pending.pop() finally: - pass # TODO handle connection closed exception + pass # TODO handle connection closed exception and attempt to reconnect + + """ MISC """ + @classmethod + async def _run_network_loop(cls) -> None: + while True: + await asyncio.sleep(2e-3) # TODO add clock tick instead + + await cls._try_enter_game() + await cls._try_send_piece() + await cls._try_send_stack() + await cls._try_send_message() + await cls._try_receive_message() + + # if conection is closed, exit loop + if cls._is_closed: + await cls._websocket.close() + break @classmethod async def _connect_to_server(cls) -> None: @@ -172,14 +210,25 @@ class _NetworkConnectionService(): # https://stackoverflow.com/a/58993145/11512104 print("Connected to server...") # TODO replace with logging + @classmethod + def close_connection(cls) -> None: + cls._is_closed = True + # DTOs class PieceDto(): - def __init__(self, x: int, y: int, type_: str) -> None: - self.x = x - self.y = y - self.type = type_ + def __init__(self, points: List, center: List) -> None: + self.points = points + self.center = center @staticmethod def create(data: Dict) -> "PieceDto": - return PieceDto(data["x"], data["y"], data["type"]) \ No newline at end of file + return PieceDto(data["points"], data["center"]) + +class StackDto(): + def __init__(self, points: List) -> None: + self.points = points + + @staticmethod + def create(data: Dict) -> "StackDto": + return StackDto(data["points"]) \ No newline at end of file diff --git a/tetri5/scene.py b/tetri5/scene.py index b139bc3..3c373a8 100644 --- a/tetri5/scene.py +++ b/tetri5/scene.py @@ -5,10 +5,10 @@ from tetri5.util import ConfigurationManager from tetri5.util import TextGenerator from tetri5.util import Controller from tetri5.util import SoundManager -from tetri5.entity import Well +from tetri5.entity import SquareDesign, Well from tetri5.entity import Stack from tetri5.entity import PieceGenerator -from tetri5.online import MultiplayerService +from tetri5.online import * """ TODO @@ -292,22 +292,18 @@ class MultiplayerScene(Scene): # next positions self._next_label_player_one_pos = ConfigurationManager.get("position", "next-label-player-1") - self._next_label_player_two_pos = ConfigurationManager.get("position", "next-label-player-2") # piece positions self._next_piece_player_one_pos = ConfigurationManager.get("position", "next-piece-player-1") - self._next_piece_player_two_pos = ConfigurationManager.get("position", "next-piece-player-2") self._spawn_piece_shift_player_one = ConfigurationManager.get("position", "spawn-piece-player-1") self._spawn_piece_shift_player_two = ConfigurationManager.get("position", "spawn-piece-player-2") # entities self._next_piece_player_one = PieceGenerator.get_piece(self._next_piece_player_one_pos) - self._next_piece_player_two = PieceGenerator.get_piece(self._next_piece_player_two_pos, True) self._current_piece_player_one = None self._current_piece_player_two = None self._change_scence = change_scene - MultiplayerService.init() def draw(self, surface: pygame.Surface) -> None: surface.fill(self._background_color) @@ -322,11 +318,9 @@ class MultiplayerScene(Scene): if self._current_piece_player_one is not None: self._current_piece_player_one.draw(surface, self._well_player_one, self._stack_player_one) if self._current_piece_player_two is not None: - self._current_piece_player_two.draw(surface, self._well_player_two, self._stack_player_two) + self._current_piece_player_two.draw(surface, self._well_player_two, self._stack_player_two, True) if self._next_piece_player_one is not None: self._next_piece_player_one.draw(surface) - if self._next_piece_player_two is not None: - self._next_piece_player_two.draw(surface) # wells if self._well_player_one is not None: @@ -345,16 +339,37 @@ class MultiplayerScene(Scene): TextGenerator.draw("Lines 0000", self._lines_label_player_two_pos, surface) # next - TextGenerator.draw("P1 NXT", self._next_label_player_one_pos, surface) - TextGenerator.draw("P2 NXT", self._next_label_player_two_pos, surface) + TextGenerator.draw("NEXT", self._next_label_player_one_pos, surface) def update(self, elapsed_time: int) -> None: self._update_piece_player_one(elapsed_time) - self._update_piece_player_two(elapsed_time) + self._update_piece_player_two() - if self._stack_player_one: + if self._stack_player_one is not None: self._stack_player_one.update(elapsed_time) + if self._stack_player_two is not None: + # get opponent stack from server and modify for current client + opponent_stack = MultiplayerService.try_receive_stack() + if opponent_stack is not None: + for square in opponent_stack.points: + for vertex in square: + vertex[0] += 400 + + if len(opponent_stack.points) > len(self._stack_player_two._points): + for _ in range(len(opponent_stack.points) - len(self._stack_player_two._points)): + self._stack_player_two._square_designs.append(self._last_square_design) + + # load opponent stack into game + self._stack_player_two._points = opponent_stack.points + + print(opponent_stack) + + if self._current_piece_player_one is not None: + MultiplayerService.send_piece(PieceDto(self._current_piece_player_one._points, self._current_piece_player_one._center)) + if self._stack_player_one is not None: + MultiplayerService.send_stack(StackDto(self._stack_player_one._points)) + def _update_piece_player_one(self, elapsed_time: int) -> None: if self._current_piece_player_one is not None: self._current_piece_player_one.update(elapsed_time,\ @@ -373,23 +388,30 @@ class MultiplayerScene(Scene): MultiplayerService.quit() sys.exit() - def _update_piece_player_two(self, elapsed_time: int) -> None: + def _update_piece_player_two(self) -> None: if self._current_piece_player_two is not None: - self._current_piece_player_two.update(elapsed_time,\ - self._well_player_two,\ - self._stack_player_two,\ - self._get_level_player_two(),\ - self._clear_current_piece_player_two) - else: - self._current_piece_player_two = self._next_piece_player_two - self._current_piece_player_two.move(self._spawn_piece_shift_player_two) - self._next_piece_player_two = PieceGenerator.get_piece(self._next_piece_player_two_pos, True) + # get opponent piece from server and modify for current client + opponent_piece = MultiplayerService.try_receive_piece() + if opponent_piece is not None: + for square in opponent_piece.points: + for vertex in square: + vertex[0] += 400 + opponent_piece.center[0] += 400 - # TODO create game over scene - if self._stack_player_two and self._current_piece_player_two.collide(self._stack_player_two): - pygame.quit() - MultiplayerService.quit() - sys.exit() + # if it was added to stack then update colors + if opponent_piece.center[1] < self._current_piece_player_two._center[1]: + base_color = self._current_piece_player_two._color + inner_color = self._current_piece_player_two._inner_border_color + outer_color = self._current_piece_player_two._border_color + + self._last_square_design = SquareDesign(base_color, inner_color, outer_color) + self._current_piece_player_two = PieceGenerator.get_opponent_piece() + + # load opponent piece into game + self._current_piece_player_two._points = opponent_piece.points + self._current_piece_player_two._center = opponent_piece.center + else: + self._current_piece_player_two = PieceGenerator.get_opponent_piece() def _get_level_player_one(self) -> int: return 0 if self._stack_player_one is None else self._stack_player_one.total_lines // self._lines_per_level