Feature Request: Performance Report

So I really think this can be done within a day or two for those who know how to code OGS features. I was able to do this on Tsumego Dragon in a dozen or so hours.

I even made drop downs with clickable moves. When you click the move number, the game navigates to that move.

For accuracy, I did number of moves within >= AI move - n where n is an offset I was playing around with. This number could also be flexible to ranks but that gets into adjustable ranks like AI Sensei has.

My suggested spot would be to put it right under the graph toggle and have it hidden by default with a toggle to show the results. The toggle can also be only for premium users and when clicked by non premium users perhaps have a popup redirecting them to the premium page. Or just have it hidden if they are not premium.

I think this would be an amazing feature and all of the numbers are already there.

–

Here is my amateur-ish code.

var gameStages = {
    "opening": {"min": 1, "max":29},
    "early_Middle_Game": {"min": 30, "max":69},
    "late_Middle_Game": {"min": 70, "max":119},
    "endgame": {"min": 120, "max":1000}
};

// Returns 1 for odd and 0 for even.
function IsOdd(num) { return num % 2;}

/*

Key Moves
	The top ai move is >= 25% more than the second best ai move.
    The top ai move is repeated multiple times.
    The top ai move is >= 15 points.   
*/

// Weights for player's accuracy score
var accuracyWinRateErrorMargin = 0.02;
var accuracyScoreErrorMargin = 1;

// List to send to bubble
var p1Brilliant = [];
var p2Brilliant = [];
var p1Best = [];
var p2Best = [];
var p1Excellent = [];
var p2Excellent = [];
var p1Good = [];
var p2Good = [];
var p1Inaccuracy = [];
var p2Inaccuracy = [];
var p1Mistake = [];
var p2Mistake = [];
var p1Blunder = [];
var p2Blunder = [];

// Moves that made the most dramatic difference
var keyMoves = [];

// Get the accuracy scores of the players.
function CheckAccuracy (data, moves) {
    
    var p1TopWinRateMoves = [];
    var p1TopScoreMoves = [];
    var p2TopWinRateMoves = [];
    var p2TopScoreMoves = [];
    var oddNum = false;
    
    for (var i=0; i<moves.length; i++) {
        
        oddNum = (IsOdd(i) === 1);
        var moveKey = "move-" + i;
        var prevMoveKey = "move-" + (i-1);
        var currentMove;
        var branches;
        var previousBranches = [];
        var move = moves[i];
        
        if(data[moveKey] != undefined) {
        	currentMove = data[moveKey];
        	branches = currentMove.branches;
        }

        if(i > 0) {

            if(data[prevMoveKey] != undefined) {
            	previousBranches = data[prevMoveKey].branches;
            }

            var good_win_rate = false;
            var good_score = false;
            
            var isBrilliant = IsBrilliant(currentMove, previousBranches, oddNum);
            var isBest = IsBest(currentMove, previousBranches, oddNum);
            var isExcellent = IsExcellent(move);
            var isGood = IsGood(move);
            var isInaccuracy = IsInaccuracy(move);
            var isMistake = IsMistake(move);
            var isBlunder = IsBlunder(move);

            // P1 Winrate Check
            if(oddNum) {
                if(isBrilliant) {p1Brilliant.push(move.move_number);}
                if(isBest) {p1Best.push(move.move_number);}
                if(isExcellent) {p1Excellent.push(move.move_number);}
                if(isGood) {p1Good.push(move.move_number);}
                if(isInaccuracy) {p1Inaccuracy.push(move.move_number);}
                if(isMistake) {p1Mistake.push(move.move_number);}
                if(isBlunder) {p1Blunder.push(move.move_number);}
                
                var currentWinRate = move.win_rate;
                var currentScore = move.score;                    
                
                if(previousBranches.length > 0) {
                    var bestMoveWinRate = previousBranches[0].win_rate - accuracyWinRateErrorMargin;
                    var bestScore = previousBranches[0].score;
                    
                    if(currentWinRate >= bestMoveWinRate) 
                        {good_win_rate = true;}

                    if(currentScore >= bestScore - accuracyScoreErrorMargin)
                    {good_score = true;}
                }

            }

            // P2 Winrate Check
            else {
                if(isBrilliant) {p2Brilliant.push(move.move_number);}
                if(isBest) {p2Best.push(move.move_number);}
                if(isExcellent) {p2Excellent.push(move.move_number);}
                if(isGood) {p2Good.push(move.move_number);}
                if(isInaccuracy) {p2Inaccuracy.push(move.move_number);}
                if(isMistake) {p2Mistake.push(move.move_number);}
                if(isBlunder) {p2Blunder.push(move.move_number);}
                
                var currentWinRate = move.win_rate;
                var currentScore = move.score;
                
                if(previousBranches.length > 0) {
                    var worstMoveWinRate = previousBranches[0].win_rate;
                    var worstScore = previousBranches[0].score;

                    if(currentWinRate <= worstMoveWinRate + accuracyWinRateErrorMargin) 
                        {good_win_rate = true;}

                    if(currentScore <= worstScore + accuracyScoreErrorMargin)
                    {good_score = true;}

                }
            }

                // Compare current move to best move.
                if(good_win_rate) {
                    if(oddNum) {
                        p1TopWinRateMoves.push(i);
                    } else {
                        p2TopWinRateMoves.push(i);
                    }
                }

                if(good_score) {
                    if(oddNum) {
                        p1TopScoreMoves.push(i);
                    } else {
                        p2TopScoreMoves.push(i);
                    }
                }
            
        }
        oddNum = !oddNum;
    }

    var bubbleData = {
        "outputlist1": p1TopWinRateMoves,
        "outputlist2":p1TopScoreMoves,
        "outputlist3": p2TopWinRateMoves,
        "outputlist4":p2TopScoreMoves
    }
    bubble_fn_top_moves_accuracy(bubbleData);
            
    bubbleData = {
        "outputlist1": p1Brilliant,
        "outputlist2":p2Brilliant,
    }
    bubble_fn_brilliant(bubbleData);
    
    bubbleData = {
        "outputlist1": p1Best,
        "outputlist2":p2Best,
    }
    bubble_fn_best(bubbleData);
    
    bubbleData = {
        "outputlist1": p1Excellent,
        "outputlist2":p2Excellent,
    }
    bubble_fn_excellent(bubbleData);
    
    bubbleData = {
        "outputlist1": p1Good,
        "outputlist2":p2Good,
    }
    bubble_fn_good(bubbleData);
    
    bubbleData = {
        "outputlist1": p1Inaccuracy,
        "outputlist2":p2Inaccuracy,
    }
    bubble_fn_inaccuracy(bubbleData);
    
    bubbleData = {
        "outputlist1": p1Mistake,
        "outputlist2":p2Mistake,
    }
    bubble_fn_mistake(bubbleData);
    
    bubbleData = {
        "outputlist1": p1Blunder,
        "outputlist2":p2Blunder,
    }
    bubble_fn_blunder(bubbleData);
}


