TowelSniffer / Anki-go

A go template for Anki
GNU General Public License v3.0
17 stars 1 forks source link

Enhancement: Identify lines of play that are actually correct #6

Open trdischat opened 1 month ago

trdischat commented 1 month ago

@TowelSniffer, I made another improvement to the templates that you might find interesting. The current template assumes that all lines of play in the problem are correct solutions. As long as you only click on moves in the tree, the template shows a green border on the back side of the card. The problem with this is that many go problems present incorrect lines of play so that they can explain why they are bad moves. I wanted to identify this in the template so that a green border will only display if you only click on a line of play AND the line of play is an actual correct answer. The logic I had in mind was this:

If the current node has no children (i.e., it is the end of a line of play):

  1. If the comment text to the current node starts with "Correct" or "Right", the current line of play is correct.
  2. If the comment text to the current node starts with "Incorrect" or "Wrong" or "Fail", the current line of play is incorrect.
  3. If the comment text to the current node starts with anything else AND the color of the current node is the same color as the first move in the problem, the current line of play is correct.
  4. Otherwise, the current line of play is incorrect.

I wrote the following code to implement this idea (apologies for the lengthy explanation).

var goFirst = true;
var errorCount = 0;
var handicap = 3;
var randVar = true;
var probVar = 0;
var first2move = 0;

if (Persistence.isAvailable()) {
    Persistence.setItem("errorCheck", false);
    Persistence.setItem("rnd", randVar);
    if (randVar) {
        probVar = Persistence.getItem("var");
        if (probVar == null) {
            probVar = Math.floor(Math.random()*16);
            Persistence.setItem("var", probVar);
        };
    }
}

function checkComment(comment) {
    const goodAnswers = ["CORRECT", "RIGHT"];
    const badAnswers = ["INCORRECT", "WRONG", "FAIL"];
    for (let answer in goodAnswers) {
        if (comment.toUpperCase().startsWith(answer)) {
            return(1);
        }
    };
    for (let answer in badAnswers) {
        if (comment.toUpperCase().startsWith(answer)) {
            return(-1);
        }
    };
    return(0);
}
var goFirst = true;
var errorCheck = false;
var randVar = false;
var probVar = 0;
var first2move = 0;

