{"id":43700205,"url":"https://github.com/nimobeeren/strands-solver","last_synced_at":"2026-02-05T05:03:15.490Z","repository":{"id":316815126,"uuid":"1056798707","full_name":"nimobeeren/strands-solver","owner":"nimobeeren","description":"A solver for Strands, the New York Times puzzle game.","archived":false,"fork":false,"pushed_at":"2026-01-10T20:41:20.000Z","size":3170,"stargazers_count":1,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-01-11T05:49:25.216Z","etag":null,"topics":["nytimes","puzzle-solver","strands"],"latest_commit_sha":null,"homepage":"","language":"Python","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/nimobeeren.png","metadata":{"files":{"readme":"README.md","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,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2025-09-14T20:28:39.000Z","updated_at":"2026-01-10T20:41:23.000Z","dependencies_parsed_at":"2025-09-26T22:28:27.687Z","dependency_job_id":"cb65d446-a23c-4cb3-b64f-22521d96d408","html_url":"https://github.com/nimobeeren/strands-solver","commit_stats":null,"previous_names":["nimobeeren/strands-solver"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/nimobeeren/strands-solver","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nimobeeren%2Fstrands-solver","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nimobeeren%2Fstrands-solver/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nimobeeren%2Fstrands-solver/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nimobeeren%2Fstrands-solver/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nimobeeren","download_url":"https://codeload.github.com/nimobeeren/strands-solver/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nimobeeren%2Fstrands-solver/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29113190,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-05T03:44:17.043Z","status":"ssl_error","status_checked_at":"2026-02-05T03:44:12.077Z","response_time":65,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"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":["nytimes","puzzle-solver","strands"],"created_at":"2026-02-05T05:03:14.643Z","updated_at":"2026-02-05T05:03:15.476Z","avatar_url":"https://github.com/nimobeeren.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Strands Solver\n\nA solver for Strands, the New York Times puzzle game.\n\n## Prerequisites\n\n- Install the [uv](https://docs.astral.sh/uv/) package manager.\n- (Optional) Set the `GEMINI_API_KEY` environment variable to a valid [Gemini API key](https://ai.google.dev/gemini-api/docs/api-key). A free tier key is sufficient for solving puzzles, though it may take longer due to rate limits. Using a paid tier key is faster but incurs a tiny cost: typically $0.00001–$0.0001 per puzzle.\n\n\u003e [!NOTE]\n\u003e Without a `GEMINI_API_KEY` the solver will try to find valid solutions but it can't accurately determine which solution is best.\n\n## Basic Usage\n\n```bash\nuvx strands-solver solve today                # solve today's puzzle\nuvx strands-solver solve YYYY-MM-DD           # solve another day's puzzle\nuvx strands-solver solve path_to_puzzle.json  # solve puzzle from a file\n```\n\nSee also [Advanced Usage](#advanced-usage).\n\n## Goal\n\nThis program attempts to solve Strands puzzles in one shot, i.e. without a way to iteratively determine whether the chosen words are correct or not.\n\nMore precisely, given\n\n- a rectangular grid of letters\n- a phrase describing a theme\n- a number specifying the number of words (or, more accurately: strands) in the solution\n\nthe solver attempts to find the set of _strands_ (sequences of adjacent letters in the grid) for which\n\n- the number of strands matches the given number of words\n- every letter in the grid is covered exactly once\n- every strand is at least 4 letters long\n- there is at least one strand (called the _spangram_) which spans the entire grid vertically or horizontally\n- every strand spells out a valid word (though the spangram may be a concatenation of multiple words)\n- all strands are maximally related to the theme (in a semantic, possibly cryptic way)\n\n### Example\n\nThis is the puzzle of 2025-10-03:\n\n```\nTheme: Who's in charge?\n\n A     M     E     L     S     S\n\n N     A     A     F     E     O\n\n G     E     R     D     I     B\n\n E     A     D     E     H     S\n\n O     H     R     C     I     O\n\n R     T     C     S     V     R\n\n D     E     P     H     R     S\n\n I     R     I     E     P     U\n\nNumber of words: 7\n```\n\nThe goal is then to find this solution:\n\n\u003cimg alt=\"The same grid of letters as earlier, with the word LEADERSHIP highlighted in yellow and the words BOSS, CHIEF, DIRECTOR, MANAGE, SUPERVISOR and HEARD highlighted in blue.\" src=\"./assets/example_solution.png\" width=\"300\" /\u003e\n\nwhere the strands are\n\n```\n🟡 LEADERSHIP (spangram)\n🔵 BOSS\n🔵 CHIEF\n🔵 DIRECTOR\n🔵 MANAGER\n🔵 SUPERVISOR\n🔵 HEAD\n```\n\nThis is the \"correct\" solution as provided by the New York Times. There are other valid solutions, but they don't match the theme quite as well.\n\n## Results\n\nThe solver has been validated and benchmarked on a set of official puzzles. Currently, it solves a subset of puzzles correctly. Results are recorded in [`results.md`](./results.md), which includes a summary and results for each puzzle.\n\n## Limitations\n\n- Some puzzles can't be solved in a reasonable amount of time (see [results](./results.md)).\n- The solver will only find a solution if the spangram is a single word or a concatenation of words which are each 4 letters or longer. In reality, the words in a concatenated spangram may be shorter than 4 letters.\n- The solver usually finds multiple solutions but it doesn't always choose the solution that best fits the theme.\n- The solver will not find solutions where the spangram contains a contraction (like YOURE), which does appear in real solutions.\n- The dictionary ([`dictionary.py`](./src/strands_solver/dictionary.py)) uses the [ENABLE1](https://rressler.quarto.pub/i_data_sets/data_word_lists.html) word list, which is comprehensive but may occasionally miss some valid words or include uncommon ones. This may cause the solver to fail to find a valid solution.\n\nSome ideas for tackling these limitations are listed in [IDEAS.md](./IDEAS.md).\n\n## Advanced Usage\n\nThe CLI provides four commands: `solve`, `show`, `benchmark`, and `embed`.\n\n### `solve`\n\nSolve a Strands puzzle.\n\n```bash\nuvx strands-solver solve today                 # solve today's puzzle\nuvx strands-solver solve YYYY-MM-DD            # solve another day's puzzle\nuvx strands-solver solve path_to_puzzle.json   # solve puzzle from a file\nuvx strands-solver solve today -o ./solutions  # write all solutions to a directory\n```\n\n### `show`\n\nDisplay the official solution for a puzzle from the NY Times API (not used for solving).\n\n```bash\nuvx strands-solver show today\nuvx strands-solver show YYYY-MM-DD\n```\n\n### `benchmark`\n\nBenchmark the solver against a set of puzzles. A report of the results is saved to a Markdown file.\n\n```bash\nuvx strands-solver benchmark                              # default: 2025-09-01 to 2025-12-31\nuvx strands-solver benchmark -s 2025-10-01 -e 2025-10-31  # custom date range\nuvx strands-solver benchmark -t 30                        # 30 second timeout per puzzle\nuvx strands-solver benchmark -r ./my_results.md           # custom report file\n```\n\n### `embed`\n\nThe solver uses semantic embeddings to determine which solution best fits the theme. These embeddings are\ngenerated while solving a puzzle and cached for future re-use. However, when solving many puzzles (such as when\nrunning a benchmark), you may run into rate limits for the embedding API. To avoid this, you can generate\nembeddings ahead of time.\n\nEmbedding the entire dictionary costs about $0.10 and takes about 60 minutes on a paid (Tier 1) Gemini project\n(based on 2025-12-30 pricing and rate limits). While it's technically possible to do on the free tier, this would\ntake a very long time due to rate limits. Storing the embeddings database also uses about 2 GB of disk space.\n\nTo generate dictionary embeddings:\n\n```bash\nuvx strands-solver embed           # embed words not already cached\nuvx strands-solver embed --reload  # re-embed all words\n```\n\nThe embeddings database is stored in the user cache directory (`~/.cache/strands-solver/embeddings.db` on Linux, `~/Library/Caches/strands-solver/embeddings.db` on macOS, `%LOCALAPPDATA%\\strands-solver\\Cache\\embeddings.db` on Windows).\n\n## How It Works\n\nThe solver finds solutions using a 4-phase algorithm:\n\n1. [`WordFinder`](./src/strands_solver/word_finder.py) — Find all strands in the grid that spell dictionary words. Starting from each cell, recursively take a step in all directions (using [DFS](https://en.wikipedia.org/wiki/Depth-first_search)), stopping if there is no word in the dictionary which starts with the strand so far.\n\n\u003e [!TIP]\n\u003e While legal, official solutions never _require_ a strand to cross itself. Therefore, we filter out self-crossing strands during word finding. This optimization reduces the total number of words and primarily speeds up following phases.\n\n2. [`GridCoverer`](./src/strands_solver/grid_coverer.py) — Find all ways to cover all cells of the grid exactly once using a subset of the words found in phase 1. This is an [exact cover](https://en.wikipedia.org/wiki/Exact_cover) problem solved with a backtracking algorithm that uses the [MRV (Minimum Remaining Values)](https://cs50.harvard.edu/extension/ai/2020/fall/notes/3/#backtracking-search) heuristic: always branch on the cell with the fewest covering strands.\n\n\u003e [!TIP]\n\u003e Official solutions never contain strands that cross each other. Therefore, we prevent this during covering. This optimization speeds up the covering phase by pruning branches early.\n\n3. [`SpangramFinder`](./src/strands_solver/spangram_finder.py) — Filter covers found in phase 2 to those containing a spangram. If the cover has more strands than the word count specified in the puzzle, try concatenating adjacent strands to form the spangram.\n\n\u003e [!TIP]\n\u003e Words that appear in multiple places in the grid (duplicates) can never be part of the solution, unless they are part of the spangram (concatenated with other words). Therefore, if a cover contains such a duplicate, we force it to be part of the spangram. This optimization speeds up spangram finding by reducing the number of concatenation combinations to try.\n\n4. [`SolutionRanker`](./src/strands_solver/solution_ranker.py) — Rank solutions found in phase 3 by semantic similarity between words in the solution and the theme. Compute [embeddings](https://en.wikipedia.org/wiki/Word_embedding) for all words and the theme, then score each solution by the average pairwise [cosine similarity](https://en.wikipedia.org/wiki/Cosine_similarity) between its words and the theme.\n\n\u003e [!TIP]\n\u003e Embeddings are cached on disk to reduce costs. Since we only embed single words from the dictionary and themes from puzzles, the total embedding cost is bounded (see [`embed`](#embed) usage).\n\nThe main orchestration logic is in [`Solver`](./src/strands_solver/solver.py).\n\n## How It Was Made\n\nI started this project to try out modern coding agents on a non-trivial but easy to validate problem. I expected a little vibe-coding would get me most of the way there, but the problem proved to be a lot more challenging than I thought! Along the way though, I learned to collaborate with my coding agent in a way that truly extended my abilities.\n\nMy first idea was simple. First, I'd find all words in the grid by looking at each cell and taking steps in all directions, stopping if it was not a valid prefix of a word in the dictionary. Then, to cover the grid I'd just try all combinations of found words, ignoring combinations where words overlap (which I assumed would often be the case). The word finding worked first try, but the covering was extremely slow. It just never completed.\n\nTo find out why, I asked my coding agent for help. I have some basic knowledge of complexity analysis, but I first wanted to refresh my memory. I prompted:\n\n\u003e how do i estimate the computational complexity of a backtracking algorithm?\n\nAfter grasping the basics, I asked:\n\n\u003e how would you estimate my algo in @solver.py? start simple and add more nuance after\n\nIt explained that my algo had a worst-case complexity of $O(2^M)$ with $M$ being the number of candidate words (typically 1000-2000). This was completely infeasible!\n\nSo let's make it faster!\n\n\u003e The puzzles I want to solve have N = 48 and M ~= 2000. What are some ways I could improve my algorithm in @solver.py to make this tractable? Think about different angles, start with the simplest/closest to my current algo and give 2 better options if they exist.\n\nIt gave me three options for algorithms, none of which I had ever heard of. It explained that the worst-case complexity was still exponential (as the problem is NP-complete), but that we could massively speed things up by always picking the cell with the smallest number of strands covering it, and recurse from there (a heuristic called MRV).\n\nOf course I asked it to implement the new algo, and it worked! I was now able to cover the grid in just a few minutes for most puzzles. (I did lose the chat history for this step, unfortunately.)\n\nI was really happy with this workflow. My coding agent could look at my code, suggest improvements I never would have thought of, implement them and make huge performance gains. I did end up rewriting some of the code to better fit my mental model and to fully understand it, as I found this was necessary to keep making improvements. But here too the agent was invaluable in helping me understand the existing code.\n\n\u003e [!TIP]\n\u003e I've added an [export of my chat](./assets/cursor_algo_analysis.md) where I asked for analysis and algo suggestions.\n\n## Development\n\n### Running locally\n\nClone the repository and run:\n\n```bash\nuv run strands-solver\n```\n\n\u003e [!NOTE]\n\u003e `uvx` runs the latest _published_ version, which doesn't include your local changes.\n\n### Tests\n\n```bash\nuv run pytest         # unit + integration tests\nuv run pytest -m e2e  # end-to-end tests\n```\n\nWe have three types of tests:\n\n- **Unit tests** (`tests/unit/`) are fast and reliable because they test individual components.\n- **Integration tests** (`tests/integration/`) test multiple components together but have no external dependencies.\n- **End-to-end tests** (`tests/e2e/`) run the full application through the CLI, relying on external APIs.\n\nBy default, end-to-end tests are skipped because they are slower and could fail if an external API changes/fails.\n\n### Type Checking\n\n```bash\nuv run pyright\n```\n\n### Formatting\n\n```bash\nuv run ruff format\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnimobeeren%2Fstrands-solver","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnimobeeren%2Fstrands-solver","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnimobeeren%2Fstrands-solver/lists"}