1
\$\begingroup\$

Refer to Wikipedia English for more context on Wordle.

This version behaves exactly like the standard Wordle game, except that it displays all possible choices in the current condition on the left side.

It accepts keyboard inputs, backspace to delete a letter, enter to confirm, and you can also select a choice from the left side.

You need this file: wordle-5.txt

If you want the icon:

enter image description here

Replace the file paths if necessary.

Interface:

enter image description here


Code

import ctypes
import re
import random
import sys
from collections import Counter, defaultdict
from math import prod
from string import ascii_lowercase
from pathlib import Path
from PyQt6.QtCore import *
from PyQt6.QtGui import *
from PyQt6.QtWidgets import *


ALIGNMENT = Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop
FLAG = Qt.ItemFlag

MAINSTYLE = """
    QListWidget {
        background: #161940;
        alternate-background-color: #1c1c5c;
        show-decoration-selected: 1;
    }
    QListWidget::item {
        border: 2px groove #204080;
        height: 20px;
        color: #89cff0;
    }
    QListWidget::item:hover {
        background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #244872, stop: 1 #00c0ff);
        color: #b2ffff;
        border: 2px groove #bfcde4;
    }
    QListWidget::item:selected {
        border: 3px groove #4080ff;
        border-radius: 6px;
        background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 0.75, x3: 0, y3: 1, stop: 0 #4080c0, stop: 0.75 #0080c0, stop: 1 #4080c0);
        color: #00ffff;
    }
    QScrollBar::vertical {
        border: 2px solid #204060;
        background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: 0 #004060, stop: 1 #00b2e4);
        position: absolute;
        width: 16px;
    }
    QScrollBar::handle:vertical {
        min-height: 32px;
        background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: 0 #00c0ff, stop: 1 #102372);
        margin: 18px;
    }
    QScrollBar::horizontal {
        border: 2px solid #204060;
        background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #004060, stop: 1 #00b2e4);
        position: absolute;
        height: 16px;
    }
    QScrollBar::handle:horizontal {
        min-width: 32px;
        background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #00c0ff, stop: 1 #102372);
        margin: 18px;
    }
    QGroupBox {
        border: 3px groove #204080;
        border-radius: 6px;
        background: #002050;
        color: #b2ffff;
    }
    QLabel {
        color: #00aeef;
        font: 'Noto Serif';
    }
    QMainWindow {
        background: #00122b;
    }
    QPushButton {
        border: 3px outset #4080c0;
        border-radius: 4px;
        background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 0.75, x3: 0, y3: 1, stop: 0 #4080c0, stop: 0.75 #00aae4, stop: 1 #4080c0);
        color: blue;
    }
    QPushButton:hover {
        background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 0.75, x3: 0, y3: 1, stop: 0 #4080c0, stop: 0.75 #b2ffff, stop: 1 #4080c0);
    }
    QPushButton:pressed {
        border: 3px inset #4080c0;
        background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 0.75, x3: 0, y3: 1, stop: 0 #4080c0, stop: 0.75 #005572, stop: 1 #4080c0);
        color: #00ffff;
    }
    QLineEdit {
        border: 2px solid #306090;
        border-radius: 6px;
        background: #1d2252;
        color: #b2ffff;
    }
    QMessageBox {
        background: #00122b;
    }
"""

LETTERS = {
    Qt.Key.Key_A: 'a',
    Qt.Key.Key_B: 'b',
    Qt.Key.Key_C: 'c',
    Qt.Key.Key_D: 'd',
    Qt.Key.Key_E: 'e',
    Qt.Key.Key_F: 'f',
    Qt.Key.Key_G: 'g',
    Qt.Key.Key_H: 'h',
    Qt.Key.Key_I: 'i',
    Qt.Key.Key_J: 'j',
    Qt.Key.Key_K: 'k',
    Qt.Key.Key_L: 'l',
    Qt.Key.Key_M: 'm',
    Qt.Key.Key_N: 'n',
    Qt.Key.Key_O: 'o',
    Qt.Key.Key_P: 'p',
    Qt.Key.Key_Q: 'q',
    Qt.Key.Key_R: 'r',
    Qt.Key.Key_S: 's',
    Qt.Key.Key_T: 't',
    Qt.Key.Key_U: 'u',
    Qt.Key.Key_V: 'v',
    Qt.Key.Key_W: 'w',
    Qt.Key.Key_X: 'x',
    Qt.Key.Key_Y: 'y',
    Qt.Key.Key_Z: 'z'
}