function IsBrilliant (move, prevBranches, isOdd) {
    /*
        Brilliant
	Opening - NA
    Early  > 0
    Late > 0
    End > 0
    */
    if(prevBranches.length < 1) {return false;}
    if(move.move_number <= gameStages.opening.max) {return false;}
    
    var aiTopScore = prevBranches[0].score;
    var score = move.score;
    
    if(score > aiTopScore && isOdd) {return true;}
    else if (score < aiTopScore && !isOdd) {return true;}
    
    return false;
}

function IsBest (move, prevBranches, isOdd) {
    /*
    Best
		Opening - NA
    	Early <= 0.2
    	Late <= 0.2
    	End <= 0.2
    */
            
    if(prevBranches.length < 1) {return false;}
    if(move.move_number <= gameStages.opening.max) {return false;}
    if(move.move.x != prevBranches[0].moves[0].x) {return false;}
    if(move.move.y != prevBranches[0].moves[0].y) {return false;}
    
    return true;
}

function IsExcellent (move) {
	
    /*
        Excellent
            Opening <= 2% -- Pro Moves
            Early > 0.2 < 1
            Late > 0.2 < 1
            End > 0.2 < 1
    */
    
    var isOdd = IsOdd(move.move_number);
    
    if(move.move_number <= gameStages.opening.max) {
    	return false;
    } else {
        if (move.score_change >= -0.2 && move.score_change <= 0.2)
        {return true;}
        else {return false;}
    }
}


function IsGood (move) {
    /*
      Good
        Opening > 20% <=10% -- Less than 10% is top Amateur moves
        Early >= 1 < 2
        Late >= 1 < 2
        End >= 1 < 2
    */
    
    var isOdd = IsOdd(move.move_number);
    
    if(move.move_number <= gameStages.opening.max) {
        return false;
    } else {
        if(isOdd) {
            if (move.score_change >= -2 && move.score_change <= 10)
            {return true;}
            else {return false;}
        } else {
            if (move.score_change > 0.2 && move.score_change <= 2)
            {return true;}
            else {return false;}
        }
    }
}

