wip: add stack and piece communication

This commit is contained in:
2021-07-15 18:58:27 -04:00
parent 640b8702bd
commit 3021f38cf4
4 changed files with 160 additions and 85 deletions

View File

@@ -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]

View File

@@ -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"

View File

@@ -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"])
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"])

View File

@@ -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