Back to skills
SkillHub ClubShip Full StackFull Stack

boggle

Solve Boggle boards — find all valid words (German + English) on a 4x4 letter grid. Use when the user shares a Boggle photo, asks for words on a grid, or plays word games. Includes 1.7M word dictionaries (DE+EN).

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
3,108
Hot score
99
Updated
March 20, 2026
Overall rating
C0.0
Composite score
0.0
Best-practice grade
A88.4

Install command

npx @skill-hub/cli install openclaw-skills-boggle

Repository

openclaw/skills

Skill path: skills/christianhaberl/boggle

Solve Boggle boards — find all valid words (German + English) on a 4x4 letter grid. Use when the user shares a Boggle photo, asks for words on a grid, or plays word games. Includes 1.7M word dictionaries (DE+EN).

Open repository

Best for

Primary workflow: Ship Full Stack.

Technical facets: Full Stack.

Target audience: everyone.

License: Unknown.

Original source

Catalog source: SkillHub Club.

Repository owner: openclaw.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install boggle into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/openclaw/skills before adding boggle to shared team environments
  • Use boggle for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
name: boggle
description: Solve Boggle boards — find all valid words (German + English) on a 4x4 letter grid. Use when the user shares a Boggle photo, asks for words on a grid, or plays word games. Includes 1.7M word dictionaries (DE+EN).
---

# Boggle Solver

Fast trie-based DFS solver with dictionary-only matching. No AI/LLM guessing — words are validated exclusively against bundled dictionaries (359K English + 1.35M German).

## Workflow (from photo)

1. **Read the 4x4 grid** from the photo (left-to-right, top-to-bottom)
2. **Show the grid to the user and ask for confirmation** before solving
3. Only after user confirms → run the solver
4. **Always run English and German SEPARATELY** — present as two labeled sections (🇬🇧 / 🇩🇪)

## Solve a board

```bash
# English
python3 skills/boggle/scripts/solve.py ELMU ZBTS ETVO CKNA --lang en

# German
python3 skills/boggle/scripts/solve.py ELMU ZBTS ETVO CKNA --lang de
```

Each row is one argument (4 letters). Or use `--letters`:
```bash
python3 skills/boggle/scripts/solve.py --letters ELMUZBTSETVOCKNA --lang en
```

## Options

| Flag | Description |
|---|---|
| `--lang en/de` | Language (default: en; **always run EN and DE separately**) |
| `--min N` | Minimum word length (default: 3) |
| `--json` | JSON output with scores |
| `--dict FILE` | Custom dictionary (repeatable) |

## Scoring (standard Boggle)

- 3-4 letters: 1 pt
- 5 letters: 2 pts
- 6 letters: 3 pts
- 7 letters: 5 pts
- 8+ letters: 11 pts

## How it works

- Builds a trie from dictionary files (one-time, ~11s)
- DFS traversal from every cell, pruned by trie prefixes
- Adjacency: 8 neighbors (horizontal, vertical, diagonal)
- Each cell used at most once per word
- **Qu tile support:** Standard Boggle "Qu" tiles are handled as a single cell (e.g., `QUENHARI...` → "QU" occupies one position)
- **All matching is dictionary-only** — no generative/guessed words

## Data

Dictionaries are auto-downloaded from GitHub on first run if missing.


- `data/words_english_boggle.txt` — 359K English words
- `data/words_german_boggle.txt` — 1.35M German words

## Performance

- Trie build: ~11s (first run, 1.7M words)
- Solve: <5ms per board



---

## Skill Companion Files

> Additional files collected from the skill directory layout.

### README.md

```markdown
# 🎲 Boggle Solver — OpenClaw Skill

Fast trie-based DFS Boggle solver for [OpenClaw](https://github.com/openclaw/openclaw).

## Features

- **1.7M dictionary words** — 359K English + 1.35M German
- **Qu-tile support** — standard Boggle rules
- **< 5ms solve time** per board
- **JSON output** with Boggle scoring
- **Bilingual** — run English and German separately

## Install

```bash
clawdhub install boggle
```

Or manually copy the `skills/boggle/` folder into your OpenClaw workspace.

## Usage

```bash
# English
python3 scripts/solve.py ELMU ZBTS ETVO CKNA --lang en

# German
python3 scripts/solve.py ELMU ZBTS ETVO CKNA --lang de

# With --letters flag
python3 scripts/solve.py --letters ELMUZBTSETVOCKNA --lang en

