Compare commits

...

4 Commits

4 changed files with 10917 additions and 24 deletions

10657
src/dict.txt Normal file

File diff suppressed because it is too large Load Diff

101
src/dict.zig Normal file
View File

@@ -0,0 +1,101 @@
const std = @import("std");
const Wordle = @import("wordle.zig");
/// A statically-initialized dictionary of all Wordle words.
var Dictionary: ?[]const Wordle.Word = null;
/// Gets or statically initializes the dictionary.
pub fn getDictionary() []const Wordle.Word {
if (Dictionary) |dict| {
return dict;
} else {
const dict = loadDict();
Dictionary = dict;
return dict;
}
}
/// Internal function to load the Wordle dictionary.
fn loadDict() []const Wordle.Word {
// directly embed dictionary contents
const src = @embedFile("dict.txt");
// count lines (words)
var num: u64 = 0;
for (src, 0..) |char, idx| {
_ = idx;
if (char == '\n') {
num += 1;
}
}
// allocate all words
const dictionary = std.heap.smp_allocator.alloc(Wordle.Word, num) catch unreachable;
// iterate over all lines
var lines = std.mem.splitSequence(u8, src, "\n");
var idx: usize = 0;
while (lines.next()) |line| {
// skip empty line at end
if (line.len == 0) {
continue;
}
// assert line is correct length
if (line.len != Wordle.WordLen) {
std.debug.panic("length of '{s}' mismatches {d} characters", .{ line, Wordle.WordLen });
}
// copy line bytes over
@memcpy(&dictionary[idx], line);
// increment dictionary entry
idx += 1;
}
// return complete dictionary
return dictionary;
}
/// Tests if a word is in the dictionary.
// TODO: binary search? need to assert that words are sorted
pub fn isInDictionary(word: Wordle.Word) bool {
// exhaustively search through dictionary
for (getDictionary()) |entry| {
// compare each letter at a time
var match = true;
for (entry, word) |entryLetter, wordLetter| {
if (entryLetter != wordLetter) {
match = false;
break;
}
}
// if every letter matches, return a complete match
if (match) {
return true;
}
}
// if no word was found, return false
return false;
}
/// Asserts that a word literal is in the dictionary.
fn expectDictionary(word: Wordle.WordLiteral) !void {
try std.testing.expect(isInDictionary(Wordle.strToWord(word)));
}
/// Asserts that a word literal is *not* in the dictionary.
fn expectNotDictionary(word: Wordle.WordLiteral) !void {
try std.testing.expect(!isInDictionary(Wordle.strToWord(word)));
}
test "in dictionary" {
try expectDictionary("abash");
}
test "not in dictionary" {
try expectNotDictionary("hbzci");
}

132
src/vote.zig Normal file
View File

@@ -0,0 +1,132 @@
const std = @import("std");
const Wordle = @import("wordle.zig");
const Dict = @import("dict.zig");
/// A "pattern": a word where nulls signify wildcards.
pub const Pattern = Wordle.Word;
/// A list of candidates: indexes of words in the dictionary.
pub const Candidates = std.ArrayList(u16);
/// A single vote.
pub const Vote = struct {
/// The pattern to match this vote to.
pattern: Pattern,
/// The number of votes placed on this pattern.
score: usize,
};
/// A collection of votes.
pub const Election = []const Vote;
/// Calculates the total number of votes in an election.
pub fn electionVoteTotal(election: Election) usize {
var total: usize = 0;
for (election) |vote| total += vote.score;
return total;
}
/// Tests if a pattern is satisfied by a given word.
pub fn patternSatisfied(word: Wordle.Word, pat: Pattern) bool {
// find mismatches
for (word, pat) |wordLetter, patLetter| {
if (patLetter != 0 and patLetter != wordLetter) {
return false;
}
}
// if no mismatches were found, the pattern is satisfied
return true;
}
/// Naively calculates how many votes a word scores in an election.
pub fn wordScore(word: Wordle.Word, election: Election) usize {
var total: usize = 0;
for (election) |vote| {
if (patternSatisfied(word, vote.pattern)) {
total += vote.score;
}
}
return total;
}
/// Elects candidates using brute-force scoring on the whole dictionary.
///
/// The result must be deinitialized after use.
pub fn naiveElect(alloc: std.mem.Allocator, election: Election) !Candidates {
// track the maximum known score
var maxScore: usize = 0;
// keep a list of all highest-scoring candidates
var candidates: Candidates = .empty;
// evaluate all words in the dictionary
for (Dict.getDictionary(), 0..) |word, idx| {
// score this word
const score = wordScore(word, election);
// if score is lower than current known score, skip
if (score < maxScore) continue;
// if score exceeds known score, clear candidates and accommodate
if (score > maxScore) {
maxScore = score;
try candidates.resize(alloc, 0);
}
// add this candidate's index
try candidates.append(alloc, @intCast(idx));
}
// return the complete list of candidates
return candidates;
}
/// Formats a list of the words referred to by candidate indices.
fn formatCandidates(alloc: std.mem.Allocator, candidates: Candidates) !std.ArrayList(u8) {
// cache dictionary for repeated lookup
const dict = Dict.getDictionary();
// dynamically allocate formatted words
var formatted: std.ArrayList(u8) = .empty;
for (candidates.items, 0..) |wordIdx, idx| {
// prefix word with space unless it's the first one
if (idx != 0) {
try formatted.append(alloc, ' ');
}
// append the word in the dictionary
try formatted.appendSlice(alloc, &dict[wordIdx]);
}
return formatted;
}
test "simple naive score" {
const election = [_]Vote{.{
.pattern = Wordle.strToWord("abas\x00"),
.score = 1,
}};
try std.testing.expectEqual(wordScore(Wordle.strToWord("abash"), &election), 1);
try std.testing.expectEqual(wordScore(Wordle.strToWord("abask"), &election), 1);
try std.testing.expectEqual(wordScore(Wordle.strToWord("abear"), &election), 0);
}
test "simple naive election" {
const election = [_]Vote{.{
.pattern = Wordle.strToWord("abas\x00"),
.score = 1,
}};
const alloc = std.heap.smp_allocator;
var candidates = try naiveElect(alloc, &election);
defer candidates.deinit(alloc);
var candidatesFmt = try formatCandidates(alloc, candidates);
defer candidatesFmt.deinit(alloc);
try std.testing.expectEqualStrings(candidatesFmt.items, "abash abask");
}

