Skip to content

Commit

Permalink
Merge pull request #258 from TheSecretOrganization/54-implement-tourn…
Browse files Browse the repository at this point in the history
…ament

54 implement tournament
  • Loading branch information
Neffi42 authored Oct 17, 2024
2 parents e275ea0 + 8391db1 commit 7684601
Show file tree
Hide file tree
Showing 19 changed files with 886 additions and 98 deletions.
1 change: 1 addition & 0 deletions django/src/core/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,5 @@
path('friends/', include('friends.urls')),
path('auth/', include('ft_auth.urls')),
path('pages/', include('pages.urls')),
path('games/', include('games.urls')),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
18 changes: 13 additions & 5 deletions django/src/games/admin.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
from django.contrib import admin
from .models import Pong
from .models import Pong, PongTournament


@admin.register(Pong)
class PongAdmin(admin.ModelAdmin):
list_display = ('user1', 'user2', 'score1', 'score2', 'created_at')
search_fields = ('user1__username', 'user2__username')
list_filter = ('created_at',)
class PongGameAdmin(admin.ModelAdmin):
list_display = ("user1", "user2", "score1", "score2", "created_at", "uuid")
search_fields = ("user1__username", "user2__username")
list_filter = ("created_at",)


@admin.register(PongTournament)
class PongTournamentAdmin(admin.ModelAdmin):
list_display = ("name", "winner", "created_at", "updated_at")
search_fields = ("name", "winner__username")
list_filter = ("created_at", "updated_at", "winner")
16 changes: 15 additions & 1 deletion django/src/games/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by Django 5.1 on 2024-09-23 14:42
# Generated by Django 5.1 on 2024-10-16 15:39

import django.db.models.deletion
from django.conf import settings
Expand All @@ -20,9 +20,23 @@ class Migration(migrations.Migration):
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('score1', models.PositiveSmallIntegerField()),
('score2', models.PositiveSmallIntegerField()),
('uuid', models.CharField(max_length=255)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('user1', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='user1', to=settings.AUTH_USER_MODEL)),
('user2', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='user2', to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='PongTournament',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, unique=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('games', models.ManyToManyField(related_name='tournaments', to='games.pong')),
('participants', models.ManyToManyField(related_name='tournaments', to=settings.AUTH_USER_MODEL)),
('winner', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='winner', to=settings.AUTH_USER_MODEL)),
],
),
]
68 changes: 65 additions & 3 deletions django/src/games/models.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,79 @@
from django.db import models
from ft_auth.models import User
from django.contrib.auth import get_user_model


class Pong(models.Model):
user1 = models.ForeignKey(
User, on_delete=models.SET_NULL, null=True, related_name="user1"
get_user_model(),
on_delete=models.SET_NULL,
null=True,
related_name="user1",
)
user2 = models.ForeignKey(
User, on_delete=models.SET_NULL, null=True, related_name="user2"
get_user_model(),
on_delete=models.SET_NULL,
null=True,
related_name="user2",
)
score1 = models.PositiveSmallIntegerField()
score2 = models.PositiveSmallIntegerField()
uuid = models.CharField(max_length=255)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)

def __str__(self):
return f"{self.user1.username} vs {self.user2.username} - {self.score1}:{self.score2}"

def serialize(self):
return {
"user1": (
{"id": self.user1.id, "username": self.user1.username}
if self.user1
else None
),
"user2": (
{"id": self.user2.id, "username": self.user2.username}
if self.user2
else None
),
"score1": self.score1,
"score2": self.score2,
"uuid": self.uuid,
"created_at": self.created_at.isoformat(),
"updated_at": self.updated_at.isoformat(),
}

class PongTournament(models.Model):
name = models.CharField(max_length=255, unique=True)
winner = models.ForeignKey(
get_user_model(),
on_delete=models.SET_NULL,
null=True,
related_name="winner",
)
participants = models.ManyToManyField(
get_user_model(), related_name="tournaments"
)
games = models.ManyToManyField(Pong, related_name="tournaments")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)

def __str__(self):
return f"Tournament: {self.name} - Winner: {self.winner}"

def serialize(self):
return {
"name": self.name,
"winner": (
{"id": self.winner.id, "username": self.winner.username}
if self.winner
else None
),
"participants": [
{"id": participant.id, "username": participant.username}
for participant in self.participants.all()
],
"games": [game.serialize() for game in self.games.all()],
"created_at": self.created_at.isoformat(),
"updated_at": self.updated_at.isoformat(),
}
131 changes: 94 additions & 37 deletions django/src/games/pong.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@

logger = logging.getLogger(__name__)

class Consumer(AsyncWebsocketConsumer):

