Skip to content

Commit 10e2ec2

Browse files
committed
Improve tournaments forms
1 parent e3053d9 commit 10e2ec2

15 files changed

Lines changed: 829 additions & 534 deletions

File tree

apps/codebattle/assets/js/widgets/pages/tournament/EditTournament.jsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ function EditTournament({ tournamentId, taskPackNames = [], userTimezone = "UTC"
140140
return (
141141
<div
142142
className="w-100 mx-auto cb-bg-panel cb-text shadow-sm cb-rounded py-4 px-3 px-md-4 mb-3"
143-
style={{ maxWidth: "1100px" }}
143+
style={{ maxWidth: "1400px" }}
144144
>
145145
<div
146146
className="d-flex justify-content-center align-items-center"
@@ -156,7 +156,7 @@ function EditTournament({ tournamentId, taskPackNames = [], userTimezone = "UTC"
156156
return (
157157
<div
158158
className="w-100 mx-auto cb-bg-panel cb-text shadow-sm cb-rounded py-4 px-3 px-md-4 mb-3"
159-
style={{ maxWidth: "1100px" }}
159+
style={{ maxWidth: "1400px" }}
160160
>
161161
<div className="alert alert-danger" role="alert">
162162
Tournament not found or you don&apos;t have permission to edit it.
@@ -195,6 +195,7 @@ function EditTournament({ tournamentId, taskPackNames = [], userTimezone = "UTC"
195195
rounds_limit: tournament.roundsLimit || 7,
196196
timeout_mode: tournament.timeoutMode || "per_task",
197197
round_timeout_seconds: tournament.roundTimeoutSeconds ?? null,
198+
tournament_timeout_seconds: tournament.tournamentTimeoutSeconds ?? null,
198199
break_duration_seconds: tournament.breakDurationSeconds || 42,
199200
use_chat: tournament.useChat !== undefined ? tournament.useChat : true,
200201
use_clan: tournament.useClan !== undefined ? tournament.useClan : false,
@@ -208,15 +209,15 @@ function EditTournament({ tournamentId, taskPackNames = [], userTimezone = "UTC"
208209
return (
209210
<div
210211
className="w-100 mx-auto cb-bg-panel cb-text shadow-sm cb-rounded py-4 px-3 px-md-4 mb-3"
211-
style={{ maxWidth: "1100px" }}
212+
style={{ maxWidth: "1400px" }}
212213
>
213214
<Notification notification={notification} onClose={setNotification} />
214215
<h1 className="text-center mb-2">Edit Tournament</h1>
215216
<h3 className="text-center mb-4 text-muted">
216217
{tournament.creator && <>Creator: {tournament.creator.name}</>}
217218
</h3>
218219
<div className="row justify-content-center">
219-
<div className="col-12 col-md-10 col-lg-8 col-xl-7">
220+
<div className="col-12 col-lg-10 col-xl-10">
220221
<TournamentForm
221222
initialValues={initialValues}
222223
onSubmit={handleSubmit}

apps/codebattle/assets/js/widgets/pages/tournament/TournamentForm.jsx

Lines changed: 114 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,21 @@ const SCORE_STRATEGIES = [
3838

3939
const TIMEOUT_MODES = [
4040
{ value: "per_task", label: "Per task timeout" },
41-
{ value: "per_round", label: "Per round timeout" },
41+
{ value: "per_round_fixed", label: "Per round (fixed)" },
42+
{ value: "per_round_with_rematch", label: "Per round (with rematch)" },
43+
{ value: "per_tournament", label: "Per tournament timeout" },
4244
];
4345

46+
const TIMEOUT_DESCRIPTIONS = {
47+
per_task:
48+
"Each game uses the task's own time limit. Different tasks may have different timeouts.",
49+
per_round_fixed: "All games in a round share a fixed timeout. One task per round.",
50+
per_round_with_rematch:
51+
"Each round has a fixed timeout. Players play multiple tasks (rematches) within the round until time runs out.",
52+
per_tournament:
53+
"One global timeout for the entire tournament. Games use the remaining tournament time. Tournament ends automatically when time expires.",
54+
};
55+
4456
const PLAYERS_LIMITS = [2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384];
4557

4658
function TournamentForm({
@@ -71,6 +83,7 @@ function TournamentForm({
7183
rounds_limit: initialValues.rounds_limit || 7,
7284
timeout_mode: initialValues.timeout_mode || "per_task",
7385
round_timeout_seconds: initialValues.round_timeout_seconds ?? 177,
86+
tournament_timeout_seconds: initialValues.tournament_timeout_seconds ?? 3600,
7487
break_duration_seconds: initialValues.break_duration_seconds || 42,
7588
use_chat: initialValues.use_chat !== undefined ? initialValues.use_chat : true,
7689
use_clan: initialValues.use_clan !== undefined ? initialValues.use_clan : false,
@@ -106,8 +119,13 @@ function TournamentForm({
106119
.split(/[\s,]+/)
107120
.map((id) => id.trim())
108121
.filter(Boolean);
109-
payload.round_timeout_seconds =
110-
formData.timeout_mode === "per_round" ? formData.round_timeout_seconds : null;
122+
payload.round_timeout_seconds = ["per_round_fixed", "per_round_with_rematch"].includes(
123+
formData.timeout_mode,
124+
)
125+
? formData.round_timeout_seconds
126+
: null;
127+
payload.tournament_timeout_seconds =
128+
formData.timeout_mode === "per_tournament" ? formData.tournament_timeout_seconds : null;
111129

112130
onSubmit(payload);
113131
},
@@ -535,16 +553,44 @@ function TournamentForm({
535553
{renderError("rounds_limit")}
536554
</div>
537555

556+
<div className="col-md-4 mb-3">
557+
<label htmlFor="break_duration_seconds" className="form-label text-white">
558+
Break Duration (seconds)
559+
</label>
560+
<input
561+
type="number"
562+
id="break_duration_seconds"
563+
name="break_duration_seconds"
564+
className={cn("form-control cb-bg-panel cb-border-color text-white cb-rounded", {
565+
"is-invalid": errors.break_duration_seconds,
566+
})}
567+
value={formData.break_duration_seconds}
568+
onChange={handleChange}
569+
min={0}
570+
max={100000}
571+
/>
572+
{renderError("break_duration_seconds")}
573+
</div>
574+
</div>
575+
</div>
576+
</div>
577+
578+
{/* Timeout Configuration Section */}
579+
<div className="card cb-card mb-4">
580+
<div className="card-header">
581+
<h5 className="mb-0">Timeout Configuration</h5>
582+
</div>
583+
<div className="card-body">
584+
<p className="text-muted small mb-3">{TIMEOUT_DESCRIPTIONS[formData.timeout_mode]}</p>
585+
<div className="row">
538586
<div className="col-md-4 mb-3">
539587
<label htmlFor="timeout_mode" className="form-label text-white">
540588
Timeout Mode
541589
</label>
542590
<select
543591
id="timeout_mode"
544592
name="timeout_mode"
545-
className={cn(
546-
"form-select custom-select cb-bg-panel cb-border-color text-white cb-rounded",
547-
)}
593+
className="form-select custom-select cb-bg-panel cb-border-color text-white cb-rounded"
548594
value={formData.timeout_mode}
549595
onChange={handleChange}
550596
>
@@ -556,46 +602,70 @@ function TournamentForm({
556602
</select>
557603
</div>
558604

559-
{formData.timeout_mode === "per_round" && (
560-
<div className="col-md-4 mb-3">
561-
<label htmlFor="round_timeout_seconds" className="form-label text-white">
562-
Round Timeout (seconds)
563-
</label>
564-
<input
565-
type="number"
566-
id="round_timeout_seconds"
567-
name="round_timeout_seconds"
568-
className={cn("form-control cb-bg-panel cb-border-color text-white cb-rounded", {
569-
"is-invalid": errors.round_timeout_seconds,
570-
})}
571-
value={formData.round_timeout_seconds}
572-
onChange={handleChange}
573-
min={10}
574-
max={10000}
575-
/>
576-
{renderError("round_timeout_seconds")}
577-
</div>
578-
)}
579-
</div>
605+
<div className="col-md-4 mb-3">
606+
<label
607+
htmlFor="round_timeout_seconds"
608+
className={cn("form-label", {
609+
"text-white": ["per_round_fixed", "per_round_with_rematch"].includes(
610+
formData.timeout_mode,
611+
),
612+
"text-muted": !["per_round_fixed", "per_round_with_rematch"].includes(
613+
formData.timeout_mode,
614+
),
615+
})}
616+
>
617+
Round Timeout (seconds)
618+
</label>
619+
<input
620+
type="number"
621+
id="round_timeout_seconds"
622+
name="round_timeout_seconds"
623+
className={cn("form-control cb-bg-panel cb-border-color text-white cb-rounded", {
624+
"is-invalid": errors.round_timeout_seconds,
625+
})}
626+
value={
627+
["per_round_fixed", "per_round_with_rematch"].includes(formData.timeout_mode)
628+
? formData.round_timeout_seconds
629+
: ""
630+
}
631+
onChange={handleChange}
632+
min={10}
633+
max={10000}
634+
disabled={
635+
!["per_round_fixed", "per_round_with_rematch"].includes(formData.timeout_mode)
636+
}
637+
/>
638+
{renderError("round_timeout_seconds")}
639+
</div>
580640

581-
<div className="row">
582641
<div className="col-md-4 mb-3">
583-
<label htmlFor="break_duration_seconds" className="form-label text-white">
584-
Break Duration (seconds)
642+
<label
643+
htmlFor="tournament_timeout_seconds"
644+
className={cn("form-label", {
645+
"text-white": formData.timeout_mode === "per_tournament",
646+
"text-muted": formData.timeout_mode !== "per_tournament",
647+
})}
648+
>
649+
Tournament Timeout (seconds)
585650
</label>
586651
<input
587652
type="number"
588-
id="break_duration_seconds"
589-
name="break_duration_seconds"
653+
id="tournament_timeout_seconds"
654+
name="tournament_timeout_seconds"
590655
className={cn("form-control cb-bg-panel cb-border-color text-white cb-rounded", {
591-
"is-invalid": errors.break_duration_seconds,
656+
"is-invalid": errors.tournament_timeout_seconds,
592657
})}
593-
value={formData.break_duration_seconds}
658+
value={
659+
formData.timeout_mode === "per_tournament"
660+
? formData.tournament_timeout_seconds
661+
: ""
662+
}
594663
onChange={handleChange}
595-
min={0}
596-
max={100000}
664+
min={60}
665+
max={36000}
666+
disabled={formData.timeout_mode !== "per_tournament"}
597667
/>
598-
{renderError("break_duration_seconds")}
668+
{renderError("tournament_timeout_seconds")}
599669
</div>
600670
</div>
601671
</div>
@@ -664,8 +734,14 @@ TournamentForm.propTypes = {
664734
tags: PropTypes.string,
665735
players_limit: PropTypes.number,
666736
rounds_limit: PropTypes.number,
667-
timeout_mode: PropTypes.oneOf(["per_task", "per_round"]),
737+
timeout_mode: PropTypes.oneOf([
738+
"per_task",
739+
"per_round_fixed",
740+
"per_round_with_rematch",
741+
"per_tournament",
742+
]),
668743
round_timeout_seconds: PropTypes.number,
744+
tournament_timeout_seconds: PropTypes.number,
669745
break_duration_seconds: PropTypes.number,
670746
use_chat: PropTypes.bool,
671747
use_clan: PropTypes.bool,

apps/codebattle/lib/codebattle/tournament/context.ex

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -429,12 +429,28 @@ defmodule Codebattle.Tournament.Context do
429429

430430
timeout_mode =
431431
params[:timeout_mode] ||
432-
if params[:round_timeout_seconds] in [nil, ""], do: "per_task", else: "per_round"
432+
cond do
433+
params[:tournament_timeout_seconds] not in [nil, ""] -> "per_tournament"
434+
params[:round_timeout_seconds] not in [nil, ""] -> "per_round_fixed"
435+
true -> "per_task"
436+
end
433437

434438
params =
435439
case timeout_mode do
436-
"per_task" -> Map.put(params, :round_timeout_seconds, nil)
437-
_ -> params
440+
"per_task" ->
441+
params
442+
|> Map.put(:round_timeout_seconds, nil)
443+
|> Map.put(:tournament_timeout_seconds, nil)
444+
445+
mode when mode in ["per_round_fixed", "per_round_with_rematch"] ->
446+
Map.put(params, :tournament_timeout_seconds, nil)
447+
448+
"per_tournament" ->
449+
Map.put(params, :round_timeout_seconds, nil)
450+
451+
# Pass through unknown modes — changeset validation will reject them
452+
_ ->
453+
params
438454
end
439455

440456
cond_result =

apps/codebattle/lib/codebattle/tournament/helpers.ex

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,8 @@ defmodule Codebattle.Tournament.Helpers do
324324
10
325325
)
326326

327-
tournament.timeout_mode == "per_round" and is_integer(tournament.round_timeout_seconds) ->
327+
tournament.timeout_mode in ["per_round_fixed", "per_round_with_rematch"] and
328+
is_integer(tournament.round_timeout_seconds) ->
328329
tournament.round_timeout_seconds
329330

330331
true ->

apps/codebattle/lib/codebattle/tournament/strategy/base.ex

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -896,13 +896,16 @@ defmodule Codebattle.Tournament.Base do
896896

897897
defp maybe_start_global_timer(tournament), do: tournament
898898

899-
# We don't want to run a timer for the swiss type, because all games already have a timeout
900-
defp maybe_start_round_timer(%{state: "active", type: "swiss"} = tournament), do: tournament
901-
899+
# per_task: no round timer, games timeout individually
902900
defp maybe_start_round_timer(%{timeout_mode: "per_task"} = tournament), do: tournament
903-
904-
defp maybe_start_round_timer(%{state: "active", type: "top200"} = tournament), do: tournament
905-
901+
# per_tournament: global timer handles it
902+
defp maybe_start_round_timer(%{timeout_mode: "per_tournament"} = tournament), do: tournament
903+
# per_round_fixed: swiss/top200 don't need round timer (games have individual timeouts,
904+
# rounds finish when all games complete)
905+
defp maybe_start_round_timer(%{timeout_mode: "per_round_fixed", type: "swiss"} = tournament), do: tournament
906+
defp maybe_start_round_timer(%{timeout_mode: "per_round_fixed", type: "top200"} = tournament), do: tournament
907+
908+
# per_round_with_rematch and per_round_fixed (for show type) need the round timer
906909
defp maybe_start_round_timer(tournament) do
907910
Process.send_after(
908911
self(),
@@ -940,26 +943,31 @@ defmodule Codebattle.Tournament.Base do
940943
end
941944

942945
defp get_round_game_timeout(tournament, task) do
943-
cond do
944-
tournament.tournament_timeout_seconds ->
946+
case tournament.timeout_mode do
947+
"per_tournament" ->
945948
max(
946949
tournament.tournament_timeout_seconds -
947950
DateTime.diff(DateTime.utc_now(), tournament.started_at),
948951
10
949952
)
950953

951-
tournament.timeout_mode == "per_round" and is_integer(tournament.round_timeout_seconds) ->
954+
mode when mode in ["per_round_fixed", "per_round_with_rematch"] ->
952955
tournament.round_timeout_seconds
953956

954-
true ->
957+
_per_task ->
955958
(task && task.time_to_solve_sec) || 300
956959
end
957960
end
958961

962+
defp get_rematch_game_timeout(%{timeout_mode: "per_round_with_rematch"} = tournament) do
963+
elapsed = NaiveDateTime.diff(NaiveDateTime.utc_now(), tournament.last_round_started_at)
964+
max(tournament.round_timeout_seconds - elapsed, 10)
965+
end
966+
959967
defp get_rematch_game_timeout(tournament), do: get_round_timeout_seconds(tournament)
960968

961969
defp get_round_timeout_seconds(tournament) do
962-
if tournament.timeout_mode == "per_round" do
970+
if tournament.timeout_mode in ["per_round_fixed", "per_round_with_rematch"] do
963971
tournament.round_timeout_seconds
964972
else
965973
tournament.match_timeout_seconds

0 commit comments

Comments
 (0)