View File

@@ -1,3 +1,5 @@
const std = @import("std");
/// The length of Wordle words.
pub const WordLen = 5;
@@ -19,8 +21,11 @@ pub const Clues = [WordLen]Clue;
/// Guesses a word given a solution and returns the clues.
pub fn checkGuess(guess: Word, solution: Word) Clues {
// init clues
var clues: Clues = undefined;
// init clues, defaulting to gray clues
var clues: Clues = @splat(Clue.Gray);
// track which letters have been used for yellow clues
var duplicates: [WordLen]bool = @splat(false);
// compare each letter pairwise
for (guess, 0..) |guessLetter, idx| {
@@ -30,13 +35,11 @@ pub fn checkGuess(guess: Word, solution: Word) Clues {
continue;
}
// default to a gray clue
clues[idx] = Clue.Gray;
// if a matching letter in the solution can be found, write yellow
for (solution) |solutionLetter| {
if (guessLetter == solutionLetter) {
for (solution, &duplicates) |solutionLetter, *duplicate| {
if (guessLetter == solutionLetter and !duplicate.*) {
clues[idx] = Clue.Yellow;
duplicate.* = true;
break;
}
}
@@ -54,30 +57,26 @@ pub fn strToWord(str: WordLiteral) Word {
}
/// Asserts that two word literals produce the expected clues.
fn expectGuessClues(guess: WordLiteral, solution: WordLiteral, expectClues: WordLiteral) error{Mismatch}!void {
fn expectGuessClues(guess: WordLiteral, solution: WordLiteral, expectClues: WordLiteral) !void {
// convert literals to the non-null word type
const guessWord = strToWord(guess);
const solutionWord = strToWord(solution);
const expectCluesWord = strToWord(expectClues);
// check the guess and produce clues
const clues = checkGuess(guessWord, solutionWord);
// confirm that the expected clues match the clues
for (clues, expectCluesWord) |clue, expectedChar| {
// convert character to associated clue variant
const expected = switch (expectedChar) {
' ' => Clue.Gray,
'Y' => Clue.Yellow,
'G' => Clue.Green,
else => unreachable,
// convert the clues to a word
var cluesWord: Word = undefined;
for (clues, &cluesWord) |src, *dst| {
dst.* = switch (src) {
Clue.Gray => ' ',
Clue.Yellow => 'Y',
Clue.Green => 'G',
};
// if clues don't match, return an error
if (expected != clue) {
return error.Mismatch;
}
}
// test the equality of the strings
try std.testing.expectEqualStrings(&cluesWord, expectClues);
}
test "full match" {
@@ -92,6 +91,10 @@ test "no match" {
try expectGuessClues("salet", "fuzzy", " ");
}
test "duplicate letters" {
try expectGuessClues("fuzzy", "zesty", " Y ");
test "anagram" {
try expectGuessClues("beats", "abets", "YYYGG");
}
test "duplicate letters" {
try expectGuessClues("fuzzy", "zesty", " Y G");
}