ruby-wordle 0.9.2 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +19 -0
- data/README.md +66 -36
- data/bin/ruby-wordle +3 -21
- data/lib/ruby-wordle/cli.rb +426 -0
- data/lib/ruby-wordle/dictionary.rb +74 -0
- data/lib/ruby-wordle/feedback.rb +73 -0
- data/lib/ruby-wordle/game.rb +84 -0
- data/lib/ruby-wordle/solver.rb +92 -0
- data/lib/ruby-wordle/style.rb +56 -0
- data/lib/ruby-wordle/version.rb +1 -1
- data/lib/ruby-wordle.rb +7 -0
- data/word-lists/5-letters/word-list-basic-uniq-letters.txt +1 -8
- data/word-lists/5-letters/word-list-basic.txt +1 -9
- data/word-lists/5-letters/word-list-uniq-letters.txt +1 -0
- data/word-lists/5-letters/word-list.txt +1 -0
- metadata +25 -29
- data/.gitignore +0 -58
- data/Gemfile +0 -3
- data/Rakefile +0 -1
- data/img/ruby-wordle-play.png +0 -0
- data/img/ruby-wordle-solver.png +0 -0
- data/lib/ruby-wordle/script-filter.rb +0 -48
- data/lib/ruby-wordle/script-play.rb +0 -124
- data/lib/ruby-wordle/script-solver.rb +0 -179
- data/lib/ruby-wordle/script-utils.rb +0 -23
- data/ruby-wordle.gemspec +0 -21
- data/word-lists/word-list-basic.txt +0 -12937
- data/word-lists/word-list-original.txt +0 -370103
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
#! /usr/bin/env ruby
|
|
2
|
-
|
|
3
|
-
NUM_LETTERS = 5
|
|
4
|
-
|
|
5
|
-
HOME_DIR = File.expand_path(File.join(File.dirname(__FILE__), "..", ".."))
|
|
6
|
-
|
|
7
|
-
original_word_list = File.readlines("#{HOME_DIR}/word-lists/word-list-original.txt")
|
|
8
|
-
basic_word_list = File.readlines("#{HOME_DIR}/word-lists/word-list-basic.txt")
|
|
9
|
-
n_letter_dir = "#{HOME_DIR}/word-lists/#{NUM_LETTERS}-letters"
|
|
10
|
-
|
|
11
|
-
unless Dir.exist? n_letter_dir
|
|
12
|
-
Dir.mkdir File.expand_path(n_letter_dir)
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
n_letter_word_file = File.new(File.join(n_letter_dir, "word-list.txt"), "w")
|
|
16
|
-
uniq_n_letter_word_file = File.new(File.join(n_letter_dir, "word-list-uniq-letters.txt"), "w")
|
|
17
|
-
basic_n_letter_word_file = File.new(File.join(n_letter_dir, "word-list-basic.txt"), "w")
|
|
18
|
-
uniq_basic_n_letter_word_file = File.new(File.join(n_letter_dir, "word-list-basic-uniq-letters.txt"), "w")
|
|
19
|
-
|
|
20
|
-
original_word_list.sort!.uniq!
|
|
21
|
-
original_word_list.each do |line|
|
|
22
|
-
chars = line.strip.split(//)
|
|
23
|
-
if chars.size == NUM_LETTERS
|
|
24
|
-
line.downcase!
|
|
25
|
-
n_letter_word_file.write line
|
|
26
|
-
if chars.uniq.size == chars.size
|
|
27
|
-
uniq_n_letter_word_file.write line
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
basic_word_list.sort!.uniq!
|
|
33
|
-
basic_word_list.each do |line|
|
|
34
|
-
chars = line.strip.split(//)
|
|
35
|
-
if chars.size == NUM_LETTERS
|
|
36
|
-
line.downcase!
|
|
37
|
-
basic_n_letter_word_file.write line
|
|
38
|
-
if chars.uniq.size == chars.size
|
|
39
|
-
uniq_basic_n_letter_word_file.write line
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
n_letter_word_file.close
|
|
45
|
-
uniq_n_letter_word_file.close
|
|
46
|
-
basic_n_letter_word_file.close
|
|
47
|
-
uniq_basic_n_letter_word_file.close
|
|
48
|
-
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
#! /usr/bin/env ruby
|
|
2
|
-
|
|
3
|
-
require "readline"
|
|
4
|
-
require "script-solver"
|
|
5
|
-
|
|
6
|
-
module RubyWordlePlay
|
|
7
|
-
NUM_LETTERS = 5
|
|
8
|
-
NUM_ATTEMPTS = 6
|
|
9
|
-
|
|
10
|
-
LPAD = " " * 1
|
|
11
|
-
PROMPT = "〉".bold.red
|
|
12
|
-
|
|
13
|
-
@word_list_basic = File.readlines("#{HOME_DIR}/word-lists/#{NUM_LETTERS}-letters/word-list-basic.txt").map(&:strip)
|
|
14
|
-
@word_list_original = File.readlines("#{HOME_DIR}/word-lists/#{NUM_LETTERS}-letters/word-list.txt").map(&:strip)
|
|
15
|
-
@word = @word_list_basic.sample
|
|
16
|
-
@not_used = []
|
|
17
|
-
@not_used_here = {1 => [], 2 => [], 3 => [], 4 => [], 5 => []}
|
|
18
|
-
@letters_known = ".....".split(//)
|
|
19
|
-
|
|
20
|
-
def self.check_dict(attempt)
|
|
21
|
-
@word_list_original.include?(attempt)
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def self.evaluate(attempt)
|
|
25
|
-
puts
|
|
26
|
-
@letters_known = attempt.split(//)
|
|
27
|
-
correct = []
|
|
28
|
-
wrong = []
|
|
29
|
-
@word.split(//).each_with_index do |l, i|
|
|
30
|
-
if attempt[i] == l
|
|
31
|
-
correct << i
|
|
32
|
-
else
|
|
33
|
-
wrong << i
|
|
34
|
-
@letters_known[i] = '.'
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
attempt.split(//).each_with_index do |l, i|
|
|
38
|
-
if correct.include?(i)
|
|
39
|
-
print " #{@word[i]} ".upcase.bg_green
|
|
40
|
-
else
|
|
41
|
-
elsewhere = wrong.map{|i|@word[i]}.index(l)
|
|
42
|
-
if elsewhere
|
|
43
|
-
print " #{l} ".upcase.bg_brown
|
|
44
|
-
@not_used_here[i + 1] << l unless @not_used_here[i + 1].include? l
|
|
45
|
-
else
|
|
46
|
-
print " ".bg_gray
|
|
47
|
-
@not_used << attempt[i] if /[a-z]/ =~ attempt[i]
|
|
48
|
-
@not_used.uniq!
|
|
49
|
-
@not_used.sort!
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
puts
|
|
54
|
-
correct.size == NUM_LETTERS
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def self.get_help
|
|
58
|
-
letters_used = []
|
|
59
|
-
@not_used_here.each do |k, v|
|
|
60
|
-
v.each do |r|
|
|
61
|
-
letters_used << [k - 1, r]
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
RubyWordleSolver.solve_wordle(@letters_known.join, letters_used, @not_used, :basic, @word)
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def self.ask_for_attempt(str)
|
|
68
|
-
puts
|
|
69
|
-
print " #{str} ".bold.bg_cyan + " "
|
|
70
|
-
attempt = Readline.readline(LPAD + PROMPT, true).strip.downcase
|
|
71
|
-
if attempt == "quit" || attempt == "q"
|
|
72
|
-
exit
|
|
73
|
-
elsif attempt == "help" || attempt == "h" || attempt == "?"
|
|
74
|
-
get_help
|
|
75
|
-
attempt = ask_for_attempt(str)
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
unless /\A[a-z]{5}\z/ =~ attempt
|
|
79
|
-
puts
|
|
80
|
-
puts LPAD + "Sorry,".red + " the input format is not right"
|
|
81
|
-
puts
|
|
82
|
-
attempt = ask_for_attempt(str)
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
unless check_dict(attempt)
|
|
86
|
-
puts
|
|
87
|
-
puts LPAD + "Sorry,".red + " it's not listed in the dictionary"
|
|
88
|
-
puts
|
|
89
|
-
attempt = ask_for_attempt(str)
|
|
90
|
-
end
|
|
91
|
-
attempt
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def self.draw_decorated_border
|
|
95
|
-
puts
|
|
96
|
-
puts (' '.bg_green * 5 + ' '.bg_gray * 3 + ' '.bg_brown * 5 + ' '.bg_gray * 3) * 4
|
|
97
|
-
puts
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
def play_wordle
|
|
101
|
-
draw_decorated_border;
|
|
102
|
-
puts "Input a word of 5 letters and press \"↵\"".cyan.bold
|
|
103
|
-
puts "Type \"?↵\" for help, \"quit↵\" to exit".gray
|
|
104
|
-
|
|
105
|
-
NUM_ATTEMPTS.times do |i|
|
|
106
|
-
attempt = ask_for_attempt("#{i + 1}/#{NUM_ATTEMPTS}")
|
|
107
|
-
result = evaluate(attempt)
|
|
108
|
-
puts
|
|
109
|
-
if result
|
|
110
|
-
puts "C O R R E C T !".upcase.bg_magenta
|
|
111
|
-
exit
|
|
112
|
-
elsif i + 1 == NUM_ATTEMPTS
|
|
113
|
-
puts " ANSWER: #{@word} ".upcase.bg_red
|
|
114
|
-
exit
|
|
115
|
-
else
|
|
116
|
-
puts @not_used.uniq.map{|l| " #{l.upcase} "}.join.bg_black if !@not_used.empty?
|
|
117
|
-
puts
|
|
118
|
-
end
|
|
119
|
-
end
|
|
120
|
-
draw_decorated_border
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
module_function :play_wordle
|
|
124
|
-
end
|
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
#! /usr/bin/env ruby
|
|
2
|
-
|
|
3
|
-
HOME_DIR = File.expand_path(File.join(File.dirname(__FILE__), "..", ".."))
|
|
4
|
-
|
|
5
|
-
require "readline"
|
|
6
|
-
require "script-utils"
|
|
7
|
-
|
|
8
|
-
module RubyWordleSolver
|
|
9
|
-
NUM_LETTERS = 5
|
|
10
|
-
MAX_SUGGESTIONS = 128
|
|
11
|
-
|
|
12
|
-
LPAD = " " * 7
|
|
13
|
-
PROMPT = "〉".bold.red
|
|
14
|
-
|
|
15
|
-
@word_list = File.readlines("#{HOME_DIR}/word-lists/#{NUM_LETTERS}-letters/word-list.txt").map(&:strip)
|
|
16
|
-
@word_list_basic = File.readlines("#{HOME_DIR}/word-lists/#{NUM_LETTERS}-letters/word-list-basic.txt").map(&:strip)
|
|
17
|
-
|
|
18
|
-
@basic_word_list = {}
|
|
19
|
-
File.readlines("#{HOME_DIR}/word-lists/#{NUM_LETTERS}-letters/word-list-basic.txt").map(&:strip).each do |b|
|
|
20
|
-
@basic_word_list[b] = true
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
@basic_word_uniq_list = {}
|
|
24
|
-
File.readlines("#{HOME_DIR}/word-lists/#{NUM_LETTERS}-letters/word-list-basic-uniq-letters.txt").map(&:strip).each do |b|
|
|
25
|
-
@basic_word_uniq_list[b] = true
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
@letters_known = nil
|
|
29
|
-
@letters_used = nil
|
|
30
|
-
@letters_not_used = nil
|
|
31
|
-
|
|
32
|
-
def self.get_letters_known
|
|
33
|
-
puts
|
|
34
|
-
puts "STEP 1".bold.bg_green + " Input " + "known letters".underline + " using alphabets and dots"
|
|
35
|
-
puts LPAD + "Then press \"↵\" (Type \"quit↵\" to exit)"
|
|
36
|
-
if @letters_known
|
|
37
|
-
puts LPAD + "Last time: #{@letters_known.bg_green}"
|
|
38
|
-
else
|
|
39
|
-
puts LPAD + "Example: #{'s...h'.bg_green}"
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
letters_known = Readline.readline(LPAD + PROMPT, true).strip.downcase
|
|
43
|
-
|
|
44
|
-
exit if letters_known == "quit" || letters_known == "q"
|
|
45
|
-
unless /\A[a-z\.]{5}\z/ =~ letters_known
|
|
46
|
-
puts LPAD + "Sorry,".red + " the input format is not right"
|
|
47
|
-
letters_known = get_letters_known
|
|
48
|
-
end
|
|
49
|
-
letters_known
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def self.get_letters_used
|
|
53
|
-
puts
|
|
54
|
-
puts "STEP 2".bold.bg_brown + " Input pairs of " + "a position and a letter".underline
|
|
55
|
-
puts LPAD + "Then press \"↵\" (Type \"quit↵\" to exit)"
|
|
56
|
-
if @letters_used
|
|
57
|
-
puts LPAD + "Last time: #{@letters_used.map{|p, l| (p + 1).to_s + l}.join(' ').bg_brown}"
|
|
58
|
-
else
|
|
59
|
-
puts LPAD + "Example: #{'1g 2e 5a'.bg_brown}"
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
letters_used = Readline.readline(LPAD + PROMPT, true).strip.downcase
|
|
63
|
-
|
|
64
|
-
exit if letters_used == "quit" || letters_used == "q"
|
|
65
|
-
if letters_used.strip == ""
|
|
66
|
-
array_letters_used = []
|
|
67
|
-
elsif /\A(?:[1-5][a-z][\s,]*)+\z/ =~ letters_used
|
|
68
|
-
array_letters_used = []
|
|
69
|
-
letters_used.gsub(/\s+/, "").split(//).each_slice(2) do |k, v|
|
|
70
|
-
array_letters_used << [k.to_i - 1, v]
|
|
71
|
-
end
|
|
72
|
-
else
|
|
73
|
-
puts LPAD + "Sorry,".red + " the input format is not right"
|
|
74
|
-
array_letters_used = get_letters_used
|
|
75
|
-
end
|
|
76
|
-
array_letters_used
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
def self.get_letters_not_used
|
|
80
|
-
puts
|
|
81
|
-
puts "STEP 3".bold.bg_gray + " Input " + "letters not used".underline + " in the word"
|
|
82
|
-
puts LPAD + "Then press \"↵\" (Type \"quit↵\" to exit)"
|
|
83
|
-
if @letters_not_used
|
|
84
|
-
puts LPAD + "Last time: #{@letters_not_used.join.bg_gray}"
|
|
85
|
-
else
|
|
86
|
-
puts LPAD + "Example: #{'ieagh'.bg_gray}"
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
letters_not_used = Readline.readline(LPAD + PROMPT, true).strip.downcase
|
|
90
|
-
|
|
91
|
-
exit if letters_not_used == "quit" || letters_not_used == "q"
|
|
92
|
-
if letters_not_used.strip == ""
|
|
93
|
-
array_letters_not_used = []
|
|
94
|
-
elsif /\A[a-z\s]+\z/ =~ letters_not_used
|
|
95
|
-
array_letters_not_used = letters_not_used.split(//)
|
|
96
|
-
else
|
|
97
|
-
puts LPAD + "Sorry,".red + " the input format is not right"
|
|
98
|
-
array_letters_not_used = get_letters_not_used
|
|
99
|
-
end
|
|
100
|
-
array_letters_not_used
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
def self.draw_decorated_border
|
|
104
|
-
puts; puts (' '.bg_green * 5 + ' '.bg_gray * 3 + ' '.bg_brown * 5 + ' '.bg_gray * 3) * 4; puts
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
def solve_wordle(letters_known, letters_used, letters_not_used, mode = :original, answer = nil)
|
|
108
|
-
word_list = mode == :original ? @word_list : @word_list_basic
|
|
109
|
-
indices_unknown_letters = (0 ... letters_known.length).find_all { |i| letters_known[i, 1] == '.' }
|
|
110
|
-
|
|
111
|
-
word_list_a = word_list.select do |word|
|
|
112
|
-
Regexp.compile(letters_known) =~ word
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
word_list_b = word_list_a.select do |word|
|
|
116
|
-
word_letters = word.split(//)
|
|
117
|
-
letters_in_candidate = word_letters.values_at(*indices_unknown_letters) - letters_used.map{|l| l[1]}
|
|
118
|
-
cond1 = letters_used.all? do |position, letter|
|
|
119
|
-
word_letters[position] != letter
|
|
120
|
-
end
|
|
121
|
-
cond2 = (letters_used.map{|l| l[1]} - word_letters).empty?
|
|
122
|
-
cond3 = (letters_in_candidate - letters_not_used).size == letters_in_candidate.size
|
|
123
|
-
cond1 && cond2 && cond3
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
if answer
|
|
127
|
-
word_list_b.delete(answer)
|
|
128
|
-
reduced_list = word_list_b.sample(MAX_SUGGESTIONS - 1)
|
|
129
|
-
reduced_list << answer
|
|
130
|
-
else
|
|
131
|
-
reduced_list = word_list_b.sample(MAX_SUGGESTIONS)
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
results = reduced_list.sort.map do |word|
|
|
135
|
-
if @basic_word_uniq_list[word]
|
|
136
|
-
mode == :original ? word.red.bold : word.red
|
|
137
|
-
elsif @basic_word_list[word]
|
|
138
|
-
mode == :original ? word.blue.bold : word.blue
|
|
139
|
-
else
|
|
140
|
-
word
|
|
141
|
-
end
|
|
142
|
-
end
|
|
143
|
-
puts
|
|
144
|
-
results.each_with_index do |word, i|
|
|
145
|
-
if (i + 1) % 8 == 0 || results.size == i + 1
|
|
146
|
-
print word + "\n"
|
|
147
|
-
else
|
|
148
|
-
print word + " "
|
|
149
|
-
end
|
|
150
|
-
end
|
|
151
|
-
word_list_b
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
def run_interactively
|
|
155
|
-
draw_decorated_border
|
|
156
|
-
@letters_known = get_letters_known
|
|
157
|
-
@letters_used = get_letters_used
|
|
158
|
-
@letters_not_used = get_letters_not_used
|
|
159
|
-
results = solve_wordle(@letters_known, @letters_used, @letters_not_used)
|
|
160
|
-
draw_decorated_border
|
|
161
|
-
|
|
162
|
-
exit if results.size < 2
|
|
163
|
-
|
|
164
|
-
puts " ? ".bold.bg_magenta + " Press \"↵\" to continue."
|
|
165
|
-
puts LPAD + "Type \"quit↵\" to exit."
|
|
166
|
-
|
|
167
|
-
response = Readline.readline(LPAD + PROMPT, true).strip.downcase
|
|
168
|
-
|
|
169
|
-
if response == "quit" || response == "q"
|
|
170
|
-
exit
|
|
171
|
-
else
|
|
172
|
-
run_interactively
|
|
173
|
-
end
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
module_function :run_interactively
|
|
177
|
-
module_function :solve_wordle
|
|
178
|
-
end
|
|
179
|
-
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
class String
|
|
2
|
-
def black; "\e[30m#{self}\e[0m" end
|
|
3
|
-
def red; "\e[31m#{self}\e[0m" end
|
|
4
|
-
def green; "\e[32m#{self}\e[0m" end
|
|
5
|
-
def brown; "\e[33m#{self}\e[0m" end
|
|
6
|
-
def blue; "\e[34m#{self}\e[0m" end
|
|
7
|
-
def magenta; "\e[35m#{self}\e[0m" end
|
|
8
|
-
def cyan; "\e[36m#{self}\e[0m" end
|
|
9
|
-
def gray; "\e[37m#{self}\e[0m" end
|
|
10
|
-
def bg_black; "\e[40m#{self}\e[0m" end
|
|
11
|
-
def bg_red; "\e[41m#{self}\e[0m" end
|
|
12
|
-
def bg_green; "\e[42m#{self}\e[0m" end
|
|
13
|
-
def bg_brown; "\e[43m#{self}\e[0m" end
|
|
14
|
-
def bg_blue; "\e[44m#{self}\e[0m" end
|
|
15
|
-
def bg_magenta; "\e[45m#{self}\e[0m" end
|
|
16
|
-
def bg_cyan; "\e[46m#{self}\e[0m" end
|
|
17
|
-
def bg_gray; "\e[47m#{self}\e[0m" end
|
|
18
|
-
def bold; "\e[1m#{self}\e[22m" end
|
|
19
|
-
def italic; "\e[3m#{self}\e[23m" end
|
|
20
|
-
def underline; "\e[4m#{self}\e[24m" end
|
|
21
|
-
def blink; "\e[5m#{self}\e[25m" end
|
|
22
|
-
def reverse_color; "\e[7m#{self}\e[27m" end
|
|
23
|
-
end
|
data/ruby-wordle.gemspec
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
# -*- encoding: utf-8 -*-
|
|
2
|
-
$:.push File.expand_path("../lib", __FILE__)
|
|
3
|
-
|
|
4
|
-
require "ruby-wordle/version"
|
|
5
|
-
|
|
6
|
-
Gem::Specification.new do |s|
|
|
7
|
-
s.name = "ruby-wordle"
|
|
8
|
-
s.version = RubyWordle::VERSION
|
|
9
|
-
s.authors = ["Yoichiro Hasebe"]
|
|
10
|
-
s.email = ["yohasebe@gmail.com"]
|
|
11
|
-
s.homepage = "http://github.com/yohasebe/ruby-wordle"
|
|
12
|
-
s.summary = %q{A set of ruby scripts to generate word-lists, solve Wordle, and play Wordle}
|
|
13
|
-
s.description = %q{A set of ruby scripts to generate word-lists, solve Wordle, and play Wordle}
|
|
14
|
-
s.licenses = ["MIT"]
|
|
15
|
-
s.files = `git ls-files`.split("\n")
|
|
16
|
-
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
|
17
|
-
s.require_paths = ["lib"]
|
|
18
|
-
|
|
19
|
-
# specify any dependencies here; for example:
|
|
20
|
-
s.add_runtime_dependency 'readline'
|
|
21
|
-
end
|