DEV Community

Daily Challenge #8 - Scrabble Word Calculator

dev.to staff on July 05, 2019

Everyone loves a game of Scrabble! Your challenge today is to calculate the scrabble score of a given word. Scoring per tile: To make things e...
Collapse
 
yzhernand profile image
Yozen Hernandez • Edited

Here's my solution in Perl, along with a few tests.

#!/usr/bin/env perl

use v5.24;
use strict;
use warnings;
use feature qw(signatures);
no warnings "experimental::signatures";
use List::Util qw(sum);
use Carp;

my %scores = (
    A   => 1, B   => 3,  C   => 3, D   => 2, E   => 1,
    F   => 4, G   => 2,  H   => 4, I   => 1, J   => 8,
    K   => 5, L   => 1,  M   => 3, N   => 1, O   => 1,
    P   => 3, Q   => 10, R   => 1, S   => 1, T   => 1,
    U   => 1, V   => 4,  W   => 4, X   => 8, Y   => 4,
    Z   => 10,
);

my %multiplier = ( D => 2, T => 3 );

sub scrabble_score ($word) {
    $word = uc($word);
    my $score = 0;
    my $mult  = 1;
    my $count = 0;

    if ( $word =~ s/\((D|T)\)// ) {
        $mult = $multiplier{$1};
    }

    while ( $word =~ s/([[:alpha:]])(\^|\*{0,2})// ) {
        ++$count;
        $score += ( $2 ne '^' ) * ( $scores{$1} + $scores{$1} * length($2) );
    }

    return ( $score * $mult ) + 50*($count == 7);
}


use Test::More tests => 7;

my $word = "quintessential";
is( scrabble_score($word), 23, "Score for $word is 23" );
$word = "he*llo**";
is( scrabble_score($word), 11, "Score for $word is 11" );
$word = "quintessential(t)";
is( scrabble_score($word), 69, "Score for $word is 69" );
$word = "q^uintessential(t)";
is( scrabble_score($word), 39, "Score for $word is 39" );
$word = "he*llo**(d)";
is( scrabble_score($word), 22, "Score for $word is 22" );
$word = "he^llo**(d)";
is( scrabble_score($word), 18, "Score for $word is 18" );
$word = "wordier(d)";
is( scrabble_score($word), 72, "Score for $word is 72" );

Edit: fixed a bug, stray print, and added the 7 letter bonus

Collapse
 
stevemoon profile image
Steve Moon • Edited

Erlang:

-module(devto8).
-export([scrabble_score/1]).

scrabble_score(Input) ->
    LS = #{$a => 1, $b => 3, $c => 3, $d => 2, $e => 1, $f => 4, $g => 2, $h => 4,
           $i => 1, $j => 8, $k => 5, $l => 1, $m => 3, $n => 1, $o => 1, $p => 3,
           $q => 10, $r => 1, $s => 1, $t => 1, $u => 1, $v => 4, $w => 4, $x => 8,
           $y => 4, $z => 10},
    LCInput = string:lowercase(Input),
    score_word(LS, LCInput, []).

score_word(_, [], Scores) when length(Scores) == 7 ->
    score_sum(Scores) + 50;
score_word(_, [$(,$t,$)], Scores) when length(Scores) == 7 ->
    score_sum(Scores) * 3 + 50;
score_word(_, [$(,$d,$)], Scores) when length(Scores) == 7 ->
    score_sum(Scores) * 2 + 50;
score_word(_, [$(,$t,$)], Scores) ->
    score_sum(Scores) * 3;
score_word(_, [$(,$d,$)], Scores) ->
    score_sum(Scores) * 2;
score_word(_, [], Scores) ->
    score_sum(Scores);
score_word(LS, [$^, _ | Rest], Scores) ->
    score_word(LS, Rest, Scores ++ 0);
score_word(LS, [Letter, $*, $* | Rest], Scores) ->
    score_word(LS, Rest, Scores ++ [maps:get(Letter,LS) * 3]);
score_word(LS, [Letter, $* | Rest], Scores) ->
    score_word(LS, Rest, Scores ++ [maps:get(Letter,LS) * 2]);
score_word(LS, [Letter | Rest], Scores) ->
    score_word(LS, Rest, Scores ++ [ maps:get(Letter, LS)]).

