The trick to this, in any language, is that you need to not only compare numbers but back-reference those numbers back to strings.
That is, sorting just a list of numbers is easy; keeping a one-to-one reference to an original field name is (a tiny bit) harder.
Then you have add in the quirks with Marketo datatypes in Velocity: be ready for scores to come in as numbers, empty strings, numeric strings, or nulls, and handle all cases gracefully.
So use this:
#set( $namedScores = [
{
"name" : "scoreField1",
"value" : $lead.scoreField1
},
{
"name" : "scoreField2",
"value" : $lead.scoreField2
},
{
"name" : "scoreField3",
"value" : $lead.scoreField3
}
])
#set( $defaultScore = 0 ) ##maybe you want $field.in(0.0).NEGATIVE_INFINITY in your case
#foreach( $score in $namedScores )
#set( $void = $score.put("value", $convert.toDouble( $display.alt( $convert.toNumber($score.value), $defaultScore) ) ) )
#end
#set( $sortedNamedScores = $sorter.sort($namedScores, "value:desc") )
#set( $highestNamedScore = $sortedNamedScores[0] )
The name of the highest score is ${highestNamedScore.name}, which has a score of ${highestNamedScore.value.intValue()}.
Note tie scores are awarded to the key that's added earliest in `namedScores`.
Obvs. replace $lead.scoreField1 et al. with the real Velocity property name of each score field, and replace "scoreField1" et al. with the friendly name of the field so you can use it in later conditions (you can have the friendly name be the same as the Velocity prop name, just wanted to point out the option).
Also note the $defaultScore. This is what you assign to a person who is unscored (null score, not the same as an assigned score of 0). There are cases where a null shouldn't be the same as 0: you may want null scores to always be less than every other score, for example, even less than negative scores.