diff --git a/Makefile b/Makefile old mode 100644 new mode 100755 index 5c42b36ad..3047336b0 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ all: runb: daphne -b 0.0.0.0 backend.asgi:application -# http://localhost:8000/chat/ +# http://10.12.2.4:8000/chat/ execbackend: diff --git a/README.md b/README.md old mode 100644 new mode 100755 diff --git a/backend/auto_reload.py b/backend/auto_reload.py new file mode 100644 index 000000000..5f689a36e --- /dev/null +++ b/backend/auto_reload.py @@ -0,0 +1,29 @@ +import time +from watchdog.observers import Observer +from watchdog.events import FileSystemEventHandler +import os +import signal +import subprocess + +class MyHandler(FileSystemEventHandler): + def __init__(self, process): + self.process = process + + def on_modified(self, event): + if event.src_path.endswith('.py'): + print(f'File changed: {event.src_path}. Restarting server.') + self.process.kill() + self.process = subprocess.Popen(['daphne', '-b', '0.0.0.0', '-p', '8000', 'backend.asgi:application']) + +process = subprocess.Popen(['daphne', '-b', '0.0.0.0', '-p', '8000', 'backend.asgi:application']) +event_handler = MyHandler(process) +observer = Observer() +observer.schedule(event_handler, path='.', recursive=True) +observer.start() + +try: + while True: + time.sleep(1) +except KeyboardInterrupt: + observer.stop() +observer.join() \ No newline at end of file diff --git a/backend/backend/asgi.py b/backend/backend/asgi.py index 9fe7d1bb3..a97f3bd67 100644 --- a/backend/backend/asgi.py +++ b/backend/backend/asgi.py @@ -8,12 +8,15 @@ """ import os +import game.routing +import chat.routing from django.core.asgi import get_asgi_application from channels.routing import ProtocolTypeRouter, URLRouter -import chat.routing from channels.auth import AuthMiddlewareStack + + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings') # application = ProtocolTypeRouter({ @@ -28,13 +31,14 @@ "http": get_asgi_application(), "websocket": AuthMiddlewareStack( URLRouter( - chat.routing.websocket_urlpatterns + chat.routing.websocket_urlpatterns + + game.routing.game_websocket_urlpatterns ) ), }) -with open('text.txt', 'a') as f: - f.write('deleting user channel names\n') -from chat.models import UserChannelName -UserChannelName.objects.all().delete() \ No newline at end of file +# with open('text.txt', 'a') as f: +# f.write('deleting user channel names\n') +# from chat.models import UserChannelName +# UserChannelName.objects.all().delete() \ No newline at end of file diff --git a/backend/backend/settings.py b/backend/backend/settings.py index ef8a444bb..73eaddff4 100644 --- a/backend/backend/settings.py +++ b/backend/backend/settings.py @@ -10,9 +10,10 @@ # See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = os.getenv('SECRET_KEY') -DEBUG = os.getenv('DEBUG') +SECRET_KEY = os.getenv("SECRET_KEY") +DEBUG = os.getenv("DEBUG") +SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") # SECURITY WARNING: don't run with debug turned on in production! @@ -32,53 +33,56 @@ if DEBUG == 'True': REDIRECT_URI = "https://10.12.2.2" else: - REDIRECT_URI = "https://transcendence-backend-znhl.onrender.com" + REDIRECT_URI = "https://transcendence-backend-znhl.onrender.com" -if DEBUG == 'True': - print("DEBUG: True: ", DEBUG) +if DEBUG == "True": + print("DEBUG: True: ", DEBUG) else: - print("DEBUG: False: ", DEBUG) + print("DEBUG: False: ", DEBUG) # Application definition INSTALLED_APPS = [ - "daphne", - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'django_otp', - 'django_otp.plugins.otp_totp', - 'rest_framework', - 'rest_framework_simplejwt.token_blacklist', - 'corsheaders', - 'user_api.apps.UserApiConfig', - 'friendship', - 'friendship_api', - 'chat', + "daphne", + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "django_otp", + "django_otp.plugins.otp_totp", + "friendship", + "rest_framework", + "rest_framework_simplejwt.token_blacklist", + "corsheaders", + "user_api.apps.UserApiConfig", + "friendship_api", + "user_block", + "game", + "chat", ] MIDDLEWARE = [ - 'corsheaders.middleware.CorsMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django_otp.middleware.OTPMiddleware', - 'django.middleware.security.SecurityMiddleware', + "corsheaders.middleware.CorsMiddleware", + "django.middleware.common.CommonMiddleware", + "django_otp.middleware.OTPMiddleware", + "django.middleware.security.SecurityMiddleware", # 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'whitenoise.middleware.WhiteNoiseMiddleware', + "django.contrib.sessions.middleware.SessionMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", + "whitenoise.middleware.WhiteNoiseMiddleware", ] CSRF_ALLOWED_ORIGINS = [ + "https://10.12.2.2", "https://api.intra.42.fr", "http://localhost:3000", "http://frontend:3000", "https://transcendence-frontend-3otz.onrender.com", - "https://zstenger93.github.io" + "https://zstenger93.github.io", ] CORS_ALLOWED_ORIGINS = [ @@ -97,37 +101,35 @@ # SESSION_COOKIE_SAMESITE = 'Lax' REST_FRAMEWORK = { - 'DEFAULT_PERMISSION_CLASSES': ( - 'rest_framework.permissions.IsAuthenticated', - ), - 'DEFAULT_AUTHENTICATION_CLASSES': ( - 'user_api.authentication.BlacklistCheckJWTAuthentication', + "DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",), + "DEFAULT_AUTHENTICATION_CLASSES": ( + "user_api.authentication.BlacklistCheckJWTAuthentication", ), } -ROOT_URLCONF = 'backend.urls' +ROOT_URLCONF = "backend.urls" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", ], }, }, ] -WSGI_APPLICATION = 'backend.wsgi.application' +WSGI_APPLICATION = "backend.wsgi.application" ## User model -AUTH_USER_MODEL = 'user_api.AppUser' +AUTH_USER_MODEL = "user_api.AppUser" # Database @@ -142,46 +144,44 @@ # } # THIS IS THE DATABASE CONFIGURATION FOR THE DOCKER CONTAINER -if DEBUG == 'True': +if DEBUG == "True": DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql', - 'NAME': 'transcend_users_db', - 'USER': 'transcend_user', - 'PASSWORD': 'transcend_pwd', - 'HOST': 'db', - 'PORT': '5432', + "default": { + "ENGINE": "django.db.backends.postgresql", + "NAME": "transcend_users_db", + "USER": "transcend_user", + "PASSWORD": "transcend_pwd", + "HOST": "db", + "PORT": "5432", } } else: - DATABASES = { - 'default': dj_database_url.parse(os.getenv('DATABASE_URL')) - } + DATABASES = {"default": dj_database_url.parse(os.getenv("DATABASE_URL"))} # Password validation # https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, ] # Internationalization # https://docs.djangoproject.com/en/5.0/topics/i18n/ -LANGUAGE_CODE = 'en-us' +LANGUAGE_CODE = "en-us" -TIME_ZONE = 'UTC' +TIME_ZONE = "UTC" USE_I18N = True @@ -191,40 +191,35 @@ # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/5.0/howto/static-files/ -STATIC_URL = '/static/' +STATIC_URL = "/static/" # STATICFILES_DIRS = [BASE_DIR / 'static'] -STATIC_ROOT = BASE_DIR / 'static' -STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' +STATIC_ROOT = BASE_DIR / "static" +STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage" # Default primary key field type # https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" # For the custom user model to work we need to include this line # AUTH_USER_MODEL = 'authentication.User' - -MEDIA_URL = '/media/' -MEDIA_ROOT = BASE_DIR / 'media' +MEDIA_URL = "/media/" +MEDIA_ROOT = BASE_DIR / "media" SIMPLE_JWT = { - 'ACCESS_TOKEN_LIFETIME': timedelta(days=99), + "ACCESS_TOKEN_LIFETIME": timedelta(days=99), } EMAIL_USE_TLS = True EMAIL_PORT = 587 -EMAIL_HOST = 'smtp.gmail.com' -EMAIL_HOST_USER = 'sioudazer8@gmail.com' -EMAIL_HOST_PASSWORD = 'sayy uonp nado adlm' -EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' +EMAIL_HOST = "smtp.gmail.com" +EMAIL_HOST_USER = "sioudazer8@gmail.com" +EMAIL_HOST_PASSWORD = "sayy uonp nado adlm" +EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" # chat settings ASGI_APPLICATION = "backend.asgi.application" -CHANNEL_LAYERS = { - 'default': { - 'BACKEND': 'channels.layers.InMemoryChannelLayer' - } -} \ No newline at end of file +CHANNEL_LAYERS = {"default": {"BACKEND": "channels.layers.InMemoryChannelLayer"}} diff --git a/backend/backend/urls.py b/backend/backend/urls.py index 8ee7ce65e..43f103eec 100644 --- a/backend/backend/urls.py +++ b/backend/backend/urls.py @@ -25,5 +25,5 @@ path('api/', include('friendship_api.urls')), # path('api/', include('friendship.urls')), path('chat/', include('chat.urls')), - + path('game/', include('game.urls')), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/backend/chat/consumers.py b/backend/chat/consumers.py index b6b5c037a..36f283195 100644 --- a/backend/chat/consumers.py +++ b/backend/chat/consumers.py @@ -4,6 +4,9 @@ from channels.exceptions import StopConsumer from asgiref.sync import sync_to_async +import logging + +logger = logging.getLogger(__name__) class ChatConsumer(AsyncWebsocketConsumer): async def connect(self): @@ -12,7 +15,7 @@ async def connect(self): if not self.scope['user'].is_authenticated: await self.close() raise StopConsumer('User is not authenticated') - + logger.info(f'User {self.scope["user"]} connected to the chat.') username = self.scope['user'].username self.room_name = username + '_chat_room' self.room_group_name = 'general_group' @@ -48,8 +51,6 @@ async def connect(self): ) - - async def disconnect(self, close_code): ################# DELETE USER CHANNEL NAME ################# diff --git a/backend/chat/migrations/0005_alter_userchannelname_user.py b/backend/chat/migrations/0005_alter_userchannelname_user.py new file mode 100644 index 000000000..1ef97aee1 --- /dev/null +++ b/backend/chat/migrations/0005_alter_userchannelname_user.py @@ -0,0 +1,21 @@ +# Generated by Django 5.0 on 2024-03-14 23:11 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('chat', '0004_alter_userchannelname_unique_together'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AlterField( + model_name='userchannelname', + name='user', + field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='chat_channel_name', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/backend/chat/models.py b/backend/chat/models.py index 3163add72..04baa5ec0 100644 --- a/backend/chat/models.py +++ b/backend/chat/models.py @@ -55,7 +55,7 @@ class Message(models.Model): timestamp = models.DateTimeField(auto_now_add=True) class UserChannelName(models.Model): - user = models.OneToOneField(AppUser, on_delete=models.CASCADE) + user = models.OneToOneField(AppUser, on_delete=models.CASCADE, related_name='chat_channel_name') channel_name = models.CharField(max_length=100) def update_channel_name(self, channel_name): diff --git a/backend/chat/tests.py b/backend/chat/tests.py deleted file mode 100644 index 7ce503c2d..000000000 --- a/backend/chat/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/backend/game/__init__.py b/backend/game/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/game/admin.py b/backend/game/admin.py new file mode 100644 index 000000000..8c38f3f3d --- /dev/null +++ b/backend/game/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/backend/game/apps.py b/backend/game/apps.py new file mode 100644 index 000000000..8ad49cb8e --- /dev/null +++ b/backend/game/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class GameConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'game' diff --git a/backend/game/consumers.py b/backend/game/consumers.py new file mode 100644 index 000000000..001700da4 --- /dev/null +++ b/backend/game/consumers.py @@ -0,0 +1,410 @@ +import asyncio +import json +import random +import math + +from channels.generic.websocket import AsyncWebsocketConsumer +from channels.db import database_sync_to_async +from channels.exceptions import StopConsumer +from rest_framework.request import Request +import secrets +import string + + +## The consumer will be responsible for handling the game logic and updating the game state(websockets) +def generate_random_string(length): + characters = string.ascii_letters + string.digits + "_" + random_string = "".join(secrets.choice(characters) for _ in range(length)) + return random_string + + +import logging + +logger = logging.getLogger(__name__) + + +class GameConsumer(AsyncWebsocketConsumer): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.game_state = {} + self.condition = asyncio.Condition() + + user_ids = {} + + connected_clients = {} + game_tasks = {} + users = {} + connected_users = set() + async def connect(self): + self.connected_users.add(self.scope["user"].id) + self.room_name = self.scope["url_route"]["kwargs"]["room_name"] + self.room_group_name = f"game_{self.room_name}" + if not self.scope["user"].is_authenticated: + await self.close() + logger.info("User is not authenticated!!!!!") + raise StopConsumer("User is not authenticated") + logger.info(f"connected clients: {self.connected_clients}") + user = self.scope["user"] + if self.room_name in self.connected_clients: + if self.user_ids[self.room_name][0] != user.id: + self.game_instance = self.connected_clients[self.room_name] + self.user_ids[self.room_name][1] = user.id + self.users[self.room_name][1] = user + self.game_state[self.room_name] = "starting" + else: + self.user_ids[self.room_name] = [user.id, None] + self.users[self.room_name] = [user, None] + self.connected_clients[self.room_name] = GameInstance() + self.game_state[self.room_name] = "waiting" + self.game_instance = self.connected_clients[self.room_name] + + await self.channel_layer.group_add(self.room_group_name, self.channel_name) + await self.accept() + # if self.room_name not in self.game_state: + # self.game_state[self.room_name] = "waiting" + + if (self.game_state[self.room_name] != "waiting"): + await self.send_room_info_to_group() + await self.startGame() + + async def send_room_info_to_group(self): + await self.send( + text_data=json.dumps( + { + "type": "room_info", + "room_name": self.room_name, + "game_state": self.game_state.get(self.room_name), + "user_ids": self.user_ids.get(self.room_name), + "users": [str(user) for user in self.users.get(self.room_name)], + } + ) + ) + + async def receive(self, text_data): + game_event = text_data + cmd = str(game_event[:2]) + userid = int(game_event[2:]) + await self.handleInput(cmd, userid) + + async def handleInput(self, cmd, userid): + if cmd == "pw" and userid == self.user_ids[self.room_name][0]: + await self.game_instance.move_p0_up("press") + elif cmd == "ps" and userid == self.user_ids[self.room_name][0]: + await self.game_instance.move_p0_down("press") + elif cmd == "rw" and userid == self.user_ids[self.room_name][0]: + await self.game_instance.move_p0_up("release") + elif cmd == "rs" and userid == self.user_ids[self.room_name][0]: + await self.game_instance.move_p0_down("release") + + elif cmd == "pw" and userid == self.user_ids[self.room_name][1]: + await self.game_instance.move_p1_up("press") + elif cmd == "ps" and userid == self.user_ids[self.room_name][1]: + await self.game_instance.move_p1_down("press") + elif cmd == "rw" and userid == self.user_ids[self.room_name][1]: + await self.game_instance.move_p1_up("release") + elif cmd == "rs" and userid == self.user_ids[self.room_name][1]: + await self.game_instance.move_p1_down("release") + await self.send_game_state_to_clients() + + async def disconnect(self, close_code): + logger.info(f"User disconnected: {self.scope['user'].id}") + self.connected_users.remove(self.scope["user"].id) + if not self.connected_users: + self.game_state = {} + self.connected_clients = {} + + await self.channel_layer.group_discard(self.room_group_name, self.channel_name) + + async def startGame(self): + logger.info(f"Game loop started status: {self.game_state[self.room_name]}") + self.game_state[self.room_name] = "starting" + await self.send_game_state_to_clients() + if self.room_name in self.connected_clients: + self.game_instance = self.connected_clients[self.room_name] + else: + self.connected_clients[self.room_name] = GameInstance() + self.game_instance = self.connected_clients[self.room_name] + if self.room_name not in self.game_tasks: + self.game_tasks[self.room_name] = asyncio.create_task(self.game_loop()) + + async def game_loop(self): + while self.game_state[self.room_name] == "starting" or self.game_state[self.room_name] == "waiting": + await self.game_instance.update_game( + self.game_state, + self.room_name, + self.user_ids[self.room_name][0], + self.user_ids[self.room_name][1], + ) + await asyncio.sleep(0.015625) + await self.send_game_state_to_clients() + self.game_tasks.pop(self.room_name) + await self.send_game_end() + + async def send_game_end(self): + game_tag = generate_random_string(20) + await self.channel_layer.group_send( + self.room_group_name, + { + "type": "ending_message", + "score": self.game_instance.score, + "player0": self.game_instance.player0, + "player1": self.game_instance.player1, + "room_name": self.room_name, + "game_state": self.game_state.get(self.room_name), + "users": [str(user) for user in self.users.get(self.room_name)], + "game_tag": game_tag, + }, + ) + + async def ending_message(self, event): + await self.send( + text_data=json.dumps( + { + "type": "ending_message", + "score": event["score"], + "player0": event["player0"], + "player1": event["player1"], + "room_name": event["room_name"], + "game_state": event["game_state"], + "users": event["users"], + "user_ids": self.user_ids.get(self.room_name), + "game_tag": event["game_tag"], + } + ) + ) + + async def send_game_state_to_clients(self): + await self.channel_layer.group_send( + self.room_group_name, + { + "type": "game_message", + "ball_x": self.game_instance.ball_x, + "ball_y": self.game_instance.ball_y, + "ball_speed_x": self.game_instance.ball_speed_x, + "ball_speed_y": self.game_instance.ball_speed_y, + "score": self.game_instance.score, + "player0": self.game_instance.player0, + "player1": self.game_instance.player1, + "hit": self.game_instance.hit, + "ball_speed": self.game_instance.ball_speed, + "room_name": self.room_name, + "game_state": self.game_state.get(self.room_name), + "w": self.user_ids.get(self.room_name), + "users": [str(user) for user in self.users.get(self.room_name)], + "sender": self.scope["user"].id, + }, + ) + # logger.info(f"Game state sent: {self.game_instance.ball_x}, {self.game_instance.ball_y}, {self.game_instance.score}, {self.game_instance.player0}, {self.game_instance.player1}, {self.game_instance.hit}, {self.game_instance.ball_speed}, {self.room_name}, {self.game_state.get(self.room_name)}, {self.user_ids.get(self.room_name)}, {[str(user) for user in self.users.get(self.room_name)]}") + + async def game_message(self, event): + # This method is called when the group receives a message + await self.send( + text_data=json.dumps( + { + "type": "game_message", + "ball_x": event["ball_x"], + "ball_y": event["ball_y"], + "ball_speed_x": event["ball_speed_x"], + "ball_speed_y": event["ball_speed_y"], + "score": event["score"], + "player0": event["player0"], + "player1": event["player1"], + "hit": event["hit"], + "ball_speed": event["ball_speed"], + "room_name": self.room_name, + "game_state": self.game_state.get(self.room_name), + "user_ids": self.user_ids.get(self.room_name), + "users": [str(user) for user in self.users.get(self.room_name)], + "sender": self.scope["user"].id, + } + ) + ) + + @database_sync_to_async + def get_room(self, room_name): + from game.models import GameRoom + + return GameRoom.objects.get(name=room_name) + + @database_sync_to_async + def get_all_user_channel_names(self): + from .models import UserChannelName + + return list(UserChannelName.objects.values("user__username")) + + +class GameInstance: + def __init__(self): + self.players_ready = 0 + self.ball_size = 5 + self.ball_max_speed = 40 + self.ball_speed_x = 5 + self.ball_x = 50 + self.ball_speed_y = 2 + self.ball_y = 50 + self.canvas_width = 1000 + self.canvas_height = 700 + self.player0 = 295 + self.player0_score = 0 + self.player1 = 295 + self.player1_score = 0 + self.paddle_height = 110 + self.paddle_width = 10 + self.score = "0:0" + self.ball_hit_counter = 1 + self.hit = 0 + self.p0_moving = 0 + self.p1_moving = 0 + self.ball_speed = 0 + self.score_to_win = 2 + + async def move_paddle(self, paddle, direction, state): + if state == "press": + if paddle == "player0": + setattr(self, paddle, self.player0 + direction) + else: + setattr(self, paddle, self.player1 + direction) + elif state == "release": + if paddle == "player0": + setattr(self, paddle, self.player0 + direction) + else: + setattr(self, paddle, self.player1 + direction) + + async def move_p0_up(self, state): + await self.move_paddle("player0", -10, state) + # self.player0 += 1 + + async def move_p0_down(self, state): + await self.move_paddle("player0", 10, state) + + async def move_p1_up(self, state): + await self.move_paddle("player1", -10, state) + + async def move_p1_down(self, state): + await self.move_paddle("player1", 10, state) + + async def update_game(self, game_state, room_name, user0, user1): + """ + Updates the game state + """ + self.player0 += self.p0_moving * 10 + self.player0 = max( + 0, min(self.canvas_height - self.paddle_height, self.player0) + ) + self.player1 += self.p1_moving * 10 + self.player1 = max( + 0, min(self.canvas_height - self.paddle_height, self.player1) + ) + await self.update_ball_position(game_state, room_name, user0, user1) + self.score = f"{self.player0_score} : {self.player1_score}" + self.ball_speed = math.sqrt(self.ball_speed_x**2 + self.ball_speed_y**2) + + async def update_ball_position(self, game_state, room_name, user0, user1): + self.ball_x += self.ball_speed_x + self.ball_y += self.ball_speed_y + + # Handle collision with Player 0 paddle + if ( + self.ball_x - self.ball_size < self.paddle_width + and self.player0 < self.ball_y < self.player0 + self.paddle_height + and self.ball_speed_x < 0 + ): # Check if ball is moving towards the paddle + await self.handle_collision_with_paddle( + self.player0, self.paddle_height, self.paddle_width + ) + + # Handle collision with Player 1 paddle + elif ( + self.ball_x + self.ball_size > self.canvas_width - self.paddle_width + and self.player1 < self.ball_y < self.player1 + self.paddle_height + and self.ball_speed_x > 0 + ): # Check if ball is moving towards the paddle + await self.handle_collision_with_paddle( + self.player1, self.paddle_height, self.paddle_width + ) + + # Handle scoring and ball reset + if self.ball_x < 0 or self.ball_x + self.ball_size > self.canvas_width: + await self.handle_goal( + "player0" if self.ball_x < 0 else "player1", + game_state, + room_name, + user0, + user1, + ) + + # Handle ball collision with top or bottom wall + elif self.is_ball_colliding_with_walls(): + self.handle_wall_collision() + + async def handle_goal(self, player_id, game_state, room_name, user0, user1): + self.ball_hit_counter = 1 + self.ball_speed_x = 5 if player_id == "player0" else -5 + self.ball_speed_y = random.uniform(-2, 2) + self.ball_x = 50 if player_id == "player0" else 750 + self.ball_y = random.uniform(20, 370) + if player_id == "player0": + self.player0_score += 1 + if self.player0_score == self.score_to_win: + game_state[room_name] = f"{self.player0_score} : {self.player1_score}" + self.player1_score = 0 + self.player0_score = 0 + self.player0 = 200 - self.paddle_height / 2 + self.player1 = 200 - self.paddle_height / 2 + else: + self.player1_score += 1 + if self.player1_score == self.score_to_win: + game_state[room_name] = f"{self.player0_score} : {self.player1_score}" + self.player1_score = 0 + self.player0_score = 0 + self.player0 = 200 - self.paddle_height / 2 + self.player1 = 200 - self.paddle_height / 2 + + def handle_wall_collision(self): + if self.ball_y - self.ball_size < 0: + self.ball_y = self.ball_size + self.ball_speed_y = abs(self.ball_speed_y) + elif self.ball_y + self.ball_size > self.canvas_height: + self.ball_y = self.canvas_height - self.ball_size + self.ball_speed_y = -abs(self.ball_speed_y) + + async def handle_collision_with_paddle(self, player, paddle_height, paddle_width): + paddle_hit = (self.ball_y - player) / paddle_height + deviate = (2 * ((paddle_hit - 0.5) ** 2)) + 1 + if paddle_hit < 0.5: + deviate *= -1 + if player == self.player0: + self.ball_x = self.paddle_width + self.ball_size + self.ball_speed_x = abs(self.ball_speed_x) + elif player == self.player1: + self.ball_x = self.canvas_width - self.paddle_width - self.ball_size + self.ball_speed_x = -abs(self.ball_speed_x) + self.ball_hit_counter += 1 + self.ball_speed_x *= 1 + (1 / self.ball_hit_counter) / 2 + self.ball_speed_y *= 1 + (1 / self.ball_hit_counter) / 2 + angle = math.pi / 14 * deviate + magnitude = math.sqrt(self.ball_speed_x**2 + self.ball_speed_y**2) + normalized_speed_x = self.ball_speed_x / magnitude + normalized_speed_y = self.ball_speed_y / magnitude + self.ball_speed_x = normalized_speed_x * math.cos( + angle + ) - normalized_speed_y * math.sin(angle) + self.ball_speed_y = normalized_speed_x * math.sin( + angle + ) + normalized_speed_y * math.cos(angle) + self.ball_speed_x *= magnitude + self.ball_speed_y *= magnitude + magnitude = math.sqrt(self.ball_speed_x**2 + self.ball_speed_y**2) + self.hit = 1 + + def is_ball_colliding_with_walls(self): + return ( + self.ball_y - self.ball_size < 0 + or self.ball_y + self.ball_size > self.canvas_height + ) + + def is_colliding_with_paddle(self, player, paddle_height, paddle_width): + return ( + self.ball_x - self.ball_size < paddle_width + and player < self.ball_y < player + paddle_height + ) diff --git a/backend/game/migrations/0001_initial.py b/backend/game/migrations/0001_initial.py new file mode 100644 index 000000000..bdedc2d34 --- /dev/null +++ b/backend/game/migrations/0001_initial.py @@ -0,0 +1,74 @@ +# Generated by Django 5.0 on 2024-03-14 23:11 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='GameHistory', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('timestamp', models.DateTimeField(auto_now=True, unique_for_date=True)), + ('type', models.CharField(max_length=50)), + ('final_score', models.CharField(max_length=50)), + ('game_tag', models.CharField(max_length=50, null=True)), + ('loser', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='gamehistory_user', to=settings.AUTH_USER_MODEL)), + ('winner', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='gamehistory_winner', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'Game History', + 'verbose_name_plural': 'Game History', + }, + ), + migrations.CreateModel( + name='GameRoom', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ('online', models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL)), + ('user1', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='game_user1', to=settings.AUTH_USER_MODEL)), + ('user2', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='game_user2', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'Game Room', + 'verbose_name_plural': 'Game Rooms', + }, + ), + migrations.CreateModel( + name='GameStats', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('wins', models.IntegerField(default=0)), + ('losses', models.IntegerField(default=0)), + ('tournaments_won', models.IntegerField(default=0)), + ('tournaments_played', models.IntegerField(default=0)), + ('game_history', models.ManyToManyField(blank=True, related_name='game_stats', to='game.gamehistory')), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='user_stats', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'Game Stats', + 'verbose_name_plural': 'Game Stats', + }, + ), + migrations.CreateModel( + name='UserChannelName', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('channel_name', models.CharField(max_length=100)), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='game_channel_name', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'unique_together': {('user', 'channel_name')}, + }, + ), + ] diff --git a/backend/game/migrations/0002_remove_gamestats_game_history_remove_gamestats_user_and_more.py b/backend/game/migrations/0002_remove_gamestats_game_history_remove_gamestats_user_and_more.py new file mode 100644 index 000000000..0072d12ec --- /dev/null +++ b/backend/game/migrations/0002_remove_gamestats_game_history_remove_gamestats_user_and_more.py @@ -0,0 +1,27 @@ +# Generated by Django 5.0 on 2024-04-19 03:22 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('game', '0001_initial'), + ] + + operations = [ + migrations.RemoveField( + model_name='gamestats', + name='game_history', + ), + migrations.RemoveField( + model_name='gamestats', + name='user', + ), + migrations.DeleteModel( + name='GameHistory', + ), + migrations.DeleteModel( + name='GameStats', + ), + ] diff --git a/backend/game/migrations/__init__.py b/backend/game/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/game/models.py b/backend/game/models.py new file mode 100644 index 000000000..5d9b1696c --- /dev/null +++ b/backend/game/models.py @@ -0,0 +1,39 @@ + +from django.db import models +from django.conf import settings +from django.core.serializers import serialize +import json + +# Create your models here. +class GameRoom(models.Model): + name = models.CharField(max_length=255) + user1 = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="game_user1", null=True) + user2 = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="game_user2", null=True) + online = models.ManyToManyField(to=settings.AUTH_USER_MODEL, blank=True) + + def __str__(self): + return f"{self.name}" + + class Meta: + verbose_name = "Game Room" + verbose_name_plural = "Game Rooms" + + + +class UserChannelName(models.Model): + AppUser = settings.AUTH_USER_MODEL + user = models.OneToOneField(AppUser, on_delete=models.CASCADE, related_name='game_channel_name') + channel_name = models.CharField(max_length=100) + + def update_channel_name(self, channel_name): + self.channel_name = channel_name + self.save() + + def delete_channel_name(self): + self.delete() + + class Meta: + unique_together = ('user', 'channel_name',) + + def __str__(self): + return self.user.username + ' ' + self.channel_name \ No newline at end of file diff --git a/backend/game/routing.py b/backend/game/routing.py new file mode 100644 index 000000000..298b0a898 --- /dev/null +++ b/backend/game/routing.py @@ -0,0 +1,6 @@ +from django.urls import re_path +from . import consumers + +game_websocket_urlpatterns = [ + re_path(r"game/(?P\w+)/$", consumers.GameConsumer.as_asgi()) +] \ No newline at end of file diff --git a/backend/game/test.js b/backend/game/test.js new file mode 100644 index 000000000..d6b38bd9c --- /dev/null +++ b/backend/game/test.js @@ -0,0 +1,31 @@ +// const websocketUrl = 'ws://40.13.7.8:8000/game/asdfasdf/'; +const websocketUrl = 'wss://10.12.2.4/chat/'; + + +// Create a new WebSocket instance +const websocket = new WebSocket(websocketUrl); + +// Event listener for WebSocket connection open +websocket.onopen = function(event) { + console.log('WebSocket connection opened.'); + + // Sending a test message to the server + websocket.send(JSON.stringify({ message: 'Hello from the client!' })); +}; + +// Event listener for WebSocket messages received +websocket.onmessage = function(event) { + console.log('Message received from server:', event.data); + // You can add further processing of the received message here +}; + +// Event listener for WebSocket errors +websocket.onerror = function(error) { + console.error('WebSocket error:', error); +}; + +// Event listener for WebSocket connection close +websocket.onclose = function(event) { + console.log('WebSocket connection closed.'); +}; + diff --git a/backend/game/urls.py b/backend/game/urls.py new file mode 100644 index 000000000..4d48a7030 --- /dev/null +++ b/backend/game/urls.py @@ -0,0 +1,8 @@ +from django.urls import path +from . import views + +app_name = "game" + +urlpatterns = [ + path("ending/", views.end_game, name="end_game"), +] diff --git a/backend/game/views.py b/backend/game/views.py new file mode 100644 index 000000000..afdb494c3 --- /dev/null +++ b/backend/game/views.py @@ -0,0 +1,75 @@ +from .models import * +from django.shortcuts import render, redirect +from django.contrib.auth.decorators import login_required +from .models import * +from user_api.models import AppUser +from django.shortcuts import get_object_or_404 +from django.http.response import JsonResponse +from django.views.decorators.http import require_GET, require_POST +from django.shortcuts import get_object_or_404 +from django.views.decorators.csrf import csrf_exempt +from .consumers import GameConsumer + +from django.contrib.sessions.models import Session +from django.contrib.auth.models import User + +import string +import random +import json +import logging +from django.http import JsonResponse +from channels.layers import get_channel_layer +from asgiref.sync import async_to_sync + + +def get_user_from_session(session_key): + session = Session.objects.get(session_key=session_key) + user_id = session.get_decoded().get("_auth_user_id") + user = User.objects.get(id=user_id) + return user + + +logger = logging.getLogger(__name__) + +# Then, instead of print, use logger.info (or logger.debug, logger.warning, etc.) + + +def generate_random_lobby_name(length=10): + # Generate a random string of the given length + lobby_name = "".join( + random.choice(string.ascii_letters + string.digits) for _ in range(length) + ) + return lobby_name + + +def my_handler(message): + user_id = message.get("user_id") + room_name = message.get("room_name") + print(f"User {user_id} joined room {room_name}") + + +def end_game(request): + lobby_info = request.GET.get("gameinfo", None) + score_board = lobby_info.split(" ") + winner_score = score_board[0] + user1_id = score_board[1] + user2_id = score_board[3] + loser_score = score_board[4] + logger.info(f"Game ended with score: {score_board}") + user1 = get_object_or_404(AppUser, id=user1_id) + user2 = get_object_or_404(AppUser, id=user2_id) + + logger.info(f"Game ended with score: {score_board}") + + return JsonResponse( + { + "winner": user1, + "score": winner_score, + + "loser": user2, + "scoree": loser_score, + } + ) + + +# id1 score1 id2 score2 diff --git a/backend/package-lock.json b/backend/package-lock.json new file mode 100644 index 000000000..bb9acfb0d --- /dev/null +++ b/backend/package-lock.json @@ -0,0 +1,114 @@ +{ + "name": "backend", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "wscat": "^5.2.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" + }, + "node_modules/read": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", + "integrity": "sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==", + "dependencies": { + "mute-stream": "~0.0.4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/ws": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", + "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/wscat": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/wscat/-/wscat-5.2.0.tgz", + "integrity": "sha512-UkVzuBdv3jk1Nt0mVCTw0wt/2kGPXry9MZMMUHYE/kEIJdtz1Ez28HD2WQdapC75tM10KZVL8EHG1/WHFK9dtw==", + "dependencies": { + "commander": "^9.3.0", + "https-proxy-agent": "^5.0.0", + "read": "^1.0.7", + "ws": "^8.0.0" + }, + "bin": { + "wscat": "bin/wscat" + } + } + } +} diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 000000000..b5672ddd4 --- /dev/null +++ b/backend/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "wscat": "^5.2.0" + } +} diff --git a/backend/user_api/views.py b/backend/user_api/views.py index ef897da30..58078d1da 100644 --- a/backend/user_api/views.py +++ b/backend/user_api/views.py @@ -45,170 +45,200 @@ class UserRegister(APIView): - permission_classes = (permissions.AllowAny,) - authentication_classes = (BlacklistCheckJWTAuthentication,) + permission_classes = (permissions.AllowAny,) + authentication_classes = (BlacklistCheckJWTAuthentication,) + + def post(self, request): + try: + clean_data = user_registration(request.data) + serializer = UserRegisterSerializer(data=clean_data) + if serializer.is_valid(raise_exception=True): + user = serializer.create(clean_data) + token = RefreshToken.for_user(user) + token["email"] = user.email + token["username"] = user.username + response = Response( + { + "refresh": str(token), + "access": str(token.access_token), + }, + status=status.HTTP_200_OK, + ) + response["Access-Control-Allow-Credentials"] = "true" + return response + except ValidationError as e: + response = Response( + { + "detail": str(e), + }, + status=status.HTTP_400_BAD_REQUEST, + ) + response["Access-Control-Allow-Credentials"] = "true" + return response - def post(self, request): - try: - clean_data = user_registration(request.data) - serializer = UserRegisterSerializer(data=clean_data) - if serializer.is_valid(raise_exception=True): - user = serializer.create(clean_data) - token = RefreshToken.for_user(user) - token['email'] = user.email - token['username'] = user.username - response = Response({ - 'refresh': str(token), - 'access': str(token.access_token), - }, status=status.HTTP_200_OK) - response["Access-Control-Allow-Credentials"] = 'true' - return response - except ValidationError as e: - response = Response({ - 'detail': str(e), - }, status=status.HTTP_400_BAD_REQUEST) - response["Access-Control-Allow-Credentials"] = 'true' - return response - - response = Response({ - }, status=status.HTTP_400_BAD_REQUEST) - response["Access-Control-Allow-Credentials"] = 'true' - return response + response = Response({}, status=status.HTTP_400_BAD_REQUEST) + response["Access-Control-Allow-Credentials"] = "true" + return response class UserLogin(APIView): - permission_classes = (permissions.AllowAny,) - authentication_classes = (BlacklistCheckJWTAuthentication,) - ## - def post(self, request): - if request.user.is_authenticated: - response = Response({ - "detail": "You are already logged in, logout if you want to identify as someone else ;)", - }, status=status.HTTP_400_BAD_REQUEST) - data = request.data - try: - assert is_valid_email(data) - assert is_valid_password(data) - serializer = UserLoginSerializer(data=data) - if serializer.is_valid(raise_exception=True): - user = serializer.check_user(data) - login(request, user) - token = RefreshToken.for_user(user) - token['email'] = user.email - token['username'] = user.username - response = Response({ - 'refresh': str(token), - 'access': str(token.access_token), - }, status=status.HTTP_200_OK) - response["Access-Control-Allow-Credentials"] = 'true' - return response - except ValidationError as e: - response = Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST) - response["Access-Control-Allow-Credentials"] = 'true' - return response + permission_classes = (permissions.AllowAny,) + authentication_classes = (BlacklistCheckJWTAuthentication,) + + ## + def post(self, request): + if request.user.is_authenticated: + response = Response( + { + "detail": "You are already logged in, logout if you want to identify as someone else ;)", + }, + status=status.HTTP_400_BAD_REQUEST, + ) + data = request.data + try: + assert is_valid_email(data) + assert is_valid_password(data) + serializer = UserLoginSerializer(data=data) + if serializer.is_valid(raise_exception=True): + user = serializer.check_user(data) + login(request, user) + token = RefreshToken.for_user(user) + token["email"] = user.email + token["username"] = user.username + response = Response( + { + "refresh": str(token), + "access": str(token.access_token), + }, + status=status.HTTP_200_OK, + ) + response["Access-Control-Allow-Credentials"] = "true" + return response + except ValidationError as e: + response = Response({"detail": str(e)}, status=status.HTTP_400_BAD_REQUEST) + response["Access-Control-Allow-Credentials"] = "true" + return response class UserLogout(APIView): - permission_classes = (permissions.AllowAny,) - authentication_classes = (BlacklistCheckJWTAuthentication,) - ## - def post(self, request): - if request.user.is_authenticated: - # Add the token to the blacklist - token = request.META.get('HTTP_AUTHORIZATION', " ").split(' ')[1] - BlacklistedToken.objects.create(user=request.user, token=token) - response = Response({"detail": "Logged out Successfully"}, status=status.HTTP_200_OK) - response["Access-Control-Allow-Credentials"] = 'true' - return response - else: - response = Response({"detail": "No active user session"}, status=status.HTTP_400_BAD_REQUEST) - response["Access-Control-Allow-Credentials"] = 'true' - return response + permission_classes = (permissions.AllowAny,) + authentication_classes = (BlacklistCheckJWTAuthentication,) + + ## + def post(self, request): + if request.user.is_authenticated: + # Add the token to the blacklist + token = request.META.get("HTTP_AUTHORIZATION", " ").split(" ")[1] + BlacklistedToken.objects.create(user=request.user, token=token) + response = Response( + {"detail": "Logged out Successfully"}, status=status.HTTP_200_OK + ) + response["Access-Control-Allow-Credentials"] = "true" + return response + else: + response = Response( + {"detail": "No active user session"}, status=status.HTTP_400_BAD_REQUEST + ) + response["Access-Control-Allow-Credentials"] = "true" + return response + class UserProfileView(APIView): - permission_classes = (permissions.IsAuthenticated,) - authentication_classes = (BlacklistCheckJWTAuthentication,) - ## - def get(self, request): - serializer = UserSerializer(request.user) - response = Response({'user': serializer.data}, status=status.HTTP_200_OK) - response["Access-Control-Allow-Credentials"] = 'true' - return response + permission_classes = (permissions.IsAuthenticated,) + authentication_classes = (BlacklistCheckJWTAuthentication,) + + ## + def get(self, request): + serializer = UserSerializer(request.user) + response = Response({"user": serializer.data}, status=status.HTTP_200_OK) + response["Access-Control-Allow-Credentials"] = "true" + return response + class UserViewSet(viewsets.ModelViewSet): - authentication_classes = (BlacklistCheckJWTAuthentication,) - queryset = AppUser.objects.all() - serializer_class = UserSerializer - ## - def create(self, request, *args, **kwargs): - response = super().create(request, *args, **kwargs) - email = request.data.get('email', '') - user = AppUser.objects.get(email=email) - refresh = RefreshToken.for_user(user) - response.data['refresh'] = str(refresh) - response.data['access'] = str(refresh.access_token) - response["Access-Control-Allow-Credentials"] = 'true' - return response + authentication_classes = (BlacklistCheckJWTAuthentication,) + queryset = AppUser.objects.all() + serializer_class = UserSerializer + + ## + def create(self, request, *args, **kwargs): + response = super().create(request, *args, **kwargs) + email = request.data.get("email", "") + user = AppUser.objects.get(email=email) + refresh = RefreshToken.for_user(user) + response.data["refresh"] = str(refresh) + response.data["access"] = str(refresh.access_token) + response["Access-Control-Allow-Credentials"] = "true" + return response class updateProfile(APIView): - permission_classes = (permissions.IsAuthenticated,) - authentication_classes = (BlacklistCheckJWTAuthentication,) - ## - def post(self, request): - if request.user.is_authenticated: - data = request.data - if 'profile_picture' in request.FILES: - if default_storage.exists(request.user.profile_picture.name): - default_storage.delete(request.user.profile_picture.name) - image = ImageFile(request.FILES['profile_picture']) - request.user.profile_picture.save(image.name, image, save=True) - if data.get('email'): - request.user.email = data.get('email') - if data.get('username'): - request.user.vusername = data.get('username') - if data.get('title'): - request.user.title = data.get('title') - if data.get('AboutMe'): - request.user.AboutMe = data.get('AboutMe') - if data.get('school'): - request.user.school = data.get('school') - if data.get('wins'): - request.user.wins = data.get('wins') - if data.get('losses'): - request.user.losses = data.get('losses') - if data.get('win_rate'): - if request.user.total_matches == 0: - request.user.win_rate = 0 - else: - request.user.win_rate = request.user.wins / request.user.total_matches - if data.get('total_matches'): - request.user.total_matches = data.get('total_matches') - if data.get('match_history'): - history = request.user.match_history - if history is None: - history = [] - history.append(data.get('match_history')) - request.user.match_history = history - if data.get('TwoFA'): - request.user.TwoFA = data.get('TwoFA') - if data.get('password'): - request.user.set_password(data.get('password')) - if data.get('ft_user'): - request.user.ft_user = data.get('ft_user') - request.user.save() - - response = Response({ - "detail": "Profile updated", - }, status=status.HTTP_200_OK) - response["Access-Control-Allow-Credentials"] = 'true' - return response - else: - response = Response({ - "detail": "No active user session", - }, status=status.HTTP_400_BAD_REQUEST) - response["Access-Control-Allow-Credentials"] = 'true' - return response + permission_classes = (permissions.IsAuthenticated,) + authentication_classes = (BlacklistCheckJWTAuthentication,) + + ## + def post(self, request): + if request.user.is_authenticated: + data = request.data + if "profile_picture" in request.FILES: + if default_storage.exists(request.user.profile_picture.name): + default_storage.delete(request.user.profile_picture.name) + image = ImageFile(request.FILES["profile_picture"]) + request.user.profile_picture.save(image.name, image, save=True) + if data.get("email"): + request.user.email = data.get("email") + if data.get("username"): + request.user.vusername = data.get("username") + if data.get("title"): + request.user.title = data.get("title") + if data.get("AboutMe"): + request.user.AboutMe = data.get("AboutMe") + if data.get("school"): + request.user.school = data.get("school") + if data.get("wins"): + request.user.wins = data.get("wins") + if data.get("losses"): + request.user.losses = data.get("losses") + if data.get("win_rate"): + if request.user.total_matches == 0: + request.user.win_rate = 0 + else: + request.user.win_rate = ( + request.user.wins / request.user.total_matches + ) + if data.get("total_matches"): + request.user.total_matches = data.get("total_matches") + if data.get("match_history"): + history = request.user.match_history + if history is None: + history = [] + history.append(data.get("match_history")) + request.user.match_history = history + if data.get("TwoFA"): + request.user.TwoFA = data.get("TwoFA") + if data.get("password"): + request.user.set_password(data.get("password")) + if data.get("ft_user"): + request.user.ft_user = data.get("ft_user") + request.user.save() + + response = Response( + { + "detail": "Profile updated", + }, + status=status.HTTP_200_OK, + ) + response["Access-Control-Allow-Credentials"] = "true" + return response + else: + response = Response( + { + "detail": "No active user session", + }, + status=status.HTTP_400_BAD_REQUEST, + ) + response["Access-Control-Allow-Credentials"] = "true" + return response class OAuthCallback(APIView): @@ -290,123 +320,151 @@ def get(self, request): class OAuthAuthorize(APIView): - permission_classes = (permissions.AllowAny,) - ## - def get(self, request): - auth_url = "https://api.intra.42.fr/oauth/authorize" - params = { - "client_id": os.environ.get("UID"), - "redirect_uri": settings.REDIRECT_URI + "/api/oauth/callback/", - "response_type": "code", - } - response = HttpResponseRedirect(f"{auth_url}?{urllib.parse.urlencode(params)}") - response["Access-Control-Allow-Credentials"] = 'true' - return response + permission_classes = (permissions.AllowAny,) + + ## + def get(self, request): + auth_url = "https://api.intra.42.fr/oauth/authorize" + params = { + "client_id": os.environ.get("UID"), + "redirect_uri": settings.REDIRECT_URI + "/api/oauth/callback/", + "response_type": "code", + } + response = HttpResponseRedirect(f"{auth_url}?{urllib.parse.urlencode(params)}") + response["Access-Control-Allow-Credentials"] = "true" + return response class accountDeletion(APIView): - permission_classes = (permissions.IsAuthenticated,) - authentication_classes = (BlacklistCheckJWTAuthentication,) - ## - def post(self, request): - if request.user.is_authenticated: - request.user.delete() - response = Response({"detail": "Account deleted"}, status=status.HTTP_200_OK) - response["Access-Control-Allow-Credentials"] = 'true' - return response - else: - response = Response({"detail": "No active user session"}, status=status.HTTP_400_BAD_REQUEST) - response["Access-Control-Allow-Credentials"] = 'true' - return response + permission_classes = (permissions.IsAuthenticated,) + authentication_classes = (BlacklistCheckJWTAuthentication,) + + ## + def post(self, request): + if request.user.is_authenticated: + request.user.delete() + response = Response( + {"detail": "Account deleted"}, status=status.HTTP_200_OK + ) + response["Access-Control-Allow-Credentials"] = "true" + return response + else: + response = Response( + {"detail": "No active user session"}, status=status.HTTP_400_BAD_REQUEST + ) + response["Access-Control-Allow-Credentials"] = "true" + return response class activateTwoFa(APIView): - permission_classes = (permissions.IsAuthenticated,) - authentication_classes = (BlacklistCheckJWTAuthentication,) - ## - def post(self, request): - if request.user.is_authenticated: - request.user.TwoFA = True - request.user.save() - response = Response({"detail": "Two Factor Authentication activated"}, status=status.HTTP_200_OK) - response["Access-Control-Allow-Credentials"] = 'true' - return response - else: - response = Response({"detail": "No active user session"}, status=status.HTTP_400_BAD_REQUEST) - response["Access-Control-Allow-Credentials"] = 'true' - return response + permission_classes = (permissions.IsAuthenticated,) + authentication_classes = (BlacklistCheckJWTAuthentication,) + + ## + def post(self, request): + if request.user.is_authenticated: + request.user.TwoFA = True + request.user.save() + response = Response( + {"detail": "Two Factor Authentication activated"}, + status=status.HTTP_200_OK, + ) + response["Access-Control-Allow-Credentials"] = "true" + return response + else: + response = Response( + {"detail": "No active user session"}, status=status.HTTP_400_BAD_REQUEST + ) + response["Access-Control-Allow-Credentials"] = "true" + return response class deactivateTwoFa(APIView): - permission_classes = (permissions.IsAuthenticated,) - authentication_classes = (BlacklistCheckJWTAuthentication,) - ## - def post(self, request): - if request.user.is_authenticated: - request.user.TwoFA = False - request.user.save() - response = Response({"detail": "Two Factor Authentication deactivated"}, status=status.HTTP_200_OK) - response["Access-Control-Allow-Credentials"] = 'true' - return response - else: - response = Response({"detail": "No active user session"}, status=status.HTTP_400_BAD_REQUEST) - response["Access-Control-Allow-Credentials"] = 'true' - return response - + permission_classes = (permissions.IsAuthenticated,) + authentication_classes = (BlacklistCheckJWTAuthentication,) + + ## + def post(self, request): + if request.user.is_authenticated: + request.user.TwoFA = False + request.user.save() + response = Response( + {"detail": "Two Factor Authentication deactivated"}, + status=status.HTTP_200_OK, + ) + response["Access-Control-Allow-Credentials"] = "true" + return response + else: + response = Response( + {"detail": "No active user session"}, status=status.HTTP_400_BAD_REQUEST + ) + response["Access-Control-Allow-Credentials"] = "true" + return response -class sendQrCode(APIView): - permission_classes = (permissions.IsAuthenticated,) - authentication_classes = (BlacklistCheckJWTAuthentication,) - ## - def get_user_totp_device(self,user, confirmed=None): - devices = devices_for_user(user, confirmed=confirmed) - for device in devices: - if isinstance(device, TOTPDevice): - return device - def post(self, request): - if request.user.is_authenticated: - if request.user.TwoFA: - device = self.get_user_totp_device(request.user) - if not device: - device = request.user.totpdevice_set.create(confirmed=True) - current_site = get_current_site(request) - - img = qrcode.make(device.config_url) - - mail_subject = 'DJANGO OTP DEMO' - byte_stream = BytesIO() - img.save(byte_stream, format='PNG') - byte_stream.seek(0) - - mail_subject = 'Iiinteernaaal Pooiinteeer Vaariaaablee' - message = f"Hello {request.user},\n\nYour QR Code is: " - to_email = request.user.email - email = EmailMessage( - mail_subject, message, to=[to_email] - ) - - fp = open('qrcode.png', 'rb') - msg_image = MIMEImage(fp.read()) - fp.close() - msg_image = MIMEImage(byte_stream.getvalue()) - msg_image.add_header('Content-ID', '') - email.attach(msg_image) - - email.content_subtype = "html" - email.send() - messages.success(request, ('Please Confirm your email to complete registration.')) - response = Response({"detail": "QR Code sent to your email"}, status=status.HTTP_200_OK) - response["Access-Control-Allow-Credentials"] = 'true' - return response - else: - response = Response({"detail": "Two Factor Authentication is not activated"}, status=status.HTTP_400_BAD_REQUEST) - response["Access-Control-Allow-Credentials"] = 'true' - return response - else: - response = Response({"detail": "No active user session"}, status=status.HTTP_400_BAD_REQUEST) - response["Access-Control-Allow-Credentials"] = 'true' - return response +class sendQrCode(APIView): + permission_classes = (permissions.IsAuthenticated,) + authentication_classes = (BlacklistCheckJWTAuthentication,) + + ## + def get_user_totp_device(self, user, confirmed=None): + devices = devices_for_user(user, confirmed=confirmed) + for device in devices: + if isinstance(device, TOTPDevice): + return device + + def post(self, request): + if request.user.is_authenticated: + if request.user.TwoFA: + device = self.get_user_totp_device(request.user) + if not device: + device = request.user.totpdevice_set.create(confirmed=True) + current_site = get_current_site(request) + + img = qrcode.make(device.config_url) + + mail_subject = "DJANGO OTP DEMO" + byte_stream = BytesIO() + img.save(byte_stream, format="PNG") + byte_stream.seek(0) + + mail_subject = "Iiinteernaaal Pooiinteeer Vaariaaablee" + message = ( + f"Hello {request.user},\n\nYour QR Code is: " + ) + to_email = request.user.email + email = EmailMessage(mail_subject, message, to=[to_email]) + + fp = open("qrcode.png", "rb") + msg_image = MIMEImage(fp.read()) + fp.close() + msg_image = MIMEImage(byte_stream.getvalue()) + msg_image.add_header("Content-ID", "") + email.attach(msg_image) + + email.content_subtype = "html" + email.send() + messages.success( + request, ("Please Confirm your email to complete registration.") + ) + response = Response( + {"detail": "QR Code sent to your email"}, status=status.HTTP_200_OK + ) + response["Access-Control-Allow-Credentials"] = "true" + return response + else: + response = Response( + {"detail": "Two Factor Authentication is not activated"}, + status=status.HTTP_400_BAD_REQUEST, + ) + response["Access-Control-Allow-Credentials"] = "true" + return response + else: + response = Response( + {"detail": "No active user session"}, status=status.HTTP_400_BAD_REQUEST + ) + response["Access-Control-Allow-Credentials"] = "true" + return response class TwoFactorAuth(APIView): @@ -451,4 +509,4 @@ def user_to_dict(user): user_dict['profile_picture'] = None # keep only fields : email, username, profile_picture, title, school, intra_level user_dict = {key: user_dict[key] for key in ['email', 'username', 'profile_picture', 'title', 'school', 'intra_level', 'ft_url']} - return user_dict \ No newline at end of file + return user_dict diff --git a/docker-compose-linux-x86_64 b/docker-compose-linux-x86_64 new file mode 100755 index 000000000..1ab9df24d Binary files /dev/null and b/docker-compose-linux-x86_64 differ diff --git a/docker-compose.yaml b/docker-compose.yaml old mode 100644 new mode 100755 index 052f2df05..a8956b4da --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -85,6 +85,7 @@ services: args: FRONTEND_URL: ${FRONTEND_URL} BACKEND_URL: ${BACKEND_URL} + BACKEND_URLL: ${BACKEND_URLL} SERVER: ${SERVER} LOCATION: ${LOCATION} entrypoint: /app/docker/nginx/nginx.sh diff --git a/docker/backend/backend.sh b/docker/backend/backend.sh index f4c416780..222e42029 100755 --- a/docker/backend/backend.sh +++ b/docker/backend/backend.sh @@ -29,11 +29,12 @@ echo "alias get='http --follow --timeout 6'" >> /root/.bashrc # python manage.py makemigrations && python manage.py migrate # tail -f /var/log/gunicorn/dev.log +export DJANGO_SETTINGS_MODULE=backend.settings +python auto_reload.py + +sleep 10 -############################################ -# daphne server # -############################################ mkdir -pv /var/{log,run}/daphne/ python manage.py makemigrations && python manage.py migrate # daphne -u /tmp/daphne.sock backend.asgi:application diff --git a/docker/nginx/config/backend.conf b/docker/nginx/config/backend.conf index cd01c5946..c1f497853 100644 --- a/docker/nginx/config/backend.conf +++ b/docker/nginx/config/backend.conf @@ -6,8 +6,8 @@ server { server_name ${SERVER}; listen 80; listen 443 ssl; - ssl_certificate /etc/ssl/localhost.crt; - ssl_certificate_key /etc/ssl/localhost.key; + ssl_certificate /etc/ssl/10.12.2.4.crt; + ssl_certificate_key /etc/ssl/10.12.2.4.key; location / { proxy_pass ${FRONTEND_URL}; @@ -44,6 +44,39 @@ server { add_header 'Access-Control-Allow-Headers' 'Origin, X-Requested-With, Content-Type, Accept, Authorization' always; } + location /ws { + proxy_pass ${FRONTEND_URL}; + proxy_set_header Host $host; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Origin $http_origin; + proxy_set_header Connection "Upgrade"; + + add_header 'Access-Control-Allow-Origin' '*' always; + add_header 'Access-Control-Allow-Credentials' 'true' always; + add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always; + add_header 'Access-Control-Allow-Headers' 'Origin, X-Requested-With, Content-Type, Accept, Authorization' always; + } + + location /game/ { + # if ($http_upgrade != "websocket") { + # rewrite ^/game/(.*)$ /$1 break; + # proxy_pass http://backend:8000; + # } + + proxy_pass http://backend:8000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + location /media/ { alias /app/backend/media/; } @@ -53,6 +86,7 @@ server { rewrite ^/chat/(.*)$ /$1 break; proxy_pass ${FRONTEND_URL}; } + proxy_pass http://backend:8000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; diff --git a/docker/nginx/config/nginx.conf b/docker/nginx/config/nginx.conf index 80b242796..5404c0189 100644 --- a/docker/nginx/config/nginx.conf +++ b/docker/nginx/config/nginx.conf @@ -66,18 +66,18 @@ http { # # See sample authentication script at: # # http://wiki.nginx.org/ImapAuthenticateWithApachePhpScript # -# # auth_http localhost/auth.php; +# # auth_http 10.12.2.4/auth.php; # # pop3_capabilities "TOP" "USER"; # # imap_capabilities "IMAP4rev1" "UIDPLUS"; # # server { -# listen localhost:110; +# listen 10.12.2.4:110; # protocol pop3; # proxy on; # } # # server { -# listen localhost:143; +# listen 10.12.2.4:143; # protocol imap; # proxy on; # } diff --git a/docker/nginx/nginx.sh b/docker/nginx/nginx.sh index 32360adff..022888cf6 100755 --- a/docker/nginx/nginx.sh +++ b/docker/nginx/nginx.sh @@ -12,8 +12,8 @@ sed -i 's|${BACKEND_URL}|'${BACKEND_URL}'|g' /etc/nginx/sites-available/backend sed -i 's|${SERVER}|'${SERVER}'|g' /etc/nginx/sites-available/backend sed -i 's|${LOCATION}|'${LOCATION}'|g' /etc/nginx/sites-available/backend -openssl req -x509 -nodes -new -sha256 -days 1024 -newkey rsa:2048 -keyout /etc/ssl/localhost.key -out /etc/ssl/localhost.pem -subj "/C=DE/CN=localhost" -openssl x509 -outform pem -in /etc/ssl/localhost.pem -out /etc/ssl/localhost.crt +openssl req -x509 -nodes -new -sha256 -days 1024 -newkey rsa:2048 -keyout /etc/ssl/10.12.2.4.key -out /etc/ssl/10.12.2.4.pem -subj "/C=DE/CN=10.12.2.4" +openssl x509 -outform pem -in /etc/ssl/10.12.2.4.pem -out /etc/ssl/10.12.2.4.crt cd /etc/nginx/sites-enabled diff --git a/docker/nginx/nginx_deployment.sh b/docker/nginx/nginx_deployment.sh index 64bd31ad3..2a2383f87 100644 --- a/docker/nginx/nginx_deployment.sh +++ b/docker/nginx/nginx_deployment.sh @@ -6,8 +6,8 @@ sed -i 's|${FRONTEND_URL}|'${FRONTEND_URL}'|g' /etc/nginx/sites-available/backen sed -i 's|${BACKEND_URL}|'${BACKEND_URL}'|g' /etc/nginx/sites-available/backend sed -i 's|${SERVER}|'${SERVER}'|g' /etc/nginx/sites-available/backend -openssl req -x509 -nodes -new -sha256 -days 1024 -newkey rsa:2048 -keyout /etc/ssl/localhost.key -out /etc/ssl/localhost.pem -subj "/C=DE/CN=localhost" -openssl x509 -outform pem -in /etc/ssl/localhost.pem -out /etc/ssl/localhost.crt +openssl req -x509 -nodes -new -sha256 -days 1024 -newkey rsa:2048 -keyout /etc/ssl/10.12.2.4.key -out /etc/ssl/10.12.2.4.pem -subj "/C=DE/CN=10.12.2.4" +openssl x509 -outform pem -in /etc/ssl/10.12.2.4.pem -out /etc/ssl/10.12.2.4.crt cd /etc/nginx/sites-enabled diff --git a/en.subject.pdf b/en.subject.pdf old mode 100644 new mode 100755 diff --git a/frontend/README.md b/frontend/README.md index 58beeaccd..4ca4260eb 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -9,7 +9,7 @@ In the project directory, you can run: ### `npm start` Runs the app in the development mode.\ -Open [http://localhost:3000](http://localhost:3000) to view it in your browser. +Open [http://10.12.2.4:3000](http://10.12.2.4:3000) to view it in your browser. The page will reload when you make changes.\ You may also see any lint errors in the console. diff --git a/frontend/package.json b/frontend/package.json index e4c228075..3065f52d7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -8,7 +8,7 @@ "watch:css": "postcss src/styles/tailwind.css -o src/index.css --watch", "start": "REACT_APP_LOCAL_URI=https://10.12.2.2 react-scripts start", "build": "HOMEPAGE=https://zstenger93.github.io/Transcendence/ node SetHomepage.js && CI=false NODE_ENV=production REACT_APP_REDIRECT_URI=https://transcendence-backend-znhl.onrender.com react-scripts build", - "local_build": "node SetHomepage.js && CI=false NODE_ENV=production REACT_APP_REDIRECT_URI=https://localhost react-scripts build", + "local_build": "node SetHomepage.js && CI=false NODE_ENV=production REACT_APP_REDIRECT_URI=https://10.12.2.4 react-scripts build", "test": "react-scripts test --detectOpenHandles", "eject": "react-scripts eject" }, diff --git a/frontend/src/components/API.js b/frontend/src/components/API.js index 7749b9a06..ac0d7dddc 100644 --- a/frontend/src/components/API.js +++ b/frontend/src/components/API.js @@ -1,6 +1,23 @@ import axios from "axios"; import Cookies from "js-cookie"; +export const getGameRoom = async ({ redirectUri, roomName }) => { + let response = {}; + try { + const token = Cookies.get("access"); + const csrfToken = Cookies.get("csrftoken"); + response = await axios.get(`${redirectUri}/game/1v1/asdfasdf/`, { + headers: { + Authorization: `Bearer ${token}`, + }, + withCredentials: true, + }); + } catch (error) { + console.log(error); + } + return response.data; + }; + export const getFriendList = async ({ redirectUri }) => { let response = {}; try { diff --git a/frontend/src/pages/Games/OriginalPong.js b/frontend/src/pages/Games/OriginalPong.js index db5b13693..c5f55d158 100644 --- a/frontend/src/pages/Games/OriginalPong.js +++ b/frontend/src/pages/Games/OriginalPong.js @@ -1,344 +1,276 @@ import React, { useEffect, useRef, useState } from "react"; -import backgroundImage from "../../images/pongbg.png"; -import { useLocation, useNavigate } from "react-router-dom"; -import BackButton from "../../components/buttons/BackButton"; -import { useTranslation } from "react-i18next"; -import { WelcomeButtonStyle } from "../../components/buttons/ButtonStyle"; -import LoseScreen from "../../components/game/LoseScreen"; -import WinScreen from "../../components/game/WinScreen"; -import handleResize from "../../components/game/HandleResize"; -import FullScreenButton from "../../components/buttons/FullScreen"; -import Cookies from "js-cookie"; -const GameCanvas = () => { - useEffect(() => { - setTimeout(() => { - const accessToken = Cookies.get("access"); +const Pong = () => { + const sender = useRef(null); + const player0 = useRef(null); + const player1 = useRef(null); + const gameSocket = useRef(null); - if (!accessToken) { - window.location.href = "/404.html"; + useEffect(() => { + function randomString(length) { + var result = ""; + var characters = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + var charactersLength = characters.length; + for (var i = 0; i < length; i++) { + result += characters.charAt(Math.floor(Math.random() * charactersLength)); } - }, 1000); - }, []); - // Default Parameters - let playerSpeed = 5; - const playerSpeedIncrease = 500; - let resize = true; - const defaultSpeedX = 300; - const winScore = 10; - const defaultSpeedY = 20; - const [scoreLeftReact, setScoreLeft] = useState(0); - const [scoreRightReact, setScoreRight] = useState(0); - let ballSize = 8; - let scoreLeft = 0; - let scoreRight = 0; - const canvasRef = useRef(null); - let paddleWidth = canvasRef.current ? canvasRef.current.width / 70 : 0; - let paddleHeight = canvasRef.current ? canvasRef.current.width / 20 : 0; - let leftPaddleY = canvasRef.current - ? canvasRef.current.height / 2 - paddleHeight / 2 - : 0; - let rightPaddleY = canvasRef.current - ? canvasRef.current.height / 2 - paddleHeight / 2 - : 0; - let ballX = canvasRef.current ? canvasRef.current.width / 2 : 5; - let ballY = canvasRef.current ? canvasRef.current.height / 2 : 5; - let ballSpeedX = defaultSpeedX; - let ballSpeedY = defaultSpeedY; - let canvasDefaultWidth = 1920; - let sizeSpeedRatio = canvasRef.current - ? canvasRef.current.width / canvasDefaultWidth - : 1; - let lastFrame = 0; - let dt = 0; + return result; + } - // This Function Adds A White Stripe in The middle of the map - const drawWhiteStripe = (ctx, canvas) => { - ctx.fillStyle = "#FFFFFF"; - const stripeWidth = 8; - const stripeHeight = canvas.height; - const x = canvas.width / 2 - stripeWidth / 2; - const y = 0; - ctx.fillRect(x, y, stripeWidth, stripeHeight); - }; + async function getData() { - // This function Updates The Ball Positions - const updateBallPosition = (canvas) => { - const ballAngleOffset = 0.02; - const ballSpeedIncrease = 50; - ballX += ballSpeedX * dt * sizeSpeedRatio; - ballY += ballSpeedY * dt * sizeSpeedRatio; - if (ballY < 0) { - ballY = 2; - ballSpeedY = -ballSpeedY; - } else if (ballY > canvas.height) { - ballY = canvas.height - 2; - ballSpeedY = -ballSpeedY; - } - if ( - ballX < paddleWidth + ballSize * sizeSpeedRatio && - ballY > leftPaddleY && - ballY < leftPaddleY + paddleHeight - ) { - const leftPaddleCenterY = leftPaddleY + paddleHeight / 2; - const distanceFromCenter = ballY - leftPaddleCenterY; - ballX = paddleWidth + 10; - ballSpeedX *= -1; - if (ballSpeedX < 0) ballSpeedX -= ballSpeedIncrease; - else ballSpeedX += ballSpeedIncrease; - ballSpeedY += - distanceFromCenter * - ballAngleOffset * - sizeSpeedRatio * - Math.abs(ballSpeedX); - } else if ( - ballX > canvas.width - paddleWidth - ballSize * sizeSpeedRatio && - ballY > rightPaddleY && - ballY < rightPaddleY + paddleHeight - ) { - const rightPaddleCenterY = rightPaddleY + paddleHeight / 2; - const distanceFromCenter = ballY - rightPaddleCenterY; - ballX = canvas.width - paddleWidth - 10; - ballSpeedX *= -1; - if (ballSpeedX < 0) ballSpeedX -= ballSpeedIncrease; - else { - ballSpeedX += ballSpeedIncrease; - playerSpeed += playerSpeedIncrease; + let room_name = randomString(10); + if (!gameSocket.current) { + gameSocket.current = new WebSocket("wss://10.12.2.4/game/pong/"); } - ballSpeedY += - distanceFromCenter * - ballAngleOffset * - sizeSpeedRatio * - Math.abs(ballSpeedX); - } else if (ballX + ballSize * 3 < 0) { - ballX = canvas.width / 2; - ballY = canvas.height / 2; - ballSpeedX = defaultSpeedX; - ballSpeedY = defaultSpeedY; - scoreRight += 1; - playerSpeed = 5; - setScoreRight(scoreRight); - } else if (ballX - ballSize * 3 > canvas.width) { - ballX = canvas.width / 2; - ballY = canvas.height / 2; - ballSpeedX = -defaultSpeedX; - ballSpeedY = -defaultSpeedY; - scoreLeft += 1; - playerSpeed = 5; - setScoreLeft(scoreLeft); - } - }; + gameSocket.current.onopen = function (event) { + console.log("Data:" + JSON.stringify(receivedData)); + console.log("WebSocket is open now.!!!!!!!!!!!!!!!"); + }; + gameSocket.current.onclose = function(event) { + console.log('WebSocket is closed now.!!!!!!!!!!!!!!', event.data); + }; + gameSocket.onerror = function(error) { + console.error('WebSocket error:?????????', error); + }; + + let canvas = document.getElementById("gameCanvas"); + canvas.width = 1000; + canvas.height = 700; + let context = canvas.getContext("2d"); + let receivedData; + gameSocket.current.onmessage = function (event) { + receivedData = JSON.parse(event.data); + + if (receivedData["type"] === "game_message") { + sender.current = receivedData["sender"]; + player0.current = receivedData["users"][0]; + player1.current = receivedData["users"][1]; + renderGameFrame(receivedData); + } + else if (receivedData["type"] === "ending_message") { + console.log("Received Data: " + JSON.stringify(receivedData)); + clearCanvas(); + displayEndScore(receivedData["game_state"]); + gameSocket.current.close(); + } + }; - // this function draws scores - const drawScores = (ctx, canvas) => { - ctx.fillStyle = "#FFFFFF"; - ctx.font = "80px Helvetica"; - ctx.fillText(`${scoreLeft}`, canvas.width / 2 - 100, 100); - ctx.fillText(`${scoreRight}`, canvas.width / 2 + 60, 100); - }; - // this Function Surprise draws a ball - const drawBall = (ctx, canvas) => { - ctx.beginPath(); - ctx.arc(ballX, ballY, ballSize * sizeSpeedRatio, 0, Math.PI * 2); - ctx.fillStyle = "#00FF00"; - ctx.fill(); - ctx.closePath(); - }; + const renderGameFrame = (gameData) => { + if (context && canvas) { + clearCanvas(); + drawField(); + var score = player0.current + " " + gameData.score + " " + player1.current; + displayScore(score); + drawPaddle(0, gameData.player0, 10, 110); + drawPaddle(1, gameData.player1, 10, 110); + drawBall( + gameData.ball_x, + gameData.ball_y, + gameData.ball_speed_x, + gameData.ball_speed_y, + gameData.ball_speed + ); + } + }; - const draw = (timestamp) => { - const canvas = canvasRef.current; - if (!canvas) return; - const ctx = canvas.getContext("2d"); - dt = (timestamp - lastFrame) / 1000; - lastFrame = timestamp; - paddleWidth = canvasRef.current ? canvasRef.current.width / 80 : 0; - paddleHeight = canvasRef.current ? canvasRef.current.width / 20 : 0; - ctx.clearRect(0, 0, canvas.width, canvas.height); - drawWhiteStripe(ctx, canvas); - ctx.fillStyle = "#FF3366"; - ctx.fillRect(0, leftPaddleY, paddleWidth, paddleHeight); - ctx.fillRect( - canvas.width - paddleWidth, - rightPaddleY, - paddleWidth, - paddleHeight - ); - updateBallPosition(canvas); - drawBall(ctx, canvas); - drawScores(ctx, canvas); - requestAnimationFrame(draw); - handleKeys(); - }; + const clearCanvas = () => { + if (context && canvas) { + context.clearRect(0, 0, canvas.width, canvas.height); + } + }; - const keysPressed = {}; + function hexToRgb(hex) { + // Remove the hash if it exists + hex = hex.replace(/^#/, ""); - const handleKeys = () => { - if (canvasRef.current) { - // Left paddle controls - // eslint-disable-next-line react-hooks/exhaustive-deps - if (keysPressed["w"]) leftPaddleY -= playerSpeed * sizeSpeedRatio; - if (keysPressed["s"]) leftPaddleY += playerSpeed * sizeSpeedRatio; - leftPaddleY = Math.max( - 0, - Math.min(leftPaddleY, canvasRef.current.height - paddleHeight) - ); + // Parse the hex values to separate R, G, B components + const bigint = parseInt(hex, 16); + const r = (bigint >> 16) & 255; + const g = (bigint >> 8) & 255; + const b = bigint & 255; - // Right paddle controls - if (keysPressed["ArrowUp"]) - // eslint-disable-next-line react-hooks/exhaustive-deps - rightPaddleY -= playerSpeed * sizeSpeedRatio; - if (keysPressed["ArrowDown"]) - rightPaddleY += playerSpeed * sizeSpeedRatio; - rightPaddleY = Math.max( - 0, - Math.min(rightPaddleY, canvasRef.current.height - paddleHeight) - ); - } - }; + // Return the RGB values as an object + return { r, g, b }; + } - useEffect(() => { - const handleKeyDown = (event) => { - keysPressed[event.key] = true; - }; + const drawBall = (x, y, speedX, speedY, ballSpeed) => { + if (context) { + // Define a color gradient based on speed + const colorGradient = [ + { speed: 7, color: "#ffff00" }, + { speed: 10, color: "#ff4d00" }, + { speed: 13, color: "#ff0000" }, + { speed: 18, color: "#0000ff" }, + { speed: 50, color: "#bf00ff" }, + ]; + + // Find the color corresponding to the ball's speed in the gradient + let ballColor = "white"; // Default color + for (const { speed, color } of colorGradient) { + if (ballSpeed <= speed) { + ballColor = color; + break; + } + } - const handleKeyUp = (event) => { - keysPressed[event.key] = false; - }; + // Display the magnitude of the speed + context.fillStyle = ballColor; + context.font = "14px Arial"; + let show_speed = `Speed: ${ballSpeed.toFixed(3)}`; + context.fillText(show_speed, x - 30, y - 30); - // touchpad controlls - const handleTouchMove = (event) => { - if (canvasRef.current) { - const touches = event.touches; - const rect = canvasRef.current.getBoundingClientRect(); - for (let i = 0; i < touches.length; i++) { - const touch = touches[i]; - const touchY = event.touches[i].clientY - rect.top - window.scrollY; - // Left paddle controls - if (touch.clientX < window.innerWidth / 2) { - // eslint-disable-next-line react-hooks/exhaustive-deps - leftPaddleY = touchY - paddleHeight / 2; - leftPaddleY = Math.max( + // Draw the main ball + context.fillStyle = ballColor; + context.beginPath(); + context.arc(x, y, 10, 0, Math.PI * 2); + context.fill(); + context.closePath(); + + // Draw the trailing circles + const trailCount = 5; // Adjust the number of circles in the trail + const trailSpacing = 1.5; // Adjust the spacing between circles in the trail + + for (let i = 1; i <= trailCount; i++) { + const trailOpacity = 1 - i / trailCount; + const trailRadius = i * trailSpacing; + + const rgb = hexToRgb(ballColor); + context.fillStyle = `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${trailOpacity})`; // Yellow color with variable opacity + context.beginPath(); + context.arc( + x - speedX * trailRadius, + y - speedY * trailRadius, + 10, 0, - Math.min(leftPaddleY, canvasRef.current.height - paddleHeight) + Math.PI * 2 ); + context.fill(); + context.closePath(); } - // Right paddle controls - else { - // eslint-disable-next-line react-hooks/exhaustive-deps - rightPaddleY = touchY - paddleHeight / 2; - rightPaddleY = Math.max( - 0, - Math.min(rightPaddleY, canvasRef.current.height - paddleHeight) - ); + } + }; + + const drawPaddle = (player, y, width, height) => { + if (context) { + if (player === 0) { + context.fillStyle = "red"; // Player 0 Paddle color + context.fillRect(0, y, width, height); + } else { + context.fillStyle = "blue"; // Player 1 Paddle color + context.fillRect(990, y, width, height); } } - } - }; + }; - window.addEventListener("keyup", handleKeyUp); - window.addEventListener("keydown", handleKeyDown); - document.addEventListener("touchmove", handleTouchMove); - window.addEventListener("resize", () => - handleResize( - canvasRef, - resize, - paddleWidth, - paddleHeight, - sizeSpeedRatio, - canvasDefaultWidth, - ballX, - ballY, - leftPaddleY, - rightPaddleY - ) - ); - handleResize( - canvasRef, - resize, - paddleWidth, - paddleHeight, - sizeSpeedRatio, - canvasDefaultWidth, - ballX, - ballY, - leftPaddleY, - rightPaddleY - ); - draw(0); + const drawField = () => { + if (context) { + context.fillStyle = "#000000"; + context.fillRect(0, 0, canvas.width, canvas.height); + } + }; - return () => { - document.removeEventListener("keyup", handleKeyUp); - document.removeEventListener("keydown", handleKeyDown); - document.removeEventListener("touchmove", handleTouchMove); - window.removeEventListener("resize", handleResize); - }; - }, [canvasRef]); + const displayScore = (score) => { + if (context) { + context.fillStyle = "white"; + context.fillRect(400, 0, 200, 70); + context.fillStyle = "black"; + context.fillText("Score", 400, 50); + context.fillText(score, 450, 50); + } + }; + const displayEndScore = (score) => { + if (context) { + context.fillStyle = "white"; + context.fillRect(300, 250, 400, 140); + context.fillStyle = "red"; + context.textAlign = "center"; + context.textBaseline = "middle"; + context.font = "bold 24px Arial"; + context.fillText("Score", canvas.width / 2, canvas.height / 2 - 40); + context.fillText(score, canvas.width / 2, canvas.height / 2); + } + }; - return ( -
- {scoreLeftReact === winScore || scoreRightReact === winScore ? ( - scoreLeftReact === winScore ? ( - - ) : ( - - ) - ) : ( - <> - - - )} -
- ); -}; + const handleKeyDown = (event) => { -const Pong = () => { - const { t } = useTranslation(); - const location = useLocation(); - const navigate = useNavigate(); - const [gameStarted, setGameStarted] = useState(false); + const user = sender.current; + console.log("Key up event: " + user); + if (gameSocket.current.readyState === WebSocket.OPEN) { + if (event.key === "w") { + gameSocket.current.send("pw" + user); + } else if (event.key === "s") { + gameSocket.current.send("ps" + user); + } else if (event.key === "i") { + gameSocket.current.send("pi" + user); + } else if (event.key === "k") { + gameSocket.current.send("pk" + user); + } + } else { + console.log("Socket not open"); + } + }; + + const handleKeyUp = (event) => { + const user = sender.current; + console.log("Key up event: " + user); + if (gameSocket.current.readyState === WebSocket.OPEN) { + if (event.key === "w") { + gameSocket.current.send("rw" + user); + } else if (event.key === "s") { + gameSocket.current.send("rs" + user); + } else if (event.key === "i") { + gameSocket.current.send("ri" + user); + } else if (event.key === "k") { + gameSocket.current.send("rk" + user); + } + } else { + console.log("Socket not open"); + } - const handleButtonClick = () => { - setGameStarted(true); - }; + }; + + const startGame = () => { + gameSocket.current.send("startgame"); + }; + + document.addEventListener("keyup", handleKeyUp); + document.addEventListener("keydown", handleKeyDown); + const startButton = document.getElementById("startGame"); + startButton.addEventListener("click", startGame); + + // Cleanup function + return () => { + console.log("closing socket and removing listener"); + gameSocket.current.close(); + document.removeEventListener("keyup", handleKeyUp); + document.removeEventListener("keydown", handleKeyDown); + startButton.removeEventListener("click", startGame); + }; + } + getData(); + }, []); + return ( -
- - {gameStarted ? ( - - ) : ( -
- Background -
- -
- -
- )} +
+ +
); }; diff --git a/garbage.md b/garbage.md old mode 100644 new mode 100755 index 299442ad3..4614edf26 --- a/garbage.md +++ b/garbage.md @@ -6,7 +6,6 @@ ## Register -> Post - Input -eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzA3OTg5NzY4LCJpYXQiOjE3MDc5MDMzNjgsImp0aSI6IjUwOTY2MDk5OTMwODQxNWE5NThjMGRjNWIxYTc3MTBiIiwidXNlcl9pZCI6MiwiZW1haWwiOiJzaW91ZGF6ZXI4MkBnbWFpbC5jb20iLCJ1c2VybmFtZSI6InJlYWx1c2VyMiJ9.2bAgvbFoKt-6_l6aqQL0Y3u8clIc_wRfGRguoBhGpsc { "email":"sioudazer82@gmail.com", "username":"realuser2", @@ -21,10 +20,14 @@ eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzA } Result <- { - "refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTcwNzIxOTM1MSwiaWF0IjoxNzA3MTMyOTUxLCJqdGkiOiJkMTM2ZDIyYjc2NjI0NDk0OWRhZDU3MDYwZjVjOTE0NyIsInVzZXJfaWQiOjEsImVtYWlsIjoic2lvdWRhemVyOEBnbWFpbC5jb20iLCJ1c2VybmFtZSI6InJlYWx1c2VyIn0.dUc-kYp91jYx1LNxiazHTHxKsbR78lyWTPMQWnMDH-o", - "access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzA3MjE5MzUxLCJpYXQiOjE3MDcxMzI5NTEsImp0aSI6ImRmYmJmNDgyN2MzNTQwM2NhNzFlODc3NjkyYjQzZjM1IiwidXNlcl9pZCI6MSwiZW1haWwiOiJzaW91ZGF6ZXI4QGdtYWlsLmNvbSIsInVzZXJuYW1lIjoicmVhbHVzZXIifQ.0cjP1qZfUfrxiUophcscm3pMv0rJxN7LluokzmAan7w" + "refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.", + "access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." } +realuser2: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzIwNTE3NTI3LCJpYXQiOjE3MTE5NjM5MjcsImp0aSI6IjFjZGY5ZDk3ZDY3YzQxODc4MmVmNTgyZDg3MmM5M2Q2IiwidXNlcl9pZCI6MSwiZW1haWwiOiJzaW91ZGF6ZXI4MkBnbWFpbC5jb20iLCJ1c2VybmFtZSI6InJlYWx1c2VyMiJ9.06R3e43s9VhLW3DgC7MgaoblXidZQ6Kd4ojrDEt26MY + +azer: + ## Put new access token to the extension ## Logout -> Post @@ -84,3 +87,13 @@ Header Changer: https://chromewebstore.google.com/detail/requestly-open-source-h { "blocked_username":"realuser" } + + + +## Notes +Paddle speed increases with ball speed + + +- start only when starg game pressed +- Prevent user from playing again itself +- Ending