DEV Community

dev.to staff
dev.to staff

Posted on

Daily Challenge #8 - Scrabble Word Calculator

Everyone loves a game of Scrabble! Your challenge today is to calculate the scrabble score of a given word.

Scoring per tile:

Here is the scoring per tile

To make things even more challenging, please consider additional scoring as follows:

Double letter (doubles the value of the letter)
-A double letter will be represented with an asterisk after the letter. he*llo would make a double letter on the e.

Triple letter (triples the value of the letter)
-A triple letter will be represented with two asterisks after the letter. he**llo would make a triple letter on the e.

Double word (double the value of the word after letter rules have been applied)
-A double word is represented by the word ending in (d)

Triple word (triple the value of the word after letter rules have been applied)
-A triple word is represented by the word ending in (t)

A blank (the letter given will score 0)
-A blank tile will be represented with a caret after the letter or asterisk is the letter has a double or triple letter value. he^llo would mean the e scores 0.

Bonus 50!
-If the word is a seven letter word an additional 50 points are awarded.

Good luck and happy coding!

This challenge comes from user grantw1991 on Codewars

Thank you to CodeWars, who has licensed redistribution of this challenge under the 2-Clause BSD License!

Want to propose a challenge for a future post? Email yo+challenge@dev.to with your suggestions!

Top comments (18)

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
 
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
 
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
 
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
 
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
 
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
 
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))))