if (Persistence.isAvailable()) {
    errorCheck = Persistence.getItem("errorCheck");
    randVar = Persistence.getItem("rnd");
    if (randVar) {
        probVar = Persistence.getItem("var");
    }
    Persistence.clear();
}
if (msg.navChange || msg.treeChange) { // Update the navigation buttons
    current = editor.getCurrent();
    if (current.parent) { // Has parent
        arraySetColor(leftElements, 'black');
        if (current.parent.children.length > 1) { // Has siblings
            arraySetColor(siblingElements, 'black');
        } else { // No siblings
            arraySetColor(siblingElements, besogo.GREY);
        }
    } else { // No parent
        arraySetColor(leftElements, besogo.GREY);
        arraySetColor(siblingElements, besogo.GREY);
    }
    if (current.children.length) { // Has children
        arraySetColor(rightElements, 'black');
        document.getElementsByClassName("besogo-board")[0].style.borderColor = "#670A0A";
    } else { // No children
        arraySetColor(rightElements, besogo.GREY);
        let chk = checkComment(current.comment);
        if ( chk==1 || ( chk==0 && current.move.color==first2move ) ) {
            document.getElementsByClassName("besogo-board")[0].style.borderColor = "limegreen";
            document.getElementsByClassName("besogo-board")[0].style.boxShadow ="0px 0px 6px 0px limegreen";
        } else if (Persistence.isAvailable()) { Persistence.setItem("errorCheck", true); };
    }
}
function navigate(x, y, shiftKey) {
    var i, move,
        children = current.children;

    // Look for move at same location in children
    for (i = 0; i < children.length; i++) {
        move = children[i].move;
        if (move && move.x === x && move.y === y) {
            current = children[i]; // Navigate to child if found
            // Notify navigation (with no tree edits)
            notifyListeners({
                navChange: true
            });
            document.getElementsByClassName("besogo-diagram")[0].style.pointerEvents = "none";
            var myTimeout = setTimeout(nextMove, 200);
            return true;
        }
    }
    if (document.getElementsByClassName("besogo-board")[0].style.borderColor !== "limegreen") {
        if (Persistence.isAvailable()) { Persistence.setItem("errorCheck", true); };
        errorCount++;
        if (errorCount === handicap) {
            errorCount = 0;
            nextMove();
            var myTimeout = setTimeout(nextMove, 200);
        }
    }
    return false;
}
besogo.loadSgf = function(sgf, editor) {
    'use strict';
    var size = {
            x: 19,
            y: 19
        }, // Default size (may be changed by load)
        root;
function loadProp(node, prop) {
    var setupFunc = 'placeSetup',
        markupFunc = 'addMarkup',
        whiteStone = 1,
        blackStone = -1,
        move;

    if (probVar & 8) {  // Swap black and white stones on the board
        whiteStone = -1;
        blackStone = 1;
    };

    switch (prop.id) {
        case 'B': // Play a black move
            move = lettersToCoords(prop.values[0]);
            node.playMove(move.x, move.y, blackStone, true);
            if (first2move == 0) {
                first2move = blackStone;
                root.nextToMove = blackStone;
            };
            break;
        case 'W': // Play a white move
            move = lettersToCoords(prop.values[0]);
            node.playMove(move.x, move.y, whiteStone, true);
            if (first2move == 0) {
                first2move = whiteStone;
                root.nextToMove = whiteStone;
            };
            break;
        case 'AB': // Setup black stones
            applyPointList(prop.values, node, setupFunc, blackStone);
            break;
        case 'AW': // Setup white stones
            applyPointList(prop.values, node, setupFunc, whiteStone);
            break;
        case 'AE': // Setup empty stones
            applyPointList(prop.values, node, setupFunc, 0);
            break;
        case 'CR': // Add circle markup
            applyPointList(prop.values, node, markupFunc, 1);
            break;
        case 'SQ': // Add square markup
            applyPointList(prop.values, node, markupFunc, 2);
            break;
        case 'TR': // Add triangle markup
            applyPointList(prop.values, node, markupFunc, 3);
            break;
        case 'M': // Intentional fallthrough treats 'M' as 'MA'
        case 'MA': // Add 'X' cross markup
            applyPointList(prop.values, node, markupFunc, 4);
            break;
        case 'SL': // Add 'selected' (small filled square) markup
            applyPointList(prop.values, node, markupFunc, 5);
            break;
        case 'L': // Intentional fallthrough treats 'L' as 'LB'
        case 'LB': // Add label markup
            applyPointList(prop.values, node, markupFunc, 'label');
            break;
        case 'C': // Comment placed on node
            if (node.comment) {
                node.comment += '\n' + prop.values.join().trim();
            } else {
                node.comment = prop.values.join().trim();
            }
            if (probVar & 8) {   // Swap 'Black' and 'White' in the comments
                node.comment = node.comment.replace(/White/g, "BBBBB");
                node.comment = node.comment.replace(/Black/g, "White");
                node.comment = node.comment.replace(/BBBBB/g, "Black");
            };
            break;
    }
} // END function loadProp
    besogo.autoInit();
    if (goFirst == false) {
        var myTimeout = setTimeout(nextMove, 200);
    }

    if (errorCheck == true) {
        document.getElementsByClassName("besogo-board")[0].style.border = "0.7vmin solid #670A0A";
    } else {
        document.getElementsByClassName("besogo-board")[0].style.border = "0.7vmin solid limegreen";
        document.getElementsByClassName("besogo-board")[0].style.boxShadow ="0px 0px 6px 0px limegreen";
    }
</script>
TowelSniffer commented 1 month ago

This is really cool. I will test it later but assuming it all works it seems like an awesome change. You have inspired me to include a similar feature for my chess template.

TowelSniffer commented 1 month ago

So playing around with it it seems to work well, though I had to change let answer in goodAnswers to let answer of goodAnswers. I have included the tracking of incorrect solution as its a nice feature to have. My implementation simplifies this a bit by just checking for "INCORRECT", "WRONG", "FAIL", and assuming the line as correct otherwise. I also don't consider children as I feel this is unnecessary and allows you to be more flexible with your comments. Seems unnecessary to specify a move as correct but let me know if you have a specific use case for it. I am about to push an update that includes checking of wrong answers as well as a your other suggestions and a few fixes.

trdischat commented 1 month ago

Specifying correct versus incorrect moves in SGF has always been a bit of a challenge. Each software program adopts a slightly different approach. I summarized the state of affairs a few years ago (in a post on lifein19x19) as follows:

I use Drago to edit problems and EasyGo to practice problems on my iPhone.

Based on my review of many published problems in books, it seemed to me that the main line is generally the correct answer, but that alternate lines often present incorrect answers. As I thought about it, it struck me that a correct answer is typically a line where the player (i.e., the first to play) plays the last move in the sequence. If the computer plays the last move, that should signify that the sequence is an incorrect answer. So it seemed best to me to let the drafter of the problem explicitly designate in the node comment (or other node property) to the last move in a line of play whether that sequence is correct or incorrect. But if there is nothing in the comments (or other SGF properties) to tell you whether a line of play is correct or incorrect, the final bit of logic would be to mark it as correct if the color of the last move matches the color of the first move, and mark it as incorrect if the colors of the first and last moves are different.

Of course, joseki practice is different, since most lines of play are correct, and each line could end with either black or white. But I hadn't worked out an approach for this yet. I view joseki as a programming challenge for another day.

TowelSniffer commented 1 month ago

Yeah ok, that's interesting. Are you able to explain what SGF property of BM[2] means? With your code tracking these alternatives should be possible. I'm not a fan of trying to accommodate them all by default, but having a variable that can be set to determine how the template handles all of this seems like a good option to have.

trdischat commented 1 month ago

In standard SGF, BM[2] means very bad move, while TE[2] means very good move (i.e., tesuji). See https://www.red-bean.com/sgf/properties.html#BM. I don't see much value in covering these use cases, as almost nobody uses them for problems. These codes are really meant for commentary on moves in a complete game record.