Merge remote-tracking branch 'origin/dev' into stable

This commit is contained in:
Alexander Karpov 2022-08-01 18:26:47 +03:00
commit 470645e460
30 changed files with 681 additions and 308 deletions

2
.gitignore vendored
View File

@ -1,6 +1,6 @@
.idea
static/
media/
media/uploads/
# Byte-compiled / optimized / DLL files
__pycache__/

View File

@ -1,17 +1,30 @@
# chess_rpg_backend
Backend for chess rpg game
DEV branch for backend for chess rpg game
<hr>
##### dev server for up to date endpoints(web socket not provided)
- https://dev.akarpov.ru
<hr>
### 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
```
### prod run
```shell
$ daphne -b 0.0.0.0 -p 8000 chess_backend.asgi:application
```
### Описание команд сокетов
```python
# подключиние к очереди(ws://room/)

View File

@ -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",)

21
chess_backend/celery.py Normal file
View File

@ -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}")

View File

@ -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'

View File

@ -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<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)

48
common/debug.py Normal file
View File

@ -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

31
docker-compose.yaml Normal file
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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'),
),
]

View File

@ -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):

View File

@ -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)

11
game/signals.py Normal file
View File

@ -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)

BIN
media/dump_data/dump.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

View File

@ -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"
}
}
]

44
nginx.conf Normal file
View File

@ -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;
}

View File

@ -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

8
requirements/base.txt Normal file
View File

@ -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

4
requirements/dev.txt Normal file
View File

@ -0,0 +1,4 @@
-r base.txt
ipython==8.4.0
termcolor==1.1.0

3
requirements/prod.txt Normal file
View File

@ -0,0 +1,3 @@
-r base.txt
daphne==3.0.2

View File

@ -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(

View File

@ -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"]

110
room/services/game_logic.py Normal file
View File

@ -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

View File

View File

@ -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,
)
for p, d_id in [(p1, deck_id_1), (p2, deck_id_2)]:
GameState.objects.create(
room=room, player=player_1, round=0, message="Game started"
room=room, player=p.player, round=0, message="Game started"
)
GameState.objects.create(
room=room, player=player_2, 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)

3
room/tasks.py Normal file
View File

@ -0,0 +1,3 @@
from celery import shared_task
# TODO: add timeout for state