Skip to content

Commit

Permalink
Merge pull request #427 from battlecode/tour-runner
Browse files Browse the repository at this point in the history
Add single elimination tournament runner in beta
  • Loading branch information
n8kim1 authored Jan 17, 2023
2 parents 890272d + f2bea77 commit 4332288
Show file tree
Hide file tree
Showing 13 changed files with 553 additions and 25 deletions.
1 change: 1 addition & 0 deletions backend/environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,4 @@ dependencies:
- django_rest_passwordreset==1.3.0
- djangorestframework-simplejwt==5.2.2
- google-cloud-scheduler==2.7.3
- requests==2.28.1
21 changes: 17 additions & 4 deletions backend/siarnaq/api/compete/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,11 @@ def has_delete_permission(self, request, obj=None):
class MatchParticipantInline(admin.TabularInline):
model = MatchParticipant
extra = 0
fields = ("team", "submission", "player_index", "score", "rating")
fields = ("team", "submission", "player_index", "score", "rating", "challonge_id")
max_num = 2
ordering = ("player_index",)
raw_id_fields = ("team", "submission")
readonly_fields = ("rating",)
readonly_fields = ("rating", "challonge_id")

def get_queryset(self, request):
return (
Expand All @@ -102,7 +102,6 @@ class MatchAdmin(admin.ModelAdmin):
{
"fields": (
"episode",
"tournament_round",
"replay",
"alternate_order",
"is_ranked",
Expand All @@ -116,6 +115,12 @@ class MatchAdmin(admin.ModelAdmin):
"fields": ("status", "created", "num_failures", "logs"),
},
),
(
"Tournament metadata",
{
"fields": ("tournament_round", "challonge_id"),
},
),
)
inlines = [MatchParticipantInline]
list_display = (
Expand All @@ -128,7 +133,15 @@ class MatchAdmin(admin.ModelAdmin):
list_filter = ("episode", "status")
ordering = ("-pk",)
raw_id_fields = ("tournament_round",)
readonly_fields = ("replay", "status", "created", "num_failures", "logs")
readonly_fields = (
"replay",
"status",
"created",
"num_failures",
"logs",
"tournament_round",
"challonge_id",
)

def formfield_for_manytomany(self, db_field, request, **kwargs):
pk = request.resolver_match.kwargs.get("object_id", None)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 4.1.2 on 2023-01-17 06:57

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("compete", "0003_initial"),
]

operations = [
migrations.AddField(
model_name="match",
name="challonge_id",
field=models.IntegerField(blank=True, null=True, unique=True),
),
migrations.AddField(
model_name="matchparticipant",
name="challonge_id",
field=models.CharField(blank=True, max_length=64, null=True),
),
]
17 changes: 16 additions & 1 deletion backend/siarnaq/api/compete/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,14 +205,22 @@ class Match(SaturnInvocation):
"""The maps to be played in this match."""

alternate_order = models.BooleanField()
"""Whether players should alternate orderGbetween successive games of this match."""
"""Whether players should alternate order between successive games of this match."""

is_ranked = models.BooleanField()
"""Whether this match counts for ranked ratings."""

replay = models.UUIDField(default=uuid.uuid4)
"""The replay file of this match."""

# NOTE: I'm not sure if this field _has_ to be unique.
# Feel free to relax it later.
# (Not enforcing it now, and then enforcing it later
# when a duplicate may have snuck in, would be hard.)
challonge_id = models.IntegerField(blank=True, null=True, unique=True)
"""If this match is referenced in a private Challonge bracket,
Challonge's internal ID of the match in the bracket."""

objects = MatchQuerySet.as_manager()

def __str__(self):
Expand Down Expand Up @@ -344,6 +352,13 @@ class MatchParticipant(models.Model):
)
"""The team's previous participation, or null if there is none."""

challonge_id = models.CharField(null=True, blank=True, max_length=64)
"""
If the associated match is in Challonge,
Challonge's internal ID of this participant.
(This saves many API calls.)
"""

objects = MatchParticipantManager()

def __str__(self):
Expand Down
36 changes: 30 additions & 6 deletions backend/siarnaq/api/episodes/admin.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from django.contrib import admin
import structlog
from django.contrib import admin, messages

from siarnaq.api.compete.models import Match
from siarnaq.api.episodes.models import (
Expand All @@ -9,6 +10,8 @@
TournamentRound,
)

logger = structlog.get_logger(__name__)


class MapInline(admin.TabularInline):
model = Map
Expand Down Expand Up @@ -108,8 +111,16 @@ def get_queryset(self, request):
return super().get_queryset(request).prefetch_related("maps")


@admin.action(description="Initialize a tournament")
def initialize(modeladmin, request, queryset):
logger.info("initialize", message=f"Initializing tournaments in {queryset}")
for tournament in queryset:
tournament.initialize()


