From a0201869ad5cf48e870ed278ffa6a7a8af1c23b9 Mon Sep 17 00:00:00 2001 From: vtm9 Date: Fri, 27 Oct 2023 14:55:52 +0200 Subject: [PATCH] Improve tournament Stats --- .../tournamentPlayer/TournamentPlayer.jsx | 111 ++++++++++++++---- .../codebattle/lib/codebattle/game/fsm.ex | 1 + .../codebattle/lib/codebattle/game/helpers.ex | 50 +++++++- .../lib/codebattle/tournament/match.ex | 2 +- .../codebattle/tournament/strategy/base.ex | 49 ++------ .../channels/tournament_player_channel.ex | 25 +--- 6 files changed, 149 insertions(+), 89 deletions(-) diff --git a/services/app/apps/codebattle/assets/js/widgets/pages/tournamentPlayer/TournamentPlayer.jsx b/services/app/apps/codebattle/assets/js/widgets/pages/tournamentPlayer/TournamentPlayer.jsx index 349782c97..bf4979f10 100644 --- a/services/app/apps/codebattle/assets/js/widgets/pages/tournamentPlayer/TournamentPlayer.jsx +++ b/services/app/apps/codebattle/assets/js/widgets/pages/tournamentPlayer/TournamentPlayer.jsx @@ -5,6 +5,7 @@ import { useInterpret } from '@xstate/react'; import cn from 'classnames'; import groupBy from 'lodash/groupBy'; import reverse from 'lodash/reverse'; +import sum from 'lodash/sum'; import { useDispatch, useSelector } from 'react-redux'; import CountdownTimer from '@/components/CountdownTimer'; @@ -23,19 +24,83 @@ import TaskAssignment from '../game/TaskAssignment'; import SpectatorEditor from './SpectatorEditor'; -const WinnerStatus = ({ playerId, matches }) => { - const playerWinMatches = matches.filter(match => match.state === MatchStatesCodes.gameOver && playerId === match.winnerId); - const opponentWinMatches = matches.filter(match => match.state === MatchStatesCodes.gameOver && playerId !== match.winnerId); +const RoundStatus = ({ playerId, matches }) => { + const finishedMatches = matches.filter(match => match.playerResults[playerId]); + const matchesCount = finishedMatches.length; + + const opponentId = matches[0].playerIds.find(id => (id !== playerId)); + const playerWinMatches = matches.filter(match => playerId === match.winnerId); + const opponentWinMatches = matches.filter(match => opponentId === match.winnerId); + + const playerScore = sum(finishedMatches.map(match => match.playerResults[playerId]?.score || 0)); + const opponnentScore = sum(finishedMatches.map(match => match.playerResults[opponentId]?.score || 0)); + + const playerAvgTests = matchesCount !== 0 ? sum(finishedMatches.map(match => match.playerResults[playerId]?.resultPercent || 0)) / matchesCount : 0; + const opponnentAvgTests = matchesCount !== 0 ? sum(finishedMatches.map(match => match.playerResults[opponentId]?.resultPercent || 0)) / matchesCount : 0; + + const playerAvgDuration = matchesCount !== 0 ? sum(finishedMatches.map(match => match.playerResults[playerId]?.durationSec || 0)) / matchesCount : 0; + const opponnentAvgDuration = matchesCount !== 0 ? sum(finishedMatches.map(match => match.playerResults[opponentId]?.durationSec || 0)) / matchesCount : 0; + + const RoundStatistics = () => ( +
+
+ + {'Wins: '} + {playerWinMatches.length} + + + {'Score: '} + {playerScore} + + + {`AVG Tests: ${playerAvgTests}%`} + + + {'AVG Duration: '} + {playerAvgDuration} + {' sec'} + +
+
+ ); + const RoundResultIcon = () => { + if ((playerWinMatches.length === opponentWinMatches.length) + && (playerScore === opponnentScore) + && (playerAvgTests === opponnentAvgTests) + && (playerAvgDuration === opponnentAvgDuration) + ) { + return ; + } - if (playerWinMatches.length > opponentWinMatches.length) { - return ; - } + if ( + (playerWinMatches.length > opponentWinMatches.length) + || ( + (playerWinMatches.length === opponentWinMatches.length) + && (playerScore > opponnentScore) + ) + || ( + (playerWinMatches.length === opponentWinMatches.length) + && (playerScore === opponnentScore) + && (playerAvgTests > opponnentAvgTests) + ) + || ((playerWinMatches.length === opponentWinMatches.length) + && (playerScore === opponnentScore) + && (playerAvgTests === opponnentAvgTests) + && (playerAvgDuration > opponnentAvgDuration) + ) + ) { + return ; + } - if (playerWinMatches.length < opponentWinMatches.length) { return ; - } + }; - return ; + return ( +
+ + +
+ ); }; const getMatchIcon = (playerId, match) => { @@ -202,29 +267,31 @@ function TournamentPlayer({

-

{groupedMatches[lastRound].map(match => (
{getMatchIcon(playerId, match)} - {match.state === MatchStatesCodes.gameOver && ( - - - {tournament.gameResults[match.gameId][playerId].durationSec || '0'} + {match.playerResults[playerId] ? ( +
+ + {'Duration: '} + {match.playerResults[playerId].durationSec} {' sec'} - {/* */} - {/* {'Score: '} */} - {/* {tournament.gameResults[match.gameId][playerId].resultPercent} */} - {/* */} - - )} - {match.state === MatchStatesCodes.timeout && ( + + {'Score: '} + {match.playerResults[playerId].score} + + + {`Tests: ${match.playerResults[playerId].resultPercent}%`} + +
+ ) : ( ¯\_(ツ)_/¯ )}
diff --git a/services/app/apps/codebattle/lib/codebattle/game/fsm.ex b/services/app/apps/codebattle/lib/codebattle/game/fsm.ex index bc0e58f68..fba3f8265 100644 --- a/services/app/apps/codebattle/lib/codebattle/game/fsm.ex +++ b/services/app/apps/codebattle/lib/codebattle/game/fsm.ex @@ -88,6 +88,7 @@ defmodule Codebattle.Game.Fsm do def transition(:timeout, game = %{state: s, players: players}, _params) when s in ["waiting_opponent", "playing"] do new_players = Enum.map(players, fn player -> %{player | result: "timeout"} end) + {:ok, %{game | state: "timeout", players: new_players}} end diff --git a/services/app/apps/codebattle/lib/codebattle/game/helpers.ex b/services/app/apps/codebattle/lib/codebattle/game/helpers.ex index aed86f890..c0e2f2909 100644 --- a/services/app/apps/codebattle/lib/codebattle/game/helpers.ex +++ b/services/app/apps/codebattle/lib/codebattle/game/helpers.ex @@ -1,6 +1,13 @@ defmodule Codebattle.Game.Helpers do @moduledoc false + @game_level_score %{ + "elementary" => 5, + "easy" => 8, + "medium" => 13, + "hard" => 21 + } + def get_state(game), do: game.state def get_game_id(game), do: game.id def get_tournament_id(game), do: game.tournament_id @@ -53,7 +60,21 @@ defmodule Codebattle.Game.Helpers do def get_player_results(game) do game |> get_players - |> Enum.map(&{&1.id, Map.take(&1, [:id, :result, :duration_sec, :result_percent])}) + |> Enum.map(fn player -> + duration_sec = + case {player.result, player.duration_sec} do + {"won", seconds} -> seconds + _ -> game.timeout_seconds + end + + result = + player + |> Map.take([:id, :result, :duration_sec, :result_percent]) + |> Map.put(:duration_sec, duration_sec) + |> Map.put(:score, get_player_score(player, game.level, game.timeout_seconds)) + + {player.id, result} + end) |> Enum.into(%{}) end @@ -115,4 +136,31 @@ defmodule Codebattle.Game.Helpers do |> Kernel.!() |> Kernel.!() end + + def get_player_score(player, game_level, timeout_seconds) do + # game_level_score is fibanachi based score for different task level + # %{"elementary" => 5, "easy" => 8, "medium" => 13, "hard" => 21} + game_level_score = @game_level_score[game_level] + + # base_winner_score = game_level_score / 2 for winner and 0 if user haven't won the match + base_winner_score = + if player.result == "won", do: @game_level_score[game_level] / 2, else: 0 + + # test_count_k is a koefficient between [0, 1] + # which linearly grow as test results + test_count_k = player.result_percent / 100.0 + + # duration_k is a koefficient between [0.33, 1] + # duration_k = 0 if duration_sec is nil + # duration_k = 1 if task was solved before 1/3 of match_timeout + # duration_k linearly goes to 0.33 if task was solved after 1/3 of match time + duration_k = + cond do + is_nil(player.duration_sec) -> 1 + player.duration_sec / timeout_seconds < 0.33 -> 1 + true -> 1.32 - player.duration_sec / timeout_seconds + end + + round(base_winner_score + game_level_score * duration_k * test_count_k) + end end diff --git a/services/app/apps/codebattle/lib/codebattle/tournament/match.ex b/services/app/apps/codebattle/lib/codebattle/tournament/match.ex index 27abefb5f..0298f5948 100644 --- a/services/app/apps/codebattle/lib/codebattle/tournament/match.ex +++ b/services/app/apps/codebattle/lib/codebattle/tournament/match.ex @@ -14,7 +14,7 @@ defmodule Codebattle.Tournament.Match do field(:id, :integer) field(:game_id, :integer) field(:player_ids, {:array, :integer}, default: []) - field(:player_results, AtomizedMap, default: []) + field(:player_results, AtomizedMap, default: %{}) field(:round, :integer) field(:state, :string) field(:winner_id, :integer) diff --git a/services/app/apps/codebattle/lib/codebattle/tournament/strategy/base.ex b/services/app/apps/codebattle/lib/codebattle/tournament/strategy/base.ex index e0e695966..2c172c075 100644 --- a/services/app/apps/codebattle/lib/codebattle/tournament/strategy/base.ex +++ b/services/app/apps/codebattle/lib/codebattle/tournament/strategy/base.ex @@ -18,13 +18,6 @@ defmodule Codebattle.Tournament.Base do @behaviour Tournament.Base import Tournament.Helpers - @game_level_score %{ - "elementary" => 5, - "easy" => 8, - "medium" => 13, - "hard" => 21 - } - def add_player(tournament, player) do update_in(tournament.players, fn players -> Map.put(players, to_id(player.id), Tournament.Player.new!(player)) @@ -144,7 +137,12 @@ defmodule Codebattle.Tournament.Base do tournament = update_in( tournament.matches[to_id(params.ref)], - &%{&1 | state: params.game_state, winner_id: winner_id} + &%{ + &1 + | state: params.game_state, + winner_id: winner_id, + player_results: params.player_results + } ) params.player_results @@ -157,13 +155,7 @@ defmodule Codebattle.Tournament.Base do tournament.players[to_id(player_result.id)], &%{ &1 - | score: - &1.score + - get_score( - player_result, - params.game_level, - tournament.match_timeout_seconds - ), + | score: &1.score + player_result.score, wins_count: &1.wins_count + if(player_result.result == "won", do: 1, else: 0) } ) @@ -394,33 +386,6 @@ defmodule Codebattle.Tournament.Base do tournament end - def get_score(player_result, game_level, tournament_match_timeout_seconds) do - # game_level_score is fibanachi based score for different task level - # %{"elementary" => 5, "easy" => 8, "medium" => 13, "hard" => 21} - game_level_score = @game_level_score[game_level] - - # base_winner_score = game_level_score / 2 for winner and 0 if user haven't won the match - base_winner_score = - if player_result.result == "won", do: @game_level_score[game_level] / 2, else: 0 - - # test_count_k is a koefficient between [0, 1] - # which linearly grow as test results - test_count_k = player_result.result_percent / 100.0 - - # duration_k is a koefficient between [0.33, 1] - # duration_k = 0 if duration_sec is nil - # duration_k = 1 if task was solved before 1/3 of match_timeout - # duration_k linearly goes to 0.33 if task was solved after 1/3 of match time - duration_k = - cond do - is_nil(player_result.duration_sec) -> 1 - player_result.duration_sec / tournament_match_timeout_seconds < 0.33 -> 1 - true -> 1.32 - player_result.duration_sec / tournament_match_timeout_seconds - end - - round(base_winner_score + game_level_score * duration_k * test_count_k) - end - defp round_ends_by_time?(%{type: "swiss"}), do: true defp round_ends_by_time?(_), do: false diff --git a/services/app/apps/codebattle/lib/codebattle_web/channels/tournament_player_channel.ex b/services/app/apps/codebattle/lib/codebattle_web/channels/tournament_player_channel.ex index 1e7da832d..831721726 100644 --- a/services/app/apps/codebattle/lib/codebattle_web/channels/tournament_player_channel.ex +++ b/services/app/apps/codebattle/lib/codebattle_web/channels/tournament_player_channel.ex @@ -21,24 +21,13 @@ defmodule CodebattleWeb.TournamentPlayerChannel do matches = Helpers.get_matches_by_players(tournament, [player_id]) - # TODO: Fix player_matches (no return default value: []) - game_results = - matches - |> Enum.map( - &(Game.Context.get_game!(&1.game_id) - |> Game.Helpers.get_player_results() - |> create_game_results(&1.game_id)) - ) - |> merge_results() - {:ok, %{ game_id: game_id, tournament_id: tournament_id, state: tournament.state, break_state: tournament.break_state, - matches: matches, - game_results: game_results + matches: matches }, assign(socket, tournament_id: tournament_id, player_id: player_id)} else _ -> @@ -63,20 +52,10 @@ defmodule CodebattleWeb.TournamentPlayerChannel do matches = Enum.filter(payload.matches, &Helpers.is_match_player?(&1, socket.assigns.player_id)) - game_results = - matches - |> Enum.map( - &(Game.Context.get_game!(&1.game_id) - |> Game.Helpers.get_player_results() - |> create_game_results(&1.game_id)) - ) - |> merge_results() - push(socket, "tournament:round_finished", %{ state: payload.state, break_state: payload.break_state, - matches: matches, - game_results: game_results + matches: matches }) {:noreply, socket}