score_sum(Scores) ->
    lists:foldl(fun(X, Sum) -> X + Sum end, 0, Scores).
devto8:scrabble_score("z**z**z**z**z**z**z**(t)").
680

devto8:scrabble_score("thiswasfun").
19
Collapse
 
deciduously profile image
Ben Lovy

Ah, neat. Pattern matching was definitely the way to go about it functionally, wish I'd done that instead!

Collapse
 
n8chz profile image
Lorraine Lee • Edited

Good old C. Be advised though there are ways to bork this one with some inputs not consistent with the rubric.

int score(char *word) {
  // Digits are one less than score values for each letter
  // This maps 1-10 to 0-9, allowing one-character representation of each score
  char *letter_scores = "02210313074020029000033739";
  int score = 0, letter_count = 0, mult=1;
  for(char *ptr = word; *ptr; ptr++) {
    if (isalpha(*ptr)) {
      letter_count++;
      int letter_score = letter_scores[toupper(*ptr)-'A']-'0'+1;
      score += letter_score;
      if (*(ptr+1) == '*') {
        score += letter_score;
        if (*(ptr+2) == '*') {
          score += letter_score;
        }
      }
    }
    if (*ptr == '(') {
      mult = *(ptr+1) == 'd' ? 2 : 3;
      break;
    }
  }
  return score*mult+50*(letter_count == 7);
}
Collapse
 
alvaromontoro profile image
Alvaro Montoro • Edited

JavaScript

const scrabbleWordValue = word => {

  // scrabble letter values
  const letterValues = { a:1, b:3, c:3, d:2, e:1, f:4, g:2, 
                         h:4, i:1, j:8, k:5, l:1, m:3, n:1, 
                         o:1, p:3, q:10, r:1, s:1, t:1, u:1, 
                         v:4, w:4, x:8, y:4, z:10 };

  // Pre-requesites - check that the string fulfills a good pattern:
  //   - it must start with a letter
  //   - followed by have any combination of letters and asterisks
  //   - optionally at the end it can have a (t) or (d) modifier
  // if it doesn't follow that pattern (invalid or empty), return 0 points
  if(!word.match(/^([a-z][\*]{0,2})+(\([t|d]\))?$/gi)) {
    return 0;
  }

  // word modifier - calculate the multiplier that will apply to the word:
  //   - 1 (default): the word will not change value
  //   - 2 (if it ends with '(d)'): the word will be multiplied by 2
  //   - 3 (if it ends with '(t)'): the word will be multiplied by 3
  // after calculating the word multiplier, update the word id applicable.
  let wordModifier = 1;
  if (word.indexOf("(") > 0) {
    wordModifier = word.indexOf("(t)") > 0 ? 3 : 2;
    word = word.slice(0,-3);
  }

  // calculate the addition of all letters
  //   - if the letter is an asterisk, use the previous letter
  //   - update the previous letter to current letter
  //   - add the value of the letter to the total
  let previous = 0;
  let valueWord = word.split('')
                      .reduce((acc, curr) => {
                        if (curr == '*') curr = previous;
                        previous = curr;
                        return letterValues[curr] + acc;
                      }, 0);

  // if the words has seven letters, add 50 extra points!
  const allLettersBonus = word.replace(/\*/g,'').length == 7 ? 50 : 0;

  // return the value of the word considering all modifiers
  return valueWord * wordModifier + allLettersBonus;
}

Live demo on CodePen.

Collapse
 
jckuhl profile image
Jonathan Kuhl

A few days late, but here's my JavaScript solution, using reduce.

