mirror of
https://github.com/evgen-app/chess_rpg_backend.git
synced 2024-11-25 11:04:03 +03:00
Added stable version for stable branch
This commit is contained in:
parent
ba0c8499e0
commit
b739665d48
133
.gitignore
vendored
Normal file
133
.gitignore
vendored
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
.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
Normal file
37
README.md
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
# 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"
|
||||||
|
}
|
||||||
|
```
|
0
chess_backend/__init__.py
Normal file
0
chess_backend/__init__.py
Normal file
18
chess_backend/asgi.py
Normal file
18
chess_backend/asgi.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
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)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
104
chess_backend/settings.py
Normal file
104
chess_backend/settings.py
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
46
chess_backend/urls.py
Normal file
46
chess_backend/urls.py
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
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)
|
16
chess_backend/wsgi.py
Normal file
16
chess_backend/wsgi.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
"""
|
||||||
|
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()
|
0
common/__init__.py
Normal file
0
common/__init__.py
Normal file
14
common/generators.py
Normal file
14
common/generators.py
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
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)]))
|
22
common/tests.py
Normal file
22
common/tests.py
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
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)
|
0
game/__init__.py
Normal file
0
game/__init__.py
Normal file
6
game/admin.py
Normal file
6
game/admin.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
|
from game.models import HeroImageSet
|
||||||
|
|
||||||
|
admin.site.register(HeroImageSet)
|
0
game/api/__init__.py
Normal file
0
game/api/__init__.py
Normal file
0
game/api/v1/__init__.py
Normal file
0
game/api/v1/__init__.py
Normal file
139
game/api/v1/serializers.py
Normal file
139
game/api/v1/serializers.py
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
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
|
169
game/api/v1/views.py
Normal file
169
game/api/v1/views.py
Normal file
|
@ -0,0 +1,169 @@
|
||||||
|
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)
|
6
game/apps.py
Normal file
6
game/apps.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class GameConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'game'
|
35
game/authentication.py
Normal file
35
game/authentication.py
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
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
|
57
game/migrations/0001_initial.py
Normal file
57
game/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
# 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'),
|
||||||
|
),
|
||||||
|
]
|
0
game/migrations/__init__.py
Normal file
0
game/migrations/__init__.py
Normal file
216
game/models.py
Normal file
216
game/models.py
Normal file
|
@ -0,0 +1,216 @@
|
||||||
|
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,
|
||||||
|
)
|
0
game/permissions.py
Normal file
0
game/permissions.py
Normal file
0
game/services/__init__.py
Normal file
0
game/services/__init__.py
Normal file
45
game/services/jwt.py
Normal file
45
game/services/jwt.py
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
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
|
0
game/tests.py
Normal file
0
game/tests.py
Normal file
21
game/urls.py
Normal file
21
game/urls.py
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
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
Executable file
22
manage.py
Executable file
|
@ -0,0 +1,22 @@
|
||||||
|
#!/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()
|
46
requirements.txt
Normal file
46
requirements.txt
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
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
|
0
room/__init__.py
Normal file
0
room/__init__.py
Normal file
6
room/apps.py
Normal file
6
room/apps.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class RoomConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'room'
|
397
room/consumers.py
Normal file
397
room/consumers.py
Normal file
|
@ -0,0 +1,397 @@
|
||||||
|
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()
|
33
room/middleware.py
Normal file
33
room/middleware.py
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
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)
|
15
room/migrations/0001_initial.py
Normal file
15
room/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
# 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 = [
|
||||||
|
]
|
0
room/migrations/__init__.py
Normal file
0
room/migrations/__init__.py
Normal file
50
room/models.py
Normal file
50
room/models.py
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
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"]
|
8
room/routing.py
Normal file
8
room/routing.py
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
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()),
|
||||||
|
]
|
0
room/services/__init__.py
Normal file
0
room/services/__init__.py
Normal file
45
room/services/room_create.py
Normal file
45
room/services/room_create.py
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
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
|
3
room/tests.py
Normal file
3
room/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
3
room/views.py
Normal file
3
room/views.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
# Create your views here.
|
Loading…
Reference in New Issue
Block a user