Skip to content

Commit 5eca66b

Browse files
feat: add copies of components in client form
1 parent aa2d705 commit 5eca66b

File tree

12 files changed

+1051
-0
lines changed

12 files changed

+1051
-0
lines changed
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
import { defaults } from 'chessground/state'
3+
import { useCallback, useContext } from 'react'
4+
import Chessground from '@react-chess/chessground'
5+
import type { DrawBrush, DrawBrushes, DrawShape } from 'chessground/draw'
6+
7+
import { BaseGame, Check } from 'src/types'
8+
import { GameControllerContext } from 'src/contexts'
9+
10+
interface Props {
11+
game: BaseGame
12+
moves?: Map<string, string[]>
13+
setCurrentMove?: (move: [string, string] | null) => void
14+
setCurrentSquare?: (key: string | null) => void
15+
move?: {
16+
fen: string
17+
move: [string, string]
18+
check?: Check
19+
}
20+
shapes?: DrawShape[]
21+
brushes?: DrawBrushes
22+
}
23+
24+
export const GameBoard: React.FC<Props> = ({
25+
game,
26+
moves,
27+
move,
28+
setCurrentMove,
29+
setCurrentSquare,
30+
shapes,
31+
brushes,
32+
}: Props) => {
33+
const { currentIndex, orientation } = useContext(GameControllerContext)
34+
35+
const after = useCallback(
36+
(from: string, to: string) => {
37+
if (setCurrentMove) setCurrentMove([from, to])
38+
if (setCurrentSquare) setCurrentSquare(null)
39+
},
40+
[setCurrentMove, setCurrentSquare],
41+
)
42+
43+
return (
44+
<Chessground
45+
contained
46+
config={{
47+
movable: {
48+
free: false,
49+
dests: moves as any,
50+
events: {
51+
after,
52+
},
53+
},
54+
events: {
55+
select: (key) => {
56+
setCurrentMove && setCurrentMove(null)
57+
setCurrentSquare && setCurrentSquare(key)
58+
},
59+
},
60+
drawable: {
61+
autoShapes: shapes || [],
62+
brushes: { ...defaults().drawable.brushes, ...brushes },
63+
},
64+
fen: move ? move.fen : game.moves[currentIndex]?.board,
65+
lastMove: move
66+
? move.move
67+
: [...((game.moves[currentIndex]?.lastMove ?? []) as any)],
68+
check: move ? move.check : game.moves[currentIndex]?.check,
69+
orientation,
70+
}}
71+
/>
72+
)
73+
}
Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
import Head from 'next/head'
2+
import type { DrawShape } from 'chessground/draw'
3+
import { useCallback, useContext, useEffect, useMemo, useState } from 'react'
4+
5+
import {
6+
AuthContext,
7+
ThemeContext,
8+
WindowSizeContext,
9+
ClientGameControllerContext,
10+
} from 'src/contexts'
11+
import {
12+
GameInfo,
13+
GameClock,
14+
GameBoard,
15+
ExportGame,
16+
StatsDisplay,
17+
ClientMovesContainer,
18+
BoardController,
19+
PromotionOverlay,
20+
} from 'src/components'
21+
import { useUnload } from 'src/hooks/useUnload'
22+
import { PlayControllerContext } from 'src/contexts/PlayControllerContext/PlayControllerContext'
23+
import { GameNode } from 'src/types/analysis'
24+
25+
interface Props {
26+
boardShapes?: DrawShape[]
27+
resign?: () => void
28+
offerDraw?: () => void
29+
playAgain?: () => void
30+
}
31+
32+
export const ClientGameplayInterface: React.FC<
33+
React.PropsWithChildren<Props>
34+
> = (props: React.PropsWithChildren<Props>) => {
35+
const {
36+
game,
37+
playType,
38+
maiaVersion,
39+
availableMoves,
40+
makeMove,
41+
player,
42+
setCurrentSquare,
43+
timeControl,
44+
stats,
45+
} = useContext(PlayControllerContext)
46+
const { theme } = useContext(ThemeContext)
47+
const { isMobile } = useContext(WindowSizeContext)
48+
49+
const {
50+
currentNode,
51+
setCurrentNode,
52+
orientation,
53+
setOrientation,
54+
goToNode,
55+
goToNextNode,
56+
goToPreviousNode,
57+
goToRootNode,
58+
plyCount,
59+
} = useContext(ClientGameControllerContext)
60+
61+
const { user } = useContext(AuthContext)
62+
63+
const [promotionFromTo, setPromotionFromTo] = useState<
64+
[string, string] | null
65+
>(null)
66+
67+
const setCurrentMove = useCallback(
68+
(move: [string, string] | null) => {
69+
if (move) {
70+
const matching = availableMoves.filter(
71+
(m) => m.from == move[0] && m.to == move[1],
72+
)
73+
74+
if (matching.length > 1) {
75+
setPromotionFromTo(move)
76+
} else {
77+
const moveUci =
78+
matching[0].from + matching[0].to + (matching[0].promotion ?? '')
79+
makeMove(moveUci)
80+
}
81+
}
82+
},
83+
[availableMoves, makeMove, setPromotionFromTo],
84+
)
85+
86+
const selectPromotion = useCallback(
87+
(piece: string) => {
88+
if (!promotionFromTo) {
89+
return
90+
}
91+
setPromotionFromTo(null)
92+
const moveUci = promotionFromTo[0] + promotionFromTo[1] + piece
93+
makeMove(moveUci)
94+
},
95+
[promotionFromTo, setPromotionFromTo, makeMove],
96+
)
97+
98+
useUnload((e) => {
99+
if (!game.termination) {
100+
e.preventDefault()
101+
return 'Are you sure you want to leave a game in progress?'
102+
}
103+
})
104+
105+
const moveMap = useMemo(() => {
106+
const result = new Map()
107+
108+
for (const move of availableMoves) {
109+
const from = move.from
110+
const to = move.to
111+
112+
if (result.has(from)) {
113+
result.get(from).push(to)
114+
} else {
115+
result.set(from, [to])
116+
}
117+
}
118+
119+
return result
120+
}, [availableMoves])
121+
122+
const maiaTitle = maiaVersion.replace('maia_kdd_', 'Maia ')
123+
const blackPlayer = player == 'black' ? user?.displayName : maiaTitle
124+
const whitePlayer = player == 'white' ? user?.displayName : maiaTitle
125+
126+
const Info = (
127+
<>
128+
<div className="flex w-full items-center justify-between text-secondary">
129+
<p>
130+
{theme == 'dark' ? '●' : '○'} {whitePlayer ?? 'Unknown'}
131+
</p>
132+
<p>
133+
{game.termination?.winner === 'white' ? (
134+
<span className="text-engine-3">1</span>
135+
) : game.termination?.winner === 'black' ? (
136+
<span className="text-human-3">0</span>
137+
) : game.termination ? (
138+
<span>1/2</span>
139+
) : null}
140+
</p>
141+
</div>
142+
<div className="flex w-full items-center justify-between text-secondary">
143+
<p>
144+
{theme == 'light' ? '●' : '○'} {blackPlayer ?? 'Unknown'}
145+
</p>
146+
<p>
147+
{game.termination?.winner === 'black' ? (
148+
<span className="text-engine-3">1</span>
149+
) : game.termination?.winner === 'white' ? (
150+
<span className="text-human-3">0</span>
151+
) : game.termination ? (
152+
<span>1/2</span>
153+
) : null}
154+
</p>
155+
</div>{' '}
156+
{game.termination ? (
157+
<p className="text-center capitalize text-secondary">
158+
{game.termination.winner !== 'none'
159+
? `${game.termination.winner} wins`
160+
: 'draw'}
161+
</p>
162+
) : null}
163+
</>
164+
)
165+
166+
const desktopLayout = (
167+
<>
168+
<div className="flex h-full flex-1 flex-col justify-center gap-1">
169+
<div className="flex w-full flex-row items-center justify-center gap-1">
170+
<div
171+
style={{
172+
maxWidth: 'min(20vw, 100vw - 75vh)',
173+
}}
174+
className="flex h-[75vh] w-[40vh] flex-col justify-between gap-1"
175+
>
176+
<GameInfo
177+
icon="swords"
178+
type={playType}
179+
title={
180+
playType === 'againstMaia'
181+
? 'Play vs. Maia'
182+
: 'Play Hand and Brain'
183+
}
184+
>
185+
{Info}
186+
</GameInfo>
187+
<div className="flex w-full flex-col gap-2">
188+
<ExportGame
189+
game={game}
190+
whitePlayer={whitePlayer ?? 'Unknown'}
191+
blackPlayer={blackPlayer ?? 'Unknown'}
192+
event={`Play vs. ${maiaTitle}`}
193+
/>
194+
<StatsDisplay stats={stats} hideSession={true} />
195+
</div>
196+
</div>
197+
<div className="relative flex aspect-square w-full max-w-[75vh]">
198+
<GameBoard
199+
game={game}
200+
moves={moveMap}
201+
setCurrentMove={setCurrentMove}
202+
setCurrentSquare={setCurrentSquare}
203+
shapes={props.boardShapes}
204+
/>
205+
{promotionFromTo ? (
206+
<PromotionOverlay
207+
player={player}
208+
file={promotionFromTo[1].slice(0)}
209+
selectPromotion={selectPromotion}
210+
/>
211+
) : null}
212+
</div>
213+
<div
214+
style={{
215+
maxWidth: 'min(20vw, 100vw - 75vh)',
216+
}}
217+
className="flex h-[75vh] w-[40vh] flex-col justify-center gap-1"
218+
>
219+
{timeControl != 'unlimited' ? (
220+
<GameClock
221+
player={controller.orientation == 'white' ? 'black' : 'white'}
222+
reversed={false}
223+
/>
224+
) : null}
225+
<div className="relative bottom-0 h-full min-h-[38px] flex-1">
226+
<ClientMovesContainer
227+
game={game}
228+
currentNode={currentNode}
229+
setCurrentNode={setCurrentNode}
230+
termination={game.termination}
231+
/>
232+
</div>
233+
<div>{props.children}</div>
234+
<div className="flex-none">
235+
<BoardController />
236+
</div>
237+
{timeControl != 'unlimited' ? (
238+
<GameClock player={controller.orientation} reversed={true} />
239+
) : null}
240+
</div>
241+
</div>
242+
</div>
243+
</>
244+
)
245+
246+
const mobileLayout = (
247+
<>
248+
<div className="flex h-full flex-1 flex-col justify-center gap-1">
249+
<div className="mt-2 flex h-full flex-col items-start justify-start gap-2">
250+
<div className="flex h-auto w-full flex-col gap-1">
251+
{timeControl != 'unlimited' ? (
252+
<GameClock
253+
player={controller.orientation == 'white' ? 'black' : 'white'}
254+
reversed={false}
255+
/>
256+
) : null}
257+
</div>
258+
<div className="relative flex aspect-square h-[100vw] w-screen">
259+
<GameBoard
260+
game={game}
261+
moves={moveMap}
262+
setCurrentMove={setCurrentMove}
263+
setCurrentSquare={setCurrentSquare}
264+
shapes={props.boardShapes}
265+
/>
266+
{promotionFromTo ? (
267+
<PromotionOverlay
268+
player={player}
269+
file={promotionFromTo[1].slice(0)}
270+
selectPromotion={selectPromotion}
271+
/>
272+
) : null}
273+
</div>
274+
<div className="flex h-auto w-full flex-col gap-1">
275+
{timeControl != 'unlimited' ? (
276+
<GameClock player={controller.orientation} reversed={true} />
277+
) : null}
278+
<div className="flex-none">
279+
<BoardController />
280+
</div>
281+
<div className="w-screen">{props.children}</div>
282+
<StatsDisplay stats={stats} hideSession={true} />
283+
<ExportGame
284+
game={game}
285+
whitePlayer={whitePlayer ?? 'Unknown'}
286+
blackPlayer={blackPlayer ?? 'Unknown'}
287+
event={`Play vs. ${maiaTitle}`}
288+
/>
289+
</div>
290+
</div>
291+
</div>
292+
</>
293+
)
294+
295+
return (
296+
<>
297+
<Head>
298+
<title>Maia Chess - Play</title>
299+
<meta name="description" content="Turing survey" />
300+
</Head>
301+
<ClientGameControllerContext.Provider value={{ ...controller }}>
302+
{isMobile ? mobileLayout : desktopLayout}
303+
</ClientGameControllerContext.Provider>
304+
</>
305+
)
306+
}

0 commit comments

Comments
 (0)