diff --git a/driver.rb b/driver.rb new file mode 100644 index 0000000..0e68720 --- /dev/null +++ b/driver.rb @@ -0,0 +1,12 @@ +#!/usr/bin/env ruby + +require_relative "lib/game" + +game = Game.new + +trap "SIGINT" do + game.exit! + exit 130 +end + +game.play diff --git a/lib/board/wordle_unlimited.rb b/lib/board/wordle_unlimited.rb new file mode 100644 index 0000000..5408236 --- /dev/null +++ b/lib/board/wordle_unlimited.rb @@ -0,0 +1,91 @@ +require "capybara" +require "selenium-webdriver" +require "webdrivers" + +module Board + class WordleUnlimited + attr :guesses, :session + + def initialize + @guesses = [] + @session = Capybara::Session.new(:selenium_chrome) + end + + def start + session.visit("https://www.wordleunlimited.com/") + end + + def answer(guess) + guess.chars.map(&:upcase).each { |letter| click(letter) } + click("Enter") + + answer_invalid? ? clear_answer! : @guesses << guess + end + + def reset! + @guesses = [] + click("Enter") + end + + def winner? + session.has_text?(:visible, "Winner!", wait: 0) + end + + def loser? + session.has_text?(:visible, "You lost!", wait: 0) + end + + def allowed_letters + (exact_letters + elsewhere_letters).uniq + end + + def bad_letters + session + .find_all('div.Game-keyboard-button.letter-absent', wait: 0) + .map(&:text) + .map(&:downcase) + end + + def close! + @session.quit + end + + def first_guess? + guesses.empty? + end + + def correct_answer + session.find('div.feedback > div > b').text + end + + private + + def exact_letters + session + .find_all('div.Game-keyboard-button.letter-correct', wait: 0) + .map(&:text) + .map(&:downcase) + end + + def elsewhere_letters + session + .find_all('div.Game-keyboard-button.letter-elsewhere', wait: 0) + .map(&:text) + .map(&:downcase) + end + + def answer_invalid? + session.has_text?(:visible, "Not a valid word", wait: 2) + end + + def click(key) + session + .find('div.Game-keyboard-button', text: /\A#{key}\Z/, wait: 0) + .click() + end + + def clear_answer! + 5.times { click("⌫") } + end + end +end diff --git a/lib/dictionary/dictionary.rb b/lib/dictionary/dictionary.rb new file mode 100644 index 0000000..d6144ac --- /dev/null +++ b/lib/dictionary/dictionary.rb @@ -0,0 +1,13 @@ +module Dictionary + class Dictionary + attr_accessor :file + + def initialize(file: "dictionary.txt") + @file = file + end + + def words + @words ||= File.readlines(file).map(&:strip) + end + end +end diff --git a/lib/game.rb b/lib/game.rb new file mode 100644 index 0000000..106e74e --- /dev/null +++ b/lib/game.rb @@ -0,0 +1,69 @@ +require "debug" + +require_relative "outcome" + +require_relative "board/wordle_unlimited" +require_relative "dictionary/dictionary" + +require_relative "strategy/naive" +require_relative "strategy/wheel_of_fortune" + +class Game + attr_reader :board, :dictionary, :start_strategy, :strategy, :outcomes + def initialize( + board: Board::WordleUnlimited, + dictionary: Dictionary::Dictionary, + start_strategy: Strategy::WheelOfFortune, + strategy: Strategy::Naive + ) + @board = board.new + @dictionary = dictionary.new + @start_strategy = start_strategy.new(dictionary: @dictionary) + @strategy = strategy.new(dictionary: @dictionary) + + @outcomes = [] + end + + def play + board.start + + loop do + guess_strategy = board.first_guess? ? start_strategy : strategy + + guess = guess_strategy + .guess( + good_letters: board.allowed_letters, + bad_letters: board.bad_letters, + guesses: board.guesses, + ) + + board.answer(guess) + + if board.winner? + @outcomes << Outcome.new( + state: :win, + correct: board.correct_answer, + guesses: board.guesses, + ) + board.reset! + elsif board.loser? + @outcomes << Outcome.new( + state: :loss, + correct: board.correct_answer, + guesses: board.guesses, + ) + board.reset! + end + end + end + + def exit! + board.close! + + puts + puts "=" * 80 + puts "Won: #{outcomes.select(&:win?).count}" + puts "Lost: #{outcomes.select(&:loss?).count}" + puts "=" * 80 + end +end diff --git a/lib/outcome.rb b/lib/outcome.rb new file mode 100644 index 0000000..bb3d029 --- /dev/null +++ b/lib/outcome.rb @@ -0,0 +1,17 @@ +class Outcome + attr :correct, :guesses, :state + + def initialize(state:, correct:, guesses:) + @state = state + @correct = correct + @guesses = guesses + end + + def win? + state == :win + end + + def loss? + state == :loss + end +end diff --git a/lib/strategy/naive.rb b/lib/strategy/naive.rb new file mode 100644 index 0000000..c55280a --- /dev/null +++ b/lib/strategy/naive.rb @@ -0,0 +1,19 @@ +module Strategy + class Naive + WORD_SIZE = 5 + + attr :dictionary + + def initialize(dictionary:) + @dictionary = dictionary + end + + def guess(good_letters:, bad_letters:, **args) + dictionary.words + .select { |word| word.length == WORD_SIZE } + .reject { |word| bad_letters.any? { |letter| word.chars.include?(letter) } } + .select { |word| (good_letters - word.chars).length == 0 } + .sample + end + end +end diff --git a/lib/strategy/wheel_of_fortune.rb b/lib/strategy/wheel_of_fortune.rb new file mode 100644 index 0000000..6008b34 --- /dev/null +++ b/lib/strategy/wheel_of_fortune.rb @@ -0,0 +1,24 @@ +module Strategy + class WheelOfFortune + WORD_SIZE = 5 + + attr :dictionary + + def initialize(dictionary:) + @dictionary = dictionary + end + + def guess(**args) + dictionary.words + .select { |word| word.length == WORD_SIZE } + .select { |word| (start_letters - word.chars).length == 1 } + .sample + end + + private + + def start_letters + %w(r s t l n e) + end + end +end