Skip to content

Commit 917c854

Browse files
Add configurable time control sliders (Lichess-style)
Co-authored-by: kevinjosethomas <46242684+kevinjosethomas@users.noreply.github.com>
1 parent d640d95 commit 917c854

4 files changed

Lines changed: 215 additions & 9 deletions

File tree

src/components/Common/PlaySetupModal.tsx

Lines changed: 70 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ import {
1414
} from 'src/types'
1515
import { ModalContext } from 'src/contexts'
1616
import { ModalContainer } from './ModalContainer'
17+
import { Slider } from './Slider'
18+
import {
19+
customToPresetTimeControl,
20+
parseTimeControl,
21+
getPresetOptions,
22+
} from 'src/lib/timeControlUtils'
1723

1824
const maiaOptions = [
1925
'maia_kdd_1100',
@@ -82,6 +88,9 @@ export const PlaySetupModal: React.FC<Props> = (props: Props) => {
8288
const [timeControl, setTimeControl] = useState<TimeControl>(
8389
props.timeControl || TimeControlOptions[0],
8490
)
91+
const [useCustomTime, setUseCustomTime] = useState<boolean>(false)
92+
const [customMinutes, setCustomMinutes] = useState<number>(10)
93+
const [customIncrement, setCustomIncrement] = useState<number>(0)
8594
const [isBrain, setIsBrain] = useState<boolean>(props.isBrain || false)
8695
const [sampleMoves, setSampleMoves] = useState<boolean>(
8796
props.sampleMoves || true,
@@ -102,9 +111,18 @@ export const PlaySetupModal: React.FC<Props> = (props: Props) => {
102111

103112
const [openMoreOptions, setMoreOptionsOpen] = useState<boolean>(true)
104113

114+
// Helper function to get the effective time control
115+
const getEffectiveTimeControl = (): TimeControl => {
116+
if (useCustomTime) {
117+
return customToPresetTimeControl(customMinutes, customIncrement)
118+
}
119+
return timeControl
120+
}
121+
105122
const start = useCallback(
106123
(color: Color | undefined) => {
107124
const player = color ?? ['white', 'black'][Math.floor(Math.random() * 2)]
125+
const effectiveTimeControl = getEffectiveTimeControl()
108126

109127
if (fen && !new Chess().validateFen(fen).valid) {
110128
toast.error('Invalid Starting FEN provided')
@@ -120,7 +138,7 @@ export const PlaySetupModal: React.FC<Props> = (props: Props) => {
120138
player: player,
121139
//maiaPartnerVersion: maiaPartnerVersion,
122140
maiaVersion: maiaVersion,
123-
timeControl: timeControl,
141+
timeControl: effectiveTimeControl,
124142
sampleMoves: sampleMoves,
125143
simulateMaiaTime: simulateMaiaTime,
126144
startFen: fen,
@@ -133,7 +151,7 @@ export const PlaySetupModal: React.FC<Props> = (props: Props) => {
133151
player: player,
134152
maiaPartnerVersion: maiaPartnerVersion,
135153
maiaVersion: maiaVersion,
136-
timeControl: timeControl,
154+
timeControl: effectiveTimeControl,
137155
isBrain: isBrain,
138156
sampleMoves: sampleMoves,
139157
simulateMaiaTime: simulateMaiaTime,
@@ -148,7 +166,7 @@ export const PlaySetupModal: React.FC<Props> = (props: Props) => {
148166
push,
149167
maiaPartnerVersion,
150168
maiaVersion,
151-
timeControl,
169+
getEffectiveTimeControl,
152170
sampleMoves,
153171
simulateMaiaTime,
154172
fen,
@@ -250,18 +268,61 @@ export const PlaySetupModal: React.FC<Props> = (props: Props) => {
250268
<div>
251269
<label
252270
htmlFor="time-control-select"
253-
className="mb-1 block text-sm font-medium text-primary"
271+
className="mb-2 block text-sm font-medium text-primary"
254272
>
255273
Time control:
256274
</label>
257-
<div id="time-control-select">
275+
276+
{/* Toggle between preset and custom */}
277+
<div className="mb-3">
258278
<OptionSelect
259-
options={TimeControlOptions}
260-
labels={TimeControlOptionNames}
261-
selected={timeControl}
262-
onChange={setTimeControl}
279+
options={[false, true]}
280+
labels={['Preset', 'Custom']}
281+
selected={useCustomTime}
282+
onChange={setUseCustomTime}
263283
/>
264284
</div>
285+
286+
{useCustomTime ? (
287+
/* Custom time controls with sliders */
288+
<div className="space-y-3 rounded bg-background-2 p-3">
289+
<Slider
290+
id="time-slider"
291+
label="Time per side"
292+
value={customMinutes}
293+
min={1}
294+
max={180}
295+
step={1}
296+
unit=" min"
297+
onChange={setCustomMinutes}
298+
/>
299+
<Slider
300+
id="increment-slider"
301+
label="Increment per move"
302+
value={customIncrement}
303+
min={0}
304+
max={60}
305+
step={1}
306+
unit=" sec"
307+
onChange={setCustomIncrement}
308+
/>
309+
<div className="mt-2 text-center">
310+
<span className="text-xs text-secondary">
311+
Time control: {customMinutes}+{customIncrement}
312+
</span>
313+
</div>
314+
</div>
315+
) : (
316+
/* Preset time controls */
317+
<div id="time-control-select">
318+
<OptionSelect
319+
options={TimeControlOptions}
320+
labels={TimeControlOptionNames}
321+
selected={timeControl}
322+
onChange={setTimeControl}
323+
/>
324+
</div>
325+
)}
265326
</div>
266327

267328
<div>

src/components/Common/Slider.tsx

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import React from 'react'
2+
3+
interface SliderProps {
4+
label: string
5+
value: number
6+
min: number
7+
max: number
8+
step?: number
9+
unit?: string
10+
onChange: (value: number) => void
11+
id?: string
12+
}
13+
14+
export const Slider: React.FC<SliderProps> = ({
15+
label,
16+
value,
17+
min,
18+
max,
19+
step = 1,
20+
unit = '',
21+
onChange,
22+
id,
23+
}) => {
24+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
25+
onChange(Number(e.target.value))
26+
}
27+
28+
return (
29+
<div className="flex flex-col gap-2">
30+
<div className="flex items-center justify-between">
31+
<label htmlFor={id} className="text-sm font-medium text-primary">
32+
{label}
33+
</label>
34+
<span className="text-sm text-secondary">
35+
{value}
36+
{unit}
37+
</span>
38+
</div>
39+
<div className="relative">
40+
<input
41+
id={id}
42+
type="range"
43+
min={min}
44+
max={max}
45+
step={step}
46+
value={value}
47+
onChange={handleChange}
48+
className="h-2 w-full cursor-pointer appearance-none rounded-lg bg-background-3 outline-none"
49+
style={{
50+
background: `linear-gradient(to right, rgb(var(--color-human-accent4)) 0%, rgb(var(--color-human-accent4)) ${
51+
((value - min) / (max - min)) * 100
52+
}%, rgb(var(--color-background3)) ${
53+
((value - min) / (max - min)) * 100
54+
}%, rgb(var(--color-background3)) 100%)`,
55+
}}
56+
/>
57+
<style jsx>{`
58+
input[type='range']::-webkit-slider-thumb {
59+
appearance: none;
60+
height: 18px;
61+
width: 18px;
62+
border-radius: 50%;
63+
background: rgb(var(--color-human-accent4));
64+
cursor: pointer;
65+
border: 2px solid rgb(var(--color-background1));
66+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
67+
}
68+
input[type='range']::-moz-range-thumb {
69+
height: 18px;
70+
width: 18px;
71+
border-radius: 50%;
72+
background: rgb(var(--color-human-accent4));
73+
cursor: pointer;
74+
border: 2px solid rgb(var(--color-background1));
75+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
76+
}
77+
`}</style>
78+
</div>
79+
</div>
80+
)
81+
}

src/components/Common/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@ export * from './ModalContainer'
1717
export * from './ContinueAgainstMaia'
1818
export * from './AnimatedNumber'
1919
export * from './DownloadModelModal'
20+
export * from './Slider'

src/lib/timeControlUtils.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/**
2+
* Utility functions for time control conversion between custom values and preset formats
3+
*/
4+
5+
import { TimeControl, TimeControlOptions } from 'src/types'
6+
7+
export interface CustomTimeControl {
8+
minutes: number
9+
increment: number
10+
}
11+
12+
/**
13+
* Convert custom time control values to the closest preset TimeControl format
14+
*/
15+
export const customToPresetTimeControl = (
16+
minutes: number,
17+
increment: number,
18+
): TimeControl => {
19+
const customFormat = `${minutes}+${increment}`
20+
21+
// Check if it matches any existing preset
22+
if (TimeControlOptions.includes(customFormat as TimeControl)) {
23+
return customFormat as TimeControl
24+
}
25+
26+
// For custom values that don't match presets, return the custom format
27+
// The game logic will need to handle this appropriately
28+
return customFormat as TimeControl
29+
}
30+
31+
/**
32+
* Parse a TimeControl string into custom time control values
33+
*/
34+
export const parseTimeControl = (
35+
timeControl: TimeControl,
36+
): CustomTimeControl => {
37+
if (timeControl === 'unlimited') {
38+
return { minutes: 0, increment: 0 }
39+
}
40+
41+
const [minutesStr, incrementStr] = timeControl.split('+')
42+
return {
43+
minutes: parseInt(minutesStr, 10) || 0,
44+
increment: parseInt(incrementStr, 10) || 0,
45+
}
46+
}
47+
48+
/**
49+
* Check if a time control is a preset option
50+
*/
51+
export const isPresetTimeControl = (timeControl: TimeControl): boolean => {
52+
return TimeControlOptions.includes(timeControl)
53+
}
54+
55+
/**
56+
* Get preset time control options with labels
57+
*/
58+
export const getPresetOptions = () => {
59+
return TimeControlOptions.map((option) => ({
60+
value: option,
61+
label: option === 'unlimited' ? 'Unlimited' : option,
62+
}))
63+
}

0 commit comments

Comments
 (0)