42Cummer's picture
Update app.py
6c4e40e verified
from flask import Flask, render_template, request, jsonify # type: ignore
from math import log2
import numpy as np # type: ignore
import os
import time
POSSIBLE_WORDS = set()
with open("wordle_words.txt", "r") as f:
for line in f:
POSSIBLE_WORDS.add(line.strip())
# There are {len(POSSIBLE_WORDS)} possible words loaded
def getFeedback(guess: str, answer: str) -> int:
"""
This function returns the feedback for a given guess and answer.
The feedback is a list of 5 integers, where 0 means grey, 1 means yellow, and 2 means green.
"""
chars = list(answer)
feedback = [2 if guess[i] == chars[i] else 0 for i in range(5)]
for i in range(5):
if feedback[i] == 0 and guess[i] in chars:
feedback[i] = 1 # yellow
chars[chars.index(guess[i])] = None # consume letter
optimized = 0
for f in feedback:
optimized = 3 * optimized + f
return optimized # optimized encoding of feedback stores guesses as decimal numbers
def filterCandidates(candidates: set, guess: str, feedback: int) -> set:
"""
This function filters the current candidates based on our guess and feedback.
"""
filtered = set(word for word in candidates if getFeedback(guess, word) == feedback)
return filtered
def bestGuessVectorized(candidates: set, allWords: set):
"""
This function returns the best guess for the next round.
It uses a vectorized approach to compute the best guess.
"""
bestWord = None
bestEntropy = -np.inf
L = len(candidates)
for guess in allWords:
counts = np.zeros(3**5)
for answer in candidates:
counts[getFeedback(guess, answer)] += 1
# Filter out zero counts to avoid log(0)
nonzero_counts = counts[counts > 0]
probabilities = nonzero_counts / L
entropy = -np.sum(probabilities * np.log2(probabilities))
if entropy > bestEntropy:
bestEntropy = entropy
bestWord = guess
return bestWord
def solveWordle(candidates: set[str], answer: str, maxGuesses: int = 6, allWords: set[str] = None):
"""
Solve Wordle by always starting with 'arise' and then using entropy-based scoring.
This has an average solve of 3.89 guesses.
"""
guesses = []
# --- First guess: fixed word ---
firstGuess = "arise"
guesses.append(firstGuess)
feedback = getFeedback(firstGuess, answer)
print(f"Round 1: guess = {firstGuess}, feedback = {feedback}")
if feedback == 242:
print("Solved in 1 guess!")
return guesses
candidates = filterCandidates(candidates, firstGuess, feedback)
print(f"After first guess, {len(candidates)} candidates remain")
# --- Remaining guesses ---
for i in range(2, maxGuesses + 1):
if len(candidates) == 0:
print("No candidates remaining!")
break
if len(candidates) == 1:
# If only one candidate remains, just guess it
nextGuess = list(candidates)[0]
else:
nextGuess = bestGuessVectorized(candidates, allWords if allWords else candidates)
guesses.append(nextGuess)
feedback = getFeedback(nextGuess, answer)
print(f"Round {i}: guess = {nextGuess}, feedback = {feedback}")
if feedback == 242:
print(f"Solved in {i} guesses!")
return guesses
candidates = filterCandidates(candidates, nextGuess, feedback)
print(f"After round {i}, {len(candidates)} candidates remain")
print(f"Failed to solve Wordle.")
return guesses
def playWordle(allWords: set[str], maxGuesses: int = 6):
"""
Interactive Wordle solver for real gameplay.
"""
candidates = allWords.copy()
guesses = []
print("Welcome to the Wordle Solver!")
print("Enter feedback as 5 characters: 'g' for green, 'y' for yellow, 'b' for black/grey")
print("Example: 'gybbb' means first letter is green, second is yellow, rest are black")
print()
# First guess: always "arise"
firstGuess = "arise"
print(f"Suggested guess 1: {firstGuess.upper()}")
while True:
feedback_str = input("Enter feedback for this guess (or 'q' to quit): ").strip().lower()
if feedback_str == 'q':
return
if len(feedback_str) == 5 and all(c in 'gyb' for c in feedback_str):
break
print("Invalid input. Please enter exactly 5 characters using only 'g', 'y', 'b'")
# Convert feedback to our integer format
feedback_list = []
for c in feedback_str:
if c == 'g': feedback_list.append(2)
elif c == 'y': feedback_list.append(1)
else: feedback_list.append(0)
feedback = 0
for f in feedback_list:
feedback = 3 * feedback + f
guesses.append(firstGuess)
if feedback == 242: # All green
print("Congratulations! Solved in 1 guess!")
return
candidates = filterCandidates(candidates, firstGuess, feedback)
print(f"Remaining candidates: {len(candidates)}")
# Remaining guesses
for round_num in range(2, maxGuesses + 1):
if len(candidates) == 0:
print("No valid words remain. There might be an error in the feedback.")
return
if len(candidates) == 1:
nextGuess = list(candidates)[0]
else:
nextGuess = bestGuessVectorized(candidates, allWords)
print(f"\nSuggested guess {round_num}: {nextGuess.upper()}")
while True:
feedback_str = input("Enter feedback for this guess (or 'q' to quit): ").strip().lower()
if feedback_str == 'q':
return
if len(feedback_str) == 5 and all(c in 'gyb' for c in feedback_str):
break
print("Invalid input. Please enter exactly 5 characters using only 'g', 'y', 'b'")
# Convert feedback to our integer format
feedback_list = []
for c in feedback_str:
if c == 'g': feedback_list.append(2)
elif c == 'y': feedback_list.append(1)
else: feedback_list.append(0)
feedback = 0
for f in feedback_list:
feedback = 3 * feedback + f
guesses.append(nextGuess)
if feedback == 242: # All green
print(f"Congratulations! Solved in {round_num} guesses!")
return
candidates = filterCandidates(candidates, nextGuess, feedback)
print(f"Remaining candidates: {len(candidates)}")
print("Reached maximum guesses. Better luck next time!")
def testSolverOnHistoricalWordles(allWords: set[str], testWords: list[str], maxGuesses: int = 6):
"""
Test the solver against a list of historical Wordle answers to measure success rate.
"""
results = []
total_guesses = 0
successes = 0
print(f"Testing solver on {len(testWords)} historical Wordle answers...")
print("=" * 60)
for i, word in enumerate(testWords, 1):
print(f"Test {i}/{len(testWords)}: {word.upper()}")
# Use the existing solveWordle function but capture results
candidates = allWords.copy()
guesses = []
solved = False
# First guess: always "arise"
firstGuess = "arise"
guesses.append(firstGuess)
feedback = getFeedback(firstGuess, word)
if feedback == 242: # All green
solved = True
num_guesses = 1
else:
candidates = filterCandidates(candidates, firstGuess, feedback)
# Remaining guesses
for round_num in range(2, maxGuesses + 1):
if len(candidates) == 0:
break
if len(candidates) == 1:
nextGuess = list(candidates)[0]
else:
nextGuess = bestGuessVectorized(candidates, allWords)
guesses.append(nextGuess)
feedback = getFeedback(nextGuess, word)
if feedback == 242: # All green
solved = True
num_guesses = round_num
break
candidates = filterCandidates(candidates, nextGuess, feedback)
else:
num_guesses = maxGuesses
# Record results
result = {
'word': word,
'solved': solved,
'guesses': num_guesses,
'guess_sequence': guesses[:num_guesses] if solved else guesses
}
results.append(result)
if solved:
successes += 1
total_guesses += num_guesses
print(f" ✓ Solved in {num_guesses} guesses: {' → '.join(g.upper() for g in result['guess_sequence'])}")
else:
print(f" ✗ Failed to solve: {' → '.join(g.upper() for g in result['guess_sequence'])}")
# Calculate and display statistics
success_rate = (successes / len(testWords)) * 100
avg_guesses = total_guesses / successes if successes > 0 else 0
print("\n" + "=" * 60)
print("RESULTS SUMMARY")
print("=" * 60)
print(f"Total tests: {len(testWords)}")
print(f"Successes: {successes}")
print(f"Failures: {len(testWords) - successes}")
print(f"Success rate: {success_rate:.1f}%")
print(f"Average guesses (for successful solves): {avg_guesses:.2f}")
# Guess distribution
guess_distribution = {i: 0 for i in range(1, maxGuesses + 1)}
for result in results:
if result['solved']:
guess_distribution[result['guesses']] += 1
print(f"\nGuess distribution:")
for guesses in range(1, maxGuesses + 1):
count = guess_distribution[guesses]
percentage = (count / successes) * 100 if successes > 0 else 0
print(f" {guesses} guesses: {count:3d} ({percentage:4.1f}%)")
return results
def loadHistoricalWordles(filename: str = "all_historical_wordles.txt"):
"""
Load historical Wordle answers from a file.
"""
try:
with open(filename, 'r') as f:
words = [line.strip().lower() for line in f if line.strip()]
print(f"Loaded {len(words)} historical Wordle answers from {filename}")
return words
except FileNotFoundError:
print(f"File {filename} not found. You can create it with historical Wordle answers.")
return []
app = Flask(__name__)
# Global state to track the game
game_state = {
'candidates': POSSIBLE_WORDS.copy(),
'current_word': 'arise',
'guesses': [],
'round': 0
}
@app.route('/')
def home():
return render_template('index.html')
@app.route('/get_next_word', methods=['POST'])
def get_next_word():
global game_state
data = request.json
feedback_string = data.get('feedback', '')
# Convert feedback string (byg) to our integer format
feedback_int = 0
for char in feedback_string:
if char == 'b':
feedback_int = feedback_int * 3 + 0 # absent
elif char == 'y':
feedback_int = feedback_int * 3 + 1 # present
elif char == 'g':
feedback_int = feedback_int * 3 + 2 # correct
# Filter candidates based on feedback
current_word = game_state['current_word']
game_state['candidates'] = filterCandidates(game_state['candidates'], current_word, feedback_int)
game_state['guesses'].append(current_word)
game_state['round'] += 1
# Get next best word
if len(game_state['candidates']) == 0:
return jsonify({'word': 'ERROR', 'candidates_remaining': 0})
elif len(game_state['candidates']) == 1:
next_word = list(game_state['candidates'])[0]
else:
next_word = bestGuessVectorized(game_state['candidates'], POSSIBLE_WORDS)
game_state['current_word'] = next_word
return jsonify({
'word': next_word.upper(),
'candidates_remaining': len(game_state['candidates'])
})
@app.route('/reset_game', methods=['POST'])
def reset_game():
global game_state
game_state = {
'candidates': POSSIBLE_WORDS.copy(),
'current_word': 'arise',
'guesses': [],
'round': 0
}
return jsonify({'word': 'ARISE'})
if __name__ == '__main__':
port = int(os.environ.get("PORT", 7860))
app.run(debug=True, host='0.0.0.0', port=port)