Computing Scrabble score in Golang I think we all know the game of Scrabble and how popular it is as a simple programming challenge. Here's my attempt at computing a Scrabble score for a given word done in Go.

I must admit, I'm fairly new to Go (been learning it for a week or so). Having said that, I'd greatly appreciate all sorts of tips for improving not only the solution itself but also the style to be more Go-ish.

I've used golint and gofmt on this, but with gofmt I'm not sure I use it correctly, as it simply prints out the code to stdout.

There's a simple test suit that comes along with the solution.

// Package scrabble deals with proper word score counting
package scrabble

import "strings"

// countScore returns a score map per given word
func countScore() map[rune]int {
    var scoreMap = map[string]int{
    "aeioulnrst": 1,
    "dg":         2,
    "bcmp":       3,
    "fhvwy":      4,
    "k":          5,
    "jx":         8,
    "qz":         10,
    }
    var actualScore = make(map[rune]int)
    for letters, score := range scoreMap {
        for _, letter := range letters {
            actualScore[letter] = score
        }
    }
    return actualScore
}

// Score count Scrabble score
func Score(word string) (score int) {
    for _, char := range strings.ToLower(word) {
        score += countScore()[char]
    }
    return
}


As for the solution, I've noticed it's kinda slow. What's more, it takes  over 600 allocs/op. That's a lot for such a simple task. Why is that?

package scrabble

import "testing"

func TestScore(t *testing.T) {
    for _, test := range scrabbleScoreTests {
        if actual := Score(test.input); actual != test.expected {
            t.Errorf("Score(%q) expected %d, Actual %d", test.input, test.expected, actual)
        }
    }
}

func BenchmarkScore(b *testing.B) {
    for i := 0; i &lt; b.N; i++ {
        for _, test := range scrabbleScoreTests {
            Score(test.input)
        }
    }
}


Test cases:

package scrabble

type scrabbleTest struct {
    input    string
    expected int
}

var scrabbleScoreTests = []scrabbleTest{
    {"a", 1},                           // lowercase letter
    {"A", 1},                           // uppercase letter
    {"f", 4},                           // valuable letter
    {"at", 2},                          // short word
    {"zoo", 12},                        // short, valuable word
    {"street", 6},                      // medium word
    {"quirky", 22},                     // medium, valuable word
    {"OxyphenButazone", 41},            // long, mixed-case word
    {"pinata", 8},                      // english-like word
    {"", 0},                            // empty input
    {"abcdefghijklmnopqrstuvwxyz", 87}, // entire alphabet available
}
I'd greatly appreciate all sorts of tips for improving not only the
  solution itself but also the style to be more Go-ish.




For a real-world code review, code should be correct, maintainable, reasonably efficient, and, most importantly, readable.

Writing code is a process of stepwise refinement.



Go was designed for simplicity, readability, and performance.



I ran your benchmark on your complicated code:

$ go version
go version devel +4d5bb9c609 Fri Dec 20 23:07:52 2019 +0000 linux/amd64
$

$ go test *.go -bench=. -benchmem -run=!
BenchmarkScore-4        9438        126800 ns/op   67275 B/op   512 allocs/op


It seems very slow.



I ran your benchmark on leaf bebop's complicated code:

$ go test *.go -bench=. -benchmem -run=!
BenchmarkScore-4     1263572           936 ns/op      16 B/op     2 allocs/op


It is much better, but still slow.



I ran your benchmark on my simpler, "Go-ish" code:

$ go test *.go -bench=. -benchmem -run=!
BenchmarkScore-4    16844323            69.5 ns/op     0 B/op     0 allocs/op

// Score returns the sum of the Scrabble tile points
// for the letters in a word.
func Score(word string) int {
    var score int
    for i := 0; i &lt; len(word); i++ {
        score += int(points[word[i]])
    }
    return score
}

var points = [256]byte{
    'A': 1, 'a': 1,
        ...
    'Z': 10, 'z': 10,
}


My code is around 1,819 times faster than your code and it makes no allocations.

My code is around 13 times faster than leaf bebop's code and it makes no allocations.



An array is a simple, fast, random access data structure. The points array is constructed once at compile time. 

scrabble.go:

/*
Package scrabble deals with proper word score counting

Scrabble Rules | Official Word Game Rules | Board Games
https://scrabble.hasbro.com/en-us/rules
*/

package scrabble

// Score returns the sum of the Scrabble tile points
// for the letters in a word.
func Score(word string) int {
    var score int
    for i := 0; i &lt; len(word); i++ {
        score += int(points[word[i]])
    }
    return score
}

var points = [256]byte{
    ' ': 0, // blank
    'A': 1, 'a': 1,
    'E': 1, 'e': 1,
    'I': 1, 'i': 1,
    'O': 1, 'o': 1,
    'U': 1, 'u': 1,
    'L': 1, 'l': 1,
    'N': 1, 'n': 1,
    'R': 1, 'r': 1,
    'S': 1, 's': 1,
    'T': 1, 't': 1,
    'D': 2, 'd': 2,
    'G': 2, 'g': 2,
    'B': 3, 'b': 3,
    'C': 3, 'c': 3,
    'M': 3, 'm': 3,
    'P': 3, 'p': 3,
    'F': 4, 'f': 4,
    'H': 4, 'h': 4,
    'V': 4, 'v': 4,
    'W': 4, 'w': 4,
    'Y': 4, 'y': 4,
    'K': 5, 'k': 5,
    'J': 8, 'j': 8,
    'X': 8, 'x': 8,
    'Q': 10, 'q': 10,
    'Z': 10, 'z': 10,
}







  Program testing can be a very effective way to show the presence of
  bugs, but it is hopelessly inadequate for showing their absence.
  
  A convincing demonstration of correctness being impossible as long as
  the mechanism is regarded as a black box, our only hope lies in not
  regarding the mechanism as a black box.
  
  Edsger W. Dijkstra




