Skip to content

Commit

Permalink
Implement Win Loss (#808)
Browse files Browse the repository at this point in the history
Co-authored-by: Serena Li <[email protected]> :shipit:
  • Loading branch information
lowtorola authored Oct 11, 2024
1 parent 650e397 commit 4a29016
Show file tree
Hide file tree
Showing 23 changed files with 822 additions and 33 deletions.
7 changes: 7 additions & 0 deletions backend/siarnaq/api/compete/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -481,5 +481,12 @@ class HistoricalRatingSerializer(serializers.Serializer):
team_rating = TeamRatingSerializer(default=None)


class ScrimmageRecordSerializer(serializers.Serializer):
team_id = serializers.IntegerField()
wins = serializers.IntegerField()
losses = serializers.IntegerField()
ties = serializers.IntegerField()


class EmptySerializer(serializers.Serializer):
pass
93 changes: 93 additions & 0 deletions backend/siarnaq/api/compete/views.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from functools import reduce
from typing import Optional

import google.cloud.storage as storage
Expand Down Expand Up @@ -28,6 +29,7 @@
HistoricalRatingSerializer,
MatchReportSerializer,
MatchSerializer,
ScrimmageRecordSerializer,
ScrimmageRequestSerializer,
SubmissionDownloadSerializer,
SubmissionReportSerializer,
Expand Down Expand Up @@ -417,6 +419,12 @@ def historical_rating(self, request, pk=None, *, episode_id):
team_ids = {parse_int(team_id) for team_id in team_ids}
queryset = queryset.filter(participants__team__in=team_ids)
elif request.user.pk is not None:
team_ids = {
team.id
for team in Team.objects.filter(members__pk=request.user.pk).filter(
episode_id=episode_id
)
}
queryset = queryset.filter(participants__team__members=request.user.pk)
else:
return Response([])
Expand Down Expand Up @@ -450,6 +458,91 @@ def historical_rating(self, request, pk=None, *, episode_id):
results = HistoricalRatingSerializer(grouped.values(), many=True).data
return Response(results, status=status.HTTP_200_OK)

@extend_schema(
parameters=[
OpenApiParameter(
name="team_id",
type=int,
description="A team to filter for. Defaults to your own team.",
),
OpenApiParameter(
name="scrimmage_type",
enum=["ranked", "unranked", "all"],
default="all",
description="Which type of scrimmages to filter for. Defaults to all.",
),
],
responses={
status.HTTP_200_OK: ScrimmageRecordSerializer(),
status.HTTP_400_BAD_REQUEST: OpenApiResponse(
description="No team found with the given ID."
),
},
)
@action(
detail=False,
methods=["get"],
permission_classes=(IsEpisodeMutable,),
)
def scrimmaging_record(self, request, pk=None, *, episode_id):
"""List the scrimmaging win-loss-tie record of a team."""
queryset = self.get_queryset().filter(tournament_round__isnull=True)

scrimmage_type = self.request.query_params.get("scrimmage_type")
if scrimmage_type is not None:
if scrimmage_type == "ranked":
queryset = queryset.filter(is_ranked=True)
elif scrimmage_type == "unranked":
queryset = queryset.filter(is_ranked=False)

team_id = parse_int(self.request.query_params.get("team_id"))
if team_id is None and request.user.pk is not None:
team_id = (
Team.objects.filter(members__pk=request.user.pk)
.filter(episode_id=episode_id)
.first()
.id
)
if team_id is None:
return Response(status=status.HTTP_400_BAD_REQUEST)
if team_id is not None:
queryset = queryset.filter(participants__team=team_id)
else:
return Response(status=status.HTTP_400_BAD_REQUEST)
has_invisible = self.get_queryset().filter(
participants__team__status=TeamStatus.INVISIBLE
)
queryset = queryset.exclude(pk__in=Subquery(has_invisible.values("pk")))

def match_handler(record, match):
"""Mutate the win-loss-tie record based on the match outcome."""
this_team = match.participants.filter(team=team_id).first()
other_team = match.participants.exclude(team=team_id).first()
if this_team is None or other_team is None:
return record
if this_team.score is None or other_team.score is None:
return record
if this_team.score > other_team.score:
record["wins"] += 1
elif this_team.score < other_team.score:
record["losses"] += 1
else:
record["ties"] += 1
return record

win_loss_tie = reduce(
match_handler,
queryset.all(),
{
"team_id": team_id,
"wins": 0,
"losses": 0,
"ties": 0,
},
)
results = ScrimmageRecordSerializer(win_loss_tie).data
return Response(results, status=status.HTTP_200_OK)

@extend_schema(
responses={
status.HTTP_204_NO_CONTENT: OpenApiResponse(
Expand Down
2 changes: 1 addition & 1 deletion frontend2/.env.development
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
REACT_APP_THIS_URL=http://localhost:3000
REACT_APP_BACKEND_URL=http://api.staging.battlecode.org/
REACT_APP_BACKEND_URL=http://localhost:8000
REACT_APP_REPLAY_URL=https://play.battlecode.org
55 changes: 55 additions & 0 deletions frontend2/schema.yml
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,45 @@ paths:
schema:
$ref: '#/components/schemas/PaginatedMatchList'
description: ''
/api/compete/{episode_id}/match/scrimmaging_record/:
get:
operationId: compete_match_scrimmaging_record_retrieve
description: List the scrimmaging win-loss-tie record of a team.
parameters:
- in: path
name: episode_id
schema:
type: string
pattern: ^[^\/.]+$
required: true
- in: query
name: scrimmage_type
schema:
type: string
enum:
- all
- ranked
- unranked
default: all
description: Which type of scrimmages to filter for. Defaults to all.
- in: query
name: team_id
schema:
type: integer
description: A team to filter for. Defaults to your own team.
tags:
- compete
security:
- jwtAuth: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/ScrimmageRecord'
description: ''
'400':
description: No team found with the given ID.
/api/compete/{episode_id}/match/tournament/:
get:
operationId: compete_match_tournament_list
Expand Down Expand Up @@ -2560,6 +2599,22 @@ components:
type: boolean
required:
- status
ScrimmageRecord:
type: object
properties:
team_id:
type: integer
wins:
type: integer
losses:
type: integer
ties:
type: integer
required:
- losses
- team_id
- ties
- wins
ScrimmageRequest:
type: object
properties:
Expand Down
12 changes: 10 additions & 2 deletions frontend2/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ import { tournamentsLoader } from "./api/loaders/tournamentsLoader";
import { tournamentLoader } from "./api/loaders/tournamentLoader";
import { homeLoader } from "./api/loaders/homeLoader";
import ErrorBoundary from "./views/ErrorBoundary";
import { searchTeamsFactory } from "api/team/teamFactories";
import { myTeamFactory, searchTeamsFactory } from "api/team/teamFactories";
import PageNotFound from "views/PageNotFound";
import TeamProfile from "views/TeamProfile";
import { passwordForgotLoader } from "api/loaders/passwordForgotLoader";
Expand All @@ -73,7 +73,7 @@ queryClient.setQueryDefaults(["team"], { retry: false });
queryClient.setQueryDefaults(["user"], { retry: false });

// Run a check to see if the user has an invalid token
await loginCheck(queryClient);
const loggedIn = await loginCheck(queryClient);

const App: React.FC = () => {
return (
Expand Down Expand Up @@ -120,6 +120,14 @@ const episodeLoader: LoaderFunction = async ({ params }) => {
),
});

// Prefetch the user's team.
if (loggedIn) {
void queryClient.ensureQueryData({
queryKey: buildKey(myTeamFactory.queryKey, { episodeId: id }),
queryFn: async () => await myTeamFactory.queryFn({ episodeId: id }),
});
}

return episodeInfo;
};

Expand Down
1 change: 1 addition & 0 deletions frontend2/src/api/_autogen/.openapi-generator/FILES
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ models/ReleaseStatusEnum.ts
models/ResetToken.ts
models/ResetTokenRequest.ts
models/SaturnInvocationRequest.ts
models/ScrimmageRecord.ts
models/ScrimmageRequest.ts
models/ScrimmageRequestRequest.ts
models/ScrimmageStatusEnum.ts
Expand Down
65 changes: 65 additions & 0 deletions frontend2/src/api/_autogen/apis/CompeteApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import type {
PaginatedMatchList,
PaginatedScrimmageRequestList,
PaginatedSubmissionList,
ScrimmageRecord,
ScrimmageRequest,
ScrimmageRequestRequest,
Submission,
Expand All @@ -41,6 +42,8 @@ import {
PaginatedScrimmageRequestListToJSON,
PaginatedSubmissionListFromJSON,
PaginatedSubmissionListToJSON,
ScrimmageRecordFromJSON,
ScrimmageRecordToJSON,
ScrimmageRequestFromJSON,
ScrimmageRequestToJSON,
ScrimmageRequestRequestFromJSON,
Expand Down Expand Up @@ -92,6 +95,12 @@ export interface CompeteMatchScrimmageListRequest {
teamId?: number;
}

export interface CompeteMatchScrimmagingRecordRetrieveRequest {
episodeId: string;
scrimmageType?: CompeteMatchScrimmagingRecordRetrieveScrimmageTypeEnum;
teamId?: number;
}

export interface CompeteMatchTournamentListRequest {
episodeId: string;
externalIdPrivate?: string;
Expand Down Expand Up @@ -470,6 +479,52 @@ export class CompeteApi extends runtime.BaseAPI {
return await response.value();
}

/**
* List the scrimmaging win-loss-tie record of a team.
*/
async competeMatchScrimmagingRecordRetrieveRaw(requestParameters: CompeteMatchScrimmagingRecordRetrieveRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<ScrimmageRecord>> {
if (requestParameters.episodeId === null || requestParameters.episodeId === undefined) {
throw new runtime.RequiredError('episodeId','Required parameter requestParameters.episodeId was null or undefined when calling competeMatchScrimmagingRecordRetrieve.');
}

const queryParameters: any = {};

if (requestParameters.scrimmageType !== undefined) {
queryParameters['scrimmage_type'] = requestParameters.scrimmageType;
}

if (requestParameters.teamId !== undefined) {
queryParameters['team_id'] = requestParameters.teamId;
}

const headerParameters: runtime.HTTPHeaders = {};

if (this.configuration && this.configuration.accessToken) {
const token = this.configuration.accessToken;
const tokenString = await token("jwtAuth", []);

if (tokenString) {
headerParameters["Authorization"] = `Bearer ${tokenString}`;
}
}
const response = await this.request({
path: `/api/compete/{episode_id}/match/scrimmaging_record/`.replace(`{${"episode_id"}}`, encodeURIComponent(String(requestParameters.episodeId))),
method: 'GET',
headers: headerParameters,
query: queryParameters,
}, initOverrides);

return new runtime.JSONApiResponse(response, (jsonValue) => ScrimmageRecordFromJSON(jsonValue));
}

/**
* List the scrimmaging win-loss-tie record of a team.
*/
async competeMatchScrimmagingRecordRetrieve(requestParameters: CompeteMatchScrimmagingRecordRetrieveRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<ScrimmageRecord> {
const response = await this.competeMatchScrimmagingRecordRetrieveRaw(requestParameters, initOverrides);
return await response.value();
}

/**
* List matches played in a tournament, or in all tournaments if not specified. Passing the external_id_private of a tournament allows match lookup for the tournament, even if it\'s private. Client uses the external_id_private parameter
*/
Expand Down Expand Up @@ -1060,3 +1115,13 @@ export class CompeteApi extends runtime.BaseAPI {
}

}

/**
* @export
* @enum {string}
*/
export enum CompeteMatchScrimmagingRecordRetrieveScrimmageTypeEnum {
All = 'all',
Ranked = 'ranked',
Unranked = 'unranked'
}
Loading

0 comments on commit 4a29016

Please sign in to comment.