Skip to content

Commit 3d3c8a1

Browse files
feat: added mvp openings page
1 parent 3150525 commit 3d3c8a1

File tree

9 files changed

+915
-0
lines changed

9 files changed

+915
-0
lines changed
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/* eslint-disable jsx-a11y/click-events-have-key-events */
2+
import Chessground from '@react-chess/chessground'
3+
4+
import { Opening, OpeningVariation } from 'src/types'
5+
6+
interface Props {
7+
openings: Opening[]
8+
selectedOpening: Opening
9+
selectedVariation: OpeningVariation | null
10+
setSelectedOpening: (opening: Opening) => void
11+
setSelectedVariation: (variation: OpeningVariation | null) => void
12+
}
13+
14+
export const OpeningBook: React.FC<Props> = ({
15+
openings,
16+
selectedOpening,
17+
selectedVariation,
18+
setSelectedOpening,
19+
setSelectedVariation,
20+
}: Props) => {
21+
return (
22+
<div className="flex max-h-[40vh] flex-1 flex-col gap-2 border border-white border-opacity-5 bg-background-1 py-2 md:h-[75vh] md:max-h-max md:min-w-[35vh] md:max-w-[45vh] md:rounded md:py-3">
23+
<div className="flex items-center gap-2 px-4 2xl:px-6">
24+
<i className="material-symbols-outlined text-xl md:text-3xl">
25+
menu_book
26+
</i>
27+
<h2 className="text-lg font-bold md:text-2xl">Opening Book</h2>
28+
</div>
29+
<div className="red-scrollbar flex flex-col overflow-y-scroll">
30+
{openings.map((opening, index) => (
31+
<div key={index} className="flex flex-col">
32+
<div
33+
role="button"
34+
tabIndex={0}
35+
onClick={() => {
36+
setSelectedOpening(opening)
37+
setSelectedVariation(null)
38+
}}
39+
className={`flex cursor-pointer flex-row items-center gap-3 px-4 py-2 md:py-4 2xl:flex-row 2xl:px-6 ${selectedOpening.id === opening.id ? 'bg-human-2/10' : 'hover:bg-human-2/5'}`}
40+
>
41+
<div className="aspect-square min-h-20 min-w-20 md:min-h-16 md:min-w-16 2xl:min-h-24 2xl:min-w-24">
42+
<Chessground
43+
contained
44+
config={{
45+
viewOnly: true,
46+
fen: opening.fen,
47+
coordinates: false,
48+
animation: { enabled: false },
49+
}}
50+
/>
51+
</div>
52+
<div className="flex max-h-20 flex-col gap-0.5 overflow-hidden 2xl:max-h-24">
53+
<h4 className="text-xs font-medium xl:text-sm 2xl:text-lg">
54+
{opening.name}
55+
</h4>
56+
<p className="text-ellipsis text-xs text-primary/60">
57+
{opening.description}
58+
</p>
59+
</div>
60+
</div>
61+
{selectedOpening.id === opening.id && opening.variations.length ? (
62+
<div className="flex flex-col bg-backdrop/50 py-1">
63+
{opening.variations.map((variation, index) => (
64+
<div
65+
key={index}
66+
role="button"
67+
tabIndex={0}
68+
onClick={() => setSelectedVariation(variation)}
69+
className={`cursor-pointer px-4 py-1 2xl:px-6 ${selectedVariation?.id === variation.id ? 'bg-human-2/10' : 'hover:bg-human-2/5'}`}
70+
>
71+
<p className="text-xs text-secondary xl:text-sm">
72+
{variation.name}
73+
</p>
74+
</div>
75+
))}
76+
</div>
77+
) : (
78+
<></>
79+
)}
80+
</div>
81+
))}
82+
</div>
83+
</div>
84+
)
85+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { Opening, OpeningVariation } from 'src/types'
2+
3+
interface Props {
4+
selectedOpening: Opening
5+
selectedVariation: OpeningVariation | null
6+
}
7+
8+
export const OpeningDetails: React.FC<Props> = ({
9+
selectedOpening,
10+
selectedVariation,
11+
}: Props) => {
12+
return (
13+
<div className="flex flex-col gap-4 overflow-hidden border border-white border-opacity-5 bg-background-1 pt-4 md:rounded">
14+
<div className="flex flex-col gap-2 px-4">
15+
<h2 className="text-2xl font-bold">{selectedOpening.name}</h2>
16+
<p className="text-sm text-primary/60">{selectedOpening.description}</p>
17+
</div>
18+
<div className="flex w-full items-center justify-center bg-background-2/80 py-1.5">
19+
<p className="text-sm font-medium uppercase text-primary/80">
20+
{selectedVariation ? selectedVariation.name : 'No Variation'}
21+
</p>
22+
</div>
23+
</div>
24+
)
25+
}
Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
import Image from 'next/image'
2+
import { useRouter } from 'next/router'
3+
import { useState, useCallback } from 'react'
4+
5+
import { Opening, OpeningVariation } from 'src/types'
6+
7+
const MAIA_VERSIONS = [
8+
{
9+
id: 'maia_kdd_1100',
10+
name: 'Maia 1100',
11+
},
12+
{
13+
id: 'maia_kdd_1200',
14+
name: 'Maia 1200',
15+
},
16+
{
17+
id: 'maia_kdd_1300',
18+
name: 'Maia 1300',
19+
},
20+
{
21+
id: 'maia_kdd_1400',
22+
name: 'Maia 1400',
23+
},
24+
{
25+
id: 'maia_kdd_1500',
26+
name: 'Maia 1500',
27+
},
28+
{
29+
id: 'maia_kdd_1600',
30+
name: 'Maia 1600',
31+
},
32+
{
33+
id: 'maia_kdd_1700',
34+
name: 'Maia 1700',
35+
},
36+
{
37+
id: 'maia_kdd_1800',
38+
name: 'Maia 1800',
39+
},
40+
{
41+
id: 'maia_kdd_1900',
42+
name: 'Maia 1900',
43+
},
44+
]
45+
46+
const TIME_CONTROLS = [
47+
{
48+
id: '3+0',
49+
name: '3+0',
50+
},
51+
{
52+
id: '5+2',
53+
name: '5+2',
54+
},
55+
{
56+
id: '10+0',
57+
name: '10+0',
58+
},
59+
{
60+
id: '15+10',
61+
name: '15+10',
62+
},
63+
{
64+
id: 'unlimited',
65+
name: 'Unlimited',
66+
},
67+
]
68+
69+
const MAIA_TIME_CONTROL = [
70+
{
71+
id: 'instant',
72+
name: 'Instant',
73+
},
74+
{
75+
id: 'human',
76+
name: 'Human-like',
77+
},
78+
]
79+
80+
interface Props {
81+
selectedOpening: Opening
82+
selectedVariation: OpeningVariation | null
83+
setOrientation: (orientation: 'white' | 'black') => void
84+
}
85+
86+
export const PlayOpening: React.FC<Props> = ({
87+
selectedOpening,
88+
selectedVariation,
89+
setOrientation,
90+
}: Props) => {
91+
const { push } = useRouter()
92+
const [version, setVersion] = useState(MAIA_VERSIONS[0])
93+
const [timeControl, setTimeControl] = useState(TIME_CONTROLS[0])
94+
const [maiaTimeControl, setMaiaTimeControl] = useState(MAIA_TIME_CONTROL[0])
95+
const [color, setColor] = useState<'white' | 'black' | 'random'>('random')
96+
97+
const start = useCallback(() => {
98+
const player =
99+
color === 'random'
100+
? ['white', 'black'][Math.floor(Math.random() * 2)]
101+
: color
102+
103+
const url = new URL('/play/maia', window.location.origin)
104+
url.searchParams.append('player', player)
105+
url.searchParams.append('maiaVersion', version.id)
106+
url.searchParams.append('timeControl', timeControl.id)
107+
url.searchParams.append('sampleMoves', 'true')
108+
url.searchParams.append(
109+
'simulateMaiaTime',
110+
maiaTimeControl.id === 'human' ? 'true' : 'false',
111+
)
112+
url.searchParams.append(
113+
'startFen',
114+
selectedVariation ? selectedVariation.fen : selectedOpening.fen,
115+
)
116+
117+
window.open(url.toString(), '_blank')
118+
}, [
119+
push,
120+
version,
121+
timeControl,
122+
maiaTimeControl,
123+
selectedOpening,
124+
selectedVariation,
125+
])
126+
127+
return (
128+
<div className="flex flex-col items-center gap-6 overflow-hidden border border-white border-opacity-5 bg-background-1 md:rounded">
129+
<div className="flex w-full flex-col items-center gap-4 py-4">
130+
<p className="text-sm font-medium text-secondary">
131+
PRACTICE AGAINST MAIA
132+
</p>
133+
<VersionPicker version={version} setVersion={setVersion} />
134+
<Switcher
135+
label="Maia Time Use:"
136+
options={MAIA_TIME_CONTROL}
137+
selected={maiaTimeControl}
138+
setSelected={setMaiaTimeControl}
139+
/>
140+
<Switcher
141+
label="Time Control:"
142+
options={TIME_CONTROLS}
143+
selected={timeControl}
144+
setSelected={setTimeControl}
145+
/>
146+
</div>
147+
<div className="flex w-full flex-col gap-4">
148+
<ColorPicker
149+
color={color}
150+
setColor={setColor}
151+
setOrientation={setOrientation}
152+
/>
153+
<button
154+
onClick={start}
155+
className="flex w-full items-center justify-center gap-1.5 bg-human-4 px-3 py-2 hover:bg-human-4/80"
156+
>
157+
<span className="material-symbols-outlined text-base">swords</span>
158+
<span>Play opening against {version.name}</span>
159+
</button>
160+
</div>
161+
</div>
162+
)
163+
}
164+
165+
function VersionPicker({
166+
version,
167+
setVersion,
168+
}: {
169+
version: { id: string; name: string }
170+
setVersion: (version: { id: string; name: string }) => void
171+
}) {
172+
return (
173+
<div className="flex w-full flex-col gap-0.5 px-4">
174+
<p className="text-sm text-secondary">Select opponent:</p>
175+
<select
176+
value={version.id}
177+
className="rounded-sm bg-human-4/60 p-2 text-sm focus:outline-none"
178+
onChange={(e) =>
179+
setVersion(
180+
MAIA_VERSIONS.find((version) => version.id === e.target.value) as {
181+
id: string
182+
name: string
183+
},
184+
)
185+
}
186+
>
187+
{MAIA_VERSIONS.map((version) => (
188+
<option key={version.id} value={version.id} className="text-sm">
189+
{version.name}
190+
</option>
191+
))}
192+
</select>
193+
</div>
194+
)
195+
}
196+
197+
function Switcher({
198+
label,
199+
options,
200+
selected,
201+
setSelected,
202+
}: {
203+
label: string
204+
options: { id: string; name: string }[]
205+
selected: { id: string; name: string }
206+
setSelected: (selected: { id: string; name: string }) => void
207+
}) {
208+
return (
209+
<div className="flex w-full flex-col gap-0.5 px-4">
210+
<p className="text-sm text-secondary">{label}</p>
211+
<div className="flex w-full flex-row overflow-hidden rounded-sm bg-background-2/80">
212+
{options.map((option, index) => (
213+
// eslint-disable-next-line jsx-a11y/click-events-have-key-events
214+
<div
215+
key={index}
216+
role="button"
217+
tabIndex={0}
218+
onClick={() => setSelected(option)}
219+
className={`flex flex-1 cursor-pointer items-center justify-center px-3 py-1.5 ${selected.id === option.id ? 'bg-human-4/60' : 'hover:bg-human-4/10'}`}
220+
>
221+
<p className="select-none text-sm">{option.name}</p>
222+
</div>
223+
))}
224+
</div>
225+
</div>
226+
)
227+
}
228+
229+
function ColorPicker({
230+
color,
231+
setColor,
232+
setOrientation,
233+
}: {
234+
color: string
235+
setColor: (color: 'white' | 'black' | 'random') => void
236+
setOrientation: (orientation: 'white' | 'black') => void
237+
}) {
238+
return (
239+
<div className="flex w-full items-end justify-center gap-2">
240+
<button
241+
title="Play as black"
242+
onClick={() => {
243+
setColor('black')
244+
setOrientation('black')
245+
}}
246+
className={`flex cursor-pointer select-none items-center justify-center rounded-sm p-2 ${color === 'black' ? 'bg-human-2/40' : 'bg-human-2/10 hover:bg-human-2/20'}`}
247+
>
248+
<div className="relative h-12 w-12">
249+
<Image src="/assets/pieces/black king.svg" fill={true} alt="" />
250+
</div>
251+
</button>
252+
<button
253+
title="Play as random colour"
254+
onClick={() => setColor('random')}
255+
className={`flex cursor-pointer select-none items-center justify-center rounded-sm p-4 ${color === 'random' ? 'bg-human-2/40' : 'bg-human-2/10 hover:bg-human-2/20'}`}
256+
>
257+
<div className="relative h-14 w-14">
258+
<Image alt="" fill={true} src="/assets/pieces/white black king.svg" />
259+
</div>
260+
</button>
261+
<button
262+
title="Play as white"
263+
onClick={() => {
264+
setColor('white')
265+
setOrientation('white')
266+
}}
267+
className={`flex cursor-pointer select-none items-center justify-center rounded-sm p-2 ${color === 'white' ? 'bg-human-2/40' : 'bg-human-2/10 hover:bg-human-2/20'}`}
268+
>
269+
<div className="relative h-12 w-12">
270+
<Image src="/assets/pieces/white king.svg" fill={true} alt="" />
271+
</div>
272+
</button>
273+
</div>
274+
)
275+
}

src/components/Openings/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from './OpeningBook'
2+
export * from './OpeningDetails'
3+
export * from './PlayOpening'

src/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@ export * from './Board'
66
export * from './Turing'
77
export * from './Profile'
88
export * from './Analysis'
9+
export * from './Openings'
910
export * from './Training'
1011
export * from './Leaderboard'

0 commit comments

Comments
 (0)