Compare commits

...

68 Commits

Author SHA1 Message Date
603a169c39 Windows folders :( 2023-05-16 21:33:25 +02:00
a9db65f8b6 cleanup 2023-05-16 21:12:22 +02:00
f1ce1bff30 actually fixed readme 2023-05-16 21:11:51 +02:00
7ea995f1ec fixed readme 2023-05-16 21:11:29 +02:00
cee70ab57d Added timouts and ready to ship ver 1.1.1 :party: 2023-05-16 21:10:07 +02:00
d66becf13a Merge branch 'main' of dumtaxat-git:boyan_k/customm 2023-05-16 17:59:51 +03:00
e61029fdf5 Small change in server 2023-05-16 17:59:48 +03:00
204e3aff28 amogus 2023-05-11 06:25:38 +02:00
e09c2ebd5b Automatic team movement kinda works, have to test 2023-05-11 05:16:41 +02:00
fdf8739fb8 Done enough ⚰️ 2023-05-09 05:33:19 +02:00
308534b711 Current 2023-05-09 05:05:22 +02:00
f7d07d2ed6 Class viewset 2023-05-09 05:04:27 +02:00
e784ea0f4d COME ON 2023-05-09 05:00:00 +02:00
489a06504f Last try 2023-05-09 04:53:50 +02:00
d3772eae5e back to all 2023-05-09 04:50:02 +02:00
6ab4083f54 trying to access data 2023-05-09 04:49:00 +02:00
978f7535f3 sdsd 2023-05-09 04:46:01 +02:00
e652c25709 testing 2023-05-09 04:42:52 +02:00
5531063035 primary keyyy 2023-05-09 04:42:13 +02:00
233fc7babf sdasdsd 2023-05-09 04:34:57 +02:00
666d778c3d drfwdrwer 2023-05-09 04:34:19 +02:00
3c897ff9a6 LOL? 2023-05-09 04:32:12 +02:00
b00fc8ac0c PLS 2023-05-09 04:28:33 +02:00
2297205327 Pls 2023-05-09 04:27:34 +02:00
1e3f1c0e5d 4 AM fixes 2023-05-09 04:23:04 +02:00
d334d7b5bd Some fixes 2023-05-09 04:20:57 +02:00
465132d940 Fixed some import errors 2023-05-09 04:11:19 +02:00
9b856fe289 formatting 2023-05-09 04:05:41 +02:00
df3a601fae lol 2023-05-09 04:04:50 +02:00
43728f8807 sdsds 2023-05-09 04:03:48 +02:00
697d3af498 Im getting the hang of copilot 2023-05-09 04:02:38 +02:00
7d1d710740 Small mistake 2023-05-09 03:57:35 +02:00
f7034de2fd Holy shit almost version 1.1.1 2023-05-09 03:56:46 +02:00
0b1117fae2 kfhrekjtrentkjrentkjernewkjret 2023-05-08 22:29:25 +02:00
6bdfe32e87 NULL 2023-05-08 22:27:47 +02:00
a56fc45b60 NULL 2023-05-08 22:25:56 +02:00
750faadf1b PLS GOD BE GRACIOUS 2023-05-08 22:24:36 +02:00
f6b5791916 Minor fixes 2023-05-08 17:48:08 +02:00
b0bc592fc4 Merge branch 'main' of dumtaxat-git:boyan_k/customm 2023-05-08 00:55:31 +02:00
c6728a401b Updated flowcharts 2023-05-08 00:54:49 +02:00
b7ecbe2630 Merge branch 'main' of https://git.confest.im/boyan_k/custoMM 2023-05-07 23:31:25 +02:00
3b90aa1d49 Tidied 2023-05-07 23:31:22 +02:00
e1b575ecc8 Delet 2023-05-07 17:09:20 +02:00
4bbcb794c6 Removed vscode.json 2023-05-07 16:18:54 +02:00
85a19d7eb0 Changed the lib for League scraping, old one bad. 2023-05-07 16:18:37 +02:00
b19b21dcf3 wirks 2023-05-07 14:40:51 +02:00
b430daa5ee Some aesthetic fixes 2023-05-06 23:11:02 +02:00
685319042b Async consistency fix 2023-05-06 23:09:10 +02:00
a342c1b01d sdaasdas 2023-05-06 23:05:32 +02:00
aab5c17527 Small mistake 2023-05-06 23:03:33 +02:00
ce0aba8b4d kekw 2023-05-06 22:58:58 +02:00
37eab5bc6f Reformed the entire repo, might revert if no work. 2023-05-06 22:55:40 +02:00
3fd5837a27 added new diagram 2023-05-06 02:22:32 +02:00
85e9ed46fc added new diagram 2023-05-06 02:20:23 +02:00
5d71faee8e added new diagram 2023-05-06 02:19:44 +02:00
1046c10d82 added new diagram 2023-05-06 02:10:29 +02:00
ea4f3ecf8f Merge branch 'main' of https://git.confest.im/boyan_k/custoMM 2023-05-06 02:08:07 +02:00
3514b3a32e Fixed params mistake in discord bot 2023-05-06 02:58:50 +02:00
5402b16f44 Fixes in discord bot, I don't want to say this, but it might work 2023-05-06 02:16:27 +02:00
8391231390 added new diagram 2023-05-06 02:07:53 +02:00
7ae775276a Added shitty check for username validty in discord bot 2023-05-06 01:37:42 +02:00
eb89cbb240 Merge branch 'main' of dumtaxat-git:/boyan_k/custoMM 2023-05-06 01:35:17 +02:00
44165c18a5 Post-merge cleanup 2023-05-06 01:35:06 +02:00
f30e7c7433 merged 2023-05-06 01:32:32 +02:00
d46c835897 Updated gitignore 2023-05-06 01:31:49 +02:00
c9b66499f7 Small tweaks to the discord bot 2023-05-06 01:31:31 +02:00
bcd9206340 Small fix in server 2023-05-06 01:29:25 +02:00
3d57521581 Syntax error on client 2023-05-05 23:33:58 +02:00
53 changed files with 852 additions and 629 deletions

19
.gitignore vendored
View File

@ -1,7 +1,22 @@
config*
# Config
config.ini
# Environment
.env/
# Database
db.sqlite3
# Build garbage
__pycache__/
migrations/
build/
dist/
dist/
main.spec
# VSCode garbage
.vscode/
# LCU_connector leftover
riotgames.pem
test.py

View File

@ -1,32 +1,32 @@
# custoMM
<p align="center">
<img src="images/logo.png"/>
<img src="images/logo.png"/><br>
<img alt="Python => 3.11" src="https://img.shields.io/badge/Python-%3E%3D3.11-green)"/>
<img alt="Version 1.1.1" src="https://img.shields.io/badge/Version-1.1.1-blueviolet" />
</p>
## What is this?
This is a project inspired by a heavy loss while playing customs in league of legends. The idea is to create fair teams, without taking rank into account(cuz that's boring and we're antiestablishmentarians 🗿♿🥶).
## Mechanism
![Mechanism](images/Mechanism.png)
CORRECTION, the client will be just a simple exe file that one runs once they want/finish a game. Lol.
![Mechanism](images/Mechanism.png)
## Registration
A user registers like so:
1. Opens the client of custoMM
2. Tells the designated bot that they want to register with their IGN(the bot assumes EUNE unless changed in the code) (e.g. !register leomesipicha)
3. The client prompts the user if they really want to proceed, showing the discord username which initiated the registration. When a username is registered, it cannot be registered again(do not worry, even if you change your name in either LoL or Discord you will still be registered). You can still lend out your account, however, using the [guest mode](#guest-mode).
Here's a flowchart explaining the technical stuff about this process:
![Registration](images/Registration.png)
## Guest mode
[Currently work in progress](https://git.confest.im/boyan_k/custoMM/issues/5#issue-17)
If you want to use somebody else's account while playing customs, you can temporarily claim it, which will make the actual owner not gain MMR from the current game. That's about it. You, as the temporary player, do receive some MMR, however it is reduced by 50%, bc god damn get your own account wtf.

View File

@ -1,24 +0,0 @@
# Client
Heavily W.I.P. at the moment.
## Config
Create a file called `config.py` and fill it with the following values(relevant to you):
```
SITE_URL = "your-server.com"
```
## Releases
Check out the releases for pre-compiled binaries of the client and server.
## TODOs
1. [x] Make the server and client talk to each other
* [x] Based on MMR
* [x] Based on cached IDs

View File

@ -1,203 +0,0 @@
from lcu_driver import Connector
import requests
# Edit config.json when running for the first time
import asyncio
import time, sys
import configparser
# Config section
config = configparser.ConfigParser()
config.read("../config.ini")
URL = config["DEFAULT"]["URL"]
# Test connection to server
try:
test = requests.get(URL).json()
except Exception:
# NEVER DO THIS
# although, what could go wrong...
print("Server seems to be down, please contact admin if it keeps doing this")
sys.exit()
# needed for windows
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
# Init connection
connector = Connector()
class WhatTheFuckDidYouDo(Exception):
# We raise this when we don't know what the fuck happened
def __init__(self, message="Congrats, you broke this program, please explain your steps as an issue at https://github.com/confestim/custoMM"):
self.message = message
super().__init__(self.message)
def calculate_kda(kills:int, assists:int, deaths:int):
"""
Calculates kill, death, assist ratio
Input: kills, assists, deaths
Output: KDA ratio
"""
if deaths == 0:
deaths = 1
return round((kills+assists)/deaths, 3)
async def parse_history(connection, history:dict, old_ids:list) -> list:
"""
Parses current player's history
Input: Logged in player's match history
Output: New data about unaccounted for custom games, ready to send to server
"""
parsed_matches = []
new = 0
for i in history["games"]["games"]:
if i["gameType"] == "CUSTOM_GAME" and str(i["gameId"]) not in old_ids and not i["gameMode"] == "PRACTICETOOL":
new += 1
match = await connection.request('get', f'/lol-match-history/v1/games/{i["gameId"]}')
match = await match.json()
print(match)
parsed_match = {
"game_id": match["gameId"],
"participants": {
"t1": {
"won": True if match["teams"][0]["win"] == "Won" else False,
"summoners": []
},
"t2": {
"won": True if match["teams"][1]["win"] == "Won" else False,
"summoners": []
}
}
}
# Sloppy solution, find fix.
print("Extracting data...")
for player in range(10):
current_player = match["participants"][player]["stats"]
kills = current_player["kills"]
assists = current_player["assists"]
deaths = current_player["deaths"]
if player <= 5:
parsed_match["participants"]["t1"]["summoners"].append({"name":match["participantIdentities"][player]["player"]["summonerName"], "kda": calculate_kda(kills, assists, deaths)})
else:
parsed_match["participants"]["t2"]["summoners"].append({"name":match["participantIdentities"][player]["player"]["summonerName"], "kda": calculate_kda(kills, assists, deaths)})
parsed_matches.append(parsed_match)
if not new:
print("Already up to date, thanks. Program will now close.")
time.sleep(3)
sys.exit()
return parsed_matches
# TODO: Format this list in the form of [{gid: GAME-ID, puuid:puuid}]
# Get current summoner
@connector.ready
async def connect(connection):
"""Data scraper for the league client"""
# TODO: Check if league is running
# Summoner
summoner = await connection.request('get', '/lol-summoner/v1/current-summoner')
summoner = await summoner.json()
# Check if account is claimed
try:
claimed = requests.get(URL+ f"players/?search={summoner['displayName']}").json()[0]
except IndexError:
print("User does not exist, register through discord please.")
print("Program will now close")
time.sleep(5)
sys.exit()
# Case 3: It belongs to nobody and has yet to be claimed.
if not claimed["discord"]:
print("This account has not yet been claimed. Please claim it (if its yours) by typing in !claim ACCOUNTNAME to the bot in Discord and running this program again.")
print(claimed)
for i in range(10):
time.sleep(.5)
sys.stdout.write(".")
sys.stdout.flush()
sys.exit()
if claimed:
# Case 1: It belongs to somebody
if claimed['lol_id'] and claimed['lol']:
print(f"Welcome, {claimed['discord']}. Thank you for contributing to custoMM!")
print(claimed)
print("If this is not you, contact admin.")
# Notify them (if that is the case) that we will do nothing about their new name (slight TODO).
if claimed['lol'] != summoner['displayName']:
print("Ah, you have changed your league handle. Oh well. I will still refer to you with your old one, as this has not been handled properly yet.")
# Case 2: Registration has begun, but hasn't finished
elif claimed['lol'] and not claimed['lol_id']:
prompt = input(f"{claimed['discord']} is trying to claim this account(which you obviously own). Do you want to do that? [y/N] ")
if prompt == ("y" or "Y"):
# TODO: Update api entry
account = requests.put(f"{URL}/players/{claimed['lol']}/", data={
"lol": summoner["displayName"],
"lol_id": summoner["puuid"],
"discord_id":claimed["discord_id"],
"discord":claimed["discord"]
})
if account.status_code == 200:
print(f"Alright, the account is now yours, {claimed['discord']}.")
else:
print("Something went wrong when claiming your account...")
else:
#TODO: Delete api entry
requests.delete(f"{URL}/players/{claimed['discord_id']}")
print("Okay, deleting the attempt.")
sys.exit()
# All cases are covered, everything else will be considered a bug.
else:
raise WhatTheFuckDidYouDo()
time.sleep(5)
# Match History
match_history = await connection.request('get', '/lol-match-history/v1/products/lol/current-summoner/matches?endIndex=99')
match_history = await match_history.json()
# Stage old ids in order for them to be parsed
old_ids = requests.get(f"{URL}/games/").json()
old_ids = [x["game_id"] for x in old_ids]
# TODO: Optimize the process of acquisition of new matches
games = await parse_history(connection, match_history, old_ids)
# Post the new games to your server(change in config.json)
for i in games:
req = requests.post(f"{URL}/games/", json=i)
print(req.content)
if req.status_code == 500:
print("Serverside error! Contact maintainer!")
sys.exit()
print("We have added " + str(len(games)) + " games that were unaccounted for to the db.")
@connector.close
async def disconnect(connection):
"""Disconnects from the league client"""
print('Harvesting is over!')
time.sleep(5)
# Begin
connector.start()

View File

@ -1,44 +0,0 @@
# -*- mode: python ; coding: utf-8 -*-
block_cipher = None
a = Analysis(
['dataScraper.py'],
pathex=[],
binaries=[],
datas=[],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='dataScraper',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)

View File

@ -1,13 +0,0 @@
[DEFAULT]
; Your URL here (with no slash at the end)
URL = https://ongod.fr
[DISCORD]
; Your token here
TOKEN = RANDOMAHHTOKEN39048209483294028
; Developer settings in discord have to be set to on
; Hover over server -> right click -> copy ID
GUILD_ID = j4543koj534kl5h4j3kl5jh
; Voice channel IDs for the channels you want your players to be split into
TEAM_1 = 1054534012963659826
TEAM_2 = 1054534093641089084

View File

@ -1,192 +0,0 @@
import asyncio
import discord
from discord.ext import commands
import random
import requests
import configparser
config = configparser.ConfigParser()
config.read("config.ini")
URL = config["DEFAULT"]["URL"]
intents = discord.Intents.default()
intents.message_content = True
bot = commands.Bot(intents=intents, command_prefix='!')
@bot.event
async def on_ready():
print(f'We have logged in as {bot.user}')
game = discord.Game("Customki - !randomize")
await bot.change_presence(activity=game, status=discord.Status.dnd)
@bot.command()
async def randomize(ctx,
team_1 = config['DISCORD']['TEAM_1'],
team_2 = config['DISCORD']['TEAM_2']):
"""Randomizes 10 people into 2 teams: !randomize"""
# Change these to designated team voice channels in config.ini
team_1 = bot.get_channel(team_1)
team_2 = bot.get_channel(team_2)
trying_prompt = await ctx.send('Trying to randomize!')
# Fetching user channel
try:
channel = bot.get_channel(ctx.author.voice.channel.id)
except AttributeError:
await trying_prompt.delete()
return await ctx.send("You think you're cool or something? Get in a channel first.")
players = channel.members
if len(players) < 10:
await trying_prompt.delete()
await ctx.send(f"Get more people in ({len(players)}/10)")
return
random.shuffle(players)
print(players)
# Embedding
one_em = discord.Embed(title=f"Team 1", colour=discord.Colour(0x8c0303))
two_em = discord.Embed(title=f"Team 2", colour=discord.Colour(0x0B5394))
# Splitting logic
modulus = len(players) % 10
if modulus != 0:
# If total players not divisible by 10, can't split
return await ctx.send("Can't split(remove bots and non-players)")
# Else, start at bottom and at top and move to middle
for i in range(0, int(len(players)/2)):
await players[i].move_to(team_1)
one_em.add_field(name=players[i].name, value=players[i].id)
await players[len(players) - (i + 1)].move_to(team_2)
two_em.add_field(name=players[len(
players) - (i + 1)].name, value=players[len(players) - (i + 1)].id)
await ctx.send(embed=one_em)
await ctx.send(embed=two_em)
await trying_prompt.delete()
return
@bot.command()
async def begin_game(ctx,
team_1 = config['DISCORD']['TEAM_1'],
team_2 = config['DISCORD']['TEAM_2']):
"""Tries to start a fair game: !begin_game"""
# Change these to designated team voice channels
team_1 = bot.get_channel(team_1)
team_2 = bot.get_channel(team_2)
trying_prompt = await ctx.send('Trying to start fair game!')
# Fetching user channel
try:
channel = bot.get_channel(ctx.author.voice.channel.id)
except AttributeError:
await trying_prompt.delete()
return await ctx.send("You think you're cool or something? Get in a channel first.")
players = channel.members
if len(players) < 10:
await trying_prompt.delete()
await ctx.send(f"Get more people in ({len(players)}/10)")
return
valid_players = []
# This does not support multiple accounts, refer to issue #7
# Initial check for existing players
for i in players:
print(i.name)
player = requests.get(f"{URL}/players/?search={i.id}").json()
print(player)
if not player:
await ctx.send(f"<@{i.id}> has no league account associated with their name. Please register in order to calculate MMR more accurately.")
else:
valid_players.append(player[0]["lol"])
# If not enough players, return
if len(valid_players) < 10:
return await ctx.send("Couldn't create fair game. Whoever isn't registered, please do.")
# Getting the players
query_string = "&".join(["".format(player) for player in valid_players])
teams = requests.get(
f"{URL}/game?{query_string}")
# Second check for existing players
# Also, funny status code
if teams.status_code == 451:
for i in teams.content:
await ctx.send(f"{i} has not been found by the API. Please register in order to calculate MMR more accurately.")
return
teams = teams.json()
# Embedding
one_em = discord.Embed(title=f"Team 1", colour=discord.Colour(0x8c0303))
two_em = discord.Embed(title=f"Team 2", colour=discord.Colour(0x0B5394))
# TODO: DEBUG, remove
print(teams)
# Splitting logic
for i in teams[0]:
player = await bot.get_user(i["discord_id"])
await player.move_to(team_1)
one_em.add_field(name=player.name, value=i["lol"])
for i in teams[1]:
player = await bot.get_user(i["discord_id"])
await player.move_to(team_2)
two_em.add_field(name=player.name, value=i["lol"])
# Sending embeds and cleanup
await ctx.send(embed=one_em)
await ctx.send(embed=two_em)
await trying_prompt.delete()
return
@bot.command()
async def register(ctx, *args):
"""Registers a user to the database: !register <league_name>"""
name = " ".join(args)
print(name)
league_name = requests.get(f"{URL}/players/{name}").json()
try:
if not league_name["detail"] == "Not found.":
return await ctx.send("Someone already claimed this account")
except KeyError:
if league_name["discord_id"]:
return await ctx.send(f"{league_name['discord']} has claimed this account.")
claim_account = requests.post(f"{URL}/players/", data={
"discord": ctx.author.name,
"discord_id": ctx.author.id,
"lol": name
})
if not claim_account.json().get('discord_id') and claim_account.json().get("lol"):
# In case that account doesn't exist at all
claim_account = requests.put(f"{URL}/players/{name}/", data={
"discord": ctx.author.name,
"discord_id": ctx.author.id,
"lol": name
})
print(claim_account.content)
# TODO: In case the account exists, but not yet claimed
print(claim_account.status_code)
if claim_account.status_code == 201 or claim_account.status_code == 200:
return await ctx.send("Success, now approve from client")
return await ctx.send("Something went wrong...")
# Change to your bot token in config.ini
bot.run(config['DISCORD']['TOKEN'])

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 21 KiB

BIN
images/bob.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View File

@ -1,23 +0,0 @@
# Generated by Django 4.0.3 on 2023-03-15 22:50
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Player',
fields=[
('username', models.CharField(max_length=60)),
('ign', models.CharField(max_length=60)),
('lol_id', models.CharField(max_length=60)),
('discord_id', models.IntegerField(primary_key=True, serialize=False)),
],
),
]

View File

@ -1,23 +0,0 @@
# Generated by Django 4.0.3 on 2023-03-15 22:52
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('server_client_api', '0001_initial'),
]
operations = [
migrations.RenameField(
model_name='player',
old_name='ign',
new_name='discord',
),
migrations.RenameField(
model_name='player',
old_name='username',
new_name='lol',
),
]

