An open API service indexing awesome lists of open source software.

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

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