@@ -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+
22135const 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