Initial commit
This commit is contained in:
10
.dockerignore
Normal file
10
.dockerignore
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
.venv/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
*.log
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
.env
|
||||||
|
.DS_Store
|
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
.venv/
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*.log
|
||||||
|
.env
|
||||||
|
.DS_Store
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
20
Dockerfile
Normal file
20
Dockerfile
Normal 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
67
app.py
Normal 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
47497
data/openthesaurus.txt
Normal file
File diff suppressed because it is too large
Load Diff
3132
data/words_de_5.txt
Normal file
3132
data/words_de_5.txt
Normal file
File diff suppressed because it is too large
Load Diff
7
idea.txt
Normal file
7
idea.txt
Normal 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
3
requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
Flask>=3,<4
|
||||||
|
wordfreq>=3,<4
|
||||||
|
gunicorn>=22,<23
|
78
scripts/generate_wordlist.py
Normal file
78
scripts/generate_wordlist.py
Normal 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
61
templates/index.html
Normal 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>Wordle‑Cheater (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>Wordle‑Cheater (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>
|
Reference in New Issue
Block a user