Skip to content

Commit 749a19d

Browse files
WIP: endgame naming/order and opening description guards
1 parent 038bd72 commit 749a19d

2 files changed

Lines changed: 168 additions & 19 deletions

File tree

‎src/components/Openings/OpeningSelectionModal.tsx‎

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -537,9 +537,11 @@ const BrowsePanel: React.FC<{
537537
Custom
538538
</span>
539539
</div>
540-
<p className="text-xs text-secondary">
541-
{opening.description}
542-
</p>
540+
{opening.description && (
541+
<p className="text-xs text-secondary">
542+
{opening.description}
543+
</p>
544+
)}
543545
</div>
544546
</div>
545547
</div>
@@ -691,9 +693,11 @@ const BrowsePanel: React.FC<{
691693
<div className="flex items-center justify-between">
692694
<div>
693695
<h3 className="font-medium">{opening.name}</h3>
694-
<p className="text-sm text-secondary">
695-
{opening.description}
696-
</p>
696+
{opening.description && (
697+
<p className="text-sm text-secondary">
698+
{opening.description}
699+
</p>
700+
)}
697701
</div>
698702
</div>
699703
</div>
@@ -926,7 +930,11 @@ const PreviewPanel: React.FC<{
926930
{previewVariation && ` → ${previewVariation.name}`}
927931
</span>
928932
</p>
929-
<p className="text-xs text-secondary">{previewOpening.description}</p>
933+
{previewOpening.description && (
934+
<p className="text-xs text-secondary">
935+
{previewOpening.description}
936+
</p>
937+
)}
930938
</div>
931939

932940
{isEndgame ? (

‎src/lib/endgames.ts‎

Lines changed: 153 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,119 @@ export const ENDGAME_TRAIT_LABELS: Record<EndgameTrait, string> = {
1919
one_pawn_total: 'One Pawn Total',
2020
}
2121

22+
const ENDGAME_CATEGORY_DISPLAY_NAMES: Record<string, string> = {
23+
'King-Pawn Endgames': 'Pawn Endgames',
24+
}
25+
26+
const ENDGAME_CATEGORY_ORDER = [
27+
'King-Pawn Endgames',
28+
'Rook Endgames',
29+
'Queen Endgames',
30+
'Bishop Endgames',
31+
'Knight Endgames',
32+
] as const
33+
34+
const ENDGAME_MOTIF_ORDER: Partial<Record<string, readonly string[]>> = {
35+
'Queen Endgames': [
36+
'QvQ',
37+
'QvR',
38+
'RvQ',
39+
'QvRR',
40+
'RRvQ',
41+
'QvK',
42+
'QvBR',
43+
'BRvQ',
44+
'QvNR',
45+
'NRvQ',
46+
],
47+
'Rook Endgames': [
48+
'RvR',
49+
'RvB',
50+
'BvR',
51+
'RvN',
52+
'NvR',
53+
'RvK',
54+
'RvBB',
55+
'BBvR',
56+
'RvNB',
57+
'NBvR',
58+
'RvNN',
59+
],
60+
'Bishop Endgames': ['BvB', 'BvN', 'NvB', 'BvK', 'BBvK', 'NBvK'],
61+
'Knight Endgames': ['NvN', 'NvK'],
62+
'King-Pawn Endgames': [
63+
'pawn_only_subtype__noflank_balanced',
64+
'pawn_only_subtype__noflank_majority',
65+
'pawn_only_subtype__noflank_minority',
66+
'pawn_only_subtype__flank_balanced',
67+
'pawn_only_subtype__flank_majority',
68+
'pawn_only_subtype__flank_minority',
69+
],
70+
}
71+
72+
const PIECE_CODE_LABELS: Record<string, string> = {
73+
K: 'King',
74+
Q: 'Queen',
75+
R: 'Rook',
76+
B: 'Bishop',
77+
N: 'Knight',
78+
}
79+
80+
const formatMaterialSide = (material: string): string => {
81+
if (!material) return material
82+
83+
const repeatedPiece = material.length === 2 && material[0] === material[1]
84+
if (repeatedPiece) {
85+
const label = PIECE_CODE_LABELS[material[0]] ?? material[0]
86+
return `Two ${label}s`
87+
}
88+
89+
if (material.length === 2) {
90+
const chars = material.split('')
91+
const normalizedPair = chars.slice().sort().join('')
92+
93+
if (normalizedPair === 'BN') return 'Bishop + Knight'
94+
if (normalizedPair === 'BR') return 'Bishop + Rook'
95+
96+
return chars.map((char) => PIECE_CODE_LABELS[char] ?? char).join(' + ')
97+
}
98+
99+
return PIECE_CODE_LABELS[material] ?? material
100+
}
101+
102+
const ENDGAME_MOTIF_DISPLAY_NAMES: Record<string, string> = {
103+
pawn_only_subtype__flank_balanced: 'Flank Pawns (Balanced)',
104+
pawn_only_subtype__flank_majority: 'Flank Pawns (Majority)',
105+
pawn_only_subtype__flank_minority: 'Flank Pawns (Minority)',
106+
pawn_only_subtype__noflank_balanced: 'Central Pawns (Balanced)',
107+
pawn_only_subtype__noflank_majority: 'Central Pawns (Majority)',
108+
pawn_only_subtype__noflank_minority: 'Central Pawns (Minority)',
109+
}
110+
111+
const getEndgameCategoryDisplayName = (categoryName: string) =>
112+
ENDGAME_CATEGORY_DISPLAY_NAMES[categoryName] ?? categoryName
113+
114+
const getEndgameMotifDisplayName = (motifName: string) => {
115+
const explicitLabel = ENDGAME_MOTIF_DISPLAY_NAMES[motifName]
116+
if (explicitLabel) return explicitLabel
117+
118+
const [left, right] = motifName.split('v')
119+
if (left && right) {
120+
return `${formatMaterialSide(left)} vs ${formatMaterialSide(right)}`
121+
}
122+
123+
return motifName
124+
}
125+
126+
const getPreferredOrderIndex = (
127+
value: string,
128+
preferredOrder: readonly string[] | undefined,
129+
) => {
130+
if (!preferredOrder) return Number.POSITIVE_INFINITY
131+
const index = preferredOrder.indexOf(value)
132+
return index === -1 ? Number.POSITIVE_INFINITY : index
133+
}
134+
22135
const normalizeFen = (fen: string): string => {
23136
const trimmed = fen.trim()
24137
if (!trimmed) return trimmed
@@ -108,27 +221,29 @@ export const buildEndgameDataset = (raw: EndgamesJson): EndgameDataset => {
108221

109222
for (const [categoryName, motifs] of Object.entries(raw)) {
110223
const categorySlug = slugify(categoryName)
224+
const categoryDisplayName = getEndgameCategoryDisplayName(categoryName)
111225

112226
const aggregatedTraits: Partial<Record<EndgameTrait, string[]>> = {}
113227

114-
const motifList: EndgameMotifData[] = Object.entries(motifs).map(
115-
([motifName, motifTraitsRaw]) => {
228+
const motifOrder = ENDGAME_MOTIF_ORDER[categoryName]
229+
230+
const motifList = Object.entries(motifs)
231+
.map(([motifName, motifTraitsRaw]) => {
116232
const motifSlug = slugify(motifName)
117233
const traitEntries = normalizeMotifTraits(motifTraitsRaw)
118234

119235
for (const trait of ENDGAME_TRAITS) {
120-
if (traitEntries[trait]?.length) {
236+
const traitList = traitEntries[trait]
237+
if (traitList?.length) {
121238
const existing = new Set(aggregatedTraits[trait] ?? [])
122-
traitEntries[trait]!.forEach((fen) =>
123-
existing.add(normalizeFen(fen)),
124-
)
239+
traitList.forEach((fen) => existing.add(normalizeFen(fen)))
125240
aggregatedTraits[trait] = Array.from(existing)
126241
}
127242
}
128243

129244
const motifData: EndgameMotifData = {
130245
slug: motifSlug,
131-
name: motifName,
246+
name: getEndgameMotifDisplayName(motifName),
132247
traits: traitEntries,
133248
}
134249

@@ -137,13 +252,24 @@ export const buildEndgameDataset = (raw: EndgamesJson): EndgameDataset => {
137252
categorySlug,
138253
}
139254

140-
return motifData
141-
},
142-
)
255+
return {
256+
rawName: motifName,
257+
data: motifData,
258+
}
259+
})
260+
.sort((a, b) => {
261+
const aIndex = getPreferredOrderIndex(a.rawName, motifOrder)
262+
const bIndex = getPreferredOrderIndex(b.rawName, motifOrder)
263+
264+
if (aIndex !== bIndex) return aIndex - bIndex
265+
266+
return a.data.name.localeCompare(b.data.name)
267+
})
268+
.map((entry) => entry.data)
143269

144270
const categoryData: EndgameCategoryData = {
145271
slug: categorySlug,
146-
name: categoryName,
272+
name: categoryDisplayName,
147273
traits: aggregatedTraits,
148274
motifs: motifList,
149275
}
@@ -152,6 +278,21 @@ export const buildEndgameDataset = (raw: EndgamesJson): EndgameDataset => {
152278
categoryMap[categorySlug] = categoryData
153279
}
154280

281+
categories.sort((a, b) => {
282+
const aIndex = getPreferredOrderIndex(
283+
a.slug,
284+
ENDGAME_CATEGORY_ORDER.map((category) => slugify(category)),
285+
)
286+
const bIndex = getPreferredOrderIndex(
287+
b.slug,
288+
ENDGAME_CATEGORY_ORDER.map((category) => slugify(category)),
289+
)
290+
291+
if (aIndex !== bIndex) return aIndex - bIndex
292+
293+
return a.name.localeCompare(b.name)
294+
})
295+
155296
return {
156297
categories,
157298
categoryMap,
@@ -207,7 +348,7 @@ export const createEndgameOpenings = (dataset: EndgameDataset): Opening[] => {
207348
const opening: Opening = {
208349
id: category.slug,
209350
name: category.name,
210-
description: `Practice ${category.name.toLowerCase()} motifs.`,
351+
description: '',
211352
fen: fallbackFen,
212353
pgn: '',
213354
setupFen: fallbackFen,

0 commit comments

Comments
 (0)