Phase 0 · Foundations

Build Wordle

By the end of this page you'll have a real, playable Wordle sitting in a folder on your computer — colours, six tries, random word and all — built out of nothing you haven't already learned.

⏱ ~75 minutes🧩 Needs: 0.0–0.6💻 Code: src/

You have probably played Wordle. You type a five-letter word, and the game paints each letter green, yellow or grey. Green means right letter in the right spot. Yellow means the letter is in the word, but you put it somewhere else. Grey means forget it. That's the whole game. And here's the thing nobody tells you. Those three colours are about forty lines of the kind of code you already know how to write.

Not a simplified version. The actual rules, including the one rule a surprising number of Wordle clones get wrong. We're going to find that bug on purpose, then fix it.

This chapter is different from the five before it. Those taught you a thing. This one spends the things. We build in four stages, and every stage runs before the next one starts. That habit — always have something that works — is how people who do this for a living stay sane.

What you'll be able to do

Before you start

Make a folder for this chapter and put your files in it. Every file below is also sitting in this chapter's src/ folder if you want to compare. Compile means turning your .java text file into something the computer can run. And run means actually running it. Those two commands, as always:

terminal
$ javac Wordle1.java
$ java Wordle1

Stage 1A secret word and one guess

Version one is tiny. One word, hard-coded. One guess. Right or wrong. You'll want to skip it and write the good version. Resist — this isn't about being careful, it's about evidence.

Think of it like this

Imagine building a bookshelf by cutting all twelve pieces of wood first, then trying to assemble it. If nothing fits, you have twelve suspects. Now imagine screwing two pieces together, checking they sit flat, then adding a third. When something goes wrong you know exactly which piece caused it — the one you just added. Programming is the second kind of job.

So: the smallest thing that is genuinely a program.

Wordle1.java
import java.util.Scanner;

public class Wordle1 {
    public static void main(String[] args) {
        String secret = "SPEED";

        Scanner in = new Scanner(System.in);
        System.out.print("Your guess: ");
        String guess = in.nextLine();

        if (guess.equals(secret)) {
            System.out.println("Correct! The word was " + secret + ".");
        } else {
            System.out.println("Nope. " + guess + " is not the word.");
        }
    }
}

Now every line, one at a time. All six are doing a job.

import java.util.Scanner; — tells Java where to find the Scanner, the tool that reads what a person types. Java ships with thousands of ready-made tools. This line says "I want that one".

String secret = "SPEED"; — a labelled box in memory holding the word. Hard-coded means the value is typed straight into the program rather than worked out while it runs. A temporary cheat. We'll remove it in Stage 4.

Scanner in = new Scanner(System.in); — makes a Scanner and points it at System.in, the name Java gives to the keyboard. We call ours in.

System.out.print("Your guess: "); — writes that text to the screen. Note print, not println. print leaves the cursor on the same line, so the player types right next to the prompt.

String guess = in.nextLine(); — waits. The program stops dead here until the player presses Enter. Then it hands you everything they typed, as one String.

if (guess.equals(secret)) — asks whether the two words hold the same letters. From 0.5: for Strings you use .equals(), never ==. The rest is if/else from 0.3.

Predict before you run

The secret is "SPEED", in capitals. Someone types speed in lower case and hits Enter. What does this program say?

what actually happens
$ java Wordle1
Your guess: speed
Nope. speed is not the word.

If that stung, good — it's the right kind of wrong. .equals() compares letters exactly. To a computer, s and S are two different characters with two different numbers behind them. Remember 0.2, where you found out that letters are secretly numbers? This is the bill arriving. We fix it in Stage 2 with one short method call, and we'll never think about it again.

Two real runs, straight from my terminal:

terminal — Stage 1, verified
$ javac Wordle1.java
$ java Wordle1
Your guess: CRANE
Nope. CRANE is not the word.
$ java Wordle1
Your guess: SPEED
Correct! The word was SPEED.

A terrible game, but not a broken one. Feel that difference and Stage 1 has done its job.

Stage 2Check the guess letter by letter

Right now the program knows only "yes" or "no". Real Wordle knows something far more useful: which letters you got right. So we stop treating your guess as one lump and start looking at it one letter at a time.

Think of it like this

Put the two words on top of each other, like two rows on squared paper:

two rows, five columns
secret   S  P  E  E  D
guess    C  R  E  S  T
         0  1  2  3  4

Now walk down the columns. Column 0: S against C, different. Column 1: P against R, different. Column 2: E against E — same. That's a green. Column 3, column 4: different. You just scored a Wordle guess with your finger. That's all the code does. Nothing more.

Those numbers under the columns matter. That's the index from 0.5 — "how far along from the start", counting from 0. So guess.charAt(2) means "the character at index 2", the third one along, because we started at zero. You've met all of this. Here it earns its keep.

Wordle2.java — the new part
String guess = in.nextLine().toUpperCase();

if (guess.length() != 5) {
    System.out.println("I need exactly 5 letters.");
    return;
}