function scoreWord(string) {
    let word = string.toLowerCase();

    const scores = {
        a: 1,
        // ... etc, cut for conciseness
        z: 10,
        q: 10
    }

    let multiplier = 1;

    if(word.substring(word.length - 1) === '2') {
        multiplier = 2;
        word = word.substring(0, word.length - 1);
    } else if(word.substring(word.length - 1) === '3') {
        multiplier = 3;
        word = word.substring(0, word.length - 1);
    }

    let bonus = word.split('').filter(char => {
        return !['*', '^'].includes(char);
    }).length >= 7 ? 50 : 0;

    return word.split('').reduce((score, letter, index, letters) => {
        const next = index + 1 < letters.length ? letters[index + 1] : null;
        if('abcdefghijklmnopqrstuvwxyz'.includes(letter)) {
            if(next && ['*', '^'].includes(next)) {
                if(next === '^') {
                    return score += 0;
                }
                if(next === '*') {
                    if(index + 2 < letters.length && letters[index + 2] === '*') {
                        score += (scores[letter] * 3);
                    } else {
                        score += (scores[letter] * 2);
                    }
                    return score;
                }
            }
            return score += scores[letter];
        } else {
            return score;
        }
    }, 0) * multiplier + bonus;
}

I changed the rules a bit, since it's hard to distinguish between double/triple words and words that naturally end in d or t, I decided to use 2 or 3 instead.

Collapse
 
deciduously profile image
Ben Lovy • Edited

Because these are fun in languages you don't actually know, here's Haskell:

import Data.Map (Map, (!))
import qualified Data.Map as Map

scores :: Map Char Int
scores = Map.fromList pairs
    where
        pairs = [
            ('a', 1),
            ('b', 3),
            ('c', 3),
            ('d', 2),
            ('e', 1),
            ('f', 4),
            ('g', 2),
            ('h', 4),
            ('i', 1),
            ('j', 8),
            ('k', 5),
            ('l', 1),
            ('m', 3),
            ('n', 1),
            ('o', 1),
            ('p', 3),
            ('q', 10),
            ('r', 1),
            ('s', 1),
            ('t', 1),
            ('u', 1),
            ('v', 4),
            ('w', 4),
            ('x', 8),
            ('y', 4),
            ('z', 10)]

scoreWord :: String -> Int
scoreWord w =
    let
        sevenLetterBonus = if (length $ stripMarkers w) == 7 then 50 else 0
        wordMultiplier =
            let
                suffix = dropWhile (/= '(') w
            in
                if length suffix > 0 then
                    case suffix !! 1 of
                        't' -> 3
                        'd' -> 2
                        _ -> 1
                else 1
        -- 
        preparedWord = expandMarkers $ takeWhile (/= '(') w
        rawScore = sum $ scoreLetters $ preparedWord
    in
        rawScore * wordMultiplier + sevenLetterBonus
    where
        scoreLetters cs = map (\c -> scores ! c) cs

-- transform doubles, triples, carats
-- if we hit an asterisk, replace it with the previous letter
-- if we hit a carat, drop the previus letter
expandMarkers :: String -> String
expandMarkers [] = []
expandMarkers (c:[]) = [c]
expandMarkers (c:rest) =
    case head rest of
        '*' ->
            if (head $ tail rest) == '*' then
                [c] ++ [c] ++ [c] ++ (expandMarkers $ drop 2 rest) else
                [c] ++ [c] ++ (expandMarkers $ tail rest)
        '^' -> expandMarkers $ tail rest
        _ -> [c] ++ expandMarkers rest

-- remove suffix and all markers for deciding on the 7-letter bonus
stripMarkers :: String -> String
stripMarkers w = filter (\c -> c /= '*' && c /= '^') $ takeWhile (/= '(') w

I didn't provide tests, but I think it works. Maybe I'll write some later on.

Collapse
 
kaspermeyer profile image
Kasper Meyer

Ruby solution

require "minitest/autorun"

class ScrabbleLetter
  SCORES = {
    'a' => 1, 'b' => 3,  'c' => 3, 'd' => 2, 'e' => 1,
    'f' => 4, 'g' => 2,  'h' => 4, 'i' => 1, 'j' => 8,
    'k' => 5, 'l' => 1,  'm' => 3, 'n' => 1, 'o' => 1,
    'p' => 3, 'q' => 10, 'r' => 1, 's' => 1, 't' => 1,
    'u' => 1, 'v' => 4,  'w' => 4, 'x' => 8, 'y' => 4,
    'z' => 10
  }.freeze

  # Parses a string of scrabble letters and separates
  # them with their multiplier still intact.
  #
  # @example
  #
  #   ScrabbleLetter.parse("h^i**")
  #   # => [#<ScrabbleLetter @letter="h^">, #<ScrabbleLetter @letter="i**">]
  #
  def self.parse string
    string.chars.each_with_object([]) do |char, letters|
      SCORES[char] ? letters << char : letters[-1] << char
    end.map { |letter| new letter }
  end

  def initialize letter
    @letter = letter
  end

  def score
    letter_score * multiplier
  end

  private

    def letter_score
      SCORES[@letter.chr]
    end

    def multiplier
      return 0 if @letter.end_with?('^')
      return 3 if @letter.end_with?('**')
      return 2 if @letter.end_with?('*')
      return 1
    end
