From 344533b3842290230f8e9a936ec03f28506b3b12 Mon Sep 17 00:00:00 2001 From: michang Date: Thu, 15 Aug 2024 16:01:55 +0900 Subject: [PATCH 1/9] =?UTF-8?q?feat:=202FA=20=EC=95=88=20=EB=8F=BC?= =?UTF-8?q?=EC=84=9C=20=EB=81=94!?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/login/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/login/views.py b/backend/login/views.py index fa6ac03..ed015ce 100644 --- a/backend/login/views.py +++ b/backend/login/views.py @@ -40,7 +40,7 @@ def callback(request): user, created = save_or_update_user(user_data) # Test 위해서 True로 설정 - user.is_2FA = True + # user.is_2FA = True if created: data = {"is_2FA": False} else: From 076117be3842c6f0146766cc9c22e01a7c2c0a3a Mon Sep 17 00:00:00 2001 From: michang Date: Thu, 15 Aug 2024 21:30:15 +0900 Subject: [PATCH 2/9] =?UTF-8?q?feat:=20=EB=A1=9C=EC=BB=AC=20=EA=B2=8C?= =?UTF-8?q?=EC=9E=84=20=EB=A7=8C=EB=93=9C=EB=8A=94=20=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/game/__init__.py | 0 backend/game/consumers.py | 239 ++++++++++++++ backend/game/routing.py | 7 + backend/game/srcs/Ball.py | 153 +++++++++ backend/game/srcs/Bar.py | 72 +++++ backend/game/srcs/GameMap.py | 22 ++ backend/game/srcs/test.py | 84 +++++ backend/game/static/game/key.mp3 | Bin 0 -> 8448 bytes backend/game/templates/game/index.html | 27 ++ backend/game/templates/game/room.html | 404 +++++++++++++++++++++++ backend/requirements.txt | 22 +- backend/transcendence/asgi.py | 14 + backend/transcendence/settings.py | 11 + frontend/key.mp3 | Bin 0 -> 8448 bytes frontend/src/components/Game-Core.js | 407 ++++++++++++++++++++++++ frontend/src/components/Game-Default.js | 9 + frontend/src/components/Game-Local.js | 8 + frontend/src/components/Main-Menu.js | 8 +- frontend/src/core/Component.js | 4 +- frontend/src/core/jwt.js | 16 + frontend/src/core/router.js | 7 +- 21 files changed, 1509 insertions(+), 5 deletions(-) create mode 100644 backend/game/__init__.py create mode 100644 backend/game/consumers.py create mode 100644 backend/game/routing.py create mode 100644 backend/game/srcs/Ball.py create mode 100644 backend/game/srcs/Bar.py create mode 100644 backend/game/srcs/GameMap.py create mode 100644 backend/game/srcs/test.py create mode 100644 backend/game/static/game/key.mp3 create mode 100644 backend/game/templates/game/index.html create mode 100644 backend/game/templates/game/room.html create mode 100644 frontend/key.mp3 create mode 100644 frontend/src/components/Game-Core.js create mode 100644 frontend/src/components/Game-Default.js create mode 100644 frontend/src/components/Game-Local.js create mode 100644 frontend/src/core/jwt.js diff --git a/backend/game/__init__.py b/backend/game/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/game/consumers.py b/backend/game/consumers.py new file mode 100644 index 0000000..2dc855a --- /dev/null +++ b/backend/game/consumers.py @@ -0,0 +1,239 @@ +import json +import asyncio +from channels.generic.websocket import AsyncWebsocketConsumer +from game.srcs.Ball import Ball +from game.srcs.Bar import Bar +from game.srcs.GameMap import GameMap +import jwt +from django.conf import settings +from channels.exceptions import DenyConnection + +SCREEN_HEIGHT = 600 +SCREEN_WIDTH = 800 + + +class GameState: + def __init__(self): + print("initializing game state!") + self.map = GameMap() + self.left_bar = Bar( + 0, + Bar.X_GAP, + SCREEN_HEIGHT // 2 - Bar.HEIGHT // 2, + 0 + ) + self.right_bar = Bar( + 1, + SCREEN_WIDTH - Bar.WIDTH - Bar.X_GAP, + SCREEN_HEIGHT // 2 - Bar.HEIGHT // 2, + SCREEN_WIDTH - Bar.WIDTH + ) + self.left_ball = Ball(0, SCREEN_HEIGHT, SCREEN_WIDTH, self.left_bar) + self.right_ball = Ball(1, SCREEN_HEIGHT, SCREEN_WIDTH, self.right_bar) + +class GameConsumer(AsyncWebsocketConsumer): + game_tasks = {} + game_states = {} + client_counts = {} + + async def connect(self): + self.room_name = self.scope["url_route"]["kwargs"]["room_name"] + self.room_group_name = f"game_{self.room_name}" + print(f"room_name: {self.room_name}") + + # Wait for authentication before joining room group + self.authenticated = False + + await self.accept() + + async def receive(self, text_data): + text_data_json = json.loads(text_data) + action = text_data_json["action"] + + print("text_data", text_data_json) + if action == "authenticate": + token = text_data_json.get("token") + if not token or not self.authenticate(token): + print("authentication failed") + await self.close(code=4001) + return + self.authenticated = True + + # Join room group after successful authentication + await self.channel_layer.group_add(self.room_group_name, self.channel_name) + + # Initialize game state for the room if it doesn't exist + if self.room_group_name not in GameConsumer.game_states: + print("new game!") + GameConsumer.game_states[self.room_group_name] = GameState() + GameConsumer.client_counts[self.room_group_name] = 0 + + GameConsumer.client_counts[self.room_group_name] += 1 + + # Send initialize game state to the client + await self.send_initialize_game() + + # Start ball movement if not already running + if self.room_group_name not in GameConsumer.game_tasks: + print("starting game loop!") + GameConsumer.game_tasks[self.room_group_name] = asyncio.create_task( + self.game_loop() + ) + elif not self.authenticated: + await self.close(code=4001) + else: + bar = text_data_json.get("bar") + state = GameConsumer.game_states[self.room_group_name] + + # Update the bar position based on the action + if bar == "left": + if action == "move_up": + state.left_bar.move_up(Bar.SPEED) + elif action == "move_down": + state.left_bar.move_down(Bar.SPEED, SCREEN_HEIGHT) + elif action == "pull": + state.left_bar.pull() + elif action == "release": + state.left_bar.set_release() + elif bar == "right": + if action == "move_up": + state.right_bar.move_up(Bar.SPEED) + elif action == "move_down": + state.right_bar.move_down(Bar.SPEED, SCREEN_HEIGHT) + elif action == "pull": + state.right_bar.pull() + elif action == "release": + state.right_bar.set_release() + + # Update game state after moving bar or resetting game + await self.send_game_state() + + def authenticate(self, token): + try: + # Decode JWT token + decoded = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"]) + uid = decoded.get("id") + print(f"uid: {uid}", f"room_name: {self.room_name}", str(uid) == str(self.room_name)) + # Check if uid matches the room_name + if str(uid) == str(self.room_name): + return True + else: + return False + except jwt.ExpiredSignatureError: + print("Token has expired") + return False + except jwt.InvalidTokenError: + print("Invalid token") + return False + + async def disconnect(self, close_code): + # Leave room group + if self.authenticated: + await self.channel_layer.group_discard(self.room_group_name, self.channel_name) + + # Decrease the client count for this room + if self.room_group_name in GameConsumer.client_counts: + GameConsumer.client_counts[self.room_group_name] -= 1 + if GameConsumer.client_counts[self.room_group_name] <= 0: + GameConsumer.game_tasks[self.room_group_name].cancel() + del GameConsumer.game_tasks[self.room_group_name] + del GameConsumer.game_states[self.room_group_name] + del GameConsumer.client_counts[self.room_group_name] + else: + GameConsumer.client_counts[self.room_group_name] = 0 + + async def send_initialize_game(self): + state = GameConsumer.game_states[self.room_group_name] + await self.send( + text_data=json.dumps( + { + "type": "initialize_game", + "left_bar_x": state.left_bar.x, + "left_bar_y": state.left_bar.y, + "right_bar_x": state.right_bar.x, + "right_bar_y": state.right_bar.y, + "screen_height": SCREEN_HEIGHT, + "screen_width": SCREEN_WIDTH, + "bar_height": Bar.HEIGHT, + "bar_width": Bar.WIDTH, + "bar_x_gap": Bar.X_GAP, + "left_ball_x": state.left_ball.x, + "left_ball_y": state.left_ball.y, + "right_ball_x": state.right_ball.x, + "right_ball_y": state.right_ball.y, + "ball_radius": Ball.RADIUS, + "map": state.map.map, + } + ) + ) + + async def game_loop(self): + count = 0 + while True: + state = GameConsumer.game_states[self.room_group_name] + state.left_bar.update() + state.right_bar.update() + + state.left_ball.move(state.left_bar) + state.left_ball.check_bar_collision(state.left_bar) + state.left_ball.check_bar_collision(state.right_bar) + state.left_ball.check_collision(state.map) + + state.right_ball.move(state.right_bar) + state.right_ball.check_bar_collision(state.left_bar) + state.right_ball.check_bar_collision(state.right_bar) + state.right_ball.check_collision(state.map) + + if count % 3 == 0: + await self.send_game_state() + await asyncio.sleep(0.01) + await state.map.init() + else: + await asyncio.sleep(0.01) + count += 1 + + async def send_game_state(self): + state = GameConsumer.game_states[self.room_group_name] + await self.channel_layer.group_send( + self.room_group_name, + { + "type": "update_game_state_message", + "left_bar_x": state.left_bar.x, + "left_bar_y": state.left_bar.y, + "right_bar_x": state.right_bar.x, + "right_bar_y": state.right_bar.y, + "left_ball_x": state.left_ball.x, + "left_ball_y": state.left_ball.y, + "right_ball_x": state.right_ball.x, + "right_ball_y": state.right_ball.y, + "map_diff": state.map.diff, + }, + ) + + async def update_game_state_message(self, event): + left_bar_x = event["left_bar_x"] + left_bar_y = event["left_bar_y"] + right_bar_x = event["right_bar_x"] + right_bar_y = event["right_bar_y"] + left_ball_x = event["left_ball_x"] + left_ball_y = event["left_ball_y"] + right_ball_x = event["right_ball_x"] + right_ball_y = event["right_ball_y"] + + # Send the updated game state to the WebSocket + await self.send( + text_data=json.dumps( + { + "type": "update_game_state", + "left_bar_x": left_bar_x, + "left_bar_y": left_bar_y, + "right_bar_x": right_bar_x, + "right_bar_y": right_bar_y, + "left_ball_x": int(left_ball_x), + "left_ball_y": int(left_ball_y), + "right_ball_x": int(right_ball_x), + "right_ball_y": int(right_ball_y), + "map_diff": GameConsumer.game_states[self.room_group_name].map.diff, + } + ) + ) diff --git a/backend/game/routing.py b/backend/game/routing.py new file mode 100644 index 0000000..479a290 --- /dev/null +++ b/backend/game/routing.py @@ -0,0 +1,7 @@ +from django.urls import re_path + +from . import consumers + +websocket_urlpatterns = [ + re_path(r"ws/game/(?P\w+)/$", consumers.GameConsumer.as_asgi()), +] diff --git a/backend/game/srcs/Ball.py b/backend/game/srcs/Ball.py new file mode 100644 index 0000000..6b303da --- /dev/null +++ b/backend/game/srcs/Ball.py @@ -0,0 +1,153 @@ +import numpy as np +from game.srcs.Bar import BarState + +RADIAN60 = np.pi / 3 +RADIAN20 = np.pi / 9 +COLLISION_IGNORE_FRAMES = 5 # 충돌 무시 프레임 수 +PENALTY_FRAMES = 50 # 페널티 프레임 수 + + +class Ball: + SPEED = 3 + RADIUS = 10 + + def __init__(self, id, screen_height, screen_width, bar): + self.id = id + self.screen_height = screen_height + self.screen_width = screen_width + self.initialize(bar) + self.penalty_timer = 0 + self.life = 0; + + def initialize(self, bar): + self.x = bar.x + bar.width / 2 + (150 if self.id == 0 else -150) + self.y = bar.y + bar.height / 2 + (1 if self.id == 0 else -1) + self.dx = -self.SPEED if self.id == 0 else self.SPEED + self.dy = 0 + self.collision_timer = 0 + self.life = 0 + + def move(self, bar): + if self.penalty_timer > 0: + self.penalty_timer -= 1 + # print(self.penalty_timer) + if self.penalty_timer == 0: + self.initialize(bar) + return + + self.x += self.dx + self.y += self.dy + if self.collision_timer > 0: + self.collision_timer -= 1 + + def check_bar_collision(self, bar): + if self.collision_timer > 0 or self.penalty_timer > 0: + return + if bar.y < self.y + self.RADIUS and bar.y + bar.height > self.y: + if bar.id == 0 and bar.x + bar.width < self.x - self.RADIUS: + return + if bar.id == 1 and bar.x > self.x + self.RADIUS: + return + speed = self.SPEED + if (bar.state == BarState.RELEASE): + speed *= bar.power / bar.max_power + 1 + self.life = bar.get_ball_power() + self.x = bar.min_x + (bar.width if bar.id == 0 else 0) + print("speed", speed) + # 바와의 충돌로 dx를 반전시키고, dy를 새로 계산 + relative_intersect_y = (bar.y + (bar.height // 2)) - self.y + normalized_relative_intersect_y = relative_intersect_y / ( + bar.height // 2 + ) + bounce_angle = normalized_relative_intersect_y * RADIAN60 + if abs(bounce_angle) < RADIAN20: + bounce_angle = RADIAN20 if bounce_angle >= 0 else -RADIAN20 + self.dx = speed * np.cos(bounce_angle) + self.dy = speed * -np.sin(bounce_angle) + # 방향을 반전시킴 + if self.x < bar.x + bar.width / 2: + self.dx = -abs(self.dx) + else: + self.dx = abs(self.dx) + self.collision_timer = COLLISION_IGNORE_FRAMES + + def check_collision(self, game_map): + if self.penalty_timer > 0: + return + # 미래 위치 예측 + future_x = self.x + self.dx + future_y = self.y + self.dy + + # 벽 충돌 검사 + if future_y - self.RADIUS <= 0 or future_y + self.RADIUS >= self.screen_height: + self.dy *= -1 + future_y = self.y + self.dy + elif future_x - self.RADIUS <= -self.RADIUS * 10: + self.x = -self.RADIUS * 5 + self.penalty_timer = PENALTY_FRAMES + return + elif future_x + self.RADIUS >= self.screen_width + self.RADIUS * 10: + self.x = self.screen_width + self.RADIUS * 5 + self.penalty_timer = PENALTY_FRAMES + return + + # 맵과의 충돌 검사 + if self.collision_timer > 0: + return + + pos = [ + (future_x - self.RADIUS, future_y - self.RADIUS), + (future_x + self.RADIUS, future_y - self.RADIUS), + (future_x - self.RADIUS, future_y + self.RADIUS), + (future_x + self.RADIUS, future_y + self.RADIUS), + ] + + for x, y in pos: + grid_x = int(x / (self.screen_width / game_map.WIDTH)) + grid_y = int(y / (self.screen_height / game_map.HEIGHT)) + if ( + grid_x < 0 + or grid_x >= game_map.WIDTH + or grid_y < 0 + or grid_y >= game_map.HEIGHT + ): + continue + current_value = game_map.map[grid_y][grid_x] + + if (self.id == 0 and current_value == 1) or ( + self.id == 1 and current_value == 0 + ): + block_top = grid_y * (self.screen_height // game_map.HEIGHT) + block_bottom = (grid_y + 1) * (self.screen_height // game_map.HEIGHT) + block_left = grid_x * (self.screen_width // game_map.WIDTH) + block_right = (grid_x + 1) * (self.screen_width // game_map.WIDTH) + + # 충돌 최소 거리 계산 + dist_top = abs(y - block_top) + dist_bottom = abs(y - block_bottom) + dist_left = abs(x - block_left) + dist_right = abs(x - block_right) + min_dist = min(dist_top, dist_bottom, dist_left, dist_right) + + if self.life == 0: + # 충돌 처리 + if min_dist == dist_top or min_dist == dist_bottom: + self.dy *= -1 + future_y = self.y + self.dy + else: + self.dx *= -1 + future_x = self.x + self.dx + else: + self.life -= 1 + + # 블록 상태 업데이트 + new_value = 1 if current_value == 0 else 0 + game_map.update(grid_x, grid_y, new_value) + + # 충돌 타이머 설정 + self.collision_timer = COLLISION_IGNORE_FRAMES + break + + # 미래 위치로 업데이트 + self.x = future_x + self.y = future_y \ No newline at end of file diff --git a/backend/game/srcs/Bar.py b/backend/game/srcs/Bar.py new file mode 100644 index 0000000..926f9fa --- /dev/null +++ b/backend/game/srcs/Bar.py @@ -0,0 +1,72 @@ +from enum import Enum + + +class Bar: + X_GAP = 40 + HEIGHT = 200 + WIDTH = 20 + SPEED = 15 + PULL_SPEED = 1 + + def __init__( + self, id, x, y, max_x, width=WIDTH, height=HEIGHT, pull_speed=PULL_SPEED + ): + self.state = BarState.IDLE + self.id = id + self.min_x = x + self.max_x = max_x + self.max_power = abs(max_x - x) + print("max_power", self.max_power, " max_x", max_x, "x", x) + self.x = x + self.y = y + self.width = width + self.height = height + self.pull_speed = pull_speed * (-1 if id == 0 else 1) + self.power = 0 + + def move_up(self, speed): + self.y = max(0, self.y - speed) + + def move_down(self, speed, screen_height): + self.y = min(screen_height - self.height, self.y + speed) + + def pull(self): + if self.state == BarState.RELEASE: + return + self.state = BarState.PULLING + if self.power < self.max_power: + self.power = abs(self.x - self.min_x) + self.x += ((-2 / (self.max_power * 2)) * self.power + 3) * self.pull_speed + print("power", self.power) + + def set_release(self): + if self.state != BarState.PULLING: + return + self.state = BarState.RELEASE + + def release(self): + self.x += -self.pull_speed * (self.power + 0.1) * 0.25 + print(-self.pull_speed * self.power * 0.25 + 0.1) + if (self.id == 0 and self.x > self.min_x + 10) or ( + self.id == 1 and self.x < self.min_x - 10 + ): + self.x = self.min_x + self.power = 0 + self.state = BarState.IDLE + + def update(self): + if self.state == BarState.RELEASE: + self.release() + + def get_ball_power(self): + if self.power < 15: + return 0 + if self.power < 30: + return 1 + return 2 + + +class BarState(Enum): + IDLE = 0 + PULLING = 1 + RELEASE = 2 diff --git a/backend/game/srcs/GameMap.py b/backend/game/srcs/GameMap.py new file mode 100644 index 0000000..2e24d9f --- /dev/null +++ b/backend/game/srcs/GameMap.py @@ -0,0 +1,22 @@ +class GameMap: + HEIGHT = 6 + WIDTH = 8 + + def __init__(self): + self.map = [[0 for _ in range(self.WIDTH)] for _ in range(self.HEIGHT)] + self.diff = [] + for i in range(0, self.HEIGHT): + for j in range(self.WIDTH // 2, self.WIDTH): + self.map[i][j] = 1 + + def debug(self): + for i in range(0, self.HEIGHT): + print(self.map[i]) + + async def init(self): + self.diff = [] + + def update(self, x, y, value): + if 0 <= x < self.WIDTH and 0 <= y < self.HEIGHT: + self.map[y][x] = value + self.diff.append((y, x, value)) \ No newline at end of file diff --git a/backend/game/srcs/test.py b/backend/game/srcs/test.py new file mode 100644 index 0000000..0ed1426 --- /dev/null +++ b/backend/game/srcs/test.py @@ -0,0 +1,84 @@ +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.patches as patches + +class Box: + def __init__(self, x, y, width, height): + self.x = x + self.y = y + self.width = width + self.height = height + self.h_width = width / 2 + self.h_height = height / 2 + + +def is_colliding(block, ball): + face = np.array( + [ + block.h_width + ball.h_width - abs(block.x - ball.x), + block.h_height + ball.h_height - abs(block.y - ball.y), + ] + ) + if face[0] < 0 or face[1] < 0: + return False, None, None + + penetration = min(face) + if face[0] < face[1]: + if block.x < ball.x: + return True, "right", penetration + else: + return True, "left", penetration + if block.y < ball.y: + return True, "top", penetration + return True, "bottom", penetration + + +def update_plot(): + ax.clear() + # 박스 그리기 + rect1 = patches.Rectangle((box.x - box.h_width, box.y - box.h_height), box.width, box.height, linewidth=1, edgecolor='r', facecolor='none') + rect2 = patches.Rectangle((ball.x - ball.h_width, ball.y - ball.h_height), ball.width, ball.height, linewidth=1, edgecolor='b', facecolor='none') + ax.add_patch(rect1) + ax.add_patch(rect2) + + # 축 설정 + ax.set_xlim(-15, 25) + ax.set_ylim(-15, 25) + ax.set_aspect('equal', adjustable='box') + plt.grid(True) + plt.xticks(np.arange(-15, 26, 1)) + plt.yticks(np.arange(-15, 26, 1)) + plt.xlabel('X-axis') + plt.ylabel('Y-axis') + + # 충돌 감지 + collision, collision_edges, penetration = is_colliding(box, ball) + if collision: + plt.title(f"{collision_edges}, penetration = {penetration}") + else: + plt.title("no collision") + + plt.draw() + +def on_key(event): + step = 0.25 + if event.key == 'up': + ball.y += step + elif event.key == 'down': + ball.y -= step + elif event.key == 'left': + ball.x -= step + elif event.key == 'right': + ball.x += step + update_plot() + +# 예제 박스 +box = Box(0, 0, 10, 10) +ball = Box(5, 6, 5, 4) + +# 초기 플롯 설정 +fig, ax = plt.subplots() +fig.canvas.mpl_connect('key_press_event', on_key) +update_plot() + +plt.show() diff --git a/backend/game/static/game/key.mp3 b/backend/game/static/game/key.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..e505e3f5ce9f11158447469d7266e43f1060c638 GIT binary patch literal 8448 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 literal 0 HcmV?d00001 diff --git a/backend/game/templates/game/index.html b/backend/game/templates/game/index.html new file mode 100644 index 0000000..a39655b --- /dev/null +++ b/backend/game/templates/game/index.html @@ -0,0 +1,27 @@ + + + + + + Chat Rooms + + + What chat room would you like to enter?
+
+ + + + + \ No newline at end of file diff --git a/backend/game/templates/game/room.html b/backend/game/templates/game/room.html new file mode 100644 index 0000000..4177554 --- /dev/null +++ b/backend/game/templates/game/room.html @@ -0,0 +1,404 @@ + + + + + + {% load static %} + Move Square + + + + + + + {{ room_name|json_script:"room-name" }} + + + + \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt index 7c3ef6d..db48f83 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,22 +1,42 @@ asgiref==3.8.1 +attrs==24.2.0 +autobahn==24.4.2 +Automat==22.10.0 certifi==2024.7.4 +cffi==1.17.0 +channels==4.1.0 charset-normalizer==3.3.2 +constantly==23.10.4 +cryptography==43.0.0 +daphne==4.1.2 Django==4.2.14 django-cors-headers==4.4.0 djangorestframework==3.15.2 djangorestframework-simplejwt==5.3.1 drf-yasg==1.21.7 +hyperlink==21.0.0 idna==3.7 +incremental==24.7.2 inflection==0.5.1 +numpy==2.0.1 packaging==24.1 psycopg2-binary==2.9.9 +pyasn1==0.6.0 +pyasn1_modules==0.4.0 +pycparser==2.22 PyJWT==2.9.0 +pyOpenSSL==24.2.1 python-dotenv==1.0.1 pytz==2024.1 PyYAML==6.0.1 requests==2.31.0 -setuptools==71.1.0 +service-identity==24.1.0 +six==1.16.0 sqlparse==0.5.1 +tomli==2.0.1 +Twisted==24.7.0 +txaio==23.1.1 typing_extensions==4.12.2 uritemplate==4.1.1 urllib3==2.2.2 +zope.interface==7.0.1 diff --git a/backend/transcendence/asgi.py b/backend/transcendence/asgi.py index a825a71..df4760e 100644 --- a/backend/transcendence/asgi.py +++ b/backend/transcendence/asgi.py @@ -8,9 +8,23 @@ """ import os +from channels.auth import AuthMiddlewareStack +from channels.routing import ProtocolTypeRouter, URLRouter +from channels.security.websocket import AllowedHostsOriginValidator +from game.routing import websocket_urlpatterns from django.core.asgi import get_asgi_application os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'transcendence.settings') application = get_asgi_application() + + +application = ProtocolTypeRouter( + { + "http": application, + "websocket": AllowedHostsOriginValidator( + AuthMiddlewareStack(URLRouter(websocket_urlpatterns)) + ), + } +) \ No newline at end of file diff --git a/backend/transcendence/settings.py b/backend/transcendence/settings.py index 8e9fdfd..58ce26a 100644 --- a/backend/transcendence/settings.py +++ b/backend/transcendence/settings.py @@ -40,6 +40,7 @@ # Application definition INSTALLED_APPS = [ + "daphne", 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', @@ -52,6 +53,7 @@ 'rest_framework_simplejwt', "users.apps.UsersConfig", "login.apps.LoginConfig", + "game", ] REST_FRAMEWORK = { @@ -161,3 +163,12 @@ # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +# Daphne +ASGI_APPLICATION = "transcendence.asgi.application" + +CHANNEL_LAYERS = { + "default": { + "BACKEND": "channels.layers.InMemoryChannelLayer", + }, +} diff --git a/frontend/key.mp3 b/frontend/key.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..e505e3f5ce9f11158447469d7266e43f1060c638 GIT binary patch literal 8448 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 literal 0 HcmV?d00001 diff --git a/frontend/src/components/Game-Core.js b/frontend/src/components/Game-Core.js new file mode 100644 index 0000000..66c02bc --- /dev/null +++ b/frontend/src/components/Game-Core.js @@ -0,0 +1,407 @@ +import { Component } from "../core/Component.js"; +import { changeUrl } from "../core/router.js"; +import { getCookie } from "../core/jwt.js"; + +export class GameCore extends Component { + gameStart() { + const COLOR = ["#f5f6fa", "#2f3640", "#f5f6fa"]; + const roomName = this.props.uid; + const canvas = document.getElementById('game-canvas'); + console.log(canvas); + const ctx = canvas.getContext('2d'); + const sounds = { + 'collision': new Audio('../../key.mp3'), + }; + let SCREEN_HEIGHT, SCREEN_WIDTH, BAR_HEIGHT, BAR_WIDTH, BAR_X_GAP, + BALL_RADIUS, LEFT_BAR_X, LEFT_BAR_Y, RIGHT_BAR_X, RIGHT_BAR_Y, + CELL_WIDTH, CELL_HEIGHT; + let counter = 0; + let leftBar, rightBar, leftBall, rightBall, map; + + function playSound(soundName) { + var sound = sounds[soundName]; + if (sound) { + console.log(sound); + sound.currentTime = 0; // 재생 위치를 처음으로 + sound.play().catch(function (error) { + console.log('Autoplay was prevented:', error); + }); + } + } + + class Bar { + constructor(x, y, width, height, canvasHeight, id) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + this.targetX = x; + this.targetY = y; + this.canvasHeight = canvasHeight; + this.speed = 4; + this.id = id; + } + + draw() { + ctx.fillStyle = COLOR[this.id + 1]; + Map.strokeRoundedRect(ctx, this.x, this.y, this.width, this.height, 6); + ctx.fill(); + ctx.strokeStyle = COLOR[this.id]; + ctx.setLineDash([0]); + ctx.strokeRect(this.x, this.y, this.width, this.height); + } + + update() { + if (Math.abs(this.targetY - this.y) < this.speed) { + this.y = this.targetY; + } else { + if (this.targetY > this.y) { + this.y += this.speed; + } else { + this.y -= this.speed; + } + } + + // if (Math.abs(this.targetX - this.x) < this.speed) { + // this.x = this.targetX; + // } else { + // if (this.targetX > this.x) { + // this.x += this.speed; + // } else { + // this.x -= this.speed; + // } + // } + this.x = this.targetX; + } + } + + class Ball { + constructor(x, y, radius, color) { + this.x = x; + this.y = y; + this.targetX = x; + this.targetY = y; + this.radius = radius; + this.speed = 5; + this.color = color; + } + + draw() { + Map.strokeRoundedRect(ctx, this.x - this.radius, this.y - this.radius, this.radius * 2, this.radius * 2, 6); + ctx.fillStyle = this.color; + ctx.fill(); + } + + update() { + let directionX = this.targetX - this.x; + let directionY = this.targetY - this.y; + let length = Math.sqrt(directionX * directionX + directionY * directionY); + + if (Math.abs(this.targetX - this.x) > this.speed * 10 || Math.abs(this.targetY - this.y) > this.speed * 10) { + this.x = this.targetX; + this.y = this.targetY; + console.log('Ball position reset'); + } + + if (length < this.speed) { + this.x = this.targetX; + this.y = this.targetY; + return; + } + + let unitDirectionX = directionX / length; + let unitDirectionY = directionY / length; + + this.x += unitDirectionX * this.speed; + this.y += unitDirectionY * this.speed; + } + } + + class Map { + constructor(map) { + this.map = map; + this.height = map.length; + this.width = map[0].length; + this.lintDash = [CELL_WIDTH / 5, CELL_WIDTH * 3 / 5]; + } + + update(mapDiff) { + mapDiff.forEach(diff => { + console.log(mapDiff) + this.map[diff[0]][diff[1]] = diff[2]; + new Particles(diff[1], diff[0], diff[2]); + playSound('collision'); + }); + } + + draw() { + for (let i = 0; i < this.height; i++) { + for (let j = 0; j < this.width; j++) { + ctx.fillStyle = COLOR[this.map[i][j]]; + Map.strokeRoundedRect( + ctx, + j * CELL_WIDTH + 1, + i * CELL_HEIGHT + 1, + CELL_WIDTH - 2, + CELL_HEIGHT - 2, + 5 + ); + } + } + } + + static strokeRoundedRect(ctx, x, y, width, height, radius) { + ctx.beginPath(); + ctx.moveTo(x + radius, y); + ctx.arcTo(x + width, y, x + width, y + height, radius); + ctx.arcTo(x + width, y + height, x, y + height, radius); + ctx.arcTo(x, y + height, x, y, radius); + ctx.arcTo(x, y, x + width, y, radius); + ctx.fill(); + ctx.closePath(); + } + } + + class Particle { + constructor(x, y, r, vx, vy, color) { + this.x = x; + this.y = y; + this.r = r; + this.vx = vx; + this.vy = vy; + this.color = color; + this.opacity = 0.5; + this.g = 0.05; + this.friction = 0.99; + } + + draw() { + ctx.fillStyle = this.color + `${this.opacity})`; + ctx.fillRect(this.x, this.y, this.r, this.r); + } + + update() { + this.vx *= this.friction; + this.vy *= this.friction; + this.vy += this.g; + this.x += this.vx; + this.y += this.vy; + this.opacity -= 0.004; + this.draw(); + } + } + + class Particles { + static array = []; + + constructor(x, y, blockId) { + this.x = x * CELL_WIDTH + CELL_WIDTH / 2; + this.y = y * CELL_HEIGHT + CELL_HEIGHT / 2; + this.blockId = blockId; + this.color = Particles.hexToRgba(COLOR[blockId + 1]); + this.particles = []; + const particleCount = 50; + const power = 20; + const radians = (Math.PI * 2) / particleCount; + this.state = 1; + + for (let i = 0; i < particleCount; i++) { + this.particles.push( + new Particle( + this.x, + this.y, + 5, + Math.cos(radians * i) * (Math.random() * power), + Math.sin(radians * i) * (Math.random() * power), + this.color + ) + ); + } + Particles.array.push(this); + } + + draw() { + for (let i = this.particles.length - 1; i >= 0; i--) { + if (this.particles[i].opacity >= 0) { + this.particles[i].update(); + } else { + this.particles.splice(i, 1); + } + } + if (this.particles.length <= 0) { + this.state = -1; + } + } + + static hexToRgba(hex) { + let cleanedHex = hex.replace('#', ''); + if (cleanedHex.length === 3) + cleanedHex = cleanedHex.split('').map(hex => hex + hex).join(''); + const bigint = parseInt(cleanedHex, 16); + const r = (bigint >> 16) & 255; + const g = (bigint >> 8) & 255; + const b = bigint & 255; + + return `rgba(${r}, ${g}, ${b},`; + } + + static drawAll() { + for (let i = Particles.array.length - 1; i >= 0; i--) { + if (Particles.array[i].state == 1) + Particles.array[i].draw(); + else + Particles.array.splice(i, 1); + } + } + } + + const gameSocket = new WebSocket( + 'ws://' + + "localhost:8000" + + '/ws/game/' + + roomName + + '/' + ); + + gameSocket.onopen = function () { + const token = getCookie("jwt"); + gameSocket.send(JSON.stringify({ 'action': 'authenticate', 'token': token })); + }; + + + gameSocket.onmessage = function (e) { + const data = JSON.parse(e.data); + if (data.type === 'initialize_game') { + initializeGame(data); + drawGame(); + } else if (data.type === 'update_game_state') { + leftBar.targetX = data.left_bar_x; + leftBar.targetY = data.left_bar_y; + rightBar.targetX = data.right_bar_x; + rightBar.targetY = data.right_bar_y; + leftBall.targetX = data.left_ball_x; + leftBall.targetY = data.left_ball_y; + rightBall.targetX = data.right_ball_x; + rightBall.targetY = data.right_ball_y; + map.update(data.map_diff); + } + }; + + gameSocket.onclose = function (e) { + console.error('Game socket closed unexpectedly'); + }; + + const keysPressed = {}; + + document.addEventListener('keydown', function (e) { + keysPressed[e.key] = true; + }); + + document.addEventListener('keyup', function (e) { + keysPressed[e.key] = false; + }); + + let isPoolingLeft = false, isPoolingRight = false; + function handleKeyPresses() { + if (keysPressed['w']) { + gameSocket.send(JSON.stringify({ 'action': 'move_up', 'bar': 'left' })); + leftBar.targetY -= 5; + } + if (keysPressed['s']) { + gameSocket.send(JSON.stringify({ 'action': 'move_down', 'bar': 'left' })); + leftBar.targetY += 5; + } + if (keysPressed['a']) { + gameSocket.send(JSON.stringify({ 'action': 'pull', 'bar': 'left' })); + isPoolingLeft = true; + } else if (isPoolingLeft) { + gameSocket.send(JSON.stringify({ 'action': 'release', 'bar': 'left' })); + isPoolingLeft = false; + } + + if (keysPressed['ArrowUp']) { + gameSocket.send(JSON.stringify({ 'action': 'move_up', 'bar': 'right' })); + rightBar.targetY -= 5; + } + if (keysPressed['ArrowDown']) { + gameSocket.send(JSON.stringify({ 'action': 'move_down', 'bar': 'right' })); + rightBar.targetY += 5; + } + if (keysPressed['ArrowRight']) { + gameSocket.send(JSON.stringify({ 'action': 'pull', 'bar': 'right' })); + isPoolingRight = true; + } else if (isPoolingRight) { + gameSocket.send(JSON.stringify({ 'action': 'release', 'bar': 'right' })); + isPoolingRight = false; + } + } + + function drawGame() { + // ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.fillStyle = "#92969D"; + ctx.fillRect(0, 0, canvas.width, canvas.height); + map.draw(); + leftBar.draw(); + rightBar.draw(); + leftBall.draw(); + rightBall.draw(); + Particles.drawAll(); + } + + function initializeGame(data) { + SCREEN_HEIGHT = data.screen_height; + SCREEN_WIDTH = data.screen_width; + LEFT_BAR_X = data.left_bar_x; + LEFT_BAR_Y = data.left_bar_y; + RIGHT_BAR_X = data.right_bar_x; + RIGHT_BAR_Y = data.right_bar_y; + BAR_HEIGHT = data.bar_height; + BAR_WIDTH = data.bar_width; + BAR_X_GAP = data.bar_x_gap; + BALL_RADIUS = data.ball_radius; + canvas.width = SCREEN_WIDTH; + canvas.height = SCREEN_HEIGHT; + CELL_WIDTH = canvas.width / data.map[0].length; + CELL_HEIGHT = canvas.height / data.map.length; + + map = new Map(data.map, COLOR[0], COLOR[1]); + leftBar = new Bar(LEFT_BAR_X, LEFT_BAR_Y, BAR_WIDTH, BAR_HEIGHT, SCREEN_HEIGHT, 0); + rightBar = new Bar(RIGHT_BAR_X, RIGHT_BAR_Y, BAR_WIDTH, BAR_HEIGHT, SCREEN_HEIGHT, 1); + leftBall = new Ball(data.left_ball_x, data.left_ball_y, BALL_RADIUS, "#FFC312"); + rightBall = new Ball(data.right_ball_x, data.right_ball_y, BALL_RADIUS, "#FFC312"); + + console.log(SCREEN_HEIGHT, SCREEN_WIDTH, BAR_HEIGHT, BAR_WIDTH, BALL_RADIUS); + setInterval(interpolate, 5); + } + + function interpolate() { + leftBar.update(); + rightBar.update(); + leftBall.update(); + rightBall.update(); + drawGame(); + + if (counter % 6 === 0) + handleKeyPresses(); + counter++; + } + } + + template() { + return ` + +