int greens = 0;
System.out.println(guess);

for (int i = 0; i < 5; i++) {
    if (guess.charAt(i) == secret.charAt(i)) {
        System.out.print("G");
        greens = greens + 1;
    } else {
        System.out.print("-");
    }
}

System.out.println();
System.out.println(greens + " letter(s) in exactly the right place.");

.toUpperCase() — bolted onto the end of nextLine(). It hands back a new String with every letter capitalised. So speed arrives as SPEED, and the Stage 1 sting is gone. Note "a new String". From 0.5: a String can never be changed, so methods like this always hand you a fresh one.

if (guess.length() != 5) — length() counts the characters, and != means "is not equal to". If the player typed four letters, charAt(4) would ask for a slot that isn't there. That's a crash, so we check first.

return; — "stop this method now". Nothing after it runs. From 0.4: return hands control back to whoever called the method. For main, that's the end of the program.

int greens = 0; — a counter, starting empty. We add one for every match.

for (int i = 0; i < 5; i++) — the loop from 0.3. Our i starts at 0, keeps going while under 5, and grows by one each round. So i takes the values 0, 1, 2, 3, 4 — our five columns, in order.

guess.charAt(i) == secret.charAt(i) — the heart of it. Pull the character at slot i out of each word and compare. And yes, ==. That's not a slip. == is wrong for Strings, but exactly right for char. A char really is a number, and numbers compare fine with ==. This one catches people who over-learn the rule from 0.5.

print("G") and print("-") build a row of marks under the word, one character at a time, no line breaks. The bare System.out.println(); after the loop prints nothing but a line break, to end that row.

Predict before you run

Secret is SPEED. Someone guesses SPEND. Write down the exact row of five G and - characters this prints. Then the count on the line after it.

what actually happens
$ java Wordle2
Your guess: SPEND
SPEND
GGG-G
4 letter(s) in exactly the right place.

S, P and E line up. Slot 3 holds N where the secret holds E, so that one is a dash. Slot 4 is D in both. Four greens out of five — in a real game you've basically won.

Now stare at that dash in slot 3 and ask an uncomfortable question. N isn't in SPEED at all, fine. But guess SPEDE instead and you get GGG-- — I ran it. Two dashes, and both are a lie, because both those letters are in the word. Our program can't say "right letter, wrong place" yet. That's Stage 3.

Stage 3Green, yellow, grey — and the bug in everyone's first attempt

Slow down for this one. Everything so far was warm-up. Stage 3 holds the one part of Wordle that's genuinely hard, and I want you to get it wrong first. Being wrong here teaches you more than being right does.

So Stage 3 comes in three goes, and you write all three. In 3a you write the obvious scorer and watch it lie to you. In 3b you build half the fix. In 3c you finish it.

All three work the same way. I give you the shape. The lines that actually decide the answer are left blank, and those are yours. Two blanks in 3a, two more in 3b, four in 3c. None is longer than one line.

I am not going to show you those six lines assembled anywhere on this page. That is deliberate, and it will be mildly annoying. Reading a solution feels like learning. Writing one is learning.

3a — Write the obvious scorer, then break it

Three cases per letter, in order:

  1. Same letter, same slot → green.
  2. Otherwise, is this letter anywhere in the secret? → yellow.
  3. Otherwise → grey.

This one you write. I'll hand you the shape and the tools, and leave the two interesting lines empty.

Wordle3a.java — the shape. The two blanks are yours
public static String score(String secret, String guess) {
    String out = "";
    for (int i = 0; i < 5; i++) {
        char g = guess.charAt(i);
        if (/* same letter, same slot? */) {
            out = out + "G";
        } else if (/* that letter anywhere in the secret? */) {
            out = out + "Y";
        } else {
            out = out + "-";
        }
    }
    return out;
}

public static String score(...) — a method, from 0.4. It takes two Strings and hands back a String.

String out = ""; — an empty String we glue marks onto. out = out + "G" makes a new String that's the old one plus a G. Strings never change, so "adding" always means "build a fresh one".

char g = guess.charAt(i); — pull this slot's letter out once and give it a short name. Then you don't have to write guess.charAt(i) three times.

The for loop is the one from Stage 2. Five slots, in order, i counting 0 to 4.

Blank one is a comparison you already wrote in Stage 2. Two char values, one ==.

Blank two needs secret.indexOf(g) from 0.5. It hunts through the secret for that character. It hands back the index where it first found it, or -1 if the character isn't there at all. So your test has to count index 0 as a find, because slot 0 is a real place.

Your turn — 3a

Stuck? My version is src/Wordle3a.java. The same code sits in src/Wordle3Naive.java too, under a name that warns you. That's the one in the next transcript.

Our secret is SPEED — note the double E. Time to make a bet.

Predict before you run

Secret SPEED, guess ERASE. Nothing lines up, so there are no greens. Write the five marks this first attempt prints. Then, separately, write what you think real Wordle would show.

what actually happens
$ java Wordle3Naive
Your guess: ERASE
ERASE
Y--YY