end

class ScrabbleWord
  DOUBLE_WORD_TOKEN = '(d)'.freeze
  TRIPLE_WORD_TOKEN = '(t)'.freeze

  def initialize word
    @word = word
  end

  def score
    letters_score * multiplier + length_bonus
  end

  private

    def letters_score
      letters.map(&:score).reduce(:+)
    end

    def multiplier
      return 2 if @word.end_with?(DOUBLE_WORD_TOKEN)
      return 3 if @word.end_with?(TRIPLE_WORD_TOKEN)
      return 1
    end

    def length_bonus
      letters.count == 7 ? 50 : 0
    end

    def letters
      ScrabbleLetter.parse word_without_multiplier
    end

    def word_without_multiplier
      @word
        .gsub(DOUBLE_WORD_TOKEN, "")
        .gsub(TRIPLE_WORD_TOKEN, "")
    end
end

class ScrabbleWordTest < MiniTest::Test
  def test_simple_word
    assert_equal 23, ScrabbleWord.new("quintessential").score
  end

  def test_double_and_triple_letters
    assert_equal 11, ScrabbleWord.new("he*llo**").score
  end

  def test_triple_word
    assert_equal 69, ScrabbleWord.new("quintessential(t)").score
  end

  def test_blank_tile_with_triple_word
    assert_equal 39, ScrabbleWord.new("q^uintessential(t)").score
  end

  def test_double_and_triple_letters_with_double_word
    assert_equal 22, ScrabbleWord.new("he*llo**(d)").score
  end

  def test_blank_tile_with_double_letter_and_double_word
    assert_equal 18, ScrabbleWord.new("he^llo**(d)").score
  end

  def test_seven_letter_word_bonus
    assert_equal 72, ScrabbleWord.new("wordier(d)").score
  end
end

I borrowed your tests, @yzhernand . Thank you for writing them, so I didn't have to.

Collapse
 
ganderzz profile image
Dylan Paulus

My overly complex (nim) solution :)

from strutils import toLower
from sequtils import filter

type WordType = enum value, multiply, global

type Word = object
  Value: int
  Type: WordType

# a..z
# [97, 122]
const score_mapping = [1, 3, 3, 2, 1, 4, 
                       2, 4, 1, 8, 5, 1, 3, 
                       1, 1, 3, 10, 1, 1, 
                       1, 1, 4, 4, 8 , 4, 10]

# Build a stack, tokenizing the characters
# This will let us apply operations in a reverse order
proc buildStack(word: string): seq[ref Word] =
  result = newSeq[ref Word]()
  let lowerWord = toLower(word)

  for i in 0..(len(lowerWord) - 1):
    let currentWord = new(Word)
    let letter = lowerWord[i]
    let letterAsInt = int(letter)

    if letterAsInt < 97 or letterAsInt > 122:
      if letter == '*':
        currentWord.Value = 2
        currentWord.Type = WordType.multiply
      if letter == '^':
        currentWord.Value = 0
        currentWord.Type = WordType.multiply
      if letter == '(':
        currentWord.Value = if lowerWord[i+1] == 't': 3 else: 2
        currentWord.Type = WordType.global
        result.add(currentWord) # we reached the end of the string
        break
    else:
      let scoreMappingPosition = letterAsInt - 97

      currentWord.Value = score_mapping[scoreMappingPosition]
      currentWord.Type = WordType.value

    result.add(currentWord)

# Gets the value of the operations
proc parseOperations(value: int, operations: seq[ref Word]): int =
  var multiplier = 1

  if len(operations) > 0:
    for operation in operations:
      if operation.Type == Wordtype.multiply:
        if operation.Value == 0:
          return 0
        else:
          multiplier += 1

  result += value * multiplier

