diff --git a/tcl/enginecomm.tcl b/tcl/enginecomm.tcl index 094498583..5e8366518 100644 --- a/tcl/enginecomm.tcl +++ b/tcl/enginecomm.tcl @@ -761,13 +761,9 @@ proc ::uci::parseline {id line} { } if {[string match "bestmove *" $line]} { - lassign [split $line] -> ::engconn(InfoBestMove_$id) ponder ponder_move - #TODO: - # lassign [lsearch -inline -index 0 $::engconn(options_$id) "Ponder"] -> do_ponder - # if {$do_ponder eq "true" && $ponder eq "ponder"} - # set ::engconn(waitReply_$id) "Go?" - # ::engine::rawsend $id position ... - # ::engine::rawsend $id go ponder ... + # assign ponder move as well + # starting ponder should not be done here because other parameter like time or depth not available here + set ::engconn(InfoBestMove_$id) [lrange [split $line] 1 3] return 1 } diff --git a/tcl/menus.tcl b/tcl/menus.tcl index e66623ef3..e4140daa5 100644 --- a/tcl/menus.tcl +++ b/tcl/menus.tcl @@ -250,6 +250,8 @@ $m add command -label ToolsStartEngine1 \ -command "::enginewin::start 1" -accelerator "F2" $m add command -label ToolsStartEngine2 \ -command "::enginewin::start 2" -accelerator "F3" +$m add command -label "Annotate Game(s)" -command "::annotation::doAnnotate" +$m add command -label "Finish Game" -command "::finishgame::finishGameDialog" $m add command -label ToolsAnalysis -command "makeAnalysisWin 1" $m add separator $m add checkbutton -label ToolsFilterGraph \ diff --git a/tcl/options.tcl b/tcl/options.tcl index 102563427..504253c2e 100644 --- a/tcl/options.tcl +++ b/tcl/options.tcl @@ -125,19 +125,26 @@ proc InitDefaultFonts {} { } proc InitDefaultAnnotate {} { - set ::isBatchOpening 0 - set ::isBatchOpeningMoves 12 - set ::isBatch 0 - set ::markTacticalExercises 0 - set ::isAnnotateVar 0 - set ::isShortAnnotation 0 - set ::addScoreToShortAnnotations 0 - set ::addAnnotatorTag 0 - set ::annotateMoves all - set ::annotateBlunders blundersonly - set ::scoreAllMoves 0 - # Blunder Threshold - set ::blunderThreshold 1.0 + set ::annotation::options(typ) "movetime" + set ::annotation::options(movetime) 1000 + set ::annotation::options(time) 1 + set ::annotation::options(depth) 20 + set ::annotation::options(engine) "" + set ::annotation::options(blunderThreshold) 0.5 + set ::annotation::options(annotateMoves) all + set ::annotation::options(annotateBlunders) blundersonly + set ::annotation::options(scoreAllMoves) 1 + set ::annotation::options(useAnalysisBook) 0 + set ::annotation::options(AnalysisBookName) "" + set ::annotation::options(tacticalExercises) 0 + set ::annotation::options(addAnnotatorTag) 1 + set ::annotation::options(OpeningErrors) 0 + set ::annotation::options(OpeningMoves) 8 + set ::annotation::options(annotateShort) 1 + set ::annotation::options(addScoreToShortAnnotations) 1 + set ::annotation::options(batchMode) 0 + set ::annotation::options(batchEnd) 0 + set ::annotation::options(anzVariation) 1 } InitDefaultFonts @@ -337,13 +344,13 @@ set ::sergame::startFromCurrent 0 set ::sergame::coachIsWatching 0 set ::sergame::timeMode "timebonus" set ::sergame::depth 3 -set ::sergame::movetime 0 +set ::sergame::movetime 1000 set ::sergame::nodes 10000 set ::sergame::ponder 0 -set ::uci::uciInfo(wtime3) [expr 5 * 60 * 1000 ] -set ::uci::uciInfo(winc3) [expr 10 * 1000 ] -set ::uci::uciInfo(btime3) [expr 5 * 60 * 1000 ] -set ::uci::uciInfo(binc3) [expr 10 * 1000 ] +set ::sergame::data(wtime) [expr 5 * 60 * 1000 ] +set ::sergame::data(winc) [expr 10 * 1000 ] +set ::sergame::data(btime) [expr 5 * 60 * 1000 ] +set ::sergame::data(binc) [expr 10 * 1000 ] # Defaults for initial directories: set initialDir(base) "." @@ -658,16 +665,15 @@ proc options.write {} { ::sergame::chosenOpening ::sergame::chosenEngine ::sergame::useBook ::sergame::bookToUse \ ::sergame::startFromCurrent ::sergame::coachIsWatching ::sergame::timeMode \ ::sergame::depth ::sergame::movetime ::sergame::nodes ::sergame::ponder ::sergame::isOpening \ - ::uci::uciInfo(wtime3) ::uci::uciInfo(winc3) ::uci::uciInfo(btime3) ::uci::uciInfo(binc3) \ + ::sergame::data(wtime) ::sergame::data(winc) ::sergame::data(btime) ::sergame::data(binc) \ boardfile_lite boardfile_dark \ FilterMaxMoves FilterMinMoves FilterStepMoves FilterMaxElo FilterMinElo FilterStepElo \ - FilterMaxYear FilterMinYear FilterStepYear FilterGuessELO lookTheme ThemePackageFile autoResizeBoard \ - isBatchOpening isBatchOpeningMoves isBatch \ - markTacticalExercises scoreAllMoves \ - isAnnotateVar isShortAnnotation addScoreToShortAnnotations annotateBlunders\ - addAnnotatorTag annotateMoves } { + FilterMaxYear FilterMinYear FilterStepYear FilterGuessELO lookTheme ThemePackageFile autoResizeBoard } { puts $optionF "set $i [list [set $i]]" } + foreach i [lsort [array names ::annotation::options]] { + puts $optionF "set ::annotation::options($i) [list $::annotation::options($i)]" + } puts $optionF "" foreach i [lsort [array names winWidth]] { diff --git a/tcl/start.tcl b/tcl/start.tcl index 51cae61a3..f1da85395 100644 --- a/tcl/start.tcl +++ b/tcl/start.tcl @@ -242,7 +242,7 @@ foreach ns { ::tools::graphs ::tools::graphs::filter ::tools::graphs::absfilter ::tools::graphs::rating ::tools::graphs::score ::tb ::optable - ::board ::move + ::board ::move ::annotation ::tacgame ::sergame ::opening ::tactics ::calvar ::uci ::fics ::reviewgame ::novag ::config ::docking ::pinfo @@ -744,6 +744,9 @@ tools/optable.tcl tools/preport.tcl tools/pinfo.tcl tools/analysis.tcl +tools/enginenowin.tcl +tools/annotate.tcl +tools/finishgame.tcl tools/wbdetect.tcl tools/graphs.tcl tools/ptracker.tcl diff --git a/tcl/tools/annotate.tcl b/tcl/tools/annotate.tcl new file mode 100644 index 000000000..f667ddb5e --- /dev/null +++ b/tcl/tools/annotate.tcl @@ -0,0 +1,579 @@ +### +### annotate.tcl: part of Scid. +### This file is part of Scid (Shane's Chess Information Database). +### Copyright (C) 2025 Uwe Klimmek +### uses code from Fulvio Benini https://github.com/benini/chess_accuracy and analysis.tcl +########################################################################################## +### Annotate Dialog: uses a chess engine to analyze and annotate a chess game. + +namespace eval ::annotation { + + set _Data(BookSlot) 1 + + proc doAnnotate {} { + global ::annotation::options ::annotation::_Data + set w .annotationDialog + # Do not do anything if the window exists + if { [winfo exists $w] } { + raise $w + focus $w + return + } + + win::createDialog $w + ::setTitle $w "Scid: $::tr(Annotate)" + catch {grab $w} + wm resizable $w 0 0 + set f [ttk::frame $w.f] + pack $f -expand 1 + + ttk::labelframe $f.annotate -text $::tr(GameReview) + ttk::frame $f.annotate.typ + ttk::radiobutton $f.annotate.typ.label -text $::tr(AnnotateTime) -variable ::annotation::options(typ) -value "movetime" + ttk::radiobutton $f.annotate.typ.ldepth -text "Depth per move" -variable ::annotation::options(typ) -value "depth" + ttk::spinbox $f.annotate.typ.spDelay -width 5 -textvariable ::annotation::options(time) -from 0.1 -to 999 \ + -validate key -justify right -validatecommand { regexp {^[0-9]*\.?[0-9]*$} %P } + ttk::spinbox $f.annotate.typ.depth -width 5 -textvariable ::annotation::options(depth) -from 2 -to 999 -validate key -justify right + ttk::radiobutton $f.annotate.allmoves -text $::tr(AnnotateAllMoves) -variable ::annotation::options(annotateBlunders) -value allmoves + ttk::radiobutton $f.annotate.blundersonly -text $::tr(AnnotateBlundersOnly) -variable ::annotation::options(annotateBlunders) -value blundersonly + ttk::frame $f.annotate.blunderbox + ttk::label $f.annotate.blunderbox.label -text $::tr(BlundersThreshold:) + ttk::spinbox $f.annotate.blunderbox.spBlunder -width 4 -validate key -textvariable ::annotation::options(blunderThreshold) \ + -from 0.1 -to 3.0 -increment 0.1 -justify right -validatecommand { regexp {^[0-9]*\.?[0-9]*$} %P } + ttk::checkbutton $f.annotate.cbBook -text $::tr(UseBook) -variable ::annotation::options(useAnalysisBook) + ::engineNoWin::createEngineOptionsFrame $f annotateEngine ::annotation::options(engine) 3 ::annotation::eng_messages + + # load book names + lassign [getBookList $options(AnalysisBookName)] idx tmp + # No book found + if { $idx < 0 } { + set options(useAnalysisBook) 0 + $f.annotate.cbBook configure -state disabled + } + if { $options(AnalysisBookName) eq "" } { set options(AnalysisBookName) [lindex $tmp $idx] } + ttk::combobox $f.annotate.comboBooks -width 12 -values $tmp -textvariable ::annotation::options(AnalysisBookName) + catch { $f.annotate.comboBooks current $idx } + pack $f.annotate.blunderbox.label -side left -padx { 20 0 } + pack $f.annotate.blunderbox.spBlunder -side left -anchor w + pack $f.annotate.typ -side top -anchor w + pack $f.annotateEngine -in $f.annotate -side top -anchor w + pack $f.annotate.allmoves $f.annotate.blundersonly $f.annotate.blunderbox -side top -anchor w + pack $f.annotate.cbBook -side top -anchor w + pack $f.annotate.comboBooks -side top -anchor w -padx 20 + grid $f.annotate.typ.label -row 0 -column 0 -sticky w + grid $f.annotate.typ.ldepth -row 1 -column 0 -sticky w + grid $f.annotate.typ.spDelay -row 0 -column 1 -sticky w + grid $f.annotate.typ.depth -row 1 -column 1 -sticky w + bind $w { .configAnnotation.f.buttons.cancel invoke } + bind $w { .configAnnotation.f.buttons.ok invoke } + + ttk::labelframe $f.av -text $::tr(AnnotateWhich) + ttk::radiobutton $f.av.all -text $::tr(AnnotateAll) -variable ::annotation::options(annotateMoves) -value all + ttk::radiobutton $f.av.white -text $::tr(AnnotateWhite) -variable ::annotation::options(annotateMoves) -value white + ttk::radiobutton $f.av.black -text $::tr(AnnotateBlack) -variable ::annotation::options(annotateMoves) -value black + ttk::checkbutton $f.av.vars -text "Store two variations" -variable ::annotation::options(anzVariation) -onvalue 2 -offvalue 1 + pack $f.av.all $f.av.white $f.av.black $f.av.vars -side top -fill x -anchor w + + ttk::labelframe $f.comment -text $::tr(Comments) + # Checkmark to enable all-move-scoring + ttk::checkbutton $f.comment.scoreAll -text $::tr(ScoreAllMoves) -variable ::annotation::options(scoreAllMoves) + ttk::checkbutton $f.comment.cbShortAnnotation -text $::tr(ShortAnnotations) -variable ::annotation::options(annotateShort) + ttk::checkbutton $f.comment.cbAddScore -text $::tr(AddScoreToShortAnnotations) -variable ::annotation::options(addScoreToShortAnnotations) + ttk::checkbutton $f.comment.cbAddAnnotatorTag -text $::tr(addAnnotatorTag) -variable ::annotation::options(addAnnotatorTag) + ttk::checkbutton $f.comment.cbMarkTactics -text $::tr(MarkTacticalExercises) -variable ::annotation::options(tacticalExercises) + pack $f.comment.scoreAll $f.comment.cbShortAnnotation $f.comment.cbAddScore \ + $f.comment.cbAddAnnotatorTag $f.comment.cbMarkTactics -fill x -anchor w + # batch annotation of consecutive games, and optional opening errors finder + ttk::labelframe $f.batch -text "Batch Annotation" + ttk::frame $f.buttons + ttk::frame $f.running + ttk::label $f.running.line1 -textvariable ::annotation::_Data(msg1) -width 60 + ttk::label $f.running.line2 -textvariable ::annotation::_Data(msg2) -width 10 + ttk::label $f.running.line3 -textvariable ::annotation::_Data(msg3) -width 10 + ttk::progressbar $f.running.progress -variable ::annotation::_Data(progress) -orient horizontal -length 600 + ttk::progressbar $f.running.games -variable ::annotation::options(games) -orient horizontal -length 600 + grid $f.running.line1 -row 0 -column 1 -sticky w -pady { 0 10 } + grid $f.running.line2 -row 1 -column 0 -sticky w + grid $f.running.line3 -row 2 -column 0 -sticky w + grid $f.running.games -row 1 -column 1 -sticky w + grid $f.running.progress -row 2 -column 1 -sticky w + grid $f.annotate -row 0 -column 0 -pady { 0 10 } -sticky nswe -padx { 0 10 } + grid $f.comment -row 0 -column 1 -pady { 0 10 } -sticky nswe -padx { 10 0 } + grid $f.av -row 1 -column 0 -pady { 10 0 } -sticky nswe -padx { 0 10 } + grid $f.batch -row 1 -column 1 -pady { 10 0 } -sticky nswe -padx { 10 0 } + grid $f.buttons -row 3 -column 1 -sticky we + + set options(batchEnd) [sc_base numGames $::curr_db] + if {$options(batchEnd) <1} { set options(batchEnd) 1 } + ttk::checkbutton $f.batch.cbBatch -text $::tr(AnnotateSeveralGames) -variable ::annotation::options(batchMode) + ttk::spinbox $f.batch.spBatchEnd -width 8 -textvariable ::annotation::options(batchEnd) \ + -from 1 -to $options(batchEnd) -increment 1 -validate all -validatecommand { regexp {^[0-9]+$} %P } + ttk::checkbutton $f.batch.cbBatchOpening -text $::tr(FindOpeningErrors) -variable ::annotation::options(OpeningErrors) + ttk::spinbox $f.batch.spBatchOpening -width 2 -textvariable ::annotation::options(OpeningMoves) \ + -from 10 -to 20 -increment 1 -validate all -validatecommand { regexp {^[0-9]+$} %P } + ttk::label $f.batch.lBatchOpening -text $::tr(moves) + pack $f.batch.cbBatch -side top -anchor w -pady { 0 0 } + pack $f.batch.spBatchEnd -side top -padx 20 -anchor w + pack $f.batch.cbBatchOpening -side top -anchor w + pack $f.batch.spBatchOpening -side left -anchor w -padx { 20 4 } + pack $f.batch.lBatchOpening -side left + + ttk::button $f.buttons.cancel -text $::tr(Cancel) -command { + if { $::autoplayMode } { + set ::autoplayMode 0 + } else { + ::engineNoWin::closeEngine annotateEngine + destroy .annotationDialog + } + } + ttk::button $f.buttons.ok -text "Annotate" -command { + if {$::annotation::options(time) < 0.1} { set ::annotation::options(time) 0.1 } + set ::annotation::options(movetime) [expr {int($::annotation::options(time) * 1000.0)}] + if { [::engineNoWin::initEngine annotateEngine $::annotation::options(engine) \ + [list ::annotation::eng_messages annotateEngine .annotationDialog.f.engpara]] } { + ::annotation::runAnnotation + } + } + pack $f.buttons.cancel $f.buttons.ok -side right -padx 5 -pady 5 + focus $f.annotate.typ.spDelay + bind $w { focus . } + } + + # reset values for every game + proc initGameAnnotation { } { + global ::annotation::options ::annotation::_Data + #reset engine + ::engine::send annotateEngine NewGame [list analysis post_pv post_wdl [sc_game variant]] + # calc amount of moves to analyze for progressbar + set firstmove [llength [sc_game moves]] + sc_game push copyfast + catch { sc_move forward 300 } + set anz [expr {[llength [sc_game moves]] - $firstmove + 1}] + sc_game pop + .annotationDialog.f.running.progress configure -maximum $anz + #reset values + set _Data(prevscore1) 0 + set _Data(prevscore2) 0 + set _Data(score) 0 + set _Data(scoremate) 0 + set _Data(prevscoremate) 0 + set _Data(prevmoves1) "" + set _Data(prevmoves2) "" + set _Data(moves) "" + set _Data(progress) 1 + set _Data(msg1) "$::tr(game) [sc_game number]: [sc_game info white] - [sc_game info black]" + set _Data(msg2) "$::tr(game) $options(games)" + set _Data(msg3) "$::tr(move) 1" + if { $options(addAnnotatorTag) } { + appendAnnotator "$options(engine) $options(typ) $options($options(typ))" + } + } + + proc annotateGame { } { + global ::annotation::options ::annotation::_Data + initGameAnnotation + makeBookAnnotation + # Annotate all remaining moves of the game + while { 1 } { + set _Data(PV1) [list "" "" ""] + set _Data(PV2) [list "" "" ""] + ::engine::send annotateEngine Go [list [sc_game UCI_currentPos] [list $options(typ) $options($options(typ))]] + vwait ::annotation::_Data(move_done) + addAnnotation + incr _Data(progress) + set _Data(msg3) "$::tr(move) $_Data(progress)" + if {[sc_pos isAt end] || ! $::autoplayMode } break + sc_move forward + ::notify::PosChanged -pgn + } + } + + proc runAnnotation { } { + global ::annotation::options + # make sure, we have 2 best lines + ::engine::send annotateEngine SetOptions [list {MultiPV 2}] + set f .annotationDialog.f + grid forget $f.annotate $f.comment $f.av $f.batch $f.optsannotateEngine + pack forget $f.buttons.ok + if {!$options(batchMode)} { grid forget $f.running.games $f.running.line2 } + # show progressbar and game infos + set options(games) 1 + set gameNo [sc_game number] + $f.running.games configure -maximum [expr {$options(batchEnd) - $gameNo + 1}] + grid $f.running -row 2 -column 0 -columnspan 2 -sticky we + + # tactical positions is selected, must be in multipv mode + if {$options(tacticalExercises)} { ::engine::send annotateEngine SetOptions [list {MultiPV 4}] } + + set ::autoplayMode 1 + set gameNo [sc_game number] + annotateGame + while {$options(batchMode)} { + sc_game save $gameNo + incr gameNo + incr options(games) + if { ! $::autoplayMode || $gameNo > $options(batchEnd) } { break } + sc_game load $gameNo + annotateGame + } + set ::autoplayMode 0 + ::engineNoWin::closeEngine annotateEngine + ::notify::PosChanged -pgn + destroy .annotationDialog + } + + ################################################################################ + # Part of annotation process : will check the moves if they are in te book, and add a comment + # when going out of it + ################################################################################ + proc makeBookAnnotation { } { + global ::annotation::options ::annotation::_Data + if {$options(useAnalysisBook)} { + set prevbookmoves "" + set bn [ file join $::scidBooksDir $options(AnalysisBookName) ] + sc_book load $bn $_Data(BookSlot) + + lassign [sc_book moves $_Data(BookSlot)] bookmoves + while {[string length $bookmoves] != 0 && ![sc_pos isAt vend]} { + # we are in book, so move immediately forward + ::move::Forward + set prevbookmoves $bookmoves + lassign [sc_book moves $_Data(BookSlot)] bookmoves + } + sc_book close $_Data(BookSlot) + + if { [ string match -nocase "*[sc_game info previousMoveNT]*" $prevbookmoves ] != 1 } { + if {$prevbookmoves != ""} { + sc_pos setComment "[sc_pos getComment] $::tr(LastBookMove) [::trans $prevbookmoves]" + } else { + sc_pos setComment "[sc_pos getComment] $::tr(LastBookMove)" + } + # last move was out of book: it needs to be analyzed, so take back + sc_move back + } else { + sc_pos setComment "[sc_pos getComment] $::tr(MoveOutOfBook)" + } + if { $options(OpeningErrors) && ([sc_pos moveNumber] < $options(OpeningMoves) ) } { + appendAnnotator "opBlunder [sc_pos moveNumber] ([sc_pos side])" + } + } + } + + ################################################################################ + # will append arg to current game Annotator tag + ################################################################################ + proc appendAnnotator { s } { + # Get the current collection of extra tags + set extra [sc_game tags get "Extra"] + set annot 0 + set other "" + set nExtra {} + # Walk through the extra tags, just copying the crap we do not need + # If we meet the existing annotator tag, add our name to the list + foreach line $extra { + if { $annot == 1 } { + lappend nExtra "Annotator \"$line, $s\"\n" + set annot 2 + } elseif { $other != "" } { + lappend nExtra "$other \"$line\"\n" + set other "" + } elseif {[string match "Annotator" $line]} { + set annot 1 + } else { + set other $line + } + } + # First annotator: Create a tag + if { $annot == 0 } { + lappend nExtra "Annotator \"$s\"\n" + } + # Put the extra tags back to the game + sc_game tags set -extra $nExtra + } + + proc addAnnotation { } { + global ::annotation::options ::annotation::_Data + # Let's try to assess the situation: + # We are here, now that the engine has analyzed the position reached by + # our last move. Currently it is the opponent to move: + set tomove [sc_pos side] + set gamemove [sc_game info previousMoveUCI] + + # And this is his best line: + lassign $_Data(PV1) score score_type _Data(moves) + if { $gamemove eq "" || $score eq "" } { set _Data(prevscore1) $score; return } + set moves $_Data(moves) + set bestMoveIsMate 0 + if { $score_type eq "mate" } { + # We do not want to insert a best-line variation into the game + # if we did play along that line. Even not when annotating all moves. + # It simply makes no sense to do so (unless we are debugging the engine!) + # Sooner or later the game will deviate anyway; a variation at that point will + # do nicely and is probably more accurate as well. + set bestMoveIsMate 1 + set _Data(scoremate) $score + set score [expr { $score < 0 ? -127 : 127 }] + set _Data(score) $score + } else { + set _Data(score) $score + set _Data(scoremate) 0 + } + + # We will add a closing line at the end of variation or game + set addClosingLine 0 + if { [sc_pos isAt vend] } { + set addClosingLine 1 + } + + # This is the score we could have had if we had played our best move + set prevscore $_Data(prevscore1) + + # Note that the engine's judgement is in relative terms, a negative score + # being favorable to opponent, a positive score favorable to player + # Looking primarily for blunders, we are interested in the score decay, + # which, for white, is (previous-current) + set deltamove [expr {$prevscore + $score}] + # and whether the game was already lost for us + set gameIsLost [expr {$prevscore < (0.0 - $::informant("+--"))}] + + # Invert this logic for black + if { $tomove == "white" } { + set gameIsLost [expr {$prevscore > $::informant("+--")}] + } + + # Set an "isBlunder" filter. + # Let's mark moves with a decay greater than the threshold. + set isBlunder 0 + if { $deltamove > $options(blunderThreshold) } { + set isBlunder 2 + } elseif { $deltamove > 0 } { + set isBlunder 1 + } + set absdeltamove [expr { abs($deltamove) } ] + + # to parse scores if the engine's name contains - or + chars (see sc_game_scores) + set engine_name [string map {"-" " " "+" " "} $options(engine)] + + # Prepare score strings for the opponent + if { $_Data(scoremate) != 0 } { + set text [format "M%d" [expr abs($_Data(scoremate))]] + } else { + set wscore [format "%+.2f" $score] + if { $tomove eq "black" } {set wscore [expr 0.0 - $wscore] } + set text "\[%eval $wscore\]" + } + + # See if we have the threshold filter activated. + # If so, take only bad moves and missed mates until the position is lost anyway + # Or that we must annotate all moves + if { ( $options(annotateBlunders) == "blundersonly" + && ($isBlunder > 1 || ($isBlunder > 0 && [expr abs($score)] >= 327.0)) + && ! $gameIsLost) + || ($options(annotateBlunders) == "allmoves") } { + if { $isBlunder > 0 } { + # Add move score nag, and possibly an exercise + if { $absdeltamove > $::informant("??") } { + markExercise $prevscore $score "??" + } elseif { $absdeltamove > $::informant("?") } { + markExercise $prevscore $score "?" + } elseif { $absdeltamove > $::informant("?!") } { + sc_pos addNag "?!" + } + } elseif { $absdeltamove > $::informant("!?") } { + sc_pos addNag "!?" + } + + # Add score comment and engine name if needed + if { ! $options(annotateShort) } { + sc_pos setComment "[sc_pos getComment] $engine_name: $text" + } elseif { $options(addScoreToShortAnnotations) || $options(scoreAllMoves) } { + sc_pos setComment "[sc_pos getComment] $text" + } + + # Add position score nag + sc_pos addNag [scoreToNag $score] + # Add the variation + sc_move back + if { $options(annotateBlunders) == "blundersonly" } { + # Add a diagram tag, but avoid doubles + if { [string first "D" "[sc_pos getNags]"] == -1 } { + sc_pos addNag "D" + } + } + + if { $_Data(prevmoves1) != "" && ( $options(annotateMoves) == "all" || + $options(annotateMoves) == "white" && $tomove == "black" || + $options(annotateMoves) == "black" && $tomove == "white" )} { + set n 1 + while { $n <= $options(anzVariation) && $_Data(prevmoves$n) ne "" } { + sc_var create + # Add the starting move + sc_move addSan [lrange $_Data(prevmoves$n) 0 0] + # Add its score + if { ! $options(annotateShort) || $options(addScoreToShortAnnotations) } { + # And for the (missed?) chance + if { $_Data(prevscoremate) != 0 } { + set prevtext [format "M%d" [expr abs($_Data(prevscoremate))]] + } else { + set wprevscore [format "%+.2f" $_Data(prevscore$n)] + if { $tomove eq "white" } {set wprevscore [expr 0.0 - $wprevscore] } + set prevtext "\[%eval $wprevscore\]" + } + sc_pos setComment "$prevtext" + } + # Add remaining moves + sc_move addSan [lrange $_Data(prevmoves$n) 1 end] + # Add position NAG, unless the line ends in mate + if { $n == 1 && $_Data(prevscoremate) == 0 } { + sc_pos addNag [scoreToNag $prevscore] + } + sc_var exit + incr n + } + } + sc_move forward + } else { + if { $isBlunder == 0 && $absdeltamove > $::informant("!?") } { + sc_pos addNag "!?" + } + if { $options(scoreAllMoves) } { + # Add a score mark anyway + sc_pos setComment "[sc_pos getComment] $text" + } + } + + if { $addClosingLine } { + sc_move back + sc_var create + sc_move addSan $gamemove + if { ($_Data(scoremate) == 0) && ( ! $options(annotateShort) || $options(addScoreToShortAnnotations)) } { + sc_pos setComment "$text" + } + sc_move addSan $moves + if { $_Data(scoremate) == 0 } { + sc_pos addNag [scoreToNag $score] + } + sc_var exit + # Now up to the end of the game + ::move::Forward + } + set _Data(prevscore1) $_Data(score) + set _Data(prevmoves1) $_Data(moves) + lassign $_Data(PV2) _Data(prevscore2) score_type _Data(prevmoves2) + set _Data(prevscoremate) $_Data(scoremate) + updateBoard -pgn + } + + ################################################################################ + # Will add **** to any position considered as a tactical shot + # check at which depth the tactical shot is found + ################################################################################ + proc markExercise { prevscore score nag} { + global ::annotation::options ::annotation::_Data + sc_pos addNag $nag + if { ! $options(tacticalExercises)} { return 0 } + + set deltamove [expr {$score + $prevscore}] + # filter tactics so only those with high gains are kept + if { [expr abs($deltamove)] < $::informant("+/-") } { return 0 } + # dismiss games where the result is already clear (high score,and we continue in the same way) + if { [expr $prevscore * $score] >= 0} { + if { [expr abs($prevscore) ] > $::informant("+--") } { return 0 } + if { [expr abs($prevscore)] > $::informant("+-") && [expr abs($score) ] < [expr 2 * abs($prevscore)]} { return 0 } + } + + # The best move is much better than others. + set sc2 [lindex $_Data(PV2) 0] + if { [expr abs( $score - $sc2 )] < 1.5 } { return 0 } + + # The best move does not lose position. + if {([sc_pos side] == "black") && ($score < [expr 0.0 - $::informant("+/-")]) } { return 0 } + if {([sc_pos side] == "white") && ($score > $::informant("+/-")) } { return 0} + + # Move is not obvious: check that it is not the first move guessed at low depths + set pv [ lindex [ lindex $_Data(PV1) 2 ] 0 ] + # bm0 must SAN, pv is UCI: convert + set bm0 [string range [lindex $pv 0] 0 4] + set bm0 [sc_pos coordToSAN $_Data(position) $bm0] + set bm0 [string range $bm0 [expr [string first "." $bm0] + 1] end] + + foreach depth {1 2 3} { + set res [ sc_pos analyze -time 1000 -hashkb 32 -pawnkb 1 -searchdepth $depth ] + set bm$depth [lindex $res 1] + } + if { $bm0 == $bm1 && $bm0 == $bm2 && $bm0 == $bm3 } { + return 0 + } + + # find what time is needed to get the solution (use internal analyze function) + set timer {1 2 5 10 50 100 200 1000} + set movelist {} + for {set t 0} {$t < [llength $timer]} { incr t} { + set res [sc_pos analyze -time [lindex $timer $t] -hashkb 1 -pawnkb 1 -mindepth 0] + set move_analyze [lindex $res 1] + lappend movelist $move_analyze + } + + # find at what timing the right move was reliably found + # only the move is checked, not if the score is close to the expected one + for {set t [expr [llength $timer] -1]} {$t >= 0} { incr t -1} { + if { [lindex $movelist $t] != $bm0 } { + break + } + } + set difficulty [expr $t +2] + + # If the base opened is read only, like a PGN file, avoids an exception + catch { sc_base gameflag [sc_base current] [sc_game number] set T } + sc_pos setComment "****D${difficulty} [format %.1f $prevscore]->[format %.1f $score] [sc_pos getComment]" + updateBoard + return 1 + } + + proc ::annotation::eng_messages {id w msg} { + global ::annotation::_Data + lassign $msg msgType msgData + switch $msgType { + "InfoConfig" { + if { $::autoplayMode } { return } + set msgData [lindex $msgData 2] + ::engineNoWin::initEngineOptions $id $w $msgData + } + "InfoPV" { + lassign $msgData multipv depth seldepth nodes nps hashfull tbhits time score score_type score_wdl pv + if { $score_type ne "mate" } { set score [expr {$score / 100.0}] } + set _Data(PV$multipv) [list $score $score_type $pv] + } + "InfoBestMove" { + lassign $msgData _Data(bestmove) + set _Data(move_done) 1 + } + "InfoGo" { + lassign $msgData _Data(position) + } + "InfoDisconnected" { + ::engineNoWin::disconnected $id $msgData + set ::autoplayMode 0 + } + } + } + ################################################################################ + # + ################################################################################ + proc scoreToNag {score} { + # Informant index strings + array set ana_informantList { 0 "+=" 1 "+/-" 2 "+-" 3 "+--" } + # Nags. Note the slight inconsistency for the "crushing" symbol (see game.cpp) + array set ana_nagList { 0 "=" 1 "+=" 2 "+/-" 3 "+-" 4 "+--" 5 "=" 6 "=+" 7 "-/+" 8 "-+" 9 "--+" } + # Find the score in the informant map + set tmp [expr { abs( $score ) }] + for { set i 0 } { $i < 4 } { incr i } { + if { $tmp < $::informant("$ana_informantList($i)") } { break } + } + # Jump into negative counterpart + if { $score < 0.0 } { + set i [expr {$i + 5}] + } + return $ana_nagList($i) + } +} diff --git a/tcl/tools/enginenowin.tcl b/tcl/tools/enginenowin.tcl new file mode 100644 index 000000000..24d52afb5 --- /dev/null +++ b/tcl/tools/enginenowin.tcl @@ -0,0 +1,103 @@ +### +### enginenowin.tcl: part of Scid. +### This file is part of Scid (Shane's Chess Information Database). +### Copyright (C) 2025 Uwe Klimmek +### uses code from Fulvio Benini https://github.com/benini/chess_accuracy and analysis.tcl +########################################################################################## +### procs for using engines without a engine window + +# engineNoWin will be used by annotate and finish game +namespace eval ::engineNoWin {} +# Open the engine and configure it +proc ::engineNoWin::initEngine { id engine callback } { + if { [info exists ::enginewin::engConfig_$id] } { return 1 } + set config [::enginecfg::get $engine] + lassign $config name cmd args wdir elo time url uci options + set ::enginewin::engConfig_$id $config + ::engine::setLogCmd $id {} + ::engine::connect $id $callback $cmd $args + if { $options ne "" } { ::engine::send $id SetOptions $options } + return 1 +} + +proc ::engineNoWin::closeEngine { id } { + ::engine::close $id + unset -nocomplain ::enginewin::engConfig_$id +} + +proc ::engineNoWin::changeEngine {id w enginevar callback} { + ::engineNoWin::closeEngine $id + $w.text configure -state normal + $w.text delete 1.0 end + foreach wchild [winfo children $w.text] { destroy $wchild } + set engine [set $enginevar] + ::engineNoWin::initEngine $id $engine [list $callback $id $w] +} + +proc ::engineNoWin::showHideOptionsFrame {id w enginevar callback col} { + if { [winfo ismapped $w] } { grid forget $w ; return } + grid $w -row 0 -column $col -rowspan 5 -sticky ne -padx 10 + set engine [set $enginevar] + ::engineNoWin::initEngine $id $engine [list $callback $id $w] +} + +#create frame for select and edit engine options +#engType: all, uci or winboard +proc ::engineNoWin::createEngineOptionsFrame {f id var col callback {engTyp "uci"}} { + ttk::frame $f.$id + set allEngList [::enginecfg::names ] + if { $engTyp ne "all"} { + set engList {} + foreach name $allEngList { + set typ [lindex [::enginecfg::get $name] 7] + if { $engTyp == "uci" && $typ || $engTyp == "winboard" && ! $typ } { + lappend engList $name + } + } + } else { + set engList $allEngList + } + if { [set $var] eq "" } { set $var [lindex $engList 0] } + ttk::combobox $f.$id.eng -width 20 -state readonly -values $engList -textvariable $var + bind $f.$id.eng <> "::engineNoWin::changeEngine $id $f.opts$id $var $callback" + ttk::button $f.$id.opts -image ::icon::filter_adv -style Toolbutton \ + -command "::engineNoWin::showHideOptionsFrame $id $f.opts$id $var $callback $col" + pack $f.$id.eng $f.$id.opts -side left -padx { 0 5 } + ttk::labelframe $f.opts$id -text "Engine Parameter" + ttk::label $f.opts$id.l -textvariable $var + ttk::button $f.opts$id.x -image tb_close -style Toolbutton -command "grid forget $f.opts$id" + ttk_text $f.opts$id.text -wrap none -padx 4 + autoscrollBars both $f.opts$id $f.opts$id.text 1 + $f.opts$id.text configure -state normal -wrap word -width 60 -height 18 + ttk::button $f.opts$id.save -text "Save Setup" -command "::engineNoWin::saveEngineSetup $id" + grid $f.opts$id.l -row 0 -column 0 -sticky w + grid $f.opts$id.x -row 0 -column 1 -sticky e + grid $f.opts$id.save -row 2 -column 0 -columnspan 2 -sticky e -pady { 5 0 } + bind $f.$id "::engineNoWin::closeEngine $id" +} + +proc ::engineNoWin::initEngineOptions {id w options} { + upvar ::enginewin::engConfig_$id engConfig_ + if { ! [winfo exists $w.text.reset] } { + lset ::enginewin::engConfig_$id 8 $options + ::enginecfg::createOptionWidgets $id $w $options + ::engine::replyInfoConfig $id + } else { + lset ::enginewin::engConfig_$id 8 $options + ::enginecfg::updateOptionWidgets $id $w $options {} + $w.text configure -state disabled + } +} + +proc ::engineNoWin::saveEngineSetup { id } { + upvar ::enginewin::engConfig_$id engConfig_ + ::enginecfg::save [set ::enginewin::engConfig_$id] +} + +proc ::engineNoWin::disconnected { id data } { + upvar ::enginewin::engConfig_$id engConfig_ + lassign $data errorMsg + lassign [set ::enginewin::engConfig_$id] engine + if {$errorMsg eq ""} { set errorMsg "The connection with the engine $id $engine terminated unexpectedly." } + tk_messageBox -icon warning -type ok -parent . -message $errorMsg +} diff --git a/tcl/tools/finishgame.tcl b/tcl/tools/finishgame.tcl new file mode 100644 index 000000000..b3d541d7e --- /dev/null +++ b/tcl/tools/finishgame.tcl @@ -0,0 +1,242 @@ +### +### finishgame.tcl: part of Scid. +### This file is part of Scid (Shane's Chess Information Database). +### Copyright (C) 2025 Uwe Klimmek +### uses code from Fulvio Benini https://github.com/benini/chess_accuracy and analysis.tcl +########################################################################################## +### finishGame Dialog: uses a chess engine to play a game + +namespace eval ::finishgame { + + set ::finishGame(annotate) 1 + set ::finishGame(annotateShort) 1 + set ::finishGame(enginewhite) "" + set ::finishGame(engineblack) "" + set ::finishGame(cmdwhite) movetime + set ::finishGame(cmdblack) movetime + set ::finishGame(cmdValuewhite) 2 + set ::finishGame(cmdValueblack) 2 + set ::finishGame(msg) "" + + ################################################################################ + # will ask engine(s) to play the game till the end + ################################################################################ + proc finishGameDialog { } { + if { $::autoplayMode } { return } + + # UCI engines + # On exit save values in options.dat + ::options.store ::finishGame(annotate) + ::options.store ::finishGamen(annotateShort) + ::options.store ::finishGame(enginewhite) + ::options.store ::finishGame(engineblack) + + set w .configFinishGame + win::createDialog $w + wm resizable $w 0 0 + ::setTitle $w "Scid: $::tr(FinishGame)" + + ttk::labelframe $w.wh_f -text "$::tr(White)" -padding 5 + grid $w.wh_f -column 0 -row 0 -columnspan 2 -sticky we -pady 8 + foreach psize $::boardSizes { + if {$psize >= 40} { break } + } + ttk::label $w.wh_f.p -image wk$psize + grid $w.wh_f.p -column 0 -row 0 -rowspan 3 + ttk::spinbox $w.wh_f.cv -width 3 -textvariable ::finishGame(cmdValuewhite) -from 1 -to 999 -justify right + ttk::radiobutton $w.wh_f.c1 -text $::tr(seconds) -variable ::finishGame(cmdwhite) -value "movetime" + ttk::radiobutton $w.wh_f.c2 -text $::tr(FixedDepth) -variable ::finishGame(cmdwhite) -value "depth" + ::engineNoWin::createEngineOptionsFrame $w fgEnginewhite ::finishGame(enginewhite) 4 ::finishgame::eng_messages + grid $w.fgEnginewhite -in $w.wh_f -column 1 -row 0 -columnspan 3 -sticky w + grid $w.wh_f.cv -column 1 -row 2 -sticky w + grid $w.wh_f.c1 -column 2 -row 2 -sticky w -padx 6 + grid $w.wh_f.c2 -column 3 -row 2 -sticky w + + ttk::labelframe $w.bk_f -text "$::tr(Black)" -padding 5 + grid $w.bk_f -column 0 -row 1 -columnspan 2 -sticky we -pady 8 + ttk::label $w.bk_f.p -image bk$psize + grid $w.bk_f.p -column 0 -row 0 -rowspan 3 + ttk::spinbox $w.bk_f.cv -width 3 -textvariable ::finishGame(cmdValueblack) -from 1 -to 999 -justify right + ttk::radiobutton $w.bk_f.c1 -text $::tr(seconds) -variable ::finishGame(cmdblack) -value "movetime" + ttk::radiobutton $w.bk_f.c2 -text $::tr(FixedDepth) -variable ::finishGame(cmdblack) -value "depth" + ::engineNoWin::createEngineOptionsFrame $w fgEngineblack ::finishGame(engineblack) 5 ::finishgame::eng_messages + grid $w.fgEngineblack -in $w.bk_f -column 1 -row 0 -columnspan 3 -sticky w + grid $w.bk_f.cv -column 1 -row 2 -sticky w + grid $w.bk_f.c1 -column 2 -row 2 -sticky w -padx 6 + grid $w.bk_f.c2 -column 3 -row 2 -sticky w + + ttk::checkbutton $w.finishGame -text $::tr(Annotate) -variable ::finishGame(annotate) + grid $w.finishGame -column 0 -row 2 -sticky w -padx 5 -pady 8 + ttk::checkbutton $w.finishGameShort -text $::tr(ShortAnnotations) -variable ::finishGame(annotateShort) + grid $w.finishGameShort -column 1 -row 2 -sticky w -padx 5 -pady 8 + ttk::label $w.line1 -textvariable ::finishGame(msg) -width 60 + + ttk::frame $w.fbuttons + ttk::button $w.fbuttons.cancel -text $::tr(Cancel) -command { + if { $::autoplayMode } { + set ::autoplayMode 0 + } else { + ::engineNoWin::closeEngine fgEnginewhite + ::engineNoWin::closeEngine fgEngineblack + destroy .configFinishGame + } + } + + ttk::button $w.fbuttons.ok -text "OK" -command { + if { [::engineNoWin::initEngine fgEnginewhite $::finishGame(enginewhite) \ + [list ::finishgame::eng_messages fgEnginewhite .configFinishGame.optsfgEnginewhite]] && + [::engineNoWin::initEngine fgEngineblack $::finishGame(engineblack) \ + [list ::finishgame::eng_messages fgEngineblack .configFinishGame.optsfgEngineblack]] } { + ::finishgame::runFinishGame + } + } + packbuttons right $w.fbuttons.cancel $w.fbuttons.ok + grid $w.fbuttons -row 3 -column 1 -columnspan 2 -sticky we + focus $w.fbuttons.ok + bind $w { .configFinishGame.cancel invoke } + bind $w { .configFinishGame.ok invoke } + bind $w { focus . } + grab $w + } + + # Open the engine and configure it + proc initfgEngine { color engine } { + set id fgEngine$color + if { [info exists ::enginewin::engConfig_$id] } { return "ok" } + set config [::enginecfg::get $engine] + lassign $config name cmd args wdir elo time url uci options + if { ! $uci } { return "Only UCI-Engines are supported!" } + set ::enginewin::engConfig_$id [list $name $cmd $args $wdir $elo $time $url $uci {}] + ::engine::setLogCmd $id {} + ::engine::connect $id [list ::finishgame::eng_messages $id] $cmd {} + lappend options "MultiPV 2" + ::engine::send $id SetOptions $options + return "ok" + } + + proc ::finishgame::annotate { tomove } { + lassign $::finishGame(PV1) score score_type pv + if { $tomove eq "black" } {set score [expr 0.0 - $score] } + if {! $::finishGame(annotateShort) } { + sc_var create + # Add the starting move + sc_move addSan $pv + sc_var exit + } + if {$::finishGame(annotate) } { + set tmp [sc_pos getComment] + if { $score_type eq "mate" } { + set score "M$score" + } else { + set score "\[%eval $score\]" + } + sc_pos setComment "$tmp $score" + } + } + + proc ::finishgame::runFinishGame { } { + set w .configFinishGame + grid forget $w.wh_f $w.bk_f $w.finishGame $w.finishGameShort $w.optsfgEnginewhite $w.optsfgEngineblack + pack forget $w.fbuttons.ok + grid $w.line1 -row 2 -column 0 -columnspan 2 -sticky we + + set ::autoplayMode 1 + set repetition {} + set moves 0 + set material 0 + set pawns "" + set tomove [sc_pos side] + set value(white) $::finishGame(cmdValuewhite) + set value(black) $::finishGame(cmdValueblack) + if { $::finishGame(cmdwhite) eq "movetime" } { set value(white) [expr {$::finishGame(cmdValuewhite) * 1000 }] } + if { $::finishGame(cmdblack) eq "movetime" } { set value(black) [expr {$::finishGame(cmdValueblack) * 1000 }] } + + sc_var create + while {$::autoplayMode} { + ::engine::send fgEngine$tomove Go [list [sc_game UCI_currentPos] [list $::finishGame(cmd$tomove) $value($tomove)]] + vwait ::finishGame(moveDone) + if { [catch { sc_move addSan $::finishGame(bestmove) }] } { + set ::autoplayMode 0 + } else { + ::finishgame::annotate $tomove + lassign [checkRepetition $repetition] isRepetition repetition + lassign [checkfiftyMoveRule $moves $material $pawns] isFifty moves material pawns + if { $isRepetition || $isFifty } { + if { $isFifty } { + set text "50-moves rule" + } else { + set text "3-fold repetition" + } + set tmp [sc_pos getComment] + sc_pos setComment "$tmp $text" + set ::autoplayMode 0 + } + } + sc_move forward + ::notify::PosChanged -pgn + set tomove [expr {$tomove eq "white" ? "black" : "white"}] + } + sc_var exit + + set ::autoplayMode 0 + set tmp [sc_pos getComment] + sc_pos setComment "$tmp\n\n$::tr(FinishGame) $::tr(White): $::finishGame(enginewhite) $::finishGame(cmdwhite) $::finishGame(cmdValuewhite)\n\n$::tr(Black): $::finishGame(engineblack) $::finishGame(cmdblack) $::finishGame(cmdValueblack)" + ::engineNoWin::closeEngine fgEnginewhite + ::engineNoWin::closeEngine fgEngineblack + ::notify::PosChanged -pgn + destroy .configFinishGame + } + + proc ::finishgame::eng_messages {id w msg} { + lassign $msg msgType msgData + switch $msgType { + "InfoConfig" { + if { $::autoplayMode } { return } + set msgData [lindex $msgData 2] + ::engineNoWin::initEngineOptions $id $w $msgData + } + "InfoPV" { + lassign $msgData multipv depth seldepth nodes nps hashfull tbhits time score score_type score_wdl pv + if { $score_type ne "mate" } { set score [expr {$score / 100.0}] } + set ::finishGame(PV$multipv) [list $score $score_type $pv] + set ::finishGame(msg) $::finishGame(PV$multipv) + } + "InfoBestMove" { + lassign $msgData ::finishGame(bestmove) + set ::finishGame(moveDone) 1 + } + "InfoGo" { + lassign $msgData ::annotate(position) + } + "InfoDisconnected" { + ::engineNoWin::disconnected $id $msgData + set ::autoplayMode 0 + } + } + } +} + +################################################################################ +# add current position for 3fold repetition detection and returns 1 if +# the position is a repetition +################################################################################ +proc checkRepetition { journal } { + set elt [lrange [split [sc_pos fen]] 0 2] + set isRep 0 + # append the position only if different from the last element + if { $elt != [ lindex $journal end ] } { lappend journal $elt } + # 3fold repetion detected + if { [llength [lsearch -all $journal $elt] ] >=3 } { set isRep 1 } + return [list $isRep $journal] +} + +proc checkfiftyMoveRule { moves prevmaterial prevpawns } { + set isFiftyRule 0 + set elt [string range [sc_pos board] 0 63] + incr moves + set material [string length [string map {"." ""} $elt]] + set pawns [string map {"n" "." "b" "." "r" "." "q" "." "k" "." "N" "." "B" "." "R" "." "Q" "." "K" "." } $elt] + if { $pawns ne $prevpawns || $material ne $prevmaterial } { set moves 0 } + if { $moves >= 100 || $material == 2 } { set isFiftyRule 1 } + return [list $isFiftyRule $moves $material $pawns] +} diff --git a/tcl/tools/sergame.tcl b/tcl/tools/sergame.tcl index 1e9851d72..b2dc71a04 100644 --- a/tcl/tools/sergame.tcl +++ b/tcl/tools/sergame.tcl @@ -9,14 +9,13 @@ namespace eval sergame { # DEBUG - set ::uci::uciInfo(log_stdout3) 0 + set ::sergame::data(log_stdout) 0 # if true, follow a specific opening set openingMovesList {} set openingMovesHash {} set openingMoves "" set outOfOpening 0 - array set engineListBox {} set engineName "" set bookSlot 2 set storeEval 0 @@ -48,58 +47,15 @@ namespace eval sergame { ttk::labelframe $w.ftime -text $::tr(TimeMode) ttk::labelframe $w.fopening -text $::tr(Opening) - grid $w.fengines -row 0 -column 0 -pady { 0 10 } -sticky we -padx { 0 10 } + grid $w.fengines -row 0 -column 0 -pady { 0 10 } -sticky nswe -padx { 0 10 } grid $w.fopening -row 0 -column 1 -pady { 0 10 } -sticky nswe -padx { 10 0 } grid $w.ftime -row 1 -column 0 -pady { 10 0 } -sticky nswe -padx { 0 10 } grid $w.fconfig -row 1 -column 1 -pady { 10 0 } -sticky we -padx { 10 0 } grid $w.fbuttons -row 2 -column 1 -sticky we # builds the list of UCI engines - ttk::frame $w.fengines.fEnginesList - ttk::treeview $w.fengines.fEnginesList.lbEngines -columns {0} -show {} -selectmode browse \ - -yscrollcommand "$w.fengines.fEnginesList.ybar set" - $w.fengines.fEnginesList.lbEngines column 0 -width 100 - $w.fengines.fEnginesList.lbEngines configure -height 5 - ttk::scrollbar $w.fengines.fEnginesList.ybar -command "$w.fengines.fEnginesList.lbEngines yview" - pack $w.fengines.fEnginesList.ybar -side right -fill y - pack $w.fengines.fEnginesList.lbEngines -side left -fill x -expand 1 - pack $w.fengines.fEnginesList -expand yes -fill x -side top - - - set i 0 - set idx 0 - foreach e $::engines(list) { - if { [lindex $e 7] != 1} { incr idx ; continue } - set ::sergame::engineListBox($i) $idx - set name [lindex $e 0] - $w.fengines.fEnginesList.lbEngines insert {} end -id $idx -values [list $name] - incr i - incr idx - } - - # Engine configuration (limit strength for example) - ttk::button $w.fengines.bEngineConfig -text $::tr(ConfigureUCIengine) -command { - set sel [.configSerGameWin.fengines.fEnginesList.lbEngines selection] - set index $::sergame::engineListBox($sel) - set engineData [lindex $::engines(list) $index] - set name [lindex $engineData 0] - set cmd [ toAbsPath [lindex $engineData 1] ] - set args [lindex $engineData 2] - set dir [ toAbsPath [lindex $engineData 3] ] - set options [lindex $engineData 8] - ::uci::uciConfig 3 [ toAbsPath $cmd ] $args [ toAbsPath $dir ] $options - } - pack $w.fengines.bEngineConfig -side top -pady 5 -anchor e -padx 4 - - # if no engines defined, bail out - if {$i == 0} { - tk_messageBox -type ok -message "No UCI engine defined" -icon error - destroy $w - return - } - - $w.fengines.fEnginesList.lbEngines selection set $::sergame::chosenEngine - $w.fengines.fEnginesList.lbEngines see $::sergame::chosenEngine + ::engineNoWin::createEngineOptionsFrame $w seriousEngine ::sergame::engineName 5 ::sergame::eng_messages + pack $w.seriousEngine -in $w.fengines -side top -pady 5 -anchor w -padx 4 # load book names ttk::checkbutton $w.fconfig.cbUseBook -text $::tr(UseBook) -variable ::sergame::useBook @@ -155,10 +111,10 @@ namespace eval sergame { ttk::label $w.ftime.timebonus.blacklseconds -text $::tr(TimeSec) grid $w.ftime.timebonus.blacklseconds -row $row -column 5 - $w.ftime.timebonus.whitespminutes set [expr $::uci::uciInfo(wtime3) / (60 * 1000)] - $w.ftime.timebonus.whitespseconds set [expr $::uci::uciInfo(winc3) / 1000] - $w.ftime.timebonus.blackspminutes set [expr $::uci::uciInfo(btime3) / (60 * 1000)] - $w.ftime.timebonus.blackspseconds set [expr $::uci::uciInfo(binc3) / 1000 ] + $w.ftime.timebonus.whitespminutes set [expr $::sergame::data(wtime) / (60 * 1000)] + $w.ftime.timebonus.whitespseconds set [expr $::sergame::data(winc) / 1000] + $w.ftime.timebonus.blackspminutes set [expr $::sergame::data(btime) / (60 * 1000)] + $w.ftime.timebonus.blackspseconds set [expr $::sergame::data(binc) / 1000 ] # Fixed depth ttk::frame $w.ftime.depth @@ -230,8 +186,6 @@ namespace eval sergame { ttk::button $w.fbuttons.close -text $::tr(Play) -command { focus . - set ::sergame::chosenEngine [.configSerGameWin.fengines.fEnginesList.lbEngines selection] - set ::sergame::engineName [.configSerGameWin.fengines.fEnginesList.lbEngines set $::sergame::chosenEngine 0] set ::sergame::chosenOpening [.configSerGameWin.fopening.fOpeningList.lbOpening selection] if {$::sergame::useBook} { set ::sergame::bookToUse [.configSerGameWin.fconfig.combo get] @@ -239,16 +193,19 @@ namespace eval sergame { set ::sergame::useBook 0 } } - set ::uci::uciInfo(wtime3) [expr [.configSerGameWin.ftime.timebonus.whitespminutes get]*1000*60] - set ::uci::uciInfo(btime3) [expr [.configSerGameWin.ftime.timebonus.blackspminutes get]*1000*60] - set ::uci::uciInfo(winc3) [expr [.configSerGameWin.ftime.timebonus.whitespseconds get]*1000] - set ::uci::uciInfo(binc3) [expr [.configSerGameWin.ftime.timebonus.blackspseconds get]*1000] - set ::uci::uciInfo(fixeddepth3) [.configSerGameWin.ftime.depth.value get] - set ::uci::uciInfo(fixednodes3) [expr [.configSerGameWin.ftime.nodes.value get]*1000] - set ::uci::uciInfo(movetime3) [expr [.configSerGameWin.ftime.movetime.value get]*1000] + set ::sergame::data(wtime) [expr [.configSerGameWin.ftime.timebonus.whitespminutes get]*1000*60] + set ::sergame::data(btime) [expr [.configSerGameWin.ftime.timebonus.blackspminutes get]*1000*60] + set ::sergame::data(winc) [expr [.configSerGameWin.ftime.timebonus.whitespseconds get]*1000] + set ::sergame::data(binc) [expr [.configSerGameWin.ftime.timebonus.blackspseconds get]*1000] + set ::sergame::data(fixeddepth) [.configSerGameWin.ftime.depth.value get] + set ::sergame::data(fixednodes) [expr [.configSerGameWin.ftime.nodes.value get]*1000] + set ::sergame::data(movetime) [expr [.configSerGameWin.ftime.movetime.value get]*1000] - destroy .configSerGameWin - ::sergame::play $::sergame::chosenEngine + set callback [list ::sergame::eng_messages seriousEngine nop] + if { [::engineNoWin::initEngine seriousEngine $::sergame::engineName $callback] } { + destroy .configSerGameWin + ::sergame::play seriousEngine + } } ttk::button $w.fbuttons.cancel -textvar ::tr(Cancel) -command "focus .; destroy $w" @@ -264,7 +221,7 @@ namespace eval sergame { ################################################################################ # ################################################################################ - proc play { engine {n 3} } { + proc play { engine } { global ::sergame::chosenOpening ::sergame::isOpening ::tacgame::openingList ::sergame::openingMovesList \ ::sergame::openingMovesHash ::sergame::openingMoves ::sergame::outOfOpening @@ -273,28 +230,18 @@ namespace eval sergame { } set ::sergame::lFen {} - - ::uci::startEngine $::sergame::engineListBox($engine) $n - set engineData [lindex $::engines(list) $::sergame::engineListBox($engine)] - foreach {option} [lindex $engineData 8] { - array set ::uciOptions$n $option - } - ::uci::sendUCIoptions $n - - set ::uci::uciInfo(prevscore$n) 0.0 - set ::uci::uciInfo(score$n) 0.0 - set ::uci::uciInfo(ponder$n) "" + set ::sergame::data(prevscore) 0.0 + set ::sergame::data(score) 0.0 + set ::sergame::data(ponder) "" if {$::sergame::startFromCurrent} { set isOpening 0 } # ponder - if {$::sergame::ponder} { - ::sergame::sendToEngine $n "setoption name Ponder value true" - } else { - ::sergame::sendToEngine $n "setoption name Ponder value false" - } + set ponder false + if {$::sergame::ponder} { set ponder true } + ::engine::send $engine SetOptions [list {Ponder true}] # if will follow a specific opening line if {$isOpening} { @@ -342,10 +289,39 @@ namespace eval sergame { ::setPlayMode "::sergame::callback" ::notify::GameChanged - clocks init $n + clocks init clocks start - ::sergame::engineGo $n + ::sergame::engineGo + } + + proc ::sergame::eng_messages {id w msg} { + lassign $msg msgType msgData + switch $msgType { + "InfoConfig" { + if { ! [winfo exists $w] } { return } + set msgData [lindex $msgData 2] + ::engineNoWin::initEngineOptions $id $w $msgData + } + "InfoPV" { + lassign $msgData multipv depth seldepth nodes nps hashfull tbhits time score score_type score_wdl pv + if { $multipv == 1 } { + set ::sergame::data(score) [expr $score / 100.0] + } + } + "InfoBestMove" { + lassign $msgData ::sergame::data(bestmove) ponder ::sergame::data(ponder) + } + "InfoGo" { + lassign $msgData ::annotate(position) + } + "InfoDisconnected" { + lassign $msgData errorMsg + if {$errorMsg eq ""} { set errorMsg "The connection with the engine terminated unexpectedly." } + tk_messageBox -icon warning -type ok -parent . -message $errorMsg + ::sergame::abortGame + } + } } proc callback {cmd args} { @@ -358,27 +334,27 @@ namespace eval sergame { return 0 } - proc abortGame { { n 3 } } { + proc abortGame { } { ::setPlayMode "" - after cancel ::sergame::engineGo $n + after cancel ::sergame::engineGo clocks stop set ::sergame::lFen {} - if { $::uci::uciInfo(pipe$n) != ""} { - ::uci::closeUCIengine $n - set ::uci::uciInfo(bestmove$n) "abort" - } + ::engine::send seriousEngine StopGo + ::engine::close seriousEngine + unset ::enginewin::engConfig_seriousEngine + set ::sergame::data(bestmove) "abort" ::notify::GameChanged } - proc clocks {cmd {n 3}} { + proc clocks {cmd} { if {$::sergame::timeMode != "timebonus"} { return } switch $cmd { init { ::gameclock::new "" 1 ::gameclock::new "" 2 - ::gameclock::setSec 1 [expr 0 - $::uci::uciInfo(wtime$n)/1000] - ::gameclock::setSec 2 [expr 0 - $::uci::uciInfo(btime$n)/1000] + ::gameclock::setSec 1 [expr 0 - $::sergame::data(wtime)/1000] + ::gameclock::setSec 2 [expr 0 - $::sergame::data(btime)/1000] } start { if { [sc_pos side] == "white" } { @@ -393,11 +369,11 @@ namespace eval sergame { } toggle { if {[::gameclock::stop 1]} { - ::gameclock::add 1 [expr $::uci::uciInfo(winc$n)/1000] + ::gameclock::add 1 [expr $::sergame::data(winc)/1000] ::gameclock::storeTimeComment 1 ::gameclock::start 2 } elseif {[::gameclock::stop 2]} { - ::gameclock::add 2 [expr $::uci::uciInfo(binc$n)/1000] + ::gameclock::add 2 [expr $::sergame::data(binc)/1000] ::gameclock::storeTimeComment 2 ::gameclock::start 1 } @@ -408,6 +384,7 @@ namespace eval sergame { proc takeBack {takebackClockW takebackClockB} { sc_move back 1 + sc_game truncate if {$takebackClockW != ""} { ::gameclock::setSec 1 [expr 0 - $takebackClockW] ::gameclock::setSec 2 [expr 0 - $takebackClockB] @@ -416,13 +393,6 @@ namespace eval sergame { ::notify::PosChanged -pgn } - ################################################################################ - # - ################################################################################ - proc sendToEngine {n text} { - ::sergame::logEngine $n "Scid : $text" - catch {puts $::uci::uciInfo(pipe$n) $text} - } ################################################################################ # returns true if last move is a mate and stops clocks ################################################################################ @@ -437,17 +407,17 @@ namespace eval sergame { ################################################################################ # ################################################################################ - proc engineGo { n } { + proc engineGo { } { global ::sergame::isOpening ::sergame::openingMovesList ::sergame::openingMovesHash ::sergame::openingMoves \ ::sergame::timeMode ::sergame::outOfOpening - after cancel ::sergame::engineGo $n + after cancel ::sergame::engineGo if { [::sergame::endOfGame] } { return } if { [sc_pos side] != $::sergame::engineColor } { set ::sergame::waitPlayerMove 1 - after 1000 ::sergame::engineGo $n + after 1000 ::sergame::engineGo return } @@ -459,9 +429,9 @@ namespace eval sergame { if {$::sergame::timeMode == "timebonus"} { set takebackClockW [::gameclock::getSec 1] set takebackClockB [::gameclock::getSec 2] - clocks toggle $n + clocks toggle } - repetition + if { [repetition] } { [return } } # make a move corresponding to a specific opening, (it is engine's turn) @@ -480,7 +450,7 @@ namespace eval sergame { -message "$::tr(NotFollowedLine) $openingMoves\n $::tr(DoYouWantContinue)" ] if {$answer == no} { takeBack $takebackClockW $takebackClockB - after 1000 ::sergame::engineGo $n + after 1000 ::sergame::engineGo return } else { set outOfOpening 1 @@ -512,10 +482,11 @@ namespace eval sergame { sc_move forward 1 } - clocks toggle $n + clocks toggle updateBoard -pgn -animate - repetition - after 1000 ::sergame::engineGo $n + if { ! [repetition] } { + after 1000 ::sergame::engineGo + } return } } @@ -530,78 +501,56 @@ namespace eval sergame { sc_move addSan $move ::utils::sound::AnnounceNewMove $move # we made a book move so assume a score = 0 - set ::uci::uciInfo(prevscore$n) 0.0 - clocks toggle $n + set ::sergame::data(prevscore) 0.0 + clocks toggle updateBoard -pgn -animate - repetition - after 1000 ::sergame::engineGo $n + if { ! [repetition] } { + after 1000 ::sergame::engineGo + } return } } # ------------------------------------------------------------- # check if the engine pondered on the right move - if { $::sergame::ponder && $::uci::uciInfo(ponder$n) == [sc_game info previousMoveUCI]} { - ::sergame::sendToEngine $n "ponderhit" + if { $::sergame::ponder && $::sergame::data(ponder) == [sc_game info previousMoveUCI]} { + ::engine::rawsend seriousEngine "ponderhit" } else { - if { $::sergame::ponder } { - ::sergame::sendToEngine $n "stop" + ::engine::send seriousEngine StopGo } - set ::analysis(waitForReadyOk$n) 1 - ::sergame::sendToEngine $n "isready" - vwait ::analysis(waitForReadyOk$n) - ::sergame::sendToEngine $n "position fen [sc_pos fen]" if {$timeMode == "timebonus"} { set wtime [expr [::gameclock::getSec 1] * 1000 ] set btime [expr [::gameclock::getSec 2] * 1000 ] - ::sergame::sendToEngine $n "go wtime $wtime btime $btime winc $::uci::uciInfo(winc$n) binc $::uci::uciInfo(binc$n)" + set parameter "wtime $wtime btime $btime winc $::sergame::data(winc) binc $::sergame::data(binc)" } elseif {$timeMode == "depth"} { - ::sergame::sendToEngine $n "go depth $::uci::uciInfo(fixeddepth$n)" + set parameter "depth $::sergame::data(fixeddepth)" } elseif {$timeMode == "movetime"} { - ::sergame::sendToEngine $n "go movetime $::uci::uciInfo(movetime$n)" + set parameter "movetime $::sergame::data(movetime)" } elseif {$timeMode == "nodes"} { - ::sergame::sendToEngine $n "go nodes $::uci::uciInfo(fixednodes$n)" + set parameter "nodes $::sergame::data(fixednodes)" } + ::engine::send seriousEngine Go [list "position fen [sc_pos fen]" $parameter]; #[list $::annotate(typ) $::annotate($::annotate(typ))]] } - set ::uci::uciInfo(bestmove$n) "" - vwait ::uci::uciInfo(bestmove$n) + set ::sergame::data(bestmove) "" + vwait ::sergame::data(bestmove) # ------------------------------------------------------------- # if weak move detected, propose the user to tack back - if { $::sergame::coachIsWatching && $::uci::uciInfo(prevscore$n) != "" } { - set blunder 0 - set delta [expr $::uci::uciInfo(score$n) - $::uci::uciInfo(prevscore$n)] - if {$delta > $::informant("?!") && $::sergame::engineColor == "white" || - $delta < [expr 0.0 - $::informant("?!")] && $::sergame::engineColor == "black" } { - set blunder 1 - } - - if {$delta > $::informant("?") && $::sergame::engineColor == "white" || - $delta < [expr 0.0 - $::informant("?")] && $::sergame::engineColor == "black" } { - set blunder 2 - } + if { $::sergame::coachIsWatching && $::sergame::data(prevscore) != "" } { + set tBlunder "" + set delta [expr $::sergame::data(score) - $::sergame::data(prevscore)] + if {$delta > $::informant("?!") } { set tBlunder "DubiousMovePlayedTakeBack" } + if {$delta > $::informant("?") } { set tBlunder "WeakMovePlayedTakeBack" } + if {$delta > $::informant("??") } { set tBlunder "BadMovePlayedTakeBack" } - if {$delta > $::informant("??") && $::sergame::engineColor == "white" || - $delta < [expr 0.0 - $::informant("??")] && $::sergame::engineColor == "black" } { - set blunder 3 - } - - if {$blunder == 1} { - set tBlunder "DubiousMovePlayedTakeBack" - } elseif {$blunder == 2} { - set tBlunder "WeakMovePlayedTakeBack" - } elseif {$blunder == 3} { - set tBlunder "BadMovePlayedTakeBack" - } - - if {$blunder != 0} { + if {$tBlunder ne ""} { clocks stop set answer [tk_messageBox -icon question -parent .main -title "Scid" -type yesno -message $::tr($tBlunder) ] if {$answer == yes} { takeBack $takebackClockW $takebackClockB - after 1000 ::sergame::engineGo $n + after 1000 ::sergame::engineGo return } clocks start @@ -609,53 +558,50 @@ namespace eval sergame { } # ------------------------------------------------------------- - if { $::uci::uciInfo(bestmove$n) == "abort" } { + if { $::sergame::data(bestmove) == "abort" } { return } - ::uci::sc_move_add $::uci::uciInfo(bestmove$n) - ::utils::sound::AnnounceNewMove $::uci::uciInfo(bestmove$n) - set ::uci::uciInfo(prevscore$n) $::uci::uciInfo(score$n) + sc_move addSan $::sergame::data(bestmove) + ::utils::sound::AnnounceNewMove $::sergame::data(bestmove) + set ::sergame::data(prevscore) $::sergame::data(score) if { $::sergame::storeEval == 1 } { - storeEvalComment $::uci::uciInfo(score$n) + set score $::sergame::data(score) + if { $::sergame::engineColor eq "black" } { set score [expr 0.0 - $score] } + storeEvalComment $score } updateBoard -pgn -animate - repetition + if { [repetition] } { return } - clocks toggle $n + clocks toggle - # ponder mode (the engine just played its move) - if {$::sergame::ponder && $::uci::uciInfo(ponder$n) != ""} { - ::sergame::sendToEngine $n "position fen [sc_pos fen] moves $::uci::uciInfo(ponder$n)" - set wtime [expr [::gameclock::getSec 1] * 1000 ] - set btime [expr [::gameclock::getSec 2] * 1000 ] + # ponder mode (the engine just played its move) ;&& $::sergame::data(ponder) != "" + if {$::sergame::ponder } { if {$timeMode == "timebonus"} { - ::sergame::sendToEngine $n "go ponder wtime $wtime btime $btime winc $::uci::uciInfo(winc$n) binc $::uci::uciInfo(binc$n)" + set wtime [expr [::gameclock::getSec 1] * 1000 ] + set btime [expr [::gameclock::getSec 2] * 1000 ] + set parameter "ponder wtime $wtime btime $btime winc $::sergame::data(winc) binc $::sergame::data(binc)" } elseif {$timeMode == "depth"} { - ::sergame::sendToEngine $n "go ponder depth $::uci::uciInfo(fixeddepth$n)" + set parameter "ponder depth $::sergame::data(fixeddepth)" } elseif {$timeMode == "movetime"} { - ::sergame::sendToEngine $n "go ponder movetime $::uci::uciInfo(movetime$n)" + set parameter "ponder movetime $::sergame::data(movetime)" } elseif {$timeMode == "nodes"} { - ::sergame::sendToEngine $n "go ponder nodes $::uci::uciInfo(fixednodes$n)" + set parameter "ponder nodes $::sergame::data(fixednodes)" } + ::engine::send seriousEngine Go [list "position fen [sc_pos fen] moves $::sergame::data(ponder)" $parameter] } - after 1000 ::sergame::engineGo $n + after 1000 ::sergame::engineGo } ################################################################################ # add current position for 3fold repetition detection and returns 1 if # the position is a repetition ################################################################################ proc repetition {} { - set elt [lrange [split [sc_pos fen]] 0 2] - # append the position only if different from the last element - if { $elt != [ lindex $::sergame::lFen end ] } { - lappend ::sergame::lFen $elt - } - - if { [llength [lsearch -all $::sergame::lFen $elt] ] >=3 } { + lassign [checkRepetition $::sergame::lFen] isRepetition ::sergame::lFen + if { $isRepetition } { tk_messageBox -type ok -message $::tr(Draw) -parent .main -icon info - puts $::sergame::lFen + ::sergame::abortGame return 1 } return 0 @@ -664,7 +610,7 @@ namespace eval sergame { # ################################################################################ proc logEngine {n text} { - if {$::uci::uciInfo(log_stdout$n)} { + if {$::sergame::data(log_stdout)} { puts stdout "$n $text" } } diff --git a/tcl/windows/book.tcl b/tcl/windows/book.tcl index e411f0bb8..42fda9246 100644 --- a/tcl/windows/book.tcl +++ b/tcl/windows/book.tcl @@ -5,6 +5,27 @@ ###################################################################### ### Book window +# return index of actBook and a list of all books, return -1 if no books available +proc getBookList { actBook } { + set bookPath $::scidBooksDir + set bookList [ lsort -dictionary [ glob -nocomplain -directory $bookPath *.bin ] ] + # No book found + if { [llength $bookList] == 0 } { + return [list -1 {}] + } + set tmp {} + set idx 0 + set i 0 + foreach file $bookList { + lappend tmp [ file tail $file ] + if {$actBook == [ file tail $file ] } { + set idx $i + } + incr i + } + return [list $idx $tmp] +} + namespace eval book { set isOpen 0 set isReadonly 0 @@ -105,29 +126,15 @@ namespace eval book { if { $name == "" && $lastBook != "" } { set name $lastBook } - set bookPath $::scidBooksDir - set bookList [ lsort -dictionary [ glob -nocomplain -directory $bookPath *.bin ] ] - + lassign [getBookList $name] idx tmp # No book found - if { [llength $bookList] == 0 } { + if { $idx < 0 } { tk_messageBox -title "Scid" -type ok -icon error -message "No books found. Check books directory" set ::book::isOpen 0 set ::book::currentBook "" ::win::closeWindow $w return } - - set i 0 - set idx 0 - set tmp {} - foreach file $bookList { - set f [ file tail $file ] - lappend tmp $f - if {$name == $f} { - set idx $i - } - incr i - } ttk::combobox $w.f.combo -width 12 -values $tmp catch { $w.f.combo current $idx } @@ -270,11 +277,8 @@ namespace eval book { ttk::frame $w.f applyThemeColor_background $w # load book names - set bookPath $::scidBooksDir - set bookList [ lsort -dictionary [ glob -nocomplain -directory $bookPath *.bin ] ] - - # No book found - if { [llength $bookList] == 0 } { + lassign [getBookList $name] idx tmp + if { $idx < 0 } { tk_messageBox -title "Scid" -type ok -icon error -message "No books found. Check books directory" set ::book::isOpen 0 set ::book::currentBook "" @@ -282,18 +286,6 @@ namespace eval book { return } - set i 0 - set idx 0 - set tmp {} - foreach file $bookList { - set f [ file tail $file ] - lappend tmp $f - if {$name == $f} { - set idx $i - } - incr i - } - ttk::combobox $w.fcombo.combo -width 12 -values $tmp catch { $w.fcombo.combo current $idx } pack $w.fcombo.combo -expand yes -fill x