Skip to content

Commit

Permalink
Improve tournament Stats
Browse files Browse the repository at this point in the history
  • Loading branch information
vtm9 committed Oct 27, 2023
1 parent 9500f74 commit a020186
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 89 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 = () => (
<div className="d-flex text-center align-items-center">
<div className="d-flex flex-column align-items-baseline">
<span className="ml-2 h4">
{'Wins: '}
{playerWinMatches.length}
</span>
<span className="ml-2 h4">
{'Score: '}
{playerScore}
</span>
<span className="ml-2 h4">
{`AVG Tests: ${playerAvgTests}%`}
</span>
<span className="ml-4 h4">
{'AVG Duration: '}
{playerAvgDuration}
{' sec'}
</span>
</div>
</div>
);
const RoundResultIcon = () => {
if ((playerWinMatches.length === opponentWinMatches.length)
&& (playerScore === opponnentScore)
&& (playerAvgTests === opponnentAvgTests)
&& (playerAvgDuration === opponnentAvgDuration)
) {
return <FontAwesomeIcon className="ml-2 text-primary" icon="handshake" />;
}

if (playerWinMatches.length > opponentWinMatches.length) {
return <FontAwesomeIcon className="ml-2 text-winner" icon="trophy" />;
}
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 <FontAwesomeIcon className="ml-2 text-warning" icon="trophy" />;
}

if (playerWinMatches.length < opponentWinMatches.length) {
return <FontAwesomeIcon className="ml-2 text-secondary" icon="trophy" />;
}
};

return <FontAwesomeIcon className="ml-2 text-primary" icon="handshake" />;
return (
<div className="d-flex">
<RoundResultIcon />
<RoundStatistics />
</div>
);
};

const getMatchIcon = (playerId, match) => {
Expand Down Expand Up @@ -202,29 +267,31 @@ function TournamentPlayer({
<div className="card border-0 rounded-lg shadow-sm h-100">
<div className="p-2 d-flex flex-column justify-content-center align-items-center text-center h-100">
<h1 className="mt-2">
<WinnerStatus
<RoundStatus
playerId={playerId}
matches={groupedMatches[lastRound]}
gameResults={tournament.gameResults}
/>
</h1>
<div>
{groupedMatches[lastRound].map(match => (
<div className="d-flex text-center align-items-center" key={match.id}>
<span className="h3">{getMatchIcon(playerId, match)}</span>
{match.state === MatchStatesCodes.gameOver && (
<span>
<span className="ml-4 h3">
{tournament.gameResults[match.gameId][playerId].durationSec || '0'}
{match.playerResults[playerId] ? (
<div className="d-flex flex-column align-items-baseline">
<span className="ml-4 h4">
{'Duration: '}
{match.playerResults[playerId].durationSec}
{' sec'}
</span>
{/* <span className="ml-2 h3"> */}
{/* {'Score: '} */}
{/* {tournament.gameResults[match.gameId][playerId].resultPercent} */}
{/* </span> */}
</span>
)}
{match.state === MatchStatesCodes.timeout && (
<span className="ml-2 h4">
{'Score: '}
{match.playerResults[playerId].score}
</span>
<span className="ml-2 h4">
{`Tests: ${match.playerResults[playerId].resultPercent}%`}
</span>
</div>
) : (
<span className="ml-4 h3">¯\_(ツ)_/¯</span>
)}
</div>
Expand Down
1 change: 1 addition & 0 deletions services/app/apps/codebattle/lib/codebattle/game/fsm.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
50 changes: 49 additions & 1 deletion services/app/apps/codebattle/lib/codebattle/game/helpers.ex
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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
Expand All @@ -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)
}
)
Expand Down Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
_ ->
Expand All @@ -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}
Expand Down

0 comments on commit a020186

Please sign in to comment.