E yellow, R grey, A grey, S yellow, E yellow. And here's the thing: that answer is correct. Real Wordle shows the same. The guess has two E's, and the secret has two E's. So both get to be yellow, and the S is in the word too.

Which is what makes this bug so mean. The broken version passes this test. It passes a lot of tests. Code that works most of the time is far more dangerous than code that never works, because you ship it.

So let's build a test designed to break it. We need a guess with more copies of a letter than the secret has. SPEED has two E's. GEESE has three.

Your turn — break your own code
terminal — my Wordle3a on GEESE, verified
$ javac Wordle3a.java
$ java Wordle3a
Your guess: GEESE
GEESE
-YGYY

Three of those five marks are about an E. The secret holds two E's. Don't fix anything yet. Just look at it.

Predict before you read on — your diagnosis, not mine

Your code asked one question about every letter. What was it — and what question should it have asked instead? Both in your own words, before you look.

the same code, pushed harder — five E's against a word with two
$ java Wordle3a
Your guess: EEEEE
EEEEE
YYGGY

Five E's in, five marks back, and not one of them grey. That's the tell.

Your code asks "is there an E in SPEED?" For every single slot the answer is yes. It is the same yes every time, because indexOf only ever reports that a letter exists.

It should ask a different question. Is there an E left? Existing and being available are not the same thing.

If you wrote something close to that, you have just diagnosed a bug that ships in real software. Hold onto your sentence. In 3b and 3c you turn it into code.

Think about what should happen. There are two E's available in the secret. One of them gets claimed as a green, because GEESE and SPEED both have an E in slot 2. That leaves exactly one spare E. But the guess is asking about three E's. Two of them can be paid for. The third one can't.

Predict before you run — this one catches professionals

Secret SPEED, guess GEESE. Write two rows of five marks: what the first attempt above prints, and what a correct Wordle should print. If they differ, say which slot differs and why.

