mirror of
https://github.com/evgen-app/chess_rpg_backend.git
synced 2024-11-25 11:04:03 +03:00
Removed files for stable branch
This commit is contained in:
parent
a3aafd19b2
commit
ba0c8499e0
133
.gitignore
vendored
133
.gitignore
vendored
|
@ -1,133 +0,0 @@
|
|||
.idea
|
||||
static/
|
||||
media/
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
pip-wheel-metadata/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
37
README.md
37
README.md
|
@ -1,37 +0,0 @@
|
|||
# chess_rpg_backend
|
||||
Backend for chess rpg game
|
||||
|
||||
### installation
|
||||
```shell
|
||||
$ python3 manage.py makemigrations & python3 manage.py migrate
|
||||
$ docker run -p 6379:6379 -d redis:5
|
||||
```
|
||||
|
||||
### run
|
||||
```shell
|
||||
$ python3 manage.py runserver 0.0.0.0:8000
|
||||
```
|
||||
|
||||
### Описание команд сокетов
|
||||
```python
|
||||
# подключиние к очереди(ws://room/)
|
||||
{
|
||||
"type": "connect",
|
||||
"deck_id": int
|
||||
}
|
||||
|
||||
# коннект к комнате (сообщение от сервера)
|
||||
{
|
||||
"type": "INFO",
|
||||
"opponent_score": int,
|
||||
"coordinates" : [(x: int, y: int, type: str, model_url: url, your: bool), ...],
|
||||
"opponent_online": true,
|
||||
"first": bool
|
||||
}
|
||||
|
||||
# состояние оппонента в комнате(сообщение от сервера)
|
||||
{
|
||||
"type": "INFO",
|
||||
"message": "opponent is online" / "opponent is offline"
|
||||
}
|
||||
```
|
|
@ -1,18 +0,0 @@
|
|||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
from channels.routing import ProtocolTypeRouter, URLRouter
|
||||
|
||||
import room.routing
|
||||
from room.middleware import HeaderAuthMiddleware
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "chess_backend.settings")
|
||||
|
||||
application = ProtocolTypeRouter(
|
||||
{
|
||||
"http": get_asgi_application(),
|
||||
"websocket": HeaderAuthMiddleware(
|
||||
URLRouter(room.routing.websocket_urlpatterns)
|
||||
),
|
||||
}
|
||||
)
|
|
@ -1,104 +0,0 @@
|
|||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = "django-insecure-%_8sy196w4hzo9^cp9(@r=i+amh47r4mxfhq_(ok&=c(@%bhmk"
|
||||
TOKEN_EXP = 2678400 # 31 day
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = []
|
||||
|
||||
INSTALLED_APPS = [
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
"django.contrib.messages",
|
||||
# Packages
|
||||
"rest_framework",
|
||||
"channels",
|
||||
# Apps
|
||||
"game",
|
||||
"room",
|
||||
]
|
||||
|
||||
if DEBUG:
|
||||
INSTALLED_APPS.append("django.contrib.staticfiles")
|
||||
INSTALLED_APPS.append("drf_yasg")
|
||||
|
||||
|
||||
MIDDLEWARE = [
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
]
|
||||
|
||||
ROOT_URLCONF = "chess_backend.urls"
|
||||
|
||||
ASGI_APPLICATION = "chess_backend.asgi.application"
|
||||
CHANNEL_LAYERS = {
|
||||
"default": {
|
||||
"BACKEND": "channels_redis.core.RedisChannelLayer",
|
||||
"CONFIG": {
|
||||
"hosts": [("127.0.0.1", 6379)],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
WSGI_APPLICATION = "chess_backend.wsgi.application"
|
||||
|
||||
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": BASE_DIR / "db.sqlite3",
|
||||
}
|
||||
}
|
||||
|
||||
LANGUAGES = [
|
||||
("en-us", "English"),
|
||||
("ru", "Russian"),
|
||||
]
|
||||
|
||||
TIME_ZONE = "Europe/Moscow"
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
MEDIA_URL = "/media/"
|
||||
STATIC_URL = "/static/"
|
||||
|
||||
if DEBUG:
|
||||
MEDIA_ROOT = os.path.join(BASE_DIR, "media/")
|
||||
STATIC_ROOT = os.path.join(BASE_DIR, "static/")
|
||||
else:
|
||||
MEDIA_ROOT = "/var/www/media/"
|
||||
STATIC_ROOT = "/var/www/static/"
|
||||
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||
|
||||
LOGGING = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"handlers": {
|
||||
"file": {
|
||||
"level": "WARNING",
|
||||
"class": "logging.FileHandler",
|
||||
"filename": "debug.log",
|
||||
},
|
||||
},
|
||||
"loggers": {
|
||||
"django": {
|
||||
"handlers": ["file"],
|
||||
"level": "DEBUG",
|
||||
"propagate": True,
|
||||
},
|
||||
},
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
from django.template.defaulttags import url
|
||||
from django.urls import path, include, re_path
|
||||
|
||||
# openapi schema
|
||||
from rest_framework import permissions
|
||||
from drf_yasg.views import get_schema_view
|
||||
from drf_yasg import openapi
|
||||
|
||||
|
||||
schema_view = get_schema_view(
|
||||
openapi.Info(
|
||||
title="Snippets API",
|
||||
default_version="v1",
|
||||
description="Test description",
|
||||
terms_of_service="https://www.google.com/policies/terms/",
|
||||
contact=openapi.Contact(email="contact@snippets.local"),
|
||||
license=openapi.License(name="BSD License"),
|
||||
),
|
||||
public=True,
|
||||
permission_classes=(permissions.AllowAny,),
|
||||
)
|
||||
|
||||
urlpatterns = [path("api/", include("game.urls"))] + static(
|
||||
settings.MEDIA_URL, document_root=settings.MEDIA_ROOT
|
||||
)
|
||||
|
||||
if settings.DEBUG:
|
||||
urlpatterns += [
|
||||
re_path(
|
||||
r"^swagger(?P<format>\.json|\.yaml)$",
|
||||
schema_view.without_ui(cache_timeout=0),
|
||||
name="schema-json",
|
||||
),
|
||||
re_path(
|
||||
r"^swagger/$",
|
||||
schema_view.with_ui("swagger", cache_timeout=0),
|
||||
name="schema-swagger-ui",
|
||||
),
|
||||
re_path(
|
||||
r"^redoc/$",
|
||||
schema_view.with_ui("redoc", cache_timeout=0),
|
||||
name="schema-redoc",
|
||||
),
|
||||
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
|
@ -1,16 +0,0 @@
|
|||
"""
|
||||
WSGI config for chess_backend 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/4.0/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'chess_backend.settings')
|
||||
|
||||
application = get_wsgi_application()
|
|
@ -1,14 +0,0 @@
|
|||
import string
|
||||
import secrets
|
||||
|
||||
from random import randint
|
||||
|
||||
|
||||
def generate_charset(length: int):
|
||||
return "".join(
|
||||
secrets.choice(string.digits + string.ascii_letters) for _ in range(length)
|
||||
)
|
||||
|
||||
|
||||
def gen_ton():
|
||||
return int("".join([str(randint(0, 9)) for _ in range(48)]))
|
|
@ -1,22 +0,0 @@
|
|||
from rest_framework.test import APITestCase
|
||||
from django.urls import reverse
|
||||
from rest_framework import status
|
||||
|
||||
|
||||
class GetBasicTest(APITestCase):
|
||||
def __init__(self, url):
|
||||
super().__init__()
|
||||
self._url = reverse(url)
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return self._url
|
||||
|
||||
@url.setter
|
||||
def url(self, value):
|
||||
self._url = reverse(value)
|
||||
|
||||
def test_accessibility(self):
|
||||
"""Test if POST request is possible"""
|
||||
response = self.client.post(self._url)
|
||||
self.assertNotEqual(response, status.HTTP_404_NOT_FOUND)
|
|
@ -1,6 +0,0 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
from game.models import HeroImageSet
|
||||
|
||||
admin.site.register(HeroImageSet)
|
|
@ -1,139 +0,0 @@
|
|||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from game.models import Hero, Player, HeroInDeck, Deck, PlayerAuthSession
|
||||
from game.services.jwt import read_jwt
|
||||
|
||||
|
||||
class CreateHeroSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Hero
|
||||
fields = (
|
||||
"type",
|
||||
"health",
|
||||
"attack",
|
||||
"speed",
|
||||
)
|
||||
|
||||
|
||||
class GetHeroSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Hero
|
||||
fields = (
|
||||
"added",
|
||||
"type",
|
||||
"idle_img",
|
||||
"attack_img",
|
||||
"die_img",
|
||||
"health",
|
||||
"attack",
|
||||
"speed",
|
||||
)
|
||||
|
||||
|
||||
class ListHeroSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Hero
|
||||
fields = (
|
||||
"uuid",
|
||||
"type",
|
||||
"idle_img",
|
||||
"attack_img",
|
||||
"die_img",
|
||||
"health",
|
||||
"attack",
|
||||
"speed",
|
||||
)
|
||||
|
||||
|
||||
class CreatePlayerSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Player
|
||||
fields = ("ton_wallet", "name")
|
||||
|
||||
|
||||
class CreateDeckSerializer(serializers.ModelSerializer):
|
||||
hero_ids = serializers.ListSerializer(
|
||||
child=serializers.UUIDField(), min_length=16, max_length=16
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Deck
|
||||
fields = ("hero_ids",)
|
||||
|
||||
def validate_hero_ids(self, value):
|
||||
if len(set(value)) != 16:
|
||||
raise ValidationError("Some of the uuids are not unique")
|
||||
|
||||
for x in value:
|
||||
if not (hero := Hero.objects.filter(uuid=x)):
|
||||
raise ValidationError(f"Hero with uuid {x} doesn't exist")
|
||||
|
||||
if hero.first().player.id != self.context["request"].user.id:
|
||||
raise ValidationError(
|
||||
f"Attempt to manipulate player with id {hero.first().player.id} hero"
|
||||
)
|
||||
|
||||
if self.context["request"].method in ["POST"]:
|
||||
if deck := HeroInDeck.objects.filter(hero=hero.first()):
|
||||
raise ValidationError(
|
||||
f"Hero with uuid {x} is already in deck with id {deck.first().deck.id}"
|
||||
)
|
||||
|
||||
return value
|
||||
|
||||
def create(self, validated_data):
|
||||
deck = Deck.objects.create(player=self.context["request"].user)
|
||||
for x in validated_data["hero_ids"]:
|
||||
HeroInDeck.objects.create(hero_id=x, deck=deck)
|
||||
return deck
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
for x in instance.get_heroes():
|
||||
HeroInDeck.objects.get(hero=x).delete()
|
||||
|
||||
for x in validated_data["hero_ids"]:
|
||||
HeroInDeck.objects.create(hero_id=x, deck=instance)
|
||||
|
||||
return instance
|
||||
|
||||
|
||||
class GetPlayerSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Player
|
||||
fields = ("id", "name")
|
||||
|
||||
|
||||
class GetDeckSerializer(serializers.ModelSerializer):
|
||||
player = GetPlayerSerializer()
|
||||
heroes = ListHeroSerializer(many=True)
|
||||
|
||||
class Meta:
|
||||
model = Deck
|
||||
fields = ("player", "heroes")
|
||||
|
||||
|
||||
class ObtainTokenPairSerializer(serializers.Serializer):
|
||||
refresh_token = serializers.CharField(max_length=300)
|
||||
|
||||
def __init__(self, instance=None, data=None, **kwargs):
|
||||
super().__init__(instance, data, **kwargs)
|
||||
self.player_id = None
|
||||
|
||||
def validate_refresh_token(self, value):
|
||||
payload = read_jwt(value)
|
||||
if not payload:
|
||||
raise ValidationError("Token is incorrect or expired")
|
||||
|
||||
if "jit" not in payload:
|
||||
raise ValidationError("Token is incorrect")
|
||||
|
||||
jit = payload["jit"]
|
||||
|
||||
try:
|
||||
session = PlayerAuthSession.objects.get(jit=jit)
|
||||
except PlayerAuthSession.DoesNotExist:
|
||||
return ValidationError("Incorrect user session")
|
||||
|
||||
self.player_id = session.player.id
|
||||
return value
|
|
@ -1,169 +0,0 @@
|
|||
from rest_framework import status
|
||||
|
||||
from rest_framework.generics import GenericAPIView, UpdateAPIView
|
||||
from rest_framework.mixins import (
|
||||
CreateModelMixin,
|
||||
RetrieveModelMixin,
|
||||
ListModelMixin,
|
||||
DestroyModelMixin,
|
||||
UpdateModelMixin,
|
||||
)
|
||||
from rest_framework.response import Response
|
||||
|
||||
from game.authentication import PlayerAuthentication
|
||||
from game.models import Hero, Deck
|
||||
from game.api.v1.serializers import (
|
||||
CreateHeroSerializer,
|
||||
GetHeroSerializer,
|
||||
CreatePlayerSerializer,
|
||||
ListHeroSerializer,
|
||||
CreateDeckSerializer,
|
||||
GetDeckSerializer,
|
||||
ObtainTokenPairSerializer,
|
||||
)
|
||||
from game.services.jwt import sign_jwt
|
||||
|
||||
|
||||
class ListCreateHeroView(GenericAPIView, CreateModelMixin, ListModelMixin):
|
||||
authentication_classes = (PlayerAuthentication,)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
return serializer.save()
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
instance = self.perform_create(serializer)
|
||||
return Response({"uuid": instance.uuid}, status=status.HTTP_201_CREATED)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
return self.list(request, *args, **kwargs)
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.request.method == "GET":
|
||||
return ListHeroSerializer
|
||||
else:
|
||||
return CreateHeroSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
return Hero.objects.filter(player_id=self.request.user.id)
|
||||
|
||||
|
||||
class RetrieveHeroView(RetrieveModelMixin, UpdateAPIView, GenericAPIView):
|
||||
serializer_class = GetHeroSerializer
|
||||
lookup_field = "uuid"
|
||||
queryset = Hero.objects.all()
|
||||
|
||||
def get_authenticators(self):
|
||||
if self.request.method != "GET":
|
||||
self.authentication_classes = [PlayerAuthentication]
|
||||
return [auth() for auth in self.authentication_classes]
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
return self.retrieve(request, *args, **kwargs)
|
||||
|
||||
def put(self, request, *args, **kwargs):
|
||||
return self.update(request, *args, **kwargs)
|
||||
|
||||
def patch(self, request, *args, **kwargs):
|
||||
return self.partial_update(request, *args, **kwargs)
|
||||
|
||||
|
||||
class PlayerCreateView(GenericAPIView, CreateModelMixin):
|
||||
serializer_class = CreatePlayerSerializer
|
||||
|
||||
def perform_create(self, serializer):
|
||||
return serializer.save()
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
instance = self.perform_create(serializer)
|
||||
|
||||
access_jwt = instance.get_access_token()
|
||||
refresh_jwt = instance.get_refresh_token()
|
||||
return Response(
|
||||
{
|
||||
"access_token": access_jwt,
|
||||
"refresh_token": refresh_jwt,
|
||||
"deck_id": instance.get_last_deck().id,
|
||||
},
|
||||
status=status.HTTP_201_CREATED,
|
||||
)
|
||||
|
||||
|
||||
class DeckCreateView(GenericAPIView, CreateModelMixin):
|
||||
serializer_class = CreateDeckSerializer
|
||||
authentication_classes = (PlayerAuthentication,)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
return serializer.save(player=self.request.user)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
instance = self.perform_create(serializer)
|
||||
heroes_list = ListHeroSerializer(instance.get_heroes(), many=True)
|
||||
heroes_list.data["deck_id"] = instance.id
|
||||
return Response(heroes_list.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
class RetireUpdateDeleteDeckView(
|
||||
RetrieveHeroView, DestroyModelMixin, UpdateModelMixin, GenericAPIView
|
||||
):
|
||||
lookup_field = "id"
|
||||
queryset = Deck.objects.all()
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.request.method == "GET":
|
||||
return GetDeckSerializer
|
||||
else:
|
||||
return CreateDeckSerializer
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
return self.retrieve(request, *args, **kwargs)
|
||||
|
||||
def perform_update(self, serializer):
|
||||
return serializer.update(self.get_object(), self.request.data)
|
||||
|
||||
def put(self, request, *args, **kwargs):
|
||||
if not self._check_user_identity(kwargs["id"]):
|
||||
return Response(
|
||||
"Attempt to change another user's deck",
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
instance = self.perform_update(serializer)
|
||||
heroes_list = ListHeroSerializer(instance.get_heroes(), many=True)
|
||||
return Response(heroes_list.data, status=status.HTTP_200_OK)
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
if not self._check_user_identity(kwargs["id"]):
|
||||
return Response(
|
||||
"Attempt to delete another user's deck",
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
self.destroy(request, *args, **kwargs)
|
||||
return Response(
|
||||
f"Destroyed deck with id {kwargs['id']}", status=status.HTTP_200_OK
|
||||
)
|
||||
|
||||
def _check_user_identity(self, deck_id) -> bool:
|
||||
return deck_id in list(
|
||||
Deck.objects.filter(player_id=self.request.user.id).values_list(
|
||||
"id", flat=True
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class RefreshAuthKey(GenericAPIView):
|
||||
serializer_class = ObtainTokenPairSerializer
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
access_jwt = sign_jwt(
|
||||
{"id": serializer.player_id, "type": "access"}, t_life=3600
|
||||
)
|
||||
return Response({"access_token": access_jwt}, status=status.HTTP_200_OK)
|
|
@ -1,6 +0,0 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class GameConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'game'
|
|
@ -1,35 +0,0 @@
|
|||
from jwt import DecodeError
|
||||
from rest_framework import authentication
|
||||
from rest_framework import exceptions
|
||||
from .models import Player
|
||||
from .services.jwt import read_jwt
|
||||
|
||||
|
||||
class PlayerAuthentication(authentication.BaseAuthentication):
|
||||
def authenticate(self, request):
|
||||
|
||||
if "Authorization" not in request.headers or not (
|
||||
token := request.headers["Authorization"]
|
||||
):
|
||||
raise exceptions.AuthenticationFailed("No credentials provided.")
|
||||
|
||||
try:
|
||||
t = read_jwt(token)
|
||||
except DecodeError:
|
||||
raise exceptions.AuthenticationFailed("Token is incorrect")
|
||||
|
||||
if not t:
|
||||
raise exceptions.AuthenticationFailed("Token is incorrect of expired")
|
||||
|
||||
if "id" not in t and "type" not in t:
|
||||
raise exceptions.AuthenticationFailed("No user data")
|
||||
|
||||
if t["type"] != "access":
|
||||
raise exceptions.AuthenticationFailed("Incorrect token type")
|
||||
|
||||
try:
|
||||
user = Player.objects.get(id=int(t["id"]))
|
||||
except Player.DoesNotExist:
|
||||
raise exceptions.AuthenticationFailed("No such user")
|
||||
|
||||
return user, None
|
|
@ -1,57 +0,0 @@
|
|||
# Generated by Django 4.0.5 on 2022-06-04 14:16
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Hero',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
|
||||
('added', models.DateTimeField(auto_now_add=True)),
|
||||
('type', models.CharField(choices=[('WIZARD', 'wizard'), ('ARCHER', 'archer'), ('WARRIOR', 'warrior')], max_length=7)),
|
||||
('idle_img', models.ImageField(upload_to='uploads/idle')),
|
||||
('attack_img', models.ImageField(upload_to='uploads/attack')),
|
||||
('die_img', models.ImageField(upload_to='uploads/die')),
|
||||
('health', models.IntegerField(validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(10)])),
|
||||
('speed', models.IntegerField(validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(10)])),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'hero',
|
||||
'verbose_name_plural': 'heroes',
|
||||
'ordering': ['-added'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Player',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('ton_wallet', models.CharField(max_length=50, verbose_name='TON wallet')),
|
||||
('name', models.CharField(blank=True, max_length=100)),
|
||||
('added', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'player',
|
||||
'verbose_name_plural': 'players',
|
||||
'ordering': ['-added'],
|
||||
},
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='player',
|
||||
index=models.Index(fields=['ton_wallet'], name='game_player_ton_wal_47dd93_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='hero',
|
||||
index=models.Index(fields=['uuid'], name='game_hero_uuid_ada5d9_idx'),
|
||||
),
|
||||
]
|
216
game/models.py
216
game/models.py
|
@ -1,216 +0,0 @@
|
|||
import random
|
||||
import uuid
|
||||
|
||||
from django.core.validators import (
|
||||
MinValueValidator,
|
||||
MaxValueValidator,
|
||||
MinLengthValidator,
|
||||
MaxLengthValidator,
|
||||
)
|
||||
from django.db import models
|
||||
|
||||
from common.generators import generate_charset
|
||||
from game.services.jwt import sign_jwt
|
||||
|
||||
|
||||
class HeroTypes(models.TextChoices):
|
||||
wizard = "WIZARD", "wizard"
|
||||
archer = "ARCHER", "archer"
|
||||
warrior = "WARRIOR", "warrior"
|
||||
king = "KING", "king"
|
||||
|
||||
|
||||
class Player(models.Model):
|
||||
"""base model to handle and store users"""
|
||||
|
||||
ton_wallet = models.CharField(
|
||||
verbose_name="TON wallet",
|
||||
validators=[MinLengthValidator(48), MaxLengthValidator(48)],
|
||||
max_length=48,
|
||||
unique=True,
|
||||
)
|
||||
name = models.CharField(max_length=100, blank=True)
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def save(
|
||||
self, force_insert=False, force_update=False, using=None, update_fields=None
|
||||
):
|
||||
"""saves user and creates deck for him with 16 heroes"""
|
||||
super(Player, self).save()
|
||||
PlayerAuthSession.objects.create(player=self)
|
||||
deck = Deck.objects.create(player=self)
|
||||
types = (
|
||||
["KING"]
|
||||
+ ["ARCHER" for _ in range(4)]
|
||||
+ ["WARRIOR" for _ in range(6)]
|
||||
+ ["WIZARD" for _ in range(2)]
|
||||
+ [random.choice(HeroTypes.choices[:3])[0] for _ in range(3)]
|
||||
)
|
||||
for t in types:
|
||||
hero = Hero()
|
||||
hero.player = self
|
||||
hero.type = t
|
||||
|
||||
hero.health = random.randint(0, 10)
|
||||
hero.attack = random.randint(0, 10)
|
||||
hero.speed = random.randint(0, 10)
|
||||
|
||||
hero.save()
|
||||
HeroInDeck.objects.create(deck=deck, hero=hero)
|
||||
|
||||
def get_last_deck(self):
|
||||
return Deck.objects.filter(player=self).last()
|
||||
|
||||
def get_auth_session(self):
|
||||
return PlayerAuthSession.objects.get(player=self).jit
|
||||
|
||||
def get_refresh_token(self):
|
||||
return sign_jwt({"jit": self.get_auth_session(), "type": "refresh"})
|
||||
|
||||
def get_access_token(self):
|
||||
return sign_jwt({"id": self.id, "type": "access"}, t_life=3600)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
indexes = [models.Index(fields=["ton_wallet"])]
|
||||
ordering = ["-created"]
|
||||
|
||||
db_table = "player"
|
||||
verbose_name = "player"
|
||||
verbose_name_plural = "players"
|
||||
|
||||
|
||||
class Hero(models.Model):
|
||||
"""Model to store heroes and their stats, connected to player"""
|
||||
|
||||
uuid = models.UUIDField(
|
||||
default=uuid.uuid4, editable=False, unique=True, primary_key=True
|
||||
)
|
||||
player = models.ForeignKey(
|
||||
Player,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="heroes",
|
||||
related_query_name="hero",
|
||||
)
|
||||
added = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
type = models.CharField(blank=False, choices=HeroTypes.choices, max_length=7)
|
||||
model = models.ForeignKey("HeroModelSet", on_delete=models.CASCADE)
|
||||
health = models.IntegerField(
|
||||
validators=[MinValueValidator(1), MaxValueValidator(10)], blank=False
|
||||
)
|
||||
attack = models.IntegerField(
|
||||
validators=[MinValueValidator(1), MaxValueValidator(10)], blank=False
|
||||
)
|
||||
speed = models.IntegerField(
|
||||
validators=[MinValueValidator(1), MaxValueValidator(10)], blank=False
|
||||
)
|
||||
|
||||
def idle_img(self):
|
||||
return self.idle_img_f.image.url
|
||||
|
||||
def attack_img(self):
|
||||
return self.attack_img_f.image.url
|
||||
|
||||
def die_img(self):
|
||||
return self.die_img_f.image.url
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.type} {self.player.name}"
|
||||
|
||||
def save(
|
||||
self, force_insert=False, force_update=False, using=None, update_fields=None
|
||||
):
|
||||
self.idle_img_f = random.choice(
|
||||
[x for x in HeroModelSet.objects.filter(hero_type=self.type)]
|
||||
)
|
||||
self.attack_img_f = random.choice(
|
||||
[x for x in HeroModelSet.objects.filter(hero_type=self.type)]
|
||||
)
|
||||
self.die_img_f = random.choice(
|
||||
[x for x in HeroModelSet.objects.filter(hero_type=self.type)]
|
||||
)
|
||||
super(Hero, self).save()
|
||||
|
||||
class Meta:
|
||||
indexes = [models.Index(fields=["uuid"])]
|
||||
ordering = ["-added"]
|
||||
|
||||
db_table = "hero"
|
||||
verbose_name = "hero"
|
||||
verbose_name_plural = "heroes"
|
||||
|
||||
|
||||
class HeroModelSet(models.Model):
|
||||
hero_type = models.CharField(blank=False, choices=HeroTypes.choices, max_length=7)
|
||||
model = models.ImageField(upload_to="uploads/")
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.hero_type} model file"
|
||||
|
||||
|
||||
class Deck(models.Model):
|
||||
player = models.ForeignKey(
|
||||
Player,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="decks",
|
||||
related_query_name="deck",
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.player.name}'s deck"
|
||||
|
||||
def get_heroes(self):
|
||||
return [x.hero for x in HeroInDeck.objects.filter(deck=self)]
|
||||
|
||||
def heroes(self):
|
||||
# added for better DRF view
|
||||
return self.get_heroes()
|
||||
|
||||
def score(self):
|
||||
return sum([x.attack + x.health + x.speed for x in self.get_heroes()])
|
||||
|
||||
class Meta:
|
||||
db_table = "deck"
|
||||
verbose_name = "deck"
|
||||
verbose_name_plural = "decks"
|
||||
|
||||
|
||||
class HeroInDeck(models.Model):
|
||||
deck = models.ForeignKey(
|
||||
Deck,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="hero_in_deck",
|
||||
related_query_name="heroes",
|
||||
)
|
||||
hero = models.OneToOneField(
|
||||
Hero,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="hero_in_deck",
|
||||
related_query_name="decks",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
db_table = "hero_in_deck"
|
||||
verbose_name = "Hero in deck"
|
||||
verbose_name_plural = "Heroes in decks"
|
||||
|
||||
|
||||
class PlayerAuthSession(models.Model):
|
||||
player = models.OneToOneField(
|
||||
Player, unique_for_month=True, on_delete=models.CASCADE
|
||||
)
|
||||
jit = models.CharField(max_length=30)
|
||||
|
||||
def save(
|
||||
self, force_insert=False, force_update=False, using=None, update_fields=None
|
||||
):
|
||||
self.jit = generate_charset(30)
|
||||
super(PlayerAuthSession, self).save(
|
||||
force_insert=force_insert,
|
||||
force_update=force_update,
|
||||
using=using,
|
||||
update_fields=update_fields,
|
||||
)
|
|
@ -1,45 +0,0 @@
|
|||
import jwt
|
||||
import pytz
|
||||
|
||||
from datetime import datetime
|
||||
from django.conf import settings
|
||||
from jwt import ExpiredSignatureError, InvalidSignatureError
|
||||
|
||||
TIMEZONE = pytz.timezone("Europe/Moscow")
|
||||
|
||||
|
||||
def sign_jwt(data: dict, t_life: None | int = None) -> str:
|
||||
"""generate and sign jwt with iat and exp using data from settings"""
|
||||
iat = int(datetime.now(tz=TIMEZONE).timestamp())
|
||||
exp = iat + settings.TOKEN_EXP if not t_life else iat + t_life
|
||||
payload = {"iat": iat, "exp": exp}
|
||||
for nm, el in data.items():
|
||||
if nm not in ["iat", "exp"]:
|
||||
payload[nm] = el
|
||||
|
||||
secret = settings.SECRET_KEY
|
||||
token = jwt.encode(payload=payload, key=secret)
|
||||
return token
|
||||
|
||||
|
||||
def read_jwt(token: str) -> dict | bool:
|
||||
"""reads jwt, validates it and return payload if correct"""
|
||||
header_data = jwt.get_unverified_header(token)
|
||||
secret = settings.SECRET_KEY
|
||||
try:
|
||||
payload = jwt.decode(token, key=secret, algorithms=[header_data["alg"]])
|
||||
except ExpiredSignatureError as e:
|
||||
return False
|
||||
except InvalidSignatureError as e:
|
||||
return False
|
||||
|
||||
if "exp" not in payload:
|
||||
return False
|
||||
|
||||
if int(datetime.now(tz=TIMEZONE).timestamp()) > payload["exp"]:
|
||||
return False
|
||||
|
||||
payload.pop("iat", None)
|
||||
payload.pop("exp", None)
|
||||
|
||||
return payload
|
21
game/urls.py
21
game/urls.py
|
@ -1,21 +0,0 @@
|
|||
from django.urls import path
|
||||
|
||||
from game.api.v1.views import (
|
||||
ListCreateHeroView,
|
||||
RetrieveHeroView,
|
||||
PlayerCreateView,
|
||||
DeckCreateView,
|
||||
RetireUpdateDeleteDeckView,
|
||||
RefreshAuthKey,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path("v1/hero/", ListCreateHeroView.as_view(), name="hero_api_create"),
|
||||
path("v1/hero/<uuid:uuid>", RetrieveHeroView.as_view(), name="hero_api_retrieve"),
|
||||
path("v1/player/refresh", RefreshAuthKey.as_view(), name="player_create_api"),
|
||||
path("v1/player/", PlayerCreateView.as_view(), name="player_create_api"),
|
||||
path("v1/deck/", DeckCreateView.as_view(), name="deck_create_api"),
|
||||
path(
|
||||
"v1/deck/<int:id>", RetireUpdateDeleteDeckView.as_view(), name="deck_retire_api"
|
||||
),
|
||||
]
|
22
manage.py
22
manage.py
|
@ -1,22 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'chess_backend.settings')
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -1,46 +0,0 @@
|
|||
asgiref==3.5.2
|
||||
asttokens==2.0.5
|
||||
attrs==21.4.0
|
||||
autobahn==22.5.1
|
||||
Automat==20.2.0
|
||||
backcall==0.2.0
|
||||
cffi==1.15.0
|
||||
channels==3.0.4
|
||||
constantly==15.1.0
|
||||
cryptography==37.0.2
|
||||
daphne==3.0.2
|
||||
decorator==5.1.1
|
||||
Django==4.0.5
|
||||
djangorestframework==3.13.1
|
||||
executing==0.8.3
|
||||
hyperlink==21.0.0
|
||||
idna==3.3
|
||||
incremental==21.3.0
|
||||
ipython==8.4.0
|
||||
jedi==0.18.1
|
||||
matplotlib-inline==0.1.3
|
||||
parso==0.8.3
|
||||
pexpect==4.8.0
|
||||
pickleshare==0.7.5
|
||||
Pillow==9.1.1
|
||||
prompt-toolkit==3.0.29
|
||||
ptyprocess==0.7.0
|
||||
pure-eval==0.2.2
|
||||
pyasn1==0.4.8
|
||||
pyasn1-modules==0.2.8
|
||||
pycparser==2.21
|
||||
Pygments==2.12.0
|
||||
PyJWT==2.4.0
|
||||
pyOpenSSL==22.0.0
|
||||
pytz==2022.1
|
||||
service-identity==21.1.0
|
||||
six==1.16.0
|
||||
sqlparse==0.4.2
|
||||
stack-data==0.2.0
|
||||
traitlets==5.2.2.post1
|
||||
Twisted==22.4.0
|
||||
txaio==22.2.1
|
||||
typing_extensions==4.2.0
|
||||
wcwidth==0.2.5
|
||||
zope.interface==5.4.0
|
||||
channels_redis
|
|
@ -1,6 +0,0 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class RoomConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'room'
|
|
@ -1,397 +0,0 @@
|
|||
import json
|
||||
|
||||
from asgiref.sync import sync_to_async
|
||||
from channels.generic.websocket import AsyncWebsocketConsumer
|
||||
from channels.layers import get_channel_layer
|
||||
|
||||
from game.models import Deck
|
||||
from room.models import PlayerInQueue, Room, PlayerInRoom, GameState
|
||||
from room.services.room_create import create_room
|
||||
|
||||
channel_layer = get_channel_layer()
|
||||
|
||||
|
||||
class QueueConsumer(AsyncWebsocketConsumer):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(args, kwargs)
|
||||
self.room_group_name = None
|
||||
|
||||
async def connect(self):
|
||||
self.room_group_name = "queue"
|
||||
|
||||
await self.accept()
|
||||
await self.check_origin()
|
||||
|
||||
await self.channel_layer.group_add(self.room_group_name, self.channel_name)
|
||||
|
||||
async def disconnect(self, close_code):
|
||||
await self.delete_user_in_queue()
|
||||
await self.channel_layer.group_discard(self.room_group_name, self.channel_name)
|
||||
|
||||
# Receive message from WebSocket
|
||||
async def receive(self, text_data):
|
||||
data = None
|
||||
|
||||
try:
|
||||
data = json.loads(text_data)
|
||||
except ValueError:
|
||||
await self.send(
|
||||
text_data=json.dumps(
|
||||
{"type": "ERROR", "message": "data is not JSON serializable"}
|
||||
)
|
||||
)
|
||||
|
||||
if data:
|
||||
# TODO move to external function/class
|
||||
if "type" not in data:
|
||||
await self.send(
|
||||
text_data=json.dumps(
|
||||
{"type": "ERROR", "message": "incorrect data typing"}
|
||||
)
|
||||
)
|
||||
else:
|
||||
if data["type"] == "connect":
|
||||
if "deck_id" not in data:
|
||||
await self.send(
|
||||
text_data=json.dumps(
|
||||
{"type": "ERROR", "message": "deck id is not provided"}
|
||||
)
|
||||
)
|
||||
else:
|
||||
deck = None
|
||||
# validate deck and check user originality
|
||||
try:
|
||||
deck_id = int(data["deck_id"])
|
||||
deck = await self.check_user_deck(deck_id)
|
||||
except ValueError:
|
||||
await self.send(
|
||||
text_data=json.dumps(
|
||||
{"type": "ERROR", "message": "deck id is incorrect"}
|
||||
)
|
||||
)
|
||||
if deck:
|
||||
# add to que, start finding players
|
||||
await self.queue_connector(deck)
|
||||
await self.send(
|
||||
text_data=json.dumps(
|
||||
{
|
||||
"type": "INFO",
|
||||
"message": f"added to queue deck with score {self.scope['score']}",
|
||||
}
|
||||
)
|
||||
)
|
||||
opponent = await self.find_user_by_score()
|
||||
|
||||
if not opponent:
|
||||
await self.send(
|
||||
text_data=json.dumps(
|
||||
{
|
||||
"type": "INFO",
|
||||
"message": "no user found, awaiting in queue",
|
||||
}
|
||||
)
|
||||
)
|
||||
else:
|
||||
# add to group and send message that opponent found to players
|
||||
room = await create_room(
|
||||
deck_id_1=self.scope["deck"],
|
||||
player_id_1=self.scope["player"],
|
||||
player_score_1=self.scope["score"],
|
||||
deck_id_2=opponent[2],
|
||||
player_id_2=opponent[3],
|
||||
player_score_2=opponent[1],
|
||||
)
|
||||
|
||||
await self.channel_layer.send(
|
||||
opponent[0],
|
||||
{
|
||||
"type": "info",
|
||||
"message": f"user found, with score {self.scope['score']}",
|
||||
"room": room,
|
||||
},
|
||||
)
|
||||
|
||||
await self.send(
|
||||
text_data=json.dumps(
|
||||
{
|
||||
"type": "INFO",
|
||||
"message": f"user found, with score {opponent[1]}",
|
||||
"room": room,
|
||||
}
|
||||
)
|
||||
)
|
||||
else:
|
||||
await self.send(
|
||||
text_data=json.dumps(
|
||||
{
|
||||
"type": "ERROR",
|
||||
"message": "such deck doesn't exist",
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
@sync_to_async
|
||||
def delete_user_in_queue(self):
|
||||
try:
|
||||
PlayerInQueue.objects.get(player_id=self.scope["player"]).delete()
|
||||
except PlayerInQueue.DoesNotExist:
|
||||
return False
|
||||
|
||||
@sync_to_async
|
||||
def find_user_by_score(self):
|
||||
s_min = self.scope["score"] * 0.95
|
||||
s_max = self.scope["score"] * 1.05
|
||||
for el in PlayerInQueue.objects.all():
|
||||
if el.player_id != self.scope["player"]:
|
||||
if s_min <= el.score <= s_max:
|
||||
return el.channel_name, el.score, el.deck.id, el.player.id
|
||||
return False
|
||||
|
||||
@sync_to_async
|
||||
def check_user_deck(self, deck_id: int):
|
||||
try:
|
||||
deck = Deck.objects.get(id=deck_id)
|
||||
if deck.player.id != self.scope["player"]:
|
||||
return False
|
||||
return deck
|
||||
except Deck.DoesNotExist:
|
||||
return False
|
||||
|
||||
@sync_to_async
|
||||
def queue_connector(self, deck):
|
||||
try:
|
||||
queue = PlayerInQueue.objects.get(player_id=self.scope["player"])
|
||||
queue.score = deck.score()
|
||||
queue.channel_name = self.channel_name
|
||||
queue.save()
|
||||
|
||||
except PlayerInQueue.DoesNotExist:
|
||||
queue = PlayerInQueue.objects.create(
|
||||
player_id=self.scope["player"],
|
||||
deck=deck,
|
||||
score=deck.score(),
|
||||
channel_name=self.channel_name,
|
||||
)
|
||||
|
||||
self.scope["queue"] = queue.id
|
||||
self.scope["deck"] = deck.id
|
||||
self.scope["score"] = queue.score
|
||||
|
||||
async def info(self, event):
|
||||
message = event["message"]
|
||||
msg = {"type": "INFO", "message": message}
|
||||
if "room" in event:
|
||||
msg["room"] = event["room"]
|
||||
|
||||
await self.send(text_data=json.dumps(msg))
|
||||
|
||||
async def check_origin(self):
|
||||
if not self.scope["player"]:
|
||||
await self.send(
|
||||
text_data=json.dumps(
|
||||
{"type": "ERROR", "message": "token is incorrect or expired"}
|
||||
)
|
||||
)
|
||||
await self.close()
|
||||
|
||||
|
||||
class RoomConsumer(AsyncWebsocketConsumer):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.room_group_name = None
|
||||
self.room_name = None
|
||||
|
||||
async def connect(self):
|
||||
await self.accept()
|
||||
await self.check_origin()
|
||||
|
||||
if not await self.connect_to_room():
|
||||
await self.close()
|
||||
else:
|
||||
message, round = await self.get_state()
|
||||
|
||||
await self.send(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "INFO",
|
||||
"opponent_score": self.scope["opponent_score"],
|
||||
"opponent_deck": self.scope["opponent_deck"],
|
||||
"opponent_online": self.scope["opponent_online"],
|
||||
"first": self.scope["first"],
|
||||
"state": message,
|
||||
"round": round,
|
||||
},
|
||||
)
|
||||
)
|
||||
if "opponent_channel" in self.scope and self.scope["opponent_channel"]:
|
||||
await self.channel_layer.send(
|
||||
self.scope["opponent_channel"],
|
||||
{
|
||||
"type": "channel",
|
||||
"channel": self.channel_name,
|
||||
},
|
||||
)
|
||||
await self.channel_layer.send(
|
||||
self.scope["opponent_channel"],
|
||||
{"type": "connection_info", "online": True},
|
||||
)
|
||||
|
||||
# Join room group
|
||||
await self.channel_layer.group_add(self.room_group_name, self.channel_name)
|
||||
|
||||
@sync_to_async
|
||||
def get_state(self):
|
||||
state = self.scope["player_in_room"].get_state()
|
||||
return state.message, state.round
|
||||
|
||||
@sync_to_async
|
||||
def connect_to_room(self):
|
||||
slug = self.scope["url_route"]["kwargs"]["room_name"]
|
||||
|
||||
self.room_name = slug
|
||||
self.room_group_name = f"room_{slug}"
|
||||
room = Room.objects.filter(slug=slug)
|
||||
|
||||
if not room:
|
||||
return False
|
||||
|
||||
self.scope["room"] = room
|
||||
|
||||
# check if player can be in a room
|
||||
p_ids = [x.player.id for x in room.first().players.all()]
|
||||
if self.scope["player"] not in p_ids:
|
||||
return False
|
||||
|
||||
# add player info to scope
|
||||
player = PlayerInRoom.objects.get(player_id=self.scope["player"])
|
||||
|
||||
self.scope["player_in_room"] = player
|
||||
self.scope["first"] = player.first
|
||||
self.scope["score"] = player.score
|
||||
self.scope["deck"] = player.deck.id
|
||||
|
||||
p_ids.remove(player.player.id)
|
||||
opponent = PlayerInRoom.objects.get(player_id=p_ids[0])
|
||||
|
||||
self.scope["opponent"] = opponent.player.id
|
||||
self.scope["opponent_channel"] = opponent.channel_name
|
||||
self.scope["opponent_score"] = opponent.score
|
||||
self.scope["opponent_deck"] = opponent.deck.id
|
||||
self.scope["opponent_first"] = opponent.first
|
||||
self.scope["opponent_online"] = opponent.online
|
||||
|
||||
player.online = True
|
||||
player.channel_name = self.channel_name
|
||||
player.save(update_fields=["online", "channel_name"])
|
||||
return True
|
||||
|
||||
async def disconnect(self, close_code):
|
||||
# Leave room group
|
||||
await self.channel_layer.group_discard(self.room_group_name, self.channel_name)
|
||||
await self.disconnect_player()
|
||||
|
||||
if "opponent_channel" in self.scope and self.scope["opponent_channel"]:
|
||||
await self.channel_layer.send(
|
||||
self.scope["opponent_channel"],
|
||||
{"type": "connection_info", "online": False},
|
||||
)
|
||||
|
||||
@sync_to_async
|
||||
def disconnect_player(self):
|
||||
if "player_in_room" in self.scope:
|
||||
self.scope["player_in_room"].online = False
|
||||
self.scope["player_in_room"].channel_name = None
|
||||
self.scope["player_in_room"].save(update_fields=["online", "channel_name"])
|
||||
|
||||
# Receive message from WebSocket
|
||||
async def receive(self, text_data):
|
||||
data = None
|
||||
|
||||
try:
|
||||
data = json.loads(text_data)
|
||||
except ValueError:
|
||||
await self.send(
|
||||
text_data=json.dumps(
|
||||
{"type": "ERROR", "message": "data is not JSON serializable"}
|
||||
)
|
||||
)
|
||||
|
||||
if data:
|
||||
if data["type"] == "start":
|
||||
if not await self.start(data):
|
||||
await self.send(
|
||||
text_data=json.dumps(
|
||||
{"type": "ERROR", "message": "opponent is offline"}
|
||||
)
|
||||
)
|
||||
|
||||
async def start(self, data):
|
||||
if self.scope["opponent_channel"] and self.scope["opponent_online"]:
|
||||
await self.channel_layer.send(
|
||||
self.scope["opponent_channel"],
|
||||
{
|
||||
"type": "info",
|
||||
"message": "opponent is ready to start",
|
||||
},
|
||||
)
|
||||
return True
|
||||
return False
|
||||
|
||||
# info type group message handler
|
||||
async def info(self, event):
|
||||
message = event["message"]
|
||||
msg = {"type": "INFO", "message": message}
|
||||
|
||||
if "opponent_score" in event:
|
||||
msg["opponent_score"] = event["opponent_score"]
|
||||
|
||||
if "opponent_deck" in event:
|
||||
msg["opponent_deck"] = event["opponent_deck"]
|
||||
|
||||
if "opponent_online" in event:
|
||||
msg["opponent_online"] = event["opponent_online"]
|
||||
|
||||
if "first" in event:
|
||||
msg["first"] = event["first"]
|
||||
|
||||
if "state" in event:
|
||||
msg["state"] = event["state"]
|
||||
|
||||
if "round" in event:
|
||||
msg["round"] = event["round"]
|
||||
|
||||
await self.send(text_data=json.dumps(msg))
|
||||
|
||||
# Receive message from room group
|
||||
async def chat_message(self, event):
|
||||
message = event["message"]
|
||||
|
||||
# Send message to WebSocket
|
||||
await self.send(text_data=json.dumps({"lot": message}))
|
||||
|
||||
async def channel(self, event):
|
||||
channel = event["channel"]
|
||||
self.scope["opponent_channel"] = channel
|
||||
|
||||
async def connection_info(self, event):
|
||||
status = event["online"]
|
||||
await self.send(
|
||||
text_data=json.dumps(
|
||||
{
|
||||
"type": "INFO",
|
||||
"message": "opponent is online"
|
||||
if status
|
||||
else "opponent is offline",
|
||||
}
|
||||
)
|
||||
)
|
||||
self.scope["opponent_online"] = status
|
||||
|
||||
async def check_origin(self):
|
||||
if not self.scope["player"]:
|
||||
await self.send(
|
||||
text_data=json.dumps(
|
||||
{"type": "ERROR", "message": "token is incorrect or expired"}
|
||||
)
|
||||
)
|
||||
await self.close()
|
|
@ -1,33 +0,0 @@
|
|||
from channels.db import database_sync_to_async
|
||||
from django.core.exceptions import PermissionDenied
|
||||
|
||||
from game.models import Player
|
||||
from game.services.jwt import read_jwt
|
||||
|
||||
|
||||
@database_sync_to_async
|
||||
def get_player(headers):
|
||||
# WARNING headers type is bytes
|
||||
if b"authorization" not in headers or not headers[b"authorization"]:
|
||||
return False
|
||||
|
||||
jwt = headers[b"authorization"].decode()
|
||||
payload = read_jwt(jwt)
|
||||
|
||||
if not payload or "id" not in payload:
|
||||
return False
|
||||
|
||||
return payload["id"]
|
||||
|
||||
|
||||
class HeaderAuthMiddleware:
|
||||
"""Custom middleware to read user auth token from string."""
|
||||
|
||||
def __init__(self, app):
|
||||
# Store the ASGI application we were passed
|
||||
self.app = app
|
||||
|
||||
async def __call__(self, scope, receive, send):
|
||||
scope["player"] = await get_player(dict(scope["headers"]))
|
||||
|
||||
return await self.app(scope, receive, send)
|
|
@ -1,15 +0,0 @@
|
|||
# Generated by Django 4.0.5 on 2022-06-23 22:56
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
]
|
|
@ -1,50 +0,0 @@
|
|||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
from game.models import Player, Deck
|
||||
|
||||
|
||||
class PlayerInQueue(models.Model):
|
||||
# TODO use redis for storing
|
||||
player = models.OneToOneField(Player, unique=True, on_delete=models.CASCADE)
|
||||
channel_name = models.CharField(max_length=50, blank=False)
|
||||
deck = models.ForeignKey(Deck, on_delete=models.CASCADE)
|
||||
score = models.IntegerField()
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.player.name} in que with score {self.score}"
|
||||
|
||||
|
||||
class Room(models.Model):
|
||||
slug = models.SlugField(max_length=16, unique=True)
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
ended = models.BooleanField(default=False)
|
||||
|
||||
def __str__(self):
|
||||
return f"room with slug {self.slug}"
|
||||
|
||||
|
||||
class PlayerInRoom(models.Model):
|
||||
player = models.OneToOneField(Player, unique=True, on_delete=models.CASCADE)
|
||||
room = models.ForeignKey(Room, on_delete=models.CASCADE, related_name="players")
|
||||
first = models.BooleanField()
|
||||
score = models.IntegerField(blank=False)
|
||||
deck = models.ForeignKey(Deck, on_delete=models.CASCADE, related_name="decks")
|
||||
online = models.BooleanField(default=False)
|
||||
channel_name = models.CharField(max_length=50, blank=True, null=True)
|
||||
|
||||
def get_state(self):
|
||||
return GameState.objects.filter(player=self.player, room=self.room).last()
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.player.name} in room {self.room.slug}"
|
||||
|
||||
|
||||
class GameState(models.Model):
|
||||
room = models.ForeignKey(Room, on_delete=models.CASCADE)
|
||||
player = models.ForeignKey(Player, on_delete=models.CASCADE)
|
||||
round = models.IntegerField(blank=False)
|
||||
message = models.CharField(max_length=100, blank=False)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["room", "player", "round"]
|
|
@ -1,8 +0,0 @@
|
|||
from django.urls import path
|
||||
|
||||
from . import consumers
|
||||
|
||||
websocket_urlpatterns = [
|
||||
path("room/", consumers.QueueConsumer.as_asgi()),
|
||||
path("room/<str:room_name>", consumers.RoomConsumer.as_asgi()),
|
||||
]
|
|
@ -1,45 +0,0 @@
|
|||
from asgiref.sync import sync_to_async
|
||||
from random import randint
|
||||
|
||||
from common.generators import generate_charset
|
||||
from game.models import Player
|
||||
from room.models import Room, PlayerInRoom, GameState
|
||||
|
||||
|
||||
@sync_to_async
|
||||
def create_room(
|
||||
deck_id_1: int,
|
||||
player_id_1: int,
|
||||
player_score_1: int,
|
||||
deck_id_2: int,
|
||||
player_id_2: int,
|
||||
player_score_2: int,
|
||||
) -> str:
|
||||
room = Room.objects.create(slug=generate_charset(16))
|
||||
player_1 = Player.objects.get(id=player_id_1)
|
||||
player_2 = Player.objects.get(id=player_id_2)
|
||||
|
||||
first_player = randint(1, 2)
|
||||
|
||||
PlayerInRoom.objects.create(
|
||||
player=player_1,
|
||||
room=room,
|
||||
score=player_score_1,
|
||||
deck_id=deck_id_1,
|
||||
first=first_player == 1,
|
||||
)
|
||||
|
||||
PlayerInRoom.objects.create(
|
||||
player=player_2,
|
||||
room=room,
|
||||
score=player_score_2,
|
||||
deck_id=deck_id_2,
|
||||
first=first_player == 2,
|
||||
)
|
||||
GameState.objects.create(
|
||||
room=room, player=player_1, round=0, message="Game started"
|
||||
)
|
||||
GameState.objects.create(
|
||||
room=room, player=player_2, round=0, message="Game started"
|
||||
)
|
||||
return room.slug
|
|
@ -1,3 +0,0 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
|
@ -1,3 +0,0 @@
|
|||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
Loading…
Reference in New Issue
Block a user