@@ -62,7 +62,8 @@ defmodule Codebattle.Tournament.TournamentResult do
6262 def upsert_results ( % { type: type , ranking_type: "by_user" , score_strategy: "75_percentile" } = tournament )
6363 when type in [ "swiss" , "top200" ] do
6464 round_position = tournament . current_round_position || 0
65- cheated_game_sql = cheated_game_sql ( "g" , tournament )
65+ player_cheater_sql = player_cheater_sql ( tournament )
66+ game_has_cheater_sql = game_has_cheater_sql ( "g" , tournament )
6667 clean_results ( tournament . id , round_position )
6768
6869 Repo . query! ( """
@@ -82,7 +83,8 @@ defmodule Codebattle.Tournament.TournamentResult do
8283 and COALESCE(round_position, 0) = #{ round_position }
8384 and state = 'game_over'
8485 and bot_won = FALSE
85- and not (#{ cheated_game_sql } )
86+ and not COALESCE(g.was_cheated, FALSE)
87+ and not (#{ cheater_won_game_sql ( "g" , tournament ) } )
8688 GROUP BY
8789 task_id, level),
8890 stats as (
@@ -96,16 +98,21 @@ defmodule Codebattle.Tournament.TournamentResult do
9698 g.tournament_id,
9799 g.id as game_id,
98100 COALESCE(g.round_position, 0) as round_position,
99- (#{ cheated_game_sql } ) AS was_cheated,
101+ (COALESCE(g.was_cheated, FALSE) OR ( #{ player_cheater_sql } ) ) AS was_cheated,
100102 COALESCE(dt.base_score, 0) AS base_score,
101103 COALESCE(dt.min_duration, 0) AS min_duration,
102104 COALESCE(dt.max_duration, 0) AS max_duration,
103105 CASE
104- WHEN (#{ cheated_game_sql } ) THEN
106+ -- Game-level cheated flag or this player is a cheater
107+ WHEN COALESCE(g.was_cheated, FALSE) OR (#{ player_cheater_sql } ) THEN
105108 0
106- -- Handle timeout case where both players lost
109+ -- Opponent of cheater who lost (cheater won) — give base_score as compensation
110+ WHEN (#{ game_has_cheater_sql } ) AND g.state = 'game_over' AND (p.player_info->>'result_percent')::numeric < 100.0 THEN
111+ COALESCE(dt.base_score, 0)
112+ -- Handle timeout case where both players lost (including opponent of cheater in timeout)
107113 WHEN g.state = 'timeout' THEN
108114 0.5 * COALESCE(dt.base_score, 0) * COALESCE((p.player_info->>'result_percent')::numeric, 0) / 100.0
115+ -- Normal scoring (includes opponent of cheater who won — gets normal win score)
109116 ELSE
110117 COALESCE(dt.base_score, 0) * (
111118 2 - ((g.duration_sec - COALESCE(dt.min_duration, 0))::numeric / GREATEST(COALESCE(dt.max_duration - COALESCE(dt.min_duration, 0), 1), 1))
@@ -163,7 +170,8 @@ defmodule Codebattle.Tournament.TournamentResult do
163170 def upsert_results ( % { type: type , ranking_type: "by_user" , score_strategy: "win_loss" } = tournament )
164171 when type in [ "swiss" ] do
165172 round_position = tournament . current_round_position || 0
166- cheated_game_sql = cheated_game_sql ( "g" , tournament )
173+ player_cheater_sql = player_cheater_sql ( tournament )
174+ game_has_cheater_sql = game_has_cheater_sql ( "g" , tournament )
167175 clean_results ( tournament . id , round_position )
168176
169177 Repo . query! ( """
@@ -180,9 +188,11 @@ defmodule Codebattle.Tournament.TournamentResult do
180188 g.task_id,
181189 g.id as game_id,
182190 COALESCE(g.round_position, 0) as round_position,
183- (#{ cheated_game_sql } ) AS was_cheated,
191+ (COALESCE(g.was_cheated, FALSE) OR ( #{ player_cheater_sql } ) ) AS was_cheated,
184192 CASE
185- WHEN (#{ cheated_game_sql } ) THEN 0
193+ WHEN COALESCE(g.was_cheated, FALSE) OR (#{ player_cheater_sql } ) THEN 0
194+ WHEN (#{ game_has_cheater_sql } ) AND g.state = 'game_over'
195+ THEN #{ @ win_score }
186196 WHEN (p.player_info->'result_percent')::numeric = 100.0
187197 THEN #{ @ win_score }
188198 ELSE #{ @ loss_score }
@@ -255,21 +265,41 @@ defmodule Codebattle.Tournament.TournamentResult do
255265 |> Repo . delete_all ( )
256266 end
257267
258- defp cheated_game_sql ( game_alias , % { cheater_ids: [ ] } ) , do: "COALESCE(#{ game_alias } .was_cheated, FALSE)"
259- defp cheated_game_sql ( game_alias , % { cheater_ids: nil } ) , do: "COALESCE(#{ game_alias } .was_cheated, FALSE)"
268+ # Checks if the winner of the game is a cheater (for excluding from percentile calculation)
269+ defp cheater_won_game_sql ( _game_alias , % { cheater_ids: ids } ) when ids in [ [ ] , nil ] , do: "FALSE"
270+
271+ defp cheater_won_game_sql ( game_alias , % { cheater_ids: cheater_ids } ) do
272+ cheater_ids_sql = cheater_ids |> Enum . uniq ( ) |> Enum . join ( "," )
273+
274+ """
275+ exists (
276+ select 1
277+ from jsonb_array_elements(#{ game_alias } .players) as wp(player_info)
278+ where (wp.player_info->>'result_percent')::numeric = 100.0
279+ and (wp.player_info->>'id')::integer = any(array[#{ cheater_ids_sql } ]::integer[])
280+ )
281+ """
282+ end
283+
284+ # Checks if THIS specific player (from lateral join) is a cheater
285+ defp player_cheater_sql ( % { cheater_ids: ids } ) when ids in [ [ ] , nil ] , do: "FALSE"
286+
287+ defp player_cheater_sql ( % { cheater_ids: cheater_ids } ) do
288+ cheater_ids_sql = cheater_ids |> Enum . uniq ( ) |> Enum . join ( "," )
289+ "(p.player_info->>'id')::integer = any(array[#{ cheater_ids_sql } ]::integer[])"
290+ end
291+
292+ # Checks if the game has any identified cheater (by cheater_ids only, not game-level flag)
293+ defp game_has_cheater_sql ( _game_alias , % { cheater_ids: ids } ) when ids in [ [ ] , nil ] , do: "FALSE"
260294
261- defp cheated_game_sql ( game_alias , % { cheater_ids: cheater_ids } ) do
262- cheater_ids_sql =
263- cheater_ids
264- |> Enum . uniq ( )
265- |> Enum . join ( "," )
295+ defp game_has_cheater_sql ( game_alias , % { cheater_ids: cheater_ids } ) do
296+ cheater_ids_sql = cheater_ids |> Enum . uniq ( ) |> Enum . join ( "," )
266297
267298 """
268- COALESCE(#{ game_alias } .was_cheated, FALSE)
269- or exists (
299+ exists (
270300 select 1
271- from jsonb_array_elements(#{ game_alias } .players) as cheater_player (player_info)
272- where (cheater_player .player_info->>'id')::integer = any(array[#{ cheater_ids_sql } ]::integer[])
301+ from jsonb_array_elements(#{ game_alias } .players) as cp (player_info)
302+ where (cp .player_info->>'id')::integer = any(array[#{ cheater_ids_sql } ]::integer[])
273303 )
274304 """
275305 end
0 commit comments