class Pong(AsyncWebsocketConsumer):
win_goal = 5

async def connect(self):
Expand All @@ -32,23 +33,35 @@ async def connect(self):
await self.close()
return

self.valid = True
await self.accept()
await self.channel_layer.group_add(self.room_id, self.channel_name)
logger.info(f"User {self.user.id} successfully connected to room {self.room_id}.")
self.valid = True
logger.info(
f"User {self.user.id} successfully connected to room {self.room_id}."
)
await self.send_message("group", "game_join", {"user": self.user.id})
await self.send_message("client", "game_pad", {"game_pad": self.pad_n})
if self.tournament_name != "0":
await self.send_message(
"client",
"tournament_name",
{"tournament_name": self.tournament_name},
)

async def initialize_game(self):
query_params = parse_qs(self.scope["query_string"].decode())
self.room_id = self.check_missing_param(query_params, "room_id")
self.mode = self.check_missing_param(query_params, "mode")
player_needed = self.check_missing_param(query_params, "player_needed")
self.room_id = self.check_missing_param(query_params, "room_id")
self.host = self.check_missing_param(query_params, "host") == "True"
self.tournament_name = self.check_missing_param(
query_params, "tournament_name"
)
self.host = await self.redis.get(self.room_id) == None
self.pad_n = "pad_1" if self.host else "pad_2"
players = await self.redis.lrange(f'pong_{self.room_id}_id', 0, -1)
players = await self.redis.lrange(f"pong_{self.room_id}_id", 0, -1)