View File

@ -1,19 +0,0 @@
# Generated by Django 4.0.3 on 2023-03-15 23:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('server_client_api', '0002_rename_ign_player_discord_rename_username_player_lol'),
]
operations = [
migrations.AddField(
model_name='player',
name='mmr',
field=models.IntegerField(default=None),
preserve_default=False,
),
]

View File

@ -1,28 +0,0 @@
# Generated by Django 4.1.7 on 2023-03-16 00:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('server_client_api', '0003_player_mmr'),
]
operations = [
migrations.AlterField(
model_name='player',
name='lol',
field=models.CharField(blank=True, max_length=60, null=True),
),
migrations.AlterField(
model_name='player',
name='lol_id',
field=models.CharField(blank=True, max_length=60, null=True),
),
migrations.AlterField(
model_name='player',
name='mmr',
field=models.IntegerField(default=0),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 4.1.7 on 2023-03-16 00:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('server_client_api', '0004_alter_player_lol_alter_player_lol_id_and_more'),
]
operations = [
migrations.AlterField(
model_name='player',
name='mmr',
field=models.IntegerField(default=0, editable=False),
),
]

View File

@ -1,25 +0,0 @@
from rest_framework import serializers
from .models import Player, Game
class PlayerSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Player
fields = ('discord', 'lol', 'lol_id', 'discord_id')
def update(self, instance, validated_data):
instance.discord_id = validated_data.get(
'discord_id', instance.discord_id)
instance.discord = validated_data.get('discord', instance.discord)
instance.lol = validated_data.get('lol', instance.lol)
instance.lol_id = validated_data.get('lol_id', instance.lol_id)
instance.save()
return instance
class GameSerializer(serializers.ModelSerializer):
class Meta:
model = Game
fields = ("game_id",)

37
src/client/README.md Normal file
View File

@ -0,0 +1,37 @@
# Client
Heavily W.I.P. at the moment.
## Config
Create a file called `config.py` and fill it with the following values(relevant to you):
```
SITE_URL = "your-server.com"
```
## Releases
Check out the releases for pre-compiled binaries/installers of the client. Remember to edit the config.ini file before running. That will be done in the setup, but still.
## Manual compilationn
Compile the client like so.
Firstly, check if your Python version is => 3.11
```
$ python --version
> Python 3.11.3
```
Then, clone the repository.
`$ git clone https://github.com/confestim/custoMM`
`$ cd custoMM/src/client`
If you don't have pyinstaller installed, do:
`$ pip install pyinstaller`
Once that's ready, you're ready to compile!
`$ python -m PyInstaller --onefile main.py`
The exe file will reside in the newly created dist/ folder. Don't forget to copy the assets and the config to the same folder that your EXE resides(that means you will have to send them over to your friends along with the EXE file):
`cp assets ../config.ini dist`

BIN
src/client/assets/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1,71 @@
import random
import time
import configparser
from .Util import WhatTheFuckDidYouDo
class Game:
def __init__(self, *, loop=None, connection):
# Config
self.config = configparser.ConfigParser()
# Relative paths bad, fix this
self.config.read ("../config.ini")
self.URL = self.config["DEFAULT"]["URL"]
self.password = self.config["LEAGUE"]["LOBBY_PASS"]
# Loop until we get connection
self.connection = connection
def list_all(self):
# List all games
time.sleep(2)
self.connection.post("/lol-lobby/v1/custom-games/refresh", data={})
return self.connection.get("/lol-lobby/v1/custom-games").json()
def search(self, query):
# Searches for game by its name
return [x["id"] for x in self.list_all() if x["lobbyName"] == query]
def join_by_id(self, id):
# Joins a game, given an id
return self.connection.post(f"/lol-lobby/v1/custom-games/{id}/join", data={"password":self.password})
def join_by_name(self,name):
# Joins a game given its name
return self.join_by_id(self.search(str(name)))
def join_random(self):
# Joins a random public game
# mainly debug reasons
return self.join_by_id(random.choice([x["id"] for x in self.list_all() if not x["hasPassword"]]))
def create(self):
# Creates game
conn = self.connection
name = "CustoMM " + str(random.randint(100000, 10000000))
game = conn.post("/lol-lobby/v2/lobby/", data={
"customGameLobby": {
"configuration": {
"gameMode": f"CLASSIC", "gameServerRegion": "", "mapId": 11, "mutators": {"id": 6}, "spectatorPolicy": "AllAllowed", "teamSize": 5
},
"lobbyName": name,
"lobbyPassword": self.password
},
"isCustom": True
})
return str(name)
def start(self):
# Starts champ select
return self.connection.post("/lol-lobby/v1/lobby/custom/start-champ-select", data={})
def move(self):
return self.connection.post("/lol-lobby/v1/lobby/custom/switch-teams", data={})
def leave(self):
return self.connection.delete("/lol-lobby/v2/lobby")
def get_teams(self):
# Gets team
cfg = self.connection.get("/lol-lobby/v2/lobby").json()["gameConfig"]
return [[x["summonerInternalName"] for x in cfg["customTeam100"]],
[x["summonerInternalName"] for x in cfg["customTeam200"]]]

View File

@ -0,0 +1,23 @@
from time import sleep
from threading import Thread
from .Scraper import Scraper
import logging
class PeriodicScraper(Thread):
def __init__(self):
Thread.__init__(self)
self.daemon = True
self.connector:Scraper = Scraper()
self.closed = False
def run(self):
while True:
if self.closed:
self.connector.connection.stop()
break
game_state = self.connector.check_for_game()
logging.info("Scraping...")
self.connector.scrape()
sleep(5)

View File

@ -0,0 +1,258 @@
from lcu_connector import Connector
from lcu_connector.exceptions import ClientProcessError
import logging
import requests
import time, sys
import json
import configparser
from .Util import WhatTheFuckDidYouDo
from .Game import Game
class Scraper:
def __init__(self, *, loop=None, ui=None):
self.ui = ui
self.config = configparser.ConfigParser()
# Relative paths bad, fix this
self.config.read ("../config.ini")
self.URL = self.config["DEFAULT"]["URL"]
# Loop until we get connection
self.connection = None
while not self.connection:
try:
self.connection = Connector()
self.connection.start()
except ClientProcessError:
print("League client not open, sleeping...")
time.sleep(90)
self.summoner = self.connection.get('/lol-summoner/v1/current-summoner').json()
self.name = self.summoner["displayName"]
def calculate_kda(self, kills:int, assists:int, deaths:int):
"""
Calculates kill, death, assist ratio
Input: kills, assists, deaths
Output: KDA ratio
"""
if deaths == 0:
deaths = 1
return round((kills+assists)/deaths, 3)
def parse_history(self, history:dict, old_ids:list) -> list:
"""
Parses current player's history
Input: Logged in player's match history
Output: New data about unaccounted for custom games, ready to send to server
"""
connection = self.connection
parsed_matches = []
new = 0
for i in history["games"]["games"]:
if i["gameType"] == "CUSTOM_GAME" and str(i["gameId"]) not in old_ids and not i["gameMode"] == "PRACTICETOOL":
new += 1
match = connection.get(f'/lol-match-history/v1/games/{i["gameId"]}').json()
parsed_match = {
"game_id": match["gameId"],
"participants": {
"t1": {
"won": True if match["teams"][0]["win"] == "Won" else False,
"summoners": []
},
"t2": {
"won": True if match["teams"][1]["win"] == "Won" else False,
"summoners": []
}
}
}
# Sloppy solution, find fix.
print("Extracting data...")
for player in range(10):
current_player = match["participants"][player]["stats"]
kills = current_player["kills"]
assists = current_player["assists"]
deaths = current_player["deaths"]
if player <= 5:
parsed_match["participants"]["t1"]["summoners"].append({"name":match["participantIdentities"][player]["player"]["summonerName"], "kda": self.calculate_kda(kills, assists, deaths)})
else:
parsed_match["participants"]["t2"]["summoners"].append({"name":match["participantIdentities"][player]["player"]["summonerName"], "kda": self.calculate_kda(kills, assists, deaths)})
parsed_matches.append(parsed_match)
if not new:
print("Already up to date.")
time.sleep(3)
return parsed_matches
def register_summoner(self, claim, claimed):
if claim:
account = requests.put(f"{self.URL}/players/{claimed['lol']}/", data={
"lol": claimed["lol"],
"lol_id": claimed["lol_id"],
"discord_id":claimed["discord_id"],
"discord":claimed["discord"]
})
if account.status_code == 200:
print(f"Alright, the account is now yours, {claimed['discord']}.")
else:
print("Something went wrong when claiming your account...")
else:
requests.delete(f"{self.URL}/players/{claimed['discord_id']}")
def check_summoner(self):
"""
Checks if summoner is registered
"""
connection = self.connection
# Summoner
# Check if account is claimed
try:
claimed = requests.get(f"{self.URL}/players/?search={self.summoner['displayName']}").json()[0]
except Exception as e:
print(e)
return "USER_DOES_NOT_EXIST"
# Case 1: It belongs to nobody and has yet to be claimed.
if not claimed["discord"]:
return "UNCLAIMED"
if claimed:
# Case 2: It belongs to somebody
if claimed['lol_id'] and claimed['lol']:
# Change name in db if different in-game
if claimed['lol'] != self.summoner['displayName']:
self.register_summoner(True, claimed)
return (claimed['discord'], claimed['lol'])
# Case 3: Registration has begun, but hasn't finished
elif claimed['lol'] and not claimed['lol_id']:
claimed["lol_id"]=self.summoner["puuid"]
return ["REGISTRATION_IN_PROGRESS", claimed]
# All cases are covered, everything else will be considered a bug.
else:
raise WhatTheFuckDidYouDo()
def move_needed(self, checker, game, name):
# Move if you have to
# This is buggy, try to find a better way to do this.
# Like for example, letting team 1 pass first, and then team 2.
local_teams = game.get_teams()
if name in local_teams[0] and not name in checker["teams"][0]:
game.move()
logging.info("Moving to Team 2")
elif name in local_teams[1] and not name in checker["teams"][1]:
game.move()
logging.info("Moving to Team 1")
def start(self, checker, game):
self.move_needed(checker, game, self.name)
time.sleep(5)
# Wait until there are 10 players(confirmed) in the lobby
timeout_counter = 0
while response := requests.get(f"{self.URL}/current/{self.name}").json()["players"] != 10:
logging.info("Waiting for players...")
timeout_counter += 5
if timeout_counter == 60:
logging.info("Timeout, aborting...")
break
time.sleep(5)
if response == 10:
logging.info("Starting game...")
game.start()
else:
game.leave()
requests.delete(f"{self.URL}/current/{self.name}")
def check_for_game(self):
"""
Checks if a game is going on right now.
"""
try:
checker = requests.get(f"{self.URL}/current").json()[0]
except IndexError:
return "NO_GAME"
game = Game(connection=self.connection)
# If you are indeed the creator, create the game and disclose its name to the server
if checker["creator"] == self.name:
created = game.create()
# TODO: DEBUG
print(checker["teams"])
r = requests.put(f"{self.URL}/current/{self.name}/", data={
"lobby_name": created,
"creator": self.name,
"players": 1,
"teams": json.dumps(checker["teams"], indent=4)
})
print(r.content)
# Start the game
self.start(checker, game)
return "CREATED"
else:
# If you have to join
try:
name = checker["lobby_name"]
except KeyError:
# Wait until lobby name becomes available
while not requests.get(f"{self.URL}/current").json()[0].get("lobby_name"):
time.sleep(10)
checker = requests.get(f"{self.URL}/current").json()
name = checker["lobby_name"]
# Join the lobby
game.join_by_name(name)
# Update count of players
requests.put(f"{self.URL}/current/{checker['creator']}", data={
"lobby_name": checker["lobby_name"],
"creator": name,
"players": int(checker["players"])+1,
"teams": json.dumps(checker["teams"], indent=4)
})
return "JOINED"
def scrape(self):
"""Scrapes current account and sends it to server"""
connection = self.connection
self.check_summoner()
# Match History
match_history = connection.get('/lol-match-history/v1/products/lol/current-summoner/matches?endIndex=99')
match_history = match_history.json()
# Stage old ids in order for them to be parsed
old_ids = requests.get(f"{self.URL}/games/").json()
old_ids = [x["game_id"] for x in old_ids]
# TODO: Optimize the process of acquisition of new matches
games = self.parse_history(match_history, old_ids)
# Post the new games to your server(change in config.json)
for i in games:
req = requests.post(f"{self.URL}/games/", json=i)
if req.status_code == 500:
print("Serverside error! Contact maintainer!")
return len(games)

69
src/client/classes/UI.py Normal file
View File

@ -0,0 +1,69 @@
import pystray
from PIL import Image
from .Scraper import Scraper
import pyautogui
from time import sleep
class UI():
def __init__(self,scraper, periodic, parent):
image = Image.open(parent + "\\assets\\icon.png")
self.periodic = periodic
self.menu = pystray.Menu(
pystray.MenuItem(
"Check registration", self.check_registration, default=True
),
pystray.MenuItem(
f"Manual game report", self.report
),
pystray.MenuItem(
f"Check for game", self.check
),
pystray.MenuItem(
"Exit", self.quit
)
)
self.icon = pystray.Icon(
"name", image, "CustoMM", self.menu)
self.scraper = scraper
self.check_registration()
self.icon.run_detached()
def check(self):
self.icon.notify("This is discouraged, as it is done automatically anyway.", "Checking for game...")
game = self.scraper.check_for_game()
if game == "NO_GAME":
self.icon.notify("Please create a game on discord.", "No game found.")
elif game == "CREATED":
self.icon.notify("GLHF!", "You are the host of a new game!",)
elif game == "JOINED":
self.icon.notify("Waiting for players...", "Game joined!")
def report(self):
self.icon.notify("Game report initiated.")
self.scraper.scrape()
self.icon.notify("Game reported", "Your game has been reported to the server.")
def check_registration(self):
check = self.scraper.check_summoner()
if check == "USER_DOES_NOT_EXIST":
self.icon.notify("You are not registered, please register on the website.")
elif check == "UNCLAIMED":
self.icon.notify("You have not claimed your account yet, please claim it on discord -> !registed <ACCOUNT_NAME>.")
elif check[0] == "REGISTRATION_IN_PROGRESS":
prompt = pyautogui.confirm(f"Your account is currently being registered by {check[1]}, do you want to proceed?")
if prompt:
self.scraper.register_summoner(True, check[1])
else:
self.scraper.register_summoner(False, check[1])
else:
self.icon.notify(f"Your account is registered to {check[0]} and your account name is {check[1]}.")
def quit(self, icon, query):
icon.stop()
self.periodic.closed = True

View File

@ -0,0 +1,5 @@
class WhatTheFuckDidYouDo(Exception):
# We raise this when we don't know what the fuck happened
def __init__(self, message="Congrats, you broke this program, please explain your steps as an issue at https://github.com/confestim/custoMM"):
self.message = message
super().__init__(self.message)

45
src/client/main.py Normal file
View File

@ -0,0 +1,45 @@
import requests
# Edit config.ini when running for the first time
import sys
import os
import configparser
from time import sleep
import logging
# Custom imports
from classes.Util import WhatTheFuckDidYouDo
from classes.UI import UI
from classes.PeriodicScraper import PeriodicScraper
from classes.Scraper import Scraper
# Config section
parent = os.path.dirname(os.path.abspath(__file__))
config = configparser.ConfigParser()
logging.info(parent + "\\config.ini")
print(parent + "\\config.ini")
URL = config["DEFAULT"]["URL"]
logging.basicConfig(format='%(asctime)s - %(message)s', level=logging.INFO)
# Test connection to server
try:
test = requests.get(URL).json()
except Exception:
# NEVER DO THIS
# although, what could go wrong...
print("Server seems to be down, please contact admin if it keeps doing this")
sys.exit()
# Get current summoner
def main():
# Match scraping
# Running the UI
periodic = PeriodicScraper()
ui = UI(scraper=periodic.connector, periodic=periodic, parent=parent)
periodic.start()
periodic.join()
if __name__ == "__main__":
main()

137
src/discord/bot.py Normal file
View File

@ -0,0 +1,137 @@
import asyncio
import discord
from discord.ext import commands
import requests
from urllib.parse import quote
from classes.Target import Target
import json
import random
intents = discord.Intents.default()
intents.message_content = True
bot = commands.Bot(intents=intents, command_prefix='!')
@bot.event
async def on_ready():
print(f'We have logged in as {bot.user}')
game = discord.Game("Customki - !randomize")
await bot.change_presence(activity=game, status=discord.Status.dnd)
@bot.command()
async def randomize(ctx):
"""Randomizes 10 people into 2 teams: !randomize"""
target = Target(ctx, bot)
await target.randomize()
return
@bot.command()
async def begin_game(ctx):
"""Tries to start a fair game: !begin_game"""
target = Target(ctx, bot)
players = await target.ready()
if not players:
return
valid_players = []
# This does not support multiple accounts, refer to issue #7
# Initial check for existing players
for i in players:
print(i.name)
player = requests.get(f"{target.URL}/players/?search={quote(i.id)}").json()
print(player)
if not player:
await ctx.send(f"<@{i.id}> has no league account associated with their name. Please register in order to calculate MMR more accurately.")
else:
valid_players.append(player[0]["lol"])
# If not enough players, return
if len(valid_players) < 10:
return await ctx.send("Couldn't create fair game. Whoever isn't registered, please do.")
# Getting the players
query_string = "?"+"&".join(["players={}".format(quote(player)) for player in valid_players])
teams = requests.get(
f"{target.URL}/game/?{query_string}")
# Second check for existing players
# Also, funny status code
if teams.status_code == 451:
for i in teams.content:
await ctx.send(f"{i} has not been found by the API. Please register in order to calculate MMR more accurately.")
return
teams = teams.json()
# TODO: Debug, remove
print(teams)
await target.split(teams[0], teams[1])
requests.post(f"{target.URL}/current/", data={
"lobby_name": None,
"players": 0,
"creator": random.choice(valid_players),
"teams": json.dumps(teams)
})
return
@bot.command()
async def register(ctx, *args):
"""Registers a user to the database: !register <league_name>"""
target = Target(ctx, bot)
name = " ".join(args)
# TODO: add confirmation dialog
if len(name) < 4:
return await ctx.send("Provide a normal username (cAsE sEnSiTiVe)")
print(target.URL)
league_name = requests.get(f"{target.URL}/players/{name}").json()
print(league_name)
try:
if not league_name["detail"] == "Not found.":
return await ctx.send("Someone already claimed this account")
except KeyError:
if league_name["discord_id"]:
return await ctx.send(f"{league_name['discord']} has claimed this account.")
claim_account = requests.post(f"{target.URL}/players/", data={
"discord": ctx.author.name,
"discord_id": ctx.author.id,
"lol": name
})
if not claim_account.json().get('discord_id') and claim_account.json().get("lol"):
# In case that account doesn't exist at all
claim_account = requests.put(f"{target.URL}/players/{name}/", data={
"discord": ctx.author.name,
"discord_id": ctx.author.id,
"lol": name
})
print(claim_account.content)
# TODO: In case the account exists, but not yet claimed
print(claim_account.status_code)
if claim_account.status_code == 201 or claim_account.status_code == 200:
return await ctx.send("Success, now approve from client")
return await ctx.send("Something went wrong...")
@bot.command()
async def leaderboard(ctx):
"""Shows the Top 5 leaderboard: !leaderboard"""
target = Target(ctx, bot)
leaderboard = requests.get(f"{target.URL}/players").json()[:5]
leaderboard = "TOP 5 PLAYERS:\n-------------\n" + '\n'.join([f"{x['lol']} with {x['mmr']}" for x in leaderboard])
print(leaderboard)
await ctx.send(f"```{leaderboard}```")
return
# Change to your bot token in config.ini
bot.run(Target("no", bot).token)

View File

@ -0,0 +1,82 @@
import configparser
import discord
from discord.ext import commands
import asyncio
from typing import List
import random
class Target:
def __init__(self, ctx, bot:commands.Bot):
config = configparser.ConfigParser()
config.read("../config.ini")
self.URL = config["DEFAULT"]["URL"]
self.team_1 = config['DISCORD']['TEAM_1']
self.team_2 = config['DISCORD']['TEAM_2']
self.token = config['DISCORD']['TOKEN']
self.ctx = ctx
self.bot = bot
async def ready(self) -> bool:
"""
Checks if players are ready to be split
"""
self.trying_prompt = await self.ctx.send('Trying to start fair game!')
# Fetching user channel
try:
channel = self.bot.get_channel(self.ctx.author.voice.channel.id)
except AttributeError:
await self.trying_prompt.delete()
await self.ctx.send("You think you're cool or something? Get in a channel first.")
return False
players = channel.members
if len(players) < 10:
await self.trying_prompt.delete()
await self.ctx.send(f"Get more people in ({len(players)}/10)")
return False
if len(players) % 10 != 0:
# If total players not divisible by 10, can't split
await self.ctx.send("Can't split(remove bots and non-players)")
return False
return players
async def split(self, players_1, players_2):
"""
Splits players into 2 teams
"""
# Declaring channels
team_1 = self.bot.get_channel(self.team_1)
team_2 = self.bot.get_channel(self.team_2)
# Embedding
one_em = discord.Embed(title=f"Team 1", colour=discord.Colour(0x8c0303))
two_em = discord.Embed(title=f"Team 2", colour=discord.Colour(0x0B5394))
# Splitting logic
for i in range(5):
await players_1[i].move_to(team_1)
one_em.add_field(name=players_1[i].name, value=f"<@{players_2[i].id}>")
await players_2[i].move_to(team_2)
two_em.add_field(name=players_2[i].name, value=f"<@{players_2[i].id}>")
# Sending embeds and cleanup
await self.ctx.send(embed=one_em)
await self.ctx.send(embed=two_em)
await self.trying_prompt.delete()
return
async def randomize(self):
players = await self.ready()
if not players:
return False
random.shuffle(players)
self.split(players[:int(len(players)/2)], players[int(len(players)/2):])
return True

20
src/requirements.txt Normal file
View File

@ -0,0 +1,20 @@
aiohttp==3.8.4
aiosignal==1.3.1
asgiref==3.6.0
async-timeout==4.0.2
attrs==23.1.0
certifi==2023.5.7
charset-normalizer==3.1.0
discord==2.2.3
discord.py==2.2.3
Django==4.2.1
django-restframework==0.0.1
frozenlist==1.3.3
idna==3.4
lcu-connector==1.0.2
multidict==6.0.4
psutil==5.9.5
requests==2.30.0
sqlparse==0.4.4
urllib3==2.0.2
yarl==1.9.2

View File

@ -6,6 +6,7 @@ class Player(models.Model):
lol_id = models.CharField(max_length=60, null=True, blank=True , unique=True)
discord_id = models.IntegerField(null=True, blank=True)
mmr = models.IntegerField(default=600, editable=False)
#mmr = models.IntegerField(default=600)
games_played = models.IntegerField(default=0)
def __str__(self):
return f"{self.discord}:{self.lol}"
@ -14,3 +15,11 @@ class Game(models.Model):
game_id = models.CharField(max_length=30, unique=True)
class Current(models.Model):
lobby_name = models.CharField(max_length=30, unique=True)
players = models.IntegerField(default=0)
creator = models.CharField(max_length=30, unique=True, primary_key=True,)
teams = models.JSONField(default=dict)
def __str__(self):
return str(self.lobby_name)

View File

@ -0,0 +1,43 @@
from rest_framework import serializers
from .models import Player, Game, Current
class PlayerSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Player
fields = ('discord', 'lol', 'lol_id', 'discord_id', 'mmr')
def update(self, instance, validated_data):
instance.discord_id = validated_data.get(
'discord_id', instance.discord_id)
instance.discord = validated_data.get('discord', instance.discord)
instance.lol = validated_data.get('lol', instance.lol)
instance.lol_id = validated_data.get('lol_id', instance.lol_id)
instance.save()
return instance
class GameSerializer(serializers.ModelSerializer):
class Meta:
model = Game
fields = ("game_id",)
class CurrentSerializer(serializers.ModelSerializer):
class Meta:
model = Current
fields = ("lobby_name", "creator", "players", "teams")
def create(self, validated_data):
"""
Create and return a new `Current` instance, given the validated data.
"""
return Current.objects.create(**validated_data)
def update(self, instance, validated_data):
instance.players = validated_data.get('players', instance.players)
instance.lobby_name = validated_data.get('lobby_name', instance.lobby_name)
instance.save()
return instance

View File

@ -4,6 +4,7 @@ from . import views
router = routers.DefaultRouter()
router.register(r'players', views.PlayerViewSet)
router.register(r'current', views.CurrentViewSet)
urlpatterns = [
path('game/', views.game),

View File

@ -1,7 +1,7 @@
from rest_framework import viewsets, filters, mixins
from rest_framework.response import Response
from .serializers import PlayerSerializer, GameSerializer
from .models import Player, Game
from .serializers import PlayerSerializer, GameSerializer, CurrentSerializer
from .models import Player, Game, Current
import math
from rest_framework import status
@ -20,7 +20,7 @@ class PlayerViewSet(viewsets.ModelViewSet):
"""
search_fields = ['discord_id','lol']
filter_backends = (filters.SearchFilter,)
queryset = Player.objects.all().order_by('mmr')
queryset = Player.objects.all().order_by('mmr').reverse()
serializer_class = PlayerSerializer
def update(self, request, *args, **kwargs):
@ -37,7 +37,28 @@ class PlayerViewSet(viewsets.ModelViewSet):
self.perform_destroy(instance)
return Response(status=status.HTTP_204_NO_CONTENT)
class CurrentViewSet(viewsets.ModelViewSet):
"""
Display and interaction with the active games in the database
"""
queryset = Current.objects.all()
serializer_class = CurrentSerializer
def update(self, request, *args, **kwargs):
partial = kwargs.pop('partial', False)
instance = self.get_object()
serializer = self.get_serializer(instance, data=request.data, partial=partial)
serializer.is_valid(raise_exception=True)
self.perform_update(serializer)
return Response(serializer.data)
def delete(self, request, *args, **kwargs):
instance = Current.objects.get(creator=request.data.get("creator"))
self.perform_destroy(instance)
return Response(status=status.HTTP_204_NO_CONTENT)
@api_view(['GET', 'POST'])
def games(request):
"""
@ -64,7 +85,7 @@ def game(request):
"""
Returns fair team composition for a given list of players
"""
player_ids = request.data.getlist("players")
player_ids = request.GET.getlist("players")
players = list()
failed = False
for player in player_ids:
@ -79,7 +100,6 @@ def game(request):
if failed:
return Response([x for x in players if x["found"] == False], status=status.HTTP_451_UNAVAILABLE_FOR_LEGAL_REASONS)
players.sort(key=lambda x: x['mmr'], reverse=True)
# Sort the list of players based on their mmr values
@ -123,7 +143,7 @@ def game(request):
team1 = group1[::2] + group2[1::2]
team2 = group2[::2] + group1[1::2]
return Response({team1, team2})
return Response((team1, team2), status=status.HTTP_200_OK)