diff --git a/.gitignore b/.gitignore
index f28dba1..deea292 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,6 @@
.idea
static/
-media/
+media/uploads/
# Byte-compiled / optimized / DLL files
__pycache__/
diff --git a/README.md b/README.md
index 12d3433..56e1fc9 100644
--- a/README.md
+++ b/README.md
@@ -1,15 +1,28 @@
# chess_rpg_backend
-Backend for chess rpg game
+DEV branch for backend for chess rpg game
+
+
+##### dev server for up to date endpoints(web socket not provided)
+
+- https://dev.akarpov.ru
+
+
### installation
```shell
$ python3 manage.py makemigrations & python3 manage.py migrate
+$ python3 manage.py loaddata media/dump_data/hero_model_fixture.json
$ docker run -p 6379:6379 -d redis:5
```
-### run
+### dev run
```shell
-$ python3 manage.py runserver 0.0.0.0:8000
+$ python3 manage.py runserver 0.0.0.0:8000
+```
+
+### prod run
+```shell
+$ daphne -b 0.0.0.0 -p 8000 chess_backend.asgi:application
```
### Описание команд сокетов
diff --git a/chess_backend/__init__.py b/chess_backend/__init__.py
index e69de29..5568b6d 100644
--- a/chess_backend/__init__.py
+++ b/chess_backend/__init__.py
@@ -0,0 +1,5 @@
+# This will make sure the app is always imported when
+# Django starts so that shared_task will use this app.
+from .celery import app as celery_app
+
+__all__ = ("celery_app",)
diff --git a/chess_backend/celery.py b/chess_backend/celery.py
new file mode 100644
index 0000000..20f73ee
--- /dev/null
+++ b/chess_backend/celery.py
@@ -0,0 +1,21 @@
+import os
+
+from celery import Celery
+
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "chess_backend.settings")
+
+app = Celery("chess_backend")
+
+# Using a string here means the worker doesn't have to serialize
+# the configuration object to child processes.
+# - namespace='CELERY' means all celery-related configuration keys
+# should have a `CELERY_` prefix.
+app.config_from_object("django.conf:settings", namespace="CELERY")
+
+# Load task modules from all registered Django apps.
+app.autodiscover_tasks()
+
+
+@app.task(bind=True, ignore_result=True)
+def debug_task(self):
+ print(f"Request: {self.request!r}")
diff --git a/chess_backend/settings.py b/chess_backend/settings.py
index a05c22c..712aa2c 100644
--- a/chess_backend/settings.py
+++ b/chess_backend/settings.py
@@ -6,15 +6,24 @@ 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
+if DEBUG:
+ TOKEN_EXP = 31536000 # 1 year
+ AUTH_EXP = 31536000 # 1 year
+else:
+ TOKEN_EXP = 2678400 # 1 month
+ AUTH_EXP = 3600 # 1 hour
ALLOWED_HOSTS = []
+if DEBUG:
+ ALLOWED_HOSTS = ["*"]
+
INSTALLED_APPS = [
"django.contrib.sessions",
"django.contrib.auth",
"django.contrib.contenttypes",
+ "django.contrib.staticfiles",
"django.contrib.messages",
# Packages
"rest_framework",
@@ -24,9 +33,22 @@ INSTALLED_APPS = [
"room",
]
-if DEBUG:
- INSTALLED_APPS.append("django.contrib.staticfiles")
- INSTALLED_APPS.append("drf_yasg")
+
+TEMPLATES = [
+ {
+ "BACKEND": "django.template.backends.django.DjangoTemplates",
+ "DIRS": [],
+ "APP_DIRS": True,
+ "OPTIONS": {
+ "context_processors": [
+ "django.template.context_processors.debug",
+ "django.template.context_processors.request",
+ "django.contrib.auth.context_processors.auth",
+ "django.contrib.messages.context_processors.messages",
+ ],
+ },
+ },
+]
MIDDLEWARE = [
@@ -102,3 +124,9 @@ LOGGING = {
},
},
}
+
+# Celery Configuration Options
+CELERY_TIMEZONE = "Europe/Moscow"
+CELERY_TASK_TRACK_STARTED = True
+CELERY_TASK_TIME_LIMIT = 30 * 60
+CELERY_BROKER_URL = 'redis://localhost:6379/0'
diff --git a/chess_backend/urls.py b/chess_backend/urls.py
index 956f93a..e60cf14 100644
--- a/chess_backend/urls.py
+++ b/chess_backend/urls.py
@@ -1,46 +1,9 @@
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)
+ + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
)
-
-urlpatterns = [path("api/", include("game.urls"))] + static(
- settings.MEDIA_URL, document_root=settings.MEDIA_ROOT
-)
-
-if settings.DEBUG:
- urlpatterns += [
- re_path(
- r"^swagger(?P\.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)
diff --git a/common/debug.py b/common/debug.py
new file mode 100644
index 0000000..5eae3be
--- /dev/null
+++ b/common/debug.py
@@ -0,0 +1,48 @@
+from common.generators import gen_ton
+from game.models import Player, Deck, Hero
+from room.services.room_create import sync_create_room
+
+
+def _check_players_score(players):
+ for player in players:
+ for player2 in players:
+ if player != player2:
+ s_min = players[player] * 0.95
+ s_max = players[player] * 1.05
+ if s_min <= players[player2] <= s_max:
+ return False
+ return True
+
+
+def generate_room():
+ players = {}
+
+ for _ in range(2):
+ player = Player.objects.create(ton_wallet=gen_ton())
+ players[player] = player.get_last_deck().score()
+
+ while _check_players_score(players):
+ player = Player.objects.create(ton_wallet=gen_ton())
+ players[player] = player.get_last_deck().score()
+
+ for player in players:
+ for player2 in players:
+ if player != player2:
+ s_min = players[player] * 0.95
+ s_max = players[player] * 1.05
+ if s_min <= players[player2] <= s_max:
+ p1 = player
+ p2 = player2
+
+ room_slug = sync_create_room(
+ p1.get_last_deck().id,
+ p1.id,
+ players[p1],
+ p2.get_last_deck().id,
+ p2.id,
+ players[p2],
+ )
+ print(f"ws://127.0.0.1:8000/room/{room_slug}")
+ print(f"Authorization: {p1.get_access_token()}")
+ print(f"Authorization: {p2.get_access_token()}")
+ return None
diff --git a/docker-compose.yaml b/docker-compose.yaml
new file mode 100644
index 0000000..4bc884a
--- /dev/null
+++ b/docker-compose.yaml
@@ -0,0 +1,31 @@
+services:
+ daphne:
+ build:
+ dockerfile: ./build/Dockerfile
+ context: .
+ image: daphne
+ volumes:
+ - type: bind
+ source: ${ROOT_DIR}/logs
+ target: /app/logs
+ ports:
+ - "8080:8080"
+ - "8000:8000"
+ depends_on:
+ - postgres
+ tty: true
+ container_name: daphne_server
+ postgres:
+ image: postgres:latest
+ ports:
+ - "5432:5432"
+ environment:
+ - POSTGRES_USER=daphne
+ - POSTGRES_PASSWORD=daphne
+ - POSTGRES_DB=daphne
+ container_name: daphne_database
+ redis:
+ image: redis:latest
+ ports:
+ - "6379:6379"
+ container_name: daphne_redis
\ No newline at end of file
diff --git a/game/api/v1/serializers.py b/game/api/v1/serializers.py
index 7b2ef01..1cdf3c5 100644
--- a/game/api/v1/serializers.py
+++ b/game/api/v1/serializers.py
@@ -22,9 +22,7 @@ class GetHeroSerializer(serializers.ModelSerializer):
fields = (
"added",
"type",
- "idle_img",
- "attack_img",
- "die_img",
+ "model",
"health",
"attack",
"speed",
@@ -37,15 +35,21 @@ class ListHeroSerializer(serializers.ModelSerializer):
fields = (
"uuid",
"type",
- "idle_img",
- "attack_img",
- "die_img",
+ "model",
"health",
"attack",
"speed",
)
+class ListHeroInDeckSerializer(serializers.ModelSerializer):
+ hero = ListHeroSerializer()
+
+ class Meta:
+ model = HeroInDeck
+ fields = ("hero", "x", "y")
+
+
class CreatePlayerSerializer(serializers.ModelSerializer):
class Meta:
model = Player
@@ -106,7 +110,7 @@ class GetPlayerSerializer(serializers.ModelSerializer):
class GetDeckSerializer(serializers.ModelSerializer):
player = GetPlayerSerializer()
- heroes = ListHeroSerializer(many=True)
+ heroes = ListHeroInDeckSerializer(many=True)
class Meta:
model = Deck
diff --git a/game/apps.py b/game/apps.py
index 8ad49cb..25a8f7d 100644
--- a/game/apps.py
+++ b/game/apps.py
@@ -2,5 +2,8 @@ from django.apps import AppConfig
class GameConfig(AppConfig):
- default_auto_field = 'django.db.models.BigAutoField'
- name = 'game'
+ default_auto_field = "django.db.models.BigAutoField"
+ name = "game"
+
+ def ready(self):
+ import game.signals
diff --git a/game/migrations/0001_initial.py b/game/migrations/0001_initial.py
index af633c5..943fd71 100644
--- a/game/migrations/0001_initial.py
+++ b/game/migrations/0001_initial.py
@@ -1,8 +1,6 @@
# Generated by Django 4.0.5 on 2022-06-04 14:16
-import django.core.validators
-from django.db import migrations, models
-import uuid
+from django.db import migrations
class Migration(migrations.Migration):
@@ -13,45 +11,4 @@ class Migration(migrations.Migration):
]
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'),
- ),
]
diff --git a/game/models.py b/game/models.py
index 7976b25..4b5175f 100644
--- a/game/models.py
+++ b/game/models.py
@@ -9,14 +9,15 @@ from django.core.validators import (
)
from django.db import models
+from chess_backend import settings
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"
+ wizard = "WIZARD", "wizard"
king = "KING", "king"
@@ -32,32 +33,6 @@ class Player(models.Model):
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()
@@ -65,10 +40,13 @@ class Player(models.Model):
return PlayerAuthSession.objects.get(player=self).jit
def get_refresh_token(self):
- return sign_jwt({"jit": self.get_auth_session(), "type": "refresh"})
+ return sign_jwt(
+ {"jit": self.get_auth_session(), "type": "refresh"},
+ t_life=settings.TOKEN_EXP,
+ )
def get_access_token(self):
- return sign_jwt({"id": self.id, "type": "access"}, t_life=3600)
+ return sign_jwt({"id": self.id, "type": "access"}, t_life=settings.AUTH_EXP)
def __str__(self):
return self.name
@@ -97,7 +75,7 @@ class Hero(models.Model):
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)
+ model_f = models.ForeignKey("HeroModelSet", on_delete=models.CASCADE)
health = models.IntegerField(
validators=[MinValueValidator(1), MaxValueValidator(10)], blank=False
)
@@ -108,14 +86,8 @@ class Hero(models.Model):
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 model(self):
+ return self.model_f.model.url
def __str__(self):
return f"{self.type} {self.player.name}"
@@ -123,16 +95,8 @@ class Hero(models.Model):
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()
+ self.model_f = random.choice(HeroModelSet.objects.filter(hero_type=self.type))
+ super(Hero, self).save(force_insert, force_update, using, update_fields)
class Meta:
indexes = [models.Index(fields=["uuid"])]
@@ -145,7 +109,7 @@ class Hero(models.Model):
class HeroModelSet(models.Model):
hero_type = models.CharField(blank=False, choices=HeroTypes.choices, max_length=7)
- model = models.ImageField(upload_to="uploads/")
+ model = models.FileField(upload_to="uploads/")
def __str__(self):
return f"{self.hero_type} model file"
@@ -163,14 +127,15 @@ class Deck(models.Model):
return f"{self.player.name}'s deck"
def get_heroes(self):
- return [x.hero for x in HeroInDeck.objects.filter(deck=self)]
+ return 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()])
+ return sum(
+ [x.hero.attack + x.hero.health + x.hero.speed for x in self.get_heroes()]
+ )
class Meta:
db_table = "deck"
@@ -191,11 +156,19 @@ class HeroInDeck(models.Model):
related_name="hero_in_deck",
related_query_name="decks",
)
+ x = models.IntegerField(
+ blank=False, validators=[MinValueValidator(1), MaxValueValidator(8)]
+ )
+ y = models.IntegerField(
+ blank=False, validators=[MinValueValidator(1), MaxValueValidator(2)]
+ )
class Meta:
db_table = "hero_in_deck"
verbose_name = "Hero in deck"
verbose_name_plural = "Heroes in decks"
+ ordering = ["y", "x"]
+ unique_together = ["hero", "x", "y"]
class PlayerAuthSession(models.Model):
diff --git a/game/services/deck_handler.py b/game/services/deck_handler.py
new file mode 100644
index 0000000..a892119
--- /dev/null
+++ b/game/services/deck_handler.py
@@ -0,0 +1,49 @@
+import random
+
+from game.models import Deck, Player, HeroTypes, Hero, HeroInDeck
+
+
+def create_first_deck(player: Player):
+ deck = Deck.objects.create(player=player)
+ positions = []
+
+ for x in range(1, 9):
+ for y in range(1, 3):
+ positions.append((x, y))
+ positions.remove((4, 1))
+ positions.remove((5, 1))
+ random.shuffle(positions)
+
+ types = ["KING", "WIZARD"] + ["ARCHER" for _ in range(4)] + ["WARRIOR" for _ in range(6)]
+
+ for _ in range(4):
+ t = random.choice(HeroTypes.choices[:3])[0]
+ if t == "WIZARD" and types.count("WIZARD") > 1:
+ t = random.choice(HeroTypes.choices[:2])[0]
+ types.append(t)
+
+ counter = 0
+ for t in types:
+ hero = Hero()
+ hero.player = player
+ hero.type = t
+
+ # set random position on deck for heroes
+ if t == "KING":
+ pos_x = 5
+ pos_y = 1
+ elif t == "WIZARD":
+ pos_x = 4
+ pos_y = 1
+ else:
+ pos_x = positions[counter][0]
+ pos_y = positions[counter][1]
+
+ counter += 1
+
+ hero.health = random.randint(1, 10)
+ hero.attack = random.randint(1, 10)
+ hero.speed = random.randint(1, 10)
+
+ hero.save()
+ HeroInDeck.objects.create(deck=deck, hero=hero, x=pos_x, y=pos_y)
diff --git a/game/signals.py b/game/signals.py
new file mode 100644
index 0000000..c3190d9
--- /dev/null
+++ b/game/signals.py
@@ -0,0 +1,11 @@
+from django.db.models.signals import post_save
+from django.dispatch import receiver
+from .models import Player, PlayerAuthSession
+from .services.deck_handler import create_first_deck
+
+
+@receiver(post_save, sender=Player)
+def create_player(sender, instance, created, **kwargs):
+ if created:
+ PlayerAuthSession.objects.create(player=instance)
+ create_first_deck(instance)
diff --git a/media/dump_data/dump.jpg b/media/dump_data/dump.jpg
new file mode 100644
index 0000000..4c60bc1
Binary files /dev/null and b/media/dump_data/dump.jpg differ
diff --git a/media/dump_data/dump_1P13Svy.jpg b/media/dump_data/dump_1P13Svy.jpg
new file mode 100644
index 0000000..4c60bc1
Binary files /dev/null and b/media/dump_data/dump_1P13Svy.jpg differ
diff --git a/media/dump_data/dump_d6lH7IJ.jpg b/media/dump_data/dump_d6lH7IJ.jpg
new file mode 100644
index 0000000..4c60bc1
Binary files /dev/null and b/media/dump_data/dump_d6lH7IJ.jpg differ
diff --git a/media/dump_data/dump_mx4zxHq.jpg b/media/dump_data/dump_mx4zxHq.jpg
new file mode 100644
index 0000000..4c60bc1
Binary files /dev/null and b/media/dump_data/dump_mx4zxHq.jpg differ
diff --git a/media/dump_data/hero_model_fixture.json b/media/dump_data/hero_model_fixture.json
new file mode 100644
index 0000000..76a6e19
--- /dev/null
+++ b/media/dump_data/hero_model_fixture.json
@@ -0,0 +1,34 @@
+[
+ {
+ "model": "game.heromodelset",
+ "pk": 1,
+ "fields": {
+ "hero_type": "KING",
+ "model": "dump_data/dump.jpg"
+ }
+ },
+ {
+ "model": "game.heromodelset",
+ "pk": 2,
+ "fields": {
+ "hero_type": "WARRIOR",
+ "model": "dump_data/dump_mx4zxHq.jpg"
+ }
+ },
+ {
+ "model": "game.heromodelset",
+ "pk": 3,
+ "fields": {
+ "hero_type": "ARCHER",
+ "model": "dump_data/dump_d6lH7IJ.jpg"
+ }
+ },
+ {
+ "model": "game.heromodelset",
+ "pk": 4,
+ "fields": {
+ "hero_type": "WIZARD",
+ "model": "dump_data/dump_1P13Svy.jpg"
+ }
+ }
+]
\ No newline at end of file
diff --git a/nginx.conf b/nginx.conf
new file mode 100644
index 0000000..e538abf
--- /dev/null
+++ b/nginx.conf
@@ -0,0 +1,44 @@
+server {
+ # Base settings for client body size
+ client_max_body_size 64M;
+ client_body_timeout 10;
+ send_timeout 10;
+ keepalive_timeout 10;
+ client_header_timeout 10;
+ client_body_buffer_size 64M;
+ client_header_buffer_size 64M;
+ client_max_header_size 64M;
+
+ location /static {
+ allow all;
+ autoindex off;
+ root /var/www;
+ }
+
+ location /media {
+ allow all;
+ autoindex off;
+ root /var/www;
+ }
+
+ location / {
+ allow all;
+ proxy_pass http://127.0.0.1:8080;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header Host $host;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ add_header P3P 'CP="ALL DSP COR PSAa PSDa OUR NOR ONL UNI COM NAV"';
+
+ # WebSocket support
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ }
+
+ listen 443 ssl;
+
+ # ssl settings
+ # ssl_certificate /etc/nginx/ssl/nginx.crt;
+ # ssl_certificate_key /etc/nginx/ssl/nginx.key;
+ # ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
+}
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index 767bdf9..0000000
--- a/requirements.txt
+++ /dev/null
@@ -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
diff --git a/requirements/base.txt b/requirements/base.txt
new file mode 100644
index 0000000..c274de8
--- /dev/null
+++ b/requirements/base.txt
@@ -0,0 +1,8 @@
+Django==4.0.5
+channels==3.0.4
+djangorestframework==3.13.1
+Pillow==9.1.1
+PyJWT==2.4.0
+channels-redis==3.4.1
+celery==5.2.7
+redis==4.3.4
\ No newline at end of file
diff --git a/requirements/dev.txt b/requirements/dev.txt
new file mode 100644
index 0000000..66015c4
--- /dev/null
+++ b/requirements/dev.txt
@@ -0,0 +1,4 @@
+-r base.txt
+
+ipython==8.4.0
+termcolor==1.1.0
\ No newline at end of file
diff --git a/requirements/prod.txt b/requirements/prod.txt
new file mode 100644
index 0000000..fcfa020
--- /dev/null
+++ b/requirements/prod.txt
@@ -0,0 +1,3 @@
+-r base.txt
+
+daphne==3.0.2
\ No newline at end of file
diff --git a/room/consumers.py b/room/consumers.py
index 04c8fab..8455b62 100644
--- a/room/consumers.py
+++ b/room/consumers.py
@@ -1,17 +1,29 @@
import json
+import os
+import django
from asgiref.sync import sync_to_async
from channels.generic.websocket import AsyncWebsocketConsumer
-from channels.layers import get_channel_layer
+
+from room.services.game_logic import move_handler
+
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "chess_backend.settings")
+django.setup()
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 BaseConsumer(AsyncWebsocketConsumer):
+ def __init__(self, *args, **kwargs):
+ super().__init__(args, kwargs)
+
+ async def send_message(self, message_type: str, **data):
+ await self.send(text_data=json.dumps({"type": message_type, **data}))
-class QueueConsumer(AsyncWebsocketConsumer):
+class QueueConsumer(BaseConsumer):
def __init__(self, *args, **kwargs):
super().__init__(args, kwargs)
self.room_group_name = None
@@ -22,6 +34,14 @@ class QueueConsumer(AsyncWebsocketConsumer):
await self.accept()
await self.check_origin()
+ if await self.check_user_already_in_room():
+ await self.send_message(
+ "INFO",
+ message=f"user already in room {self.scope['room_id']}",
+ room=self.scope["room"],
+ )
+ await self.close()
+
await self.channel_layer.group_add(self.room_group_name, self.channel_name)
async def disconnect(self, close_code):
@@ -35,27 +55,17 @@ class QueueConsumer(AsyncWebsocketConsumer):
try:
data = json.loads(text_data)
except ValueError:
- await self.send(
- text_data=json.dumps(
- {"type": "ERROR", "message": "data is not JSON serializable"}
- )
- )
+ await self.send_message("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"}
- )
- )
+ await self.send_message("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"}
- )
+ await self.send_message(
+ "ERROR", message="deck id is not provided"
)
else:
deck = None
@@ -64,32 +74,22 @@ class QueueConsumer(AsyncWebsocketConsumer):
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"}
- )
+ await self.send_message(
+ "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']}",
- }
- )
+ await self.send_message(
+ "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",
- }
- )
+ await self.send_message(
+ "INFO", message="no user found, awaiting in queue"
)
else:
# add to group and send message that opponent found to players
@@ -111,32 +111,35 @@ class QueueConsumer(AsyncWebsocketConsumer):
},
)
- await self.send(
- text_data=json.dumps(
- {
- "type": "INFO",
- "message": f"user found, with score {opponent[1]}",
- "room": room,
- }
- )
+ await self.send_message(
+ "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",
- }
- )
+ await self.send_message(
+ "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()
+ PlayerInQueue.objects.get(player_id=self.scope["player"])
except PlayerInQueue.DoesNotExist:
return False
+ @sync_to_async
+ def check_user_already_in_room(self):
+ try:
+ p = PlayerInRoom.objects.get(player_id=self.scope["player"])
+
+ self.scope["room"] = p.room.slug
+ self.scope["room_id"] = p.room.id
+
+ return True
+ except PlayerInRoom.DoesNotExist:
+ return False
+
@sync_to_async
def find_user_by_score(self):
s_min = self.scope["score"] * 0.95
@@ -178,24 +181,20 @@ class QueueConsumer(AsyncWebsocketConsumer):
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))
+ await self.send_message(
+ "INFO", message=event["message"], room=event["room"]
+ )
+ else:
+ await self.send_message("INFO", message=event["message"])
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.send_message("ERROR", message="token is incorrect or expired")
await self.close()
-class RoomConsumer(AsyncWebsocketConsumer):
+class RoomConsumer(BaseConsumer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.room_group_name = None
@@ -210,18 +209,14 @@ class RoomConsumer(AsyncWebsocketConsumer):
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,
- },
- )
+ await self.send_message(
+ "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(
@@ -239,6 +234,10 @@ class RoomConsumer(AsyncWebsocketConsumer):
# Join room group
await self.channel_layer.group_add(self.room_group_name, self.channel_name)
+ # load and send board
+ await self.load_board()
+ await self.send_board()
+
@sync_to_async
def get_state(self):
state = self.scope["player_in_room"].get_state()
@@ -255,7 +254,7 @@ class RoomConsumer(AsyncWebsocketConsumer):
if not room:
return False
- self.scope["room"] = room
+ self.scope["room"] = room.first()
# check if player can be in a room
p_ids = [x.player.id for x in room.first().players.all()]
@@ -269,6 +268,7 @@ class RoomConsumer(AsyncWebsocketConsumer):
self.scope["first"] = player.first
self.scope["score"] = player.score
self.scope["deck"] = player.deck.id
+ self.scope["state"] = room.first().states.last().round
p_ids.remove(player.player.id)
opponent = PlayerInRoom.objects.get(player_id=p_ids[0])
@@ -310,20 +310,19 @@ class RoomConsumer(AsyncWebsocketConsumer):
try:
data = json.loads(text_data)
except ValueError:
- await self.send(
- text_data=json.dumps(
- {"type": "ERROR", "message": "data is not JSON serializable"}
- )
- )
+ await self.send_message("ERROR", message="data is not JSON serializable")
if data:
- if data["type"] == "start":
+ if "type" not in data:
+ await self.send_message("ERROR", message="incorrect data typing")
+ elif data["type"] == "start":
if not await self.start(data):
- await self.send(
- text_data=json.dumps(
- {"type": "ERROR", "message": "opponent is offline"}
- )
- )
+ await self.send_message("ERROR", message="opponent is offline")
+ elif data["type"] == "move":
+ if all(x in data for x in ["x", "y", "px", "py"]):
+ await self.perform_move(data)
+ else:
+ await self.send_message("ERROR", message="incorrect data typing")
async def start(self, data):
if self.scope["opponent_channel"] and self.scope["opponent_online"]:
@@ -337,6 +336,57 @@ class RoomConsumer(AsyncWebsocketConsumer):
return True
return False
+ async def perform_move(self, data):
+ if await move_handler(
+ data["px"],
+ data["py"],
+ data["x"],
+ data["y"],
+ self.room_name,
+ self.scope["player_in_room"],
+ ):
+ await self.send_board()
+ if self.scope["opponent_channel"] and self.scope["opponent_online"]:
+ await self.channel_layer.send(
+ self.scope["opponent_channel"],
+ {
+ "type": "move",
+ "x": data["x"],
+ "y": data["y"],
+ "px": data["px"],
+ "py": data["py"],
+ },
+ )
+ return True
+ return False
+
+ @sync_to_async
+ def load_board(self):
+ # loads bord from db to scope
+ room = self.scope["room"]
+ board = [
+ [None, None, None, None, None, None, None, None],
+ [None, None, None, None, None, None, None, None],
+ [None, None, None, None, None, None, None, None],
+ [None, None, None, None, None, None, None, None],
+ [None, None, None, None, None, None, None, None],
+ [None, None, None, None, None, None, None, None],
+ [None, None, None, None, None, None, None, None],
+ [None, None, None, None, None, None, None, None],
+ ]
+ for el in room.heroes.all():
+ board[el.y - 1][el.x - 1] = [el.hero.type, el.health]
+
+ self.scope["board"] = board
+
+ async def send_board(self):
+ # sends board to client
+ await self.send_message(
+ "INFO",
+ message=f"game's board for round {self.scope['state']}",
+ board=self.scope["board"],
+ )
+
# info type group message handler
async def info(self, event):
message = event["message"]
@@ -362,13 +412,6 @@ class RoomConsumer(AsyncWebsocketConsumer):
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
@@ -387,6 +430,19 @@ class RoomConsumer(AsyncWebsocketConsumer):
)
self.scope["opponent_online"] = status
+ async def move(self, event):
+ await self.send(
+ text_data=json.dumps(
+ {
+ "type": "MOVE",
+ "x": event["x"],
+ "y": event["y"],
+ "px": event["px"],
+ "py": event["py"],
+ }
+ )
+ )
+
async def check_origin(self):
if not self.scope["player"]:
await self.send(
diff --git a/room/models.py b/room/models.py
index 047412b..5e64bec 100644
--- a/room/models.py
+++ b/room/models.py
@@ -1,7 +1,8 @@
+from django.core.validators import MinValueValidator, MaxValueValidator
from django.db import models
# Create your models here.
-from game.models import Player, Deck
+from game.models import Player, Deck, Hero
class PlayerInQueue(models.Model):
@@ -41,10 +42,40 @@ class PlayerInRoom(models.Model):
class GameState(models.Model):
- room = models.ForeignKey(Room, on_delete=models.CASCADE)
+ room = models.ForeignKey(Room, related_name="states", 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"]
+
+
+class HeroInGame(models.Model):
+ hero = models.ForeignKey(Hero, on_delete=models.CASCADE)
+ player = models.ForeignKey(PlayerInRoom, on_delete=models.CASCADE)
+ room = models.ForeignKey(Room, related_name="heroes", on_delete=models.CASCADE)
+
+ # state on board
+ x = models.IntegerField(
+ blank=False, validators=[MinValueValidator(1), MaxValueValidator(8)]
+ )
+ y = models.IntegerField(
+ blank=False, validators=[MinValueValidator(1), MaxValueValidator(8)]
+ )
+ health = models.IntegerField(blank=False)
+ dead = models.BooleanField(default=False)
+
+ def __str__(self):
+ return f"{self.hero.type} in room {self.room.slug}"
+
+ def save(
+ self, force_insert=False, force_update=False, using=None, update_fields=None
+ ):
+ if not self.health and not self.dead:
+ self.health = self.hero.health
+
+ super().save(force_insert, force_update, using, update_fields)
+
+ class Meta:
+ unique_together = ["x", "y", "hero"]
diff --git a/room/services/game_logic.py b/room/services/game_logic.py
new file mode 100644
index 0000000..ad8a776
--- /dev/null
+++ b/room/services/game_logic.py
@@ -0,0 +1,110 @@
+from asgiref.sync import sync_to_async
+from termcolor import colored
+
+from room.models import HeroInGame, Room, PlayerInRoom
+
+
+def _check_path(f_x: int, f_y: int, x: int, y: int, room: Room, move_type: str):
+ if move_type == "DIAGONAL":
+ return (
+ HeroInGame.objects.filter(
+ room=room, x__range=(f_x, x), y__range=(f_y, y)
+ ).count()
+ == 0
+ )
+ elif move_type == "HORIZONTAL":
+ return HeroInGame.objects.filter(room=room, x=x, y__range=(f_y, y)).count() == 0
+ elif move_type == "VERTICAL":
+ return HeroInGame.objects.filter(room=room, x__range=(f_x, x), y=y).count() == 0
+ return False
+
+
+def _validate_hero_movement(
+ hero_type: str,
+ prev_x: int,
+ prev_y: int,
+ x: int,
+ y: int,
+ room: Room,
+ first: bool = False, # needed for warrior
+):
+ if hero_type == "KING":
+ if abs(x - prev_x) > 1 or abs(y - prev_y) > 1:
+ return False
+ elif hero_type == "WIZARD":
+ if abs(x - prev_x) == abs(y - prev_y):
+ return _check_path(prev_x, prev_y, x, y, room, "DIAGONAL")
+ elif x == prev_x and y != prev_y:
+ return _check_path(prev_x, prev_y, x, y, room, "HORIZONTAL")
+ elif x != prev_x and y == prev_y:
+ return _check_path(prev_x, prev_y, x, y, room, "VERTICAL")
+ return False
+ elif hero_type == "ARCHER":
+ if abs(x - prev_x) == abs(y - prev_y):
+ return _check_path(prev_x, prev_y, x, y, room, "DIAGONAL")
+ return False
+ elif hero_type == "WARRIOR":
+ if first:
+ if x == prev_x and y - prev_y == 1:
+ return True
+ elif abs(x - prev_x) == 1 and y - prev_y == 1:
+ return True
+ else:
+ if x == prev_x and prev_y - y == 1:
+ return True
+ return False
+
+
+def _print_board(room: Room):
+ for y in range(1, 9):
+ for x in range(1, 9):
+ try:
+ hero = HeroInGame.objects.get(x=x, y=y, room=room)
+ if hero.hero.type == "KING":
+ if hero.player.first:
+ print(colored("♔", 'green', attrs=['bold']), end="")
+ else:
+ print(colored("♚", 'red', attrs=['bold']), end="")
+ elif hero.hero.type == "WIZARD":
+ if hero.player.first:
+ print(colored("♕", 'green', attrs=['bold']), end="")
+ else:
+ print(colored("♛", 'red', attrs=['bold']), end="")
+ elif hero.hero.type == "ARCHER":
+ if hero.player.first:
+ print(colored("♗", 'green', attrs=['bold']), end="")
+ else:
+ print(colored("♝", 'red', attrs=['bold']), end="")
+ else:
+ if hero.player.first:
+ print(colored("♙", 'green', attrs=['bold']), end="")
+ else:
+ print(colored("♟", 'red', attrs=['bold']), end="")
+ except HeroInGame.DoesNotExist:
+ print("*", end="")
+ print()
+
+
+@sync_to_async
+def move_handler(
+ prev_x: int, prev_y: int, x: int, y: int, room_slug: str, player: PlayerInRoom
+):
+ room = Room.objects.get(slug=room_slug)
+ _print_board(room) # TODO: Remove in production
+ try:
+ hero = HeroInGame.objects.get(x=prev_x, y=prev_y, room=room, player=player)
+ except HeroInGame.DoesNotExist:
+ return False
+
+ if x == prev_x and y == prev_y:
+ return False
+
+ h_t = hero.hero.type
+
+ if _validate_hero_movement(h_t, prev_x, prev_y, x, y, room, first=player.first):
+ hero.x = x
+ hero.y = y
+ hero.save(update_fields=["x", "y"])
+ return True
+
+ _print_board(room) # TODO: Remove in production
diff --git a/room/services/read_message.py b/room/services/read_message.py
new file mode 100644
index 0000000..e69de29
diff --git a/room/services/room_create.py b/room/services/room_create.py
index 46e4bfa..1ac38a7 100644
--- a/room/services/room_create.py
+++ b/room/services/room_create.py
@@ -2,26 +2,26 @@ 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
+from game.models import Player, Deck
+from room.models import Room, PlayerInRoom, GameState, HeroInGame
-@sync_to_async
-def create_room(
+def sync_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(
+ p1 = PlayerInRoom.objects.create(
player=player_1,
room=room,
score=player_score_1,
@@ -29,17 +29,37 @@ def create_room(
first=first_player == 1,
)
- PlayerInRoom.objects.create(
+ p2 = 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"
- )
+ for p, d_id in [(p1, deck_id_1), (p2, deck_id_2)]:
+ GameState.objects.create(
+ room=room, player=p.player, round=0, message="Game started"
+ )
+ for hero_in_deck in Deck.objects.get(id=d_id).heroes():
+ if p.first:
+ HeroInGame.objects.create(
+ hero=hero_in_deck.hero,
+ player=p,
+ room=room,
+ x=hero_in_deck.x,
+ y=hero_in_deck.y,
+ )
+ else:
+ HeroInGame.objects.create(
+ hero=hero_in_deck.hero,
+ player=p,
+ room=room,
+ x=hero_in_deck.x,
+ y=8 if hero_in_deck.y == 1 else 7,
+ )
return room.slug
+
+
+@sync_to_async
+def create_room(**kwargs):
+ return sync_create_room(**kwargs)
diff --git a/room/tasks.py b/room/tasks.py
new file mode 100644
index 0000000..c41f66a
--- /dev/null
+++ b/room/tasks.py
@@ -0,0 +1,3 @@
+from celery import shared_task
+
+# TODO: add timeout for state