Scrabble excercise

I’m attempting the scrabble exercise.

I’m attempting it using an associative array. First of all, how many formats of an associative array are there?

I’ve seen:

declare -A letters
letters=( ["b"]="3" ["c"]="3" ["m"]="3" ["p"]="3" )

and also:

declare -A letters
letters["bcmp"]=3
letters["aeioulnrst"]=1
letters["dg"]=2
letters["fhvwy"]=4
letters["k"]=5
letters["jx"]=8
letters["qz"]=10

I’ve tried both, does it matter which?

That last one is what I’m trying now. When I enter a string at the terminal and run the script with debugging turned on and some echo statements, the output just scrolls endlessly.

set -x

i=0
counter="0"
 
declare -A letters

letters["bcmp"]=3
letters["aeioulnrst"]=1
letters["dg"]=2
letters["fhvwy"]=4
letters["k"]=5
letters["jx"]=8
letters["qz"]=10
 
 while [[ $i < "${#letters[@]}" ]]
 
  do
 
     if [[ $@ == "${!letters[@]}" ]]
       then
 
       (( counter+=${letters[@]} ))
        echo "counter is: $counter"
        ((i++))
        echo "i is $i"
   
     fi
 
  done

It’s supposed to be checking the input against each element of the array and assigning the value to counter

It just scrolls this output endlessly:

[ 0 < 7 ]]
[[ dgfjqb == \q\z \d\g \f\h\v\w\y \k \b\c\m\p \j\x \a\e\i\o\u\l\n\r\s\t ]]

As always, any suggestions are appreciated!

Absolutely. Try both in a new terminal then do declare -p letters afterwards. That will show what you set. They will look different.

As the output shows, neither side of the check is a single letter so the if never matches and i is never incremented. Since i never changes, the loop never exits.

When displaying the different formats of the Associative array with declare -p, they show exactly what I would expect of them:

declare -A letters=([qz]="10" [dg]="2" [fhvwy]="4" [k]="5" [bcmp]="3" [jx]="8" [aeioulnrst]="1" )

and:

declare -A letters=([p]="3" [m]="3" [c]="3" [b]="3" )

The issue is neither one seems to be “browseable” in the sense that when the input hits them, even one character at a time, they don’t find the match:

[[ 0 < 7 ]]
+ [[ d == \q\z \d\g \f\h\v\w\y \k \b\c\m\p \j\x \a\e\i\o\u\l\n\r\s\t ]]

d should obviously match the [dg]="10" element of the array, but it can’t seem to see into it.

d and dg have different values. Why would you expect them to match? Though you’re not even comparing those two values! The output shows you’re checking if d is equal to \q\z \d\g \f\h\v\w\y \k \b\c\m\p \j\x \a\e\i\o\u\l\n\r\s\t. d would only match d and nothing longer than one character.

This does not expand to a single array key, it expands to all the keys the array has (a list of strings). There is a syntax to test for the existance of a key in an array and there is a syntax to access the contents of the array by a key. But there is no syntax to partially match an array key and retreive the full key. This is why the difference between letters=( ["b"]="3" ["c"]="3" ["m"]="3" ["p"]="3" ) and letters["bcmp"]=3 matters: the latter has one single 4-letter-key, the first has 4 separate keys.

What you need to do for multi-letter keys is loop over the list of keys, partially string match your letter to each key and then use the full key to access the array content. For single letter keys you only have to read the value from the array by using the letter as the key.

1 Like

I tried the following:

declare -A letters
  
  letters["bcmp"]=3
  letters["aeioulnrst"]=1
  letters["dg"]=2
  letters["fhvwy"]=4
  letters["k"]=5
  letters["jx"]=8
  letters["qz"]=10

  counter=0

 read -p "Enter a letter: " input

for input in "${!letters[@]}"
 
 do
 
 (( counter+=${letters[@]} ))
   echo "counter is: $counter"
 
 done

I get 7 of these errors:

