Skip to content

Commit b22e3f7

Browse files
fix: moves container component for drills
1 parent 7400608 commit b22e3f7

4 files changed

Lines changed: 189 additions & 79 deletions

File tree

src/components/Board/MovesContainer.tsx

Lines changed: 85 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ interface Props {
1313
showVariations?: boolean
1414
disableKeyboardNavigation?: boolean
1515
disableMoveClicking?: boolean
16+
startFromNode?: GameNode
17+
restrictNavigationBefore?: GameNode
1618
}
1719

1820
const getMoveClassification = (node: GameNode | null) => {
@@ -42,6 +44,8 @@ export const MovesContainer: React.FC<Props> = (props) => {
4244
showVariations = true,
4345
disableKeyboardNavigation = false,
4446
disableMoveClicking = false,
47+
startFromNode,
48+
restrictNavigationBefore,
4549
} = props
4650
const { isMobile } = useContext(WindowSizeContext)
4751
const containerRef = useRef<HTMLDivElement>(null)
@@ -60,8 +64,51 @@ export const MovesContainer: React.FC<Props> = (props) => {
6064
const controller = useContext(TreeControllerContext)
6165

6266
const mainLineNodes = useMemo(() => {
63-
return controller.gameTree.getMainLine() ?? game.tree.getMainLine()
64-
}, [game, controller.gameTree, controller.currentNode])
67+
const fullMainLine =
68+
controller.gameTree.getMainLine() ?? game.tree.getMainLine()
69+
70+
if (startFromNode) {
71+
// Find the index of the start node in the full main line
72+
const startIndex = fullMainLine.findIndex(
73+
(node) => node.fen === startFromNode.fen,
74+
)
75+
76+
if (startIndex !== -1) {
77+
// Return the main line starting from the specified node
78+
const customMainLine = fullMainLine.slice(startIndex)
79+
console.log('MovesContainer mainLineNodes (custom start):', {
80+
fullMainLineLength: fullMainLine.length,
81+
startIndex,
82+
customMainLineLength: customMainLine.length,
83+
startNodeFen: startFromNode.fen,
84+
currentNode: controller.currentNode?.fen,
85+
})
86+
return customMainLine
87+
} else {
88+
// If start node not found in main line, build from start node
89+
const customMainLine = [startFromNode]
90+
let current = startFromNode.mainChild
91+
while (current) {
92+
customMainLine.push(current)
93+
current = current.mainChild
94+
}
95+
console.log('MovesContainer mainLineNodes (built from start):', {
96+
customMainLineLength: customMainLine.length,
97+
startNodeFen: startFromNode.fen,
98+
currentNode: controller.currentNode?.fen,
99+
})
100+
return customMainLine
101+
}
102+
}
103+
104+
console.log('MovesContainer mainLineNodes (default):', {
105+
controllerTreeMainLine: controller.gameTree?.getMainLine()?.length || 0,
106+
gameTreeMainLine: game.tree?.getMainLine()?.length || 0,
107+
finalMainLineLength: fullMainLine?.length || 0,
108+
currentNode: controller.currentNode?.fen,
109+
})
110+
return fullMainLine
111+
}, [game, controller.gameTree, controller.currentNode, startFromNode])
65112

66113
useEffect(() => {
67114
if (disableKeyboardNavigation) return
@@ -79,6 +126,14 @@ export const MovesContainer: React.FC<Props> = (props) => {
79126
case 'ArrowLeft':
80127
event.preventDefault()
81128
if (controller.currentNode.parent) {
129+
// Check if navigation is restricted
130+
if (
131+
restrictNavigationBefore &&
132+
controller.currentNode.parent.fen === restrictNavigationBefore.fen
133+
) {
134+
// Don't navigate before the restriction point
135+
return
136+
}
82137
controller.goToNode(controller.currentNode.parent)
83138
}
84139
break
@@ -89,7 +144,12 @@ export const MovesContainer: React.FC<Props> = (props) => {
89144

90145
window.addEventListener('keydown', handleKeyDown)
91146
return () => window.removeEventListener('keydown', handleKeyDown)
92-
}, [controller.currentNode, controller.goToNode, disableKeyboardNavigation])
147+
}, [
148+
controller.currentNode,
149+
controller.goToNode,
150+
disableKeyboardNavigation,
151+
restrictNavigationBefore,
152+
])
93153