if self.host:
await self.redis.set(self.room_id, 1)
self.info = self.Info(
creator=self.user.id,
room_id=self.room_id,
Expand All @@ -57,13 +70,14 @@ async def initialize_game(self):
self.ball = self.Ball()
self.pad_1 = self.Pad(True)
self.pad_2 = self.Pad(False)
logger.info(f"User {self.user.id} is the host for room {self.room_id}.")
await self.redis.set(self.room_id, 1)
logger.info(
f"User {self.user.id} is the host for room {self.room_id}."
)
else:
if not await self.redis.get(self.room_id):
raise ConnectionRefusedError("Invalid room")
if str(self.user.id).encode("utf-8") in players:
raise ConnectionRefusedError("User already connected to the room")
raise ConnectionRefusedError(
"User already connected to the room"
)
if len(players) == 2:
raise ConnectionRefusedError("Room is full")

Expand All @@ -72,21 +86,33 @@ async def initialize_game(self):
async def disconnect(self, close_code):
self.connected = False

if self.host:
players = await self.redis.lrange(f'pong_{self.room_id}_id', 0, -1)
if hasattr(self, "host") and self.host:
await self.channel_layer.group_send(
f"{self.room_id}_watcher",
{"type": "watcher_stop", "id": self.room_id},
)
players = await self.redis.lrange(f"pong_{self.room_id}_id", 0, -1)

if hasattr(self, "game_task"):
self.game_task.cancel()

await self.redis.delete(self.room_id)
await self.redis.delete(f'pong_{self.room_id}_id')
await self.redis.delete(f"pong_{self.room_id}_id")
await self.redis.delete(f"{self.room_id}_watcher")
logger.info(f"Room {self.room_id} has been closed by the host.")

if self.mode == "online" and len(players) == 2:
if not hasattr(self, "winner"):
self.punish_coward(self.info.creator)
await self.save_pong_to_db(self.winner)

if hasattr(self, "valid"):
await self.send_message("group", "game_stop", {"user": self.user.id})
await self.channel_layer.group_discard(self.room_id, self.channel_name)
await self.send_message(
"group", "game_stop", {"user": self.user.id}
)
await self.channel_layer.group_discard(
self.room_id, self.channel_name
)

await self.redis.close()
logger.info(f"User {self.scope['user'].id} has disconnected.")
Expand All @@ -101,7 +127,9 @@ async def receive(self, text_data):
if "content" not in data:
raise AttributeError("Missing 'content'")

logger.debug(f"Received '{msg_type}' message from user {self.scope['user'].id}.")
logger.debug(
f"Received '{msg_type}' message from user {self.scope['user'].id}."
)
match msg_type:
case "game_stop":
await self.close()
Expand All @@ -112,7 +140,9 @@ async def receive(self, text_data):
case _:
raise ValueError("Unknown 'type' in data")
except Exception as e:
logger.warning(f"Invalid message received from user {self.scope['user'].id}: {str(e)}")
logger.warning(
f"Invalid message received from user {self.scope['user'].id}: {str(e)}"
)
await self.send_error("Invalid message")

async def game_join(self, event):
Expand Down Expand Up @@ -142,9 +172,13 @@ async def game_move(self, event):
raise AttributeError("Missing 'pad_n'")
if event["content"].get("direction") == None:
raise AttributeError("Missing 'direction'")
await self.move_pad(event["content"]["pad_n"], event["content"]["direction"])
await self.move_pad(
event["content"]["pad_n"], event["content"]["direction"]
)
except AssertionError as e:
logger.warning(f"Invalid game_move received in {self.room_id}: {str(e)}")
logger.warning(
f"Invalid game_move received in {self.room_id}: {str(e)}"
)

async def game_stop(self, event):
if not self.connected:
Expand All @@ -171,30 +205,46 @@ async def loop(self):
while True:
try:
self.ball.move()
self.handle_collisions()
if self.info.score[0] == self.win_goal or self.info.score[1] == self.win_goal:
await self.send_message("group", "game_stop", {"winner": self.get_winner()})
await self.handle_collisions()
if (
self.info.score[0] == self.win_goal
or self.info.score[1] == self.win_goal
):
await self.send_message(
"group", "game_stop", {"winner": self.get_winner()}
)
break
await self.send_game_state()
await asyncio.sleep(fps)
except Exception as e:
logger.error(f"Unexpected error in the loop from game {self.room_id}: {str(e)}")
logger.error(
f"Unexpected error in the loop from game {self.room_id}: {str(e)}"
)
break

def get_winner(self):
if self.mode == "online":
return self.info.players[0] if self.info.score[0] > self.info.score[1] else self.info.players[1]
return (
self.info.players[0]
if self.info.score[0] > self.info.score[1]
else self.info.players[1]
)
else:
return "Pad 1" if self.info.score[0] > self.info.score[1] else "Pad 2"
return (
"Pad 1" if self.info.score[0] > self.info.score[1] else "Pad 2"
)

async def move_pad(self, pad_number, direction):
pad = self.pad_1 if pad_number == "pad_1" else self.pad_2
step = pad.step if direction == "down" else -pad.step
pad.y = min(max(pad.y + step, 0), 1 - pad.height)
await self.send_game_state()

def handle_collisions(self):
if self.ball.y - self.ball.radius / 2 <= 0 or self.ball.y + self.ball.radius / 2 >= 1:
async def handle_collisions(self):
if (
self.ball.y - self.ball.radius / 2 <= 0
or self.ball.y + self.ball.radius / 2 >= 1
):
self.ball.revert_velocity(1)
elif self.check_pad_collision():
self.ball.revert_velocity(0)
Expand All @@ -203,9 +253,9 @@ def handle_collisions(self):
else:
self.ball.x = 1 - self.pad_2.width - self.ball.radius / 2
elif self.ball.x + self.ball.radius / 2 <= self.pad_1.width:
self.score_point(1)
await self.score_point(1)
elif self.ball.x - self.ball.radius / 2 >= 1 - self.pad_2.width:
self.score_point(0)
await self.score_point(0)

def check_pad_collision(self):
if self.ball.x - self.ball.radius <= self.pad_1.width:
Expand All @@ -216,7 +266,7 @@ def check_pad_collision(self):
return True
return False

def score_point(self, winner: int):
async def score_point(self, winner: int):
self.info.score[winner] += 1
self.reset_game()

Expand Down Expand Up @@ -248,19 +298,26 @@ async def send_game_state(self):
)

async def save_pong_to_db(self, winner):
Pong = apps.get_model('games', 'Pong')
pong = apps.get_model("games", "Pong")
try:
await sync_to_async(Pong.objects.create)(
user1=await sync_to_async(get_user_model().objects.get)(id=self.info.players[0]),
user2=await sync_to_async(get_user_model().objects.get)(id=self.info.players[1]),
await sync_to_async(pong.objects.create)(
user1=await sync_to_async(get_user_model().objects.get)(
id=self.info.players[0]
),
user2=await sync_to_async(get_user_model().objects.get)(
id=self.info.players[1]
),
score1=self.info.score[0],
score2=self.info.score[1]
score2=self.info.score[1],
uuid=self.room_id,
)
logger.debug(f"Game {self.room_id} saved to db.")
except Exception as e:
logger.error(f"Could not save game {self.room_id} to db: {str(e)}")

def check_missing_param(self, query_params: dict[Any, List], param_name: str):
def check_missing_param(
self, query_params: dict[Any, List], param_name: str
):
param = query_params.get(param_name, [None])[0]
if param == None:
raise AttributeError(f"Missing query parameter: {param_name}")
Expand Down
Loading

0 comments on commit 7684601

Please sign in to comment.