Closed davidcr01 closed 1 year ago
To manage the game of the tournaments, it is necessary to implement a new method in the Game viewset. This is because in the tournaments the games are created automatically, and not by a player. Because of this, the POST and PATCH method executed by the player1 and player2 respectively, implemented in #50 is not useful.
A new patch method has been added:
def partial_update_tournament(self, request, *args, **kwargs):
instance = self.get_object()
player = getattr(request.user, 'player', None)
if not player:
return Response({'error': 'Player not found'}, status=404)
if player == instance.player1:
if instance.player1_xp != 0 or instance.player1_time != 0:
return Response({'error': 'You have already updated your information for this game.'}, status=400)
opponent = instance.player2
opponent_xp = instance.player2_xp
opponent_time = instance.player2_time
elif player == instance.player2:
if instance.player2_xp != 0 or instance.player2_time != 0:
return Response({'error': 'You have already updated your information for this game.'}, status=400)
opponent = instance.player1
opponent_xp = instance.player1_xp
opponent_time = instance.player1_time
else:
return Response({'error': 'You do not have permission to update this game.'}, status=403)
# Data requested change depending on the player who is executing the method
if instance.player1 == player:
allowed_fields = ['player1_xp', 'player1_time', 'player1_attempts', 'word']
elif instance.player2 == player:
allowed_fields = ['player2_xp', 'player2_time', 'player2_attempts', 'word']
else:
return Response({'error': 'You do not have permission to update this game.'}, status=403)
data = {key: request.data.get(key) for key in allowed_fields}
if 'winner' in data:
return Response({'error': 'The "winner" field cannot be modified.'}, status=400)
if instance.word != '':
data.pop('word')
serializer = self.get_serializer(instance, data=data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save()
player_xp = serializer.validated_data.get('player1_xp' if player == instance.player1 else 'player2_xp', 0)
player_time = serializer.validated_data.get('player1_time' if player == instance.player1 else 'player2_time', 0)
# Winner is calculated when all data is available
if (player_xp != 0 and player_time != 0 and opponent_xp != 0 and opponent_time != 0):
if player_xp > opponent_xp:
instance.winner = player
instance.winner.wins_pvp += 1
instance.winner.save()
instance.save()
elif player_xp < opponent_xp:
instance.winner = opponent
opponent.wins_pvp += 1
instance.winner.save()
instance.save()
else:
if player_time <= opponent_time:
instance.winner = player
instance.winner.wins_pvp += 1
instance.winner.save()
instance.save()
else:
instance.winner = opponent
opponent.wins_pvp += 1
instance.winner.save()
instance.save()
if instance.winner is not None:
return Response({'winner': instance.winner.user.username})
else:
return Response({'message': 'Game updated successfully.'})
This method is executed by both players. Every player fills its information, and when all the information is stored, the winner is calculated. Some error cases are checked:
Player2 fills its information:
Player1 fills its information:
And the winner is set. Notice that the word 'beach' passed in the second patch was ignored.
To automatically create the rounds and games when the tournament is closed. To do this, is necessary to edit the ParticipationViewSet, in the part when the tournament is closed:
participation = Participation.objects.create(tournament=tournament, player=player)
tournament.num_players += 1
# Close the tournament if is full
if tournament.num_players >= tournament.max_players:
tournament.is_closed = True
rounds = int(math.log2(tournament.max_players))
for round_number in range(1, rounds+1):
round = Round.objects.create(tournament=tournament, number=round_number)
if round_number == 1:
# Assign games to the first round
participants = Participation.objects.filter(tournament=tournament)
participants_count = participants.count()
for i in range(0, participants_count, 2):
player1 = participants[i].player
player2 = participants[i + 1].player
new_game = Game.objects.create(player1=player1, player2=player2, is_tournament_game=True)
RoundGame.objects.create(round=round, game=new_game)
This code snippet creates all the rounds of the tournament, knowing the max number of participants; and creates the related games to the first round.
Same code has been added to the admin.py
file to manage this logic when an administrator creates a pre-selected tournament.
Now, is necessary to create a new logic that creates the games of the next round when the current round is over (when all the related games are completed). To do this, a new signal has been added to the signals.py
. This signal is executed everytime a tournament game is finished:
@receiver(post_save, sender=Game)
def game_completed(sender, instance, created, **kwargs):
if instance.is_tournament_game and instance.winner:
round = Round.objects.filter(roundgame__game=instance).last()
if round:
round_games = RoundGame.objects.filter(round=round)
if all(game.game.winner for game in round_games):
tournament = round.tournament
# Get current round
current_round_number = tournament.current_round
num_rounds = int(math.log2(tournament.max_players))
# Get winners of current round
winners = [game.game.winner for game in round_games]
# If its the last round, games are not created (tournament end)
if current_round_number == num_rounds:
winner = instance.winner
winner.wins_tournament += 1
winner.xp += 1000
winner.save()
else:
# Get next round
next_round_number = current_round_number + 1
next_round = Round.objects.get(tournament=tournament, number=next_round_number)
tournament.current_round = next_round_number
tournament.save()
# Create games and assign them to the round
for i in range(0, len(winners), 2):
player1 = winners[i]
player2 = winners[i + 1]
new_game = Game.objects.create(player1=player1, player2=player2, is_tournament_game=True)
RoundGame.objects.create(round=next_round, game=new_game)
This code creates the next round with the winners of the previous round if the round is not the last one. If it is, the winner is fetched and updated.
Screenshot of a tournament of 4 participants. Notice how the round 1 is created and the round 2 contains the winners of the round 1.
When the round 2 is completed (last one), no round is created and the user is updated.
To fetch this information, new methods in the TournamentViewSet have been added:
Returns the rounds of the tournament
Returns the games of the round by number
The mockup for the tournament rounds is:
To allow the user join a tournament, a new function in the tournament page has been added:
async joinTournament(idTournament: number) {
(await this.apiService.createParticipation(idTournament)).subscribe(
(response) => {
this.toastService.showToast('You joined the tournament successfully!', 2000, 'top', 'success');
console.log('Participation added successfully', response);
},
(error) => {
if (error.status === 400) {
this.toastService.showToast('You already participate in this tournament!', 2000, 'top', 'warning');
} else {
this.toastService.showToast('There was an error jonining to the tournament!', 2000, 'top', 'danger');
}
console.log('Participation could not be added', error);
}
);
}
<ion-button expand="block" class="join-button" [disabled]="tournament.is_closed" (click)="joinTournament(tournament.id)">Join</ion-button>
With this, the participation is created and the user has joined the tournament.
A new segment in the tournament page has been added to display the tournaments that the player is joined:
This segment uses a new function getMyTournaments of the apiService. The related view is:
@action(detail=False, methods=['get'])
def player_tournaments(self, request):
player = getattr(request.user, 'player', None)
if not player:
return Response({'error': 'Player not found'}, status=404)
tournaments = Tournament.objects.filter(participation__player=player).order_by('-is_closed')
serializer = TournamentSerializer(tournaments, many=True)
return Response(serializer.data)
When the user enters into a tournament, a new page tournamentrounds
is displayed. This page contains the games of every round of the tournament:
Notice that:
To display this information, new methods have been added to the apiService: getRounds, getGameRounds, y getTournamentInfo. The related methods are:
async loadTournamentInfo() {
this.isLoading = true;
(await this.apiService.getTournamentInfo(this.tournamentId)).subscribe(
(response) => {
console.log(response);
console.log(response.current_round);
this.tournamentInfo = response;
this.currentRound = response.current_round;
this.selectedSegment = response.current_round;
},
(error) => {
console.error('Error loading tournament info:', error);
this.isLoading = true;
}
);
}
async loadTournamentRounds() {
this.isLoading = true;
(await this.apiService.getRounds(this.tournamentId)).subscribe(
(data: any) => {
this.rounds = data;
this.loadRoundGames(this.selectedSegment);
},
(error) => {
console.error('Error loading tournament rounds:', error);
this.isLoading = false;
}
);
}
async loadRoundGames(roundNumber: number) {
this.isLoading = true;
(await this.apiService.getGamesRound(this.tournamentId, roundNumber)).subscribe(
(data: any) => {
console.log(data);
this.roundGames = data;
this.isLoading = false;
},
(error) => {
console.error('Error loading round games:', error);
this.isLoading = false;
}
);
}
Every time a segment is clicked, the games related to the round selected are loaded.
When the last round is completed, the username of the tournament winner is displayed:
This is implemented by modifying the loadroundGames:
if (roundNumber === this.lastRound && this.roundGames[0].winner) {
(await this.apiService.getPlayerData(this.roundGames[0].winner)).subscribe(
(data: any) => {
this.winnerTournamentUsername = data.user.username;
},
(error) => {
console.error('Error loading winner username:', error);
this.isLoading = false;
}
);
}
As the tournament games are different from the multiplayer games (the tournament games are already created, they are not created by any player), is necessary to implement a new page with a new logic to play the tournament games game-tournament
.
The HTML code is very similar to respond-game
:
<ion-header>
<ion-toolbar *ngIf="tournamentInfo">
<ion-title>Game: {{ tournamentInfo.name }}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ng-container *ngIf="player1 && player2">
<div class="game-info">
<div class="username">{{ player1 }}</div>
<img src="../../../assets/icon/vs.png" alt="VS icon">
<div class="username">{{ player2 }}</div>
</div>
</ng-container>
<ng-container *ngIf="wordLength && !selectedWord">
<app-wordle-dashboard [WORDS_LENGTH]="wordLength" [isMultiplayer]="true" (gameFinished)="finishGame($event.time, $event.xp, $event.attempts, $event.selectedWord)"></app-wordle-dashboard>
</ng-container>
<ng-container *ngIf="wordLength && selectedWord">
<app-wordle-dashboard [rightGuessString]="selectedWord" [WORDS_LENGTH]="wordLength" [isMultiplayer]="true" (gameFinished)="finishGame($event.time, $event.xp, $event.attempts, $event.selectedWord)"></app-wordle-dashboard>
</ng-container>
</ion-content>
Notice that the selectedWord
may be passed if the game already has a word assigned. If not, the word will be selected by the selectRandomWord
of the Wordle Dashboard component.
In this case, both players use the same API method, but with different bodies:
async finishGame(time: number, xp: number, attempts: number, selectedWord: string) {
let gameData: any = {};
gameData = {
player1_time: time,
player1_xp: xp,
player1_attempts: attempts,
word: selectedWord,
};
if (this.selfUsername === this.player2) {
gameData = {
player2_time: time,
player2_xp: xp,
player2_attempts: attempts,
word: selectedWord,
};
}
(await this.apiService.resolveTournamentGame(this.gameId, gameData)).subscribe(
(response) => {
console.log('Game resolved successfully', response);
if (response.message) {
setTimeout( () => this.showAlert('Amazing!', 'Who will win?'), 2500);
}
else {
if (response.winner === this.selfUsername) {
setTimeout( () => this.showAlert('Congratulations!', 'You won! You will be in the next round!'), 2500);
} else {
setTimeout( () => this.showAlert('Bad news!', 'You lost. Try next time!'), 2500);
}
}
},
(error) => {
console.log('Game could not be resolved', error);
}
);
}
Same conditions errors are checked as create-game
and respond-game
Description
As a player, is necessary to implement the logic and UI to join a tournament.
Notice that, when the tournament is closed, is necessary to create the associate
Tasks