Initial commit

This commit is contained in:
2025-08-19 11:10:05 +02:00
commit d6d23a230e
10 changed files with 50883 additions and 0 deletions

10
.dockerignore Normal file
View File

@@ -0,0 +1,10 @@
.venv/
__pycache__/
*.pyc
*.pyo
*.pyd
*.log
.git/
.gitignore
.env
.DS_Store

8
.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
.venv/
__pycache__/
*.py[cod]
*.log
.env
.DS_Store
.vscode/
.idea/

20
Dockerfile Normal file
View File

@@ -0,0 +1,20 @@
FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PYTHONUTF8=1
WORKDIR /app
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY . ./
# Wortliste während des Builds erzeugen (nutzt data/openthesaurus.txt, falls vorhanden)
RUN python scripts/generate_wordlist.py
EXPOSE 8000
# Starte Flask-App via Gunicorn (3 Worker, Thread-Worker)
CMD ["gunicorn", "-w", "3", "-k", "gthread", "--threads", "2", "-b", "0.0.0.0:8000", "app:app"]

67
app.py Normal file
View File

@@ -0,0 +1,67 @@
from pathlib import Path
from flask import Flask, render_template, request
app = Flask(__name__)
def load_words() -> list[str]:
data_path = Path(__file__).parent / "data" / "words_de_5.txt"
if not data_path.exists():
return []
words: list[str] = []
with data_path.open("r", encoding="utf-8") as f:
for line in f:
word = line.strip().lower()
if len(word) == 5 and word.isalpha():
words.append(word)
return words
def filter_words(words: list[str], position_letters: list[str], includes_text: str, excludes_text: str) -> list[str]:
results: list[str] = []
includes_letters = [ch for ch in includes_text.lower() if ch.isalpha()]
excludes_letters = [ch for ch in excludes_text.lower() if ch.isalpha()]
for word in words:
# feste Positionen
if any(ch and word[idx] != ch for idx, ch in enumerate(position_letters)):
continue
# muss-enthalten
if not all(ch in word for ch in includes_letters):
continue
# darf-nicht-enthalten
if any(ch in word for ch in excludes_letters):
continue
results.append(word)
return results
@app.route("/", methods=["GET", "POST"])
def index():
all_words = load_words()
results: list[str] | None = None
pos: list[str] = ["", "", "", "", ""]
includes: str = ""
excludes: str = ""
if request.method == "POST":
pos = [
(request.form.get("pos1") or "").strip().lower(),
(request.form.get("pos2") or "").strip().lower(),
(request.form.get("pos3") or "").strip().lower(),
(request.form.get("pos4") or "").strip().lower(),
(request.form.get("pos5") or "").strip().lower(),
]
includes = (request.form.get("includes") or "").strip()
excludes = (request.form.get("excludes") or "").strip()
results = filter_words(all_words, pos, includes, excludes)
return render_template(
"index.html",
results=results,
pos=pos,
includes=includes,
excludes=excludes,
words_count=len(all_words),
)
if __name__ == "__main__":
app.run(debug=True)

47497
data/openthesaurus.txt Normal file

File diff suppressed because it is too large Load Diff

3132
data/words_de_5.txt Normal file

File diff suppressed because it is too large Load Diff

7
idea.txt Normal file
View File

@@ -0,0 +1,7 @@
erstelle eine web-app, die beim lösen von wordle rätseln hilft:
- hinterlege eine liste aller deutschen wörter mit 5 buchstaben
- der benutzer soll die buchstaben mit richtiger position an stelle [1..5] und weitere buchstaben angeben können, von denen nicht sicher ist, an welcher stelle sie auftreten.
- die app soll dann mögliche lösungen ausgeben.
es soll das venv in .venv genutzt werden.
es wird unter windows entwickelt.

3
requirements.txt Normal file
View File

@@ -0,0 +1,3 @@
Flask>=3,<4
wordfreq>=3,<4
gunicorn>=22,<23

View File