proc getScore(stack: seq[ref Word]): int =
  var s = stack # make mutable
  var operations: seq[ref Word]
  var globalMultiplier = 1

  if stack.filter(proc(p: ref Word): bool = p.Type == Wordtype.value).len >= 7:
    result += 50

  while len(s) > 0:
    let item = s.pop()

    if item.Type == WordType.value:
      result += parseOperations(item.Value, operations)
      operations = @[]
    elif item.Type == Wordtype.global:
      globalMultiplier = item.Value
    else:
      operations.add(item)

  result *= globalMultiplier

echo buildStack("d**e*v^(d)").getScore()
Collapse
 
margo1993 profile image
margo1993

Calculates only by letter points


import "strings"

var LETTER_POINTS = map[byte]int{
    'a': 1, 'b': 3, 'c': 3, 'd': 1, 'e': 1,
    'f': 4, 'g': 2, 'h': 4, 'i': 1, 'j': 8,
    'k': 5, 'l': 1, 'm': 3, 'n': 1, 'o': 1,
    'p': 3, 'q': 10, 'r': 1, 's': 1, 't': 1,
    'u': 1, 'v': 4, 'w': 4, 'x': 8, 'y': 4, 'z': 10,
}

func CalculateScrabblePoints(word string) int {
    points := 0

    if len(word) == 7 {
        return 50
    }

    lowerWord := strings.ToLower(word)

    for _, letter := range lowerWord {
        points += LETTER_POINTS[byte(letter)]
    }

    return points
}

Collapse
 
n8chz profile image
Lorraine Lee

Elixir. Admittedly my Exercism solution with a bunch of String.replace calls to handle the more sophisticated way the problem is stated here.