WORDLIST = Path('D:/corpus/wordle-5.txt').read_text().splitlines()
frequencies = Counter(''.join(WORDLIST))

QLABEL_BASE = "QLabel {{background: #{}; color: #00aeef; border: 3px inset #5252cc; font: 'Noto Serif';}}"
QLABEL_DEFAULT = "QLabel {background: #002050; color: #00aeef; border: 3px outset #5252cc; font: 'Noto Serif';}"

def get_weight(word):
    occurences = Counter()
    weight = 0
    for letter in word:
        weight += frequencies[letter] * 0.5**occurences[letter]
        occurences[letter]+=1
    return weight

def sort_by_weight(words):
    return sorted(words, key=lambda x: - get_weight(x))

WORDLIST = sort_by_weight(WORDLIST)

class Cell(QLabel):
    representation = {
        'Absent': QLABEL_BASE.format('ff4200'),
        'Correct': QLABEL_BASE.format('55ff00'),
        'Present': QLABEL_BASE.format('ccff00')
    }
    def __init__(self):
        super().__init__()
        self.setFixedSize(64, 64)
        self.setFont(Font(36))
        self.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self.setStyleSheet(QLABEL_DEFAULT)
    
    def represent(self, state):
        self.setStyleSheet(Cell.representation[state])
        self.repaint()
    
    def reset(self):
        self.setStyleSheet(QLABEL_DEFAULT)
        self.setText('')

class Font(QFont):
    def __init__(self, size):
        super().__init__()
        self.setFamily("Noto Serif")
        self.setStyleHint(QFont.StyleHint.Times)
        self.setStyleStrategy(QFont.StyleStrategy.PreferAntialias)
        self.setPointSize(size)
        self.setHintingPreference(QFont.HintingPreference.PreferFullHinting)

class LeftPane(QListWidget):
    def __init__(self):
        super(LeftPane, self).__init__()
        font = Font(9)
        self.setFont(font)
        self.setDragDropMode(QAbstractItemView.DragDropMode.NoDragDrop)
        self.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
        self.setAlternatingRowColors(True)
        self.setFixedWidth(80)
        self.setFocusPolicy(Qt.FocusPolicy.NoFocus)
        self.itemClicked.connect(self.onItemClicked)
    
    def onItemClicked(self, item):
        choice = item.text()
        board = self.window().board
        if board.tries < 6:
            board.guess = list(choice)
            for i, l in zip(range(5), choice):
                board.grid.itemAtPosition(board.tries, i).widget().setText(l)
            board.evaluate()

class Board(QGroupBox):
    def __init__(self):
        super().__init__()
        self.tries = 0
        self.column = 0
        self.answer = random.choice(WORDLIST)
        self.guess = []
        self.agent = Agent()
        self.grid = QGridLayout(self)
        self.won = False
        for row in range(6):
            for column in range(5):
                self.grid.addWidget(Cell(), row, column)
        self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
    
    def judge(self, guess):
        judgement = []
        for a, b in zip(self.answer, guess):
            if b not in self.answer:
                judgement.append('Absent')
            elif a == b:
                judgement.append('Correct')
            else:
                judgement.append('Present')
        return judgement
    
    def represent(self, judgement):
        for column, state in zip(range(5), judgement):
            self.grid.itemAtPosition(self.tries, column).widget().represent(state)
    
    def reset(self):
        self.answer = random.choice(WORDLIST)
        self.agent = Agent()
        self.guess.clear()
        for row in range(self.tries):
            for column in range(5):
                self.grid.itemAtPosition(row, column).widget().reset()
        self.column = 0
        self.tries = 0
        self.window().leftpane.clear()
        self.window().leftpane.addItems(WORDLIST)
    
    def evaluate(self):
        if self.tries < 6:
            if ''.join(self.guess) not in WORDLIST:
                msg = QMessageBox(self.window())
                msg.setIcon(QMessageBox.Icon.Warning)
                msg.setText('Inputted word is not present in the wordlist')
                msg.setWindowTitle('Invalid Input')
                msg.setStandardButtons(QMessageBox.StandardButton.Ok)
                msg.exec()
                return
            if len(self.guess) == 5:
                judgement = self.judge(self.guess)
                self.agent.update(list(zip(self.guess, judgement)))
                self.window().leftpane.clear()
                self.window().leftpane.addItems(self.agent.pool)
                self.guess.clear()
                self.column = 0
                self.represent(judgement)
                self.tries += 1
                if judgement == ['Correct'] * 5:
                    if ''.join(self.guess) not in WORDLIST:
                        msg = QMessageBox(self.window())
                        msg.setIcon(QMessageBox.Icon.Information)
                        msg.setText('Congratulations! You won!')
                        msg.setWindowTitle('Success')
                        msg.setStandardButtons(QMessageBox.StandardButton.Ok)
                        msg.exec()
                    self.reset()
        if self.tries == 6:
            msg = QMessageBox(self.window())
            msg.setIcon(QMessageBox.Icon.Information)
            msg.setText('Condolences... You lost...')
            msg.setWindowTitle('Failure')
            msg.setStandardButtons(QMessageBox.StandardButton.Ok)
            msg.exec()
            self.reset()
    
    
    def keyPressEvent(self, e: QKeyEvent) -> None:
        if e.key() in LETTERS:
            letter = LETTERS[e.key()]
            if self.column < 5:
                self.guess.append(letter)
                self.grid.itemAtPosition(self.tries, self.column).widget().setText(letter)
                self.column += 1
        elif e.key() == Qt.Key.Key_Backspace:
            if self.column > 0:
                self.guess.pop(-1)
                self.column -= 1
                self.grid.itemAtPosition(self.tries, self.column).widget().reset()
        elif e.key() == Qt.Key.Key_Return:
            self.evaluate()