@@ -0,0 +1,78 @@
from __future__ import annotations
import re
from pathlib import Path
from typing import Iterable
try:
# Optional, nur Fallback
from wordfreq import top_n_list # type: ignore
except Exception: # pragma: no cover
top_n_list = None # type: ignore
ALLOWED_FIVE_LETTER = re.compile(r"^[a-zäöüß]{5}$")
def is_valid_five_letter_word(word: str) -> bool:
return bool(ALLOWED_FIVE_LETTER.match(word))
def clean_token(token: str) -> str:
# entferne Klammerzusätze wie (ugs.), (fachspr.), etc.
no_paren = re.sub(r"\([^)]*\)", "", token)
# entferne Auslassungszeichen (Präfix-/Suffix-Markierungen)
no_ellipsis = no_paren.replace("...", "").replace("", "")
# nur Buchstaben (inkl. äöüß) behalten
letters_only = re.sub(r"[^A-Za-zÄÖÜäöüß]", "", no_ellipsis)
return letters_only.strip().lower()
def extract_from_openthesaurus(path: Path) -> list[str]:
words: set[str] = set()
with path.open("r", encoding="utf-8") as f:
for raw_line in f:
line = raw_line.strip()
if not line or line.startswith("#"):
continue
parts = line.split(";")
for part in parts:
# Einträge mit Auslassungen überspringen (keine vollständigen Wörter)
if "..." in part or "" in part:
continue
token = clean_token(part)
if is_valid_five_letter_word(token):
words.add(token)
return sorted(words)
def extract_from_wordfreq(limit: int = 500_000) -> list[str]:
if top_n_list is None:
return []
candidates = top_n_list("de", limit)
words = {clean_token(w) for w in candidates}
return sorted(w for w in words if is_valid_five_letter_word(w))
def main() -> None:
root = Path(__file__).resolve().parents[1]
source_ot = root / "data" / "openthesaurus.txt"
out_path = root / "data" / "words_de_5.txt"
out_path.parent.mkdir(parents=True, exist_ok=True)
if source_ot.exists():
words = extract_from_openthesaurus(source_ot)
source = "OpenThesaurus"
else:
words = extract_from_wordfreq()
source = "wordfreq"
with out_path.open("w", encoding="utf-8") as f:
for w in words:
f.write(w + "\n")
print(f"Gespeichert: {len(words)} Wörter (Quelle: {source}) -> {out_path}")
if __name__ == "__main__":
main()

61
templates/index.html Normal file
View File

@@ -0,0 +1,61 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>WordleCheater (DE)</title>
<style>
body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; margin: 2rem; }
.container { max-width: 800px; margin: 0 auto; }
.grid { display: grid; grid-template-columns: repeat(5, 3rem); gap: .5rem; }
.grid input { text-align: center; font-size: 1.25rem; padding: .4rem; }
label { font-weight: 600; display: block; margin-top: 1rem; margin-bottom: .25rem; }
.results { margin-top: 1.5rem; }
.badge { display: inline-block; padding: .25rem .5rem; background: #f3f4f6; border-radius: .375rem; margin-right: .25rem; margin-bottom: .25rem; }
button { margin-top: 1rem; padding: .5rem 1rem; font-size: 1rem; }
summary { cursor: pointer; }
</style>
</head>
<body>
<div class="container">
<h1>WordleCheater (Deutsch)</h1>
<p>Wortliste geladen: <strong>{{ words_count }}</strong> Wörter</p>
<form method="post">
<label for="pos1">Buchstaben mit korrekter Position</label>
<div class="grid">
<input id="pos1" name="pos1" maxlength="1" value="{{ pos[0] }}" />
<input id="pos2" name="pos2" maxlength="1" value="{{ pos[1] }}" />
<input id="pos3" name="pos3" maxlength="1" value="{{ pos[2] }}" />
<input id="pos4" name="pos4" maxlength="1" value="{{ pos[3] }}" />
<input id="pos5" name="pos5" maxlength="1" value="{{ pos[4] }}" />
</div>
<label for="includes">Weitere enthaltene Buchstaben (beliebige Reihenfolge)</label>
<input id="includes" name="includes" value="{{ includes }}" />
<label for="excludes">Ausgeschlossene Buchstaben</label>
<input id="excludes" name="excludes" value="{{ excludes }}" />
<button type="submit">Suchen</button>
</form>
{% if results is not none %}
<div class="results">
<h2>Vorschläge ({{ results|length }})</h2>
{% if results|length == 0 %}
<p>Keine Treffer. Bitte Bedingungen anpassen.</p>
{% else %}
<details open>
<summary>Liste anzeigen</summary>
<p>
{% for w in results %}
<span class="badge">{{ w }}</span>
{% endfor %}
</p>
</details>
{% endif %}
</div>
{% endif %}
</div>
</body>
</html>