+ for input in "${!letters[@]}"
+ ((  counter+=10 2 4 5 3 8 1  ))
./scrabble_test.sh: line 30: ((: counter+=10 2 4 5 3 8 1 : syntax error in expression (error token is "2 4 5 3 8 1 ")
+ echo 'counter is: 10'
counter is: 10

With each iteration incrementing counter by 10, so it goes from 10 to 70.
So it seems to be only assigning the value of ["qz"]=10, instead of looping through each value, checking input against it.

I tried replacing input with $@, but I received an error that said it was invalid.

I also tried using substring expansion, but I couldn’t get it to work. I read in the manual that substring expansion produces undefined results with Associative arrays.

read -p "Enter a letter: " input

OK $input is whatever the user typed in.

for input in "${!letters[@]}"

But now, $input is one of the array keys. We no longer know what the user typed in. You want to use a different loop variable. Let’s use a

for a in "${!letters[@]}"
do
  ...
done

Inside the loop, we need to test if the user input is contained in $a.
We can use this check

  if [[ "$a" == *"$input"* ]]
  then
    ...
  fi

Inside double brackets, the == operators does glob matching.

If the user input is the letter t, and $a is bcmp then [[ bcmp == *t* ]] is falsy, but when $a is aeioulnrst then [[ aeioulnrst == *t* ]] is truthy.

What do we do when we find a match? We add the score to the counter. But we need to add the score for the current $a, not use all the scores at the same time

(( counter += ${letters[$a]} ))
# ......................^^

# or, bash allows us to drop the "parameter expansion
# syntax" in an arithmetic expression
(( counter += letters[$a] ))

Putting that all together

read -p "Enter a letter: " input

for a in "${!letters[@]}"
do
  if [[ "$a" == *"$input"* ]]
  then
    (( counter += letters[$a] ))
    echo "counter is: $counter"
  fi
done

That solution works perfectly for individual characters, but not for whole words, which is what the exercise asks for.

I tried to get it to work with whole words:

#!/bin/bash
  
  declare -A letters
   
  letters["bcmp"]=3
  letters["aeioulnrst"]=1
  letters["dg"]=2
  letters["fhvwy"]=4
  letters["k"]=5
  letters["jx"]=8
  letters["qz"]=10
  
  counter=0
   
  set -x
  echo "Enter a letter: "
  read -a input
  
  for x in "${input[@]}"
    
    do
       
     if [[ "$x" == "${!letters[@]}" ]]
  
      then
  
       (( counter+=${letters[@]} ))
        
     fi 
  
   done
  
  echo "counter is: $counter"

Using the word “cabbage” as in the instructions, I get the following output with debugging turned on:

+ echo 'Enter a letter: '
Enter a letter: 
+ read -a input
c a b b a g e
+ for x in "${input[@]}"
+ [[ c == *\q\z \d\g \f\h\v\w\y \k \b\c\m\p \j\x \a\e\i\o\u\l\n\r\s\t* ]]
+ for x in "${input[@]}"
+ [[ a == *\q\z \d\g \f\h\v\w\y \k \b\c\m\p \j\x \a\e\i\o\u\l\n\r\s\t* ]]
+ for x in "${input[@]}"
+ [[ b == *\q\z \d\g \f\h\v\w\y \k \b\c\m\p \j\x \a\e\i\o\u\l\n\r\s\t* ]]
+ for x in "${input[@]}"
+ [[ b == *\q\z \d\g \f\h\v\w\y \k \b\c\m\p \j\x \a\e\i\o\u\l\n\r\s\t* ]]
+ for x in "${input[@]}"
+ [[ a == *\q\z \d\g \f\h\v\w\y \k \b\c\m\p \j\x \a\e\i\o\u\l\n\r\s\t* ]]
+ for x in "${input[@]}"
+ [[ g == *\q\z \d\g \f\h\v\w\y \k \b\c\m\p \j\x \a\e\i\o\u\l\n\r\s\t* ]]
+ for x in "${input[@]}"
+ [[ e == *\q\z \d\g \f\h\v\w\y \k \b\c\m\p \j\x \a\e\i\o\u\l\n\r\s\t* ]]
+ echo 'counter is: 0'
counter is: 0

I’m trying to get it to cycle/browse through the array keys, but it’s not working.
And I have to enter the input with spaces between each character:
c a b b a g e

I tried substring expansion, but that didn’t work either.

So is it pointless to try substring expansions in Associative arrays?
It seems like a perfect way to cycle through each character in the keys, but the GNU manual says it’s undefined to use it with Associative arrays.

As always, any help is appreciated.

Could you explain your approach/logic in English first? It’s not clear what/how you expect this to work. I’d start with a plan, in English, of what you’re trying to achieve, vs sharing code which definitely doesn’t work and expecting us to figure from non-working code which part isn’t working as intended … and also work out the intent from code that clearly doesn’t follow the intent.

@IsaacG

I’m not sure what you don’t understand about my last post. It’s written the same as any other that I’ve posted here, and you’ve responded to. My intent is to solve the Scrabble exercise, as has been stated. As far as posting code that doesn’t work, that’s how it’s done on most of these forums. If my code is bad, it’s because I’m new to this, and so not very good.

Could you try explaining your approach to solving this exercise?

Usually walking through an approach, in English, is the first step and best approach to breaking down what steps ought to happen. Once you have the steps (an “algorithm”), it’s a lot easier to see issues in the code, as you can tell which steps do or do not line up with the stated algorithm.

Without an approach/algorithm, it can be difficult to discuss logical bugs in code. So, I would encourage you to start with a set of steps using English and no coding constructs (eg “keys” or “associative arrays”).

1.) Create an associative array, whose keys contain the letters of the alphabet, and whose values are the values that each letter is assigned.

2.) Accept user input through a Read command, which places the input into an array.