what actually happens — both versions, same guess (Wordle3 prints a row of coloured tiles too; that's the next section)
$ java Wordle3Naive
Your guess: GEESE
GEESE
-YGYY          <-- wrong: three E marks

$ java Wordle3
Your guess: GEESE
GEESE
-YGY-          <-- right: the last E is grey

Look at the final slot. The broken version says yellow — "there's an E in the word!" True, but irrelevant. Both of the secret's E's have already been spent. One went on the green in slot 2, the other on the yellow in slot 1. There's no third E to point at. Real Wordle greys it out, and that grey is information. It tells you the word has exactly two E's, not three. The broken version throws that away and actively misleads the player.

If you predicted -YGY-, you've just beaten a large number of published Wordle clones. If you predicted -YGYY, you're in extremely good company. And you now understand the bug better than someone who guessed right by accident.

3b — Pass one: the greens, and a used array

Here's the mental shift, and it's the most valuable sentence on the page. Stop thinking of yellow as "this letter is in the word". Start thinking of it as "this letter claims one particular copy of itself in the secret". Each letter of the secret can be claimed once, and once only. After that it's gone.

Think of it like this

The secret is five numbered cinema seats. Every letter of your guess wants a seat with its own name on it. Greens go first, because they're booked — a green letter sits in the seat directly opposite it. Only then do the leftovers hunt for a matching free seat. If a third E turns up and both E-seats are taken, it goes home. Grey.

The broken version never checks whether the seat is free. It only checks whether such a seat exists. That's why it hands out too many tickets.

Two rules fall out of that, and the order isn't negotiable:

  1. Pass 1 — greens. Walk the five slots. Where the letters match, mark green and tick that secret letter off as used.
  2. Pass 2 — yellows. Walk the five slots again, skipping the greens. For each one, hunt for a matching secret letter nobody has ticked off yet. Found one? Yellow, and tick it off. Found none? Grey.

3b is rule 1 and nothing else. Greens, and the ticking off. You won't write a single yellow in this step, so the output will come out looking half-finished. That's the plan.

To hold the ticks we need five things we can flip on and off. That's an array of five boolean values, from 0.5 and 0.2. Java starts every slot of a new boolean array at false — exactly the "nothing claimed yet" state we want.

Wordle3b.java — pass one. The two blanks are yours
public static String score(String secret, String guess) {
    boolean[] used = new boolean[5];   // which secret letters are spoken for
    char[] mark = new char[5];         // 'G', 'Y' or '-' for each guess slot

    // PASS 1 — greens. Right letter, right place.
    for (int i = 0; i < 5; i++) {
        if (/* BLANK 1 — is this slot a green? */) {
            mark[i] = 'G';
            /* BLANK 2 — spend that letter of the secret */
        } else {
            mark[i] = '-';
        }
    }

    // GIVEN — a peek at used, printed so you can watch the claims appear.
    String claimed = "";
    for (int j = 0; j < 5; j++) {
        if (used[j] == true) {
            claimed = claimed + secret.charAt(j);
        } else {
            claimed = claimed + ".";
        }
    }
    System.out.println("claimed: " + claimed);

    String out = "";
    for (int i = 0; i < 5; i++) {
        out = out + mark[i];
    }
    return out;
}

boolean[] used = new boolean[5]; — five true/false flags, one per slot of the secret. All false to start.

char[] mark = new char[5]; — five characters, one per slot of the guess. This is where the answer gets built. Two arrays, two different words: used is about the secret, mark is about the guess. Keep those two apart in your head and you've won most of the battle.

The pass 1 loop walks the five slots, same as Stage 2. Green? Write 'G' into mark[i]. Otherwise write '-'.

Blank one is a test you have already written twice — once in Stage 2, once as 3a's first blank. Two characters pulled out at the same slot, compared with ==. Nothing new. Write it from memory this time.

Blank two is the line that is the whole chapter, and it is one short statement. It puts true into used. The only real decision is which slot.

Think it through rather than guessing. A green means the two words agree at the same slot. So the secret letter you just spent sits at that same slot number. Your loop counter is already holding it. Nothing else may ever point at that letter again.

If you get an error

A blank left as a comment is not valid Java. if (/* ... */) will not compile — Java needs something true-or-false in those brackets. So the compiler shouting at you means you have not filled a blank in yet, not that you broke something.

The next loop is given, because it teaches nothing. It's only there so you can see the claims happen. It uses j as its counter, because it walks the secret, not the guess. Claimed slot? Add that letter of the secret to claimed. Unclaimed? Add a dot instead. Then System.out.println prints the row.

The last little loop glues the five marks into one String and return hands it back. Also given.

Your turn — 3b

Only after it runs: src/Wordle3b.java is my version, blanks filled. Checking your answer against it is fine. Reading it first is stealing from yourself.

Predict before you run — half a scorer

Secret SPEED, guess GEESE, and pass 2 doesn't exist yet. Write the five marks. Then write the claimed row — five characters, letters or dots.

terminal — Wordle3b on GEESE, verified
$ javac Wordle3b.java
$ java Wordle3b
Your guess: GEESE
GEESE
claimed: ..E..
--G--

One green, four dashes, and one letter of the secret spent. Slot 2 is the only place the two words agree.

Two things worth noticing. The S and the other two Es got dashes. That's wrong, because they really are in the word.

And the claimed row shows a single E. One of the secret's two E's is gone. The other is still sitting there, unspent.

Now the honest bit about 3b. Nothing in it ever reads the used array. You fill it in and then ignore it, which feels like a waste of typing.

It isn't. That row of dots and letters is the answer to the question you diagnosed a page ago. "Is there an E left?" is a question you can only ask something that keeps track. 3c is where you finally ask it.

3c — Pass two: the yellows, against unclaimed letters only

Rule 2, from the list above. Walk the five slots again. Skip the greens. For every other slot, hunt the secret for a matching letter nobody has claimed. Found one? Yellow, and claim it. Found none? Leave the dash.

Greens first, always. Do yellows first and a stray E can claim the very seat a green E was owed. That turns a perfect letter into a merely-present one. Order here is the algorithm.

This block goes straight after your pass 1 loop, before the claimed peek.

Four blanks this time, and they are the four hardest lines in the chapter. Here is the skeleton around them.

Wordle3c.java — the block you add. Four blanks, all yours
    // PASS 2 — yellows. Only against secret letters nobody claimed yet.
    for (int i = 0; i < 5; i++) {
        if (mark[i] == 'G') {
            continue;
        }
        for (int j = 0; j < 5; j++) {
            if (/* BLANK 3 — a matching secret letter that nobody has claimed? */) {
                /* BLANK 4 — this guess slot is a yellow */
                /* BLANK 5 — that secret letter is now spent */
                /* BLANK 6 — stop hunting */
            }
        }
    }

for (int i = 0; i < 5; i++) — the same five slots of the guess, walked a second time. Given.

if (mark[i] == 'G') { continue; } — continue, from 0.3, means "skip the rest of this round, go to the next i". Greens are settled, so leave them alone. Also given.

Then the inner loop, worth reading twice. for (int j = 0; j < 5; j++) uses a second counter, j. We're walking the secret while i stays parked on one slot of the guess. Two loops, one inside the other, so two names. Never reuse i.

Now your four. Take them one at a time, and say each one out loud in English before you write any Java.

Blank three is two separate tests, and both must hold. Join them with &&, which means "and", from 0.3.

Test one: is secret slot j still free? used[j] is a boolean, so compare it against false. Test two: is the letter sitting at guess slot i the same as the letter at secret slot j? That's two charAt calls and one ==. Mind which word gets which counter.

This is your "is there an E left?" sentence, finally written in Java. It is the entire difference between 3a and a correct scorer.

Blank four is one statement. Put a 'Y' into mark, at the guess slot you are standing on. Single quotes, because mark holds char values, not Strings.

Blank five is the twin of blank two, with one difference that matters enormously. The index is j now, not i. You're spending a secret letter that sits somewhere else in the word. That's what makes it yellow, not green. Write i here and you'll claim the wrong seat.

Blank six is one word and a semicolon, straight from 0.3. You've found your letter and claimed it, so leave the inner loop right now. Do not carry on walking the secret.

That last one looks like a tidiness thing. It isn't. Leave it out and the scorer stays wrong in a way you won't notice. You're about to prove that, one section down.

Your turn — 3c

Got the wrong answer? Good — that's a debugging job, and it's yours. Print i, j and the claimed row inside the loop and watch where reality parts company with your plan. src/Wordle3c.java is my filled-in version, for after you've run yours.

terminal — Wordle3c on GEESE, verified
$ javac Wordle3c.java
$ java Wordle3c
Your guess: GEESE
GEESE
claimed: S.EE.
-YGY-

Read the claimed row first, because it explains the marks. Both E's of the secret are spent, and so is the S. Only the P and the D are still free.

So when slot 4's E goes hunting, there is no E left to claim. It keeps its dash. You didn't have to write a rule about doubled letters. The used array made the grey happen on its own.

That's the scorer, finished, and you wrote all of it. src/Wordle3.java holds the same score method with the claimed line taken out, plus the coloured tiles from the next section.

Now watch your algorithm run, one step at a time. The used row is the star. Square brackets mean "claimed".

score("SPEED", "GEESE")

Setup. Secret SPEED, guess GEESE. Nothing is claimed and nothing is marked.

state
secret    S  P  E  E  D
guess     G  E  E  S  E
used      -  -  -  -  -
mark      ?  ?  ?  ?  ?

The secret holds two E's, in slots 2 and 3. The guess asks about three. Somebody's going to be disappointed.

Pass 1, slot 0. Guess has G, secret has S. Not a match, so mark[0] = '-' and nothing gets claimed.

state
used      S  P  E  E  D
mark      -  ?  ?  ?  ?

Pass 1, slot 1. Guess E, secret P. No match. A dash — for now. Pass 2 may yet upgrade it.

state
used      S  P  E  E  D
mark      -  -  ?  ?  ?

Pass 1, slot 2. A green. Guess E, secret E. Match! mark[2] = 'G', and used[2] = true. That E in the secret is now spent, and nothing else may point at it.

state
used      S  P [E] E  D
mark      -  -  G  ?  ?

One E left in the pot. Keep an eye on it.

Pass 1, slots 3 and 4. S against E: no. E against D: no. Both dashes. Pass 1 is done.

state after pass 1
used      S  P [E] E  D
mark      -  -  G  -  -

Pass 2, slot 0. Guess G. Walk the whole secret looking for an unclaimed G: S, P, E, E, D. There's no G anywhere. The inner loop finishes without finding anything, so the dash stays. Grey.

state
used      S  P [E] E  D
mark      -  -  G  -  -

Pass 2, slot 1. Guess E. Walk the secret: j=0 is S, no. j=1 is P, no. j=2 is E — but used[2] is already true, so we walk past it. j=3 is E and unclaimed. Yellow. Claim it and break.

state
used      S  P [E][E] D
mark      -  Y  G  -  -

Both E's in the secret are now spoken for. The pot is empty.

Pass 2, slot 2. mark[2] is 'G', so continue fires and we skip straight on. Greens aren't up for renegotiation.

state
used      S  P [E][E] D
mark      -  Y  G  -  -

Pass 2, slot 3. Guess S. j=0 is S and unclaimed. Yellow. Claim slot 0 and break.

state
used     [S] P [E][E] D
mark      -  Y  G  Y  -

Pass 2, slot 4. The moment of truth. Guess E again. Walk the secret one more time. S is claimed, P is not an E, and slots 2 and 3 are both claimed. D is not an E either, so no unclaimed E exists. The loop ends empty-handed and the dash survives. Grey.

final state
used     [S] P [E][E] D
mark      -  Y  G  Y  -

Final answer: -YGY-. This is a real trace, printed by a program that walked the arrays and reported them. It isn't something I wrote out by hand and hoped was right.

You just built the hard part

Two passes, one boolean array, one break. That's the whole trick, and it's the piece most first attempts at a Wordle clone get wrong. Everything from here on is scaffolding around it.

Now delete your break, and find out what it was for

Blank six was one word. If you thought "I would forget that", you're right to — people do forget it. So let's make forgetting it memorable rather than expensive.

Take it out. Run the scorer on four words. Put it back. Two minutes, and you'll never wonder about it again.

Your turn — break the break

src/Wordle3d.java is my copy with the break deliberately missing. Use it if you want something real to check your four rows against.

Predict before you run — three of these four hide the bug

The break is gone. Of those four guesses, three print exactly what they printed before. One changes. Which word, and what does it change to?

terminal — same guess, one word of difference in the code, verified
$ java Wordle3c
Your guess: ERASE
ERASE
claimed: S.EE.
Y--YY          <-- with the break: last E yellow

$ java Wordle3d
Your guess: ERASE
ERASE
claimed: S.EE.
Y--Y-          <-- break deleted: last E grey

All four words, run both ways on my machine:

guesswith breakno break
GEESE-YGY--YGY-identical — bug hidden
ERASEY--YYY--Y-different — bug exposed
EEEEE--GG---GG-identical — bug hidden
SPEDEGGGYYGGGYYidentical — bug hidden

One word in four. Here's what happens inside it.

Without the break, one guess letter greedily claims every remaining copy of itself. It starves the letters after it.

Walk ERASE slowly. Slot 0 holds E. It finds the secret's E at slot 2, claims it, and keeps going. It finds the E at slot 3 and claims that too. Both E's are now spent by one letter. So when slot 4's E comes hunting, the pot is empty. Grey.

And look at the claimed rows in that terminal block. They're identical — S.EE. both times. Your debugging peek doesn't help here. Both E's got spent either way. The difference is who spent them.

The word that keeps doing this

Notice which word exposed it. ERASE — the same word this chapter used back in 3a. That's where the naive scorer gave the right answer and hid its bug. Two different bugs now, one word, and the same reason both times.

Doubled letters only bite when the copies run short. Most word pairs never run short, so most word pairs tell you nothing. A bug that shows on one word in four is exactly why "it works on my test words" proves nothing. Four words isn't a test. It's four coincidences.

Try to break it yourself

You have the two scorers now: the naive one and the correct one. So put them side by side and go hunting. Type any secret and any guess below and both run at once.

Start with the four buttons. SPEED / GEESE is the pair that exposes the bug. SPEED / ERASE is the one where both scorers agree — and that agreement is the reason this bug ships in real code. Most word pairs never reveal it. Then invent your own: try a guess with three of the same letter, or a secret with a doubled letter you don't guess at all.

The naive way

GEESE
-YGYY

The correct way

GEESE
-YGY-

They disagree. The naive version is wrong.

Which of the secret's letters got used up
SPEED
A letter can only be spoken for once. That is the whole fix.

The bottom row is the part worth watching. It shows which of the secret's letters have been used up. Every green claims one. Every yellow claims one. Once a letter is claimed it cannot be claimed again, and that single sentence is the entire difference between the two scorers.

Making it actually colourful

Your terminal treats a few odd strings as instructions instead of text. They all start with one invisible character called escape, which is character number 27. Print escape, then [42m, and the background turns green.

Wordle3.java — colour
public static String colour(char letter, char mark) {
    String esc = "" + (char) 27;          // character number 27 is ESC
    String bg = esc + "[47;30m";          // grey background, black text
    if (mark == 'G') {
        bg = esc + "[42;30m";             // green background
    }
    if (mark == 'Y') {
        bg = esc + "[43;30m";             // yellow background
    }
    return bg + " " + letter + " " + esc + "[0m";
}

Line by line. esc is character 27 glued onto an empty String, so we can add it to other Strings. bg holds the code for the background we want. 47;30 is grey behind black text, 42 is green, 43 is yellow. The two ifs swap the default grey for one of those. Then esc + "[0m" resets the terminal, and the spaces around letter make the tile chunky.

Watch out

Leave off the [0m reset and your terminal stays green all day. And some terminals ignore these codes and print the raw gibberish. That's why our game always prints the plain G Y - row too.

Going deeper Where [42;30m comes from, and why 27 works

These are ANSI escape codes, named after the standards body that wrote them down in the 1970s. They were commands for physical terminals — machines with a screen and a keyboard and no computer inside.

The numbers come in blocks. 30–37 set the text colour, 40–47 set the background, in the order black, red, green, yellow, blue, magenta, cyan, white. So 42 is a green background and 32 would be green text. The semicolon joins two settings into one instruction, and m ends it.

And (char) 27 works because of 0.2. A char really is a number, so the cast just tells Java to treat 27 as a character. Character 27 has no shape to draw, which is exactly why it can mean "an instruction starts here".

Stage 4The whole game

We've got a scorer. We haven't got a game. A game needs six tries, a real winner and loser, and a word we didn't choose ourselves. It also needs patience with players who type nonsense. All four are small.

A random word

Wordle4.java — pickWord
public static String pickWord() {
    String[] words = {
        "SPEED", "ALLOY", "CRANE", "GHOST", "PLUMB",
        "JOKER", "WHISK", "BRAVE", "TOTAL", "MIRTH"
    };
    int i = (int) (Math.random() * words.length);
    return words[i];
}

String[] words = { ... }; — an array of ten Strings, written out directly in curly brackets. From 0.5.

Math.random() — hands back a double somewhere from 0.0 up to (but never reaching) 1.0. Something like 0.7231.

* words.length — words.length is 10, so multiplying stretches that range to 0.0 up to just under 10.0. Our 0.7231 becomes 7.231.

(int) — the cast from 0.2. It chops the decimal part clean off, so 7.231 becomes 7. Whole numbers from 0 to 9. Exactly the slots our array has. Note it chops rather than rounds, which is precisely why we never get 10 and never crash.

Rejecting nonsense

Wordle4.java — isValid
public static boolean isValid(String guess) {
    if (guess.length() != 5) {
        System.out.println("  Five letters, please. That one had " + guess.length() + ".");
        return false;
    }
    for (int i = 0; i < 5; i++) {
        char c = guess.charAt(i);
        if (c < 'A' || c > 'Z') {
            System.out.println("  Letters only. '" + c + "' is not a letter.");
            return false;
        }
    }
    return true;
}

A method that hands back true or false. From 0.4: a method's job is to answer one question, and this one answers "can I use this guess?".

The length check first, with a helpful message. return false leaves the method immediately — no point checking the letters of a word that's the wrong size.

if (c < 'A' || c > 'Z') — the sneakiest line here, and my favourite. We're comparing characters with < and >, which only makes sense because characters are numbers. In Java's numbering, A through Z sit in one unbroken run. So "smaller than A or bigger than Z" does the job. It catches every digit, space, comma and emoji in one go. || means "or", from 0.3.

We uppercase the guess before calling this, so lower-case letters are already handled by the time we get here.

The loop that is the game

Wordle4.java — main
public static void main(String[] args) {
    String secret = pickWord();
    if (args.length > 0) {
        secret = args[0].toUpperCase();
    }

    Scanner in = new Scanner(System.in);
    System.out.println("WORDLE — five letters, six tries.");
    System.out.println();

    int tries = 0;
    boolean won = false;

    while (tries < 6 && won == false) {
        System.out.print("Guess " + (tries + 1) + " of 6: ");
        if (in.hasNextLine() == false) {
            break;
        }
        String guess = in.nextLine().trim().toUpperCase();

        if (isValid(guess) == false) {
            continue;
        }

        tries = tries + 1;
        String marks = score(secret, guess);
        System.out.println("  " + paint(guess, marks) + "  " + marks);

        if (marks.equals("GGGGG")) {
            won = true;
        }
    }

    System.out.println();
    if (won) {
        System.out.println("Got it in " + tries + " out of 6. Nice.");
    } else {
        System.out.println("Out of tries. The word was " + secret + ".");
    }
}

String secret = pickWord(); — a random word from the list.

if (args.length > 0) { secret = args[0].toUpperCase(); } — args is that mysterious thing that has sat in main's brackets since 0.1. It's an array of any extra words you type after the program's name. So java Wordle4 SPEED puts "SPEED" into args[0]. Practice mode — and how I tested everything on this page.

int tries = 0; and boolean won = false; — the game's entire memory. How many guesses counted, and whether it's over.

while (tries < 6 && won == false) — from 0.3. Keep going while there are tries left and nobody has won. Either condition failing ends the game.

"Guess " + (tries + 1) + " of 6: " — those brackets round tries + 1 aren't decoration. Without them Java glues the text and the number together and prints "Guess 01 of 6". I checked. Brackets force the sum first.

if (in.hasNextLine() == false) { break; } — asks the Scanner "is another line coming?". If input has run dry, break leaves the loop instead of crashing. You won't notice this playing by hand. It matters when you feed the game a file instead.

.trim().toUpperCase() — trim() shaves stray spaces off either end, then toUpperCase() capitalises. Both hand back new Strings, chained left to right.

if (isValid(guess) == false) { continue; } — the line that makes the game feel fair. continue jumps back to the top without touching tries. A typo costs the player nothing.

tries = tries + 1; comes after that check, on purpose. Then score gives the marks and paint turns them into tiles. We print tiles and plain marks side by side, so the game works with or without colour.

if (marks.equals("GGGGG")) — five greens is the only way to win. Note .equals, because these are Strings.

After the loop one if decides the ending. Won: say how many tries it took. Lost: reveal the word, because a puzzle you never learn the answer to is merely annoying.

Predict before you run

A player types cat, then SP3ED, then GEESE. The secret is SPEED. After all three, how many of the six tries have they used — and what will the prompt say next?

what actually happens (escape shown as ^[ — invisible on screen)
$ java Wordle4 SPEED
WORDLE — five letters, six tries.

Guess 1 of 6:   Five letters, please. That one had 3.
Guess 1 of 6:   Letters only. '3' is not a letter.
Guess 1 of 6:   ^[[47;30m G ^[[0m^[[43;30m E ^[[0m^[[42;30m E ^[[0m^[[43;30m S ^[[0m^[[47;30m E ^[[0m  -YGY-
Guess 2 of 6: 

One try. The first two guesses never counted, so the prompt printed "Guess 1 of 6" three times before anything stuck. That's continue doing its job — a rejected guess isn't a guess.

And notice that -YGY- on the end: our duplicate-letter fix, running inside the finished game, on a real word. The messy-looking ^[[47;30m bits are the colour instructions. On your screen they vanish and you get tiles instead.

Here's a real win, played against the practice word, and a real loss with the answer revealed. Both straight from the terminal.

terminal — a win in three (colour codes stripped for reading)
$ java Wordle4 SPEED
WORDLE — five letters, six tries.

Guess 1 of 6:    C  R  A  N  E   ----Y
Guess 2 of 6:    S  P  E  N  D   GGG-G
Guess 3 of 6:    S  P  E  E  D   GGGGG

Got it in 3 out of 6. Nice.
terminal — running out of tries (same six-guess run, first three lines cut so this fits)
$ java Wordle4 SPEED
Guess 4 of 6:    P  L  U  M  B   Y----
Guess 5 of 6:    W  H  I  S  K   ---Y-
Guess 6 of 6:    T  O  T  A  L   -----

Out of tries. The word was SPEED.

That's a game. Go and get someone to play it.

Section 5Make it yours

Ideas, not instructions. There are no step-by-step answers here, deliberately — everything below is reachable with what you already know. Pick the one that actually appeals to you. Something you chose yourself is worth five exercises somebody set you.

Your turn

Work through the four stages in order. Do not skip to Stage 4 — every tick below is a program that runs.

Section 6Check yourself

Why must the greens pass run before the yellows pass?

You ran this one yourself, so it's a memory check. Delete the break; from the inner loop of score. Secret SPEED, guess ERASE — which used to give Y--YY. What comes out now?

A friend's Wordle clone scores secret ALLOY, guess LLAMA as Y G Y - Y. Correct Wordle gives Y G Y - -. Without seeing their code, what's your best bet about the bug?

Section 7What broke, and what surprised you

Six programs is plenty of room to go wrong. Before you close this page, write down — actually write — the answers to these:

If a run didn't match what this page claims, trust your terminal and go find out why. Every output here came from a real run on JDK 17. But your machine is the one that counts.

Section 8Everything you just used — and why Phase 1 exists

You learned almost nothing new today. That was the point. Look at what the four stages spent:

FromWhat it wasWhere it turned up
0.2Variables, types, casting, chars are numbersint tries, boolean won, (int) on a random double, (char) 27 for escape, c < 'A'
0.3if/else, while, for, &&, ||, break, continueEvery stage. The game loop, the two passes, the nested search, skipping bad input
0.4Methods, parameters, return values, the call stackscore, colour, paint, isValid, pickWord — each answering one question
0.5Arrays, Strings, charAt, length, equals, immutabilityused, mark, the word list, args, and every letter comparison on the page

Five chapters of small ideas add up to a game. The pieces are boring and the combinations aren't. That's what programming is like.

Now the honest part

Open your Wordle4.java and count the names main declares. Don't take my word for it — count them.

Wordle4.java — everything main is holding at once
String secret     // the word
Scanner in        // the keyboard
int tries         // guesses used so far
boolean won       // is it over
String guess      // this go
String marks      // this go's result

Six. Six loose names in one method, and nothing joins them up. As far as Java knows, tries has nothing to do with secret.

Now add one idea from Section 5. Score tracking needs gamesPlayed, gamesWon and triesTotal. That's nine. Hard mode has to remember every green so far, so add lockedGreens. Ten names in one method.

The methods get it worse, because anything they need has to be handed to them in brackets:

how isValid's brackets grow, one feature at a time
isValid(guess)                              // today: 1 parameter
isValid(guess, lockedGreens)                // + hard mode: 2
isValid(guess, lockedGreens, wordList)      // + real words only: 3

Two features. Four extra names in main, two extra parameters on one method. Now imagine ten features, and a second player who needs their own copy of all of it.

Think of it like this

You're cooking, and every ingredient is loose on the counter. Fine for beans on toast. Now cook a three-course meal on that same counter. What you want is bowls — this bowl holds the sauce's things, that one holds the salad's. A bowl keeps its own contents together and hands them out only when asked.

Java has bowls. They're called objects. One of them could hold a game's secret word, its tries used and its rows so far. The scoring would live inside the bowl, next to the data it works on. Then score wouldn't need the secret passed in. It would already know.

That's a claim, so here it is concretely. Your six loose names, gathered under one label:

a sketch of Phase 1 — don't type this yet
class Game {
    String secret;        // the same six names as your main
    int tries;            // but they belong to one Game now
    boolean won;

    String score(String guess) { ... }   // no secret in the brackets
}

class is the word Java uses for a kind of thing. You've typed it in every file since 0.1, and chapter 1.1 explains it properly. The ... is me leaving the body out. It would be your 3c code, unchanged.

Look at what did and didn't move. The names are the same names. score lost a parameter, because the secret is now sitting right there beside it. That's the whole trick, and it's smaller than people make it sound.

Two players becomes two bowls. Score tracking becomes one more thing in the bowl. And static — a word you've typed for six chapters without ever being told what it means — finally gets explained. It turns out to mean "this method does not belong to a bowl". Which is why you've needed it so far, and why you'll mostly stop needing it next.

Six names today. Ten after two features. That number going up is the ache, and you counted it yourself. It's the best possible preparation for Phase 1. See you there.

Corrections

Nice. Chapter marked complete — and you've got a game to show for it.