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