94154
useEffect(() => {
95155
if (currentMoveRef.current && containerRef.current) {
@@ -102,7 +162,23 @@ export const MovesContainer: React.FC<Props> = (props) => {
102162
}, [controller.currentNode])
103163

104164
const moves = useMemo(() => {
105-
const nodes = mainLineNodes.slice(1)
165+
// When using startFromNode, we want to show moves AFTER that node
166+
// When using default behavior, we want to skip the root node (slice(1))
167+
const nodes = startFromNode
168+
? mainLineNodes.slice(1)
169+
: mainLineNodes.slice(1)
170+
console.log('MovesContainer moves calculation:', {
171+
mainLineNodesLength: mainLineNodes.length,
172+
nodesAfterSliceLength: nodes.length,
173+
firstNodeTurn: nodes[0]?.turn,
174+
startFromNode: startFromNode?.fen,
175+
allNodes: nodes.map((n, i) => ({
176+
index: i,
177+
move: n?.san,
178+
turn: n?.turn,
179+
})),
180+
})
181+
106182
const rows: (GameNode | null)[][] = []
107183

108184
const firstNode = nodes[0]
@@ -119,6 +195,7 @@ export const MovesContainer: React.FC<Props> = (props) => {
119195
}, [])
120196
}
121197

198+
console.log('MovesContainer final rows:', rows.length)
122199
return rows
123200
}, [game, mainLineNodes, controller.gameTree])
124201