class Agent:
    def __init__(self):
        self.pool = WORDLIST.copy()
        self.letters = ascii_lowercase
        self.pattern = ''
        self.present = set()
    
    def update(self, conditions):
        pool = list()
        self.process(conditions)
        for word in self.pool:
            if re.match(self.pattern, word):
                if not self.present or all(l in word for l in self.present):
                    pool.append(word)
        self.pool = sort_by_weight(pool)
    
    def process(self, conditions):
        for letter, state in conditions:
            if state == 'Absent':
                self.letters = self.letters.replace(letter, '')
        
        pattern = []
        self.present.clear()
        for letter, state in conditions:
            if state == 'Absent':
                pattern.append('[' + self.letters + ']')
            elif state == 'Present':
                pattern.append('[' + self.letters.replace(letter, '') + ']')
                self.present.add(letter)
            elif state == 'Correct':
                pattern.append(letter)
        
        self.pattern = '^' + ''.join(pattern) + '$'

class Window(QMainWindow):
    def __init__(self):
        super().__init__()
        frame = self.frameGeometry()
        center = self.screen().availableGeometry().center()
        frame.moveCenter(center)
        self.move(frame.topLeft())
        font = Font(10)
        self.setFont(font)
        self.centralwidget = QWidget(self)
        self.setCentralWidget(self.centralwidget)
        self.setStyleSheet(MAINSTYLE)
        self.hbox = QHBoxLayout(self.centralwidget)
        self.leftpane = LeftPane()
        self.leftpane.addItems(WORDLIST)
        self.hbox.addWidget(self.leftpane)
        self.board = Board()
        self.hbox.addWidget(self.board)
        self.setWindowTitle('Wordle')

if __name__ == '__main__':
    app = QApplication([])
    app.setStyle('Fusion')
    window = Window()
    window.show()
    window.setFixedSize(window.size())
    ICON = QIcon(str(Path(__file__).parent) + '/icons/icon.png')
    window.setWindowIcon(ICON)
    ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID("Wordle")
    sys.exit(app.exec())

This is my first game so any help would be appreciated.


Update

I just updated the code so that the suggestions are sorted by their weight descending, the weight is calculated as the sum of the letter frequencies of the letters in the word, but the frequency counts half as much as the previous appearance of the same letter for each consecutive appearances of the same letter.

The above method ensures the more frequent and different letters the word has, the bigger the weight, so as to trigger better checks.

Video Example (Google Drive)

If I have time I will improve the algorithm further and figure out how to hook a separate thread to the program to run a AI that automates the testing and to train the wordle solving model.

\$\endgroup\$
1
  • \$\begingroup\$ To me, it seems like a wonderful implementation. \$\endgroup\$
    – yrjarv
    May 2, 2022 at 11:08

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Browse other questions tagged or ask your own question.