3.) Loop through the user input (array) with a For loop.

4.) Use a nested For loop, to loop through the keys (alphabet), of the array.

5.) If the current letter of the user input is equal to the current letter of the array keys…

6.) Increment the Counter with the value of the letter.

7.) When the loop is finished, display the value of Counter.

-This is based on my current idea for the code, not what is posted above.

Step 3 loops over an array of words so the current element will be a word.

Step 4 loops over letters.

Step 5 is comparing … words to letters? Those likely wouldn’t match in most cases.

The whole point of an associate array is that you can do key lookups. Looping over the keys of an associative array defeats the whole point of them.

@IsaacG They want to use an associative array with words as keys, like bcmp. So they have to loop over the key itself to match to the input letter.

@duvel You now have a written down algorithm, please add examples of the variables with values you expect to have in each step like so:

  1. Create an associative array, […]
letter['bcmp']=3

This should show you, which steps are missing. And we can see, where you may have wrong assumptions about how associative arrays in BASH work.

Step 4 mentions looping through keys. Keys implies associative array. The only associative array mentioned prior to step 4 would be in step 1. Step 1 is the letter to score napping, not the user input.

To be specific, I want to loop through the individual characters of the keys, not as whole words: b c m p not 'bcmp'. I’m assuming (maybe incorrectly) this would work for both whole words or individual letters entered by the user.

I slightly changed the wording of the algorithm to reflect this.

1.) Create an associative array, whose keys contain the letters of the alphabet, and whose values are the values that each letter is assigned.

letter['bcmp']=3

2.) Accept user input through a Read command, which places the input into an array.

echo "Enter a letter/word: "
read -a input

3.) Loop through the user input (array) one char at a time, with a For loop.

for x in "${input[@]}"

4.) Use a nested For loop, to loop through the keys (alphabet) one char at a time, of the array.

for i in "${!letters[@]}"

5.) If the current letter of the user input is equal to the current letter of the array key.

if [[ "$x" == "$i" ]]

6.) Increment the Counter with the value of the letter.

(( counter+=${letters[@]} ))

7.) When the loop is finished, display the value of Counter.

echo “counter is: $counter”

This loops through elements of the array, which are words, and not characters.

This also loops through keys of the array, which are groups of letters, not individual characters.