# JSON output
python3 scripts/solve.py ELMU ZBTS ETVO CKNA --lang en --json
```

## Options

| Flag | Description |
|---|---|
| `--lang en/de` | Dictionary language (default: en) |
| `--min N` | Minimum word length (default: 3) |
| `--json` | JSON output with scores |
| `--dict FILE` | Custom dictionary (repeatable) |

## Scoring (standard Boggle)

- 3-4 letters: 1 pt
- 5 letters: 2 pts
- 6 letters: 3 pts
- 7 letters: 5 pts
- 8+ letters: 11 pts

## How it works

- Builds a trie from dictionary files
- DFS traversal from every cell, pruned by trie prefixes
- Adjacency: 8 neighbors (horizontal, vertical, diagonal)
- Each cell used at most once per word
- Qu tiles handled as single cell
- **All matching is dictionary-only** — no AI guessing

## AI-Reviewed

This skill was reviewed by **Codex** and **Gemini Code Assist** across 5 review rounds. All findings addressed.

## License

MIT

```

### _meta.json

```json
{
  "owner": "christianhaberl",
  "slug": "boggle",
  "displayName": "Boggle Solver",
  "latest": {
    "version": "1.0.0",
    "publishedAt": 1769882702822,
    "commit": "https://github.com/clawdbot/skills/commit/b22a1ec6202329c753b05f613d0748c82b080f61"
  },
  "history": []
}

```

### scripts/solve.py

