Storyteller Word Odds
A text adventure game writes its narration with a small language model. At each step the model gives every candidate next word a raw score, and the game turns those scores into probabilities to pick from. A "creativity" slider lets players choose between safe, predictable narration and wild surprises.
The slider is the temperature. Divide every score by it, then turn the results into probabilities with softmax. A low temperature sharpens the odds toward the top word. A high temperature flattens them so unlikely words get a real chance. The temperature setting in chat assistants works this way.
Your task
Write wordOdds(scores, temperature). Let zᵢ = scores[i] / temperature. Return the list of probabilities pᵢ = e^zᵢ / (e^z₀ + e^z₁ + …), in the same order as scores.
Scores can be large, so your answer must not overflow. The probabilities must add up to 1.
wordOdds(scores: float[], temperature: float) → float[] Numbers are accepted within 1e-6.
Examples
scores = [2, 1, 0] temperature = 1
[0.6652409557748218, 0.24472847105479764, 0.09003057317038046]
e² ≈ 7.389, e¹ ≈ 2.718, e⁰ = 1. Their sum is about 11.107, so the odds are about 0.665, 0.245 and 0.090.
scores = [2, 1, 0] temperature = 0.5
[0.8668133321973349, 0.11731042782619838, 0.015876239976466765]
Dividing by 0.5 doubles the scores to 4, 2, 0. The gaps are now bigger, so the top word takes more of the probability: about 0.867, 0.117, 0.016. Lower temperature, safer choice.
scores = [1000, 998] temperature = 1
[0.8807970779778823, 0.11920292202211755]
e^1000 is far too big for a floating-point number, so typing the formula in directly breaks. The answer is still well defined: the odds come out about 0.881 and 0.119, exactly the same as for scores [2, 0]. Only the gap between the scores matters.
Constraints
- 1 ≤ scores.length ≤ 10,000
- -10,000 ≤ scores[i] ≤ 10,000
- 0.01 ≤ temperature ≤ 100
- Answers within 0.000001 of the expected values are accepted.
Start with brute force
The textbook formula is direct: e^z for each score, add them, divide. It is already O(n), so speed is not the issue. Correctness is.
With a score of 1000, e^1000 overflows: JavaScript gives Infinity and you end up with Infinity / Infinity = NaN; Python raises an OverflowError. A low temperature makes this worse, because dividing by 0.01 multiplies every score by 100. The fix costs one extra pass to find the maximum.
Hints
0 of 4 shown- Hint 1
Work out the odds for scores
[2, 0]and for[1002, 1000]on paper. Do they differ? What does that tell you about adding the same number to every score? - Hint 2
Adding or subtracting the same constant to every
zmultiplies the top and bottom of every fraction by the same amount, so the probabilities do not change. You are free to shift the scores. - Hint 3
Pick the shift that makes overflow impossible: subtract the largest
z. Then every exponent is 0 or negative, so everye^zis between 0 and 1, and the largest is exactly 1, so the sum can never be 0. - Hint 4
Divide each score by the temperature first. Find the maximum of those. Compute
e^(z - max)for each, add them up, then divide each by the total.
Walkthrough and solution
Unlocks when you pass every test. Stuck for a while? Looking is fine too, as long as you then close it and write the code yourself.
How to solve it
Softmax turns any list of scores into probabilities: exponentiate, then divide by the total. Temperature is applied first, by dividing the scores. The one trick is to subtract the largest scaled score before exponentiating, which changes nothing mathematically but keeps the numbers in range.
Trace scores = [2, 1, 0], temperature 0.5:
- Scale: 2 / 0.5 = 4, 1 / 0.5 = 2, 0 / 0.5 = 0.
- Largest is 4. Shift: 0, -2, -4.
- Exponentiate: 1, 0.1353, 0.0183. Total 1.1536.
- Divide: 0.8668, 0.1173, 0.0159.
At temperature 1 the top word had about 0.665. Halving the temperature pushed it to about 0.867. Taking temperature toward 0 approaches always picking the top word (greedy decoding). Raising it toward infinity approaches picking uniformly at random.
function wordOdds(scores, temperature) {
const scaled = scores.map((s) => s / temperature);
const top = Math.max(...scaled);
// Shift so the largest is 0: every exp is then between 0 and 1, never Infinity.
const weights = scaled.map((s) => Math.exp(s - top));
let total = 0;
for (const x of weights) total += x;
return weights.map((x) => x / total);
}import math
def word_odds(scores, temperature):
scaled = [s / temperature for s in scores]
top = max(scaled)
# Shift so the largest is 0: every exp is then between 0 and 1, never overflows.
weights = [math.exp(s - top) for s in scaled]
total = sum(weights)
return [x / total for x in weights]Run your code to see results here. Run checks the examples. Submit checks every test, including hidden edge cases.