Skip to content

Commit 24cdecc

Browse files
David WagerDavid Wager
authored andcommitted
feat: add 'Drill from Position' functionality to the analysis screen
1 parent e9eadab commit 24cdecc

File tree

5 files changed

+112
-8
lines changed

5 files changed

+112
-8
lines changed

src/components/Analysis/ConfigurableScreens.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ interface Props {
2020
onDeleteCustomGame?: () => void
2121
onAnalyzeEntireGame?: () => void
2222
onLearnFromMistakes?: () => void
23+
onDrillFromPosition?: () => void
2324
isAnalysisInProgress?: boolean
2425
isLearnFromMistakesActive?: boolean
2526
autoSave?: {
@@ -51,6 +52,7 @@ export const ConfigurableScreens: React.FC<Props> = ({
5152
onDeleteCustomGame,
5253
onAnalyzeEntireGame,
5354
onLearnFromMistakes,
55+
onDrillFromPosition,
5456
isAnalysisInProgress,
5557
isLearnFromMistakesActive,
5658
autoSave,
@@ -161,6 +163,7 @@ export const ConfigurableScreens: React.FC<Props> = ({
161163
onDeleteCustomGame={onDeleteCustomGame}
162164
onAnalyzeEntireGame={onAnalyzeEntireGame}
163165
onLearnFromMistakes={onLearnFromMistakes}
166+
onDrillFromPosition={onDrillFromPosition}
164167
isAnalysisInProgress={isAnalysisInProgress}
165168
isLearnFromMistakesActive={isLearnFromMistakesActive}
166169
autoSave={autoSave}

src/components/Analysis/ConfigureAnalysis.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ interface Props {
1212
onDeleteCustomGame?: () => void
1313
onAnalyzeEntireGame?: () => void
1414
onLearnFromMistakes?: () => void
15+
onDrillFromPosition?: () => void
1516
isAnalysisInProgress?: boolean
1617
isLearnFromMistakesActive?: boolean
1718
autoSave?: {
@@ -30,6 +31,7 @@ export const ConfigureAnalysis: React.FC<Props> = ({
3031
onDeleteCustomGame,
3132
onAnalyzeEntireGame,
3233
onLearnFromMistakes,
34+
onDrillFromPosition,
3335
isAnalysisInProgress = false,
3436
isLearnFromMistakesActive = false,
3537
autoSave,
@@ -90,6 +92,17 @@ export const ConfigureAnalysis: React.FC<Props> = ({
9092
</div>
9193
</button>
9294
)}
95+
{onDrillFromPosition && (
96+
<button
97+
onClick={onDrillFromPosition}
98+
className="flex w-full items-center gap-1.5 rounded-sm bg-human-4/60 !px-2 !py-1 !text-sm text-primary/70 transition duration-200 hover:bg-human-4/80 hover:text-primary"
99+
>
100+
<div className="flex items-center justify-center gap-1.5">
101+
<span className="material-symbols-outlined !text-sm">explore</span>
102+
<span className="text-xs">Drill from this position</span>
103+
</div>
104+
</button>
105+
)}
93106
{autoSave &&
94107
game.type !== 'custom-pgn' &&
95108
game.type !== 'custom-fen' &&

src/hooks/useOpeningDrillController/useOpeningDrillController.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,9 @@ export const useOpeningDrillController = (
169169

170170
setAnalysisProgress({ total: 0, completed: 0, currentMove: null })
171171

172+
// Use custom FEN if available, otherwise default starting position
172173
const startingFen =
174+
currentDrill.opening.fen ||
173175
'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1'
174176
const gameTree = new GameTree(startingFen)
175177

src/pages/analysis/[...id].tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,35 @@ const Analysis: React.FC<Props> = ({
463463
setAnalysisEnabled(true) // Auto-enable analysis when stopping learn mode
464464
}, [controller.learnFromMistakes])
465465

466+
const handleDrillFromPosition = useCallback(() => {
467+
const fen = controller.currentNode?.fen as string
468+
const turn = controller.currentNode?.turn || 'w'
469+
const path = controller.currentNode?.getPath()
470+
471+
let pgn = ''
472+
path?.forEach((node, index) => {
473+
if (index === 0) return // Skip the root node
474+
const moveIndex = index - 1
475+
if (moveIndex % 2 === 0) {
476+
pgn += `${Math.floor(moveIndex / 2) + 1}. `
477+
}
478+
pgn += node.san + ' '
479+
})
480+
pgn = pgn.trim()
481+
482+
if (!fen) return
483+
484+
// Navigate to openings page with current position as starting FEN
485+
const url =
486+
'/openings?mode=drill-from-position&fen=' +
487+
encodeURIComponent(fen) +
488+
'&turn=' +
489+
encodeURIComponent(turn) +
490+
'&pgn=' +
491+
encodeURIComponent(pgn)
492+
router.push(url)
493+
}, [controller.currentNode, router])
494+
466495
const handleShowSolution = useCallback(() => {
467496
controller.learnFromMistakes.showSolution()
468497
setAnalysisEnabled(true) // Auto-enable analysis when showing solution
@@ -922,6 +951,7 @@ const Analysis: React.FC<Props> = ({
922951
onDeleteCustomGame={handleDeleteCustomGame}
923952
onAnalyzeEntireGame={handleAnalyzeEntireGame}
924953
onLearnFromMistakes={handleLearnFromMistakes}
954+
onDrillFromPosition={handleDrillFromPosition}
925955
isAnalysisInProgress={controller.gameAnalysis.progress.isAnalyzing}
926956
isLearnFromMistakesActive={
927957
controller.learnFromMistakes.state.isActive

src/pages/openings/index.tsx

Lines changed: 64 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@ import {
2121
AuthContext,
2222
MaiaEngineContext,
2323
} from 'src/contexts'
24-
import { DrillConfiguration, AnalyzedGame } from 'src/types'
24+
import { DrillConfiguration, AnalyzedGame, OpeningSelection } from 'src/types'
2525
import { GameNode } from 'src/types/base/tree'
2626
import { MIN_STOCKFISH_DEPTH } from 'src/constants/analysis'
27+
import { MAIA_MODELS } from 'src/constants/common'
2728
import openings from 'src/lib/openings/openings.json'
2829

2930
const LazyOpeningDrillAnalysis = lazy(() =>
@@ -59,7 +60,12 @@ import {
5960
const OpeningsPage: NextPage = () => {
6061
const router = useRouter()
6162
const { user } = useContext(AuthContext)
62-
const [showSelectionModal, setShowSelectionModal] = useState(true)
63+
const { mode, fen, turn, pgn } = router.query // Extract mode, fen and turn from query parameters
64+
65+
const isDrillFromPosition = mode === 'drill-from-position' && fen
66+
67+
const [showSelectionModal, setShowSelectionModal] =
68+
useState(!isDrillFromPosition)
6369
const [isReopenedModal, setIsReopenedModal] = useState(false)
6470

6571
const handleCloseModal = () => {
@@ -69,8 +75,49 @@ const OpeningsPage: NextPage = () => {
6975
router.push('/')
7076
}
7177
}
78+
79+
// Create drill configuration for drill-from-position mode
80+
const createCustomDrillConfiguration = useCallback(
81+
(fenPosition: string, turn: string, pgn: string): DrillConfiguration => {
82+
const customSelection: OpeningSelection = {
83+
id: `custom-position-${Date.now()}`,
84+
opening: {
85+
id: 'custom',
86+
name: 'Custom Position',
87+
description: 'Drill from analysis position',
88+
fen: fenPosition, // Use the custom FEN as starting position
89+
pgn: pgn, // Use the provided PGN
90+
variations: [],
91+
},
92+
variation: null,
93+
playerColor: turn === 'b' ? 'black' : 'white', // Default to white if not specified
94+
maiaVersion: MAIA_MODELS[0], // Default Maia model
95+
targetMoveNumber: 10, // Default target moves
96+
}
97+
98+
return {
99+
selections: [customSelection],
100+
drillCount: 1,
101+
drillSequence: [customSelection],
102+
}
103+
},
104+
[],
105+
)
106+
107+
// Initialize drill configuration based on mode
72108
const [drillConfiguration, setDrillConfiguration] =
73-
useState<DrillConfiguration | null>(null)
109+
useState<DrillConfiguration | null>(() => {
110+
if (
111+
isDrillFromPosition &&
112+
typeof fen === 'string' &&
113+
typeof turn === 'string' &&
114+
typeof pgn === 'string'
115+
) {
116+
return createCustomDrillConfiguration(fen, turn, pgn)
117+
}
118+
return null
119+
})
120+
74121
const [promotionFromTo, setPromotionFromTo] = useState<
75122
[string, string] | null
76123
>(null)
@@ -658,9 +705,10 @@ const OpeningsPage: NextPage = () => {
658705

659706
// Show selection modal when no drill configuration exists (after model is ready)
660707
if (
661-
!drillConfiguration ||
662-
drillConfiguration.selections.length === 0 ||
663-
showSelectionModal
708+
(!drillConfiguration ||
709+
drillConfiguration.selections.length === 0 ||
710+
showSelectionModal) &&
711+
!isDrillFromPosition // Don't show selection modal in drill-from-position mode
664712
) {
665713
return (
666714
<>
@@ -1072,10 +1120,18 @@ const OpeningsPage: NextPage = () => {
10721120
return (
10731121
<>
10741122
<Head>
1075-
<title>Opening Drills – Maia Chess</title>
1123+
<title>
1124+
{isDrillFromPosition
1125+
? 'Drill from Position – Maia Chess'
1126+
: 'Opening Drills – Maia Chess'}
1127+
</title>
10761128
<meta
10771129
name="description"
1078-
content="Master chess openings with interactive drills against Maia AI. Practice popular openings, learn key variations, and get performance analysis to improve your opening repertoire."
1130+
content={
1131+
isDrillFromPosition
1132+
? 'Practice from your analysis position with Maia AI. Continue playing from where you left off and improve your technique in real positions.'
1133+
: 'Master chess openings with interactive drills against Maia AI. Practice popular openings, learn key variations, and get performance analysis to improve your opening repertoire.'
1134+
}
10791135
/>
10801136
</Head>
10811137
<TreeControllerContext.Provider

0 commit comments

Comments
 (0)