@@ -136,7 +213,10 @@ export const MovesContainer: React.FC<Props> = (props) => {
136213
const mobileMovePairs = useMemo(() => {
137214
if (!isMobile) return []
138215

139-
const nodes = mainLineNodes.slice(1)
216+
// Use the same logic as the moves calculation for consistency
217+
const nodes = startFromNode
218+
? mainLineNodes.slice(1)
219+
: mainLineNodes.slice(1)
140220
const pairs: MovePair[] = []
141221
let currentPair: MovePair | null = null
142222

src/components/Openings/DrillPerformanceModal.tsx

Lines changed: 77 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,8 @@ const AnimatedGameReplay: React.FC<{
8888
openingFen: string
8989
playerColor: 'white' | 'black'
9090
gameTree: GameTree
91-
}> = ({ openingFen, playerColor, gameTree }) => {
91+
openingEndNode: GameNode
92+
}> = ({ openingFen, playerColor, gameTree, openingEndNode }) => {
9293
const [currentFen, setCurrentFen] = useState(openingFen)
9394
const [currentNode, setCurrentNode] = useState<GameNode | null>(null)
9495

@@ -230,16 +231,15 @@ const AnimatedGameReplay: React.FC<{
230231

231232
{/* Move History - Use MovesContainer for consistency */}
232233
<div className="flex min-h-0 flex-1 flex-col border-t border-white/10">
233-
<TreeControllerContext.Provider value={treeController}>
234-
<MovesContainer
235-
game={{
236-
id: 'drill-performance',
237-
tree: gameTree,
238-
}}
239-
showAnnotations={true}
240-
showVariations={false}
241-
/>
242-
</TreeControllerContext.Provider>
234+
<MovesContainer
235+
game={{
236+
id: 'drill-performance',
237+
tree: gameTree,
238+
}}
239+
startFromNode={openingEndNode}
240+
showAnnotations={true}
241+
showVariations={false}
242+
/>
243243
</div>
244244
</div>
245245
)
@@ -728,6 +728,7 @@ const DesktopLayout: React.FC<{
728728
onNextDrill: () => void
729729
isLastDrill: boolean
730730
gameTree: GameTree
731+
openingEndNode: GameNode
731732
playerMoveCount: number
732733
treeController: ReturnType<typeof useTreeController>
733734
gameNodesMap: Map<string, GameNode>
@@ -745,6 +746,7 @@ const DesktopLayout: React.FC<{
745746
onNextDrill,
746747
isLastDrill,
747748
gameTree,
749+
openingEndNode,
748750
playerMoveCount,
749751
treeController,
750752
gameNodesMap,
@@ -785,14 +787,15 @@ const DesktopLayout: React.FC<{
785787
<div className="flex w-1/3 flex-col border-r border-white/10">
786788
<TreeControllerContext.Provider
787789
value={{
788-
gameTree: treeController.tree,
790+
gameTree: gameTree,
789791
...treeController,
790792
}}
791793
>
792794
<AnimatedGameReplay
793795
openingFen={openingFen}
794796
playerColor={drill.selection.playerColor}
795797
gameTree={gameTree}
798+
openingEndNode={openingEndNode}
796799
/>
797800
</TreeControllerContext.Provider>
798801
</div>
@@ -1041,6 +1044,7 @@ const MobileLayout: React.FC<{
10411044
activeTab: 'replay' | 'analysis' | 'insights'
10421045
setActiveTab: (tab: 'replay' | 'analysis' | 'insights') => void
10431046
gameTree: GameTree
1047+
openingEndNode: GameNode
10441048
playerMoveCount: number
10451049
treeController: ReturnType<typeof useTreeController>
10461050
gameNodesMap: Map<string, GameNode>
@@ -1060,6 +1064,7 @@ const MobileLayout: React.FC<{
10601064
activeTab,
10611065
setActiveTab,
10621066
gameTree,
1067+
openingEndNode,
10631068
playerMoveCount,
10641069
treeController,
10651070
gameNodesMap,
@@ -1130,14 +1135,15 @@ const MobileLayout: React.FC<{
11301135
{activeTab === 'replay' && (
11311136
<TreeControllerContext.Provider
11321137
value={{
1133-
gameTree: treeController.tree,
1138+
gameTree: gameTree,
11341139
...treeController,
11351140
}}
11361141
>
11371142
<AnimatedGameReplay
11381143
openingFen={openingFen}
11391144
playerColor={drill.selection.playerColor}
11401145
gameTree={gameTree}
1146+
openingEndNode={openingEndNode}
11411147
/>
11421148
</TreeControllerContext.Provider>
11431149
)}
@@ -1241,62 +1247,74 @@ export const DrillPerformanceModal: React.FC<Props> = ({
12411247
// For now, let's work directly with the nodes instead of trying to recreate the full tree
12421248
// This is a temporary solution until we can properly access the GameTree
12431249

1244-
// Create a proper GameTree starting from the opening end node
1245-
const gameTree = useMemo(() => {
1246-
// Get the root node and build the tree from there
1247-
let root = drill.finalNode
1248-
while (root.parent) {
1249-
root = root.parent
1250+
// Get the original game tree and opening end node for the drill context
1251+
const { gameTree, openingEndNode } = useMemo(() => {
1252+
// Get the original game tree from the drill
1253+
const originalRoot = (() => {
1254+
let root = drill.finalNode
1255+
while (root.parent) {
1256+
root = root.parent
1257+
}
1258+
return root
1259+
})()
1260+
1261+
// Create the original GameTree from root
1262+
const originalGameTree = new GameTree(originalRoot.fen)
1263+
// Set the root to be the actual root node
1264+
Object.defineProperty(originalGameTree, 'root', { value: originalRoot })
1265+
1266+
// Find the opening end node by working backwards from the final node
1267+
// The opening end should be the first move analysis that is NOT a player move
1268+
const firstPlayerMoveAnalysis = moveAnalyses.find(move => move.isPlayerMove)
1269+
if (firstPlayerMoveAnalysis && firstPlayerMoveAnalysis.fenBeforeMove) {
1270+
// Find the node that represents the position before the first player move
1271+
const findNodeByFen = (node: GameNode, targetFen: string): GameNode | null => {
1272+
if (node.fen === targetFen) {
1273+
return node
1274+
}
1275+
// Check main child
1276+
if (node.mainChild) {
1277+
const found = findNodeByFen(node.mainChild, targetFen)
1278+
if (found) return found
1279+
}
1280+
// Check all children
1281+
for (const child of node.children) {
1282+
const found = findNodeByFen(child, targetFen)
1283+
if (found) return found
1284+
}
1285+
return null
1286+
}
1287+
1288+
const foundOpeningEndNode = findNodeByFen(originalRoot, firstPlayerMoveAnalysis.fenBeforeMove)
1289+
if (foundOpeningEndNode) {
1290+
return {
1291+
gameTree: originalGameTree,
1292+
openingEndNode: foundOpeningEndNode,
1293+
}
1294+
}
12501295
}
12511296

1252-
// Find the opening end node from the drill selection
1297+
// Fallback: use the selection-based approach
12531298
const openingEndFen = drill.selection.variation
12541299
? drill.selection.variation.fen
12551300
: drill.selection.opening.fen
12561301

12571302
// Find the actual opening end node in the tree
1258-
let openingEndTreeNode = root
1259-
let current: GameNode | null = root
1303+
let foundOpeningEndNode = originalRoot
1304+
let current: GameNode | null = originalRoot
12601305
while (current) {
12611306
if (current.fen === openingEndFen) {
1262-
openingEndTreeNode = current
1307+
foundOpeningEndNode = current
12631308
break
12641309
}
12651310
current = current.mainChild
12661311
}
12671312

1268-
// Create a new GameTree starting from the opening end node
1269-
const newTree = new (class extends GameTree {
1270-
constructor(startNode: GameNode) {
1271-
super(startNode.fen)
1272-
// Replace the root with our opening end node
1273-
this.setRoot(startNode)
1274-
}
1275-
1276-
private setRoot(node: GameNode) {
1277-
// Use reflection to set the private root field
1278-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
1279-
;(this as any).root = node
1280-
}
1281-
1282-
getRoot(): GameNode {
1283-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
1284-
return (this as any).root
1285-
}
1286-
1287-
getMainLine(): GameNode[] {
1288-
const mainLine = []
1289-
let current: GameNode | null = this.getRoot()
1290-
while (current) {
1291-
mainLine.push(current)
1292-
current = current.mainChild
1293-
}
1294-
return mainLine
1295-
}
1296-
})(openingEndTreeNode)
1297-
1298-
return newTree
1299-
}, [drill.finalNode, drill.selection])
1313+
return {
1314+
gameTree: originalGameTree,
1315+
openingEndNode: foundOpeningEndNode,
1316+
}
1317+
}, [drill.finalNode, drill.selection, moveAnalyses])
13001318

13011319
// Create tree controller for navigation
13021320
const treeController = useTreeController(
@@ -1377,10 +1395,8 @@ export const DrillPerformanceModal: React.FC<Props> = ({
13771395
return evaluationChart.slice(startIndex)
13781396
}, [evaluationChart, moveAnalyses])
13791397

1380-
// Get opening FEN from the drill
1381-
const openingFen = drill.selection.variation
1382-
? drill.selection.variation.fen
1383-
: drill.selection.opening.fen
1398+
// Get opening FEN from the opening end node
1399+
const openingFen = openingEndNode.fen
13841400

13851401
return (
13861402
<ModalContainer dismiss={onContinueAnalyzing}>
@@ -1396,6 +1412,7 @@ export const DrillPerformanceModal: React.FC<Props> = ({
13961412
activeTab={activeTab}
13971413
setActiveTab={setActiveTab}
13981414
gameTree={gameTree}
1415+
openingEndNode={openingEndNode}
13991416
playerMoveCount={playerMoveCount}
14001417
treeController={treeController}
14011418
gameNodesMap={gameNodesMap}
@@ -1412,6 +1429,7 @@ export const DrillPerformanceModal: React.FC<Props> = ({
14121429
onNextDrill={onNextDrill}
14131430
isLastDrill={isLastDrill}
14141431
gameTree={gameTree}
1432+
openingEndNode={openingEndNode}
14151433
playerMoveCount={playerMoveCount}
14161434
treeController={treeController}
14171435
gameNodesMap={gameNodesMap}

0 commit comments

Comments
 (0)