@@ -2,6 +2,17 @@ import initOld, { Engine as EngineOld } from './pkg-old/hydrochess_wasm.js';
22import initNew , { Engine as EngineNew } from './pkg-new/hydrochess_wasm.js' ;
33import { VARIANTS , getVariantData } from './variants.js' ;
44
5+ // Map internal engine piece letters to infinitechess.org ICN codes (lowercase for ICN)
6+ function engineLetterToICNCode ( letter ) {
7+ const map = {
8+ 'k' : 'k' , 'q' : 'q' , 'r' : 'r' , 'b' : 'b' , 'n' : 'n' , 'p' : 'p' ,
9+ 'm' : 'am' , 'c' : 'ch' , 'a' : 'ar' , 'h' : 'ha' , 'g' : 'gu' ,
10+ 'l' : 'ca' , 'i' : 'gi' , 'z' : 'ze' , 'e' : 'ce' , 'y' : 'rq' ,
11+ 'd' : 'rc' , 's' : 'nr' , 'u' : 'hu' , 'o' : 'ro' , 'x' : 'ob' , 'v' : 'vo'
12+ } ;
13+ return map [ letter ] || letter ;
14+ }
15+
516// UI Elements
617const statusDot = document . getElementById ( 'statusDot' ) ;
718const statusText = document . getElementById ( 'statusText' ) ;
@@ -28,8 +39,8 @@ const sprtOutput = document.getElementById('sprtOutput');
2839const gameLogEl = document . getElementById ( 'gameLog' ) ;
2940const copyLogBtn = document . getElementById ( 'copyLog' ) ;
3041const downloadLogsBtn = document . getElementById ( 'downloadLogs' ) ;
31- const downloadGamesBtn = document . getElementById ( 'downloadGames' ) ;
32- const downloadGamesJsonBtn = document . getElementById ( 'downloadGamesJson ' ) ;
42+ const downloadGamesTxtBtn = document . getElementById ( 'downloadGames-txt ' ) ;
43+ const downloadGamesJsonBtn = document . getElementById ( 'downloadGames-json ' ) ;
3344const icnOutputEl = document . getElementById ( 'icnOutput' ) ;
3445const icnTextEl = document . getElementById ( 'icnText' ) ;
3546const sprtStatusEl = document . getElementById ( 'sprtStatus' ) ;
@@ -430,16 +441,23 @@ function generateICNFromWorkerLog(workerLog, gameIndex, result, newPlaysWhite, e
430441 } ) . filter ( Boolean ) ;
431442
432443 const movesStr = moves . join ( '|' ) ;
433- // Determine promotion ranks for the variant (default to standard 8 for white, 1 for black)
444+ // Determine promotion ranks and allowed promotions for the variant
445+ // Default to standard 8 for white, 1 for black, with q,r,b,n allowed
434446 let whiteRank = '8' ;
435447 let blackRank = '1' ;
448+ let promotionsAllowed = [ 'q' , 'r' , 'b' , 'n' ] ;
436449 let worldBoundsStr = '' ;
437450 try {
438451 const vdata = getVariantData ( variantName ) ;
439- if ( vdata && vdata . game_rules && vdata . game_rules . promotion_ranks ) {
440- const ranks = vdata . game_rules . promotion_ranks ;
441- if ( ranks . white && ranks . white . length > 0 ) whiteRank = ranks . white [ 0 ] ;
442- if ( ranks . black && ranks . black . length > 0 ) blackRank = ranks . black [ 0 ] ;
452+ if ( vdata && vdata . game_rules ) {
453+ if ( vdata . game_rules . promotion_ranks ) {
454+ const ranks = vdata . game_rules . promotion_ranks ;
455+ if ( ranks . white && ranks . white . length > 0 ) whiteRank = ranks . white [ 0 ] ;
456+ if ( ranks . black && ranks . black . length > 0 ) blackRank = ranks . black [ 0 ] ;
457+ }
458+ if ( vdata . game_rules . promotions_allowed && Array . isArray ( vdata . game_rules . promotions_allowed ) ) {
459+ promotionsAllowed = vdata . game_rules . promotions_allowed ;
460+ }
443461 }
444462 // Compute world bounds: if worldBorder is a number, calculate finite bounds
445463 // Otherwise, use infinite bounds
@@ -476,7 +494,10 @@ function generateICNFromWorkerLog(workerLog, gameIndex, result, newPlaysWhite, e
476494 // Use infinite bounds on error
477495 worldBoundsStr = '-999999999999999,1000000000000008,-999999999999999,1000000000000008' ;
478496 }
479- const promotionRanksToken = `(${ whiteRank } |${ blackRank } )` ;
497+ // Build promotion token: (whiteRank;promo1,promo2,...|blackRank;promo1,promo2,...)
498+ // Convert engine piece letters to ICN codes (e.g., 'm' -> 'am')
499+ const promotionsICN = promotionsAllowed . map ( p => engineLetterToICNCode ( p ) ) . join ( ',' ) ;
500+ const promotionRanksToken = `(${ whiteRank } ;${ promotionsICN } |${ blackRank } ;${ promotionsICN } )` ;
480501 return `${ headers } ${ nextTurn } ${ halfmove } /100 ${ fullmove } ${ promotionRanksToken } ${ worldBoundsStr } ${ startPositionStr } ${ movesStr ? ' ' + movesStr : '' } ` ;
481502}
482503
@@ -850,6 +871,10 @@ async function runSprt() {
850871 let llr = 0 ;
851872 gameLogs = [ ] ;
852873
874+ // Disable download buttons as logs are cleared
875+ downloadGamesTxtBtn . disabled = true ;
876+ downloadGamesJsonBtn . disabled = true ;
877+
853878 sprtOutput . innerHTML = '' ;
854879 perVariantStats = { } ;
855880 clearLog ( ) ;
@@ -925,6 +950,9 @@ async function runSprt() {
925950 msg . variantName , // Add variant to ICN log
926951 ) ;
927952 gameLogs . push ( icnLog ) ;
953+ // Enable download buttons immediately upon first result
954+ downloadGamesTxtBtn . disabled = false ;
955+ downloadGamesJsonBtn . disabled = false ;
928956 // Global results
929957 if ( result === 'win' ) wins ++ ;
930958 else if ( result === 'loss' ) losses ++ ;
@@ -1085,12 +1113,6 @@ async function runSprt() {
10851113 sprtRunning = false ;
10861114 runSprtBtn . disabled = false ;
10871115 stopSprtBtn . disabled = true ;
1088- // Show/enable download games if we have any ICN logs
1089- const hasGames = gameLogs . length > 0 ;
1090- downloadGamesBtn . disabled = ! hasGames ;
1091- downloadGamesBtn . style . display = hasGames ? '' : 'none' ;
1092- downloadGamesJsonBtn . disabled = ! hasGames ;
1093- downloadGamesJsonBtn . style . display = hasGames ? '' : 'none' ;
10941116}
10951117
10961118function stopSprt ( ) {
@@ -1136,12 +1158,10 @@ function stopSprt() {
11361158 sprtLog ( ' Elo Difference: ' + ( lastElo >= 0 ? '+' : '' ) + lastElo . toFixed ( 1 ) + ' ±' + lastEloError . toFixed ( 1 ) ) ;
11371159 }
11381160 }
1139- // Allow downloads of games if any finished before abort
1161+ // Allow download of any completed games
11401162 const hasGamesAbort = gameLogs . length > 0 ;
1141- downloadGamesBtn . disabled = ! hasGamesAbort ;
1142- downloadGamesBtn . style . display = hasGamesAbort ? '' : 'none' ;
1163+ downloadGamesTxtBtn . disabled = ! hasGamesAbort ;
11431164 downloadGamesJsonBtn . disabled = ! hasGamesAbort ;
1144- downloadGamesJsonBtn . style . display = hasGamesAbort ? '' : 'none' ;
11451165}
11461166
11471167function copyLog ( ) {
@@ -1193,144 +1213,14 @@ function downloadGames() {
11931213 URL . revokeObjectURL ( url ) ;
11941214}
11951215
1196- /**
1197- * Parse an ICN string and extract game metadata and moves into a JSON-friendly object.
1198- * ICN format after headers: turn halfmove/100 fullmove (whitePromo|blackPromo) worldBounds startPosition moves
1199- */
1200- function parseICNToJSON ( icnString ) {
1201- if ( ! icnString || typeof icnString !== 'string' ) return null ;
1202-
1203- const result = {
1204- headers : { } ,
1205- startPosition : null ,
1206- worldBounds : null ,
1207- moves : [ ] ,
1208- turn : 'w' ,
1209- halfmoveClock : 0 ,
1210- fullmoveNumber : 1 ,
1211- promotionRanks : null
1212- } ;
1213-
1214- // Parse headers: [Key "Value"]
1215- const headerRegex = / \[ ( [ ^ \] ] + ) \s + " ( [ ^ " ] * ) " \] / g;
1216- let match ;
1217- let lastHeaderEnd = 0 ;
1218- while ( ( match = headerRegex . exec ( icnString ) ) !== null ) {
1219- result . headers [ match [ 1 ] ] = match [ 2 ] ;
1220- lastHeaderEnd = headerRegex . lastIndex ;
1221- }
1222-
1223- const afterHeaders = icnString . slice ( lastHeaderEnd ) . trim ( ) ;
1224- if ( ! afterHeaders ) return result ;
1225-
1226- // Split by whitespace robustly
1227- const tokens = afterHeaders . split ( / \s + / ) ;
1228-
1229- let idx = 0 ;
1230-
1231- // Token 0: turn (w or b)
1232- if ( tokens . length > idx && ( tokens [ idx ] === 'w' || tokens [ idx ] === 'b' ) ) {
1233- result . turn = tokens [ idx ] ;
1234- idx ++ ;
1235- }
1236-
1237- // Token 1: halfmove/100
1238- if ( tokens . length > idx && tokens [ idx ] . includes ( '/' ) ) {
1239- const halfmovePart = tokens [ idx ] ;
1240- const slashIdx = halfmovePart . indexOf ( '/' ) ;
1241- result . halfmoveClock = parseInt ( halfmovePart . slice ( 0 , slashIdx ) , 10 ) || 0 ;
1242- idx ++ ;
1243- }
1244-
1245- // Token 2: fullmove number
1246- if ( tokens . length > idx && / ^ \d + $ / . test ( tokens [ idx ] ) ) {
1247- result . fullmoveNumber = parseInt ( tokens [ idx ] , 10 ) || 1 ;
1248- idx ++ ;
1249- }
1250-
1251- // Token 3: promotion ranks like (8|1)
1252- if ( tokens . length > idx && tokens [ idx ] . startsWith ( '(' ) && tokens [ idx ] . includes ( '|' ) ) {
1253- result . promotionRanks = tokens [ idx ] ;
1254- idx ++ ;
1255- }
1256-
1257- // Token 4 might be world bounds (format: minX,maxX,minY,maxY - 4 numbers separated by 3 commas)
1258- if ( tokens . length > idx ) {
1259- const token = tokens [ idx ] ;
1260- const commaCount = ( token . match ( / , / g) || [ ] ) . length ;
1261- const hasLetterExceptE = / [ a - d f - z A - D F - Z ] / . test ( token ) ;
1262- const hasPipe = token . includes ( '|' ) ;
1263- const hasGt = token . includes ( '>' ) ;
1264-
1265- if ( commaCount === 3 && ! hasLetterExceptE && ! hasPipe && ! hasGt ) {
1266- result . worldBounds = token ;
1267- idx ++ ;
1268- }
1269- }
1270-
1271- // Remaining tokens: start position and moves
1272- let positionParts = [ ] ;
1273-
1274- for ( let i = idx ; i < tokens . length ; i ++ ) {
1275- const token = tokens [ i ] ;
1276- if ( ! token ) continue ;
1277-
1278- if ( token . includes ( '>' ) ) {
1279- // Moves token (pipe-separated)
1280- const movesRaw = token . split ( '|' ) ;
1281- for ( const moveStr of movesRaw ) {
1282- if ( ! moveStr . includes ( '>' ) ) continue ;
1283- const braceIdx = moveStr . indexOf ( '{' ) ;
1284- const cleanMove = braceIdx !== - 1 ? moveStr . slice ( 0 , braceIdx ) . trim ( ) : moveStr . trim ( ) ;
1285- if ( cleanMove . includes ( '>' ) ) {
1286- result . moves . push ( cleanMove ) ;
1287- }
1288- }
1289- } else if ( token . includes ( '|' ) || / [ a - z A - Z ] / . test ( token ) ) {
1290- // Position piece(s)
1291- const pieces = token . split ( '|' ) . filter ( p => p . length > 0 ) ;
1292- positionParts . push ( ...pieces ) ;
1293- } else if ( token . includes ( ',' ) ) {
1294- // Possible world bounds if not already found, or coordinate-only piece
1295- positionParts . push ( token ) ;
1296- }
1297- }
1298-
1299- if ( positionParts . length > 0 ) {
1300- result . startPosition = positionParts . join ( '|' ) ;
1301- }
1302-
1303- return result ;
1304- }
1305-
13061216
13071217function downloadGamesJson ( ) {
13081218 if ( ! gameLogs . length ) {
13091219 log ( 'No games to download yet' , 'warn' ) ;
13101220 return ;
13111221 }
13121222
1313- const games = gameLogs . map ( ( icn , index ) => {
1314- const parsed = parseICNToJSON ( icn ) ;
1315- return {
1316- gameIndex : index + 1 ,
1317- event : parsed ?. headers ?. Event || null ,
1318- variant : parsed ?. headers ?. Variant || 'Classical' ,
1319- result : parsed ?. headers ?. Result || '*' ,
1320- white : parsed ?. headers ?. White || null ,
1321- black : parsed ?. headers ?. Black || null ,
1322- termination : parsed ?. headers ?. Termination || null ,
1323- timeControl : parsed ?. headers ?. TimeControl || null ,
1324- utcDate : parsed ?. headers ?. UTCDate || null ,
1325- utcTime : parsed ?. headers ?. UTCTime || null ,
1326- worldBounds : parsed ?. worldBounds || null ,
1327- startPosition : parsed ?. startPosition || null ,
1328- moves : parsed ?. moves || [ ] ,
1329- rawICN : icn
1330- } ;
1331- } ) ;
1332-
1333- const jsonOutput = JSON . stringify ( { games, exportedAt : new Date ( ) . toISOString ( ) } , null , 2 ) ;
1223+ const jsonOutput = JSON . stringify ( gameLogs , null , 2 ) ;
13341224 const blob = new Blob ( [ jsonOutput ] , { type : 'application/json' } ) ;
13351225 const url = URL . createObjectURL ( blob ) ;
13361226 const a = document . createElement ( 'a' ) ;
@@ -1348,7 +1238,7 @@ runSprtBtn.addEventListener('click', runSprt);
13481238stopSprtBtn . addEventListener ( 'click' , stopSprt ) ;
13491239copyLogBtn . addEventListener ( 'click' , copyLog ) ;
13501240downloadLogsBtn . addEventListener ( 'click' , downloadLogs ) ;
1351- downloadGamesBtn . addEventListener ( 'click' , downloadGames ) ;
1241+ downloadGamesTxtBtn . addEventListener ( 'click' , downloadGames ) ;
13521242downloadGamesJsonBtn . addEventListener ( 'click' , downloadGamesJson ) ;
13531243sprtVariantsEl . addEventListener ( 'change' , updateSelectedVariants ) ;
13541244
@@ -1425,11 +1315,6 @@ window.__sprt_compute_features = async (rawSamples) => {
14251315} ;
14261316
14271317initWasm ( ) ;
1428- // Initially hide & disable games download until we have results
1429- downloadGamesBtn . disabled = true ;
1430- downloadGamesBtn . style . display = 'none' ;
1431- downloadGamesJsonBtn . disabled = true ;
1432- downloadGamesJsonBtn . style . display = 'none' ;
14331318
14341319/* UI Logic for TC Mode */
14351320if ( typeof sprtTcMode !== 'undefined' && sprtTcMode ) {
0 commit comments