Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Win Loss #808

Merged
merged 5 commits into from
Oct 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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")))

lowtorola marked this conversation as resolved.
Show resolved Hide resolved
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
lowtorola marked this conversation as resolved.
Show resolved Hide resolved
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";

Expand All @@ -72,7 +72,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 @@ -119,6 +119,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
Loading