defmodule Scrabble do
  @spec slw?(String.t()) :: boolean
  def slw?(word) do
    word
    |> String.replace(~r/\(.*/, "")
    |> String.replace("*", "")
    |> String.length
    |> Kernel.==(7)
  end

  @spec score(String.t()) :: non_neg_integer
  def score(word) do
    word
    |> String.replace(~r/(.)\*\*/, "\\1\\1\\1")
    |> String.replace(~r/(.)\*/, "\\1\\1")
    |> String.replace(~r/^(.*)\(t\)$/, "\\1\\1\\1")
    |> String.replace(~r/^(.*)\(d\)$/, "\\1\\1")
    |> String.downcase
    |> String.to_charlist
    |> Enum.reduce(if(slw?(word), do: 50, else: 0), fn c, acc ->
      acc + cond do
        c == ?^ -> 0
        c in 'urtoenails' -> 1
        c in 'dg' -> 2
        c in 'bcmp' -> 3
        c in 'fhvwy' -> 4
        c == ?k -> 5
        c in 'jx' -> 8
        c in 'qz' -> 10
        true -> 0
      end
    end)
  end
end
Collapse
 
johncip profile image
jmc

Clojure:

(ns scrabble
  (:require [clojure.string :as s]))

(def values
  (zipmap
    [\a \b \c \d \e \f \g \h \i \j \k \l \m \n \o \p \q \r \s \t \u \v \w \x \y \z]
    [ 1  3  3  2  1  4  2  4  1  8  5  1  3  1  1  3 10  1  1  1  1  4  4  8  4 10]))

(defn letter-score [[a b c]]
  (let [base (values a 0)
        mult (cond (= b \^)                0
                   (and (= b \*) (= c \*)) 3
                   (= b \*)                2
                   :else                   1)]
    (* base mult)))

(defn trim [word]
  (s/replace word #"\(\w\)$" ""))

(defn base-word-score [word]
  (->> (trim word)
       (partition-all 3 1)
       (map letter-score)
       (apply +)))

(defn bonus [word]
  (if (= 7 (count (s/replace (trim word) #"\W" ""))) 50 0))

(defn score [word]
  (let [base (base-word-score word)
        mult (cond (s/ends-with? word "(t)") 3
                   (s/ends-with? word "(d)") 2
                   :else                     1)]
    (+ (bonus word) (* base mult))))
Collapse
 
wolverineks profile image
Kevin Sullivan • Edited

codesandbox.io/s/daily-challenges-...

import { pipe, toUpper } from "ramda";

// CHARACTERS, POINTS, BONUSES //////////////////////////////////////
const A = { char: "A", points: 1 } as const;
const B = { char: "B", points: 3 } as const;
const C = { char: "C", points: 3 } as const;
const D = { char: "D", points: 2 } as const;
const E = { char: "E", points: 1 } as const;
const F = { char: "F", points: 4 } as const;
const G = { char: "G", points: 2 } as const;
const H = { char: "H", points: 4 } as const;
const I = { char: "I", points: 1 } as const;
const J = { char: "J", points: 8 } as const;
const K = { char: "K", points: 5 } as const;
const L = { char: "L", points: 1 } as const;
const M = { char: "M", points: 3 } as const;
const N = { char: "N", points: 1 } as const;
const O = { char: "O", points: 1 } as const;
const P = { char: "P", points: 3 } as const;
const Q = { char: "Q", points: 10 } as const;
const R = { char: "R", points: 1 } as const;
const S = { char: "S", points: 1 } as const;
const T = { char: "T", points: 1 } as const;
const U = { char: "U", points: 1 } as const;
const V = { char: "V", points: 4 } as const;
const W = { char: "W", points: 4 } as const;
const X = { char: "X", points: 8 } as const;
const Y = { char: "Y", points: 4 } as const;
const Z = { char: "Z", points: 10 } as const;
// prettier-ignore
const charMap = {Q,W,E,R,T,Y,U,I,O,P,A,S,D,F,G,H,J,K,L,Z,X,C,V,B,N,M} as const;
const tripleWordBonus = "(T)";
const doubleWordBonus = "(D)";
const tripleLetterBonus = "**";
const doubleLetterBonus = "*";

// TYPES ////////////////////////////////////////////////////////////
type ValueOf<T> = T[keyof T];
type Char = ValueOf<typeof charMap>;
type LetterTile = { type: "LETTER" };
type BlankTile = { type: "BLANK" };
type Square = { letterBonus: 1 | 2 | 3; wordBonus: 1 | 2 | 3 };

type Tile = ((LetterTile) | BlankTile) & Char & Square;

// CONVERT STRING TO TILES //////////////////////////////////////////
const REGEX = /[A-Z](\*|\^|\((T|D)\))*/g; // https://bit.ly/31LZ0mZ
const parse = (input: string): Tile[] => {
  const strings: string[] = input.match(REGEX) || [];
  return strings.map(toTile);
};

const type = (input: string) => (input.includes("^") ? "BLANK" : "LETTER");
const letterAndPoints = (char: Tile["char"]) => charMap[char];
const letterBonus = (input: string) => {
  if (input.includes(tripleLetterBonus)) return 3;
  if (input.includes(doubleLetterBonus)) return 2;
  return 1;
};
const wordBonus = (input: string) =>
  input.includes(tripleWordBonus) ? 3 : input.includes(doubleWordBonus) ? 2 : 1;
const toTile = (input: string): Tile => {
  const chars = input.split("");
  const [char, ...rest] = chars;
  const modifiers = rest.join("");
  return {
    type: type(modifiers),
    ...letterAndPoints(char),
    letterBonus: letterBonus(modifiers),
    wordBonus: wordBonus(modifiers)
  };
};

// CALCULATE SCORE //////////////////////////////////////////////////
const letterPoints = (tiles: Tile[]) =>
  tiles.reduce(
    (result, { type, char, letterBonus }) =>
      type === "BLANK" ? result : result + charMap[char].points * letterBonus,
    0
  );
const wordBonusMultiplier = (tiles: Tile[]) =>
  tiles.reduce((result, { wordBonus }) => result * wordBonus, 1);
const BINGO = { length: 7, points: 50 };
const bingoPoints = (tiles: Tile[]) =>
  tiles.length === BINGO.length ? BINGO.points : 0;

const scoreTiles = (tiles: Tile[]) =>
  letterPoints(tiles) * wordBonusMultiplier(tiles) + bingoPoints(tiles);

// WHOLE ENCHILADA //////////////////////////////////////////////////
export const getScore = pipe(
  toUpper,
  parse,
  scoreTiles
);
Collapse
 
oscherler profile image
Olivier “Ölbaum” Scherler

I’m learning Erlang.

I had forgotten that you can have several elements to the Head in a pattern match (then I saw @stevemoon ’s solution), so I used lists:foldr to process the word from the end, which means I accumulate the multipliers and apply them once I encounter a letter. I could have named variables better, but with short names it’s easier on the eye, with this state tuple I’m moving around.

For fun I decided to allow several word multipliers, and to allow the blank indicator before, after, or in the middle of the asterisks.

I made my own tests, and stole other peoples’ as well.

-module( scrabble ).
-export( [ word_score/1, letter_score/1 ] ).

-include_lib("eunit/include/eunit.hrl").

letter_score( Letter ) ->
    Points = #{
        $a => 1, $b => 3, $c => 3, $d => 2, $e => 1,
        $f => 4, $g => 2, $h => 4, $i => 1, $j => 8,
        $k => 5, $l => 1, $m => 3, $n => 1, $o => 1,
        $p => 3, $q => 10, $r => 1, $s => 1, $t => 1,
        $u => 1, $v => 4, $w => 4, $x => 8, $y => 4,
        $z => 10
    },
    maps:get( Letter, Points ).

word_score( Word ) ->
    { Score, Wm, _Lm, Lc, _State } = lists:foldr(
        fun char/2,
        { 0, 1, 1, 0, normal },
        string:lowercase( Word )
    ),
    Bonus = case Lc of 7 -> 50; _ -> 0 end,
    Score * Wm + Bonus.

% Score
% Wm = Word multiplier
% Lm = Letter multiplier
% Lc = Letter count
% Parser state
char( $\), { Score, Wm, Lm, Lc, normal } ) ->
    { Score, Wm, Lm, Lc, word_mult };
char( $d, { Score, Wm, Lm, Lc, word_mult } ) ->
    { Score, 2 * Wm, Lm, Lc, word_mult };
char( $t, { Score, Wm, Lm, Lc, word_mult } ) ->
    { Score, 3 * Wm, Lm, Lc, word_mult };
char( $\(, { Score, Wm, Lm, Lc, word_mult } ) ->
    { Score, Wm, Lm, Lc, normal };
char( $*, { Score, Wm, 0, Lc, normal } ) ->
    { Score, Wm, 0, Lc, normal };
char( $*, { Score, Wm, 1, Lc, normal } ) ->
    { Score, Wm, 2, Lc, normal };
char( $*, { Score, Wm, 2, Lc, normal } ) ->
    { Score, Wm, 3, Lc, normal };
char( $^, { Score, Wm, _Lm, Lc, normal } ) ->
    { Score, Wm, 0, Lc, normal };
char( L, { Score, Wm, Lm, Lc, normal } ) ->
    { Score + Lm * letter_score( L ), Wm, 1, Lc + 1, normal }.

char_test() -> [
    ?assert( char( $\), { 0, 1, 1, 0, normal } ) =:= { 0, 1, 1, 0, word_mult } ),
    ?assert( char( $d, { 0, 1, 1, 0, word_mult } ) =:= { 0, 2, 1, 0, word_mult } ),
    ?assert( char( $d, { 0, 2, 1, 0, word_mult } ) =:= { 0, 4, 1, 0, word_mult } ),
    ?assert( char( $t, { 0, 2, 1, 0, word_mult } ) =:= { 0, 6, 1, 0, word_mult } ),
    ?assert( char( $\(, { 0, 3, 1, 0, word_mult } ) =:= { 0, 3, 1, 0, normal } ),
    ?assert( char( $a, { 0, 1, 1, 0, normal } ) =:= { 1, 1, 1, 1, normal } ),
    ?assert( char( $b, { 1, 1, 1, 1, normal } ) =:= { 4, 1, 1, 2, normal } ),
    ?assert( char( $*, { 4, 1, 1, 2, normal } ) =:= { 4, 1, 2, 2, normal } ),
    ?assert( char( $*, { 4, 1, 2, 2, normal } ) =:= { 4, 1, 3, 2, normal } ),
    ?assert( char( $^, { 4, 1, 1, 2, normal } ) =:= { 4, 1, 0, 2, normal } ),
    ?assert( char( $*, { 4, 1, 0, 2, normal } ) =:= { 4, 1, 0, 2, normal } )
].

score_test() -> [
    1 = word_score("A"),
    2 = word_score("A(d)"),
    3 = word_score("A(t)"),
    6 = word_score("A(dt)"),
    6 = word_score("A(td)"),
    12 = word_score("A(ddt)"),
    12 = word_score("A(dtd)"),
    18 = word_score("A(ttd)"),
    4 = word_score("AB"),
    5 = word_score("A*B"),
    6 = word_score("A**B"),
    14 = word_score("SCRABBLE"),
    28 = word_score("SCRABBLE(d)"),
    42 = word_score("SCRABBLE(t)"),
    21 = word_score("F**OX"),
    38 = word_score("F**O*X**"),
    63 = word_score("PROBLEM"),
    77 = word_score("PR*OB**LE*M**"),
    12 = word_score("ZER^O"),
    12 = word_score("ZER*^O"),
    12 = word_score("ZER**^O"),
    12 = word_score("ZER^*O"),
    12 = word_score("ZER^**O"),
    12 = word_score("ZER*^*O"),
    68 = word_score("P**RO*B^LE*M"),

    23 = word_score("QUINTESSENTIAL"),
    11 = word_score("HE*LLO**"),
    69 = word_score("QUINTESSENTIAL(t)"),
    39 = word_score("Q^UINTESSENTIAL(t)"),
    22 = word_score("HE*LLO**(d)"),
    18 = word_score("HE^LLO**(d)"),
    72 = word_score("WORDIER(d)"),

    680 = word_score("Z**Z**Z**Z**Z**Z**Z**(t)"),
    19 = word_score("THISWASFUN")
].

To run:

% erl
1> c(scrabble).
{ok,scrabble}
2> scrabble:test().
  2 tests passed.
ok
Collapse
 
coreyja profile image
Corey Alexander • Edited

Ooh I love Scrabble! I'm excited for this one lol

Collapse
 
coreyja profile image
Corey Alexander

Here is my over engineered solution in Rust!

I broke it down into multiple different Rust strucs and enums! I'm getting more and more familiar with the Rust type system, and breaking down a game like this is great practice!

One thing I slightly added, is the ability to parse multiple word modifiers together! I opted for a (t)(d) syntax do indicate both a triple and double word score. This is possible in Scrabble, so I wanted to include support for it

My solution and test cases is just over 250 lines so here is the link to the source file in Github!
github.com/coreyja/dev-to-challeng...

Collapse
 
coreyja profile image
Corey Alexander

Oh no, I didn't get to it today! Tomorrow will have to be a double day!

Collapse
 
zapperdulchen profile image
Zapperdulchen

Clojure:

(def letter-scores
  {\a 1, \b 3,  \c 3, \d 2, \e 1,
   \f 4, \g 2,  \h 4, \i 1, \j 8,
   \k 5, \l 1,  \m 3, \n 1, \o 1,
   \p 3, \q 10, \r 1, \s 1, \t 1,
   \u 1, \v 4,  \w 4, \x 8, \y 4,
   \z 10})

(def letter-bonus {[\*] 2, [\*\*] 3, [\^] 0})
(def word-bonus {"d" 2, "t" 3})

(defn letter-score [letter]
  (* (letter-scores (first letter)) (get letter-bonus (rest letter) 1)))

(defn scrabble-score [word]
  (let [tokens (re-seq #"([a-z][*^]*)|\(([dt])\)" word)
;; (re-seq #"([a-z][*^]*)|\(([dt])\)"  "he*llo**(d)")
;; => (["h" "h" nil] ["e*" "e*" nil] ["l" "l" nil] ["l" "l" nil] ["o**" "o**" nil] ["(d\
)" nil "d"])
        letters (filter some? (map second tokens))
        word-factor (get word-bonus
                         (first (filter some? (map #(nth % 2) tokens)))
                         1)
        seven-letter-bonus (if (= 7 (count letters)) 50 0)]
    (+ seven-letter-bonus
     (* word-factor
        (reduce + (map letter-score letters)))))))
Enter fullscreen mode Exit fullscreen mode