Your tests don't test for control characters, punctuation, numbers, and non-ASCII characters.

Your complex code and data structures look like a black box to your tests.



Locate a definitive description of the Scrabble tile points and scoring system:

Scrabble Rules | Official Word Game Rules | Board Games

Encode the tile points system in readable, array form. Like the definition, uppercase and lowercase letters and their points are on the same line. The lines are in the same order as the definition. Carefully match the array to the definition and the definition to the array, left-to-right then right-to-left and top-to-bottom then bottom-to-top. The Go compiler will check that the keys are unique.

Consider some automated tests, which can also serve as regression tests.

For the points array, compute sum and count verification totals, listing the elements in the same order as the definition, matching array to definition and definition to array, right-to-left, and left-to right.

We are now be able to conclude, if not prove, that the points array is correct.

The Score function, by design is simple and readable.

// Score returns the sum of the Scrabble tile points
// for the letters in a word.
func Score(word string) int {
    var score int
    for i := 0; i &lt; len(word); i++ {
        score += int(points[word[i]])
    }
    return score
}


For a comprehensive Score function test, use the definition of points encoding, with the tests in the same form as the definition for easy comparison.

Add the test written while developing the algorithm for the Score function. It includes special, unusual, and error cases.



peterso_test.go:

package scrabble

import (
    "strings"
    "testing"
)

/*
Scrabble Rules | Official Word Game Rules | Board Games
https://scrabble.hasbro.com/en-us/rules
    (0 point)-blank
    (1 point)-A, E, I, O, U, L, N, S, T, R
    (2 points)-D, G
    (3 points)-B, C, M, P
    (4 points)-F, H, V, W, Y
    (5 points)-K
    (8 points)- J, X
    (10 points)-Q, Z
*/

func TestScores(t *testing.T) {
    var (
        sum   = 2 * (0*1 + 1*10 + 2*2 + 3*4 + 4*5 + 5*1 + 8*2 + 10*2)
        count = int(('z' - 'a' + 1) + ('Z' - 'A' + 1))
    )

    as, ac := 0, 0 // all
    us, uc := 0, 0 // upper
    ls, lc := 0, 0 // lower
    for i := 0; i &lt; len(points); i++ {
        score := int(points[i])
        if score != 0 {
            as += score
            ac++
            if 'A' &lt;= i &amp;&amp; i &lt;= 'Z' {
                us += score
                uc++
            }
            if 'a' &lt;= i &amp;&amp; i &lt;= 'z' {
                ls += score
                lc++
            }
        }
    }
    if as != sum {
        t.Errorf("all sum: got %d; want %d", as, sum)
    }
    if ac != count {
        t.Errorf("all count: got %d; want %d", ac, count)
    }
    if 2*ls != sum {
        t.Errorf("lower sum: got %d; want %d", 2*ls, sum)
    }
    if 2*lc != count {
        t.Errorf("lower count: got %d; want %d", 2*lc, count)
    }
    if 2*us != sum {
        t.Errorf("upper sum: got %d; want %d", 2*us, sum)
    }
    if 2*uc != count {
        t.Errorf("upper count: got %d; want %d", 2*uc, count)
    }
}

func TestScore2(t *testing.T) {
    var tests1 = []struct {
        word   string
        points int
    }{
        {" ", 0},          // (0 point)-blank
        {"AEIOULNSTR", 1}, // (1 point)-A, E, I, O, U, L, N, S, T, R
        {"DG", 2},         // (2 points)-D, G
        {"BCMP", 3},       // (3 points)-B, C, M, P
        {"FHVWY", 4},      // (4 points)-F, H, V, W, Y
        {"K", 5},          // (5 points)-K
        {"JX", 8},         // (8 points)- J, X
        {"QZ", 10},        // (10 points)-Q, Z
    }
    for _, tt := range tests1 {
        word := tt.word
        word += strings.ToUpper(tt.word)
        word += strings.ToLower(tt.word)
        got := Score(word)
        score := 3 * (len(tt.word) * tt.points)
        if got != score {
            t.Errorf("Score(%s) : got %d; want %d", word, got, score)
        }
    }

    var tests2 = []struct {
        word  string
        score int
    }{
        {"", 0},
        {"Go", 3},
        {"Scrabble", 14},
        {"09\t\r\n.?!", 0},
        {"Français", 10},
        {"羅生門", 0}, // Rashōmon
    }
    for _, tt := range tests2 {
        got := Score(tt.word)
        if got != tt.score {
            t.Errorf("Score(%s) : got %d; want %d", tt.word, got, tt.score)
        }
    }
}







  with gofmt I'm not sure I use it correctly, as it simply prints out
  the code to stdout.


For most purposes, use the go fmt command.


  Gofmt (reformat) package sources

Usage:

go fmt [-n] [-x] [packages]

  
  Fmt runs the command 'gofmt -l -w' on the packages named by the import
  paths. It prints the names of the files that are modified.