From c4a73078ab42fd1ddd3d47805ec30f52d59d1cc3 Mon Sep 17 00:00:00 2001 From: CHIBOUB Chakib Date: Mon, 22 Jul 2024 17:21:58 +0200 Subject: [PATCH 1/2] added frontend, backend game logic v1 --- Dockerfile | 18 +-- docker-compose.yaml | 35 +++--- env_template | 12 -- helloworld/urls.py | 25 ---- helloworld/views.py | 4 - helloworld/wsgi.py | 16 --- makefile | 19 +-- manage.py | 2 +- {helloworld => pong}/__init__.py | 0 pong/asgi.py | 30 +++++ pong/game/__init__.py | 0 pong/game/consumers.py | 47 ++++++++ pong/game/game.py | 86 +++++++++++++ pong/game/matchmaking.py | 65 ++++++++++ pong/game/models.py | 6 + pong/game/routing.py | 8 ++ pong/game/urls.py | 11 ++ pong/game/views.py | 64 ++++++++++ {helloworld => pong}/settings.py | 73 +++++------ pong/static/game.js | 201 +++++++++++++++++++++++++++++++ pong/static/index.html | 42 +++++++ pong/static/styles.css | 113 +++++++++++++++++ pong/urls.py | 15 +++ requirements.txt | 2 + 24 files changed, 765 insertions(+), 129 deletions(-) delete mode 100644 env_template delete mode 100644 helloworld/urls.py delete mode 100644 helloworld/views.py delete mode 100644 helloworld/wsgi.py rename {helloworld => pong}/__init__.py (100%) create mode 100644 pong/asgi.py create mode 100644 pong/game/__init__.py create mode 100644 pong/game/consumers.py create mode 100644 pong/game/game.py create mode 100644 pong/game/matchmaking.py create mode 100644 pong/game/models.py create mode 100644 pong/game/routing.py create mode 100644 pong/game/urls.py create mode 100644 pong/game/views.py rename {helloworld => pong}/settings.py (57%) create mode 100644 pong/static/game.js create mode 100644 pong/static/index.html create mode 100644 pong/static/styles.css create mode 100644 pong/urls.py diff --git a/Dockerfile b/Dockerfile index ce4e9e3..d66e6d4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,15 @@ -FROM python:3.12.4 +FROM python:latest -WORKDIR /ft_transcendence +# Set environment variables +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONUNBUFFERED 1 + +WORKDIR /transcendence RUN apt update && apt upgrade -y RUN apt install -y vim COPY requirements.txt . -COPY manage.py . RUN python3 -m venv venv RUN venv/bin/pip3 install --upgrade pip @@ -14,9 +17,10 @@ RUN venv/bin/pip3 install --no-cache-dir -r requirements.txt COPY . . -#RUN venv/bin/python3 manage.py migrate --noinput -#RUN venv/bin/python manage.py collectstatic --noinput +# Collect static files during the build +RUN venv/bin/python manage.py collectstatic --noinput -EXPOSE 8000 +EXPOSE 80 -CMD ["venv/bin/python", "manage.py", "runserver", "0.0.0.0:8000"] +# CMD ["venv/bin/python", "manage.py", "runserver", "0.0.0.0:80"] +CMD ["daphne", "-b", "0.0.0.0", "-p", "80", "pong.asgi:application"] \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index d67d45c..7265c83 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,6 +1,6 @@ services: db: - image: postgres:16.3 + image: postgres:latest container_name: postgres restart: always volumes: @@ -15,9 +15,9 @@ services: networks: - app-network environment: + POSTGRES_DB: ${POSTGRES_DB} POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} - POSTGRES_DB: ${POSTGRES_DB} backend: build: @@ -26,23 +26,26 @@ services: image: backend container_name: backend restart: always - command: /bin/sh -c "sleep 5 && venv/bin/python3 manage.py migrate --noinput && venv/bin/python3 manage.py runserver 0.0.0.0:8000" + command: /bin/sh -c "sleep 5 && + venv/bin/python manage.py makemigrations --noinput && + venv/bin/python manage.py migrate --noinput && + venv/bin/daphne -b 0.0.0.0 -p 80 pong.asgi:application" volumes: - - helloword_project:/ft_transcendence/helloworld + - ./pong:/transcendence/pong ports: - - "8000:8000" + - "80:80" depends_on: - db networks: - app-network environment: - POSTGRES_DB: ${POSTGRES_DB} - POSTGRES_USER: ${POSTGRES_USER} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} - DB_HOST: ${DB_HOST} - DB_PORT: ${DB_PORT} + DB_HOST: db + DB_PORT: 5432 + DB_NAME: ${POSTGRES_DB} + DB_USER: ${POSTGRES_USER} + DB_PASSWORD: ${POSTGRES_PASSWORD} healthcheck: - test: ["CMD-SHELL", "curl -f http://localhost:8000 || exit 1"] + test: ["CMD-SHELL", "curl -f http://localhost:80 || exit 1"] interval: 10s timeout: 5s retries: 5 @@ -50,16 +53,6 @@ services: volumes: postgres_data: driver: local - driver_opts: - type: none - device: /home/motoko/ft_transcendence/data/db - o: bind - helloword_project: - driver: local - driver_opts: - type: none - device: /home/motoko/ft_transcendence/helloworld - o: bind networks: app-network: diff --git a/env_template b/env_template deleted file mode 100644 index eaef9b2..0000000 --- a/env_template +++ /dev/null @@ -1,12 +0,0 @@ -# Django settings -SECRET_KEY= -DEBUG=True -DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1] - -# PostgreSQL settings -POSTGRES_DB= -POSTGRES_USER= -POSTGRES_PASSWORD= - -DB_HOST=db -DB_PORT=5432 diff --git a/helloworld/urls.py b/helloworld/urls.py deleted file mode 100644 index 9c564a7..0000000 --- a/helloworld/urls.py +++ /dev/null @@ -1,25 +0,0 @@ -"""helloworld URL Configuration - -The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/2.2/topics/http/urls/ -Examples: -Function views - 1. Add an import: from my_app import views - 2. Add a URL to urlpatterns: path('', views.home, name='home') -Class-based views - 1. Add an import: from other_app.views import Home - 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') -Including another URLconf - 1. Import the include() function: from django.urls import include, path - 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) -""" -from django.contrib import admin -from django.urls import path -from helloworld import views - -urlpatterns = [ - path('admin/', admin.site.urls), - - # Hello, world! - path('', views.index, name='index') -] diff --git a/helloworld/views.py b/helloworld/views.py deleted file mode 100644 index ceff077..0000000 --- a/helloworld/views.py +++ /dev/null @@ -1,4 +0,0 @@ -from django.http import HttpResponse - -def index(request): - return HttpResponse("Hello, CHAKIB!") diff --git a/helloworld/wsgi.py b/helloworld/wsgi.py deleted file mode 100644 index a7f29a9..0000000 --- a/helloworld/wsgi.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -WSGI config for helloworld project. - -It exposes the WSGI callable as a module-level variable named ``application``. - -For more information on this file, see -https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/ -""" - -import os - -from django.core.wsgi import get_wsgi_application - -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'helloworld.settings') - -application = get_wsgi_application() diff --git a/makefile b/makefile index e892915..b73923b 100644 --- a/makefile +++ b/makefile @@ -2,23 +2,24 @@ all: build @echo "Building Docker images..." - sudo mkdir -p $$HOME/ft_transcendence/data/db - sudo docker compose -f ./docker-compose.yaml up -d --build + @sudo mkdir -p data/db + @sudo docker compose -f ./docker-compose.yaml up --build down: @echo "Stopping Docker containers..." - sudo docker compose -f ./docker-compose.yaml down + @sudo docker compose -f ./docker-compose.yaml down clean: @echo "Cleaning up Docker resources..." - sudo docker stop $$(docker ps -qa);\ - sudo docker rm $$(docker ps -qa);\ - sudo docker rmi $$(docker image ls -q);\ - sudo docker volume rm $$(docker volume ls -q);\ - sudo rm -rf $$HOME/ft_transcendence/data/db ;\ + @sudo docker stop $$(docker ps -qa) ;\ + sudo docker rm $$(docker ps -qa) ;\ + sudo docker rmi $$(docker image ls -q) ;\ + sudo docker volume rm $$(docker volume ls -q) ;\ + sudo docker network rm $$(docker network ls -q) ;\ + sudo rm -rf data ;\ logs: @echo "Displaying Docker logs..." - sudo docker compose logs -f + @sudo docker compose logs -f re: down clean build diff --git a/manage.py b/manage.py index 08e5b9d..d01a08d 100755 --- a/manage.py +++ b/manage.py @@ -5,7 +5,7 @@ import sys def main(): - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'helloworld.settings') + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'pong.settings') try: from django.core.management import execute_from_command_line except ImportError as exc: diff --git a/helloworld/__init__.py b/pong/__init__.py similarity index 100% rename from helloworld/__init__.py rename to pong/__init__.py diff --git a/pong/asgi.py b/pong/asgi.py new file mode 100644 index 0000000..b47b355 --- /dev/null +++ b/pong/asgi.py @@ -0,0 +1,30 @@ +# /pong/asgi.py + +""" +ASGI config for pong project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/ +""" + +import os +import django + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'pong.settings') +django.setup() + +from django.core.asgi import get_asgi_application +from channels.routing import ProtocolTypeRouter, URLRouter +from channels.auth import AuthMiddlewareStack +import pong.game.routing # Import your routing module + +application = ProtocolTypeRouter({ + "http": get_asgi_application(), + "websocket": AuthMiddlewareStack( + URLRouter( + pong.game.routing.websocket_urlpatterns + ) + ), +}) diff --git a/pong/game/__init__.py b/pong/game/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pong/game/consumers.py b/pong/game/consumers.py new file mode 100644 index 0000000..8495636 --- /dev/null +++ b/pong/game/consumers.py @@ -0,0 +1,47 @@ +# /pong/game/consumers.py + +import json +from channels.generic.websocket import AsyncWebsocketConsumer +from django.contrib.auth.models import User +from channels.db import database_sync_to_async +from .matchmaking import match_maker # Import the match_maker instance + +class GameConsumer(AsyncWebsocketConsumer): + async def connect(self): + await self.accept() + print("User connected") + + async def receive(self, text_data): + data = json.loads(text_data) + #print(f"MESSAGE RECEIVED: {data['type']}") + if data['type'] == 'authenticate': + await self.authenticate(data['token']) + elif data['type'] == 'key_press': + await match_maker.handle_key_press(self, data['key']) + + async def authenticate(self, token): + user = await self.get_user_from_token(token) + if user: + self.user = user + await self.send(text_data=json.dumps({'type': 'authenticated'})) + print(f"User {self.user} authenticated") + await self.join_waiting_room() + else: + await self.send(text_data=json.dumps({'type': 'error', 'message': 'Authentication failed'})) + print("Authentication failed") + + @database_sync_to_async + def get_user_from_token(self, token): + try: + user = User.objects.filter(auth_token=token).first() + return user + except User.DoesNotExist: + return None + + async def join_waiting_room(self): + await self.send(text_data=json.dumps({'type': 'waiting_room'})) + await match_maker.add_player(self) + + async def disconnect(self, close_code): + await match_maker.remove_player(self) + print(f"User {self.user} disconnected") diff --git a/pong/game/game.py b/pong/game/game.py new file mode 100644 index 0000000..2b2af26 --- /dev/null +++ b/pong/game/game.py @@ -0,0 +1,86 @@ +# /pong/game/game.py + +import json +import asyncio +import random + +class Game: + def __init__(self, game_id, player1, player2): + self.game_id = game_id + self.player1 = player1 + self.player2 = player2 + self.game_state = { + 'player1_name': player1.user.username, + 'player2_name': player2.user.username, + 'player1_position': 200, # middle of the game field + 'player2_position': 200, + 'ball_position': {'x': 400, 'y': 300}, # middle of the game field + 'ball_velocity': {'x': random.choice([-5, 5]), 'y': random.choice([-5, 5])}, + 'player1_score': 0, + 'player2_score': 0 + } + self.game_loop_task = None + + async def start_game(self): + print(f"- Game {self.game_id} START") + self.game_loop_task = asyncio.create_task(self.game_loop()) + + async def game_loop(self): + while True: + self.update_game_state() + await self.send_game_state() + await asyncio.sleep(1/60) # 60 FPS + + def update_game_state(self): + # Update ball position + self.game_state['ball_position']['x'] += self.game_state['ball_velocity']['x'] + self.game_state['ball_position']['y'] += self.game_state['ball_velocity']['y'] + + # Check for collisions with top and bottom walls + if self.game_state['ball_position']['y'] <= 0 or self.game_state['ball_position']['y'] >= 600: + self.game_state['ball_velocity']['y'] *= -1 + + # Check for scoring + if self.game_state['ball_position']['x'] <= 0: + self.game_state['player2_score'] += 1 + self.reset_ball() + elif self.game_state['ball_position']['x'] >= 800: + self.game_state['player1_score'] += 1 + self.reset_ball() + + # Check for collisions with paddles + if self.game_state['ball_position']['x'] <= 20 and \ + self.game_state['player1_position'] - 50 <= self.game_state['ball_position']['y'] <= self.game_state['player1_position'] + 50: + self.game_state['ball_velocity']['x'] *= -1 + elif self.game_state['ball_position']['x'] >= 780 and \ + self.game_state['player2_position'] - 50 <= self.game_state['ball_position']['y'] <= self.game_state['player2_position'] + 50: + self.game_state['ball_velocity']['x'] *= -1 + + def reset_ball(self): + self.game_state['ball_position'] = {'x': 400, 'y': 300} + self.game_state['ball_velocity'] = {'x': random.choice([-5, 5]), 'y': random.choice([-5, 5])} + + async def send_game_state(self): + message = json.dumps({ + 'type': 'game_state_update', + 'game_state': self.game_state + }) + await self.player1.send(message) + await self.player2.send(message) + + async def handle_key_press(self, player, key): + if player == self.player1: + if key == 'arrowup' and self.game_state['player1_position'] > 0: + self.game_state['player1_position'] -= 10 + elif key == 'arrowdown' and self.game_state['player1_position'] < 550: + self.game_state['player1_position'] += 10 + elif player == self.player2: + if key == 'arrowup' and self.game_state['player2_position'] > 0: + self.game_state['player2_position'] -= 10 + elif key == 'arrowdown' and self.game_state['player2_position'] < 550: + self.game_state['player2_position'] += 10 + + async def end_game(self): + if self.game_loop_task: + self.game_loop_task.cancel() + # Add any cleanup code here \ No newline at end of file diff --git a/pong/game/matchmaking.py b/pong/game/matchmaking.py new file mode 100644 index 0000000..bd96aa2 --- /dev/null +++ b/pong/game/matchmaking.py @@ -0,0 +1,65 @@ +# /pong/game/matchmaking.py + +import json +import asyncio +from .game import Game + +class MatchMaker: + def __init__(self): + self.waiting_players = [] + self.active_games = {} + self.matching_task = None + + async def add_player(self, player): + if player not in self.waiting_players: + self.waiting_players.append(player) + print(f"User {player.user.username} joins the WAITING ROOM") + if not self.matching_task or self.matching_task.done(): + self.matching_task = asyncio.create_task(self.match_loop()) + + async def remove_player(self, player): + if player in self.waiting_players: + self.waiting_players.remove(player) + + async def match_loop(self): + while True: + if len(self.waiting_players) >= 2: + player1 = self.waiting_players.pop(0) + player2 = self.waiting_players.pop(0) + print(f"MATCH FOUND: {player1.user.username} vs {player2.user.username}") + game_id = await self.create_game(player1, player2) + else: + # No players to match, wait for a short time before checking again + await asyncio.sleep(1) + + async def create_game(self, player1, player2): + game_id = len(self.active_games) + 1 + print(f"- Creating game: {game_id}") + new_game = Game(game_id, player1, player2) + self.active_games[game_id] = new_game + await self.notify_players(player1, player2, game_id) + asyncio.create_task(new_game.start_game()) + return game_id + + async def notify_players(self, player1, player2, game_id): + await player1.send(json.dumps({ + 'type': 'game_start', + 'game_id': game_id, + 'player1': player1.user.username, + 'player2': player2.user.username + })) + await player2.send(json.dumps({ + 'type': 'game_start', + 'game_id': game_id, + 'player1': player1.user.username, + 'player2': player2.user.username + })) + + async def handle_key_press(self, player, key): + for game in self.active_games.values(): + if player in [game.player1, game.player2]: + await game.handle_key_press(player, key) + break + +# Instance of the class +match_maker = MatchMaker() diff --git a/pong/game/models.py b/pong/game/models.py new file mode 100644 index 0000000..85b415b --- /dev/null +++ b/pong/game/models.py @@ -0,0 +1,6 @@ +# /pong/game/models.py + +from django.db import models +from django.contrib.auth.models import User + +User.add_to_class('auth_token', models.CharField(max_length=100, null=True, blank=True, unique=True)) \ No newline at end of file diff --git a/pong/game/routing.py b/pong/game/routing.py new file mode 100644 index 0000000..8b421b9 --- /dev/null +++ b/pong/game/routing.py @@ -0,0 +1,8 @@ +# /pong/game/routing.py + +from django.urls import re_path +from . import consumers + +websocket_urlpatterns = [ + re_path(r'ws/game/$', consumers.GameConsumer.as_asgi()), +] diff --git a/pong/game/urls.py b/pong/game/urls.py new file mode 100644 index 0000000..96488b0 --- /dev/null +++ b/pong/game/urls.py @@ -0,0 +1,11 @@ +# /pong/game/urls.py + +from django.urls import path +from . import views + +urlpatterns = [ + path('', views.index, name='index'), + path('check_user_exists/', views.check_user_exists, name='check_user_exists'), + path('register_user/', views.register_user, name='register_user'), + path('authenticate_user/', views.authenticate_user, name='authenticate_user'), +] diff --git a/pong/game/views.py b/pong/game/views.py new file mode 100644 index 0000000..279a317 --- /dev/null +++ b/pong/game/views.py @@ -0,0 +1,64 @@ +# /pong/game/views.py + +from django.shortcuts import render + +def index(request): + return render(request, 'index.html') + +from django.http import JsonResponse +from django.contrib.auth.models import User +from django.contrib.auth import authenticate +from django.views.decorators.csrf import csrf_exempt +import json +import uuid + +@csrf_exempt +def register_user(request): + if request.method == 'POST': + data = json.loads(request.body) + username = data.get('username') + password = data.get('password') + if not User.objects.filter(username=username).exists(): + user = User.objects.create_user(username=username, password=password) + token = get_or_create_token(user) + return JsonResponse({'registered': True, 'token': token}) + return JsonResponse({'registered': False, 'error': 'User already exists'}) + return JsonResponse({'error': 'Invalid request method'}, status=400) + +@csrf_exempt +def check_user_exists(request): + if request.method == 'POST': + data = json.loads(request.body) + username = data.get('username') + if User.objects.filter(username=username).exists(): + return JsonResponse({'exists': True}) + return JsonResponse({'exists': False}) + return JsonResponse({'error': 'Invalid request method'}, status=400) + +@csrf_exempt +def authenticate_user(request): + if request.method == 'POST': + try: + data = json.loads(request.body) + username = data.get('username', '') + password = data.get('password', '') + user = authenticate(username=username, password=password) + if user is not None: + token = get_or_create_token(user) + return JsonResponse({'authenticated': True, 'token': token, 'user_id': user.id}) + else: + return JsonResponse({'authenticated': False}, status=401) + except Exception as e: + return JsonResponse({'error': str(e)}, status=500) + else: + return JsonResponse({'error': 'Method not allowed'}, status=405) + +def get_or_create_token(user): + if not user.auth_token: + while True: + token = str(uuid.uuid4()) + if not User.objects.filter(auth_token=token).exists(): + user.auth_token = token + user.save() + break + return user.auth_token diff --git a/helloworld/settings.py b/pong/settings.py similarity index 57% rename from helloworld/settings.py rename to pong/settings.py index 9300164..e9f5627 100644 --- a/helloworld/settings.py +++ b/pong/settings.py @@ -1,34 +1,28 @@ +# /pong/settings.py + """ -Django settings for helloworld project. +Django settings for pong project. -Generated by 'django-admin startproject' using Django 2.2.3. - -For more information on this file, see -https://docs.djangoproject.com/en/2.2/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/2.2/ref/settings/ +Generated by 'django-admin startproject' using Django 3.2. """ +from pathlib import Path import os -from dotenv import load_dotenv - -# Build paths inside the project like this: os.path.join(BASE_DIR, ...) -BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent # Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/ +# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'matxp6k!wbkmdlk)97)ew2qr%&9nr=n#v_-+v#yel4^r&czf7q' +SECRET_KEY = '12345678' # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -# A list of strings representing the host/domain names that this Django site can serve. -ALLOWED_HOSTS = ['127.0.0.1', 'localhost'] +ALLOWED_HOSTS = ['*'] # Application definition @@ -40,6 +34,8 @@ INSTALLED_APPS = [ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'channels', # Add Django Channels + 'pong.game', # Your game app ] MIDDLEWARE = [ @@ -52,12 +48,12 @@ MIDDLEWARE = [ 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] -ROOT_URLCONF = 'helloworld.urls' +ROOT_URLCONF = 'pong.urls' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], + 'DIRS': [os.path.join(BASE_DIR, 'pong', 'static')], # Ensure templates are found 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ @@ -70,25 +66,24 @@ TEMPLATES = [ }, ] -WSGI_APPLICATION = 'helloworld.wsgi.application' - +ASGI_APPLICATION = 'pong.asgi.application' # Add ASGI application # Database -# https://docs.djangoproject.com/en/2.2/ref/settings/#databases +# https://docs.djangoproject.com/en/3.2/ref/settings/#databases DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', - 'NAME': os.getenv('POSTGRES_DB', 'default_db_name'), - 'USER': os.getenv('POSTGRES_USER', 'default_user'), - 'PASSWORD': os.getenv('POSTGRES_PASSWORD', 'default_password'), - 'HOST': os.getenv('DB_HOST', 'db'), - 'PORT': os.getenv('DB_PORT', '5432'), + 'NAME': os.getenv('DB_NAME'), + 'USER': os.getenv('DB_USER'), + 'PASSWORD': os.getenv('DB_PASSWORD'), + 'HOST': os.getenv('DB_HOST'), + 'PORT': '5432', } } # Password validation -# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators +# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ { @@ -105,26 +100,36 @@ AUTH_PASSWORD_VALIDATORS = [ }, ] - # Internationalization -# https://docs.djangoproject.com/en/2.2/topics/i18n/ +# https://docs.djangoproject.com/en/3.2/topics/i18n/ LANGUAGE_CODE = 'en-us' TIME_ZONE = 'UTC' -# If you set this to False, Django will make some optimizations so as not -# to load the internationalization machinery. USE_I18N = True -# If you set this to False, Django will not format dates, numbers and -# calendars according to the current locale USE_L10N = True USE_TZ = True # Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/2.2/howto/static-files/ +# https://docs.djangoproject.com/en/3.2/howto/static-files/ STATIC_URL = '/static/' +STATICFILES_DIRS = [os.path.join(BASE_DIR, 'pong/static')] +STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') + +# Default primary key field type +# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +# Channels +# Define the channel layers for WebSockets +CHANNEL_LAYERS = { + 'default': { + 'BACKEND': 'channels.layers.InMemoryChannelLayer', + }, +} diff --git a/pong/static/game.js b/pong/static/game.js new file mode 100644 index 0000000..5e40a1a --- /dev/null +++ b/pong/static/game.js @@ -0,0 +1,201 @@ +document.addEventListener('DOMContentLoaded', () => { + const checkNicknameButton = document.getElementById('check-nickname'); + const registerButton = document.getElementById('register'); + const loginButton = document.getElementById('login'); + const authForm = document.getElementById('auth-form'); + const gameContainer = document.getElementById('game1'); + const nicknameInput = document.getElementById('nickname'); + const passwordInput = document.getElementById('password'); + const confirmPasswordInput = document.getElementById('confirm-password'); + const loginPasswordInput = document.getElementById('login-password'); + const loginForm = document.getElementById('login-form'); + const registerForm = document.getElementById('register-form'); + + let socket; + let token; + let gameState; + + checkNicknameButton.addEventListener('click', handleCheckNickname); + registerButton.addEventListener('click', handleRegister); + loginButton.addEventListener('click', handleLogin); + + async function handleCheckNickname() { + const nickname = nicknameInput.value.trim(); + if (nickname) { + try { + const exists = await checkUserExists(nickname); + if (exists) { + authForm.style.display = 'none'; + loginForm.style.display = 'block'; + } else { + authForm.style.display = 'none'; + registerForm.style.display = 'block'; + } + } catch (error) { + console.error('Error checking user existence:', error); + } + } else { + alert('Please enter a nickname.'); + } + } + + async function checkUserExists(username) { + const response = await fetch('/api/check_user_exists/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ username }) + }); + const data = await response.json(); + return data.exists; + } + + async function handleRegister() { + const nickname = nicknameInput.value.trim(); + const password = passwordInput.value.trim(); + const confirmPassword = confirmPasswordInput.value.trim(); + + if (password === confirmPassword) { + try { + const result = await registerUser(nickname, password); + if (result) { + registerForm.style.display = 'none'; + gameContainer.style.display = 'flex'; + startGame(); + } else { + alert('Registration failed. Please try again.'); + } + } catch (error) { + console.error('Error registering user:', error); + } + } else { + alert('Passwords do not match.'); + } + } + + async function registerUser(username, password) { + const response = await fetch('/api/register_user/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ username, password }) + }); + const data = await response.json(); + if (data.registered) { + token = data.token; + } + return data.registered; + } + + async function handleLogin() { + const nickname = nicknameInput.value.trim(); + const password = loginPasswordInput.value.trim(); + try { + const result = await authenticateUser(nickname, password); + if (result) { + loginForm.style.display = 'none'; + gameContainer.style.display = 'flex'; + startWebSocketConnection(token); + } else { + alert('Authentication failed. Please try again.'); + } + } catch (error) { + console.error('Error authenticating user:', error); + } + } + + async function authenticateUser(username, password) { + const response = await fetch('/api/authenticate_user/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ username, password }) + }); + const data = await response.json(); + if (data.authenticated) { + token = data.token; + } + return data.authenticated; + } + + function startWebSocketConnection(token) { + socket = new WebSocket(`ws://${window.location.host}/ws/game/`); + + socket.onopen = function (event) { + console.log('WebSocket connection established'); + socket.send(JSON.stringify({ type: 'authenticate', token: token })); + }; + + socket.onmessage = function (event) { + const data = JSON.parse(event.data); + if (data.type === 'authenticated') { + console.log('Authentication successful'); + } else if (data.type === 'waiting_room') { + console.log('Entered the waiting room'); + } else if (data.type === 'game_start') { + console.log('Game started:', data.game_id, '(', data.player1, 'vs', data.player2, ')'); + startGame(data.game_id, data.player1, data.player2); + } else if (data.type === 'game_state_update') { + updateGameState(data.game_state); + } else if (data.type === 'error') { + console.error(data.message); + } else { + console.log('Message from server:', data.type, data.message); + } + }; + + socket.onclose = function (event) { + console.log('WebSocket connection closed'); + }; + + socket.onerror = function (error) { + console.error('WebSocket error:', error); + }; + } + + function startGame(gameCode, player1_name, player2_name) { + document.getElementById('gameCode').textContent = `Game Code: ${gameCode}`; + document.getElementById('player1-name').textContent = `${player1_name}`; + document.getElementById('player2-name').textContent = `${player2_name}`; + document.addEventListener('keydown', handleKeyDown); + } + + function handleKeyDown(event) { + if (event.key === 'ArrowUp' || event.key === 'ArrowDown') { + sendKeyPress(event.key.toLowerCase()); + } + } + + function sendKeyPress(key) { + if (socket.readyState === WebSocket.OPEN) { + socket.send(JSON.stringify({ type: 'key_press', key })); + } + } + + function updateGameState(newState) { + gameState = newState; + renderGame(); + } + + function renderGame() { + const player1Pad = document.getElementById('player1-pad'); + player1Pad.style.top = `${gameState.player1_position}px`; + + const player2Pad = document.getElementById('player2-pad'); + player2Pad.style.top = `${gameState.player2_position}px`; + + const ball = document.getElementById('ball'); + ball.style.left = `${gameState.ball_position.x}px`; + ball.style.top = `${gameState.ball_position.y}px`; + + const player1Score = document.getElementById('player1-score'); + player1Score.textContent = gameState.player1_score; + + const player2Score = document.getElementById('player2-score'); + player2Score.textContent = gameState.player2_score; + } + +}); diff --git a/pong/static/index.html b/pong/static/index.html new file mode 100644 index 0000000..1aad5b6 --- /dev/null +++ b/pong/static/index.html @@ -0,0 +1,42 @@ +{% load static %} + + + + + + Pong Game + + + +
+ + + +
+ + + + + + diff --git a/pong/static/styles.css b/pong/static/styles.css new file mode 100644 index 0000000..1dfd64b --- /dev/null +++ b/pong/static/styles.css @@ -0,0 +1,113 @@ +/* General styles */ +body { + font-family: Arial, sans-serif; + color: #ffffff; + background-color: #000000; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100vh; +} + +label { + margin: 10px 0 5px; +} + +input { + padding: 10px; + margin: 5px 0 20px; + width: 200px; +} + +button { + padding: 10px 20px; + cursor: pointer; +} + +#game1 { + width: 810px; + height: 500px; + position: relative; + background-color: #000; + overflow: hidden; + display: flex; + justify-content: center; + align-items: center; +} + +.game-code { + font-size: 24px; + position: absolute; + top: 10px; +} + +#gameCode { + left: 10px; +} + +.name { + font-size: 24px; + position: absolute; + top: 30px; +} + +#player1-name { + left: 10px; /* Adjust player score position */ +} + +#player2-name { + right: 10px; /* Adjust bot score position */ +} + +#game2 { + top: 60px; + width: 800px; + height: 400px; + position: absolute; + background-color: #000; + overflow: hidden; + border: 2px solid red; /* Add red border */ + display: flex; + justify-content: center; + align-items: center; +} + +.score { + font-size: 24px; + position: absolute; + top: 10px; +} + +#player1-score { + left: 50px; /* Adjust player score position */ +} + +#player2-score { + right: 50px; /* Adjust bot score position */ +} + +.pad { + width: 10px; + height: 100px; + background-color: #ffffff; + position: absolute; +} + +#player1-pad { + left: 10px; +} + +#player2-pad { + right: 10px; +} + +#ball { + width: 20px; + height: 20px; + background-color: #ff0000; + border-radius: 50%; + position: absolute; +} diff --git a/pong/urls.py b/pong/urls.py new file mode 100644 index 0000000..665ace0 --- /dev/null +++ b/pong/urls.py @@ -0,0 +1,15 @@ +# /pong/urls.py + +from django.contrib import admin +from django.urls import path, include +from django.conf import settings +from django.conf.urls.static import static + +urlpatterns = [ + path('admin/', admin.site.urls), + path('api/', include('pong.game.urls')), + path('', include('pong.game.urls')), +] + +if settings.DEBUG: + urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 7fd1954..9b9e85a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,5 @@ Django psycopg2 python-dotenv +channels +daphne \ No newline at end of file From 93ba6f6011b487cc126c852449fa8134c4a3efd8 Mon Sep 17 00:00:00 2001 From: CHIBOUB Chakib Date: Mon, 22 Jul 2024 18:10:26 +0200 Subject: [PATCH 2/2] deleted .gitignore --- .gitignore | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 .gitignore diff --git a/.gitignore b/.gitignore deleted file mode 100644 index d3b77e6..0000000 --- a/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -.gitignore -.env -docker-compose.yaml -makefile -venv/ -__pycache__ -data/