```python
#!/usr/bin/env python3
"""
Boggle solver — finds all valid words on a 4x4 board.
Uses DFS with trie-based pruning for speed.

Usage:
  python3 solve.py HOPL NERI NIDO IEOT
  python3 solve.py --letters "H O P L N E R I N I D O I E O T"
  python3 solve.py --letters HOPLNERINIDOIEOT

Dictionaries auto-loaded from:
  1. skills/boggle/data/words_english_boggle.txt
  2. skills/boggle/data/words_german_boggle.txt
Or specify: --dict /path/to/wordlist.txt [--dict another.txt]
"""

import argparse
from itertools import groupby
import os
import sys
import time

SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
SKILL_DIR = os.path.dirname(SCRIPT_DIR)
DATA_DIR = os.path.join(SKILL_DIR, "data")

DEFAULT_DICTS = [
    os.path.join(DATA_DIR, "words_english_boggle.txt"),
    os.path.join(DATA_DIR, "words_german_boggle.txt"),
]

DEFAULT_MIN_WORD_LEN = 3

def ensure_dictionaries(paths):
    """Auto-download dictionary files from GitHub if missing."""
    import urllib.request
    BASE_URL = 'https://raw.githubusercontent.com/christianhaberl/boggle-openclaw-skill/main/data'
    for path in paths:
        if not os.path.exists(path):
            fname = os.path.basename(path)
            url = f'{BASE_URL}/{fname}'
            print(f'Downloading {fname}...', file=sys.stderr)
            os.makedirs(os.path.dirname(path), exist_ok=True)
            urllib.request.urlretrieve(url, path)
            print(f'  -> {path}', file=sys.stderr)


SCORING = {3: 1, 4: 1, 5: 2, 6: 3, 7: 5}  # 8+ = 11
NEIGHBORS = tuple((dr, dc) for dr in (-1, 0, 1) for dc in (-1, 0, 1) if not (dr == 0 and dc == 0))


class TrieNode:
    __slots__ = ['children', 'is_word']
    def __init__(self):
        self.children = {}
        self.is_word = False


def build_trie(words):
    root = TrieNode()
    for word in words:
        node = root
        for ch in word:
            if ch not in node.children:
                node.children[ch] = TrieNode()
            node = node.children[ch]
        node.is_word = True
    return root


def load_dictionaries(paths, min_word_len=DEFAULT_MIN_WORD_LEN):
    words = set()
    for path in paths:
        if not os.path.exists(path):
            print(f"WARNING: Dictionary not found: {path}", file=sys.stderr)
            continue
        before = len(words)
        with open(path, 'r', encoding='utf-8', errors='ignore') as f:
            for line in f:
                w = line.strip().lower()
                if len(w) >= min_word_len and w.isalpha():
                    words.add(w)
        added = len(words) - before
        print(f"Loaded {os.path.basename(path)}: {added} new words ({len(words)} total)", file=sys.stderr)
    return words


def tokenize_tiles(text):
    """Tokenize a string into Boggle tiles, handling multi-char tiles like 'Qu'.
    
    Standard Boggle has a single 'Qu' tile. This tokenizer scans left-to-right
    and greedily matches 'qu' as one tile; all other characters are single tiles.
    Returns a list of tile strings (lowercased).
    """
    tiles = []
    i = 0
    text = text.lower()
    while i < len(text):
        if i + 1 < len(text) and text[i:i+2] == 'qu':
            tiles.append('qu')
            i += 2
        else:
            tiles.append(text[i])
            i += 1
    return tiles


def parse_board(args):
    """Parse board from various input formats.
    
    Raises ValueError on invalid input instead of calling sys.exit(),
    so callers can handle errors gracefully and the function is testable.
    Supports standard Boggle 'Qu' tiles (counted as one cell).
    """
    if args.letters:
        raw = args.letters.replace(" ", "").replace(",", "")
        tiles = tokenize_tiles(raw)
        if len(tiles) != 16:
            raise ValueError(f"Need 16 tiles, got {len(tiles)} (hint: 'Qu' counts as one tile)")
        return [tiles[i:i+4] for i in range(0, 16, 4)]
    
    if args.rows:
        rows = []
        for r in args.rows:
            r = r.replace(" ", "")
            tiles = tokenize_tiles(r)
            rows.append(tiles)
        if len(rows) != 4 or any(len(r) != 4 for r in rows):
            raise ValueError("Need 4 rows of 4 tiles each (hint: 'Qu' counts as one tile)")
        return rows
    
    raise ValueError("Provide board as 4 row arguments or --letters")


def solve(board, trie, min_word_len=DEFAULT_MIN_WORD_LEN):
    found = set()
    rows, cols = len(board), len(board[0])
    
    def dfs(r, c, node, path, visited):
        tile = board[r][c]  # tile can be multi-char (e.g., 'qu')
        
        # Walk the trie through each character in the tile
        current = node
        for ch in tile:
            if ch not in current.children:
                return
            current = current.children[ch]
        
        new_path = path + tile
        
        if current.is_word:
            found.add(new_path)
        
        if not current.children:
            return
        
        visited.add((r, c))
        for dr, dc in NEIGHBORS:
            nr, nc = r + dr, c + dc
            if 0 <= nr < rows and 0 <= nc < cols and (nr, nc) not in visited:
                dfs(nr, nc, current, new_path, visited)
        visited.remove((r, c))
    
    for r in range(rows):
        for c in range(cols):
            dfs(r, c, trie, "", set())
    
    return found


def score_word(word):
    n = len(word)
    if n >= 8:
        return 11
    return SCORING.get(n, 0)


def main():
    parser = argparse.ArgumentParser(description="Boggle Solver")
    parser.add_argument("rows", nargs="*", help="4 rows of 4 letters (e.g., HOPL NERI NIDO IEOT)")
    parser.add_argument("--letters", "-l", help="All 16 letters as one string")
    parser.add_argument("--dict", "-d", action="append", dest="dicts", help="Dictionary file(s)")
    parser.add_argument("--min", type=int, default=3, help="Minimum word length (default: 3)")
    parser.add_argument("--json", action="store_true", help="JSON output")
    parser.add_argument("--lang", choices=["en", "de"], default="en", help="Dictionary language: 'en' for English, 'de' for German (run once per language for bilingual boards)")
    args = parser.parse_args()

    min_word_len = args.min

    try:
        board = parse_board(args)
    except ValueError as e:
        print(f"ERROR: {e}", file=sys.stderr)
        sys.exit(1)
    
    # Print board
    print("Board:", file=sys.stderr)
    for row in board:
        print("  " + " ".join(t.upper() for t in row), file=sys.stderr)
    print(file=sys.stderr)

    # Load dictionaries
    dict_paths = args.dicts if args.dicts else []
    if not dict_paths:
        if args.lang == "en":
            dict_paths = [DEFAULT_DICTS[0]]
        elif args.lang == "de":
            dict_paths = [DEFAULT_DICTS[1]]

    ensure_dictionaries(dict_paths)
    t0 = time.time()
    words = load_dictionaries(dict_paths, min_word_len)
    if not words:
        print("ERROR: No dictionary words loaded", file=sys.stderr)
        sys.exit(1)
    
    t1 = time.time()
    trie = build_trie(words)
    t2_trie = time.time()
    print(f"Dictionary loaded in {t1-t0:.2f}s, trie built in {t2_trie-t1:.2f}s", file=sys.stderr)

    # Solve
    found = solve(board, trie, min_word_len)
    t2 = time.time()
    print(f"Solved in {t2-t2_trie:.3f}s — {len(found)} words found", file=sys.stderr)

    # Sort by length (desc) then alphabetically
    sorted_words = sorted(found, key=lambda w: (-len(w), w))
    
    total_score = sum(score_word(w) for w in sorted_words)

    if args.json:
        import json
        result = {
            "board": [row for row in board],
            "words": [{"word": w, "length": len(w), "score": score_word(w)} for w in sorted_words],
            "total_words": len(sorted_words),
            "total_score": total_score,
            "solve_time_ms": int((t2-t2_trie)*1000),
        }
        print(json.dumps(result, indent=2, ensure_ascii=False))
    else:
        # Group by length using itertools.groupby (sorted_words already sorted by -len)
        for length, group in groupby(sorted_words, key=len):
            words_list = list(group)
            pts = score_word(words_list[0])
            print(f"\n{'='*40}")
            print(f"{length} letters ({pts} pts each) — {len(words_list)} words:")
            print(", ".join(w.upper() for w in words_list))
        
        print(f"\n{'='*40}")
        print(f"TOTAL: {len(sorted_words)} words, {total_score} points")


if __name__ == "__main__":
    main()

```