function IsInaccuracy (move) {
	
    /*
    Inaccuracy
		Opening > 10% <=24% -- Rest of the Opening
    	Early >= 2 < 10
    	Early >= 2 < 10
    	End >= 2 < 10
    */
    
    var isOdd = IsOdd(move.move_number);
    
    if(move.move_number <= gameStages.opening.max) {
    	if(isOdd) {
            if (move.win_rate_change >= -25 && move.win_rate_change < -10)
            {return true;}
            else {return false;}
        } else {
            if (move.win_rate_change <= 25 && move.win_rate_change > 10)
            {return true;}
            else {return false;}
        }
    }  else {
        if(isOdd) {
            if (move.score_change < -2 && move.score_change >= -10)
            {return true;}
            else {return false;}
        } else {
            if (move.score_change > 2 && move.score_change <= 10)
            {return true;}
            else {return false;}
        }
    }
    
    
}


    function IsMistake (move) {
	
    /*
    Mistake
        Opening > 25% <= 35% -- Some mistake likely in joseki.
        Early > 10 <= 14
        Late > 10 <= 14
        End > 10 <= 14
    */
        
        var isOdd = IsOdd(move.move_number);
            
    if(move.move_number <= gameStages.opening.max) {
    	if(isOdd) {
            if (move.win_rate_change >= -35 && move.win_rate_change < -24)
            {return true;}
            else {return false;}
        } else {
            if (move.win_rate_change <= 35 && move.win_rate_change > 24)
            {return true;}
            else {return false;}
        }
    }  else {
        if(isOdd) {
            if (move.score_change < -10 && move.score_change >= -14)
            {return true;}
            else {return false;}
        } else {
            if (move.score_change > 10 && move.score_change <= 14)
            {return true;}
            else {return false;}
        }
    }   
}

function IsBlunder (move) {
	
    /*
    Mistake
        Opening > 35% -- Some mistake likely in joseki.
        Early > 10 <= 14
        Late > 10 <= 14
        End > 10 <= 14
    */
    
    var isOdd = IsOdd(move.move_number);
            
    if(move.move_number <= gameStages.opening.max) {
    	if(isOdd) {
            if (move.win_rate_change < -35)
            {return true;}
            else {return false;}
        } else {
            if (move.win_rate_change > 35)
            {return true;}
            else {return false;}
        }
    }  else {
        if(isOdd) {
            if (move.score_change <= -15)
            {return true;}
            else {return false;}
        } else {
            if (move.score_change >= 15)
            {return true;}
            else {return false;}
        }
    }   
}
4 Likes

A bold opening gambit.

11 Likes

A bold opening gambit.

In this case it’s true - at least the tablulation, if not the click-to-move, because we already have something like that :slight_smile:

It’s used by moderators in AI cheating detection.

I guess there’s an argument that says “if Tsumego Dragon has it, then there must be a reason why our users would like it, and no reason not to open it up”.

It does feel like a “highly premium” feature.

Personally, I feel sad when more information that assists cheaters to know whether they are detectable becomes available, but I can see the utility of this feature outside of that.

Maybe I should look at it as “pay for cheat assistance” :smiley:

2 Likes

How would this help cheaters? Isn’t this only available after the game? If I’m going to cheat I’m using Katrain lol

2 Likes

True, only available after the game.

But when you see it, if you put your “I want to evade detection” hat on, you go “geez, I can see right in my face that all my moves are AI like … I better fix that next time”.

Possibly the average good-spirited OGS user has no idea how dense some of our AI cheaters are - many just found a new toy, and are using it without even really thinking about detection, it seems.

As a person involved in AI detection, I’m very grateful for this, and wish this would continue.

(Not the cheating! Just the “easy to detect” :D)

3 Likes

Have you considered contributing to the OGS source code? There is a detailed documentation on how to set it up.

//Edit: You can simplify and shorten your code though:

→

return move.score_change * (isOdd ? 1 : -1) <= -15;
1 Like

[No Code, just Copy and Ask] I’m also curious about my personal growth journey with Go (which I call “WeiQi and the Beast”), so yesterday I simply asked ChatGPT for an analysis by copying the profile link and game snapshots. The results were quite unexpected!


So what would be your feature request exactly?

1 Like

Good question. FYI (Mine has been editted by Gemini by Google:) Here’s how you can phrase your request to ChatGPT | (and can try with Claude, Perplexity, Deep Seek et al):


Analyzing OGS History with GPT

  1. Start with the OGS link: Begin by pasting the OGS (Online Go Server) profile link directly into the chat.
  2. Position GPT: Then, prompt ChatGPT with this: “As an international big data expert, please analyze my (or my partner’s) OGS history from the link provided above.”
  3. Refine your request: After this initial prompt, you can ask more detailed questions like:
  • “What trends do you observe in my game performance?”
  • “Can you identify any strengths or weaknesses in my play?”
  • “Are there specific opponents or game types that seem to influence my results?”
  • “Based on the data, what areas should I focus on for improvement?”

This approach positions ChatGPT as an expert and gives it a clear starting point for its analysis. Remember, the more specific you are with your follow-up questions, the more tailored and insightful the results are likely to be!