https://github.com/jexp/wordle-graph
Wordle Solver as Neo4j graph database
https://github.com/jexp/wordle-graph
Last synced: 9 months ago
JSON representation
Wordle Solver as Neo4j graph database
- Host: GitHub
- URL: https://github.com/jexp/wordle-graph
- Owner: jexp
- Created: 2022-02-02T17:58:25.000Z (over 4 years ago)
- Default Branch: main
- Last Pushed: 2022-02-13T11:38:02.000Z (over 4 years ago)
- Last Synced: 2025-08-23T01:37:55.082Z (11 months ago)
- Language: Shell
- Size: 7.55 MB
- Stars: 8
- Watchers: 2
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: readme.adoc
Awesome Lists containing this project
README
= The Wordle Graph
:imagesdir: img
I know I'm late to the game as Wordle was sold to the @nytimes
But I thought late night that it would be fun to represent the wordle world as a graph.
https://twitter.com/mesirii/status/1488657605799321600
== Dataset
I found a scraped wordle list in this https://github.com/coolbutuseless/wordle/blob/main/R/words.R[repository of an R solver^], which I turned into a CSV.
We can load the dataset with LOAD CSV into `Word` nodes.
[source,cypher]
----
LOAD CSV FROM "url" AS row
CREATE (:Word {word:row[0]})
----
That's a lot (12k) nodes.
image::wordle-bloom.png[]
== Tale of 2 Models
=== Model 1 - Letter Positions as Nodes
Initially I represented characters at positions with dedicated nodes with a `char` and an `index` property connected to the word and to each other.
[source,cypher]
----
MATCH (w:Word)
CALL { WITH w
WITH w, split(w.name,"") AS chars
MERGE (start:CharAtPos {idx:0, char:chars[0]})
MERGE (w)-[:STARTS]->(start)
MERGE (w)-[:HAS]->(start)
WITH *
UNWIND range(1,size(chars)-1) AS idx
MERGE (next:CharAtPos {idx:idx, char:chars[idx]})
MERGE (w)-[:HAS]->(next)
WITH *
MATCH (prev:CharAtPos {idx:idx-1, char:chars[idx-1]})
MERGE (prev)-[:NEXT]->(next)
} IN TRANSACTIONS OF 1000 ROWS;
----
image::wordle-crash.png[]
To solve a word, you pass in the letters you know with their positions and the letters that you don't have the right position for and match any words that fit this pattern.
[source,cypher]
----
MATCH (c1:CharAtPos {idx:0, char:'c'}),
(c5:CharAtPos {idx:4, char:'h'}),
(c:CharAtPos {char:'a'})
match (w:Word)-[:HAS]->(c1),
(w)-[:HAS]->(c5),
(w)-[:HAS]->(c)
return w.name;
----
----
╒════════╕
│"w.name"│
╞════════╡
│"clach" │
├────────┤
│"clash" │
├────────┤
│"caneh" │
├────────┤
│"coach" │
├────────┤
│"catch" │
├────────┤
│"crash" │
└────────┘
----
image::wordle-solver.png[]
If we have more information, then we can extend the query by excluding letters or positions and get a smaller result set.
[source,cypher]
----
match (c1:CharAtPos {idx:0, char:'c'}), // correct
(c2:CharAtPos {idx:1, char:'a'}), // wrong pos
(c3:CharAtPos {char:'l'}), // incorrect
(c4:CharAtPos {char:'i'}), // incorrect
(c5:CharAtPos {idx:4, char:'h'}), // correct
(c:CharAtPos {char:'a'})
match (w:Word)-[h1:HAS]->(c1),
(w)-[h2:HAS]->(c5), (w)-[h3:HAS]->(c)
WHERE not exists { (w)-[:HAS]->(c2) } and not exists { (w)-[:HAS]->(c3) } and not exists { (w)-[:HAS]->(c4) }
return *
----
=== Model 2 Positions in Relationships
An alternative model represents just the 26 characters and puts the position onto the relationship either as a property or as the rel-type.
[source,cypher]
----
MATCH (w:Word)
WITH w, split(w.name,"") AS chars
MERGE (c0:Char {char:chars[0]})
MERGE (w)-[:POS0]->(c0)
MERGE (c1:Char {char:chars[1]})
MERGE (w)-[:POS1]->(c1)
MERGE (c2:Char {char:chars[2]})
MERGE (w)-[:POS2]->(c2)
MERGE (c3:Char {char:chars[3]})
MERGE (w)-[:POS3]->(c3)
MERGE (c4:Char {char:chars[4]})
MERGE (w)-[:POS4]->(c4);
----
For resolving our wordle puzzle (v1) we could use this Cypher using this time the relationships as structuring means.
[source,cypher]
----
MATCH (c:Char {char:'c'}),
(h:Char {char:'h'}),
(a:Char {char:'a'})
MATCH (wordle:Word)-[p0:POS0]->(c),
(wordle)-[p4:POS4]->(h),
(wordle)-[px]->(a)
WHERE not exists { (wordle)-[:POS1]->(a) }
AND not exists { (wordle)-[:POS2]->(:Char {char:'l'}) }
AND not exists { (wordle)-[:POS3]->(:Char {char:'i'}) }
RETURN *;
----
image::wordle-rel-model.png[]
If we have more information, then we can extend the query by excluding letters or positions and get a smaller result set.
[source,cypher]
----
MATCH (c:Char {char:'c'}),
(h:Char {char:'h'}),
(a:Char {char:'a'})
MATCH (wordle:Word)-[p0:POS0]->(c),
(wordle)-[p4:POS4]->(h),
(wordle)-[px]->(a)
WHERE not exists { (wordle)-[:POS1]->(a) }
AND not exists { (wordle)-[:POS2]->(:Char {char:'l'}) }
AND not exists { (wordle)-[:POS3]->(:Char {char:'i'}) }
RETURN *;
----
image::wordle-rel-model-exclusions.png[]
=== Playing wordle in your Terminal
If you just want to play, run `./wordle-neo4j.sh` in your terminal, it sends a Cypher query to a wordle database in demo.neo4j.labs.com (username, password, database = wordle) to see if your guesses were right.
[source,shell]
----
./wordle-neo4j.sh
Guess 1: diver
🟨⬜⬜🟩🟩
Guess 2: later
🟨⬜⬜🟩🟩
Guess 3: elder
🟩🟩🟩🟩🟩
Guessed "elder" aka 🟩🟩🟩🟩🟩 in 3 rounds.
----
The statement that's running is:
[source,cypher]
----
match (w:Word)
with w skip $word limit 1
with split($guess,'') as guessed, split(w.name,'') as letters, w.name as name
return reduce(res='', idx in range(0,size(letters)-1) | res +
case when guessed[idx] = letters[idx] then '🟩'
when name contains guessed[idx] then '🟨'
else '⬜' end) as res
----
=== Ideas
* explain two models
* loading
* post-processing
* look at char frequencies
* recommend starting words (based on top frequencies)
* rarest words
* solve word
* visualize solver
* input word so far, e.g. `C a l! i! H` and have a generic cypher-statement to run with this word
* implement wordle -> split input + match