PA2: Hot Letters
If you have played Wordle, you already know how this works. Guess a secret word, and every letter of your guess comes back with a color:
- 🟩
{s}Green is on fire. Right letter, right spot. - 🟨
[s]Yellow is warm. Right letter, wrong spot. - ⬜
(s)Gray is ice cold. That letter isn't in the word at all.
Colors aren't easy for everyone to tell apart, so in this PA each letter also gets wrapped in brackets that say the same thing. Think of it as a thermometer: the curlier the bracket, the hotter the letter.
Haven't played? No problem. Those three rules, plus one twist below, are the whole game.
Here's the twist, and plenty of players go months without noticing it.
Say the answer is abide and you guess speed.
Both es in your guess are "in the word", so are they both yellow?
No: the answer has only one e, so only one of yours turns yellow, and the other is gray.
The game treats the answer's letters like a supply that gets used up.
In this PA, you decide how hot every letter is. Your code works out which of the answer's letters are left over after the greens, then colors a guess one letter at a time, using up a letter each time it gives a yellow. And then you write the tests that prove it handles the tricky cases.
This is one of the two PA2 options; do this one or Magazine Collage, not both.
What you'll practice
- Looping through a string by index, so you can compare the guess and the answer letter by letter.
- Counting letters in a dictionary: adding a key the first time you see it, and adding 1 to it after that.
- Checking whether a key is in a dictionary before you look it up.
- Building a new string one character at a time.
- Writing
pytesttests withassert, for ordinary inputs and for the edge cases.
Get set up
Download both files and save them in the Week08 folder inside ~/CS149:
-
hot.py, the module with your two functions -
test_hot.py, the test file you fill in for Part C
Open hot.py and look around.
Both functions are already there, each with type hints and a docstring that says exactly what it takes and what it returns, and each body is just pass.
Your job is to replace those pass lines with real code.
Read each docstring all the way through, examples included, before you start that function.
The main block at the bottom is written for you; leave it alone.
Put your name and today's date in place of YOUR NAME and THE DATE at the top of both files.
Gradescope checks.
Submit early to Gradescope
If you don't submit to Gradescope and get a nonzero score, you will receive a reflection grade of no higher than 15/30 points.
Upload both files, every time
Gradescope needs both files in every submission, or it gives you 0 points. If you haven't started your tests yet, upload the test file just as you downloaded it; your functions still get graded.
How scoring works
A score is a string that holds every letter of the guess, in order, each one wrapped in brackets that show how hot it is:
| Color | Temperature | Brackets | Example |
|---|---|---|---|
| green | on fire 🔥 | { } |
{s} |
| yellow | warm | [ ] |
[s] |
| gray | ice cold | ( ) |
(s) |
So a score for a five-letter guess is always 15 characters long: three for each letter.
Here is the whole rule, and it takes two passes over the word:
- Greens first. Any letter in the same spot in both words is green. Those letters of the answer are used up.
- Then the rest, left to right. A letter that is not green is yellow if the answer has one of it left over, and that yellow uses it up. Otherwise it is gray.
Here is speed against abide:
| Spot | 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|---|
| Guess | s | p | e | e | d |
| Answer | a | b | i | d | e |
| Tile | (s) |
(p) |
[e] |
(e) |
[d] |
No letter matches its spot, so nothing is green, and all five letters of abide are left over: one each of a, b, i, d, and e.
The first e in the guess takes the answer's only e and turns yellow.
By the time the second e comes along, there are none left, so it is gray.
The d takes the d.
Score: (s)(p)[e](e)[d].
Quick check: what is the score for eat against tea?
[e][a][t].
No letter is in the right spot, but every letter is somewhere in the answer, and each appears only once.
Part A: count what's left with leftovers
leftovers(guess, answer) does step 1 of the rule.
It goes through the two words spot by spot, and for every spot where the guess has a different letter from the answer, it counts the answer's letter in a dictionary.
Letters the guess got right are not counted, because the greens use them up.
>>> leftovers("speed", "abide")
{'a': 1, 'b': 1, 'i': 1, 'd': 1, 'e': 1}
>>> leftovers("sheep", "geese")
{'g': 1, 'e': 2, 's': 1}
>>> leftovers("speed", "speed")
{}
Here's the second example, spot by spot, so you can watch the dictionary fill up:
| Spot | Guess | Answer | Same? | Dictionary afterward |
|---|---|---|---|---|
| 0 | s | g | no, count g |
{'g': 1} |
| 1 | h | e | no, count e |
{'g': 1, 'e': 1} |
| 2 | e | e | yes, skip it | {'g': 1, 'e': 1} |
| 3 | e | s | no, count s |
{'g': 1, 'e': 1, 's': 1} |
| 4 | p | e | no, count e |
{'g': 1, 'e': 2, 's': 1} |
geese has three es, but the middle one is green (sheep has an e there too), so only two are left over.
Notice that you always count the answer's letter, never the guess's.
Predict it: what does leftovers("lilac", "llama") return?
{'l': 1, 'a': 2, 'm': 1}.
Spot 0 is green (l and l), so that l is not counted.
Every other spot is different, so the answer's second l, its m, and both of its as are counted.
Loop over the indexes, not the letters
You need guess[i] and answer[i] at the same time, so a loop over one string's letters is not enough.
Loop over range(len(answer)).
Then counting is the pattern from class: if the letter is already a key in the dictionary, add 1 to it; otherwise set it to 1.
Part B: color a guess with score
score(guess, answer) does step 2 of the rule.
It calls your leftovers to get the counts, then builds the tiles one letter at a time, starting from an empty string.
Each letter of the guess is one of three cases, in this order:
- It matches the answer in that spot: on fire, so wrap it in
{}. - The counts have one of it to spare (it is a key, and its count is more than 0): warm, so wrap it in
[], and take 1 off its count. - Anything else: ice cold, so wrap it in
().
>>> score("speed", "abide")
'(s)(p)[e](e)[d]'
>>> score("sheep", "geese")
'[s](h){e}[e](p)'
>>> score("geese", "geese")
'{g}{e}{e}{s}{e}'
Gluing on a tile
Each tile is three strings joined together: an opening bracket, the letter, and a closing bracket.
You already know how to join strings with +.
(If you try an f-string instead, watch out: { and } mean something special inside one.)
Your turn: what is score("allay", "llama")?
[a]{l}[l][a](y).
Only spot 1 is on fire.
That leaves one l, two as, and an m to spare.
The first a takes an a, the second l takes the last l, the second a takes the last a, and y is not in llama.
It's fine to change that dictionary
score takes 1 off the counts every time it gives a yellow.
That is safe because leftovers builds a new dictionary every time you call it, so nothing outside score sees the change.
Trace it: what is score("sassy", "stamp")?
{s}[a](s)(s)(y).
The first s is green, so it uses up the answer's only s.
The a is yellow.
The other two ss find no s left over, so they are gray, and so is y.
This is the case that catches a scorer that just asks "is this letter in the answer?"
Part C: prove it with test_hot.py
Open test_hot.py.
It imports your two functions and has one test function for each, test_leftovers and test_score, with a docstring suggesting what to test.
Replace each pass with at least three assert statements, each checking one call against an answer you worked out yourself:
Run your tests from the terminal, in your Week08 folder:
or click the beaker icon in VS Code's Testing view, as you did in the pytest lab.
Gradescope grades your tests in three ways:
- They must pass on a correct
hot.py. A test that fails there has a wrong expected value. Gradescope runs your tests against the correct solution here, not against your own code. - They must fail on a
hot.pywhose functions return nonsense, so every test has to check what a function returns. - They must catch bugs. Gradescope plants a mistake in a correct
hot.py, one at a time, and at least one of your tests has to fail on each. The mistakes are the kind people really make in this PA.
Where the bugs hide
The easy cases (all green, all gray) are worth a test, but almost any code gets them right. The bugs hide in the cases from this page: a repeated letter, and a letter that is green in one spot and repeated somewhere else. For every function, ask yourself, "What is the case most likely to go wrong?" and write that test.
Test it before you submit
Run pytest after every change.
If a test fails, read the message: it shows the value your function returned next to the one your test expected.
Before you decide the function is wrong, work out the answer by hand once more, because sometimes the test is the one with the mistake.
When both functions work, press Run on hot.py, and the main block replays a game whose answer is geese:
Before you submit, score guesses 1 and 2 on paper, and check that your output agrees.
Rules
Only what we have covered
Gradescope rejects a hot.py that uses any of these:
- a
whileloop (Week 9) - slicing, like
guess[1:3](Week 10) - a comprehension or a
lambda(Week 11) count(),index(),find(), orreplace()- the dictionary methods
get(),setdefault(),keys(),values(), anditems()(Week 11) - any
import, such asCounterfromcollections
Each of these would do a loop's job for you, and writing the loop yourself is what this PA is for. Your test file may use anything.
Submission
Submit both hot.py and test_hot.py to the Gradescope assignment PA2a Hot Letters, then commit and push your Week08 folder (how).
Gradescope also checks that your name and the date are in both docstrings, and that ruff check is clean on both files.
Remember to upload both files every time, even if test_hot.py is still the starter version.
Do this option or Magazine Collage, not both.
Where the game comes from
Hot Letters uses the scoring rule from Wordle, which Josh Wardle made in 2021 as a present for his partner, a word-game fan. It spread to millions of players within months and is now part of The New York Times Games. Your version is just the scorer; the daily word, the six tries, and the keyboard are up to the real thing. This PA is a smaller cousin of an older CS 149 project, Words, which built a whole game like it.