{"id":15691537,"url":"https://github.com/jexp/wordle-graph","last_synced_at":"2025-10-12T09:14:14.280Z","repository":{"id":66744019,"uuid":"454880944","full_name":"jexp/wordle-graph","owner":"jexp","description":"Wordle Solver as Neo4j graph database","archived":false,"fork":false,"pushed_at":"2022-02-13T11:38:02.000Z","size":7916,"stargazers_count":8,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-08-23T01:37:55.082Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Shell","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jexp.png","metadata":{"files":{"readme":"readme.adoc","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-02-02T17:58:25.000Z","updated_at":"2022-04-06T21:28:27.000Z","dependencies_parsed_at":"2023-06-29T13:13:32.958Z","dependency_job_id":null,"html_url":"https://github.com/jexp/wordle-graph","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/jexp/wordle-graph","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jexp%2Fwordle-graph","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jexp%2Fwordle-graph/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jexp%2Fwordle-graph/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jexp%2Fwordle-graph/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jexp","download_url":"https://codeload.github.com/jexp/wordle-graph/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jexp%2Fwordle-graph/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279010963,"owners_count":26084837,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-12T02:00:06.719Z","response_time":53,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-03T18:22:11.398Z","updated_at":"2025-10-12T09:14:14.241Z","avatar_url":"https://github.com/jexp.png","language":"Shell","funding_links":[],"categories":[],"sub_categories":[],"readme":"= The Wordle Graph\n:imagesdir: img\n\nI know I'm late to the game as Wordle was sold to the @nytimes \nBut I thought late night that it would be fun to represent the wordle world as a graph.\n\nhttps://twitter.com/mesirii/status/1488657605799321600\n\n== Dataset\n\nI 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.\n\nWe can load the dataset with LOAD CSV into `Word` nodes.\n\n[source,cypher]\n----\nLOAD CSV FROM \"url\" AS row\nCREATE (:Word {word:row[0]})\n----\n\nThat's a lot (12k) nodes.\n\nimage::wordle-bloom.png[]\n\n== Tale of 2 Models\n\n=== Model 1 - Letter Positions as Nodes\n\nInitially I represented characters at positions with dedicated nodes with a `char` and an `index` property connected to the word and to each other.\n\n[source,cypher]\n----\nMATCH (w:Word) \nCALL { WITH w \nWITH w, split(w.name,\"\") AS chars\nMERGE (start:CharAtPos {idx:0, char:chars[0]})\nMERGE (w)-[:STARTS]-\u003e(start)\nMERGE (w)-[:HAS]-\u003e(start)\nWITH *\nUNWIND range(1,size(chars)-1) AS idx\nMERGE (next:CharAtPos {idx:idx, char:chars[idx]})\nMERGE (w)-[:HAS]-\u003e(next)\nWITH *\nMATCH (prev:CharAtPos {idx:idx-1, char:chars[idx-1]})\nMERGE (prev)-[:NEXT]-\u003e(next)\n} IN TRANSACTIONS OF 1000 ROWS;\n----\n\nimage::wordle-crash.png[]\n\nTo 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.\n\n[source,cypher]\n----\nMATCH (c1:CharAtPos {idx:0, char:'c'}), \n      (c5:CharAtPos {idx:4, char:'h'}),\n      (c:CharAtPos {char:'a'})\nmatch (w:Word)-[:HAS]-\u003e(c1),\n      (w)-[:HAS]-\u003e(c5),\n      (w)-[:HAS]-\u003e(c)\nreturn w.name;\n----\n\n----\n╒════════╕\n│\"w.name\"│\n╞════════╡\n│\"clach\" │\n├────────┤\n│\"clash\" │\n├────────┤\n│\"caneh\" │\n├────────┤\n│\"coach\" │\n├────────┤\n│\"catch\" │\n├────────┤\n│\"crash\" │\n└────────┘\n----\n\nimage::wordle-solver.png[]\n\nIf we have more information, then we can extend the query by excluding letters or positions and get a smaller result set.\n\n[source,cypher]\n----\nmatch (c1:CharAtPos {idx:0, char:'c'}), // correct \n      (c2:CharAtPos {idx:1, char:'a'}), // wrong pos     \n      (c3:CharAtPos {char:'l'}),  // incorrect    \n      (c4:CharAtPos {char:'i'}),  // incorrect\n      (c5:CharAtPos {idx:4, char:'h'}), // correct\n      (c:CharAtPos {char:'a'})\nmatch (w:Word)-[h1:HAS]-\u003e(c1),\n      (w)-[h2:HAS]-\u003e(c5), (w)-[h3:HAS]-\u003e(c)\nWHERE not exists { (w)-[:HAS]-\u003e(c2) } and not exists { (w)-[:HAS]-\u003e(c3) } and not exists { (w)-[:HAS]-\u003e(c4) }\nreturn *\n----\n\n=== Model 2 Positions in Relationships\n\nAn alternative model represents just the 26 characters and puts the position onto the relationship either as a property or as the rel-type.\n\n[source,cypher]\n----\nMATCH (w:Word) \nWITH w, split(w.name,\"\") AS chars\nMERGE (c0:Char {char:chars[0]})\nMERGE (w)-[:POS0]-\u003e(c0)\nMERGE (c1:Char {char:chars[1]})\nMERGE (w)-[:POS1]-\u003e(c1)\nMERGE (c2:Char {char:chars[2]})\nMERGE (w)-[:POS2]-\u003e(c2)\nMERGE (c3:Char {char:chars[3]})\nMERGE (w)-[:POS3]-\u003e(c3)\nMERGE (c4:Char {char:chars[4]})\nMERGE (w)-[:POS4]-\u003e(c4);\n----\n\nFor resolving our wordle puzzle (v1) we could use this Cypher using this time the relationships as structuring means.\n\n[source,cypher]\n----\nMATCH (c:Char {char:'c'}), \n      (h:Char {char:'h'}),\n      (a:Char {char:'a'})\nMATCH (wordle:Word)-[p0:POS0]-\u003e(c),\n      (wordle)-[p4:POS4]-\u003e(h),\n      (wordle)-[px]-\u003e(a)\nWHERE not exists { (wordle)-[:POS1]-\u003e(a) } \n  AND not exists { (wordle)-[:POS2]-\u003e(:Char {char:'l'}) } \n  AND not exists { (wordle)-[:POS3]-\u003e(:Char {char:'i'}) }\nRETURN *;\n----\n\nimage::wordle-rel-model.png[]\n\nIf we have more information, then we can extend the query by excluding letters or positions and get a smaller result set.\n\n[source,cypher]\n----\nMATCH (c:Char {char:'c'}), \n      (h:Char {char:'h'}),\n      (a:Char {char:'a'})\nMATCH (wordle:Word)-[p0:POS0]-\u003e(c),\n      (wordle)-[p4:POS4]-\u003e(h),\n      (wordle)-[px]-\u003e(a)\nWHERE not exists { (wordle)-[:POS1]-\u003e(a) } \n  AND not exists { (wordle)-[:POS2]-\u003e(:Char {char:'l'}) } \n  AND not exists { (wordle)-[:POS3]-\u003e(:Char {char:'i'}) }\nRETURN *;\n----\nimage::wordle-rel-model-exclusions.png[]\n\n\n=== Playing wordle in your Terminal\n\nIf 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.\n\n[source,shell]\n----\n./wordle-neo4j.sh \nGuess 1: diver\n🟨⬜⬜🟩🟩\nGuess 2: later\n🟨⬜⬜🟩🟩\nGuess 3: elder\n🟩🟩🟩🟩🟩\nGuessed \"elder\" aka 🟩🟩🟩🟩🟩 in 3 rounds.\n----\n\nThe statement that's running is:\n\n[source,cypher]\n----\nmatch (w:Word) \nwith w skip $word limit 1 \nwith split($guess,'') as guessed, split(w.name,'') as letters, w.name as name \nreturn reduce(res='', idx in range(0,size(letters)-1) | res + \n  case when guessed[idx] = letters[idx] then '🟩' \n  when name contains guessed[idx] then '🟨'\n  else '⬜' end) as res\n----\n\n=== Ideas\n\n* explain two models\n* loading\n* post-processing\n* look at char frequencies\n* recommend starting words (based on top frequencies)\n* rarest words\n* solve word\n* visualize solver\n* input word so far, e.g. `C a l! i! H` and have a generic cypher-statement to run with this word\n\n* implement wordle -\u003e split input + match","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjexp%2Fwordle-graph","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjexp%2Fwordle-graph","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjexp%2Fwordle-graph/lists"}