@admin.register(Tournament)
class TournamentAdmin(admin.ModelAdmin):
actions = [initialize]
fieldsets = (
(
"General",
Expand All @@ -134,7 +145,10 @@ class TournamentAdmin(admin.ModelAdmin):
(
"Challonge configuration",
{
"fields": ("challonge_private", "challonge_public", "in_progress"),
"fields": (
"challonge_id_private",
"challonge_id_public",
),
},
),
)
Expand All @@ -146,12 +160,10 @@ class TournamentAdmin(admin.ModelAdmin):
"episode",
"submission_freeze",
"is_public",
"in_progress",
)
list_filter = ("episode",)
list_select_related = ("episode",)
ordering = ("-episode__game_release", "-submission_freeze")
readonly_fields = ("in_progress",)
search_fields = ("name_short", "name_long")
search_help_text = "Search for a full or abbreviated name."

Expand All @@ -174,21 +186,33 @@ def has_delete_permission(self, request, obj):
return False


@admin.action(description="Create and enqueue matches of a tournament round")
def enqueue(modeladmin, request, queryset):
logger.info("enqueue", message=f"Enqueueing tournament rounds {queryset}")
for round in queryset:
try:
round.enqueue()
except RuntimeError as e:
messages.error(request, str(e))


@admin.register(TournamentRound)
class TournamentRoundAdmin(admin.ModelAdmin):
actions = [enqueue]
fields = (
"name",
"tournament",
"challonge_id",
"release_status",
"maps",
"in_progress",
)
inlines = [MatchInline]
list_display = ("name", "tournament", "release_status")
list_display = ("name", "tournament", "release_status", "in_progress")
list_filter = ("tournament", "release_status")
list_select_related = ("tournament",)
ordering = ("-tournament__submission_freeze", "challonge_id")
readonly_fields = ("challonge_id",)
readonly_fields = ("challonge_id", "in_progress")

def formfield_for_manytomany(self, db_field, request, **kwargs):
pk = request.resolver_match.kwargs.get("object_id", None)
Expand Down
101 changes: 101 additions & 0 deletions backend/siarnaq/api/episodes/challonge.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# An API-esque module for our usage.
# Commands here are not very generic (like a good API),
# and are instead tailored to Battlecode's specific usage,
# to improve dev efficiency

import requests
from django.conf import settings

_headers = {
"Accept": "application/json",
"Authorization-Type": "v1",
"Authorization": settings.CHALLONGE_API_KEY,
"Content-Type": "application/vnd.api+json",
# requests' default user agent causes Challonge's API to crash.
"User-Agent": "",
}

AUTH_TYPE = "v1"
URL_BASE = "https://api.challonge.com/v2/"


def set_api_key(api_key):
"""Set the challonge.com api credentials to use."""
_headers["Authorization"] = api_key


def create_tournament(
tournament_url, tournament_name, is_private=True, is_single_elim=True
):
tournament_type = "single elimination" if is_single_elim else "double elimination"

url = f"{URL_BASE}tournaments.json"

payload = {
"data": {
"type": "tournaments",
"attributes": {
"name": tournament_name,
"tournament_type": tournament_type,
"private": is_private,
"url": tournament_url,
},
}
}

r = requests.post(url, headers=_headers, json=payload)
r.raise_for_status()


def bulk_add_participants(tournament_url, participants):
"""
Adds participants in bulk.
Expects `participants` to be formatted in the format Challonge expects.
Note especially that seeds must be 1-indexed.
"""
url = f"{URL_BASE}tournaments/{tournament_url}/participants/bulk_add.json"

payload = {
"data": {
"type": "Participant",
"attributes": {
"participants": participants,
},
}
}

r = requests.post(url, headers=_headers, json=payload)
r.raise_for_status()


def start_tournament(tournament_url):
url = f"{URL_BASE}tournaments/{tournament_url}/change_state.json"

payload = {"data": {"type": "TournamentState", "attributes": {"state": "start"}}}

r = requests.put(url, headers=_headers, json=payload)
r.raise_for_status()


def get_tournament(tournament_url):
url = f"{URL_BASE}tournaments/{tournament_url}.json"

r = requests.get(url, headers=_headers)
r.raise_for_status()
return r.json()


def update_match(tournament_url, match_id, match):
url = f"{URL_BASE}tournaments/{tournament_url}/matches/{match_id}.json"

payload = {
"data": {
"type": "Match",
"attributes": {
"match": match,
},
}
}

r = requests.put(url, headers=_headers, json=payload)
r.raise_for_status()
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Generated by Django 4.1.2 on 2023-01-17 06:57

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
(
"episodes",
"0002_rename_release_version_episode_release_version_public_and_more",
),
]

operations = [
migrations.RemoveField(
model_name="tournament",
name="challonge_private",
),
migrations.RemoveField(
model_name="tournament",
name="challonge_public",
),
migrations.RemoveField(
model_name="tournament",
name="in_progress",
),
migrations.AddField(
model_name="tournament",
name="challonge_id_private",
field=models.SlugField(blank=True, null=True),
),
migrations.AddField(
model_name="tournament",
name="challonge_id_public",
field=models.SlugField(blank=True, null=True),
),
migrations.AddField(
model_name="tournamentround",
name="in_progress",
field=models.BooleanField(default=False),
),
]
Loading

0 comments on commit 4332288

Please sign in to comment.